Agent Skills › docker/sandbox-kit-spec › migrate-kit-to-v3

migrate-kit-to-v3

GitHub

用于将Docker沙箱Kit从v2规范迁移至v3,涵盖描述符转换、构建、运行及合规性验证。适用于升级Kit版本、添加mixin变体或查询v3 Kit构建与测试流程的场景。

skills/migrate-kit-to-v3/SKILL.md docker/sandbox-kit-spec

Trigger Scenarios

迁移Kit到v3 转换v2 spec.yaml 构建和运行v3 Kit 进行合规性检查

Install

npx skills add docker/sandbox-kit-spec --skill migrate-kit-to-v3 -g -y
More Options

Use without installing

npx skills use docker/sandbox-kit-spec@migrate-kit-to-v3

指定 Agent (Claude Code)

npx skills add docker/sandbox-kit-spec --skill migrate-kit-to-v3 -a claude-code -g -y

安装 repo 全部 skill

npx skills add docker/sandbox-kit-spec --all -g -y

预览 repo 内 skill

npx skills add docker/sandbox-kit-spec --list

SKILL.md

Frontmatter
{
    "name": "migrate-kit-to-v3",
    "description": "Migrates a Docker sandbox kit from the v2 `spec.yaml` grammar to the v3 kit descriptor, then builds it with docker buildx, runs it with sbx, and verifies it with the kit-tck conformance suite. Use when migrating or porting a kit to schemaVersion 3, when converting a v2 spec.yaml \/ sandbox.image \/ setup hooks \/ permissions block into v3 capabilities, when adding a `-mixin` variant of a workload kit, or when asked how to build, run, or conformance-check a v3 kit."
}

Migrate a kit to v3

A v2 kit is one spec.yaml plus a Dockerfile that builds a named image. A v3 kit is one OCI image: the descriptor declares what the kit needs as typed capabilities and rides in a manifest annotation, the image config carries the runtime contract (entrypoint, env, user, workdir), and the layers carry the content.

Tooling

Three published tools, nothing repo-local:

  • docker buildx builds kits. Nothing to install for the kit frontend — BuildKit pulls docker/sandbox-kit:3 from the descriptor's # syntax= line.

  • sbx runs them. Install the stable CLI from Docker Docs / sbx-releases; current releases support Kits v3 in local and cloud mode.

  • kit-tck judges conformance: go install github.com/docker/sandbox-kit-spec/v3/cmd/kit-tck@latest, or take an archive from the releases page. (A go install of an untagged ref reports dev; @v3.x.y and the release archives both carry the real version, which internal/version reads from the build info.)

    This repository is private, so both routes need access to it. The public module proxy cannot serve it — proxy.golang.org answers 404 — so go install resolves direct and needs a git credential plus GOPRIVATE=github.com/docker/*. In CI that means a token with read access, and a fork's token does not have one: a fork can still validate a descriptor by building it, since the frontend validates during the build, but it cannot run kit-tck. If the repository becomes public, that whole constraint disappears.

Authorities

In precedence order:

  1. spec/ — the Go implementation. Where docs and code disagree, code wins.
  2. SPEC-v3.md — the grammar (§3 authoring forms, §4 top-level fields, §5 provides/requires, §6 args, §7 capabilities, §8 launch modes).
  3. capability pages — one per capability type, normative for its config schema and runtime behavior.
  4. examples — worked kits. claude and claude-mixin are the migration of a real v2 kit into both shapes; read them first.

Working inside the spec repo, all four are also on disk at spec/, docs/spec/ and examples/.

Workflow

Track progress with this checklist:

- [ ] 1. Read the v2 kit end to end, including its README and testdata
- [ ] 2. Write the v3 descriptor, recipe, and context file
- [ ] 3. Add the -mixin variant (workload kits only)
- [ ] 4. Validate the descriptor (fails in seconds on a bad field)
- [ ] 5. Build the kit
- [ ] 6. Run it with sbx and exercise the agent
- [ ] 7. Verify with kit-tck
- [ ] 8. Publish, and delete the CI that published the v2 pair

1. Read the v2 kit first

Read spec.yaml, Dockerfile, README.md and testdata/tck.yaml before writing anything. v2 kits carry their reasoning in comments, and that prose is the most valuable thing to carry across. testdata/tck.yaml is the only record of whether a working non-interactive invocation was ever established (promptArgs), which decides whether the migrated kit declares agent-sessions@1.

2. Write the v3 files

For a kit named <kit>, migrating in place:

v2 v3
<kit>/spec.yaml <kit>/<kit>.yaml, first line # syntax=docker/sandbox-kit:3
<kit>/Dockerfile <kit>/<kit>.dockerfile — found by filename stem, so no dockerfile: field
agentInstructions.content <kit>/<kit>-context.md, referenced as contentFile: ./<kit>-context.md
<kit>/testdata/tck.yaml delete — v2-only harness
<kit>/.dockerignore keep and audit. BuildKit still applies it to a v3 kit's build context, so deleting it puts back whatever it was excluding — secrets and large generated files included. Drop only the entries that named v2 files.
<kit>/<kit>_tck_test.go delete — it loads the v2 spec.yaml through tck.NewSuiteFromDir(".") and cannot compile once that file is gone

The complete field-by-field mapping, the capability rules, and the gotcha list are in FIELD-MAPPING.md. Read it before writing the descriptor.

Migrate faithfully: preserve every declared host, credential, hook, volume, port, env var and instruction, and keep base images verbatim. Where v3 cannot express something, or where a v2 declaration turns out to be dead config, mark it with an inline # MIGRATION NOTE: comment rather than dropping it silently — the claude example shows the convention.

Faithful does not mean literal in one respect: a v2 install hook is often a v2 limitation rather than a v3 requirement, because a v2 mixin had no way to ship content. Decide per hook whether it becomes a layer — the rule, and the five cases where a hook is still correct, are in FIELD-MAPPING.md.

3. Add the -mixin variant

Every workload kit gets a sibling <kit>-mixin/ holding <kit>-mixin.yaml, <kit>-mixin.dockerfile and a context file. Name that last one <kit>-context.md, which is what six of the seven example mixins do — the staged name only has to avoid kit.yaml and kit.dockerfile, so the -mixin- infix buys nothing and the repository is already near-unanimous. The mixin declares the same credentials, network policy, volumes and hooks, minus what only the kit that owns the environment can carry. See the mixin variants section for the exact subtractions and the overlay recipe patterns.

4. Validate the descriptor

The frontend decodes and validates the descriptor before it builds any content, so an export-less build is the fast loop while the descriptor is wrong:

cd <kit> && docker buildx build . -f <kit>.yaml --output type=cacheonly

A malformed descriptor fails in about a second, naming the offending field and its line. Be clear about what cacheonly does, though: it suppresses the export, not the build. Once the descriptor is valid the frontend goes on to solve the whole recipe — downloads, installs and all — so this is fail-fast for bad input rather than a validation-only step. Iterate here until it is clean: a malformed descriptor still costs only the second it takes to reject, even though a valid one costs the whole recipe.

Pass build-phase args by the kit's arg name, not the buildArg name the recipe sees, and supply anything declared required or validation fails:

docker buildx build . -f <kit>.yaml --build-arg version=2.99.0 --output type=cacheonly

Because the recipe is solved too, a bad FROM or COPY --from surfaces here rather than waiting for step 5 — you just do not get an image out of it.

5. Build the kit

Swap the export for --load to put an ordinary tagged image in the local store:

docker buildx build . -f <kit>.yaml -t <kit>-kit:<tag> --load

--load is not optional on the docker-container driver, which is what buildx create gives you: without an output the result stays in the builder's cache and docker run <kit>-kit:<tag> reports no such image.

To judge the artifact with kit-tck without a registry, export an OCI layout directory instead:

docker buildx build . -f <kit>.yaml -t <kit>-kit:<tag> \
  --output type=oci,dest=/tmp/<kit>-layout,tar=false

A workload's recipe must build on a base carrying the platform floor — bash, the agent user (uid 1000), git, a CA store — which the hardened dhi.io/sbx-templates:* images carry. A bare distro base builds fine and fails at agent launch.

6. Run it with sbx

Point sbx at the kit directory — the runtime builds source-form kits on demand, keyed by source hash, so this needs no registry and no push:

sbx run ./<kit> .                            # a workload kit
sbx run ./<workload> --kit ./<kit>-mixin .   # a mixin, composed onto a workload

A mixin cannot run alone; compose it onto the migrated workload or onto a shell workload. Pass kit args with --kit-arg name=value (or --kit-arg kit.name=value to target one kit), and bind a credential the kit declares with sbx secret set <service> before expecting authenticated calls to work. (The -g flag older docs show is deprecated; global is the default now.)

Inside the sandbox, the kit is self-describing — use it to check that what you declared is what arrived:

cat /usr/share/sandbox/kit/<kit>/kit.yaml        # the published descriptor
cat /usr/share/sandbox/kit/<kit>/kit.dockerfile  # the recipe that built it
cat /var/log/sbx-kit-startup.log                 # startup hook output

Then exercise the kit for real: run the agent's own version command, confirm install hooks left what they should, confirm a declared volume is writable by agent, and confirm an undeclared host is refused while a declared one is not.

sbx kit inspect ./<kit> builds the source kit and prints its resolved declarations (kind, network counts, credentials, args) without starting a sandbox. Add --kit-arg to preview how args resolve. The first source build creates the shared builder sandbox and is slow; sbx kit builder status shows it and sbx kit builder rm reclaims the cache.

For the published path instead of the local loop, push the kit as an ordinary image and run it by reference — a kit image that only exists in the local Docker store cannot run, because the runtime resolves kit images from registries:

docker buildx build . -f <kit>.yaml --push -t docker.io/<you>/sbx-kit-<kit>:<tag> \
  --platform linux/amd64,linux/arm64 --provenance=true
sbx run docker.io/<you>/sbx-kit-<kit>:<tag> .

7. Verify with kit-tck

kit-tck judges an artifact's annotations, layers, staged sources and image config, with every check linked to the clause it enforces. The same checks run inside the frontend during a build, so running them here is how an artifact changed by an exporter or a registry on its way out gets judged — and how a kit this frontend did not build gets judged at all.

kit-tck validate --layout /tmp/<kit>-layout <tag>     # the OCI layout from step 5
kit-tck validate docker.io/<you>/sbx-kit-<kit>:<tag>  # a published kit

For the layout form, <tag> is the tag alone as the layout records it (1.0.0), not the full <kit>-kit:1.0.0 reference the build was tagged with.

Add --verbose to list the checks that passed, --format json for every check with its spec link, and --plain-http for a registry served over HTTP.

A published kit built on a multi-node builder warns that the index carries no kit annotations. That is expected, not a defect: a multi-node build merges per-node results into a fresh index client-side, which dissolves them, and §9.3 requires consumers to fall back to the platform manifest, which does carry them. The verdict is still ✓ conforms. A single-node build shows no warning.

Runtime conformance is a separate suite, for people implementing a runtime rather than authoring a kit: kit-tck runtime --adapter <path> drives hundreds of sandbox lifecycles against an adapter implementing conformance.md. It runs long — the bare command defaults --timeout to 30m and fails when that expires, so a real run needs something like --timeout 2h — and migrating a kit does not need it at all.

8. Publish

One artifact means one push. A v2 kit pointed sandbox.image at an image published separately from the kit itself, so shipping a change meant building and pushing both and keeping the reference between them honest. In v3 the recipe's FROM builds the content, the frontend annotates it, and the result in the registry is the whole kit:

docker buildx build . -f <kit>.yaml --platform linux/amd64,linux/arm64 --push \
  -t <registry>/sbx-kit-<kit>:<version> -t <registry>/sbx-kit-<kit>:latest

Audit the CI that published the v2 pair. A pipeline built around two artifacts does not fail once there is only one — it keeps pushing an image nothing references now that sandbox.image is gone, and the step that packed the kit has nothing left to pack. Both halves get deleted, not rewired.

Check what the existing tag actually names. A repo publishing a family of kits into one repository tags them by name (…/sbx-kits:<kit>), which is a tag that says nothing about which build it points at. Carried into v3 unchanged and paired with a descriptor that never set version:, it yields a published kit with no version anywhere in it. Add <kit>-<version> beside the bare name, and set version: regardless — the joined tag is not version-shaped, so it cannot stand in for the field.

Signing is optional, unchanged by the migration, and described with the rest of the publish flow in create-kit-v3.

What actually catches bugs

Each check below caught real defects in a migration of 87 kits that the cheaper checks above it did not. They are ordered by what they cost.

  1. Validation catches malformed descriptors, and the build catches more than you would guess: readContextFile and RequireAuthoredProvides both run before the content loop, so a contentFile: naming a missing file and an authored deb/ provide fail in step 4, not later. What neither sees is a *-context.md no descriptor references, or a v2 instruction body that was dropped — both fail at nothing at all. Script those two file-level audits; they take seconds and they found a kit whose instructions would silently never have reached the agent.
  2. Building catches recipes. It does not prove the content works.
  3. Reading the exported layer catches ownership, in two parts. Export with --output type=oci,dest=<dir>,tar=false and run kit-tck validate --layout <dir> <tag>: overlay-home-ownership fails an overlay whose /home is not root's or whose /home/agent is not uid 1000's, which is what six of the migrated overlays got wrong. Then count every other owner: for b in <dir>/blobs/sha256/*; do tar --numeric-owner -tvf "$b"; done | awk '{print ($2 ~ /\//) ? $2 : $3"/"$4}' | sort | uniq -c. Every count must be 0/0 or 1000/1000; that is what found four overlays shipping files owned by package publishers' uids. The awk reads GNU tar's joined 0/0 field or bsdtar's split pair, because a pipeline written for either silently reports the other's size and date.
  4. Composing the overlay and running the tool catches the rest. Three mixins shipped dangling symlinks whose build-time test -x passed because the real tree was still present in the build stage: the installer had relocated a launcher but not its payload. kit-tck's overlay-links-resolve now warns about those, but only the composition shows whether a link the overlay cannot resolve by itself finds its target on the base. Compose it through the assembler, which is what merges the overlay's ENV and PATH and puts it on a base carrying the platform floor: sbx run ./<workload> --kit ./<kit> --detached --name t ., then sbx exec t tool --version. Use sbx exec and not sbx run … -- tool: arguments after -- are agent arguments and never run the binary. The --load plus throwaway COPY --from=<overlay> / / trick is faster and finds the same dangling symlinks, but it transfers files only — the image config is dropped, so a mixin relying on its own ENV fails there for a reason the assembler would not produce. Full detail in Verifying an overlay.
  5. kit-tck judges the published artifact — see step 7.

When you change ownership, re-run step 4, not just step 3. A chown that fixes the numbers can still break the tool.

Known gaps

  • sbx kit validate does not accept a v3 source kit: its load path has no kit builder configured. Validate with the build in step 4, and inspect the built artifact with sbx kit inspect.
  • A migration strands every script, workflow and test that globs the old layout. Audit them as part of the work: discovery globbing */spec.yaml returns nothing, so CI passes by building nothing at all, which is the worst failure mode available.

Migration conventions

These are the conventions the sbx-kits-contrib v3 migration followed. Keep them unless the task says otherwise:

  • Migrate in place and delete the v2 spec.yaml and Dockerfile once the v3 pair exists.
  • Keep each kit's base images verbatim; a grammar migration is not the moment to re-point a base.
  • Keep README.md, updating the filenames and any v2 grammar it quotes; keep README.image.md; delete testdata/tck.yaml. Keep .dockerignore — it still governs the v3 build context — and prune only the entries that named v2 files.
  • Heavily commented YAML is the house style. Carry the v2 comments across — they are the reasoning behind the declarations.

Version History

  • b1c53cb Current 2026-09-27 10:21

Same Skill Collection

skills/create-kit-v3/SKILL.md

Metadata

Files
0
Version
b1c53cb
Hash
adbf2cb9
Indexed
2026-09-27 10:21

Home - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-09-28 01:03
浙ICP备14020137号-1