7.2 KiB
TECH.md — Preseed Gemini CLI config to skip onboarding
1. Context
When a cloud agent run uses the Gemini harness, we launch gemini --yolo -i "$(cat <prompt>)" in a non-interactive terminal. On a fresh machine Gemini CLI prompts twice before accepting input:
- An auth-type picker (
Login with Google,Gemini API key,Vertex AI, …), driven bysecurity.auth.selectedTypein~/.gemini/settings.json. - A folder-trust dialog for the current working dir, driven by
~/.gemini/trustedFolders.json.
Both prompts block the run since no one is at the TUI. We need to preseed both files so Gemini starts straight into the prompt, while leaving other user-owned state in those files alone.
The harness layer already has the hook we need: ThirdPartyHarness::prepare_environment_config runs in AgentDriver::prepare_harness (app/src/ai/agent_sdk/driver.rs:1476) right before build_runner, and errors are wrapped into AgentDriverError::HarnessConfigSetupFailed { harness, error } (driver.rs:367). The Claude harness already implements this hook (app/src/ai/agent_sdk/driver/harness/claude_code.rs:45) using two private helpers, read_json_file_or_default and write_json_file, to patch Claude's own JSON config. Gemini's impl was a no-op default (harness/mod.rs:55-61, harness/gemini.rs pre-change).
Relevant files:
app/src/ai/agent_sdk/driver/harness/mod.rs:55-61— defaultprepare_environment_configno-op.app/src/ai/agent_sdk/driver/harness/gemini.rs— Gemini harness; gets the new impl.app/src/ai/agent_sdk/driver/harness/claude_code.rs (45-56, 688-723 pre-change)— existing Claude impl + JSON helpers.app/src/ai/agent_sdk/driver.rs:1471-1476— whereprepare_environment_configis invoked.
2. Proposed changes
2a. Extract shared JSON helpers
Move read_json_file_or_default and write_json_file out of claude_code.rs into a new app/src/ai/agent_sdk/driver/harness/json_utils.rs, declared from harness/mod.rs. Claude's impl already uses the exact same read → parse-or-default → pretty-write idiom Gemini needs; a second per-harness copy would drift. Visibility stays pub(super) so the helpers are shared by sibling harness modules but not public API.
2b. Implement GeminiHarness::prepare_environment_config
Take working_dir (was _working_dir), resolve ~/.gemini via dirs::home_dir(), and call two focused helpers:
prepare_gemini_settings(path)— readsettings.jsoninto a typedGeminiSettings { security: Option<GeminiSecurity { auth: Option<GeminiAuth { selected_type, .. }> }>, .. }, setsecurity.auth.selectedType = "gemini-api-key", write back pretty JSON. Each level uses#[serde(flatten)] extra: Map<String, Value>so unrelated keys at every nesting level round-trip verbatim.prepare_gemini_trusted_folders(path, working_dir)— readtrustedFolders.jsonintoHashMap<String, String>, insertworking_dir → "TRUST_FOLDER", write back. The flat map shape matches Gemini's on-disk schema exactly, so no wrapper struct is needed.
Constants (GEMINI_API_KEY_AUTH_TYPE = "gemini-api-key", GEMINI_TRUST_LEVEL_FOLDER = "TRUST_FOLDER", file/dir names) live at the bottom of gemini.rs and link to the upstream Gemini source (packages/core/src/core/contentGenerator.ts, packages/cli/src/config/trustedFolders.ts) in doc comments so we know what to update if Gemini changes their discriminants.
Errors bubble up through anyhow::Result and are converted at the ThirdPartyHarness boundary into HarnessConfigSetupFailed { harness: "gemini", error }, matching Claude.
2c. Design notes
- Typed structs vs
serde_json::Valueforsettings.json: typed wins because we only touch one field and want compile-time safety on the path; theflattenextras map gives us lossless round-trip of everything else without hand-written merge logic. - Why not
HashMap<String, Value>for settings too: Gemini's config is nested; a flat map would require either walking by string key at each level or silently clobbering siblings. The typed path makes the invariant "onlysecurity.auth.selectedTypeis ours" explicit. - Surfacing malformed JSON as an error (rather than overwriting) is deliberate: the file is user-owned and may contain hand edits; silently clobbering it on a parse error would be a footgun. Tests lock this in.
3. Testing and validation
All tests live in app/src/ai/agent_sdk/driver/harness/gemini_tests.rs. Each invariant below maps to one or more tests against a TempDir-backed path.
- Fresh install produces the expected settings —
prepare_gemini_settings_creates_file_with_api_key_auth: from a missing file, the helper writessecurity.auth.selectedType = "gemini-api-key". - User edits survive —
prepare_gemini_settings_preserves_unrelated_keys: top-level (ui.theme), sibling-branch (security.folderTrust.enabled), and sibling-of-target (security.auth.enforcedType) keys all round-trip unchanged whileselectedTypeis added. - Malformed config is surfaced, not clobbered —
prepare_gemini_settings_surfaces_malformed_json_as_error: a wrong-typedsecuritycausesErr, leaving the file on disk untouched. - Idempotent —
prepare_gemini_settings_is_idempotent: two back-to-back calls produce byte-identical output. - Trusted folder is written for the working dir —
prepare_gemini_trusted_folders_creates_file_with_working_dir: the new entry mapsworking_dir → "TRUST_FOLDER". - Other trust entries survive —
prepare_gemini_trusted_folders_preserves_existing_entries:TRUST_PARENT/DO_NOT_TRUSTentries for unrelated paths are preserved alongside the new entry. - Re-trusting overrides prior level —
prepare_gemini_trusted_folders_overwrites_prior_level_for_same_path: a previousDO_NOT_TRUSTon the same path becomesTRUST_FOLDER.
Unit tests cover the helpers directly; prepare_environment_config itself is a thin wrapper and doesn't need its own test.
Manual validation (one-off, not automated): run a cloud agent with the Gemini harness on a fresh environment and confirm the TUI boots directly into the prompt with no auth / trust prompts. Presubmit (./script/presubmit) covers fmt + clippy + the new tests.
4. Risks and mitigations
- Gemini changes the discriminant strings (
"gemini-api-key","TRUST_FOLDER"). Mitigation: constants are documented with links to the upstream files so a Gemini CLI upgrade that changes them is a grep away; a broken run would surface as the prompt returning, which is noisy and catchable during bring-up. trustedFolders.jsonuses a flatHashMap<String, String>, so a future schema change to an object per entry would silently fail parse. We'd seeHarnessConfigSetupFailedimmediately — same error path as malformed user JSON — which is the right signal.~/.geminiis shared with a user's local Gemini CLI. The selected-type patch is harmless (users can change it back in-UI) and the trusted-folder write is strictly additive for the current working dir; no existing keys are removed.
5. Follow-ups
- REMOTE-1407 — pipe the agent system prompt into Gemini (TODO in
GeminiHarnessRunner::new). - REMOTE-1408 — upload the Gemini conversation transcript in
save_conversation, alongside the block snapshot we already upload.