agent_profile/adapter/
opencode.rs1use 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 = "OPENCODE_CONFIG_DIR";
20
21static METADATA: AdapterMetadata = AdapterMetadata {
22 id: "opencode",
23 executable: "opencode",
24 mechanism: Mechanism::Env(VAR),
25 support: SupportLevel::Proven,
26 evidence: AdapterEvidence {
27 mechanism_id: "opencode-config-dir-v1",
28 verified_at: "2026-11-09",
29 upstream_version: "v2.2.6",
30 source_url: "https://opencode.ai/docs/cli/",
31 notes: "the config-side service.json followed OPENCODE_CONFIG_DIR into the target while \
32 session/state data (opencode.db with -shm/-wal, shell/, opencode.log) stayed at \
33 the default .local/share/opencode (§9 outcome 1, both repetitions); \
34 OPENCODE_CONFIG names a config file to READ and relocates none of the agent's \
35 writes, so the mechanism is the directory variable alone; the probe's launches run \
36 unauthenticated, so auth.json is never written and no credential file appears",
37 },
38 capabilities: &[
39 CapabilityClaim {
40 capability: Capability::ConfigIsolation,
41 state: CapabilityState::Supported,
42 basis: "measured: `+ f /home/agent/probe-target/service.json` — the config-side write \
43 followed OPENCODE_CONFIG_DIR into the target, and the second delta \
44 (`envfile:OPENCODE_CONFIG`) shows the same write at the default \
45 `.config/opencode/service.json` when only the file variable is set",
46 },
47 CapabilityClaim {
48 capability: Capability::CredentialIsolation,
49 state: CapabilityState::Unknown,
50 basis: "unmeasured: `auth.json` was never observed anywhere in the deltas — \
51 unauthenticated launches never write it — so where stored credentials live was \
52 not established and §6's 'probe establishes where credentials are stored' \
53 exception is not met; `ANTHROPIC_API_KEY` and `OPENAI_API_KEY` (strings \
54 measured) are the documented bypass variables, and no stored-credential \
55 observation was made",
56 },
57 CapabilityClaim {
58 capability: Capability::StateIsolation,
59 state: CapabilityState::NotSupported,
60 basis: "measured: `+ f /home/agent/.local/share/opencode/opencode.db` (with `-shm`/\
61 `-wal`), `+ d .../shell/...` and `opencode.log` stayed at \
62 `.local/share/opencode` under both mechanisms: neither names a data directory \
63 and the strings scan contains no `OPENCODE_DATA*`/`OPENCODE_HOME` token (Q2)",
64 },
65 ],
66 env: &[EnvOverride { name: VAR, sensitive: false }],
67 conflicts: &[],
68 credential_env: &["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
69};
70
71#[derive(Debug, Clone, Copy)]
73pub struct Opencode;
74
75impl Adapter for Opencode {
76 fn metadata(&self) -> &'static AdapterMetadata {
77 &METADATA
78 }
79
80 fn paths(&self, root: &AppRoot, profile: &ProfileName) -> Vec<(PathBuf, PathKind)> {
81 vec![(profile_dir(root, profile, METADATA.id), PathKind::Dir)]
86 }
87
88 fn plan(&self, ctx: &PlanContext<'_>) -> Result<PlannedLaunch> {
89 METADATA.mechanism.plan(self, ctx)
90 }
91}