Grants for standard client libraries
The agent's code calls the OpenAI and Anthropic libraries as they ship, and never holds the key
Agent-written code reaches for the client library it knows: the OpenAI or Anthropic SDK (software development kit, the vendor's own package for calling its API). That library wants an API key, and you do not want to hand your key to an agent's code. In plimsoll, a grant (INTERNAL · glossary →), one run's permission to call listed routes of one API, can now carry what the library needs: the run's sandbox (INTERNAL · glossary →) gets a local address, the library is told to use it and is handed a dummy key, and the broker (INTERNAL · glossary →), the part of plimsolld (the plimsoll server, or daemon) outside the sandbox that makes every API call, checks each request and puts the real key on it. Below, both libraries ran unmodified that way, and the API they called wrote down everything it received.
The headline numbers, each a check computed from the files beside this page (13 of 13 hold):
Run on 2026-10-29, with openai 3.41.0 and anthropic 2.3.0 on Python 3.14.8, on one development machine. Every run's run record (INTERNAL · glossary →), plimsoll's statement of what was sent, what came back and where it ran, is signed into bundle.jsonl.
The 30-second version
The operator writes a grant profile (INTERNAL · glossary →), a grant stored on the server under a name: the routes it allows, the real key (named by an environment variable of the daemon's), and two new fields. sdk_env is the environment the library reads; pass_headers names the headers the broker may copy from the library's request.
A run that selects the profile gets plimsoll's forwarder listening on 127.0.0.1:8471 inside the sandbox, and those variables in its environment. The container still has no network: 127.0.0.1 is its own loopback, the address a machine uses to reach itself, and the forwarder hands each connection, byte for byte, to the broker's Unix socket, a local connection through a file rather than a network.
The broker checks the route, rebuilds the request with only the passed headers and the real key, sends it to the API, and passes the answer back: a streamed one piece by piece, as it arrives.
The two profiles this run used, as the daemon read them from its grants file (grants.json; the base URLs are the stub's two loopback ports):
{
"openai": {
"base_url": "http://127.0.0.1:45519",
"allow": [
"POST /v1/chat/completions",
"POST /v1/files"
],
"sdk_env": {
"OPENAI_API_KEY": "dummy-openai-key-in-the-sandbox",
"OPENAI_BASE_URL": "{origin}/v1"
},
"route_max_request_bytes": {
"POST /v1/files": 16777216
},
"route_content_types": {
"POST /v1/files": [
"multipart/form-data"
]
},
"max_request_bytes": 67108864,
"allowed_callers": [
"sdk-grants-example"
],
"token": {
"type": "static",
"env": "STUB_OPENAI_KEY"
}
}
}{
"anthropic": {
"base_url": "http://127.0.0.1:43323",
"allow": [
"POST /v1/messages"
],
"pass_headers": [
"anthropic-version"
],
"sdk_env": {
"ANTHROPIC_API_KEY": "dummy-anthropic-key-in-the-sandbox",
"ANTHROPIC_BASE_URL": "{origin}"
},
"allowed_callers": [
"sdk-grants-example"
],
"token": {
"type": "static",
"env": "STUB_ANTHROPIC_KEY"
}
}
}
The values in sdk_env are what the guest sees. {origin} stands for the local address. The token names the daemon's environment variable that holds the real key; the key itself is never in the file.
What the guest's environment held
The guest (INTERNAL · glossary →), the code running in the sandbox, printed its whole environment, and the environment of every process it could see. These are the variables the profile's sdk_env set, as each run's guest saw them:
| Run | Variable | Value in the sandbox |
|---|---|---|
| The OpenAI library | OPENAI_API_KEY | dummy-openai-key-in-the-sandbox |
| The OpenAI library | OPENAI_BASE_URL | http://127.0.0.1:8471/v1 |
| The Anthropic library | ANTHROPIC_API_KEY | dummy-anthropic-key-in-the-sandbox |
| The Anthropic library | ANTHROPIC_BASE_URL | http://127.0.0.1:8471 |
The key is a placeholder: both libraries refuse to start without one, and the broker replaces whatever the library sends. The keys the daemon held appear nowhere in what any run was sent or answered: not in the guest's environment, not in its output. The program searched the stored requests and answers in the signed bundle for each key, byte for byte. Each guest could read the environment of its own process and of no other: in the OpenAI run, pid 1, node --eval (PermissionError errno 13) and pid 13, node /runner.mjs (PermissionError errno 13) refused it (errno 13 is permission denied). Those two are plimsoll's own: the forwarder and the project runner. The Anthropic run's guest saw the same 2 refusals.
The OpenAI run's whole environment (15 variables)
HOME | / |
HOSTNAME | 41b808e93688 |
HOST_API_LISTEN | 127.0.0.1:8471 |
HOST_API_SOCKET | /run/host-api.sock |
NODE_OPTIONS | --import=file:///tmp/plimsoll-host.mjs |
NODE_VERSION | 22.23.4 |
OMP_NUM_THREADS | 1 |
OPENAI_API_KEY | dummy-openai-key-in-the-sandbox |
OPENAI_BASE_URL | http://127.0.0.1:8471/v1 |
OPENBLAS_NUM_THREADS | 1 |
PATH | /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin |
PLIMSOLL_RUNNER_PID | 13 |
PWD | /work |
SHLVL | 1 |
YARN_VERSION | 1.22.22 |
What the library sent, and what the API received
For one call of each library: every header as the library sent it to the local address (read back through the library's own with_raw_response, which adds its x-stainless-raw-response header), beside the same header as the stub received it from the broker.
OpenAI: client.chat.completions.create(...)
Sent to http://127.0.0.1:8471/v1/chat/completions; the stub received POST /v1/chat/completions and answered 200: “Hello from the stub API.”
| Header | The library sent | The API received | What happened |
|---|---|---|---|
Host | 127.0.0.1:8471 | 127.0.0.1:45519 | set by the broker, in place of the library's |
Accept-Encoding | gzip, deflate | gzip | set by the broker, in place of the library's |
Connection | keep-alive | none | dropped by the broker |
authorization | Bearer dummy-openai-key-in-the-sandbox | Bearer stub-openai-key-held-by-the-daemon-bc8e8a1ea6ba1cda1c7a4124 | set by the broker, in place of the library's |
accept | application/json | application/json | set by the broker (the same value) |
content-type | application/json | application/json | set by the broker (the same value) |
user-agent | OpenAI/Python 3.41.0 | Go-http-client/1.1 | set by the broker, in place of the library's |
x-stainless-lang | python | none | dropped by the broker |
x-stainless-package-version | 3.41.0 | none | dropped by the broker |
x-stainless-os | Linux | none | dropped by the broker |
x-stainless-arch | x64 | none | dropped by the broker |
x-stainless-runtime | CPython | none | dropped by the broker |
x-stainless-runtime-version | 3.14.8 | none | dropped by the broker |
x-stainless-async | false | none | dropped by the broker |
openai-organization | org-chosen-by-the-guest | none | dropped by the broker |
x-stainless-raw-response | true | none | dropped by the broker |
x-stainless-retry-count | 0 | none | dropped by the broker |
x-stainless-read-timeout | 600 | none | dropped by the broker |
Content-Length | 74 | 74 | set by the broker (the same value) |
Anthropic: client.messages.create(...)
Sent to http://127.0.0.1:8471/v1/messages; the stub received POST /v1/messages and answered 200: “Hello from the stub API.”
| Header | The library sent | The API received | What happened |
|---|---|---|---|
Host | 127.0.0.1:8471 | 127.0.0.1:43323 | set by the broker, in place of the library's |
Accept-Encoding | gzip, deflate | gzip | set by the broker, in place of the library's |
Connection | keep-alive | none | dropped by the broker |
x-stainless-timeout | 600 | none | dropped by the broker |
x-stainless-retry-count | 0 | none | dropped by the broker |
x-stainless-read-timeout | 600 | none | dropped by the broker |
Accept | application/json | application/json | set by the broker (the same value) |
Content-Type | application/json | application/json | set by the broker (the same value) |
User-Agent | Anthropic/Python 2.3.0 | Go-http-client/1.1 | set by the broker, in place of the library's |
X-Stainless-Lang | python | none | dropped by the broker |
X-Stainless-Package-Version | 2.3.0 | none | dropped by the broker |
X-Stainless-OS | Linux | none | dropped by the broker |
X-Stainless-Arch | x64 | none | dropped by the broker |
X-Stainless-Runtime | CPython | none | dropped by the broker |
X-Stainless-Runtime-Version | 3.14.8 | none | dropped by the broker |
Authorization | Bearer dummy-anthropic-key-in-the-sandbox | Bearer stub-anthropic-key-held-by-the-daemon-29dc33e2fa88addcb933ca5f | set by the broker, in place of the library's |
X-Stainless-Async | false | none | dropped by the broker |
anthropic-version | 2026-10-21 | 2026-10-21 | passed: the profile lists it |
anthropic-beta | plimsoll-example-not-passed | none | dropped by the broker |
X-Stainless-Raw-Response | raw | none | dropped by the broker |
Content-Length | 90 | 90 | set by the broker (the same value) |
In the two tables: the library's key (Authorization: Bearer dummy-…) never left the broker: the API got the daemon's key in its place. The library's User-Agent gave way to the broker's own, and the x-stainless-* headers both libraries send (their telemetry: language, version, operating system, retry count) were dropped. So were the two headers the guest added on purpose: OpenAI-Organization, which would let the guest choose which organization a call is billed to, and anthropic-beta, the header that switches on a beta feature, server-side tools among them. The transport headers (Accept, Content-Type, Host and the like) are the broker's, set on every request it makes. Of the headers the library chose, only anthropic-version, which the Claude API requires and the profile lists in pass_headers, was copied through. The broker sends every key as Authorization: Bearer, which is how the Claude API documents sending an API key (authentication (EXTERNAL · official docs ↗), read 2026-10-29; x-api-key is its older header).
The stream, piece by piece
A streamed answer comes back as server-sent events (EXTERNAL · official docs ↗): one HTTP response whose body is a series of small events, written as the model produces them. The stub waited 400 ms between pieces. Had anything on the way held the answer until it ended (the broker, the forwarder or the library), every piece would have reached the library after the stub wrote the last one. Instead, each piece reached the library before the stub had written the next. The times are from one clock: the stub and the sandbox run on the same Linux kernel, and a container reads the host's wall clock.
OpenAI: stream=True, 8 chunks
Each dark mark is the stub writing a piece; the teal mark below it is the library handing that piece to the guest's loop. The longest wait from a write to its arrival: 2 ms. First to last arrival: 2804 ms. The hollow mark is the stub's last write, data: [DONE], which ends an OpenAI stream: the library reads it and hands no piece over. Each piece's text is the one the stub wrote (8 of 8).
Every piece, in milliseconds from the stub's first write
| Piece | Text | Stub wrote | Library got | Stub wrote the next |
|---|---|---|---|---|
| 1 | Pieces | 0 | 2 | 400 |
| 2 | of | 400 | 401 | 801 |
| 3 | an | 801 | 802 | 1202 |
| 4 | answer, | 1202 | 1202 | 1602 |
| 5 | one | 1602 | 1603 | 2003 |
| 6 | at | 2003 | 2004 | 2405 |
| 7 | a | 2405 | 2406 | 2806 |
| 8 | time. | 2806 | 2806 | 3218 |
Anthropic: stream=True, 13 events
The Claude stream sends named events: a start, the text deltas, a stop. The longest wait from a write to its arrival: 4 ms. First to last arrival: 4807 ms. Each event's type and text are the ones the stub wrote (13 of 13).
Every event, in milliseconds from the stub's first write
| Event | Name | Stub wrote | Library got | Stub wrote the next |
|---|---|---|---|---|
| 1 | message_start | 0 | 4 | 401 |
| 2 | content_block_start | 401 | 402 | 802 |
| 3 | content_block_delta: Pieces | 802 | 803 | 1204 |
| 4 | content_block_delta: of | 1204 | 1204 | 1604 |
| 5 | content_block_delta: an | 1604 | 1605 | 2005 |
| 6 | content_block_delta: answer, | 2005 | 2005 | 2406 |
| 7 | content_block_delta: one | 2406 | 2407 | 2807 |
| 8 | content_block_delta: at | 2807 | 2807 | 3208 |
| 9 | content_block_delta: a | 3208 | 3209 | 3609 |
| 10 | content_block_delta: time. | 3609 | 3609 | 4009 |
| 11 | content_block_stop | 4009 | 4010 | 4410 |
| 12 | message_delta | 4410 | 4411 | 4811 |
| 13 | message_stop | 4811 | 4812 |
The upload
The guest uploaded 8 MiB with client.files.create(file=("sample.bin", data, "application/octet-stream"), purpose="user_data"). The library sends that as multipart/form-data (EXTERNAL · official docs ↗), one request body in named parts. A route takes a body over 1 MiB only when its profile raises the route's limit and names the type: here POST /v1/files is raised to 16 MiB and takes multipart/form-data, and the run may send 64 MiB in all.
| Part | File name | Type | Bytes | SHA-256 at the stub |
|---|---|---|---|---|
purpose | | 9 | 3279da18bfd0f199ae711d946059b48b901df03b584d4c51f14db38b15144137 | |
file | sample.bin | application/octet-stream | 8388608 | fa16e68dc6e4ff55d4eaf64f51a79ea3d3444b4b128c6856137e74b008605a06 |
The guest computed SHA-256 fa16e68dc6e4ff55d4eaf64f51a79ea3d3444b4b128c6856137e74b008605a06 over the bytes it sent; the stub computed fa16e68dc6e4ff55d4eaf64f51a79ea3d3444b4b128c6856137e74b008605a06 over the file part it received, the same. The whole request body the stub received was 8388891 bytes, and the library got back the stub's file object, file-stub-1.
The refusal
The guest called client.embeddings.create(model="stub-embedding", input="hello"), which is POST /v1/embeddings. The OpenAI profile allows POST /v1/chat/completions and POST /v1/files and nothing else. The library raised PermissionDeniedError, status 403, with the broker's own answer:
forbidden by sandbox capability allowlist
Three things show that the broker refused it and the API never saw it. The stub serves /v1/embeddings and would have answered 200, yet it received 0 requests there. The answer carries X-Should-Retry: false, which every answer the broker makes itself carries, and the library honors it: the attempt that raised was its first (x-stainless-retry-count: 0), so the refusal spent no further calls. And the daemon's audit line for the run counts 1 call denied by policy.
The call trace, as the daemon reports it
The broker keeps a metadata-only list of each run's calls: the route pattern each matched, its status, its sizes and its time, never a path, a body or a key. The daemon does not send that list back with the answer; it reports it as counts on the run's audit line (the line the daemon logs for every run) and per route in /metrics (the counters it serves to monitoring), both read after the runs:
| Run | Profile | Calls brokered | Denied | Per route, from /metrics | Requests the stub got |
|---|---|---|---|---|---|
| The OpenAI library | openai | 3 | 1 | POST /v1/chat/completions: 2 in 3.219 sPOST /v1/files: 1 in 0.023 s | 3 |
| The Anthropic library | anthropic | 2 | 0 | POST /v1/messages: 2 in 4.812 s | 2 |
| The control run: no grant | none | 0 | 0 | none | 0 |
The latency is the broker's round trip to the stub and back for the calls of that route, summed; for the streamed calls it includes the stub's 400 ms waits.
The run records, and the second seccomp profile
Each record states where its run ran: the isolation tier (INTERNAL · glossary →), how strong the wall around it was; the image, the packaged file system the sandbox started from, by its content identity; and the policy. The policy field names the seccomp (INTERNAL · glossary →) profile docker applied to the run, the kernel's list of the operations a program may ask it for, by the SHA-256 of its bytes. The shipped default allows Unix sockets only, so under it no forwarder could listen. A run whose grant sets sdk_env gets a second profile, which the daemon derives from the default at startup (SANDBOX_DOCKER_SECCOMP_SDK=derived) and pins by its digest, so the two records below name different profiles:
| Run | Tier | Image | Policy | Profile | Record |
|---|---|---|---|---|---|
| The OpenAI library | container | bf5af9606ed3 | c5b32b1a17c3 | the sdk_env variant | fda8ba7ada2c |
| The Anthropic library | container | bf5af9606ed3 | c5b32b1a17c3 | the sdk_env variant | f4f25aa46924 |
| The control run: no grant | container | bf5af9606ed3 | a39bc7bf6cf7 | the default | 372262671761 |
Both profiles are published beside this page: seccomp-default.json (8.2 KiB, the repository's docker/seccomp.json, SHA-256 a39bc7bf6cf7…) and seccomp-sdk.json (13.0 KiB, the bytes the daemon pinned, c5b32b1a17c3…). The daemon's own description of the pair, as it stated it: seccomp:sha256:a39bc7bf6cf712f4d65d108f615aeae5785d8def8bc0e658fbd4e36a6305b089 sdk_env:seccomp:sha256:c5b32b1a17c39da0438800a99bd0607700877ac8d01b72b839394023f0d64376. Compared one syscall rule at a time, the variant keeps 243 of the default's rules as they are, takes out 2 and adds 10:
| Syscall | When the arguments are | The rule's own comment | |
|---|---|---|---|
| removed | getsockopt | any | |
| removed | setsockopt | any | |
| added | getsockopt | arg1 == 1, arg2 == 3 | getsockopt allowed for one option libuv reads (docs/seccomp.md). |
| added | getsockopt | arg1 == 1, arg2 == 4 | getsockopt allowed for one option libuv reads (docs/seccomp.md). |
| added | setsockopt | arg1 == 1, arg2 == 2 | setsockopt allowed for one option the forwarder or a client transport sets (docs/seccomp.md). |
| added | setsockopt | arg1 == 1, arg2 == 9 | setsockopt allowed for one option the forwarder or a client transport sets (docs/seccomp.md). |
| added | setsockopt | arg1 == 6, arg2 == 1 | setsockopt allowed for one option the forwarder or a client transport sets (docs/seccomp.md). |
| added | setsockopt | arg1 == 6, arg2 == 4 | setsockopt allowed for one option the forwarder or a client transport sets (docs/seccomp.md). |
| added | setsockopt | arg1 == 6, arg2 == 5 | setsockopt allowed for one option the forwarder or a client transport sets (docs/seccomp.md). |
| added | setsockopt | arg1 == 6, arg2 == 6 | setsockopt allowed for one option the forwarder or a client transport sets (docs/seccomp.md). |
| added | socket | arg0 == 2, arg1 & 0xfff7f7ff == 1, arg2 == 0 | AF_INET (2) stream sockets, protocol 0: the type is masked so only SOCK_NONBLOCK and SOCK_CLOEXEC may join SOCK_STREAM; UDP, raw, SCTP (132) and MPTCP (262) fail the type or protocol check. |
| added | socket | arg0 == 2, arg1 & 0xfff7f7ff == 1, arg2 == 6 | AF_INET (2) stream sockets, IPPROTO_TCP (6). |
In words: socket may now make an IPv4 (AF_INET, 2) TCP stream socket, and nothing else new; setsockopt and getsockopt, which the default allows with any arguments, are allowed only for the few options the forwarder and the libraries set. What each run's guest tried, and got:
| Run | IPv4 TCP socket | UDP socket | IPv6 socket | Connect to 192.0.2.1:443 |
|---|---|---|---|---|
| The OpenAI library | opens | refused: PermissionError: errno 1, Operation not permitted | refused: PermissionError: errno 1, Operation not permitted | refused: OSError: errno 101, Network unreachable |
| The Anthropic library | opens | refused: PermissionError: errno 1, Operation not permitted | refused: PermissionError: errno 1, Operation not permitted | refused: OSError: errno 101, Network unreachable |
| The control run: no grant | refused: PermissionError: errno 1, Operation not permitted | refused: PermissionError: errno 1, Operation not permitted | refused: PermissionError: errno 1, Operation not permitted | refused: PermissionError: errno 1, Operation not permitted |
192.0.2.1 is set aside for documentation (RFC 5737 (EXTERNAL · official docs ↗)): the address only shows whether the sandbox has a route out, and under --network none it has none. The added sockets reach only the container's loopback, but they are kernel code the default withholds, which is why the variant is a profile of its own and applies only to runs whose grant sets sdk_env; docs/seccomp.md (EXTERNAL · official docs ↗) says what it adds in full.
What this proves, and what it doesn't
- Every run's record is signed by the harness and checks against the request and answer stored with it. (3 single runs verified, 0 sessions)
- No request the API received carried the guest's dummy key, in a header or in a body. (0 occurrences in 5 requests)
- Every request the API received carried the key the daemon holds for that API. (5 of 5 requests)
- The keys the daemon holds appear nowhere in what each run was sent or answered: not in its environment, not in any process environment the guest could read, not in its output. (0 occurrences; 3 process environments read)
- Of the guest's headers, only the one the profile passes reached the API; the broker set its own credential and transport headers. (OpenAI-Organization dropped, anthropic-beta dropped, anthropic-version passed; 0 headers leaked, altered or lost)
- Each piece of both streamed answers reached the guest's library before the stub wrote the next one. (openai: 8 of 8 pieces before the next was written, 8 of 8 matching the stub's, first to last 2804 ms; anthropic: 12 of 12 pieces before the next was written, 13 of 13 matching the stub's, first to last 4807 ms)
- The 8 MiB upload reached the API byte for byte, on a route raised above the 1 MiB default. (guest fa16e68dc6e4ff55, stub fa16e68dc6e4ff55)
- The call to a route the profile does not allow was refused by the broker, marked not to retry, made once, and never reached the API. (status 403, X-Should-Retry "false", retry count "0", 0 requests at the route, 1 denied on the audit line)
- The two runs whose grant sets sdk_env ran under the derived variant, the control run under the default, each named by digest in its record. (openai variant, anthropic variant, plain default)
- Under the variant an IPv4 TCP socket opens and a UDP or IPv6 socket does not, and an outside address is unreachable; under the default no IPv4 socket opens. (openai IPv4 TCP allowed, UDP errno 1, IPv6 errno 1, outside address errno 101; anthropic IPv4 TCP allowed, UDP errno 1, IPv6 errno 1, outside address errno 101; control IPv4 TCP errno 1, UDP errno 1, IPv6 errno 1, outside address errno 1)
- The guest imported the pinned versions of both libraries. (openai 3.41.0 (pinned 3.41.0), anthropic 2.3.0 (pinned 2.3.0))
- The stub received only calls on routes the profiles allow: three from the OpenAI run, two from the Anthropic run, none from the control run. (0 off the allow lists; 3, 2 and 0)
- The daemon's audit line for each run counts the calls the stub received, and the one refusal. (openai 3 calls 1 denied; anthropic 2 and 0; plain 0)
- How OpenAI's or Anthropic's servers answer. The stub stands in for them, shaped like the requests the two libraries send and the answers they parse; neither vendor's API was called.
- E2B and Docker Cloud. On those providers the sandbox is a virtual machine that reaches the broker through the guard (INTERNAL · glossary →), an HTTPS address outside it, and the guard's raw route takes a standard client's request; the documentation describes the live tests (
TestE2BGuardLive,TestDockerCloudGuardLive) that send one through it with Node'sfetch.sdk_envitself is not built on those providers, and none of that ran for this page. - That a chat API's own tools are safe to grant. The documentation's warning, quoted: “The broker checks the route and never reads the body, so everything the body can ask the API to do is granted along with the route.” A guest allowed
POST /v1/messagescan ask the Claude API to fetch a web page, and one allowed the Responses API can name a remote MCP (INTERNAL · glossary →) server, a program serving tools to a model; turning those tools off is done on the API's side (the warning and what to do about it (EXTERNAL · official docs ↗)). - Sessions, snippets, or another language's library. These were three project runs of Python code. A grant with
sdk_envin a session (INTERNAL · glossary →) is refused before anything runs. - That the stub is honest. Its log is its own statement. The harness (INTERNAL · glossary →), the program outside the daemon that signs records, signed the log's digest beside the runs' records, which shows the log was not changed afterwards, not that it is true. The guest's side (the headers the library sent, when each piece arrived) is in each run's own output, which its record covers.
- That the hardware is honest. The isolation tier in a record is the server's configuration, the provider's evidence and its startup checks, not a hardware attestation.
Run it yourself
From a clone of the repository (EXTERNAL · source repo ↗), with docker:
make docker-images # plimsoll's sandbox images, plimsoll/sandbox-python among them go run ./examples/sdk-grants # builds the example's image and plimsolld, runs everything, rewrites this page go run ./examples/sdk-grants -verify # rechecks the files beside this page; starts nothing go run ./cmd/plimsoll-attest verify -pub docs/examples/sdk-grants/harness.pub docs/examples/sdk-grants/bundle.jsonl
The example's image is plimsoll's Python image with the two libraries installed from wheels (Python's package files) pinned by version and SHA-256 (requirements.txt (EXTERNAL · source repo ↗), 17 packages): openai 3.41.0 (EXTERNAL · package index ↗, wheel 7060a74b2396…) and anthropic 2.3.0 (EXTERNAL · package index ↗, wheel aa6a3984f927…). Building it fetches those wheels; the runs have no network. Image plimsoll-wip/sdk-grants:dev, bf5af9606ed3, built on plimsoll/sandbox-python:latest, 48666c14b387. The program spends nothing: no model, no paid sandbox. Everything on this page comes from evidence.json, which -verify rebuilds from these files:
- bundle.jsonl, 45237 bytes
- harness.pub, 113 bytes
- observations.dsse.json, 6687 bytes
- stub.jsonl, 6144 bytes
- grants.json, 1156 bytes
- seccomp-default.json, 8388 bytes
- seccomp-sdk.json, 13327 bytes
observations.dsse.json is the harness's signed statement (a DSSE (INTERNAL · glossary →) envelope, the signing format the run records use) of what it saw outside the runs (the daemon's description of itself, its audit lines and /metrics, the image, the keys it handed the daemon, and the SHA-256 of the stub's log, the grants file and both profiles), naming the three run records. The keys are random values made for this run; they open nothing but the stub, which no longer runs. Harness key ID 9264f8cf24ee225014c4359279b8b3513ffca1e771b2f4719b148404ce21d7c7.