Skip to main content

agent_profile/adapter/
codex.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//! Codex CLI: `CODEX_HOME` per profile (SP2 design ยง5.2).
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 = "CODEX_HOME";
20
21/// Shown when the profile's home did not exist at plan time.
22pub const NEW_PROFILE_NOTE: &str =
23    "new profile starts logged out; run codex login with this profile";
24
25static METADATA: AdapterMetadata = AdapterMetadata {
26    id: "codex",
27    executable: "codex",
28    mechanism: Mechanism::Env(VAR),
29    support: SupportLevel::Proven,
30    evidence: AdapterEvidence {
31        mechanism_id: "codex-home-v1",
32        verified_at: "2026-11-09",
33        upstream_version: "0.167.0",
34        source_url: "measured",
35        notes: "codex --help: -p/--profile layers $CODEX_HOME/<name>.config.toml; binary strings OPENAI_API_KEY, \
36                CODEX_API_KEY, CODEX_ACCESS_TOKEN, CODEX_SQLITE_HOME; CODEX_HOME semantics per the Codex CLI \
37                configuration documentation",
38    },
39    capabilities: &[
40        CapabilityClaim {
41            capability: Capability::ConfigIsolation,
42            state: CapabilityState::Supported,
43            basis: "cited: config.toml and <name>.config.toml live in CODEX_HOME; project-level \
44                    configuration layering is not measured",
45        },
46        CapabilityClaim {
47            capability: Capability::CredentialIsolation,
48            state: CapabilityState::Conditional,
49            basis: "measured: OPENAI_API_KEY, CODEX_API_KEY and CODEX_ACCESS_TOKEN bypass it; that \
50                    auth.json and the keyring key follow CODEX_HOME is cited, not measured",
51        },
52        CapabilityClaim {
53            capability: Capability::StateIsolation,
54            state: CapabilityState::Conditional,
55            basis: "cited: CODEX_SQLITE_HOME relocates the state database; the variable is present in \
56                    the binary but the probe sets only CODEX_HOME, so the relocation is not measured",
57        },
58    ],
59    env: &[EnvOverride { name: VAR, sensitive: false }],
60    conflicts: &[],
61    credential_env: &["OPENAI_API_KEY", "CODEX_API_KEY", "CODEX_ACCESS_TOKEN"],
62};
63
64/// The Codex CLI adapter.
65#[derive(Debug, Clone, Copy)]
66pub struct Codex;
67
68impl Adapter for Codex {
69    fn metadata(&self) -> &'static AdapterMetadata {
70        &METADATA
71    }
72
73    fn paths(&self, root: &AppRoot, profile: &ProfileName) -> Vec<(PathBuf, PathKind)> {
74        vec![(profile_dir(root, profile, METADATA.id), PathKind::Dir)]
75    }
76
77    fn plan(&self, ctx: &PlanContext<'_>) -> Result<PlannedLaunch> {
78        let mut planned = METADATA.mechanism.plan(self, ctx)?;
79        if !planned.paths[0].existed {
80            planned.notes.push(NEW_PROFILE_NOTE.to_owned());
81        }
82        Ok(planned)
83    }
84}