Add cursor kit - #262
Draft
mdelapenya wants to merge 1 commit into
Draft
Conversation
A standalone `kind: sandbox` kit for Cursor Agent, modelled on the `droid` kit: a spec, a base image built from a Dockerfile in the kit directory, and a TCK config. The kit publishes `docker.io/sbx/cursor-image:latest` from `docker/sandbox-templates:shell-docker` with the vendor install script on top, and names the installed binary by absolute path in the entrypoint so the launch does not depend on PATH being inherited. Egress is declared explicitly rather than inherited: kits run under a deny-all baseline, so the allow list carries Cursor's API and package hosts alongside the apt sources the base image ships with. Signed-off-by: Manuel de la Peña <manuel.delapena@docker.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
cursor/kit — a standalonekind: sandbox,schemaVersion: "2"kit forCursor Agent, extracted from the built-in
cursoragent. Modelled on
droid/, which is the closest steady-state shape in therepo:
spec.yaml, aDockerfilefordocker.io/sbx/cursor-image:latest,README.md,README.image.md,.dockerignore,testdata/tck.yaml.The kit runs
/home/agent/.local/bin/cursor-agent --yolo, declares onecursorcredential with both an
apiKeyand anoauthshape, pre-trusts the workspaceso the TUI's trust prompt does not appear on every run, and pins Cursor's agent
connection to HTTP/1.1 + SSE so it goes through the forward proxy.
Read the "Spec choices worth flagging" section before this is treated as a
replacement for the built-in — it is not one yet, for two independent reasons.
Spec choices worth flagging for review
1. This kit is reviewable, not yet usable — and cannot simply be swapped in
Two constraints point in opposite directions:
sbxrefuses a kit whose name collides with abuilt-in agent, and
cursoris still built in.testdata/tck.yamlsetsextractedFromBuiltin: true, which turns that one failure into a skip, so thecontrib e2e harness skips this kit entirely. Green CI here means TCK plus
the local-image container assertions — not the name/registration path.
credential wiring is currently provided by
sbxrather than by this spec.After sign-in resolves, the OAuth access-token sentinel has to reach the agent
through an environment variable (this kit deliberately writes no credential
file — see below), and the spec grammar has no field to declare that
delivery.
sbxperforms it for an agent it knows by the namecursor, and itreads the sentinel to deliver from the built-in agent's own definition, not
from this file. Delete the built-in and the delivery goes with it: a user who
signed in on the host would still be asked to sign in inside the sandbox.
So this is a leg-3 blocker, not a leg-2 one. The kit is published for parity
and review; retiring the built-in behind it needs the grammar to grow a way to
declare that env delivery. There is no kit-side workaround — see item 3.
The README says all of this in user-facing terms, including at the very top, so
nobody follows the Usage section into a hard registration error.
2. Deviations from the built-in spec, and why
image: docker/sandbox-templates:cursor-agent-dockerdocker.io/sbx/cursor-image:latest, built fromshell-dockerscripts/check-image-ref.shhard-fails otherwise.entrypoint: [cursor-agent, --yolo]/home/agent/.local/bin/cursor-agentPATH(verified), butPATHbelongs to the runtime, which may replace it, and the entrypoint is exec'd rather than run through a login shell. The base image's ownCMDspells it absolutely too.:80, Cursor hosts:443archive.ubuntu.com:80blocks the moment a mirror answers over HTTPS — and becauseapt-get updatefails wholesale when one source is unreachable, that is not a partial failure.*.cursor.shhosts*.cursor.shapi2,api3are both already named) and sbx's own default policy wildcards the domain rather than enumerating it. Scope is stated exactly in the comment:*never crosses a dot, so this covers one more single-label sibling and nothing deeper.resourceHostson the OAuth blockresourceHostslistedapiKey.proxyManaged: truerequired: trueon the credentialsbx createwould remove a working path.droidsets it becausedroidgenuinely cannot authenticate without a credential.curl -fsS … | bashcurl -fsSL … | bashplustest -xon the installed binary-fdoes not treat a 3xx as an error, so without-La redirect pipes an empty body tobash,bashexits 0,pipefailsees nothing, and the image builds "successfully" with no agent in it. Thetest -xcovers the installer's own failure paths for the same reason — nothing else in local verification asserts the binary exists.header:/format:on each injectscheme: bearersugar normalises to exactly this, but no shipped kit here uses it yet and an oldersbxwould reject the unknown field under strict decoding. Not worth the compatibility risk for four lines.Also carried over unchanged, deliberately:
skipIfEnv: [CURSOR_API_KEY](ano-op under
schemaVersion: "2"binding-driven resolution, kept so a diffagainst the built-in stays minimal) and the
refreshTokensentinel (neverrendered into the container, but required for the proxy to mask the real refresh
token and recognise sentinel-bearing refresh requests).
3. Two deliberate omissions in the credential block
credentialFile. Cursor's file-backed store validates whatever tokenit finds, so materialising a sentinel into it is worse than writing nothing:
validation fails and Cursor re-prompts for login.
AGENT_CLI_CREDENTIAL_STORE=memorykeeps Cursor from reading that file at all. This is what forces the env-var
delivery discussed in item 1.
apiKey.proxyManaged: true, unlike most API-key kits here — and thisone is a trap worth naming.
proxyManagedsets the env var to the sentinelunconditionally, in every sandbox. Cursor treats a set-but-unbacked
CURSOR_API_KEYas a real key and reports it invalid rather than falling backto sign-in, so enabling it would turn a working login prompt into an
authentication error on any host with no Cursor credential.
sbxalready setsthat variable conditionally, only when a host credential exists. Declaring
the flag here would not add the variable — it would remove the condition.
(This is also why the auth-token variable cannot be faked via
environment.variables: same unconditional-sentinel failure.)4.
promptArgsomitted fromtck.yamlTwo independent reasons, either sufficient: with no credential on the runner
Cursor starts interactive sign-in and a non-interactive prompt would block
rather than fail; and the prompt subtest invokes bare
cursor-agent, whichresolves only if
~/.local/binis on the exec's inheritedPATH— exactly theassumption the absolute entrypoint declines to make. Whoever wires
promptArgsshould expect to set
binary:to the absolute path as well. Both notes are intck.yaml.Origin
Ported from the built-in
cursoragent insbx— same behaviour, prose writtenfresh for this repo. Second leg of the embedded-agent extraction, after
droid(#249). Contrib-only: no engine changes.
Test plan
CI runs
kit validateand the TCK on every PR. CI does not run e2e onfork PRs (Docker Hub secrets aren't exposed there), so the e2e step below is
required from your side before requesting review.
sbx kit validate ./cursor/passes./scripts/test-kit.sh cursorpasses (the TCK)./scripts/test-kit-e2e.sh cursorpasses. The script applies the samedeny-allbaseline CI uses and scopes everything to its own daemon(
--app-name sbx-kits-contrib-tck), so my main sbx state is untouched.Every entry I added to
network.allowedDomainscame fromsbx --app-name sbx-kits-contrib-tck policy log <tck-e2e-…>, not a guess.sbx run --kit ./cursor/ cursorand verified the kit'sbinary / files / env are inside the running container.
The three unticked boxes are unticked because they cannot be run here, not
because they were skipped. This work was done inside a sandbox, and
sbxdoesnot run inside a sandbox, so
sbx kit validate,test-kit-e2e.shand the manualsmoke all need a host daemon. Please treat them as genuinely uncovered. Note too
that even on a host,
test-kit-e2e.shwill skip this kit while thecursorbuilt-in exists (
extractedFromBuiltin: true).What was verified here:
./scripts/test-kit.sh cursor— full TCK green, including thecontainersubtests: both install commands execute cleanly as their declared users, the
setup.filesentry lands, env vars are set, tmpfs mounts are present.go run ./scripts/verify-kit-spec ./cursor—valid, no warnings../scripts/check-image-ref.sh docker.io/sbx latest— passes.spec.yamlinside the base image withWORKSPACE_DIRset: writes~/.cursor/projects/home-agent-workspace/.workspace-trustedwith the expectedJSON. Its guard branch (unset
WORKSPACE_DIR) is what the TCK exercises, andit exits 0.
/home/agent/.local/binis on the base image'sPATH— checked, which is whythe entrypoint rationale is "don't depend on it", not "it's missing".
setup.filescontent: no
${...}other than${WORKDIR}(there are none at all), so thereleased-
sbxplaceholder rejection does not apply.Two caveats on the above, stated plainly:
policy is not the constraint — the sandbox's own firewall denies
downloads.cursor.com, which is wherecursor.com/installredirects thetarball fetch. Confirmed as a policy block (
403 Blocked by network policy).Silver lining: that is how
downloads.cursor.comwas confirmed to be aninstall dependency and not just a self-update one. The
Dockerfileitselfis therefore unbuilt and unverified; CI's build leg is the first real
test of it.
containersubtests ran against a local stand-intagged from
docker/sandbox-templates:shell-dockerrather than the real kitimage. Every assertion those subtests make (install commands,
setup.files,env, tmpfs) is a base-image behaviour, so the run is meaningful — but it does
not prove
cursor-agentis present in the image.Update after the first CI run
CI has now closed both gaps above:
kit (cursor) / imagepassed. TheDockerfilebuilds for real, whichcovers caveat 1 — including the new
test -xassertion, socursor-agentisconfirmed present at
/home/agent/.local/bin/cursor-agentin the built image.artifact / publishand bothoverviewjobs also passed.test-kit (cursor),Analyze (go),Analyze (actions),CodeQL,verify-spec-refsand DCO all pass.e2e-release/e2e-nightlylegs report success, but read that as theskip it is:
extractedFromBuiltin: trueis doing its job while thecursorbuilt-in exists. It is not evidence the name/registration path works.The three unticked boxes in the test plan stay unticked — a host
sbxis stillthe only place
kit validate, the e2e harness proper, and the manual smoke canrun.