plimsoll on Kubernetes agent-sandbox: signed runs, their bills of materials, and five refusals

plimsoll is a service that runs code an AI agent wrote and says, on every run, behind which boundary it ran. Kubernetes agent-sandbox is a Kubernetes project that keeps sandboxes as pods (Kubernetes' unit of one or more containers) in a warm pool, started ahead of time so a run does not wait for one. This page is the same two real requests run through plimsoll as agent-sandbox under gVisor (recorded 2026-10-23 20:09 UTC) and agent-sandbox under Kata Containers (recorded 2026-10-23 20:10 UTC), each run signed and converted into a bill of materials, what a pod's kernel said in the vm room, and five things plimsoll refused to do on the same cluster. Every claim below is either signed, and checkable with the commands at the end, or marked as a transcript.

Boundaries
kernel · vm
agent-sandbox under gVisor and agent-sandbox under Kata Containers: the tier plimsoll read and checked in each pod, in hardened mode
Signed runs
4
each with a CycloneDX bill of materials
Same request, 4 rooms
same results
agent-sandbox under gVisor, agent-sandbox under Kata Containers, docker under gVisor and a Docker Cloud microVM ran the same request bytes
Refusals
5
each before any code ran, or before the daemon served

Words this page uses

What happens on each run

The caller code to run, and the floor: kernel plimsolld, hardened 1. reads the template, runtime class and policies 2. claims one pod from the warm pool 3. reads that pod back: image digest, spec, policies 4. its own probe first: kernel log, boot ID, seccomp 5. sends the run's code; returns the result and the record 6. deletes the claim, so the pod is gone any check that fails: refused, nothing ran One fresh pod gVisor's kernel (runsc), or a Kata virtual machine with a Linux kernel of its own sandboxd, agent-sandbox's daemon in the pod the code, as uid 61000, no network out attest harness, outside the daemon checks the record, signs it into bundle.jsonl examples/sbom one CycloneDX file per signed line

Step 4 is new. A handler's name is configuration on each node, which the Kubernetes API does not show, so before any of the run's code plimsoll runs one command of its own in the claimed pod, as the guest. Under gVisor the first line of the kernel log must be gVisor's own boot line. Under Kata it must be a Linux kernel's boot line, and the kernel's boot ID must differ from the one the pod's node reports through the API, since a pod started on the node's own kernel shares the node's boot ID. Under runc the seccomp mode must show a filter. The pod is fresh, so nothing of a guest's has run in it yet. On this cluster the check cost nothing measurable under gVisor: the median snippet took 75 ms from claim to result with it and without it, over ten runs each. Refusal 4 below is why it exists, and the vm room's section shows what it reads in a pod of that room.

The signed runs

The program is a three-file invoice reconciliation an agent might write, the same one the INTERNAL · CycloneDX example page → uses; the second request adds one step that tries to fetch a web page. This page sent each request once as agent-sandbox under gVisor and agent-sandbox under Kata Containers, each time through a hardened daemon for that runtime class, and the harness signed every answer with the key in INTERNAL · harness.pub →.

node reconcile.test.js, then node main.js

Request digest00c95be443d358949d123d77464e9c40b03dafd3f3094b709d8719b004c0a7a4
Floorkernel, the weakest tier the request accepts
Imagesha256:31d84a633d71c261126efc1f2b40efdabc65e50857ce3f7ca6136e45c1b15430, in every room on this page
RoomOutcomeTierResult and record digestsSigned, and its bill of materials
agent-sandbox under gVisor
runtime class gvisor, started 2026-10-23T20:09:44Z
completed in 527 mskernelresult 7a38c50fe500b5827774fc68352cc66a153f44e89e7a7f6d6965f5823ad404d9
record bf8725dad834b3ad5c4c24b66427b7d209c372016d7e8814168e8387cba809dc
line 1 of INTERNAL · bundle.jsonl →
INTERNAL · run-bf8725dad834.cdx.json →
agent-sandbox under Kata Containers
runtime class kata-clh-runtime-rs, started 2026-10-23T20:10:02Z
completed in 770 msvmresult 7a38c50fe500b5827774fc68352cc66a153f44e89e7a7f6d6965f5823ad404d9
record 6f6ccbd10029e02c7afbf5328a4f3641fec293af481e22e6eae2eed097056ee5
line 1 of INTERNAL · kata/bundle.jsonl →
INTERNAL · kata/run-6f6ccbd10029.cdx.json →
What each step printed, in every room above: one result digest
StepExitTimeOutput
node reconcile.test.js098 ms
reconcile: 3 assertions passed
node main.js0104 ms
{"invoices":4,"payments":4,"matched":["INV-2001","INV-2002"],"unpaid":[{"id":"INV-2003","owed":7250}],"overpaid":[{"id":"INV-2004","excess":1000}]}

node reconcile.test.js, then node main.js, then node egress.js

Request digestb5386b8a32215a20f843fddccc796543ab45ff0bf7fa63d38ca87855d201a7e1
Floorkernel, the weakest tier the request accepts
Imagesha256:31d84a633d71c261126efc1f2b40efdabc65e50857ce3f7ca6136e45c1b15430, in every room on this page
RoomOutcomeTierResult and record digestsSigned, and its bill of materials
agent-sandbox under gVisor
runtime class gvisor, started 2026-10-23T20:09:44Z
completed in 5755 mskernelresult 960733790831a2bc56def99ba8f069c2921817a8aae9303c62a4f2c87ccc977d
record 27382a699379b867600ce8a7c9d7e6b0c5048d94c56d1d023393cf76a7384eae
line 3 of INTERNAL · bundle.jsonl →
INTERNAL · run-27382a699379.cdx.json →
agent-sandbox under Kata Containers
runtime class kata-clh-runtime-rs, started 2026-10-23T20:10:03Z
completed in 6209 msvmresult 960733790831a2bc56def99ba8f069c2921817a8aae9303c62a4f2c87ccc977d
record 38070ad2aa8d3724f9b3e3752958efbc2a62a10397856838e98f11593eacc37f
line 3 of INTERNAL · kata/bundle.jsonl →
INTERNAL · kata/run-38070ad2aa8d.cdx.json →
What each step printed, in every room above: one result digest
StepExitTimeOutput
node reconcile.test.js0107 ms
reconcile: 3 assertions passed
node main.js0104 ms
{"invoices":4,"payments":4,"matched":["INV-2001","INV-2002"],"unpaid":[{"id":"INV-2003","owed":7250}],"overpaid":[{"id":"INV-2004","excess":1000}]}
node egress.js35238 ms
egress: refused: TimeoutError

The network step's exit 3 is the program's own report that its fetch failed: the pod has no route out, so the attempt times out. That is a result, not an error, and the run completed. A step's time is the first room's.

The same request in 4 rooms

The INTERNAL · CycloneDX example → sent these exact request bytes to docker under gVisor and a Docker Cloud microVM. The request digest covers the files, steps, floor and timeout, so equal digests mean equal requests. The result digest covers what each step printed and its exit code, not how long it took.

node reconcile.test.js, then node main.js · request 00c95be443d3…
RoomTierImage identity the record statesResult digestTime
agent-sandbox under gVisor (this page)
provider agentsandbox
kernelagentsandbox-image:sha256:31d84a633d71c261126efc1f2b40efdabc65e50857ce3f7ca6136e45c1b154307a38c50fe500… same527 ms · INTERNAL · its BOM →
agent-sandbox under Kata Containers (this page)
provider agentsandbox
vmagentsandbox-image:sha256:31d84a633d71c261126efc1f2b40efdabc65e50857ce3f7ca6136e45c1b154307a38c50fe500… same770 ms · INTERNAL · its BOM →
docker under gVisor
provider docker
kerneldocker-image:sha256:32da77b1be24de4c152667c28ab63531425cd69000acf30a328985cb857377f77a38c50fe500… same923 ms · INTERNAL · its BOM →
a Docker Cloud microVM
provider dockercloud
vmdockercloud-image:sha256:2c752226d477b4a886378baa95b9af252be59301b725fdb0b7e15208131505a87a38c50fe500… same4330 ms · INTERNAL · its BOM →
node reconcile.test.js, then node main.js, then node egress.js · request b5386b8a3221…
RoomTierImage identity the record statesResult digestTime
agent-sandbox under gVisor (this page)
provider agentsandbox
kernelagentsandbox-image:sha256:31d84a633d71c261126efc1f2b40efdabc65e50857ce3f7ca6136e45c1b15430960733790831… same5755 ms · INTERNAL · its BOM →
agent-sandbox under Kata Containers (this page)
provider agentsandbox
vmagentsandbox-image:sha256:31d84a633d71c261126efc1f2b40efdabc65e50857ce3f7ca6136e45c1b15430960733790831… same6209 ms · INTERNAL · its BOM →
docker under gVisor
provider docker
kerneldocker-image:sha256:32da77b1be24de4c152667c28ab63531425cd69000acf30a328985cb857377f7960733790831… same6163 ms · INTERNAL · its BOM →
a Docker Cloud microVM
provider dockercloud
vmdockercloud-image:sha256:2c752226d477b4a886378baa95b9af252be59301b725fdb0b7e15208131505a8960733790831… same8550 ms · INTERNAL · its BOM →

Both requests gave byte-identical results in all 4 rooms, the network step's failed fetch included: in each, the program's attempt timed out with nowhere to go.

The vm room: agent-sandbox under Kata Containers

Runtime class kata-clh-runtime-rs, handler kata-clh-runtime-rs, which plimsoll reads as the vm tier: Kata Containers 4.2.0 (installed by its kata-deploy chart), Cloud Hypervisor on runtime-rs, Kata's Rust runtime; each pod is a virtual machine sized to the pod's 256 MiB, plus a fixed 130 MiB and 250 millicores the runtime class adds for the VM. Its runs were signed on 2026-10-23 20:10 UTC; the pool's template and the daemon's settings are under how this was recorded.

What a pod's kernel said

In each claimed pod, before any of a run's code, plimsoll reads as the guest the kernel log's first line, which must be a Linux kernel's boot line, and the kernel's boot ID, which must differ from the boot ID the pod's node reports. It keeps neither value, so one more request was signed in a third pod of the same pool: a snippet that reads them again after plimsoll's own check had passed in that pod. The node's side was read from the Kubernetes API just after, at the time the table gives.

The guest, in the podThe node, from the Kubernetes API
Kernel[ 0.000000] Linux version 6.18.35 (@540110ce73d4) (gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #1 SMP Tue Sep 15 07:34:33 UTC 20266.6.87.2-microsoft-standard-WSL2
Boot IDd8776183-1e63-42c1-b396-fed1f8f370db2c96a215-a66a-4657-9304-92279b9201e3
Sourcesigned: line 1 of INTERNAL · kata/kernel.jsonl →, record cf45974ad915…, a snippet whose floor was vmtranscript: the node object's status.nodeInfo, read 2026-10-23T20:10:10Z

two kernels The boot IDs differ, so the pod's processes did not run on the node's kernel: a pod that a node's Kata handler started under runc shares the node's kernel, and with it the node's boot ID.

Everything the snippet printed
kernel log, first line: [    0.000000] Linux version 6.18.35 (@540110ce73d4) (gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #1 SMP Tue Sep 15 07:34:33 UTC 2026
boot id: d8776183-1e63-42c1-b396-fed1f8f370db
/proc/version: Linux version 6.18.35 (@540110ce73d4) (gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #1 SMP Tue Sep 15 07:34:33 UTC 2026
MemTotal: 263160 kB
Seccomp: 0
mount /: virtiofs ro
mount /work: tmpfs rw
mount /tmp: tmpfs rw
mount /dev/shm: tmpfs rw

What the startup smoke test proved

Before serving, the daemon claimed two pods of the pool, as runs do, and checked from inside what reading the configuration cannot show: the kernel log begins [ 0.000000] Linux version 6.18.35 (@540110ce73d4) (gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0, GNU ld (GNU Binutils for Ubuntu) 2.38) #1 SMP Tue Sep 15 07:34:33 UTC 2026; of 4901 paths the guest could reach, writes succeeded only in /dev/shm, /tmp, /work, each a tmpfs of the guest's own kernel, so in the virtual machine's memory. No volume was filled to its limit, since under Kata none keeps it:

/dev/shm is tmpfs rw,relatime
/tmp is tmpfs rw,relatime
/work is tmpfs rw,relatime

Every connection out failed:

tcp 1.1.1.1:443: timeout
tcp 10.244.0.1:10250: timeout
tcp 169.254.169.254:80: timeout
tcp 172.24.0.2:6443: timeout
tcp 8.8.8.8:53: timeout

What this room shows

What it does not show

Five refusals

Each of these is a transcript, unsigned, recorded on this cluster on 2026-10-18 04:23:27 UTC, with the first room's earlier runs; the rooms' signed runs above were made again on the dates they state, for record version 4: a refusal made before any code ran leaves no run record, since there is no result to state. The caller's lines are what plimsoll-attest run printed; the daemon's line is from its log, where an environment refusal's detail is kept, since it can carry the cluster's own error text.

1. The template edited under a running daemon

What was done

With the hardened daemon serving, one environment variable was added to the template's container, a field the per-run read-back does not decode, and the reconcile request was sent again.

What the caller got
plimsoll-attest: refused before dispatch (reason environment), so nothing ran; the bundle is unchanged: internal: internal error eb60cffd4ea34aee; the daemon's log holds its detail
What the daemon logged
agentsandbox: template evidence-gvisor changed after the startup smoke test proved it; plimsoll runs only on the template it proved, so restart plimsolld to prove this one
Then

With the edit removed, the next request was refused too, reason isolation, because the failed check had set the daemon's tier to unknown (“failed_precondition: sandbox isolation requirement not met: provider isolation unknown is below requested minimum kernel”). Readiness proved the template again 7 seconds after the edit was removed, and the same request then ran (“ran on agentsandbox (kernel): PROJECT_OUTCOME_COMPLETED, 2 steps”).

What it shows

Once the startup smoke test passes, only the template it proved runs, as a docker image tag re-pointed after startup is refused. A field plimsoll does not read cannot change under a running daemon.

2. Hardened mode on the runc pool

What was done

The same hardened settings, pointed at the pool whose runtime class names runc.

What the daemon logged
provider "agentsandbox" reports isolation "container"; hardened mode requires vm (e2b, dockercloud) or verified kernel (docker with SANDBOX_DOCKER_RUNTIME=runsc, agentsandbox on a runsc runtime class)
Then

The daemon did not start.

What it shows

runc is an ordinary container, the container tier, and hardened mode serves only a VM or a verified user-space kernel.

3. A kernel floor on the runc pool

What was done

The same settings without hardened mode: the daemon started at the container tier (its smoke test found a seccomp filter in force, mode 2), and the reconcile request, whose floor is kernel, was sent to it.

What the caller got
plimsoll-attest: refused before dispatch (reason isolation), so nothing ran; the bundle is unchanged: sandbox isolation requirement not met
failed_precondition: sandbox isolation requirement not met: provider isolation container is below requested minimum kernel
Then

No pod was claimed.

What it shows

The floor is the caller's: a request states the weakest tier it accepts, and a provider that cannot meet it refuses before anything runs.

4. A runsc handler that is not gVisor, under a running daemon

What was done

With the hardened daemon serving after a passing smoke test under gVisor, the node's containerd configuration was edited so that its runsc handler starts ordinary runc containers, and the pool's pods were recreated. The runtime class still names runsc, so the Kubernetes API shows nothing different. A snippet that prints the kernel version was then sent.

What the caller got
plimsoll-attest: refused before dispatch (reason environment), so nothing ran; the bundle is unchanged: internal: internal error 63a568140598488b; the daemon's log holds its detail
What the daemon logged
agentsandbox: claim plimsoll-757ad683977cf1c7: the runtime class's handler is runsc but the guest's kernel log begins "[178435.536685]  dump_header+0x51/0x260", not gVisor's
Then

Before the fix this recording led to, the same setup let a daemon run this snippet and sign it at the kernel tier.

That run's record is published as it was signed, by the same harness key, under record version 2: INTERNAL · before-the-fix.jsonl →, record 106b10d3df55…, provider agentsandbox, tier kernel, exit 0. This page states it as a transcript: today's verifier reads only record version 4, and the run cannot be made again, because the per-pod check it led to refuses it. Release v0.22.0, which published these bytes, verifies it: git checkout v0.22.0 && go run ./cmd/plimsoll-attest verify -pub docs/examples/agentsandbox/harness.pub docs/examples/agentsandbox/before-the-fix.jsonl. What the snippet printed:

Linux version 6.6.87.2-microsoft-standard-WSL2 (root@439a258ad544) (gcc (GCC) 11.2.0, GNU ld (GNU Binutils) 2.37) #1 SMP PREEMPT_DYNAMIC Thu Jun  5 18:30:46 UTC 2025
Seccomp:	0

The signature is genuine and the claim was wrong: a signature proves who stated a record and that it was not changed, not that the statement was true. That is why the tier has to be checked where the code runs.

What it shows

A handler's name is configuration on each node, which the Kubernetes API does not show. So plimsoll's first command in every claimed pod, before any of the run's code, reads the kernel log and the seccomp mode as the guest, and a pod that does not match its tier is refused.

5. The same node, a fresh daemon

What was done

A new hardened daemon started against the remapped node.

What the daemon logged
sandbox smoke test: agentsandbox smoke: boundary test: agentsandbox: claim plimsoll-ded06d7ff709ec7b: the runtime class's handler is runsc but the guest's kernel log begins "[178435.536685]  dump_header+0x51/0x260", not gVisor's
Then

The daemon did not start. The node was then restored and the pool's pods recreated under gVisor.

What it shows

The startup smoke test claims pods as runs do, so it meets the same check.

What the startup smoke test proved under gVisor and runc

Before serving, the daemon claimed two pods as runs do and checked from inside what reading the configuration cannot show (the vm room's smoke test is in its own section). Under gVisor: the kernel log begins [ 0.000000] Starting gVisor...; of 4902 paths the guest could reach, writes succeeded only in /dev/shm, /tmp, /work; each volume refused writes once full (/tmp 32 MiB then ENOSPC; /work 128 MiB then ENOSPC); and every connection out failed:

tcp 1.1.1.1:443: timeout
tcp 10.244.0.1:10250: timeout
tcp 169.254.169.254:80: timeout
tcp 172.24.0.2:6443: timeout
tcp 8.8.8.8:53: timeout

Under runc (refusal 3's daemon), the guest's seccomp mode was 2, a filter in force, and the guest could create POSIX message queues (created in /dev/mqueue, at most 819200 bytes for the guest's uid), which the smoke test accepts only under a small per-user limit.

Check it yourself

From a checkout of the repository. Each verify checks every signature, digest and link in one bundle; each -check rebuilds a bill of materials from the signed line it attaches and compares it, pinning the harness key you trust.

go run ./cmd/plimsoll-attest verify -pub docs/examples/agentsandbox/harness.pub docs/examples/agentsandbox/bundle.jsonl
go run ./cmd/plimsoll-attest verify -pub docs/examples/agentsandbox/harness.pub docs/examples/agentsandbox/kata/bundle.jsonl
go run ./cmd/plimsoll-attest verify -pub docs/examples/agentsandbox/harness.pub docs/examples/agentsandbox/kata/kernel.jsonl
cd examples/sbom
go run . -check ../../docs/examples/agentsandbox/run-bf8725dad834.cdx.json \
  -trusted-pub ../../docs/examples/agentsandbox/harness.pub \
  -check-bundle ../../docs/examples/agentsandbox/bundle.jsonl
go run . -check ../../docs/examples/agentsandbox/kata/run-6f6ccbd10029.cdx.json \
  -trusted-pub ../../docs/examples/agentsandbox/harness.pub \
  -check-bundle ../../docs/examples/agentsandbox/kata/bundle.jsonl
cd ../..
go run ./examples/agentsandbox   # writes this page again from the files beside it

The harness key is the CycloneDX example's (key ID 88d311fb942e0fd87cdb1214b385db5fc354c166e84763f6d37885dbc96df379): one operator's harness signed both pages' runs.

How this was recorded

What this does not show