Skip to main content

Module backend_cursor

Module backend_cursor 

Source
Expand description

Cursor agent backend: drives the Cursor CLI headless (agent --print --output-format stream-json).

Ground truth is the decided route in docs/scoping/cursor-cli-backend.md (decision revised 2026-07-09: direct-parser) and the committed wire fixture docs/scoping/cursor-probe-evidence/fixture-stream-json.jsonl, captured from a real write-capable run. The parser is built against THAT shape first; the event mapping table in the scoping doc’s implementation brief is the authority for every arm below.

This module is single-shot only: --resume exists on the CLI but was never exercised by the probe, and there is no streaming-input mode, so CursorSession::send_user_message and a resumed SessionSpec are both rejected at the seam rather than translated into flags.

Several SessionSpec fields are claude-isms with no cursor equivalent and are deliberately ignored when building argv: json_schema, max_budget_usd, resume, permission_mode, allowed_tools / disallowed_tools, tools, settings_json, and effort — the CLI has no --effort flag, and the --model bracket-override syntax ('model[effort=high]') is documented only for parameterized models and was never probed, so effort is NOT munged into the model id.

Permission posture (probe item 6, observed): read-only sessions map to --mode ask (turn-level read-only, the validator role), writable sessions to default mode + --force (writes/shell proceed unprompted, the worker role; --yolo is only a documented alias of --force, never separately live-tested, so --force is the emitted spelling). --trust rides every session: headless --print otherwise prompts for workspace trust. The CLI’s own --sandbox flag is NEVER emitted — the probe observed it make no difference to outbound network access or writes outside --workspace, so it is not a kranz isolation boundary. The no-push/no-publish/ no-main-write invariants therefore hold exactly the way the scoping doc prescribes: turn-level read-only modes for validators, throwaway --workspace directories, scoped credentials, and (when requested) the engine’s external process sandbox — never this flag. Because the resolved OS sandbox is not applied by this backend, BackendKind::Cursor::supports_sandbox_enforcement is false and config::validate fails closed on enforced-sandbox pairings.

Auth posture (probe, verified): the CLI’s login state does not survive a relocated $HOME (HOME=/tmp/x agent status reports “Not logged in”), and on macOS the credential itself is Keychain-backed (the sandboxed probe crashed with SecItemCopyMatching failed -50) — no credential FILE exists under ~/.cursor to copy. The scratch-HOME seed therefore carries only the small account-identity/CLI-config files ([CURSOR_SEED_ENTRIES]), never transcripts or caches, and the one ambient var a cursor session may authenticate with — CURSOR_API_KEY, the scoping doc’s sanctioned headless channel — is injected explicitly, never the ambient set. A session whose seed+key is insufficient fails auth loudly (“Authentication required”, pre-billing), which the stream watcher turns into an honest configuration-style failure rather than a retryable one (probe item 5). One macOS addendum found by the first live mission (m-a5a8fd, 2026-08-08): the CLI consults the login keychain at startup EVEN with CURSOR_API_KEY set, and the keychain domain resolves through HOME, so a relocated HOME without Library/Keychains/login.keychain-db dies pre-auth with security exit 154. Both spawn branches therefore seed an EMPTY login keychain (ensure_session_login_keychain) — never a link to the operator’s real keychain.

Hook-status lane (ticket agent-hooks-status-signals, crate::hook_status): when the runner seeds SessionSpec::hook_status (mission config hookStatus.enabled AND this hook-capable backend), CursorBackend::start installs the lane into the session-private HOME BEFORE spawning: <home>/.cursor/ hooks.json (the CLI’s documented user-level hook file — verified 2026-08-06 against https://cursor.com/docs/hooks: version: 1 with per-event [{command, timeout}] handlers, payloads delivered on stdin, exit 0 = ok / 2 = block / other = fail-open; there is NO HTTP hook type, so delivery to kranz’s endpoint is the installed kranz hook-status relay) plus the per-session spec file the relay reads. The install NEVER touches the workspace’s tracked .cursor/hooks.json — the project-level file is the operator’s own, and mutating it as a side effect of spawning would be exactly the silent tracked-tree write the ticket forbids. Every install failure degrades to NO lane (loud warning, ordinary session): hooks are non-authoritative observability, and the lane disabled is the byte-identical default. SessionSpec::hook_status is the one field this backend consumes beyond argv/env; the claude-ism fields stay ignored as documented above.

Structs§

CursorBackend
The AgentBackend for agent --print --output-format stream-json: single-shot with the --mode ask / --force permission posture selected from the session role.
CursorSession
A live agent --print --output-format stream-json session (the AgentSession impl).

Functions§

build_args
Build the argv (excluding the binary itself) for one session.
discover_cursor_binary
Locate a working cursor agent binary.
parse_cursor_line
Parse one stdout line into zero or more AgentEvents. model is the configured model id, used as the Init fallback (the wire’s init event carries a model DISPLAY string, e.g. “GPT-5.6 Luna 272K Low”, which is preferred when present) and as the pricing key for the terminal event’s client-side cost computation.
parse_cursor_value
Map one parsed stream-json value to events (see module docs / docs/scoping/cursor-cli-backend.md’s event-to-AgentEvent table). Unrecognized type/subtype combinations route to AgentEvent::Other rather than being guessed at.