Skip to main content

agent_profile/adapter/
cursor.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//! Cursor Agent CLI: `CURSOR_CONFIG_DIR` per profile (SP4 design §8, table row; probe outcome 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 = "CURSOR_CONFIG_DIR";
20
21static METADATA: AdapterMetadata = AdapterMetadata {
22    id: "cursor",
23    executable: "cursor-agent",
24    mechanism: Mechanism::Env(VAR),
25    support: SupportLevel::Experimental,
26    evidence: AdapterEvidence {
27        mechanism_id: "cursor-config-dir-v1",
28        verified_at: "2026-11-09",
29        upstream_version: "2026.11.05-86047df",
30        source_url: "https://cursor.com/docs/cli/reference/configuration",
31        notes: "the agent's one pre-network write moved under CURSOR_CONFIG_DIR \
32                (`+ f /home/agent/probe-target/cli-config.json`) while the default \
33                `~/.cursor/cli-config.json` and the watched XDG fallback `.config/cursor` \
34                recorded nothing (§9 outcome 1); the probe installed by the vendor script, \
35                which refuses a version argument (§12), so upstream_version is the \
36                `version-extracted` value; `CURSOR_API_KEY` is the documented bypass variable \
37                (help: `--api-key <key> ... (can also use CURSOR_API_KEY env var)`); no \
38                login/auth step ran and no credential-bearing file appeared",
39    },
40    capabilities: &[
41        CapabilityClaim {
42            capability: Capability::ConfigIsolation,
43            state: CapabilityState::Supported,
44            basis: "measured: `+ f /home/agent/probe-target/cli-config.json` — the agent's one \
45                    pre-network write followed CURSOR_CONFIG_DIR into the target, and the watched \
46                    fallback location recorded `f /home/agent/.config/cursor  (absent)`, so the \
47                    XDG fallback was not chosen",
48        },
49        CapabilityClaim {
50            capability: Capability::CredentialIsolation,
51            state: CapabilityState::Unknown,
52            basis: "unmeasured: where stored credentials live was not established (Q4) — no \
53                    login/auth step ran and no credential-bearing file surfaced anywhere in the \
54                    delta; `CURSOR_API_KEY` (strings: `documented CURSOR_API_KEY: present`) is \
55                    the documented bypass variable, and no stored-credential observation was made",
56        },
57        CapabilityClaim {
58            capability: Capability::StateIsolation,
59            state: CapabilityState::Unknown,
60            basis: "unmeasured: the agent wrote only `cli-config.json` before it needed network \
61                    access, so no session/state file was observable — the delta tail names \
62                    nothing beyond `+ f /home/agent/probe-target/cli-config.json`",
63        },
64    ],
65    env: &[EnvOverride { name: VAR, sensitive: false }],
66    conflicts: &[],
67    credential_env: &["CURSOR_API_KEY"],
68};
69
70/// The Cursor Agent CLI adapter.
71#[derive(Debug, Clone, Copy)]
72pub struct Cursor;
73
74impl Adapter for Cursor {
75    fn metadata(&self) -> &'static AdapterMetadata {
76        &METADATA
77    }
78
79    fn paths(&self, root: &AppRoot, profile: &ProfileName) -> Vec<(PathBuf, PathKind)> {
80        // `CURSOR_CONFIG_DIR` names the configuration directory itself: the measured delta shows
81        // `cli-config.json` written directly inside the target, so the profile directory IS the
82        // config directory (the same call as opencode's). The agent's other writes stay at their
83        // defaults and are deliberately not declared.
84        vec![(profile_dir(root, profile, METADATA.id), PathKind::Dir)]
85    }
86
87    fn plan(&self, ctx: &PlanContext<'_>) -> Result<PlannedLaunch> {
88        METADATA.mechanism.plan(self, ctx)
89    }
90}