Guidance for AI coding agents (and the humans steering them) making changes to the Docker Sandbox Kit Specification. It is the operational companion to CONTRIBUTING.md and GOVERNANCE.md: those say what a change must carry, this says how to produce and verify one without surprising a reviewer.
Three things that move together:
| Part | Where | What it is |
|---|---|---|
| The specification | docs/spec/SPEC-v3.md, docs/spec/capabilities/, docs/spec/conformance.md |
Normative text. In SPEC-v3 and the capability pages every MUST/SHOULD is anchored and accounted for by a check or a waiver; conformance.md binds adapters and the suite itself and carries no anchors |
| The reference implementation | spec/, resolve/, assemble/, fetch/, cmd/frontend/, schema/ |
Go types and validation (the grammar's source of truth), the resolver, the BuildKit frontend, and the JSON Schema pinned to the Go types |
| The conformance suites | tck/, cmd/kit-tck/ |
kit-tck validate judges a published Kit; kit-tck runtime judges a runtime through an adapter. tck/sandbox also judges the suite itself against a fake adapter |
Plus examples/ (every descriptor must decode and validate against the
current grammar; tests enforce it), skills/ (tool-agnostic agent skills
for authoring and migrating Kits; nothing tests them), and
scripts/kit-version.sh (the map of which example Kits track an upstream
release).
When two representations disagree — Go types, JSON Schema, normative
prose, an example — the code in spec/ wins, and the fix is to bring the
others back in line, not to special-case the reader.
- Read the relevant normative page before touching behavior. A change to what a runtime must do is a change to a capability page first, and to the code second.
- When authoring or porting a Kit under
examples/, read and followskills/create-kit-v3/SKILL.mdorskills/migrate-kit-to-v3/SKILL.md. They are the repository's own instructions for that job, and they were verified against real artifacts. task --listnames every task with its purpose. Prefer a Task target over the underlyinggo/dockerinvocation: the targets carry guards and pinned versions that a hand-typed command does not.
Toolchain the tasks assume: Go at the version in go.mod,
Task, golangci-lint v2, Docker with buildx (for
lint:md, kit:dev, test:e2e), and the sbx CLI only for
tck:runtime:sbx.
Run what CI runs, from the fast signal outward. CI runs validate,
lint, test:unit, and test:tck on every pull request, so a clean
local run is the same verdict rather than a different one.
| Task | What it does | When |
|---|---|---|
task validate |
gofmt -l + go vet |
Every Go change. Seconds |
task lint |
golangci-lint and markdownlint with the repo configs (lint:go, lint:md) |
Every change. lint:md needs Docker; it runs a digest-pinned image over every **/*.md |
task test:unit |
Every Go test except the sandbox conformance suite. Includes decoding and validating every descriptor under examples/ and the schema-to-spec pins |
Every change. Under a minute |
task test:tck |
The sandbox conformance suite against the fake adapter, including every fake-adapter mutation | Any change under tck/, docs/spec/, or to a capability page. Minutes |
task test |
test:unit and test:tck together, as one go test ./.... It does not run validate or lint; those are separate |
Before opening a pull request, alongside validate and lint |
task kit:dev KIT=hello |
Builds the frontend from the current source under a unique tag and builds one example Kit against it | Any change under cmd/frontend/. Needs Docker with buildx |
task kits:dev |
The same for every Kit in KITS |
A frontend change that touches what every descriptor goes through (decoding, staging, export) |
task test:e2e |
Publishes two Kits to a throwaway registry and builds a set over them | A change to set building, fetch/, or assemble/. Needs Docker; CI does not run it |
task tck:kit REF=<ref> |
kit-tck validate against a published Kit |
When you changed what the artifact rules check and have a real Kit to judge |
task tck:runtime:sbx |
The full runtime suite against Docker Sandboxes | Do not run unprompted: it needs a signed-in sbx, drives hundreds of sandbox lifecycles, and takes up to two hours |
Narrowing is fine while iterating (go test ./spec/...,
go test ./tck/sandbox/ -run TestEveryStatement), but the tasks above
are what a reviewer expects to have passed.
Do not iterate on frontend changes by rebuilding docker/sandbox-kit:3
locally: BuildKit caches frontend resolution per reference and can keep
dispatching the stale binary. kit:dev and frontend:dev exist for
exactly this reason.
Use semantic branch names in the form <type>/<short-description>,
with a type from the commit conventions below and a lowercase,
hyphen-separated description of the change. Examples:
fix/resolver-cancellation, feat/kit-inspection, and
docs/branch-naming.
Do not include AI tool, model, agent, or harness identifiers in branch
names: prefixes such as codex/, claude/, or cursor/ expose local
tooling in repository history and pull requests. This convention
overrides tool-generated branch naming defaults. If a tool creates a
branch with such a name, rename it before pushing or opening a pull
request.
Commit subjects follow Conventional Commits:
<type>(<scope>): <imperative, lowercase summary, no trailing period>
Types in use, most to least common: fix, feat, docs, chore, ci,
test, refactor, perf. Scopes are the area the change lands in:
spec, schema, frontend, tck, kit-tck, resolve, assemble,
fetch, examples, skills, readme, task, deps, version,
builder. A change spanning two areas lists both (fix(spec,tck): …);
a change with no single home omits the scope (feat: expand example runtime kits). A breaking change marks the type with !
(feat(kit-tck)!: add inspect, rename kit to validate) and says what
breaks in the body.
Examples from the history:
fix(kit-tck): honor cancellation in the layer walk
feat(spec): derive a workload's provides from the packages its image carries
docs(skills): add v3 kit authoring and migration skills
chore(deps): bump the go-modules group with 3 updates
The body explains why, in prose, when the diff cannot. This codebase comments its reasoning heavily — a guard "earns its lines", a pinned digest says what a moved tag would have broken — and commit messages follow the same habit. Do not restate the diff.
Rules that hold for every commit:
- One logical change per commit, with its tests. Unrelated fixes go in separate commits so they can be reviewed and reverted independently.
- Sign off every commit (
git commit -s). Contributions are accepted under the Developer Certificate of Origin, and theSigned-off-bytrailer must match the author'suser.nameanduser.email. Never fabricate a sign-off for someone else; if you are committing on a user's behalf, the identity is theirs and the sign-off is their decision. - Never amend, rebase, or force-push over commits you did not create in this session unless explicitly asked.
- Do not hard-wrap the body at 72 or 80 columns. Write one paragraph per line and let the viewer wrap it: GitHub preserves line breaks in commit messages, so manual wrapping fixes where each line ends. The 72-column habit belongs to Markdown files in the tree (see Style), not to messages.
The title is a Conventional Commit subject, exactly as a commit's would be — pull requests are merged with a merge commit whose body is the title, so a title that breaks the convention breaks the history.
The body follows the template, which has two parts:
- What changed and why. If the change alters the grammar, a capability contract, or a conformance duty, name the normative statements that moved and how each is now accounted for: the anchor and the check that judges it, or the anchor and the recorded waiver. If the change renames or removes a descriptor field, say so explicitly — strict decoding means Kits already published under the old spelling stop decoding rather than degrading, and a reviewer needs that stated rather than inferred.
- A
release-noteblock. One user-facing sentence, orNONE. Keep the fenced block and itsrelease-noteinfo string intact; that is the shape a release reads from.
Keep the pull request as small as its subject. A refactor that a fix needed is a separate commit in the same pull request, not a second subject buried in the first.
As with commit bodies, do not hard-wrap the description: one paragraph per line. Pull request descriptions render as Markdown; this is a source-editing convention so prose can be revised without reflowing lines to a fixed column width. Keep intentional Markdown structure, such as lists and code blocks.
The descriptor grammar has four representations, and tests hold them together. A change to one is a change to all four in the same pull request:
spec/— the Go types, strict decoding, validation. Source of truth.schema/kit.schema.jsonandschema/capabilities/— the JSON Schema.spec/schema_test.gopins its constants (need types, enums, field regexes) against the spec package, so a drift failstest:unit.docs/spec/SPEC-v3.mdanddocs/spec/capabilities/— the normative text, with anchors (below).examples/— every descriptor must still decode and validate.
And a fifth that nothing tests: the skills under skills/ teach the
grammar, so update them by hand.
Decoding is strict: an unrecognized field is an error, not a warning.
That means adding a field is not free — a descriptor using it cannot
be read by a frontend or runtime built before it. Before changing a
capability's config, read the versioning rules in
RELEASES.md: a config schema that has shipped in a tagged
release moves its @N version and the old version stays published; a
capability that has never shipped may still change in place; a
top-level descriptor change is a schemaVersion move and is treated as a
new specification document. Do not bump schemaVersion or a capability
version without stating in the pull request which rule forced it.
docs/spec/SPEC-v3.md and the capability pages are held to an
accounting that tck/sandbox/coverage_test.go enforces, and test:tck
fails when it slips:
- Every statement using MUST, MUST NOT, SHOULD, or SHOULD
NOT carries a
<!-- tck: <id> -->anchor on the line that opens it (a bullet, a table row, or a paragraph's first line). The anchor is invisible in rendered docs and is the statement's stable identity; do not rename one without updating every check, cover, and waiver that names it. - Every anchored statement is judged by a check whose requirement id
matches it, listed under another check's entry in the
coversorkitCoversmaps, or recorded inwaivedwith a reason. A statement that is both waived and covered fails: drop the waiver. - An observable runtime duty comes with a fixture and a fake-adapter
mutation in
tck/sandbox/sandbox_test.go, so the suite proves the check fails when the behavior is absent. A check no mutation can fail is a check nobody has seen fail, and the suite refuses it. - A duty the suite cannot observe records a waiver saying what cannot be observed yet. A waiver is a decision reviewed like any other line, not a way past the guard.
docs/spec/conformance.md is outside this accounting by design: it
binds adapters and the suite itself.
Use RFC 2119 keywords only where you mean them. A sentence that reads like a requirement but is not one is either mislabeled or missing an anchor, and the guard will tell you which.
- The first line of a descriptor is
# syntax=docker/sandbox-kit:3and must stay first; theyaml-language-servermodeline, when present, is line two. - Kits that track an upstream release (
scripts/kit-version.sh kitslists them) have their version default refreshed bytask versions:update KIT=<kit>, not by hand — the tag a build publishes under is read from the same map, so a hand edit and the script can disagree about latest.task versions:checkreports without editing. - A new example Kit joins
KITSinTaskfile.yamlunless it is a set whose members are registry references (team,claude-acp-set,codex-acp-set), which only build once those references exist. It is tested automatically byspec/examples_test.goor, for a comment-descriptor.dockerfile,cmd/frontend/examples_test.go. - A
-mixinvariant of a workload Kit is its own directory with its own descriptor; the skills say when one is warranted.
- Do not push images or tags.
kit:pushrefuses withoutREGISTRY, andfrontend:pushrefuses withoutFRONTEND_PUSH_OK=1, becausedocker/sandbox-kit:<tag>is a public syntax reference that every descriptor resolves. Those guards are not yours to satisfy. Releases arev3.X.Ygit tags pushed by a maintainer; see RELEASES.md. - Do not edit
go.sumby hand, and do not bump the commit SHAs pinned on GitHub Actions: Dependabot owns the Go module and the action pins, and moves the version comment beside each SHA with it. The tool versions the workflows and Taskfile pin (golangci-lint,task, themarkdownlint-cli2image digest) are deliberate and shared between CI and local runs; changing one is its ownci:orchore:commit with the reason, never a side effect of another change. - Do not relax a lint rule to make a change pass.
.golangci.ymland.markdownlint.ymleach say why every disabled rule is disabled; a new exemption needs the same justification and belongs in its own commit. - Do not add tool-specific copies of the skills.
skills/is the source of truth;.cursor/skillsand.claude/skillsare git-ignored local symlinks (seeskills/README.md)..cursor/,.claude/, and.vscode/local settings stay uncommitted. - Do not commit build outputs (
/frontend,/kit-tck,/bin,/dist) or anything under/tmpa task created..gitignorealready covers them; do not widen it to hide a new artifact. - Do not add a
.mdfile undertck/**/testdata/; conformance fixtures are test inputs the suite asserts on, not documentation, and the markdownlint ignore list is scoped to exactly that.
- Go:
gofmt,go vet, and golangci-lint's standard set are the floor. Tests usetestify/require. Errors are returned, not logged and swallowed. - Comments explain a decision the reader could not recover from the code: why a guard exists, what breaks without it, which spelling is portable between BSD and GNU tools. Do not comment what the next line does.
- Markdown is wrapped by hand at roughly 72 columns; tables use compact
separators (
|---|---|); no emoji.MD013(line length) is off because the specification hand-wraps and carries long reference tables, not as a license for one-line paragraphs. - The house voice is plain and declarative, and states the reason next to the rule. Match it in docs, comments, and commit bodies alike.