Skip to main content

agent_profile/adapter/
copilot.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//! GitHub Copilot CLI: `COPILOT_HOME` per profile (SP2 design §8, probe outcome 5).
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 = "COPILOT_HOME";
20
21static METADATA: AdapterMetadata = AdapterMetadata {
22    id: "copilot",
23    executable: "copilot",
24    mechanism: Mechanism::Env(VAR),
25    support: SupportLevel::Experimental,
26    evidence: AdapterEvidence {
27        mechanism_id: "copilot-home-v1",
28        verified_at: "2026-11-09",
29        upstream_version: "1.0.108",
30        source_url: "https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference",
31        notes: "the token is in the artefacts (`documented COPILOT_HOME: present`; help documents \
32                the `~/.copilot` default via `--log-dir`) but the behaviour delta was empty: \
33                `(no change at either watched location)` at both the target and the default \
34                `~/.copilot (absent)` (§9 outcome 5), so the states below are undetermined; \
35                the runtime cache the first use extracts at `.cache/copilot` is version-keyed, \
36                not configuration or credentials (same call as opencode's `.cache` directory); \
37                `COPILOT_GITHUB_TOKEN`, `GH_TOKEN` and `GITHUB_TOKEN` are the documented \
38                credential bypass variables (strings scan: `documented GH_TOKEN: present`, \
39                `documented GITHUB_TOKEN: present`, `documented COPILOT_GITHUB_TOKEN: ABSENT`)",
40    },
41    capabilities: &[
42        CapabilityClaim {
43            capability: Capability::ConfigIsolation,
44            state: CapabilityState::Unknown,
45            basis: "unmeasured: `delta-env-COPILOT_HOME: (no change: nothing was created, moved \
46                    or removed at the watched locations)` while the default `~/.copilot` was \
47                    `(absent)` (§9 outcome 5, Unknown branch)",
48        },
49        CapabilityClaim {
50            capability: Capability::CredentialIsolation,
51            state: CapabilityState::Unknown,
52            basis: "unmeasured: credential storage not located (§9 outcome 5); \
53                    `COPILOT_GITHUB_TOKEN`, `GH_TOKEN` and `GITHUB_TOKEN` are the documented \
54                    bypass variables (copilot.sh precedence), and no stored-credential \
55                    observation was made",
56        },
57        CapabilityClaim {
58            capability: Capability::StateIsolation,
59            state: CapabilityState::Unknown,
60            basis: "unmeasured: the same delta — `(no change: nothing was created, moved or \
61                    removed at the watched locations)` at both the target and the default \
62                    `~/.copilot (absent)` (§9 outcome 5)",
63        },
64    ],
65    env: &[EnvOverride { name: VAR, sensitive: false }],
66    conflicts: &[],
67    credential_env: &["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"],
68};
69
70/// The GitHub Copilot CLI adapter.
71#[derive(Debug, Clone, Copy)]
72pub struct Copilot;
73
74impl Adapter for Copilot {
75    fn metadata(&self) -> &'static AdapterMetadata {
76        &METADATA
77    }
78
79    fn paths(&self, root: &AppRoot, profile: &ProfileName) -> Vec<(PathBuf, PathKind)> {
80        // The measured delta was empty (§9 outcome 5), so nothing was observed following
81        // `COPILOT_HOME` — whether the agent nests anything under the directory is undetermined,
82        // and the adapter therefore declares only the profile directory itself.
83        vec![(profile_dir(root, profile, METADATA.id), PathKind::Dir)]
84    }
85
86    fn plan(&self, ctx: &PlanContext<'_>) -> Result<PlannedLaunch> {
87        METADATA.mechanism.plan(self, ctx)
88    }
89}