diff --git a/.gitattributes b/.gitattributes index b6148e1..2e4a502 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,6 @@ # Preserve the exact bytes used by aeoess-aps fixture checksums. integrations/aeoess-aps/fixtures/delegation-chain/*.json text eol=lf integrations/aeoess-aps/fixtures/action-receipt/*.json text eol=lf + +# Registry bytes; the tests check them against the digests Docker Hub serves. +integrations/docker-sandbox-kit/fixtures/*.json -text diff --git a/.github/workflows/docker-sandbox-kit-conformance.yml b/.github/workflows/docker-sandbox-kit-conformance.yml new file mode 100644 index 0000000..863db1f --- /dev/null +++ b/.github/workflows/docker-sandbox-kit-conformance.yml @@ -0,0 +1,59 @@ +name: docker-sandbox-kit conformance +on: + push: + paths: + - "integrations/docker-sandbox-kit/**" + - ".github/workflows/docker-sandbox-kit-conformance.yml" + pull_request: + paths: + - "integrations/docker-sandbox-kit/**" + - ".github/workflows/docker-sandbox-kit-conformance.yml" + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + conformance: + strategy: + fail-fast: false + matrix: + python: ["3.11", "3.12", "3.13"] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: ${{ matrix.python }} + - name: Install released TRACE packages + working-directory: integrations/docker-sandbox-kit + run: python -m pip install -r requirements.txt + # Builds, signs and verifies records from two published Kits, and checks + # every refusal path. + - name: Integration tests + working-directory: integrations/docker-sandbox-kit + run: python -m pytest -q + - name: TRACE Level 0 + working-directory: integrations/docker-sandbox-kit + run: | + python - <<'PY' + import json, pathlib + from agentrust_trace import generate_key, key_to_jwk, sign_record + from kit_to_trace import KitEvidence, build_from_kit + key = generate_key() + raw = pathlib.Path("fixtures/claude-acp-set-1.0.1.amd64.manifest.json").read_bytes() + record = build_from_kit( + KitEvidence.from_manifest(raw), + subject="spiffe://example.org/agent/claude-acp", + model_provider="anthropic", + model_id="claude-sonnet-4-6", + workload_digest="sha256:" + "e" * 64, + jwk=key_to_jwk(key), + kit_reference="docker.io/docker/sbx-kit-claude-acp-set", + ) + pathlib.Path("signed-record.json").write_text(json.dumps(sign_record(record, key))) + PY + trace-tests verify --record signed-record.json --level 0 diff --git a/README.md b/README.md index 98d6993..f6956ae 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ TRACE only works as a standard if it is genuinely neutral. Integrations are list | [comply54](integrations/comply54/) | comply54 | trace | community | | [ComputeID AgentPassport TRACE Adapter](integrations/computeid-agentpassport-trace/) | ComputeID | trace | community | | [DecisionAssure](integrations/decisionassure/) | DecisionAssure (a1k7) | trace | community | +| [Docker Sandbox Kit](integrations/docker-sandbox-kit/) | agentrust-io | trace | community | | [EPI Recorder](integrations/epilabs-epi-recorder/) | EPI Labs | trace, wcm | verified | | [Google ADK](integrations/google-adk/) | agentrust-io | trace | verified | | [LangChain](integrations/langchain/) | agentrust-io | trace | verified | diff --git a/integrations/docker-sandbox-kit/README.md b/integrations/docker-sandbox-kit/README.md new file mode 100644 index 0000000..72080be --- /dev/null +++ b/integrations/docker-sandbox-kit/README.md @@ -0,0 +1,99 @@ +# Docker Sandbox Kit to TRACE + +A [Docker Sandbox Kit](https://github.com/docker/sandbox-kit-spec) is one OCI +image whose manifest annotation `vnd.docker.sandbox.kit.descriptor` lists what +the agent inside it asks to reach: network allow and deny rules by phase, +credentials, volumes, ports. This adapter takes a published Kit's platform +manifest and builds a TRACE Level 0 Trust Record whose `policy.bundle_hash` is +the digest of that descriptor, byte for byte as the frontend published it. + +Written against [SPEC-v3](https://github.com/docker/sandbox-kit-spec/blob/main/docs/spec/SPEC-v3.md) +at commit `31791ea` (2026-10-01). + +## Run it + +Fetch the platform manifest exactly as the registry serves it. The digest is +over these bytes, so a pretty-printed copy will not match. For a public Docker +Hub Kit: + +```bash +REPO=docker/sbx-kit-claude-acp-set +TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$REPO:pull" | jq -r .token) +curl -s -H "Authorization: Bearer $TOKEN" \ + -H "Accept: application/vnd.oci.image.manifest.v1+json" \ + "https://registry-1.docker.io/v2/$REPO/manifests/sha256:86d56a3b2ea714d55c2a48b55b9ca64e2145245f43583100dd944eccfb8fe78e" > kit.json +``` + +The platform digest comes from the Kit's image index, one entry per platform. + +Then build the unsigned record: + +```bash +pip install -r integrations/docker-sandbox-kit/requirements.txt +python integrations/docker-sandbox-kit/kit_to_trace.py kit.json \ + --expected-digest sha256:86d56a3b2ea714d55c2a48b55b9ca64e2145245f43583100dd944eccfb8fe78e \ + --kit-reference docker.io/docker/sbx-kit-claude-acp-set \ + --subject spiffe://example.org/agent/claude-acp \ + --model-provider anthropic --model-id claude-sonnet-4-6 \ + --workload-digest sha256: \ + --jwk pubkey.jwk > record.json +``` + +Pass the result to `agentrust_trace.sign_record`. Signing is kept separate from +assembly. + +## What the record claims + +| TRACE field | Value | +|---|---| +| `policy.bundle_hash` | SHA-256 of the published descriptor annotation | +| `policy.enforcement_mode` | `declared`, always | +| `policy.policy_uri` | `oci://@`, when `--kit-reference` is given | +| `origin.kind` | `third-party-control-plane` | +| `origin.producer` | The frontend named in `vnd.docker.sandbox.kit.built-by`, else `docker/sandbox-kit` | +| `origin.source_event_id` | The Kit manifest digest | +| `build_provenance.digest` | The digest the caller supplies for the image that ran | +| `runtime.platform` | `software-only` | +| `appraisal` | `none`, stated by this adapter | + +**Why `declared`.** The descriptor is a request. What a host granted is the +permission surface of the effective descriptor, with this installation's +create-phase args expanded, and it lives in the runtime's lock (SPEC-v3 +sections 7.4 and 10). The image carries neither, so a record built from it can +name the policy the Kit asked for and claim nothing about enforcement. The +adapter takes no mode argument. A record claiming `enforce` needs evidence from +the runtime that it applied this descriptor. + +**Why the Kit digest is not the workload digest.** SPEC-v3 section 10: "What +runs is never a published artifact." The assembler emits an ordinary image +identified by the lock, and that is the digest `build_provenance.digest` +needs. The Kit digest identifies where the policy came from. + +## Failure behavior + +The adapter builds no record when the input is an image index, an artifact +manifest, an image without the descriptor annotation, a v2 Kit, a `set`, a +descriptor that is not JSON, a schema version other than 3 or one that +disagrees with its annotation, or a `vnd.docker.sandbox.kit.capabilities` +index that does not mirror the descriptor's capability types. With +`--expected-digest`, it also refuses bytes that hash to anything else. The CLI +exits 2 and says which check failed. + +## Tests and conformance + +The fixtures are the registry bytes of `docker/sbx-kit-claude-acp-set` 1.0.1 +and `docker/doodle` 2026 (linux/amd64), with the index for the refusal case. + +```bash +pip install -r integrations/docker-sandbox-kit/requirements.txt +python -m pytest integrations/docker-sandbox-kit -q +``` + +The suite builds, schema-checks, signs and verifies a record from both Kits +with the released packages pinned in `requirements.txt`, and checks every +refusal above. A signed record from either Kit passes +`trace-tests verify --level 0`. + +`agentrust-trace-adapters` 0.1.1 predates the `declared` mode, so the adapter +passes `build_record` a small policy object of its own. It switches to +`PolicyEvidence` once a release includes that mode. diff --git a/integrations/docker-sandbox-kit/fixtures/claude-acp-set-1.0.1.amd64.manifest.json b/integrations/docker-sandbox-kit/fixtures/claude-acp-set-1.0.1.amd64.manifest.json new file mode 100644 index 0000000..3dd3d68 --- /dev/null +++ b/integrations/docker-sandbox-kit/fixtures/claude-acp-set-1.0.1.amd64.manifest.json @@ -0,0 +1,88 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:f574f4e645a69af730bea2cefa2a1edc241335cfaf7b1453c54e8821d851d039", + "size": 5657 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+zstd", + "digest": "sha256:b20d35759a3fdbfb296f1e1d24bad9cbaedfcf588a8433dcedb3535319426f24", + "size": 509810173 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:d44b2c508e5d42d6bd9ae06116e44b34344208f8e07b859cb2c1311032e99502", + "size": 3474 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:122927cf9707ae12104f6599c70ae640fc0d3dedb2ec3436b8f0e64ff4fb1ff2", + "size": 108251415 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:e5a0ef53424dd1ef8dbca427f85e3e3bb229ffee5d29f879a6ac2c0a7f6d34be", + "size": 495 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:5437e0152b4f29f6032685855c5d5fb4b3360be6dbad6983e07404decb41e627", + "size": 3989 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:7ccecef549fd6811a690e30e734b57f325b42887ed9db22fb270aa4490992bd1", + "size": 40790101 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:f2d0e8fbf9878f0459b2b85f0dabe1c0f5dcf478781582b60ef94f58c3449f7d", + "size": 117190874 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:4d24a4f148d2c6809fb72b893f9d5ed42802bad1e5a384d14529882f583152a9", + "size": 1350 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:e2b3ae7b251c65862b2f025a9518fae34da2eba7c86fc3c89630188de29d3640", + "size": 1387 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:fe9ecd71e1822b294e9da1d125f907cd5c6924fe345e2d3ccf316a4e328aaf2d", + "size": 778 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:78621ac2b63c4f29f18d8f70fac1b0d68c626343e236161646360c196a641c60", + "size": 2312 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:0c558c82ba28b0205cb3161ec14be910efefa0c389c38381090d452a6227fcb9", + "size": 1531 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:d075d71c4975cf94068e21d8acb794a7df0e7cf5d5b7559ce96538cd8d83c136", + "size": 4534 + } + ], + "annotations": { + "org.opencontainers.image.authors": "Docker, Inc.", + "org.opencontainers.image.description": "A sandbox an ACP client can drive: a shell base, Claude Code, and the Agent Client Protocol adapter that fronts it — published as one kit. The same three kits compose by hand; this exists so an editor can be pointed at one reference instead of three that have to agree.", + "org.opencontainers.image.licenses": "Apache-2.0", + "org.opencontainers.image.source": "https://github.com/docker/sandbox-kit-spec", + "org.opencontainers.image.title": "Claude over ACP", + "org.opencontainers.image.version": "1.0.1", + "vnd.docker.sandbox.kit.built-by": "{\"name\":\"docker/sandbox-kit\",\"version\":\"dev\",\"revision\":\"56ddfa629076cc4509094290a59a38aed3aa59c9-dirty\"}", + "vnd.docker.sandbox.kit.capabilities": "com.docker.sandbox/agent-context@1,com.docker.sandbox/agent-skills@1,com.docker.sandbox/credential@1,com.docker.sandbox/lifecycle@1,com.docker.sandbox/network-policy@1,com.docker.sandbox/sbx@1,com.docker.sandbox/volume@1", + "vnd.docker.sandbox.kit.descriptor": "{\"schemaVersion\":\"3\",\"displayName\":\"Claude over ACP\",\"author\":\"Docker, Inc.\",\"description\":\"A sandbox an ACP client can drive: a shell base, Claude Code, and the Agent Client Protocol adapter that fronts it — published as one kit. The same three kits compose by hand; this exists so an editor can be pointed at one reference instead of three that have to agree.\",\"sourceUrl\":\"https://github.com/docker/sandbox-kit-spec\",\"version\":\"1.0.1\",\"licenses\":[\"Apache-2.0\"],\"kind\":\"workload\",\"provides\":[\"claude@2.1.285\",\"claude-acp@0.84.0\",\"deb/adduser@3.152\",\"deb/apt@3.0.3\",\"deb/base-files@13.8\",\"deb/base-passwd@3.6.7\",\"deb/bash@5.2.37\",\"deb/bc@1.07.1\",\"deb/bind9-dnsutils@9.20.26\",\"deb/bind9-host@9.20.26\",\"deb/bind9-libs@9.20.26\",\"deb/binutils-common@2.44\",\"deb/binutils@2.44\",\"deb/bsdextrautils@2.41.5\",\"deb/bubblewrap@0.12.0\",\"deb/ca-certificates-java@20240118\",\"deb/ca-certificates@20250419\",\"deb/containerd.io@2.3.5\",\"deb/coreutils@9.7\",\"deb/cpp-14@14.2.0\",\"deb/cpp@14.2.0\",\"deb/curl@8.14.1\",\"deb/dash@0.5.12\",\"deb/debconf@1.5.91\",\"deb/debian-archive-keyring@2025.1\",\"deb/debianutils@5.23.2\",\"deb/default-jdk-headless@1.21\",\"deb/default-jre-headless@1.21\",\"deb/diffutils@3.10\",\"deb/dirmngr@2.4.7\",\"deb/docker-buildx-plugin@0.37.1\",\"deb/docker-ce-cli@29.8.1\",\"deb/docker-ce@29.8.1\",\"deb/docker-compose-plugin@5.5.1\",\"deb/dpkg@1.22.22\",\"deb/findutils@4.10.0\",\"deb/gcc-14-base@14.2.0\",\"deb/gcc-14@14.2.0\",\"deb/gcc@14.2.0\",\"deb/gh@2.100.0\",\"deb/git@2.47.3\",\"deb/gnu-which@2.21\",\"deb/gnupg-l10n@2.4.7\",\"deb/gnupg@2.4.7\",\"deb/golang-1.26-oci-config@1.26.8\",\"deb/golang-1.26@1.26.8\",\"deb/gpg-agent@2.4.7\",\"deb/gpg@2.4.7\",\"deb/gpgconf@2.4.7\",\"deb/gpgsm@2.4.7\",\"deb/gpgv@2.4.7\",\"deb/grep@3.11\",\"deb/groff-base@1.23.0\",\"deb/gzip@1.13\",\"deb/htop@3.4.1\",\"deb/iproute2@6.15.0\",\"deb/iptables@1.8.11\",\"deb/iputils-ping@20240905\",\"deb/java-common@0.76\",\"deb/jq@1.8.2\",\"deb/less@668\",\"deb/libacl1@2.3.2\",\"deb/libapparmor1@4.1.0\",\"deb/libapt-pkg7.0@3.0.3\",\"deb/libasan8@14.2.0\",\"deb/libassuan9@3.0.2\",\"deb/libatomic1@14.2.0\",\"deb/libattr1@2.5.2\",\"deb/libaudit-common@4.0.2\",\"deb/libaudit1@4.0.2\",\"deb/libbinutils@2.44\",\"deb/libblkid1@2.41.5\",\"deb/libbpf1@1.5.0\",\"deb/libbrotli1@1.1.0\",\"deb/libbsd0@0.12.2\",\"deb/libbz2-1.0@1.0.8\",\"deb/libc-bin@2.41\",\"deb/libc-dev-bin@2.41\",\"deb/libc-l10n@2.41\",\"deb/libc6-dev@2.41\",\"deb/libc6@2.41\",\"deb/libcap-ng0@0.8.5\",\"deb/libcap2-bin@2.75\",\"deb/libcap2@2.75\",\"deb/libcbor0.10@0.10.2\",\"deb/libcc1-0@14.2.0\",\"deb/libcom-err2@1.47.2\",\"deb/libcrypt-dev@4.4.38\",\"deb/libcrypt1@4.4.38\",\"deb/libctf-nobfd0@2.44\",\"deb/libctf0@2.44\",\"deb/libcurl4t64@8.14.1\",\"deb/libdb5.3t64@5.3.28\",\"deb/libdebconfclient0@0.280\",\"deb/libedit2@3.1\",\"deb/libelf1t64@0.192\",\"deb/liberror-perl@0.17030\",\"deb/libevent-core-2.1-7t64@2.1.13\",\"deb/libexpat1@2.8.3\",\"deb/libffi8@3.4.8\",\"deb/libfido2-1@1.15.0\",\"deb/libfstrm0@0.6.1\",\"deb/libgcc-14-dev@14.2.0\",\"deb/libgcc-s1@14.2.0\",\"deb/libgcrypt20@1.11.0\",\"deb/libgdbm-compat4t64@1.24\",\"deb/libgdbm6t64@1.24\",\"deb/libgmp10@6.3.0\",\"deb/libgnutls30t64@3.8.9\",\"deb/libgomp1@14.2.0\",\"deb/libgpg-error0@1.51\",\"deb/libgpm2@1.20.7\",\"deb/libgprofng0@2.44\",\"deb/libgssapi-krb5-2@1.21.3\",\"deb/libhogweed6t64@3.10.1\",\"deb/libhwasan0@14.2.0\",\"deb/libidn2-0@2.3.8\",\"deb/libip4tc2@1.8.11\",\"deb/libip6tc2@1.8.11\",\"deb/libisl23@0.27\",\"deb/libitm1@14.2.0\",\"deb/libjansson4@2.14\",\"deb/libjemalloc2@5.3.0\",\"deb/libjpeg62-turbo@2.1.5\",\"deb/libjq1@1.8.2\",\"deb/libjson-c5@0.18\",\"deb/libk5crypto3@1.21.3\",\"deb/libkeyutils1@1.6.3\",\"deb/libkrb5-3@1.21.3\",\"deb/libkrb5support0@1.21.3\",\"deb/libksba8@1.6.7\",\"deb/liblastlog2-2@2.41.5\",\"deb/liblcms2-2@2.16\",\"deb/libldap2@2.6.10\",\"deb/liblmdb0@0.9.31\",\"deb/liblsan0@14.2.0\",\"deb/liblsof0@4.99.4\",\"deb/liblz4-1@1.10.0\",\"deb/liblzma5@5.8.1\",\"deb/libmaxminddb0@1.12.2\",\"deb/libmd0@1.1.0\",\"deb/libmnl0@1.0.5\",\"deb/libmount1@2.41.5\",\"deb/libmpc3@1.3.1\",\"deb/libmpfr6@4.2.2\",\"deb/libncursesw6@6.5\",\"deb/libnetfilter-conntrack3@1.1.0\",\"deb/libnettle8t64@3.10.1\",\"deb/libnfnetlink0@1.0.2\",\"deb/libnftables1@1.1.3\",\"deb/libnftnl11@1.2.9\",\"deb/libnghttp2-14@1.64.0\",\"deb/libnghttp3-9@1.8.0\",\"deb/libnpth0t64@1.8\",\"deb/libnss3@3.110\",\"deb/libonig5@6.9.9\",\"deb/libp11-kit0@0.25.5\",\"deb/libpam-modules-bin@1.7.0\",\"deb/libpam-modules@1.7.0\",\"deb/libpam-runtime@1.7.0\",\"deb/libpam0g@1.7.0\",\"deb/libpcre2-8-0@10.46\",\"deb/libpcsclite1@2.3.3\",\"deb/libperl5.40@5.40.1\",\"deb/libpipeline1@1.5.8\",\"deb/libproc2-0@4.0.4\",\"deb/libprotobuf-c1@1.5.1\",\"deb/libpsl5t64@0.21.2\",\"deb/libpython-3.14-minimal@3.14.7\",\"deb/libpython-3.14-stdlib@3.14.7\",\"deb/libpython-3.14@3.14.7\",\"deb/libreadline8t64@8.2\",\"deb/librtmp1@2.4\",\"deb/libsasl2-2@2.1.28\",\"deb/libsasl2-modules-db@2.1.28\",\"deb/libseccomp2@2.6.0\",\"deb/libselinux1@3.8.1\",\"deb/libsemanage-common@3.8.1\",\"deb/libsemanage2@3.8.1\",\"deb/libsepol2@3.8.1\",\"deb/libsframe1@2.44\",\"deb/libsmartcols1@2.41.5\",\"deb/libsodium23@1.0.18\",\"deb/libsqlite3-0@3.46.1\",\"deb/libssh2-1t64@1.11.1\",\"deb/libssl3t64@3.5.7\",\"deb/libstdc++6@14.2.0\",\"deb/libsystemd0@257.13\",\"deb/libtasn1-6@4.20.0\",\"deb/libtinfo6@6.5\",\"deb/libtirpc-common@1.3.6\",\"deb/libtirpc3t64@1.3.6\",\"deb/libtsan2@14.2.0\",\"deb/libubsan1@14.2.0\",\"deb/libuchardet0@0.0.8\",\"deb/libudev1@257.13\",\"deb/libunistring5@1.3\",\"deb/liburcu8t64@0.15.2\",\"deb/libuuid1@2.41.5\",\"deb/libuv1t64@1.50.0\",\"deb/libwrap0@7.6\",\"deb/libxml2@2.12.7\",\"deb/libxtables12@1.8.11\",\"deb/libxxhash0@0.8.3\",\"deb/libzstd1@1.5.7\",\"deb/linux-libc-dev@6.12.107\",\"deb/locales@2.41\",\"deb/login.defs@4.17.4\",\"deb/lsof@4.99.4\",\"deb/make@4.4.1\",\"deb/man-db@2.13.1\",\"deb/mawk@1.3.4\",\"deb/nano@8.4\",\"deb/ncurses-base@6.5\",\"deb/ncurses-bin@6.5\",\"deb/net-tools@2.10\",\"deb/netbase@6.5\",\"deb/netcat-openbsd@1.229\",\"deb/nftables@1.1.3\",\"deb/node-corepack@0.36.0\",\"deb/nodejs-24@24.21.0\",\"deb/npm@11.19.1\",\"deb/openjdk-21-jdk-headless@21.0.12\",\"deb/openjdk-21-jre-headless@21.0.12\",\"deb/openssh-client@10.0\",\"deb/openssl-provider-legacy@3.5.7\",\"deb/openssl@3.5.7\",\"deb/passwd@4.17.4\",\"deb/perl-base@5.40.1\",\"deb/perl-modules-5.40@5.40.1\",\"deb/perl@5.40.1\",\"deb/pinentry-curses@1.3.1\",\"deb/procps@4.0.4\",\"deb/psmisc@23.7\",\"deb/python-3.14-minimal@3.14.7\",\"deb/python-3.14@3.14.7\",\"deb/python3-pip-whl@26.2.1\",\"deb/python3-pip@26.2.1\",\"deb/python3-setuptools-whl@84.0.0\",\"deb/python3-wheel@0.48.0\",\"deb/readline-common@8.2\",\"deb/ripgrep@14.1.1\",\"deb/rpcsvc-proto@1.4.3\",\"deb/sed@4.9\",\"deb/socat@1.8.0\",\"deb/sudo@1.9.16\",\"deb/tar@1.35\",\"deb/tini@0.19.0\",\"deb/tmux@3.5\",\"deb/traceroute@2.1.6\",\"deb/tree@2.2.1\",\"deb/unzip@6.0\",\"deb/util-linux@2.41.5\",\"deb/uv@0.12.15\",\"deb/vim-common@9.1.1230\",\"deb/vim-runtime@9.1.1230\",\"deb/vim@9.1.1230\",\"deb/wget@1.25.0\",\"deb/yarn-classic@1.22.22\",\"deb/zlib1g@1.3\"],\"capabilities\":[{\"type\":\"com.docker.sandbox/network-policy@1\",\"config\":{\"runtime\":{\"allow\":[\"api.anthropic.com:443\",\"platform.claude.com:443\",\"downloads.claude.ai:443\",\"claude.com:443\",\"mcp-proxy.anthropic.com:443\",\"bridge.claudeusercontent.com:443\",\"deb.debian.org\",\"dhi.io\"]}}},{\"type\":\"com.docker.sandbox/credential@1\",\"optional\":true,\"config\":{\"apiKey\":{\"inject\":[{\"domain\":\"api.anthropic.com\",\"format\":\"%s\",\"header\":\"x-api-key\"},{\"domain\":\"mcp-proxy.anthropic.com\",\"format\":\"%s\",\"header\":\"x-api-key\"}],\"name\":\"ANTHROPIC_API_KEY\"},\"oauth\":{\"credentialFile\":{\"path\":\"~/.claude/.credentials.json\",\"structure\":{\"claudeAiOauth\":{\"accessToken\":\"{{.AccessToken}}\",\"expiresAt\":\"{{.ExpiresAt}}\",\"refreshToken\":\"{{.RefreshToken}}\",\"scopes\":\"{{.Scopes}}\"},\"primaryApiKey\":\"{{.PrimaryApiKey}}\"}},\"sentinels\":{\"accessToken\":\"sk-ant-oat01-proxy-managed\",\"refreshToken\":\"sk-ant-ort01-proxy-managed\"},\"tokenEndpoint\":{\"host\":\"platform.claude.com\",\"path\":\"/v1/oauth/token\"}},\"phase\":\"runtime\",\"service\":\"anthropic\"},\"description\":\"Anthropic API access (API key or claude.ai OAuth)\"},{\"type\":\"com.docker.sandbox/volume@1\",\"config\":{\"path\":\"/home/agent/.claude/projects\",\"size\":\"2g\"},\"description\":\"Conversation history; grows without bound, gets the headroom\"},{\"type\":\"com.docker.sandbox/volume@1\",\"config\":{\"path\":\"/home/agent/.claude/sessions\",\"size\":\"512m\"},\"description\":\"Per-session state; load-bearing for `claude -c`\"},{\"type\":\"com.docker.sandbox/volume@1\",\"config\":{\"path\":\"/home/agent/.claude/todos\",\"size\":\"512m\"},\"description\":\"TodoWrite state\"},{\"type\":\"com.docker.sandbox/volume@1\",\"config\":{\"path\":\"/home/agent/.claude/shell-snapshots\",\"size\":\"512m\"},\"description\":\"Bash state snapshots across sessions\"},{\"type\":\"com.docker.sandbox/volume@1\",\"config\":{\"path\":\"/home/agent/.claude/statsig\",\"size\":\"512m\"},\"description\":\"Local feature-flag cache\"},{\"type\":\"com.docker.sandbox/agent-skills@1\",\"optional\":true,\"config\":{\"path\":\"/home/agent/.claude/skills\"}},{\"type\":\"com.docker.sandbox/sbx@1\"},{\"type\":\"com.docker.sandbox/lifecycle@1\",\"config\":{\"install\":[{\"command\":[\"sh\",\"-c\",\"set -e\\nws=\\\"${WORKSPACE_DIR:-/}\\\"\\nesc=$(printf '%s' \\\"$ws\\\" | sed 's/\\\\\\\\/\\\\\\\\\\\\\\\\/g; s/\\\"/\\\\\\\\\\\"/g; s/\\\\t/\\\\\\\\t/g; s/\\\\r/\\\\\\\\r/g')\\nprojects=\\\"\\\\\\\"/\\\\\\\": { \\\\\\\"hasTrustDialogAccepted\\\\\\\": true }\\\"\\n[ \\\"$ws\\\" = \\\"/\\\" ] || projects=\\\"$projects, \\\\\\\"$esc\\\\\\\": { \\\\\\\"hasTrustDialogAccepted\\\\\\\": true }\\\"\\nprintf '%s\\\\n' \\\"{\\n \\\\\\\"bypassPermissionsModeAccepted\\\\\\\": true,\\n \\\\\\\"hasCompletedOnboarding\\\\\\\": true,\\n \\\\\\\"projects\\\\\\\": { $projects }\\n}\\\" \\u003e /home/agent/.claude.json\\nchown agent:agent /home/agent/.claude.json\\n\"],\"description\":\"Seed Claude bypass and trust flags\",\"env\":[\"WORKSPACE_DIR\"],\"user\":\"0\"},{\"command\":[\"sh\",\"-c\",\"mkdir -p /home/agent/.claude \\u0026\\u0026 chown agent:agent /home/agent/.claude\"],\"description\":\"Ensure ~/.claude is agent-owned before settings seed\",\"user\":\"0\"},{\"command\":[\"sh\",\"-c\",\"set -e\\nHELPER=''\\nif [ \\\"${SBX_CRED_ANTHROPIC_MODE:-none}\\\" != none ]; then\\n HELPER=' \\\"apiKeyHelper\\\": \\\"echo proxy-managed\\\",\\n'\\nfi\\nprintf '%s' \\\"{\\n \\\\\\\"themeId\\\\\\\": 1,\\n \\\\\\\"alwaysThinkingEnabled\\\\\\\": true,\\n${HELPER} \\\\\\\"permissions\\\\\\\": { \\\\\\\"defaultMode\\\\\\\": \\\\\\\"bypassPermissions\\\\\\\" },\\n \\\\\\\"bypassPermissionsModeAccepted\\\\\\\": true,\\n \\\\\\\"skipDangerousModePermissionPrompt\\\\\\\": true\\n}\\n\\\" \\u003e /home/agent/.claude/settings.json\\n\"],\"description\":\"Seed Claude settings.json from the surfaced auth mode\",\"env\":[\"SBX_CRED_ANTHROPIC_MODE\"],\"user\":\"agent\"},{\"command\":[\"sh\",\"-c\",\"set -e\\n[ -n \\\"$MCP_GATEWAY_URL\\\" ] || exit 0\\nclaude mcp add mcp-gateway \\\"$MCP_GATEWAY_URL\\\" \\\\\\n --transport http \\\\\\n --scope user \\\\\\n --header \\\"Authorization: Bearer $MCP_SENTINEL_TOKEN_NAME\\\" || true\\n\"],\"description\":\"Register the sandbox MCP gateway at install\",\"env\":[\"MCP_GATEWAY_URL\",\"MCP_SENTINEL_TOKEN_NAME\"],\"user\":\"agent\"}],\"startup\":[{\"command\":[\"sh\",\"-c\",\"chown -R agent:agent /home/agent/.claude/projects /home/agent/.claude/sessions /home/agent/.claude/todos /home/agent/.claude/shell-snapshots /home/agent/.claude/statsig 2\\u003e/dev/null || true\"],\"description\":\"Re-own claude session-state volume mount roots to agent\",\"user\":\"0\"},{\"command\":[\"sh\",\"-c\",\"set -e\\n[ -n \\\"$MCP_GATEWAY_URL\\\" ] || exit 0\\nclaude mcp add mcp-gateway \\\"$MCP_GATEWAY_URL\\\" \\\\\\n --transport http \\\\\\n --scope user \\\\\\n --header \\\"Authorization: Bearer $MCP_SENTINEL_TOKEN_NAME\\\" || true\\n\"],\"description\":\"Register the sandbox MCP gateway (startup fallback)\",\"env\":[\"MCP_GATEWAY_URL\",\"MCP_SENTINEL_TOKEN_NAME\"],\"user\":\"agent\"}]}},{\"type\":\"com.docker.sandbox/agent-context@1\",\"config\":{\"contentFile\":\"/usr/share/sandbox/kit/claude-acp-set/context.md\",\"filename\":\"AGENTS.md\"}}],\"args\":{\"registry\":{\"default\":\"docker.io/docker\",\"description\":\"Registry namespace the listed kits were pushed to\",\"pattern\":\"^[a-z0-9][a-z0-9._/:-]*$\",\"buildArg\":\"KIT_REGISTRY\"}},\"kits\":[{\"ref\":\"docker.io/docker/sbx-kit-shell:1.0.0\",\"digest\":\"sha256:367c9a4b3fd550f777e1d57cc53550afb4a1385055d79111acf6f996272ff6de\"},{\"ref\":\"docker.io/docker/sbx-kit-claude-mixin:2.1.285\",\"digest\":\"sha256:52500d618027b350294720d86ad8a86214067bb9d4332db5d533b5457dfaa3cd\"},{\"ref\":\"docker.io/docker/sbx-kit-claude-acp:0.84.0\",\"digest\":\"sha256:29058553ef6afa1e3e524d04847fbf1997f79260191989b55c4bed29e4bc1f83\"}]}", + "vnd.docker.sandbox.kit.schema-version": "3" + } +} \ No newline at end of file diff --git a/integrations/docker-sandbox-kit/fixtures/claude-acp-set-1.0.1.index.json b/integrations/docker-sandbox-kit/fixtures/claude-acp-set-1.0.1.index.json new file mode 100644 index 0000000..a06a106 --- /dev/null +++ b/integrations/docker-sandbox-kit/fixtures/claude-acp-set-1.0.1.index.json @@ -0,0 +1,62 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:86d56a3b2ea714d55c2a48b55b9ca64e2145245f43583100dd944eccfb8fe78e", + "size": 17034, + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:b19428d314cef4c9990fc52d3c9071335fde988cab702b567f013d24b08f61d3", + "size": 17034, + "platform": { + "architecture": "arm64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:09acb5237a871e2f5fa0870c6c620048c8802d5cc74ea86dc2786f00466c30b2", + "size": 1114, + "annotations": { + "vnd.docker.reference.digest": "sha256:86d56a3b2ea714d55c2a48b55b9ca64e2145245f43583100dd944eccfb8fe78e", + "vnd.docker.reference.type": "attestation-manifest" + }, + "platform": { + "architecture": "unknown", + "os": "unknown" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:a145f8d59fce3f6d74a9f53338733110919754a692763801bb6faa9a741e3da0", + "size": 1114, + "annotations": { + "vnd.docker.reference.digest": "sha256:b19428d314cef4c9990fc52d3c9071335fde988cab702b567f013d24b08f61d3", + "vnd.docker.reference.type": "attestation-manifest" + }, + "platform": { + "architecture": "unknown", + "os": "unknown" + } + } + ], + "annotations": { + "org.opencontainers.image.authors": "Docker, Inc.", + "org.opencontainers.image.description": "A sandbox an ACP client can drive: a shell base, Claude Code, and the Agent Client Protocol adapter that fronts it — published as one kit. The same three kits compose by hand; this exists so an editor can be pointed at one reference instead of three that have to agree.", + "org.opencontainers.image.licenses": "Apache-2.0", + "org.opencontainers.image.source": "https://github.com/docker/sandbox-kit-spec", + "org.opencontainers.image.title": "Claude over ACP", + "org.opencontainers.image.version": "1.0.1", + "vnd.docker.sandbox.kit.built-by": "{\"name\":\"docker/sandbox-kit\",\"version\":\"dev\",\"revision\":\"56ddfa629076cc4509094290a59a38aed3aa59c9-dirty\"}", + "vnd.docker.sandbox.kit.capabilities": "com.docker.sandbox/agent-context@1,com.docker.sandbox/agent-skills@1,com.docker.sandbox/credential@1,com.docker.sandbox/lifecycle@1,com.docker.sandbox/network-policy@1,com.docker.sandbox/sbx@1,com.docker.sandbox/volume@1", + "vnd.docker.sandbox.kit.descriptor": "{\"schemaVersion\":\"3\",\"displayName\":\"Claude over ACP\",\"author\":\"Docker, Inc.\",\"description\":\"A sandbox an ACP client can drive: a shell base, Claude Code, and the Agent Client Protocol adapter that fronts it — published as one kit. The same three kits compose by hand; this exists so an editor can be pointed at one reference instead of three that have to agree.\",\"sourceUrl\":\"https://github.com/docker/sandbox-kit-spec\",\"version\":\"1.0.1\",\"licenses\":[\"Apache-2.0\"],\"kind\":\"workload\",\"provides\":[\"claude@2.1.285\",\"claude-acp@0.84.0\",\"deb/adduser@3.152\",\"deb/apt@3.0.3\",\"deb/base-files@13.8\",\"deb/base-passwd@3.6.7\",\"deb/bash@5.2.37\",\"deb/bc@1.07.1\",\"deb/bind9-dnsutils@9.20.26\",\"deb/bind9-host@9.20.26\",\"deb/bind9-libs@9.20.26\",\"deb/binutils-common@2.44\",\"deb/binutils@2.44\",\"deb/bsdextrautils@2.41.5\",\"deb/bubblewrap@0.12.0\",\"deb/ca-certificates-java@20240118\",\"deb/ca-certificates@20250419\",\"deb/containerd.io@2.3.5\",\"deb/coreutils@9.7\",\"deb/cpp-14@14.2.0\",\"deb/cpp@14.2.0\",\"deb/curl@8.14.1\",\"deb/dash@0.5.12\",\"deb/debconf@1.5.91\",\"deb/debian-archive-keyring@2025.1\",\"deb/debianutils@5.23.2\",\"deb/default-jdk-headless@1.21\",\"deb/default-jre-headless@1.21\",\"deb/diffutils@3.10\",\"deb/dirmngr@2.4.7\",\"deb/docker-buildx-plugin@0.37.1\",\"deb/docker-ce-cli@29.8.1\",\"deb/docker-ce@29.8.1\",\"deb/docker-compose-plugin@5.5.1\",\"deb/dpkg@1.22.22\",\"deb/findutils@4.10.0\",\"deb/gcc-14-base@14.2.0\",\"deb/gcc-14@14.2.0\",\"deb/gcc@14.2.0\",\"deb/gh@2.100.0\",\"deb/git@2.47.3\",\"deb/gnu-which@2.21\",\"deb/gnupg-l10n@2.4.7\",\"deb/gnupg@2.4.7\",\"deb/golang-1.26-oci-config@1.26.8\",\"deb/golang-1.26@1.26.8\",\"deb/gpg-agent@2.4.7\",\"deb/gpg@2.4.7\",\"deb/gpgconf@2.4.7\",\"deb/gpgsm@2.4.7\",\"deb/gpgv@2.4.7\",\"deb/grep@3.11\",\"deb/groff-base@1.23.0\",\"deb/gzip@1.13\",\"deb/htop@3.4.1\",\"deb/iproute2@6.15.0\",\"deb/iptables@1.8.11\",\"deb/iputils-ping@20240905\",\"deb/java-common@0.76\",\"deb/jq@1.8.2\",\"deb/less@668\",\"deb/libacl1@2.3.2\",\"deb/libapparmor1@4.1.0\",\"deb/libapt-pkg7.0@3.0.3\",\"deb/libasan8@14.2.0\",\"deb/libassuan9@3.0.2\",\"deb/libatomic1@14.2.0\",\"deb/libattr1@2.5.2\",\"deb/libaudit-common@4.0.2\",\"deb/libaudit1@4.0.2\",\"deb/libbinutils@2.44\",\"deb/libblkid1@2.41.5\",\"deb/libbpf1@1.5.0\",\"deb/libbrotli1@1.1.0\",\"deb/libbsd0@0.12.2\",\"deb/libbz2-1.0@1.0.8\",\"deb/libc-bin@2.41\",\"deb/libc-dev-bin@2.41\",\"deb/libc-l10n@2.41\",\"deb/libc6-dev@2.41\",\"deb/libc6@2.41\",\"deb/libcap-ng0@0.8.5\",\"deb/libcap2-bin@2.75\",\"deb/libcap2@2.75\",\"deb/libcbor0.10@0.10.2\",\"deb/libcc1-0@14.2.0\",\"deb/libcom-err2@1.47.2\",\"deb/libcrypt-dev@4.4.38\",\"deb/libcrypt1@4.4.38\",\"deb/libctf-nobfd0@2.44\",\"deb/libctf0@2.44\",\"deb/libcurl4t64@8.14.1\",\"deb/libdb5.3t64@5.3.28\",\"deb/libdebconfclient0@0.280\",\"deb/libedit2@3.1\",\"deb/libelf1t64@0.192\",\"deb/liberror-perl@0.17030\",\"deb/libevent-core-2.1-7t64@2.1.13\",\"deb/libexpat1@2.8.3\",\"deb/libffi8@3.4.8\",\"deb/libfido2-1@1.15.0\",\"deb/libfstrm0@0.6.1\",\"deb/libgcc-14-dev@14.2.0\",\"deb/libgcc-s1@14.2.0\",\"deb/libgcrypt20@1.11.0\",\"deb/libgdbm-compat4t64@1.24\",\"deb/libgdbm6t64@1.24\",\"deb/libgmp10@6.3.0\",\"deb/libgnutls30t64@3.8.9\",\"deb/libgomp1@14.2.0\",\"deb/libgpg-error0@1.51\",\"deb/libgpm2@1.20.7\",\"deb/libgprofng0@2.44\",\"deb/libgssapi-krb5-2@1.21.3\",\"deb/libhogweed6t64@3.10.1\",\"deb/libhwasan0@14.2.0\",\"deb/libidn2-0@2.3.8\",\"deb/libip4tc2@1.8.11\",\"deb/libip6tc2@1.8.11\",\"deb/libisl23@0.27\",\"deb/libitm1@14.2.0\",\"deb/libjansson4@2.14\",\"deb/libjemalloc2@5.3.0\",\"deb/libjpeg62-turbo@2.1.5\",\"deb/libjq1@1.8.2\",\"deb/libjson-c5@0.18\",\"deb/libk5crypto3@1.21.3\",\"deb/libkeyutils1@1.6.3\",\"deb/libkrb5-3@1.21.3\",\"deb/libkrb5support0@1.21.3\",\"deb/libksba8@1.6.7\",\"deb/liblastlog2-2@2.41.5\",\"deb/liblcms2-2@2.16\",\"deb/libldap2@2.6.10\",\"deb/liblmdb0@0.9.31\",\"deb/liblsan0@14.2.0\",\"deb/liblsof0@4.99.4\",\"deb/liblz4-1@1.10.0\",\"deb/liblzma5@5.8.1\",\"deb/libmaxminddb0@1.12.2\",\"deb/libmd0@1.1.0\",\"deb/libmnl0@1.0.5\",\"deb/libmount1@2.41.5\",\"deb/libmpc3@1.3.1\",\"deb/libmpfr6@4.2.2\",\"deb/libncursesw6@6.5\",\"deb/libnetfilter-conntrack3@1.1.0\",\"deb/libnettle8t64@3.10.1\",\"deb/libnfnetlink0@1.0.2\",\"deb/libnftables1@1.1.3\",\"deb/libnftnl11@1.2.9\",\"deb/libnghttp2-14@1.64.0\",\"deb/libnghttp3-9@1.8.0\",\"deb/libnpth0t64@1.8\",\"deb/libnss3@3.110\",\"deb/libonig5@6.9.9\",\"deb/libp11-kit0@0.25.5\",\"deb/libpam-modules-bin@1.7.0\",\"deb/libpam-modules@1.7.0\",\"deb/libpam-runtime@1.7.0\",\"deb/libpam0g@1.7.0\",\"deb/libpcre2-8-0@10.46\",\"deb/libpcsclite1@2.3.3\",\"deb/libperl5.40@5.40.1\",\"deb/libpipeline1@1.5.8\",\"deb/libproc2-0@4.0.4\",\"deb/libprotobuf-c1@1.5.1\",\"deb/libpsl5t64@0.21.2\",\"deb/libpython-3.14-minimal@3.14.7\",\"deb/libpython-3.14-stdlib@3.14.7\",\"deb/libpython-3.14@3.14.7\",\"deb/libreadline8t64@8.2\",\"deb/librtmp1@2.4\",\"deb/libsasl2-2@2.1.28\",\"deb/libsasl2-modules-db@2.1.28\",\"deb/libseccomp2@2.6.0\",\"deb/libselinux1@3.8.1\",\"deb/libsemanage-common@3.8.1\",\"deb/libsemanage2@3.8.1\",\"deb/libsepol2@3.8.1\",\"deb/libsframe1@2.44\",\"deb/libsmartcols1@2.41.5\",\"deb/libsodium23@1.0.18\",\"deb/libsqlite3-0@3.46.1\",\"deb/libssh2-1t64@1.11.1\",\"deb/libssl3t64@3.5.7\",\"deb/libstdc++6@14.2.0\",\"deb/libsystemd0@257.13\",\"deb/libtasn1-6@4.20.0\",\"deb/libtinfo6@6.5\",\"deb/libtirpc-common@1.3.6\",\"deb/libtirpc3t64@1.3.6\",\"deb/libtsan2@14.2.0\",\"deb/libubsan1@14.2.0\",\"deb/libuchardet0@0.0.8\",\"deb/libudev1@257.13\",\"deb/libunistring5@1.3\",\"deb/liburcu8t64@0.15.2\",\"deb/libuuid1@2.41.5\",\"deb/libuv1t64@1.50.0\",\"deb/libwrap0@7.6\",\"deb/libxml2@2.12.7\",\"deb/libxtables12@1.8.11\",\"deb/libxxhash0@0.8.3\",\"deb/libzstd1@1.5.7\",\"deb/linux-libc-dev@6.12.107\",\"deb/locales@2.41\",\"deb/login.defs@4.17.4\",\"deb/lsof@4.99.4\",\"deb/make@4.4.1\",\"deb/man-db@2.13.1\",\"deb/mawk@1.3.4\",\"deb/nano@8.4\",\"deb/ncurses-base@6.5\",\"deb/ncurses-bin@6.5\",\"deb/net-tools@2.10\",\"deb/netbase@6.5\",\"deb/netcat-openbsd@1.229\",\"deb/nftables@1.1.3\",\"deb/node-corepack@0.36.0\",\"deb/nodejs-24@24.21.0\",\"deb/npm@11.19.1\",\"deb/openjdk-21-jdk-headless@21.0.12\",\"deb/openjdk-21-jre-headless@21.0.12\",\"deb/openssh-client@10.0\",\"deb/openssl-provider-legacy@3.5.7\",\"deb/openssl@3.5.7\",\"deb/passwd@4.17.4\",\"deb/perl-base@5.40.1\",\"deb/perl-modules-5.40@5.40.1\",\"deb/perl@5.40.1\",\"deb/pinentry-curses@1.3.1\",\"deb/procps@4.0.4\",\"deb/psmisc@23.7\",\"deb/python-3.14-minimal@3.14.7\",\"deb/python-3.14@3.14.7\",\"deb/python3-pip-whl@26.2.1\",\"deb/python3-pip@26.2.1\",\"deb/python3-setuptools-whl@84.0.0\",\"deb/python3-wheel@0.48.0\",\"deb/readline-common@8.2\",\"deb/ripgrep@14.1.1\",\"deb/rpcsvc-proto@1.4.3\",\"deb/sed@4.9\",\"deb/socat@1.8.0\",\"deb/sudo@1.9.16\",\"deb/tar@1.35\",\"deb/tini@0.19.0\",\"deb/tmux@3.5\",\"deb/traceroute@2.1.6\",\"deb/tree@2.2.1\",\"deb/unzip@6.0\",\"deb/util-linux@2.41.5\",\"deb/uv@0.12.15\",\"deb/vim-common@9.1.1230\",\"deb/vim-runtime@9.1.1230\",\"deb/vim@9.1.1230\",\"deb/wget@1.25.0\",\"deb/yarn-classic@1.22.22\",\"deb/zlib1g@1.3\"],\"capabilities\":[{\"type\":\"com.docker.sandbox/network-policy@1\",\"config\":{\"runtime\":{\"allow\":[\"api.anthropic.com:443\",\"platform.claude.com:443\",\"downloads.claude.ai:443\",\"claude.com:443\",\"mcp-proxy.anthropic.com:443\",\"bridge.claudeusercontent.com:443\",\"deb.debian.org\",\"dhi.io\"]}}},{\"type\":\"com.docker.sandbox/credential@1\",\"optional\":true,\"config\":{\"apiKey\":{\"inject\":[{\"domain\":\"api.anthropic.com\",\"format\":\"%s\",\"header\":\"x-api-key\"},{\"domain\":\"mcp-proxy.anthropic.com\",\"format\":\"%s\",\"header\":\"x-api-key\"}],\"name\":\"ANTHROPIC_API_KEY\"},\"oauth\":{\"credentialFile\":{\"path\":\"~/.claude/.credentials.json\",\"structure\":{\"claudeAiOauth\":{\"accessToken\":\"{{.AccessToken}}\",\"expiresAt\":\"{{.ExpiresAt}}\",\"refreshToken\":\"{{.RefreshToken}}\",\"scopes\":\"{{.Scopes}}\"},\"primaryApiKey\":\"{{.PrimaryApiKey}}\"}},\"sentinels\":{\"accessToken\":\"sk-ant-oat01-proxy-managed\",\"refreshToken\":\"sk-ant-ort01-proxy-managed\"},\"tokenEndpoint\":{\"host\":\"platform.claude.com\",\"path\":\"/v1/oauth/token\"}},\"phase\":\"runtime\",\"service\":\"anthropic\"},\"description\":\"Anthropic API access (API key or claude.ai OAuth)\"},{\"type\":\"com.docker.sandbox/volume@1\",\"config\":{\"path\":\"/home/agent/.claude/projects\",\"size\":\"2g\"},\"description\":\"Conversation history; grows without bound, gets the headroom\"},{\"type\":\"com.docker.sandbox/volume@1\",\"config\":{\"path\":\"/home/agent/.claude/sessions\",\"size\":\"512m\"},\"description\":\"Per-session state; load-bearing for `claude -c`\"},{\"type\":\"com.docker.sandbox/volume@1\",\"config\":{\"path\":\"/home/agent/.claude/todos\",\"size\":\"512m\"},\"description\":\"TodoWrite state\"},{\"type\":\"com.docker.sandbox/volume@1\",\"config\":{\"path\":\"/home/agent/.claude/shell-snapshots\",\"size\":\"512m\"},\"description\":\"Bash state snapshots across sessions\"},{\"type\":\"com.docker.sandbox/volume@1\",\"config\":{\"path\":\"/home/agent/.claude/statsig\",\"size\":\"512m\"},\"description\":\"Local feature-flag cache\"},{\"type\":\"com.docker.sandbox/agent-skills@1\",\"optional\":true,\"config\":{\"path\":\"/home/agent/.claude/skills\"}},{\"type\":\"com.docker.sandbox/sbx@1\"},{\"type\":\"com.docker.sandbox/lifecycle@1\",\"config\":{\"install\":[{\"command\":[\"sh\",\"-c\",\"set -e\\nws=\\\"${WORKSPACE_DIR:-/}\\\"\\nesc=$(printf '%s' \\\"$ws\\\" | sed 's/\\\\\\\\/\\\\\\\\\\\\\\\\/g; s/\\\"/\\\\\\\\\\\"/g; s/\\\\t/\\\\\\\\t/g; s/\\\\r/\\\\\\\\r/g')\\nprojects=\\\"\\\\\\\"/\\\\\\\": { \\\\\\\"hasTrustDialogAccepted\\\\\\\": true }\\\"\\n[ \\\"$ws\\\" = \\\"/\\\" ] || projects=\\\"$projects, \\\\\\\"$esc\\\\\\\": { \\\\\\\"hasTrustDialogAccepted\\\\\\\": true }\\\"\\nprintf '%s\\\\n' \\\"{\\n \\\\\\\"bypassPermissionsModeAccepted\\\\\\\": true,\\n \\\\\\\"hasCompletedOnboarding\\\\\\\": true,\\n \\\\\\\"projects\\\\\\\": { $projects }\\n}\\\" \\u003e /home/agent/.claude.json\\nchown agent:agent /home/agent/.claude.json\\n\"],\"description\":\"Seed Claude bypass and trust flags\",\"env\":[\"WORKSPACE_DIR\"],\"user\":\"0\"},{\"command\":[\"sh\",\"-c\",\"mkdir -p /home/agent/.claude \\u0026\\u0026 chown agent:agent /home/agent/.claude\"],\"description\":\"Ensure ~/.claude is agent-owned before settings seed\",\"user\":\"0\"},{\"command\":[\"sh\",\"-c\",\"set -e\\nHELPER=''\\nif [ \\\"${SBX_CRED_ANTHROPIC_MODE:-none}\\\" != none ]; then\\n HELPER=' \\\"apiKeyHelper\\\": \\\"echo proxy-managed\\\",\\n'\\nfi\\nprintf '%s' \\\"{\\n \\\\\\\"themeId\\\\\\\": 1,\\n \\\\\\\"alwaysThinkingEnabled\\\\\\\": true,\\n${HELPER} \\\\\\\"permissions\\\\\\\": { \\\\\\\"defaultMode\\\\\\\": \\\\\\\"bypassPermissions\\\\\\\" },\\n \\\\\\\"bypassPermissionsModeAccepted\\\\\\\": true,\\n \\\\\\\"skipDangerousModePermissionPrompt\\\\\\\": true\\n}\\n\\\" \\u003e /home/agent/.claude/settings.json\\n\"],\"description\":\"Seed Claude settings.json from the surfaced auth mode\",\"env\":[\"SBX_CRED_ANTHROPIC_MODE\"],\"user\":\"agent\"},{\"command\":[\"sh\",\"-c\",\"set -e\\n[ -n \\\"$MCP_GATEWAY_URL\\\" ] || exit 0\\nclaude mcp add mcp-gateway \\\"$MCP_GATEWAY_URL\\\" \\\\\\n --transport http \\\\\\n --scope user \\\\\\n --header \\\"Authorization: Bearer $MCP_SENTINEL_TOKEN_NAME\\\" || true\\n\"],\"description\":\"Register the sandbox MCP gateway at install\",\"env\":[\"MCP_GATEWAY_URL\",\"MCP_SENTINEL_TOKEN_NAME\"],\"user\":\"agent\"}],\"startup\":[{\"command\":[\"sh\",\"-c\",\"chown -R agent:agent /home/agent/.claude/projects /home/agent/.claude/sessions /home/agent/.claude/todos /home/agent/.claude/shell-snapshots /home/agent/.claude/statsig 2\\u003e/dev/null || true\"],\"description\":\"Re-own claude session-state volume mount roots to agent\",\"user\":\"0\"},{\"command\":[\"sh\",\"-c\",\"set -e\\n[ -n \\\"$MCP_GATEWAY_URL\\\" ] || exit 0\\nclaude mcp add mcp-gateway \\\"$MCP_GATEWAY_URL\\\" \\\\\\n --transport http \\\\\\n --scope user \\\\\\n --header \\\"Authorization: Bearer $MCP_SENTINEL_TOKEN_NAME\\\" || true\\n\"],\"description\":\"Register the sandbox MCP gateway (startup fallback)\",\"env\":[\"MCP_GATEWAY_URL\",\"MCP_SENTINEL_TOKEN_NAME\"],\"user\":\"agent\"}]}},{\"type\":\"com.docker.sandbox/agent-context@1\",\"config\":{\"contentFile\":\"/usr/share/sandbox/kit/claude-acp-set/context.md\",\"filename\":\"AGENTS.md\"}}],\"args\":{\"registry\":{\"default\":\"docker.io/docker\",\"description\":\"Registry namespace the listed kits were pushed to\",\"pattern\":\"^[a-z0-9][a-z0-9._/:-]*$\",\"buildArg\":\"KIT_REGISTRY\"}},\"kits\":[{\"ref\":\"docker.io/docker/sbx-kit-shell:1.0.0\",\"digest\":\"sha256:367c9a4b3fd550f777e1d57cc53550afb4a1385055d79111acf6f996272ff6de\"},{\"ref\":\"docker.io/docker/sbx-kit-claude-mixin:2.1.285\",\"digest\":\"sha256:52500d618027b350294720d86ad8a86214067bb9d4332db5d533b5457dfaa3cd\"},{\"ref\":\"docker.io/docker/sbx-kit-claude-acp:0.84.0\",\"digest\":\"sha256:29058553ef6afa1e3e524d04847fbf1997f79260191989b55c4bed29e4bc1f83\"}]}", + "vnd.docker.sandbox.kit.schema-version": "3" + } +} \ No newline at end of file diff --git a/integrations/docker-sandbox-kit/fixtures/doodle-2026.amd64.manifest.json b/integrations/docker-sandbox-kit/fixtures/doodle-2026.amd64.manifest.json new file mode 100644 index 0000000..768f9be --- /dev/null +++ b/integrations/docker-sandbox-kit/fixtures/doodle-2026.amd64.manifest.json @@ -0,0 +1,95 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:c014d74e53ed36c41f6f21082bfff58895e5e611c7d7076908fb1e17509b7e3d", + "size": 7581 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:f7ee36c9aa34bbb665f975c76e5c0d1607f0674b94c84cfb0061f87006ea5d10", + "size": 3787595 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:258a0c4b9d8fdcb40d277c42514cf069338a638ff7ac8086d23c57ba2f7edb82", + "size": 34778636 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:bf362782d85ca1b887c36aa07917ea376bd505792a196ff0ef0342130ce42026", + "size": 295 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:1041346e8a5699d54f8121bb1b14ae14b9b71416045445e4d556eb90265d4ba8", + "size": 185 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:2ac258d6784fcd78e95afb355786898b767edb9834c7bb155fc21dbf7f1ca8df", + "size": 3661750 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:c2d770e84b7946570236f95f57c50d55b21d59805c5b3641b652938458268e24", + "size": 142 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:3015b1d1510bad3431adecf1c2ced6e1dfd73e36d13f8769edeeb459c79229b1", + "size": 92 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:1a9fa16fe7948f9554eca1e64c8e50bb672790ec6b692ebfeed7c010b7bdfb28", + "size": 15654059 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:f7152a78b78a6d46adcd39d41e49e97315fb1cec993e0781726a36079b5ca096", + "size": 404 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:33216e54d51cb413a78b16d76f155cbc6a88b6720d8f321e34e70d499bbfe150", + "size": 8844 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:3e06f39ce59ad9fd5d0b219a67504c3143bfbbd5e66226522ab792a7f696e6cb", + "size": 1259258 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:ebcdde06a840f1b396b310e3980842e4dec971f8e5275d30659836b29a580a5b", + "size": 7372631 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1", + "size": 32 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:1d689ecfff9bbdabda8d253282b40e906db4db8b98acf99d8e71237ca1df6273", + "size": 687 + }, + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:6e28536b60eb2f35b7c2398ae63e4d96235c4812a26032e8de23560b87c18cff", + "size": 2011 + } + ], + "annotations": { + "org.opencontainers.image.description": "Runs docker/doodle — the terminal Moby viewer built with OpenTUI and Bun — as the sandbox's interactive process.", + "org.opencontainers.image.source": "https://github.com/cdupuis/sbx-kits", + "org.opencontainers.image.title": "Docker Doodle", + "org.opencontainers.image.version": "2026", + "vnd.docker.sandbox.kit.capabilities": "com.docker.sandbox/agent-context@1,com.docker.sandbox/sbx@1", + "vnd.docker.sandbox.kit.descriptor": "{\"schemaVersion\":\"3\",\"displayName\":\"Docker Doodle\",\"description\":\"Runs docker/doodle — the terminal Moby viewer built with OpenTUI and Bun — as the sandbox's interactive process.\",\"sourceUrl\":\"https://github.com/cdupuis/sbx-kits\",\"version\":\"2026\",\"kind\":\"workload\",\"provides\":[\"doodle@2026\",\"apk/alpine-baselayout-data@3.7.0\",\"apk/alpine-baselayout@3.7.0\",\"apk/alpine-keys@2.5\",\"apk/alpine-release@3.22.5\",\"apk/apk-tools@2.14.10\",\"apk/bash@5.2.37\",\"apk/brotli-libs@1.1.0\",\"apk/busybox-binsh@1.37.0\",\"apk/busybox@1.37.0\",\"apk/c-ares@1.34.8\",\"apk/ca-certificates-bundle@20260611\",\"apk/ca-certificates@20260909\",\"apk/curl@8.14.1\",\"apk/git-init-template@2.49.1\",\"apk/git@2.49.1\",\"apk/libapk2@2.14.10\",\"apk/libcrypto3@3.5.7\",\"apk/libcurl@8.14.1\",\"apk/libexpat@2.8.4\",\"apk/libgcc@14.2.0\",\"apk/libidn2@2.3.7\",\"apk/libncursesw@6.5\",\"apk/libpsl@0.21.5\",\"apk/libssl3@3.5.7\",\"apk/libstdc++@14.2.0\",\"apk/libunistring@1.3\",\"apk/musl-utils@1.2.5\",\"apk/musl@1.2.5\",\"apk/ncurses-terminfo-base@6.5\",\"apk/nghttp2-libs@1.69.0\",\"apk/pcre2@10.46\",\"apk/readline@8.2.13\",\"apk/scanelf@1.3.8\",\"apk/zlib@1.3.2\",\"apk/zstd-libs@1.5.7\"],\"capabilities\":[{\"type\":\"com.docker.sandbox/sbx@1\"},{\"type\":\"com.docker.sandbox/agent-context@1\",\"config\":{\"contentFile\":\"/usr/share/sandbox/kit/doodle/doodle-context.md\",\"filename\":\"AGENTS.md\"}}],\"args\":{\"tag\":{\"default\":\"2026\",\"description\":\"docker/doodle image tag to run.\",\"pattern\":\"^[0-9]+(\\\\.[0-9A-Za-z-]+)*$\",\"buildArg\":\"DOODLE_TAG\"}}}", + "vnd.docker.sandbox.kit.schema-version": "3" + } +} \ No newline at end of file diff --git a/integrations/docker-sandbox-kit/integration.yaml b/integrations/docker-sandbox-kit/integration.yaml new file mode 100644 index 0000000..d3b24be --- /dev/null +++ b/integrations/docker-sandbox-kit/integration.yaml @@ -0,0 +1,21 @@ +name: Docker Sandbox Kit +vendor: agentrust-io +integrates_with: + - trace +description: Binds a Docker Sandbox Kit v3 descriptor into a TRACE Trust Record as a declared policy, refusing any manifest that is not a published v3 Kit. +maintainer: + github: imran-siddique +repository: https://github.com/agentrust-io/integrations +homepage: https://github.com/docker/sandbox-kit-spec +license: Apache-2.0 +tier: community +marketplace: + category: Security & policy + mark: SK + keywords: [Docker, OCI, sandbox, permissions] +trace_roles: + - record-producer +trace_conformance_level: 0 +tested_against: + agentrust-trace: "0.11.0" + agentrust-trace-tests: "0.6.1" diff --git a/integrations/docker-sandbox-kit/kit_to_trace.py b/integrations/docker-sandbox-kit/kit_to_trace.py new file mode 100644 index 0000000..e5c84f3 --- /dev/null +++ b/integrations/docker-sandbox-kit/kit_to_trace.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +"""Docker Sandbox Kit (v3) manifest -> TRACE Trust Record, policy declared. + +A Kit is one OCI image whose manifest annotation +``vnd.docker.sandbox.kit.descriptor`` carries the Kit's typed capability +requests: network allow and deny lists, credentials by phase, volumes, ports +(`SPEC-v3 `_, +section 1). The published descriptor is, in the spec's words, "what signatures +cover, what the resolver and the gate judge, and what the lock records" +(section 9.1). That makes it a real policy artifact, and this adapter binds its +exact bytes into ``policy.bundle_hash``. + +**Why the record always says ``declared``.** The descriptor is a request. What +a host granted, and what a runtime then enforced, is the permission surface of +the *effective* descriptor, with this installation's create-phase args expanded, +stored in the runtime's lock (sections 7.4 and 10). None of that is in the +image. A record built from the image alone can name the policy the Kit asked +for and nothing more, which is exactly what TRACE ``declared`` means. Claiming +``enforce`` needs evidence from the runtime that it applied this descriptor, and +this adapter does not accept a mode argument that could say otherwise. + +**Why the Kit digest is not the workload digest.** "What runs is never a +published artifact" (section 10): at create, an assembler emits an ordinary +image identified by the lock. The caller supplies the digest of the image that +ran. The Kit manifest digest goes into ``origin.source_event_id`` and +``policy.policy_uri``, where it identifies the evidence rather than the +execution. + +Usage: + python kit_to_trace.py kit-manifest.json \\ + --kit-reference docker.io/docker/sbx-kit-claude-acp-set \\ + --subject spiffe://example.org/agent/claude-acp \\ + --model-provider anthropic --model-id claude-sonnet-4-6 \\ + --workload-digest sha256:<64 hex, the assembled image that ran> \\ + --jwk pubkey.jwk > record.json + +``kit-manifest.json`` is the raw platform manifest, byte for byte, as returned +by ``docker buildx imagetools inspect --raw @`` or +``crane manifest``. Re-serialised JSON has a different digest. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import pathlib +import re +import sys +from dataclasses import dataclass +from typing import Any + +from agentrust_trace_adapters import MissingEvidence, SourceSystem, build_record, digest_bytes + +SPEC_URL = "https://github.com/docker/sandbox-kit-spec/blob/main/docs/spec/SPEC-v3.md" +ADAPTER_URI = "https://github.com/agentrust-io/integrations/tree/main/integrations/docker-sandbox-kit" + +DESCRIPTOR = "vnd.docker.sandbox.kit.descriptor" +SCHEMA_VERSION = "vnd.docker.sandbox.kit.schema-version" +CAPABILITIES = "vnd.docker.sandbox.kit.capabilities" +BUILT_BY = "vnd.docker.sandbox.kit.built-by" +# Annotations a v2 Kit carried. Seeing them means the grammar this adapter reads +# is absent, which deserves a better message than "not a Kit". +V2_MARKERS = ("vnd.docker.sandbox.kit.kind", "vnd.docker.sandbox.kit.name") + +IMAGE_MANIFEST = "application/vnd.oci.image.manifest.v1+json" +IMAGE_INDEX = "application/vnd.oci.image.index.v1+json" +DOCKER_LIST = "application/vnd.docker.distribution.manifest.list.v2+json" + +_REFERENCE_RE = re.compile(r"^[a-z0-9]+([._-][a-z0-9]+)*(:[0-9]+)?(/[a-z0-9]+([._-][a-z0-9]+)*)+$") + + +@dataclass(frozen=True) +class DeclaredPolicy: + """``policy`` block for a descriptor nothing evaluated. + + ``agentrust_trace_adapters.PolicyEvidence`` 0.1.1, the released version, + predates ``declared`` and rejects it; ``agentrust-trace`` 0.9.0 and later + accept it. ``build_record`` reads only ``bundle_hash`` and ``to_policy()``, + so this carries the same two members with the mode fixed. Replace it with + ``PolicyEvidence`` once a release includes the ``declared`` mode. + """ + + bundle: bytes + version: str + policy_uri: str | None = None + + @property + def bundle_hash(self) -> str: + return digest_bytes(self.bundle) + + def to_policy(self) -> dict[str, object]: + block: dict[str, object] = { + "bundle_hash": self.bundle_hash, + "enforcement_mode": "declared", + "version": self.version, + } + if self.policy_uri is not None: + block["policy_uri"] = self.policy_uri + return block + + +@dataclass(frozen=True) +class KitEvidence: + """A Kit's published descriptor, taken from a manifest the caller holds.""" + + manifest_digest: str + descriptor: bytes + kind: str + capability_types: tuple[str, ...] + version: str | None + frontend: str | None + + @classmethod + def from_manifest(cls, raw: bytes, *, expected_digest: str | None = None) -> "KitEvidence": + """Parse and check one platform manifest. Raises rather than guessing.""" + if not isinstance(raw, (bytes, bytearray)) or not raw: + raise MissingEvidence("no manifest bytes: there is no Kit here to describe") + digest = "sha256:" + hashlib.sha256(bytes(raw)).hexdigest() + if expected_digest is not None and digest != expected_digest: + raise MissingEvidence( + f"manifest bytes hash to {digest}, not {expected_digest}. The file was " + "re-serialised or is a different manifest; fetch it raw." + ) + try: + manifest = json.loads(raw) + except ValueError as exc: + raise MissingEvidence(f"manifest is not JSON: {exc}") from exc + if not isinstance(manifest, dict): + raise MissingEvidence("manifest is not a JSON object") + + media_type = manifest.get("mediaType") + if media_type in (IMAGE_INDEX, DOCKER_LIST) or "manifests" in manifest: + raise MissingEvidence( + "this is an image index, not a platform manifest. An index digest names " + "no runnable image, and index annotations are an optimisation a " + "multi-node build can drop (SPEC-v3 section 9.3). Pass the platform " + "manifest the runtime pulled." + ) + if media_type != IMAGE_MANIFEST or "artifactType" in manifest: + raise MissingEvidence( + f"mediaType {media_type!r}: a v3 Kit is a plain OCI image manifest with " + "no artifactType (SPEC-v3 section 10)" + ) + + annotations = manifest.get("annotations") or {} + if DESCRIPTOR not in annotations: + if any(m in annotations for m in V2_MARKERS): + raise MissingEvidence( + "this Kit was published with the v2 grammar, which carries no " + f"{DESCRIPTOR} annotation. Republish it with the v3 frontend." + ) + raise MissingEvidence(f"no {DESCRIPTOR} annotation: this image is not a Kit") + + descriptor_text = annotations[DESCRIPTOR] + if not isinstance(descriptor_text, str) or not descriptor_text: + raise MissingEvidence(f"{DESCRIPTOR} is empty") + try: + descriptor = json.loads(descriptor_text) + except ValueError as exc: + # A YAML-valued annotation from before the switch to JSON would land + # here. Its bytes are still a descriptor, but this adapter has no YAML + # parser to check it with, and an unchecked bundle is not one to bind. + raise MissingEvidence( + f"{DESCRIPTOR} is not JSON ({exc}). Kits published before the frontend " + "switched to compact JSON are not supported." + ) from exc + if not isinstance(descriptor, dict): + raise MissingEvidence(f"{DESCRIPTOR} is not a JSON object") + + schema = descriptor.get("schemaVersion") + if schema != "3" or annotations.get(SCHEMA_VERSION) != schema: + raise MissingEvidence( + f"schemaVersion {schema!r} with annotation " + f"{annotations.get(SCHEMA_VERSION)!r}: this adapter reads schema 3, and " + "the two MUST be equal (SPEC-v3 section 9.3)" + ) + kind = descriptor.get("kind") + if kind not in ("workload", "mixin"): + raise MissingEvidence( + f"kind {kind!r}: a published Kit is a workload or a mixin, never a set " + "(SPEC-v3 section 11)" + ) + + requested = descriptor.get("capabilities") or [] + if not isinstance(requested, list) or not all( + isinstance(c, dict) and isinstance(c.get("type"), str) for c in requested + ): + raise MissingEvidence("descriptor capabilities are not a list of typed entries") + types = tuple(sorted({c["type"] for c in requested})) + # The capabilities annotation is "an index, never a second source". If + # it disagrees with the descriptor, something edited one of them, and a + # policy filter reading the index would judge a different Kit from the + # one this record names. + index = annotations.get(CAPABILITIES) + if (index or None) != (",".join(types) or None): + raise MissingEvidence( + f"{CAPABILITIES} is {index!r} but the descriptor requests " + f"{','.join(types) or 'nothing'}. The index must mirror the descriptor." + ) + + frontend = None + if BUILT_BY in annotations: + try: + built = json.loads(annotations[BUILT_BY]) + frontend = f"{built['name']}/{built['version']}" + except (ValueError, KeyError, TypeError): + frontend = None + + version = descriptor.get("version") + return cls( + manifest_digest=digest, + descriptor=descriptor_text.encode("utf-8"), + kind=kind, + capability_types=types, + version=version if isinstance(version, str) else None, + frontend=frontend, + ) + + +def build_from_kit( + evidence: KitEvidence, + *, + subject: str, + model_provider: str, + model_id: str, + workload_digest: str, + jwk: dict[str, Any], + kit_reference: str | None = None, + data_class: str = "unclassified", + model_version: str | None = None, +) -> dict[str, Any]: + """Map one Kit onto an unsigned Trust Record with a declared policy.""" + policy_uri = None + if kit_reference is not None: + if not _REFERENCE_RE.fullmatch(kit_reference): + raise ValueError( + f"kit_reference {kit_reference!r} must be a registry/repository name " + "with no tag or digest; the digest comes from the manifest bytes" + ) + policy_uri = f"oci://{kit_reference}@{evidence.manifest_digest}" + + record = build_record( + source=SourceSystem( + producer=evidence.frontend or "docker/sandbox-kit", + kind="third-party-control-plane", + source_event_id=evidence.manifest_digest, + ), + subject=subject, + model_provider=model_provider, + model_id=model_id, + model_version=model_version, + policy=DeclaredPolicy( + bundle=evidence.descriptor, + version="docker-sandbox-kit-v3", + policy_uri=policy_uri, + ), + data_class=data_class, + workload_digest=workload_digest, + jwk=jwk, + ) + # TRACE wants a URI here. Name this adapter as the party stating "none", + # not Docker: no appraisal ran, and the record should not imply one did. + record["appraisal"]["verifier"] = ADAPTER_URI + return record + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("manifest", help="Raw platform manifest bytes of the Kit") + ap.add_argument("--expected-digest", help="Refuse unless the manifest hashes to this sha256: digest") + ap.add_argument("--kit-reference", help="Repository the Kit was pulled from, e.g. docker.io/docker/doodle") + ap.add_argument("--subject", required=True, help="spiffe:// or did: identity of the workload") + ap.add_argument("--model-provider", required=True) + ap.add_argument("--model-id", required=True) + ap.add_argument("--model-version") + ap.add_argument( + "--workload-digest", + required=True, + help="sha256: digest of the assembled image that ran, not of the Kit (SPEC-v3 section 10)", + ) + ap.add_argument("--jwk", required=True, help="File holding the public confirmation key (JWK)") + ap.add_argument("--data-class", default="unclassified") + args = ap.parse_args() + + try: + evidence = KitEvidence.from_manifest( + pathlib.Path(args.manifest).read_bytes(), expected_digest=args.expected_digest + ) + record = build_from_kit( + evidence, + subject=args.subject, + model_provider=args.model_provider, + model_id=args.model_id, + model_version=args.model_version, + workload_digest=args.workload_digest, + jwk=json.loads(pathlib.Path(args.jwk).read_text()), + kit_reference=args.kit_reference, + data_class=args.data_class, + ) + except MissingEvidence as exc: + print(f"cannot build a truthful record: {exc}", file=sys.stderr) + return 2 + + json.dump(record, sys.stdout, indent=2) + print() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/integrations/docker-sandbox-kit/requirements.txt b/integrations/docker-sandbox-kit/requirements.txt new file mode 100644 index 0000000..0e4cdf5 --- /dev/null +++ b/integrations/docker-sandbox-kit/requirements.txt @@ -0,0 +1,4 @@ +agentrust-trace==0.11.0 +agentrust-trace-adapters==0.1.1 +agentrust-trace-tests==0.6.1 +pytest>=8 diff --git a/integrations/docker-sandbox-kit/test_kit_to_trace.py b/integrations/docker-sandbox-kit/test_kit_to_trace.py new file mode 100644 index 0000000..a4bb881 --- /dev/null +++ b/integrations/docker-sandbox-kit/test_kit_to_trace.py @@ -0,0 +1,200 @@ +"""Tests for the Docker Sandbox Kit adapter. + +The fixtures are real platform manifests pulled from Docker Hub on 2026-10-01, +kept byte for byte so their digests match the registry. Two things are pinned: +a published Kit becomes a record the released TRACE packages accept, and every +way a manifest can fail to be a v3 Kit ends in a refusal rather than a record. +""" + +from __future__ import annotations + +import hashlib +import json +import pathlib +import subprocess +import sys + +import pytest + +HERE = pathlib.Path(__file__).parent +sys.path.insert(0, str(HERE)) + +from agentrust_trace import TrustRecord, generate_key, iter_errors, key_to_jwk, sign_record, verify_record # noqa: E402 +from agentrust_trace_adapters import MissingEvidence # noqa: E402 +from kit_to_trace import ADAPTER_URI, CAPABILITIES, DESCRIPTOR, KitEvidence, build_from_kit # noqa: E402 + +ACP = HERE / "fixtures" / "claude-acp-set-1.0.1.amd64.manifest.json" +ACP_INDEX = HERE / "fixtures" / "claude-acp-set-1.0.1.index.json" +DOODLE = HERE / "fixtures" / "doodle-2026.amd64.manifest.json" +# Registry digests, as served in Docker-Content-Digest on 2026-10-01. +ACP_DIGEST = "sha256:86d56a3b2ea714d55c2a48b55b9ca64e2145245f43583100dd944eccfb8fe78e" +DOODLE_DIGEST = "sha256:c9bce67af2c384dc0eadee2b4900e9c0a5beb557ed395ce05709eb0989945d91" +RAN = "sha256:" + "e" * 64 + + +def build(evidence, jwk=None, **over): + kwargs = dict( + subject="spiffe://example.org/agent/claude-acp", + model_provider="anthropic", + model_id="claude-sonnet-4-6", + workload_digest=RAN, + jwk=jwk or key_to_jwk(generate_key()), + kit_reference="docker.io/docker/sbx-kit-claude-acp-set", + ) + kwargs.update(over) + return build_from_kit(evidence, **kwargs) + + +def mutate(path, change): + manifest = json.loads(path.read_bytes()) + change(manifest) + return json.dumps(manifest).encode() + + +def test_fixtures_are_the_registry_bytes(): + assert "sha256:" + hashlib.sha256(ACP.read_bytes()).hexdigest() == ACP_DIGEST + assert "sha256:" + hashlib.sha256(DOODLE.read_bytes()).hexdigest() == DOODLE_DIGEST + + +@pytest.mark.parametrize("path", [ACP, DOODLE]) +def test_published_kit_signs_and_verifies(path): + key = generate_key() + jwk = key_to_jwk(key) + record = build(KitEvidence.from_manifest(path.read_bytes()), jwk=jwk) + assert list(iter_errors(record)) == [] + TrustRecord.model_validate(record) + verify_record(sign_record(record, key), jwk) + + +def test_policy_binds_the_descriptor_bytes_and_says_declared(): + evidence = KitEvidence.from_manifest(ACP.read_bytes(), expected_digest=ACP_DIGEST) + record = build(evidence) + annotation = json.loads(ACP.read_bytes())["annotations"][DESCRIPTOR] + policy = record["policy"] + assert policy["bundle_hash"] == "sha256:" + hashlib.sha256(annotation.encode()).hexdigest() + assert policy["enforcement_mode"] == "declared" + assert policy["policy_uri"] == f"oci://docker.io/docker/sbx-kit-claude-acp-set@{ACP_DIGEST}" + assert "com.docker.sandbox/network-policy@1" in evidence.capability_types + assert "com.docker.sandbox/credential@1" in evidence.capability_types + + +def test_kit_digest_identifies_evidence_not_the_execution(): + record = build(KitEvidence.from_manifest(ACP.read_bytes())) + assert record["origin"] == { + "kind": "third-party-control-plane", + "producer": "docker/sandbox-kit/dev", + "source_event_id": ACP_DIGEST, + } + assert record["build_provenance"]["digest"] == RAN + assert record["runtime"]["platform"] == "software-only" + assert record["appraisal"] == {"status": "none", "verifier": ADAPTER_URI} + + +def test_built_by_absent_falls_back_to_frontend_name(): + record = build(KitEvidence.from_manifest(DOODLE.read_bytes())) + assert record["origin"]["producer"] == "docker/sandbox-kit" + + +def test_refuses_an_index(): + with pytest.raises(MissingEvidence, match="image index"): + KitEvidence.from_manifest(ACP_INDEX.read_bytes()) + + +def test_refuses_reserialised_bytes_when_digest_is_pinned(): + raw = json.dumps(json.loads(ACP.read_bytes()), indent=2).encode() + with pytest.raises(MissingEvidence, match="re-serialised"): + KitEvidence.from_manifest(raw, expected_digest=ACP_DIGEST) + + +def test_refuses_an_image_that_is_not_a_kit(): + raw = mutate(ACP, lambda m: m["annotations"].pop(DESCRIPTOR)) + with pytest.raises(MissingEvidence, match="not a Kit"): + KitEvidence.from_manifest(raw) + + +def test_names_the_v2_grammar(): + def to_v2(m): + m["annotations"] = {"vnd.docker.sandbox.kit.kind": "agent", "vnd.docker.sandbox.kit.name": "x"} + + with pytest.raises(MissingEvidence, match="v2 grammar"): + KitEvidence.from_manifest(mutate(ACP, to_v2)) + + +def test_refuses_capability_index_that_disagrees_with_descriptor(): + raw = mutate(ACP, lambda m: m["annotations"].__setitem__(CAPABILITIES, "com.docker.sandbox/sbx@1")) + with pytest.raises(MissingEvidence, match="must mirror"): + KitEvidence.from_manifest(raw) + + +def test_refuses_capability_index_on_a_kit_that_requests_nothing(): + def strip(m): + d = json.loads(m["annotations"][DESCRIPTOR]) + d["capabilities"] = [] + m["annotations"][DESCRIPTOR] = json.dumps(d) + + with pytest.raises(MissingEvidence, match="must mirror"): + KitEvidence.from_manifest(mutate(DOODLE, strip)) + + +def test_refuses_schema_version_disagreement(): + raw = mutate(ACP, lambda m: m["annotations"].__setitem__("vnd.docker.sandbox.kit.schema-version", "2")) + with pytest.raises(MissingEvidence, match="MUST be equal"): + KitEvidence.from_manifest(raw) + + +def test_refuses_a_set(): + def to_set(m): + d = json.loads(m["annotations"][DESCRIPTOR]) + d["kind"] = "set" + m["annotations"][DESCRIPTOR] = json.dumps(d) + + with pytest.raises(MissingEvidence, match="never a set"): + KitEvidence.from_manifest(mutate(ACP, to_set)) + + +def test_refuses_an_artifact_manifest(): + raw = mutate(ACP, lambda m: m.__setitem__("artifactType", "application/vnd.example")) + with pytest.raises(MissingEvidence, match="no artifactType"): + KitEvidence.from_manifest(raw) + + +def test_refuses_yaml_descriptor(): + raw = mutate(ACP, lambda m: m["annotations"].__setitem__(DESCRIPTOR, "schemaVersion: '3'\nkind: workload\n")) + with pytest.raises(MissingEvidence, match="not JSON"): + KitEvidence.from_manifest(raw) + + +def test_requires_the_digest_of_what_ran(): + with pytest.raises(MissingEvidence, match="build_provenance.digest"): + build(KitEvidence.from_manifest(ACP.read_bytes()), workload_digest=None) + + +def test_rejects_a_reference_carrying_its_own_digest(): + with pytest.raises(ValueError, match="no tag or digest"): + build(KitEvidence.from_manifest(ACP.read_bytes()), kit_reference=f"docker.io/docker/x@{ACP_DIGEST}") + + +def test_cli_emits_a_record_and_refuses_an_index(tmp_path): + jwk = tmp_path / "pub.jwk" + jwk.write_text(json.dumps(key_to_jwk(generate_key()))) + common = [ + "--subject", "spiffe://example.org/agent/claude-acp", + "--model-provider", "anthropic", + "--model-id", "claude-sonnet-4-6", + "--workload-digest", RAN, + "--jwk", str(jwk), + ] + ok = subprocess.run( + [sys.executable, str(HERE / "kit_to_trace.py"), str(ACP), "--expected-digest", ACP_DIGEST, *common], + capture_output=True, text=True, check=False, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + assert ok.returncode == 0, ok.stderr + assert json.loads(ok.stdout)["policy"]["enforcement_mode"] == "declared" + refused = subprocess.run( + [sys.executable, str(HERE / "kit_to_trace.py"), str(ACP_INDEX), *common], + capture_output=True, text=True, check=False, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + assert refused.returncode == 2 + assert "image index" in refused.stderr diff --git a/marketplace/catalog.json b/marketplace/catalog.json index 2d5b540..46e1492 100644 --- a/marketplace/catalog.json +++ b/marketplace/catalog.json @@ -1,6 +1,6 @@ { "catalog_version": 1, - "count": 41, + "count": 42, "integrations": [ { "name": "Claude Code", @@ -454,6 +454,29 @@ "policy" ] }, + { + "name": "Docker Sandbox Kit", + "package_name": "Docker Sandbox Kit", + "vendor": "agentrust-io", + "description": "Binds a Docker Sandbox Kit v3 descriptor into a TRACE Trust Record as a declared policy, refusing any manifest that is not a published v3 Kit.", + "path": "integrations/docker-sandbox-kit", + "url": "https://github.com/agentrust-io/integrations/tree/main/integrations/docker-sandbox-kit", + "homepage": "https://github.com/docker/sandbox-kit-spec", + "repository": "https://github.com/agentrust-io/integrations", + "tier": "community", + "stack": [ + "TRACE" + ], + "category": "Security & policy", + "mark": "SK", + "featured": null, + "keywords": [ + "Docker", + "OCI", + "sandbox", + "permissions" + ] + }, { "name": "EPI Recorder", "package_name": "EPI Recorder",