Add codex kit - #261
Draft
mdelapenya wants to merge 4 commits into
Draft
Conversation
Publishes the codex agent as a public `kind: sandbox` kit: spec, Dockerfile for the `docker.io/sbx/codex-image` base, both READMEs, .dockerignore, and TCK config. The kit boots a base image built on `docker/sandbox-templates:shell-docker` with the Codex CLI installed from npm, seeds `~/.codex/config.toml` and `~/.codex/auth.json` from the resolved credential mode, registers the sandbox MCP gateway on startup, and declares its own egress allow-list so it works under a deny-all policy. `testdata/tck.yaml` sets `extractedFromBuiltin: true`, so `sbx create` name collisions against the still-present built-in are skipped rather than failing. Signed-off-by: Manuel de la Peña <manuel.delapena@docker.com>
The mcp_registration subtest's transportKey and forbiddenKeys assertions were written against quoted JSON keys, because the only two kits that register a gateway both write JSON. A kit whose agent reads TOML could express neither: transportKey would fail a correct script, and forbiddenKeys would pass any script at all — a check that cannot fail, which reads as coverage while providing none. Add configFormat to the mcp: block — json (the default) or toml — and match keys accordingly. A TOML key is a bare word, so it is only distinguishable by position: line-anchored and followed by `=`, with leading whitespace tolerated because a heredoc body inside a YAML block scalar keeps its indentation. Forbidden keys additionally match the final segment of a [table.header], which is how a wrong key actually reaches a TOML config, and are segment-anchored so `headers` does not match `http_headers`. The format is declared rather than inferred from configPath's extension: configPath is optional, and an inference that silently falls back to JSON reintroduces the same assertions-that-cannot-fail problem. The extension is still cross-checked, so a .toml path left at the json default is rejected instead of quietly asserting the wrong syntax. Defaulting to json leaves copilot and kiro passing unchanged. Split the subtest body into assertMCPRegistration over a narrowed testing interface so tck/mcp_test.go can drive it with a recording T and show the new assertions actually failing on a wrong script — including a TOML script that spells the header table `headers`. Signed-off-by: Manuel de la Peña <manuel.delapena@docker.com>
The kit set BROWSER=xdg-open over a base image that ships neither xdg-open nor xdg-settings, and documented the resulting command-not-found as an acceptable fallback. A kit that builds its own image has no reason to document a missing program it could just install. xdg-utils with --no-install-recommends is dependency-free on this base: one architecture-independent package of shell scripts, about 400 KB on the image, because the recommends are the X11 desktop-integration half that a headless sandbox has no use for. The layer sits above the floating npm install so it stays a cache hit across the nightly rebuilds, and asserts `command -v xdg-open` rather than trusting apt's exit code. BROWSER=xdg-open does not make xdg-open re-invoke itself: Debian and Ubuntu patch the script to strip its own name out of $BROWSER before consulting it. With no browser and no display a link still does not open, but xdg-open now says so and exits non-zero instead of not existing at all. Rewrite the spec comment and document the behaviour in both READMEs, so the docs describe what the kit does rather than a failure mode it no longer has. Signed-off-by: Manuel de la Peña <manuel.delapena@docker.com>
With configFormat in the TCK, the assertions this kit had to opt out of are expressible, so declare them: configFormat: toml, transportKey: url, and the two wrong spellings worth catching. `headers` is the one that matters. Codex spells the header table http_headers and ignores unknown keys without complaint, so a gateway registered with `headers` looks healthy and sends no Authorization at all. `httpUrl` is the spelling reached for when transliterating an MCP config from a JSON agent. Verified by reintroducing the `headers` table in spec.yaml and watching the subtest fail on it. 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
Publishes the
codexagent as a publickind: sandboxkit. Contrib only — noengine changes. Six files, modelled on
droid/:codex/spec.yamlopenaicredential (apiKey + oauth), egress allow-list, env, install + startup hookscodex/Dockerfiledocker.io/sbx/codex-image, built ondocker/sandbox-templates:shell-dockercodex/README.mdcodex/README.image.mdcodex/.dockerignorecodex/testdata/tck.yamlextractedFromBuiltin: true+ themcp:blockThe kit dir and
name:are both exactlycodex, becausecodex-acpandcodex-app-serverpinrequires.agent: codexand affinity matching is byexact name. Both mixins' TCK suites still pass against this branch.
No workflow edits: image build/publish is discovered from the presence of
codex/Dockerfileplussandbox.image.Spec choices worth flagging for review
Three fixes to entries carried over from the built-in agent's spec. These
are the deliberate divergences, not accidents:
MCP header table renamed
headers→http_headers— the onecorrectness fix, and the important one. Codex spells the key
http_headersand silently ignores unknown config keys, so aheaderstable fails open: the gateway registers, looks healthy, and every tool
call through it goes out with no
Authorizationat all. Verified againstcodex-cli 0.151.0 in the built image — with
headers,codex mcp get mcp-gatewayreportshttp_headers: -; withhttp_headersit reports
http_headers: Authorization=*****andAuth: Bearer token.headersis the correct spelling for the JSON-configured agents(
copilot,kiro); it does not transliterate to Codex's TOML.corepack npm install -g→ plainnpm install -g— the built-inimage drives the install through
corepack npmand then attemptscorepack disable npm. Both halves are unsound on this base:corepack npmdownloads an unpinned npm on every build which then warns it does notsupport the image's Node (
npm v12.0.2 does not support Node.js v22.22.1), andcorepack disable npmcannot succeed at all — the layerruns as the non-root
agentuser, so unlinking the root-owned/usr/bin/npmfailsEACCESand only the|| echofallback keeps thebuild green. It has nothing to remove either, since nothing ever ran
corepack enable. The end state that matters is unchanged and asserted:/usr/bin/npmis the real apt npm and is first onPATH, which is whatCodex's self-updater needs. Same
codex-cli 0.151.0, same install path,and the build log is now clean.
mkdir -p "$HOME/.codex"added to the MCP startup hook — smallhardening. The hook previously went straight to
touch "$cfg", which is aset -efailure if the directory is ever missing.copilot's equivalenthook already does this.
Not fixed, flagged instead — both are behaviour changes I did not want to
make unilaterally inside a port:
apiKey.injectincludes the bareopenai.comapex with anAuthorization: Bearerheader. The apex serves OpenAI's website, not theAPI; everything observed from a live CLI goes to
api.openai.com,chatgpt.com,auth.openai.com,files.openai.com. While it stands, anyprocess in the sandbox that fetches
openai.comgets the user's real keyattached by the proxy. Kept because a missing header is a silent 401 on
whatever path did want it, and "no path wants it" is a negative this port
cannot prove. There is a
TODOon the entry and a README section on it —please drop it if you can rule the last path out.
apiKeydoes not setproxyManaged: true, unlike most apiKey-basedkits here. On the paths this kit configures Codex reads its key from
auth.json, where the kit already writes theproxy-managedsentinelitself — so the env var is not the delivery path. The consequence is still
that the real key sits in the container env rather than only in the proxy.
Turning it on looks more correct and is unverified against Codex's own
key-discovery order.
Other deviations from the built-in spec, all deliberate:
sandbox.imageisdocker.io/sbx/codex-image:latest, not the sandboxtemplate.
scripts/check-image-ref.shhard-fails anything else for a kitthat ships a
Dockerfile. BuiltFROM docker/sandbox-templates:shell-docker.:443/:80; every other kit here uses bare hosts, and a hardcoded:80on the apt mirrors breaks
apt-get updateoutright the day the baseimage's sources move to https.
codex-app-serveralready proves bare apthosts work under
deny-all.version: "1.0.0",displayName,descriptionadded — kit metadata withno built-in equivalent; the TCK requires the latter two.
BROWSER=xdg-openkept for parity even though the base image ships noxdg-utils(verified absent), so the practical effect is that Codex'sbrowser-open attempt fails fast and it falls back to printing the URL.
codex --version. An install command'sexit code only says npm exited 0;
--include=optionalis what actuallydecides whether a usable binary landed, so the outcome is asserted rather
than assumed.
testdata/tck.yamlsetsconfigPathbut deliberately omitstransportKeyandforbiddenKeys. Those TCK assertions search forquoted JSON keys (
"url":), and Codex's config is TOML (url = "…") —declaring
transportKey: urlwould fail against a correct script, and theforbiddenKeyssearches could only ever trivially pass. A check thatcannot fail reads as coverage, so it is left out with the reason recorded
in the file. Worth a follow-up in the TCK if more TOML-configured agents
land.
Known ceiling on CI, please read before trusting the green tick: while the
codexbuilt-in still exists, kit registration rejects the name collision, andthe contrib e2e harness skips
extractedFromBuiltinkits. So green CI heremeans TCK plus a local-image build — it does not exercise the name path or
a real
sbx create.Origin
Extracted from the
codexbuilt-in agent shipped insidesbx, ported to astandalone v2 contrib kit. Leg 2 of the embedded-agent extraction (
cursorisa separate PR). Prose written fresh; no engine internals reproduced.
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 ./codex/passes./scripts/test-kit.sh codexpasses (the TCK)./scripts/test-kit-e2e.sh codexpasses. 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 ./codex/ codexand verified the kit'sbinary / files / env are inside the running container.
The three unticked boxes are unticked on purpose: all of them need
sbxagainst a host daemon, and
sbxdoes not run inside a sandbox. They have notbeen run, and nothing below should be read as substituting for them — in
particular, the egress allow-list has not been validated under
deny-all,which is why the speculative
*.githubusercontent.comhost was left out ratherthan guessed in.
What was verified, from inside the sandbox:
./scripts/test-kit.sh codex— green, includinginstall_execution(theinstall hook runs for real, as the
agentuser, in the built image) andmcp_registration../scripts/test-kit.sh codex-acpand./scripts/test-kit.sh codex-app-server— both green, so neither mixin regressed and both still resolve their
requires.agent: codexaffinity target by exact name.go test ./spec/... ./scripts/...— green.scripts/check-image-ref.sh docker.io/sbx latestandscripts/check-release-tag.sh codex/v1.0.0— both pass.docker buildofcodex/Dockerfile— succeeds,codex-cli 0.151.0, binaryat
/usr/local/share/npm-global/bin/codex(exactly the pathcodex-app-server's wrapper hardcodes),/usr/bin/npmintact at 9.2.0.SBX_CRED_OPENAI_MODEvalues (
oauth,apikey,none, unset): correct branch each time, validTOML in every case,
auth.jsoncreated in the managed modes and removed inthe others, and re-running after a mode change leaves no stale block behind.
with one reserved, stays a single table when run again, and
codex mcp get mcp-gateway/codex mcp listconfirm Codex actually parsesthe result and attaches the bearer.
🤖 Generated with Claude Code