Skip to content

Add cursor kit - #262

Draft
mdelapenya wants to merge 1 commit into
mainfrom
add-cursor-kit
Draft

Add cursor kit#262
mdelapenya wants to merge 1 commit into
mainfrom
add-cursor-kit

Conversation

@mdelapenya

@mdelapenya mdelapenya commented Aug 31, 2026

Copy link
Copy Markdown
Member

Summary

Adds a cursor/ kit — a standalone kind: sandbox, schemaVersion: "2" kit for
Cursor Agent, extracted from the built-in cursor
agent. Modelled on droid/, which is the closest steady-state shape in the
repo: spec.yaml, a Dockerfile for docker.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 one cursor
credential with both an apiKey and an oauth shape, pre-trusts the workspace
so 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:

  • Today it will not load. sbx refuses a kit whose name collides with a
    built-in agent, and cursor is still built in. testdata/tck.yaml sets
    extractedFromBuiltin: true, which turns that one failure into a skip, so the
    contrib e2e harness skips this kit entirely. Green CI here means TCK plus
    the local-image container assertions — not the name/registration path.
  • Removing the built-in is not sufficient either. Some of Cursor's
    credential wiring is currently provided by sbx rather 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. sbx performs it for an agent it knows by the name cursor, and it
    reads 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

Built-in This kit Why
image: docker/sandbox-templates:cursor-agent-docker docker.io/sbx/cursor-image:latest, built from shell-docker A kit that ships a Dockerfile must publish into the namespace CI authenticates to; scripts/check-image-ref.sh hard-fails otherwise.
entrypoint: [cursor-agent, --yolo] absolute /home/agent/.local/bin/cursor-agent The installer puts the binary there. The directory is on the image's PATH (verified), but PATH belongs to the runtime, which may replace it, and the entrypoint is exec'd rather than run through a login shell. The base image's own CMD spells it absolutely too.
apt hosts pinned :80, Cursor hosts :443 no ports at all A portless pattern matches any port. archive.ubuntu.com:80 blocks the moment a mirror answers over HTTPS — and because apt-get update fails wholesale when one source is unreachable, that is not a partial failure.
three enumerated *.cursor.sh hosts same three plus *.cursor.sh Cursor shards its API across numbered hosts (api2, api3 are 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.
no resourceHosts on the OAuth block resourceHosts listed Purely descriptive: routing is the union of the apiKey inject domains, the token-endpoint host, and this list, and that union is unchanged. It makes the OAuth grant self-describing instead of only inferable from the apiKey block, and surfaces in the credential-consent summary.
n/a no apiKey.proxyManaged: true See item 3.
n/a no required: true on the credential Cursor is usable with nothing bound (it falls back to its own sign-in), so failing fast at sbx create would remove a working path. droid sets it because droid genuinely cannot authenticate without a credential.
curl -fsS … | bash curl -fsSL … | bash plus test -x on the installed binary -f does not treat a 3xx as an error, so without -L a redirect pipes an empty body to bash, bash exits 0, pipefail sees nothing, and the image builds "successfully" with no agent in it. The test -x covers the installer's own failure paths for the same reason — nothing else in local verification asserts the binary exists.
header:/format: on each inject kept verbatim The scheme: bearer sugar normalises to exactly this, but no shipped kit here uses it yet and an older sbx would reject the unknown field under strict decoding. Not worth the compatibility risk for four lines.

Also carried over unchanged, deliberately: skipIfEnv: [CURSOR_API_KEY] (a
no-op under schemaVersion: "2" binding-driven resolution, kept so a diff
against the built-in stays minimal) and the refreshToken sentinel (never
rendered 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

  • No credentialFile. Cursor's file-backed store validates whatever token
    it finds, so materialising a sentinel into it is worse than writing nothing:
    validation fails and Cursor re-prompts for login. AGENT_CLI_CREDENTIAL_STORE=memory
    keeps Cursor from reading that file at all. This is what forces the env-var
    delivery discussed in item 1.
  • No apiKey.proxyManaged: true, unlike most API-key kits here — and this
    one is a trap worth naming. proxyManaged sets the env var to the sentinel
    unconditionally, in every sandbox. Cursor treats a set-but-unbacked
    CURSOR_API_KEY as a real key and reports it invalid rather than falling back
    to sign-in, so enabling it would turn a working login prompt into an
    authentication error on any host with no Cursor credential. sbx already sets
    that 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. promptArgs omitted from tck.yaml

Two 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, which
resolves only if ~/.local/bin is on the exec's inherited PATH — exactly the
assumption the absolute entrypoint declines to make. Whoever wires promptArgs
should expect to set binary: to the absolute path as well. Both notes are in
tck.yaml.

Origin

Ported from the built-in cursor agent in sbx — same behaviour, prose written
fresh for this repo. Second leg of the embedded-agent extraction, after droid
(#249). Contrib-only: no engine changes.

Test plan

CI runs kit validate and the TCK on every PR. CI does not run e2e on
fork 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 cursor passes (the TCK)
  • ./scripts/test-kit-e2e.sh cursor passes. The script applies the same
    deny-all baseline 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.allowedDomains came from
    sbx --app-name sbx-kits-contrib-tck policy log <tck-e2e-…>, not a guess.
  • Manual smoke: sbx run --kit ./cursor/ cursor and verified the kit's
    binary / 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 sbx does
not run inside a sandbox, so sbx kit validate, test-kit-e2e.sh and the manual
smoke all need a host daemon. Please treat them as genuinely uncovered. Note too
that even on a host, test-kit-e2e.sh will skip this kit while the cursor
built-in exists (extractedFromBuiltin: true).

What was verified here:

  • ./scripts/test-kit.sh cursor — full TCK green, including the container
    subtests: both install commands execute cleanly as their declared users, the
    setup.files entry lands, env vars are set, tmpfs mounts are present.
  • go run ./scripts/verify-kit-spec ./cursorvalid, no warnings.
  • ./scripts/check-image-ref.sh docker.io/sbx latest — passes.
  • The workspace pre-trust command, run byte-for-byte as extracted from
    spec.yaml inside the base image with WORKSPACE_DIR set: writes
    ~/.cursor/projects/home-agent-workspace/.workspace-trusted with the expected
    JSON. Its guard branch (unset WORKSPACE_DIR) is what the TCK exercises, and
    it exits 0.
  • /home/agent/.local/bin is on the base image's PATH — checked, which is why
    the entrypoint rationale is "don't depend on it", not "it's missing".
  • Re-ran the pre-fix(spec): stop rejecting non-WORKDIR ${...} in initFiles content #200 inline-placeholder validator against setup.files
    content: no ${...} other than ${WORKDIR} (there are none at all), so the
    released-sbx placeholder rejection does not apply.

Two caveats on the above, stated plainly:

  1. The image build could not complete in this sandbox. The kit's egress
    policy is not the constraint — the sandbox's own firewall denies
    downloads.cursor.com, which is where cursor.com/install redirects the
    tarball fetch. Confirmed as a policy block (403 Blocked by network policy).
    Silver lining: that is how downloads.cursor.com was confirmed to be an
    install dependency and not just a self-update one. The Dockerfile itself
    is therefore unbuilt and unverified; CI's build leg is the first real
    test of it.
  2. Because of (1), the TCK's container subtests ran against a local stand-in
    tagged from docker/sandbox-templates:shell-docker rather than the real kit
    image. 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-agent is present in the image.

Update after the first CI run

CI has now closed both gaps above:

  • kit (cursor) / image passed. The Dockerfile builds for real, which
    covers caveat 1 — including the new test -x assertion, so cursor-agent is
    confirmed present at /home/agent/.local/bin/cursor-agent in the built image.
    artifact / publish and both overview jobs also passed.
  • test-kit (cursor), Analyze (go), Analyze (actions), CodeQL,
    verify-spec-refs and DCO all pass.
  • The e2e-release / e2e-nightly legs report success, but read that as the
    skip it is: extractedFromBuiltin: true is doing its job while the
    cursor built-in exists. It is not evidence the name/registration path works.

The three unticked boxes in the test plan stay unticked — a host sbx is still
the only place kit validate, the e2e harness proper, and the manual smoke can
run.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant