Skip to main content

agent_profile/adapter/
gemini.rs

1// Copyright (c) 2026 Costas Kirgoussios
2//
3// This software is licensed under the PolyForm Noncommercial License 1.0.0.
4// You may obtain a copy of the License at:
5// https://polyformproject.org/licenses/noncommercial/1.0.0/
6
7//! Gemini CLI: `GEMINI_CLI_HOME` per profile (SP2 design §8.1).
8
9use std::path::PathBuf;
10
11use super::{
12    Adapter, AdapterEvidence, AdapterMetadata, Capability, CapabilityClaim, CapabilityState,
13    EnvOverride, Mechanism, PathKind, PlanContext, PlannedLaunch, SupportLevel, profile_dir,
14};
15use crate::config::AppRoot;
16use crate::error::Result;
17use crate::name::ProfileName;
18
19const VAR: &str = "GEMINI_CLI_HOME";
20
21static METADATA: AdapterMetadata = AdapterMetadata {
22    id: "gemini",
23    executable: "gemini",
24    mechanism: Mechanism::Env(VAR),
25    support: SupportLevel::Proven,
26    evidence: AdapterEvidence {
27        mechanism_id: "gemini-cli-home-v1",
28        verified_at: "2026-11-09",
29        upstream_version: "0.67.0",
30        source_url: "https://geminicli.com/docs/cli/enterprise/",
31        notes: "the sandbox delta moved config (projects.json) and history/tmp state into \
32                <target>/.gemini while the default location gained nothing; \
33                GEMINI_API_KEY, GOOGLE_API_KEY and GOOGLE_APPLICATION_CREDENTIALS are the \
34                credential bypass variables (binary strings measured)",
35    },
36    capabilities: &[
37        CapabilityClaim {
38            capability: Capability::ConfigIsolation,
39            state: CapabilityState::Supported,
40            basis: "measured: `+ d /home/agent/probe-target/.gemini` and \
41                    `+ f /home/agent/probe-target/.gemini/projects.json` — the variable moved the \
42                    configuration inside the target, nested one level as the agent creates `.gemini` \
43                    itself",
44        },
45        CapabilityClaim {
46            capability: Capability::CredentialIsolation,
47            state: CapabilityState::Unknown,
48            basis: "unmeasured: separating stored credentials requires a vendor session; \
49                    GEMINI_API_KEY, GOOGLE_API_KEY and GOOGLE_APPLICATION_CREDENTIALS \
50                    (strings measured) are the documented bypass variables, and no \
51                    stored-credential observation was made",
52        },
53        CapabilityClaim {
54            capability: Capability::StateIsolation,
55            state: CapabilityState::Supported,
56            basis: "measured: history and tmp state moved as \
57                    `+ d /home/agent/probe-target/.gemini/history` and \
58                    `+ d /home/agent/probe-target/.gemini/tmp`",
59        },
60    ],
61    env: &[EnvOverride { name: VAR, sensitive: false }],
62    conflicts: &[],
63    credential_env: &["GEMINI_API_KEY", "GOOGLE_API_KEY", "GOOGLE_APPLICATION_CREDENTIALS"],
64};
65
66/// The Gemini CLI adapter.
67#[derive(Debug, Clone, Copy)]
68pub struct Gemini;
69
70impl Adapter for Gemini {
71    fn metadata(&self) -> &'static AdapterMetadata {
72        &METADATA
73    }
74
75    fn paths(&self, root: &AppRoot, profile: &ProfileName) -> Vec<(PathBuf, PathKind)> {
76        // `GEMINI_CLI_HOME` names a directory CONTAINING `.gemini` (probe and transcript): the agent
77        // creates the nested `.gemini` itself, so the adapter declares only the profile directory.
78        vec![(profile_dir(root, profile, METADATA.id), PathKind::Dir)]
79    }
80
81    fn plan(&self, ctx: &PlanContext<'_>) -> Result<PlannedLaunch> {
82        METADATA.mechanism.plan(self, ctx)
83    }
84}