Repository navigation
A sandbox that runs an agent's code is only as private as the wire that carries the code. This release closes the one open wire in the Kubernetes agent-sandbox provider, and it breaks existing agent-sandbox setups until the operator adds a certificate. The rest includes four example pages (a 40-agent recipe experiment, agent memory through a grant, and two lessons for newcomers), image scanning at startup, and fixes from reviews.
Version: v0.26.0.
Breaking, for Kubernetes agent-sandbox operators: the channel to each pod's sandboxd is now TLS, and a deployment without it does not start. Before, plimsolld sent the sandboxd token, the code, each project's plan with the run's report key and every result over cleartext HTTP/2 on the pod network, where anyone on that path could read or change them. Now each pod runs plimsoll's own small TLS terminator in front of sandboxd, and plimsolld connects over TLS 1.3 to a certificate that chains to a certificate authority (CA) you name, never the system's roots. A pod that cannot prove itself is sent nothing, and the run is refused as not dispatched. What you must do:
- Make a CA and one server certificate for the name plimsolld checks (
sandboxd.plimsoll.invalidby default; it never resolves, because plimsolld dials each pod's address). openssl or cert-manager's CA issuer both work. - Put the certificate and key in a
kubernetes.io/tlsSecret in the warm pool's namespace, and put the CA certificate (PEM, certificates only) in a file on the plimsolld host. - Set
SANDBOX_AGENTSANDBOX_SANDBOXD_CA_FILEto that file. It is now required. SetSANDBOX_AGENTSANDBOX_SANDBOXD_SERVER_NAMEonly if your certificate carries another name. - Rebuild the image from
docker/agentsandbox.Dockerfile: it now carries the terminator at/usr/local/bin/plimsoll-sandboxd-tls. Update the template to match: that binary is the container's command, followed by sandboxd's flags with--listen-host=127.0.0.1and--grpc-port=9091added; the Secret is mounted read-only at/etc/plimsoll/sandboxd-tlswithdefaultMode: 0400; and ingress is allowed only to port 9090. The startup read-back refuses a template that differs. - Restart plimsolld. Its startup smoke test now takes the same TLS path, so a wrong CA or name stops it from serving, with the CA file and name in the message.
Hardened mode still requires SANDBOX_AGENTSANDBOX_ENCRYPTED_POD_NETWORK=1, your statement that the pod network is authenticated and encrypted, until per-pod keys land: every pod's terminator serves the same certificate, so the channel proves the peer is a plimsoll sandbox pod of your deployment, not the pod plimsolld dialed. Someone who can rewrite traffic on the pod network could redirect one caller's connection to another pod of the deployment (for example one their own run holds) and receive that run's code, files and token; the encrypted, authenticated pod network is what stops them until then. Rotate the Secret and the sandboxd token on a schedule; the operator guide has the steps, including how to change the CA without downtime (agent-sandbox guide). The channel was run live on a kind cluster under gVisor, runc and Kata, and refused a pod with another CA or another name.
Breaking, for Docker operators who configure a withheld set: unreadable image files now stop startup unless you have explicitly listed them for that image. After reviewing the bytes the scan cannot read, run plimsoll-withheld known-unreadable -set withheld.json -known known-images.json IMAGE, replacing withheld.json with your withheld-set file and IMAGE with each configured image. Set PLIMSOLL_WITHHELD_KNOWN_FILE to the resulting known-images.json before restarting. Lists bind the image's content ID, file path, unreadable format or limit, and size; rebuilding an image requires a new list. This also applies to audit-only sets. Listing acknowledges unread bytes, not that they contain no secrets. The image-scan guide explains the limits and the command.
Server and clients stay on protocol 5. No request or record format changed, so existing clients keep working against this daemon.
Also included:
- Two new example pages. Does a fresh agent follow the plan it is handed?: 40 fresh agents built a drone controller from an honest 32-token recipe, the same words shuffled, or nothing; the recipe's order moved the landings, no recipe item reached the code by name, and every recipe agent said it followed the recipe only partly. Every number comes from signed run records, and
examples/squeeze -staged -verifyrechecks them. Agent memory through a grant: agent-written code stores and recalls a memory in a hosted memory API through a grant thatplimsoll-specgenbuilt from the API's OpenAPI document; the key stays in the broker, the trace names route templates only, and two calls outside the grant are refused. The API is a local fake, so it needs no account and spends nothing. - The squeeze page keeps what it says about the ship scan and the two other checks, and no longer describes how the answer-key tripwire matches values or how the canary run is carried out. Both of those checks are still not part of the open-source release; the page keeps what each protects against and the canary run's recorded output. Copies from v0.25.0 and the module mirror keep the old text.
- The ship scan reads images:
plimsoll-withheld scan-imagereads a docker image's files against a withheld set, and plimsolld with the docker provider and a withheld set scans each image it runs at startup, by the content ID it verified, and refuses to serve while a refuse-mode entry is in one. An audit-mode find is logged. Any unreadable file not on its image's known list now refuses startup, including with audit-only sets;plimsoll-withheld known-unreadable -set SET -known FILE IMAGEwrites the list, andPLIMSOLL_WITHHELD_KNOWN_FILEloads it. Lists match the image ID, path, reason and whole file size; rebuilt images need a new list. With no list configured, every unreadable file is unknown (ship scan). - The SDK grants page and example gain a live mode against the real OpenAI-compatible and Anthropic APIs, off by default, reached by no test, with a call budget in code; the verifiers page gains a real-model section with the key held by a loopback relay and the billed tokens capped there.
- Time: durations and deadlines use the monotonic clock, never differences of wall-clock readings. On a host whose time service steps the clock, the in-sandbox project runner's shared deadline and step durations could be wrong, and under gVisor a process's start time and uptime follow the wall clock, which made a session sweep kill a process that was still young.
- Docker provider: every container names the sandbox instance that started it (a random
io.plimsoll.instancelabel), so tests and orphan reconciliation count only their own containers.sessionkit.ControlArgvrefuses an environment variable that would choose what a control program loads or runs (aPATHentry used to replace the fixed one). - The physics oracle's controller runs under node's permission model with reads only on the files beside it, so it cannot open the plant module, the runner, or run child processes, workers, addons or writes.
- Broker: the credential can go in a named header or as HTTP basic auth, for APIs that do not use
Authorization: Bearer; the default is unchanged.plimsoll-specgenstarts every route with the OpenAPI server's base path, which a document served under a path (such as/client/v4) used to lose. - JavaScript dependencies: refreshed the optional framework test dependencies and the Trigger.dev example; the client's locked graph passes npm audit. The example retains a documented development-only minimatch advisory: the affected optimization mode is not enabled by its command-line tool. The Zod input-validation peer minimum is now 4.6.6, its first patched release. The example documents its development-tool overrides.
- MCP server:
SetCallFileslets an embedder write files into everyrun_codecall, fresh or in a sandbox, before the call's own. - Tests: the sandbox, OpenShell and docker suites run many of their tests in parallel, and the race pass no longer runs the sandbox package's hermetic tests one by one; QuickJS compiles once per process, as its cache's comment always claimed. A startup race in the session expiry tests is gone.
- Time witnesses for receipts:
attest/tsanow ships the fetch as well as the check.Authority.Fetchsends one time-stamp request (a digest and a random nonce, nothing else) to an RFC 3161 time-stamp authority, andtsa.Publicnames the two public authorities plimsoll's tools pin, DigiCert and FreeTSA, with their root certificates, so a token from either must chain to the pinned root. Before, the open-source code could check a time-stamp token but not get one. The tests check recorded answers from both authorities and make no network call. - Two pages for people new to sandboxes: what a sandbox is runs five real programs in plimsoll's weakest isolation and explains the four isolation tiers as rooms with different locks, and proof of what ran explains the signed record of every run and lets you check five real records in the browser.
- The README and the site index list the four new pages.
Not in this release: the AWS Lambda microVM provider (it has no live smoke test yet), driver mode's conformance suite (the mode does not exist yet), a daemon rule that proves a required tool ran before a hand-in, and a published format for the signed reading record (both planned for a later release).
A signed record protects recorded statements from unnoticed changes. It does not prove that code is correct or safe. Isolation tiers describe configuration and provider evidence plus startup checks, not hardware attestation.