From f54e2a46ae271e034a4b81f2786c87a86533d1cb Mon Sep 17 00:00:00 2001 From: javis-bot Date: Thu, 11 Jun 2026 00:58:46 +0900 Subject: [PATCH] fix(brain): drop --approval-mode yolo from Gemini CLI search yolo auto-approves every tool call, so a real-time search query could in principle trigger write/shell tools. Default approval mode still auto-runs the CLI's read-only web search in headless mode but never silently approves destructive tools. Verified end-to-end: a grounded query returns a current answer in ~23s with the account OAuth login. Test asserts yolo is absent; specs and docs/llm_contexts.md updated. Co-Authored-By: Claude Opus 4.7 --- docs/llm_contexts.md | 2 +- docs/stream_browser_modes.md | 5 +++-- src/jarvis/tools/builtin/realtime_search.py | 6 +++++- src/jarvis/tools/builtin/web_search.spec.md | 2 +- tests/test_realtime_gemini_cli.py | 4 ++++ 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/llm_contexts.md b/docs/llm_contexts.md index 745877b..e55f803 100644 --- a/docs/llm_contexts.md +++ b/docs/llm_contexts.md @@ -172,7 +172,7 @@ Every distinct LLM call in Jarvis, what feeds it, what consumes it, and how it i - **Weather** ([src/jarvis/tools/builtin/weather.py](src/jarvis/tools/builtin/weather.py), ~line 60) — `ollama_chat_model`, parses location/time/unit from the query. - **Nutrition log_meal** ([src/jarvis/tools/builtin/nutrition/log_meal.py](src/jarvis/tools/builtin/nutrition/log_meal.py), lines 48 & 136) — `ollama_chat_model`, extracts nutrients, confirms logging. - **Gemini real-time search** ([src/jarvis/tools/builtin/realtime_search.py](src/jarvis/tools/builtin/realtime_search.py)) — **external Gemini model**, NOT Ollama. Only on the `webSearch` route when `STREAM_BROWSER=false`; the sub-mode is `cfg.gemini_auth` (env `GEMINI_AUTH`, default `oauth`): - - `oauth` (default) `gemini_cli_search()` — shells out to the Gemini CLI (`gemini -p -o json --skip-trust --approval-mode yolo`) authenticated by the user's Google-account login (`GEMINI_API_KEY`/`GOOGLE_API_KEY` stripped from the child env, `GOOGLE_GENAI_USE_GCA=true` set to select OAuth); model is whatever the CLI/account defaults to. Uses the CLI's built-in web-search grounding. Bounded by a 30s subprocess timeout. + - `oauth` (default) `gemini_cli_search()` — shells out to the Gemini CLI (`gemini -p -o json --skip-trust`, default approval mode) authenticated by the user's Google-account login (`GEMINI_API_KEY`/`GOOGLE_API_KEY` stripped from the child env, `GOOGLE_GENAI_USE_GCA=true` set to select OAuth); model is whatever the CLI/account defaults to. Uses the CLI's built-in web-search grounding. Bounded by a 30s subprocess timeout. - `apikey` `gemini_search()` — one REST `generateContent` call (`gemini_model`, default `gemini-2.0-flash`) with the `google_search` grounding tool; keyed by `GEMINI_API_KEY`. Both return the fenced UNTRUSTED-WEB-EXTRACT envelope consumed by the main loop (#1). Fail-open: CLI missing / login expired / quota 429 / timeout / errors / missing key all fall through to the DDG cascade. The `STREAM_BROWSER=true` route (`browser_search()`) makes NO LLM call — it drives Chrome and scrapes Google results. diff --git a/docs/stream_browser_modes.md b/docs/stream_browser_modes.md index 8cac09a..82fc4fd 100644 --- a/docs/stream_browser_modes.md +++ b/docs/stream_browser_modes.md @@ -33,8 +33,9 @@ info is fetched by a tool the reply engine calls. `STREAM_BROWSER` selects HOW: `UNTRUSTED WEB EXTRACT` envelope. Keeps the 39k-line Python brain dep-free. - Gemini has two auth sub-modes (`GEMINI_AUTH`): - `oauth` (default): shell out to the Gemini CLI (`gemini -p -o json - --skip-trust --approval-mode yolo`) authenticated by the user's Google - account login. `GEMINI_API_KEY`/`GOOGLE_API_KEY` are stripped from the child + --skip-trust`, default approval mode — read-only tools like web search run + headless, but write/shell tools are never auto-approved) authenticated by + the user's Google account login. `GEMINI_API_KEY`/`GOOGLE_API_KEY` are stripped from the child env, and `GOOGLE_GENAI_USE_GCA=true` is set, so the CLI uses the account login (not API-key auth) and fails fast when no login exists rather than erroring on "no auth method". The CLI is resolved from `PATH` or diff --git a/src/jarvis/tools/builtin/realtime_search.py b/src/jarvis/tools/builtin/realtime_search.py index 4e67341..1959bdc 100644 --- a/src/jarvis/tools/builtin/realtime_search.py +++ b/src/jarvis/tools/builtin/realtime_search.py @@ -131,7 +131,11 @@ def gemini_cli_search(query: str, timeout: int = 30) -> Optional[str]: env["GOOGLE_GENAI_USE_GCA"] = "true" try: proc = subprocess.run( - [binary, "-p", query, "-o", "json", "--skip-trust", "--approval-mode", "yolo"], + # Default approval mode (no --yolo): the CLI auto-runs read-only + # tools like its web search in headless mode but will not silently + # approve write/shell tools, so a search query can't trigger + # destructive actions. + [binary, "-p", query, "-o", "json", "--skip-trust"], capture_output=True, text=True, timeout=timeout, diff --git a/src/jarvis/tools/builtin/web_search.spec.md b/src/jarvis/tools/builtin/web_search.spec.md index 92efce1..8957869 100644 --- a/src/jarvis/tools/builtin/web_search.spec.md +++ b/src/jarvis/tools/builtin/web_search.spec.md @@ -17,7 +17,7 @@ Before the DuckDuckGo cascade, `run()` routes by the env flag `STREAM_BROWSER` - **false**: Gemini answers, with the sub-mode chosen by `cfg.gemini_auth` (env `GEMINI_AUTH`, default `oauth`): - `oauth` (default): `gemini_cli_search()` shells out to the Gemini CLI - (`gemini -p -o json --skip-trust --approval-mode yolo`) using the + (`gemini -p -o json --skip-trust`, default approval mode) using the user's Google-account login and the CLI's built-in web-search grounding. `GEMINI_API_KEY` / `GOOGLE_API_KEY` are stripped from the child env so the CLI uses the account login, not API-key auth. Requires a one-time diff --git a/tests/test_realtime_gemini_cli.py b/tests/test_realtime_gemini_cli.py index 6f08324..50d27ab 100644 --- a/tests/test_realtime_gemini_cli.py +++ b/tests/test_realtime_gemini_cli.py @@ -89,3 +89,7 @@ def test_api_key_stripped_from_child_env(monkeypatch): assert captured["env"].get("GOOGLE_GENAI_USE_GCA") == "true" # invoked headless with JSON output assert "-p" in captured["cmd"] and "-o" in captured["cmd"] and "json" in captured["cmd"] + # never auto-approve all tools: a search query must not be able to trigger + # write/shell tool execution. + assert "yolo" not in captured["cmd"] + assert "--yolo" not in captured["cmd"]