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

migrate-kit-to-v3

GitHub

指导将 Docker Sandbox Kit 从 v2 spec.yaml 迁移至 v3 OCI 描述符,涵盖构建、运行及合规性验证全流程。

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

Trigger Scenarios

迁移或移植 Kit 到 schemaVersion 3 将 v2 spec.yaml 转换为 v3 capabilities 询问如何构建、运行或检查 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 inline context
- [ ] 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. It says nothing about interactive verbs: declare agent-interactive-sessions@1 beside it only for verbs you have verified against the tool's real CLI, never from a pattern.

When the Kit can run an ACP agent, also declare acp@1 with optional: true, the unversioned agent provide name, and the complete launch command. A mixin may supply the command for an agent from another Kit. Verify the command against the installed tool; do not infer flags from another agent. This declares the launch command and its environment.

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 agent-context@2 with inline config.content; audit Kit placeholders before rebuilding
<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.

Volume declarations at one cleaned path merge across Kits only when their expanded settings match. Equivalent byte-size and octal-mode spellings match; unspecified size or mode differs from an explicit value. Block storage belongs to the owner scope, sandbox name, and cleaned path: re-create and delete followed by create under that name retain it across composition changes. Unselected paths remain unmounted until requested again, and incompatible retained settings refuse create or re-create. Explicit storage removal or a documented, visible retention policy ends its lifetime. Enforced block size is dedicated capacity additive to the root disk; runtimes disclose advisory sizing and durability guarantees. Fresh storage starts empty before hooks. Host mounts still have one Kit owner and conflict with volumes at the same destination. Re-exported volume text inputs remain separate contributions in a published set until create expands and selects their concrete requests, and so does any entry whose key, such as a port's transport, holds a re-exported reference. Do not replace a configurable size with its default to make a set merge.

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 and <kit>-mixin.dockerfile. Put guidance inline in agent-context@2 in both descriptors. 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

Create a named container builder once; the commands below select it explicitly:

docker buildx create --name kit-builder --driver docker-container --bootstrap

A publication through the docker driver with the classic image store can discard the descriptor's manifest annotation. OCI export support also depends on the image store. Use the container builder for exports and publication, then validate the artifact that actually left it.

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 --builder kit-builder . \
  -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 --builder kit-builder . -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 --builder kit-builder . -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 --builder kit-builder . -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 workload sandbox. Add --kit-arg to preview how args resolve. Recipe-based local builds use host Docker Buildx when Docker is available, otherwise the shared builder sandbox. The host builder needs OCI export support; select the named container builder with BUILDX_BUILDER=kit-builder sbx kit inspect ./<kit>, or require the sandbox backend with SBX_KIT_BUILDER=sandbox. SBX_KIT_BUILDER=host requires the host backend. Declaration-only mixins need neither. sbx kit builder status, history, and removal describe only the sandbox backend; removing it does not clear host-builder caches.

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 --builder kit-builder . -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. Missing index annotations alone do not invalidate a Kit whose platform manifest carries the descriptor. An OCI layout's outer index.json catalog is separate from the image index it references.

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 --builder kit-builder . -f <kit>.yaml \
  --platform linux/amd64,linux/arm64 --provenance=mode=min --push \
  -t <registry>/sbx-kit-<kit>:<version> -t <registry>/sbx-kit-<kit>:latest
kit-tck validate <registry>/sbx-kit-<kit>:<version>

Validate after publication too: checks inside the frontend cannot see metadata lost by an exporter or registry.

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 an authored deb/ provide fails in step 4, not later. For retained agent-context@1, a missing contentFile fails there too. Neither catches a migration that drops an instruction body: compare the old text with inline @2 content and audit orphan @1 context files. Script those text and file 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.

Coupled optional features

Use a group when skipping a capability also needs to skip its hooks, files, or guidance. Put optional: true on the group, never on its members (even optional: false is invalid). Groups are nonempty and cannot nest; required and one-member groups are valid. See examples/optional-cache/optional-cache.yaml.

The selection API includes every member or none, before composition. A runtime supplies decisions on expanded entries; the API owns atomic selection and ordering. Validate all member configs even in skipped groups. Selected entries must still satisfy cross-entry rules, and a composition conflict is an error rather than a reason to skip another group. Singleton arity is per declaration block; lifecycle lists from selected blocks concatenate, and duplicate file paths are errors.

Published sets may carry declarationBlocks on ordinary items or one-member group wrappers. Preserve these descriptor-local IDs when reading a published descriptor: they keep duplicate checks within an original block after expansion, without coupling optional selection. A publisher remaps them per contribution occurrence, retains all IDs on reconciled concrete requests, and never derives them from diagnostic source metadata. Hand-authored Kits normally omit this field.

Selection lasts for one sandbox installation, including restarts. Recreation selects afresh. A failing selected hook is an execution failure, not optional unavailability. Skipping a group does not remove old files from reused volumes, omit image layers, or suppress argument environment exports. Group names are display labels, not merge keys.

Groups extend the unfinished schema-3 grammar in place. Descriptors using them require a reader implementing this extension; older strict readers reject them.

Final environment references

Use ${{ kit.env.HOME }} in capability configuration strings when a path depends on the final container environment. Image defaults compose first, argument env exports replace them, and runtime environment overrides win last. Expansion happens before validation and selection, including inside groups. Missing names fail; empty values remain empty. This is structural string substitution, not shell evaluation: $HOME, ${HOME}, and ~/ are unchanged. Do not use environment references in mapping keys or Kit metadata, and do not pass Kit placeholders inside argument or environment values. Persist the expanded descriptor for restart.

Migrating agent-context@1 to @2

Move a file-backed body's text into config.content and remove contentFile. Change the capability type to com.docker.sandbox/agent-context@2, audit ${{ kit.args.* }} and ${{ kit.env.* }} spellings that staged files previously left literal, and rebuild. $VAR and ${VAR} remain literal. No build-only include form exists. Keep large reference material in bundled files or skills and use short guidance pointing to it.

Migrate all selected contributors together: mixing @1 and @2 is an explicit composition error, including inline @1 requests. Profile placement and existing user instruction preservation remain; @2 writes combined guidance directly rather than @1's progressive per-Kit index. Runtime consumers must advertise @2 support. Serialized budgets apply after expansion and composition: warn above 64 KiB, fail above 512 KiB. @1 remains supported and is not deprecated by this migration.

Workspace and sandbox identity

A workload declaring sbx@1 receives runtime-owned WORKSPACE_DIR, SANDBOX_NAME, SANDBOX_ID, and IS_SANDBOX=1. Do not bake these into an image, export them from Kit arguments, or set them in shell profiles: the runtime's values win over image, argument, and process overrides. Treat SANDBOX_ID as opaque; it identifies the sandbox across restarts and container replacement, not a backing container or VM.

WORKSPACE_DIR is an absolute sandbox path when a workspace is assigned, including internal and cloned workspaces; it does not imply a host mount or finished preparation. It is unset with no workspace, and then a ${{ kit.env.WORKSPACE_DIR }} reference fails at create. A process cwd does not change the assigned workspace. Hooks list each of these inputs they consume in env; none is in lifecycle's automatic baseline.

Version History

  • 6838e3e Current 2026-11-07 09:59

    合并主分支以进行 sbx 环境审查;定义 sbx 工作区和身份环境,使工作区发现通过 sbx@1 实现。

  • 4d93245 2026-11-02 09:37

    文档澄清了 kit 发布和前端推广策略,补充了源码构建后端选择说明。

  • 210ccc3 2026-10-18 15:54

    文档调整:聚焦 ACP 能力于代理启动配置;修复声明块在发布时的保留问题;合并卷生命周期分支并添加 ACP 适配器能力。

  • b1c53cb 2026-09-27 10:21

Same Skill Collection

examples/review-skill/content/SKILL.md
skills/create-kit-v3/SKILL.md
skills/sbxenv/SKILL.md

Metadata

Files
0
Version
6838e3e
Hash
6a220a23
Indexed
2026-09-27 10:21

Главная - Вики-сайт
Copyright © 2011-2026 iteam. Current version is 2.156.0. UTC+08:00, 2026-11-08 10:23
浙ICP备14020137号-1