feat(cursor): run cursor-agent inside docker like claude #1032
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!1032
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/cursor-in-docker"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Brings the
cursorprovider to feature parity with theanthropic(claude) provider's containerised dispatch. Same image, same shim, same per-agent identity, same MCP wiring. Drops@cursor/sdkfrom the runtime tree.One shim, two providers.
CLAUDE_HOOKS_CONTAINER_BINenv var picks which binary the shim execs (defaults toclaudefor back-compat). Cursor path sets it tocursor-agent.Changes
Image (
Dockerfile) — installcursor-agentnext toclaudefrom the officialdownloads.cursor.comtarball, pinned byARG CURSOR_AGENT_VERSION. Mirror of the claude-code install block.Shim (
infrastructure/container/container.ts) —ensureExecShim()reads${CLAUDE_HOOKS_CONTAINER_BIN:-/usr/local/bin/claude}to pick the in-container CLI. New constantCONTAINER_CURSOR_EXECUTABLE. Test pins the contract: default fallback unchanged for claude; env override swaps the binary.Env (
domain/agent/agent-runner.ts) —CONTAINER_FORWARDED_ENVaddsCURSOR_API_KEY.buildAgentEnvacceptsprovider; on"cursor"setsCLAUDE_HOOKS_CONTAINER_BIN. Tests pin both the cursor and claude paths.runAgentTaskthreadstaskProviderthrough.Adapter (
infrastructure/agent/cursor-cli-adapter.ts) — picks binary in priority order:req.env.CURSOR_AGENT_BIN??req.pathToExecutable(the container shim) ??cursor-agent(host fallback). Now the only cursor backend inagent-runner.ts; the in-processSdkCursorAgentis gone.MCP per agent (
infrastructure/agent-env-sync/render-for-instance.ts) — newrenderCursorMcpJsonwrites<envDir>/cursor/mcp.jsonfrom the sameDefaultMcpRegistry.serversForsource that builds claude's.claude.json::mcpServers.container-reconcile.tsadds the bind mount${credsDir}/cursor:/home/claude/.cursor:ro. Same per-agent MCP registry powers both providers without a translation layer.Drop the SDK — removed
cursor-sdk-adapter.ts+ its tests + thecursor-replay-conversation.jsonfixture + theapps/server/src/repro/spike scripts.sdk-adapter.ts::fetchCursorModelsshells out tocursor-agent modelsinstead ofCursor.models.list.@cursor/sdkremoved fromapps/server/package.json(~16 MB out ofnode_modules).Spec (
docs/specs/cursor-in-docker.md) — design notes + checklist + rollout plan.Why
The in-process
@cursor/sdk1.0.12 backend:NGHTTP2_FRAME_SIZE_ERRORwhenever cwd contains.git/(server-side bug in theGetTeamReposOrEmptyIfNotInTeamflow). PR #1027 worked around it via a symlink wrapper. The CLI sidesteps the codepath entirely.charleswith full filesystem access. Inconsistent with the claude path, where every tool runs inside the agent's container asclaude:claude.--author.~/.cursor/mcp.jsonper-agent; MCP config leaks from the operator's host.Containerising
cursor-agent(which mirrors the claude-code CLI's-p --output-format stream-jsoncontract) closes all four gaps with the same infra claude already uses.Test plan
bun test apps/server/src/infrastructure/{agent,container,agent-env-sync}— 194/194 passbun test apps/server/src/domain/agent/agent-runner.test.ts— 83/83 pass (incl. newbuildAgentEnvcases for the cursor + anthropic provider paths)bun x tsc --noEmit -p apps/servercleangrep -r '@cursor/sdk' apps/server/srcreturns only doc comments — no live importjust containers-rebuild devrebuilds image withcursor-agentbaked inprovider: cursordev agent — confirmcursor-agentruns inside the container, branch pushed under per-agent identity, PR opened via forge MCPOut of scope
@anthropic-ai/claude-agent-sdkonto bare CLI spawn. The SDK still pulls weight (abort + stdin steer + typed SDKMessage); cleanup later if useful.~/.cursor/rules/*.mdcfrom claude's~/.claude/skills/*.md. Not load-bearing for a functional cursor-in-docker dispatch; separate spec.CURSOR_AGENT_VERSION. Pinned viaARGfor now; operator bumps manually like claude.🤖 Generated with Claude Code
Brings the `cursor` provider to feature parity with the `anthropic` (claude) provider's containerised dispatch. Same image, same shim, same per-agent identity, same MCP wiring. Drops `@cursor/sdk` from the runtime tree. ## Changes **Image** (`Dockerfile`) - Install `cursor-agent` next to the existing `claude` CLI from the official `downloads.cursor.com` tarball, pinned by `ARG CURSOR_AGENT_VERSION`. Mirror of the claude-code install block. **Shim** (`infrastructure/container/container.ts`) - `ensureExecShim()` now reads `${CLAUDE_HOOKS_CONTAINER_BIN}` to pick which CLI inside the container the shim execs, defaulting to the existing `/usr/local/bin/claude`. One shim, two providers. - New constant `CONTAINER_CURSOR_EXECUTABLE = /usr/local/bin/cursor-agent`. - Test pins the contract: default fallback is the claude path, env override swaps the binary. **Env wiring** (`domain/agent/agent-runner.ts`) - `CONTAINER_FORWARDED_ENV` adds `CURSOR_API_KEY` so the host-injected key reaches the container. - `buildAgentEnv` accepts `provider`; when `"cursor"` it sets `CLAUDE_HOOKS_CONTAINER_BIN` to the cursor-agent path. Tests pin both the cursor and claude paths. - `runAgentTask` threads `taskProvider` into `buildAgentEnv`. **Adapter** (`infrastructure/agent/cursor-cli-adapter.ts`) - Binary picked from `req.env.CURSOR_AGENT_BIN` ?? `req.pathToExecutable` ?? `cursor-agent`. Container mode threads the shim path through `pathToExecutable` (set by agent-runner). Host mode falls back to the system binary. - Adapter is now the only cursor backend in `agent-runner.ts`. The in-process `SdkCursorAgent` fallback is removed. **MCP per agent** (`infrastructure/agent-env-sync/render-for-instance.ts`) - New `renderCursorMcpJson` writes `<envDir>/cursor/mcp.json` from the same `DefaultMcpRegistry.serversFor` source that builds claude's `.claude.json::mcpServers`. Schema mirrors cursor's documented `~/.cursor/mcp.json` shape, so the same per-agent MCP registry powers both providers without a translation layer. - `container-reconcile.ts` adds the bind mount `${credsDir}/cursor:/home/claude/.cursor:ro`. **Drop the SDK** - Removed `apps/server/src/infrastructure/agent/cursor-sdk-adapter.ts`, its test file, the `cursor-sdk-delta.test.ts`, the `cursor-replay-conversation.json` fixture, and the `apps/server/src/repro/` spike scripts. - `sdk-adapter.ts::fetchCursorModels` shells out to `cursor-agent models` instead of `Cursor.models.list`. - `@cursor/sdk` removed from `apps/server/package.json`. **Spec** (`docs/specs/cursor-in-docker.md`) — design notes + checklist. ## Test plan - [x] `bun test apps/server/src/infrastructure/{agent,container,agent-env-sync}` clean - [x] `bun test apps/server/src/domain/agent/agent-runner.test.ts` clean (incl. new buildAgentEnv cases) - [x] `bun x tsc --noEmit -p apps/server` clean - [ ] `just containers-rebuild dev` rebuilds image with `cursor-agent` baked in - [ ] Live kick on `provider: cursor` dev agent — confirm `cursor-agent` runs inside container, branch pushed under per-agent identity, PR opened via forge MCP. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>df632842d68245954cecSubagent code review — flagged 3 substantive issues that should be resolved before merge.
1.
--workspacearg points at host cwd, not in-container cwdcursor-cli-adapter.ts:200passesreq.cwdto--workspace. In container mode,agent-runner.ts:977setsreq.cwd = process.cwd()(host service cwd viaresolveHostCwd), socursor-agentis told--workspace /home/charles/Workspace/claude-hooks— a path that does not exist inside the container. The shim's-w "$CLAUDE_HOOKS_DOCKER_CWD"fixes the process cwd to the in-container worktree, so cursor might fall back to its own cwd, but the explicit--workspaceflag is at best confusing and at worst will make cursor-agent error or operate against the wrong tree.docs/specs/cursor-in-docker.md:120wrongly assumesreq.cwdis already the in-container path.Fix: thread
inContainerCwdseparately onRunTaskRequest, or have the cursor adapter preferreq.env.CLAUDE_HOOKS_DOCKER_CWDoverreq.cwd.2.
cursor-cli-adapter.ts(438 LoC) has zero unit testsDeleted
cursor-sdk-adapter.test.tscovered ~957 lines of mapping behaviour. Replacement adapter has identical event-mapping complexity (mapEvent,pickToolEntry,summarizeArgs,summarizeResult,shouldResume,normalizeUserTurns) and is now the only cursor backend. Needs equivalent coverage — esp. binary-selection priority order (req.env.CURSOR_AGENT_BIN>req.pathToExecutable> default), resume-sentinel blacklist, result-event ok/error mapping.3. MCP bind path: spec contradicts impl
docs/specs/cursor-in-docker.md:132says mount at/home/claude/.config/cursor/. Implementation atcontainer-reconcile.ts:424mounts at/home/claude/.cursor. Implementation matches official cursor docs (~/.cursor/mcp.json) so likely correct — update spec.Confirmed clean
/usr/local/bin/claudepreserved; both branches pinned incontainer.test.ts:279-289gitIdentityEnv→agentEnv→ shim →docker exec -e GIT_AUTHOR_NAME; no operator leakCURSOR_API_KEYforwarded via shim's per-process env (agent-runner.ts:954), not host wholesaleMCP per agent: sameserversForsource for.claude.jsonandcursor/mcp.json; ro bindcursor-sdk-adapter.ts,cursor-sdk-delta.test.ts,cursor-replay-conversation.json,apps/server/src/repro/);@cursor/sdkgone frompackage.json+bun.lockfetchCursorModelsshells out tocursor-agent modelswith per-callCURSOR_API_KEYCURSOR_AGENT_VERSIONARG, mirrors claude-code install, hadolint cleanpipeline-stall.test.ts_resetConfigForTest()is the correct fix; same bleed risk exists pre-existing in otherloadWebhookConfigcallers (http/webhook.test.ts,analytics/agents-health.test.ts,forge/adapter-factory.test.ts) — separate cleanupCI green, full suite 3405/3405.
8245954cecbf7e6bb57a