Dependency behavior receipt, live through plimsoll
You installed a package. Here is what its code did, signed and tied to the run that installed it.
Installing a package with npm, the package manager for Node.js (a JavaScript runtime), runs the package's install script, a command its package.json names (such as postinstall) that npm runs on the machine doing the install (npm's scripts guide (EXTERNAL · official docs ↗)). Importing the package runs its code again. Nothing in the install's output says what that code did. On this page plimsoll ran three installs, each in its own fresh sandbox (INTERNAL · glossary →) under gVisor (INTERNAL · glossary →), and for each one signed a receipt: what the programs in that sandbox did, read from outside it, and which run it was.
What a receipt is, and what made it
gVisor is a kernel written as an ordinary program: the code in the pod talks to it instead of to the machine's own kernel. Every system call (INTERNAL · glossary →) the code makes, such as opening a file, starting a program or opening a connection, is answered by gVisor. Started with its --strace option, gVisor's runsc (INTERNAL · glossary →) also writes every such call to a log file on the node (gVisor's debugging guide (EXTERNAL · official docs ↗)). The node is the machine that runs the pod, and the log file is outside the pod, where the code inside cannot write. Reading package behavior from that log is also the approach of package-analysis, the Open Source Security Foundation's tool for watching what published packages do (EXTERNAL · source repo ↗).
The receipt is what examples/dependency-receipt reads from one such log: the processes and the programs started in them, every file written, and every open for writing, file change and network call, each with its outcome. It is signed as an in-toto (INTERNAL · glossary →) statement (the Statement specification (EXTERNAL · source repo ↗)) in a DSSE (INTERNAL · glossary →) envelope, the same envelope plimsoll signs its run records (INTERNAL · glossary →) in. A run record is plimsoll's statement of one run: what was sent, what came back, and which sandbox ran it. The receipt names the log by its digest (INTERNAL · glossary →) and the run record by its digest, so it is about exactly one log and one run.
These runs used plimsoll's agent-sandbox (INTERNAL · glossary →) provider: Kubernetes, a system that runs containers across machines, keeps a warm pool of ready pods (a pod is Kubernetes' unit of one or more containers run together; Kubernetes' pods page (EXTERNAL · official docs ↗)), and each run claims one, uses it once, and deletes it. The pods ran under the RuntimeClass (INTERNAL · glossary →) gvisor-receipt, whose handler runsc-receipt is gVisor with the system-call log turned on; plimsolld, the plimsoll server, was told that handler is gVisor and held every pod to gVisor's boot line before sending it any code. All of it ran on a local kind (EXTERNAL · official docs ↗) cluster on 2026-10-29; nothing was paid for.
The three installs
Each run sent one package's files as a project and ran two steps in its pod: an offline npm install of the package from its directory (--install-links copies it into node_modules as an install from a tarball would, and runs its install script), then a node command that imports it. The packages are the fixtures beside the receipt code; none comes from the public registry, and plimsoll sends no run to a pod unless a network policy denies the pod's traffic out.
| Package | Its install script | Steps (exit code, time) | Pod UID | Tier | Run record (bundle line) | runsc log | Receipt |
|---|---|---|---|---|---|---|---|
| quiet | none | 1: exit 0, 5.5 s 2: exit 0, 0.1 s |
fcbbc735… | kernel | b69b8960903c (1) | 3.5 MB 11,264 calls |
379 KB |
| network | postinstall: node install.js |
1: exit 0, 6.8 s 2: exit 0, 0.1 s |
6ff9c6f1… | kernel | bf86e832588e (2) | 3.6 MB 11,518 calls |
381 KB |
| canary | postinstall: node install.js |
1: exit 1, 6.4 s 2: not run (a step that fails stops the run) |
7f4001ae… | kernel | 3400f04835e2 (3) | 3.5 MB 11,308 calls |
380 KB |
Tier is the isolation tier (INTERNAL · glossary →) the run record states: kernel means gVisor stood between the code and the node's kernel. Every request asked for at least kernel, and the daemon was set to refuse any run below it. Each run record's start and end are 6.9 s (quiet), 7.2 s (network) and 6.8 s (canary) apart. The log sizes are as runsc wrote them; the published copies are gzip-compressed.
What each package's install script does, from its source in examples/dependency-receipt/fixtures/packages: quiet has none. network starts a TCP connection (the kind a web request uses) to 203.0.113.7 port 443, looks up the name canary.example (a DNS lookup, which turns a name into an address), and sends a UDP datagram (one packet, sent with no connection) to 198.51.100.9 port 53. Both addresses are reserved for documentation, so no real host has them (RFC 5737 (EXTERNAL · standard ↗)). canary first writes /root/.canary; if that works it goes on to write through a symlink into /etc, rename and remove a file, and write to /proc. Here its first write was refused, so its script stopped and npm failed:
What npm printed for the canary install (exit 1)
$ npm install --offline --no-audit --no-fund --foreground-scripts --install-links ./packages/canary
> plimsoll-fixture-canary@1.0.0 postinstall
> node install.js
node:fs:2430
return binding.writeFileUtf8(
^
Error: EACCES: permission denied, open '/root/.canary'
at Object.writeFileSync (node:fs:2430:20)
at Object.<anonymous> (/work/project/node_modules/plimsoll-fixture-canary/install.js:4:4)
at Module._compile (node:internal/modules/cjs/loader:1781:14)
at Object..js (node:internal/modules/cjs/loader:1913:10)
at Module.load (node:internal/modules/cjs/loader:1505:32)
at Function._load (node:internal/modules/cjs/loader:1309:12)
at wrapModuleLoad (node:internal/modules/cjs/loader:254:19)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
at node:internal/main/run_main_module:36:49 {
errno: -13,
code: 'EACCES',
syscall: 'open',
path: '/root/.canary'
}
Node.js v22.23.4
npm error code 1
npm error path /work/project/node_modules/plimsoll-fixture-canary
npm error command failed
npm error command sh -c node install.js
npm error A complete log of this run can be found in: /work/.npm/_logs/2026-10-29T18_14_18_523Z-debug-0.log
Who did what in each pod
A receipt covers the pod's whole life, not only the package: sandboxd (agent-sandbox's daemon in the pod, which starts each command plimsoll sends), plimsoll's own check of the pod, plimsoll's runner, npm, and the package. The receipt lists every process by its PID (process ID), the programs started in it, and which process made each call. This page groups the processes by the command that started them: a step's shell is matched to the step's exact text in the signed request, and an install script's shell to the script's exact text in the package.json the run sent. The grouping is the page's reading; the receipt itself lists every process and call ungrouped.
effected the call returned success, attempted it returned an error, undetermined the log does not show its outcome (the next section says more), written files written. For the package's own groups (amber) every call is listed; for the others, every call that did not succeed and every network call other than opening a new socket.
The quiet package: 13 processes, 19 programs started, 563 files written
Receipt receipts/quiet.intoto.json.gz: 1164 effected24 attempted0 undetermined and no entry the reader could not read as one event.
- PID 1, exit 0: no program load in the log
- PID 8 (from 1), exit 0: no program load in the log
7 calls listed
| effected | bind | ::1 (AF_INET6) | ok | PID 1 |
| effected | bind | ::ffff:7f00:1 (AF_INET6) | ok | PID 1 |
| effected | bind | port 9090 (AF_INET6) | ok | PID 1 |
| effected | listen | socket:[4] | ok | PID 1 |
| effected | bind | port 8080 (AF_INET6) | ok | PID 1 |
| effected | listen | socket:[5] | ok | PID 1 |
| attempted | socket | AF_INET SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK | errno=22 (invalid argument) | PID 1 |
- PID 9 (from 1), exit 0: cat /proc/mounts
- PID 11 (from 9), exit 0: no program load in the log
- PID 12 (from 11), exit 0: dmesg
- PID 13 (from 11), exit 0: head -n 1
- PID 14 (from 9), exit 0: cat /proc/sys/kernel/random/boot_id
- PID 15 (from 9), exit 0: grep ^Seccomp: /proc/self/status
3 calls listed
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 9 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 9 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 9 |
- PID 16 (from 1), exit 0: node /runner.mjs
- PID 17 (from 16), exit 0: mkdir -p /work/project
- PID 24 (from 16), exit 0: /usr/local/bin/node -e const fs=require("node:fs"); const p="/proc/"+process.ppid; const checks=[[p+"/fd/0",fs.constants.O_RDONLY],[p+"/fd/1",fs.const …
9 calls listed
| attempted | open for writing | /proc/16/fd/1 | errno=13 (permission denied) | PID 24 |
| attempted | mkdir | / | errno=17 (file exists) | PID 17 |
| attempted | mkdir | /work/ | errno=17 (file exists) | PID 17 |
| attempted | mkdir | /work/project | errno=17 (file exists) | PID 16 |
| attempted | mkdir | /work/project/packages/quiet | errno=2 (no such file or directory) | PID 16 |
| attempted | mkdir | /work/project/packages/quiet | errno=17 (file exists) | PID 16 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 16 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 16 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 16 |
- PID 31 (from 16), exit 0: node /usr/local/bin/npm install --offline --no-audit --no-fund --foreground-scripts --install-links ./packages/quiet
18 calls listed
| effected | connect | 127.0.0.1 port 65535 (AF_INET) | ok | PID 31 |
| effected | connect | ::1 port 65535 (AF_INET6) | ok | PID 31 |
| effected | bind | no address stated (AF_INET) | ok | PID 31 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 31 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 31 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 31 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 31 |
| attempted | mkdir | /tmp/node-compile-cache/v22.23.4-x64-9ac5647c-61000 | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/project | errno=17 (file exists) | PID 31 |
| attempted | mkdir | /work/project/node_modules/plimsoll-fixture-quiet | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/project/node_modules/plimsoll-fixture-quiet | errno=17 (file exists) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/tmp | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/content-v2/sha512/ad/89 | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/content-v2/sha512/ad | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/content-v2/sha512 | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/content-v2/sha512/ad/89 | errno=17 (file exists) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/index-v5/fe/d1 | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/index-v5/fe | errno=2 (no such file or directory) | PID 31 |
- PID 43 (from 16), exit 0: node -e require("plimsoll-fixture-quiet")
The network package: 14 processes, 21 programs started, 565 files written
Receipt receipts/network.intoto.json.gz: 1176 effected25 attempted1 undetermined and no entry the reader could not read as one event.
- PID 1, exit 0: no program load in the log
- PID 8 (from 1), exit 0: no program load in the log
7 calls listed
| effected | bind | ::1 (AF_INET6) | ok | PID 1 |
| effected | bind | ::ffff:7f00:1 (AF_INET6) | ok | PID 1 |
| effected | bind | port 9090 (AF_INET6) | ok | PID 1 |
| effected | listen | socket:[4] | ok | PID 1 |
| effected | bind | port 8080 (AF_INET6) | ok | PID 1 |
| effected | listen | socket:[5] | ok | PID 1 |
| attempted | socket | AF_INET SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK | errno=22 (invalid argument) | PID 1 |
- PID 9 (from 1), exit 0: cat /proc/mounts
- PID 11 (from 9), exit 0: no program load in the log
- PID 12 (from 11), exit 0: dmesg
- PID 13 (from 11), exit 0: head -n 1
- PID 14 (from 9), exit 0: cat /proc/sys/kernel/random/boot_id
- PID 15 (from 9), exit 0: grep ^Seccomp: /proc/self/status
3 calls listed
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 9 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 9 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 9 |
- PID 16 (from 1), exit 0: node /runner.mjs
- PID 17 (from 16), exit 0: mkdir -p /work/project
- PID 24 (from 16), exit 0: /usr/local/bin/node -e const fs=require("node:fs"); const p="/proc/"+process.ppid; const checks=[[p+"/fd/0",fs.constants.O_RDONLY],[p+"/fd/1",fs.const …
10 calls listed
| attempted | open for writing | /proc/16/fd/1 | errno=13 (permission denied) | PID 24 |
| attempted | mkdir | / | errno=17 (file exists) | PID 17 |
| attempted | mkdir | /work/ | errno=17 (file exists) | PID 17 |
| attempted | mkdir | /work/project | errno=17 (file exists) | PID 16 |
| attempted | mkdir | /work/project/packages/network | errno=2 (no such file or directory) | PID 16 |
| attempted | mkdir | /work/project/packages/network | errno=17 (file exists) | PID 16 |
| attempted | mkdir | /work/project/packages/network | errno=17 (file exists) | PID 16 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 16 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 16 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 16 |
- PID 31 (from 16), exit 0: node /usr/local/bin/npm install --offline --no-audit --no-fund --foreground-scripts --install-links ./packages/network
18 calls listed
| effected | connect | 127.0.0.1 port 65535 (AF_INET) | ok | PID 31 |
| effected | connect | ::1 port 65535 (AF_INET6) | ok | PID 31 |
| effected | bind | no address stated (AF_INET) | ok | PID 31 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 31 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 31 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 31 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 31 |
| attempted | mkdir | /tmp/node-compile-cache/v22.23.4-x64-9ac5647c-61000 | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/project | errno=17 (file exists) | PID 31 |
| attempted | mkdir | /work/project/node_modules/plimsoll-fixture-network | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/project/node_modules/plimsoll-fixture-network | errno=17 (file exists) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/tmp | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/content-v2/sha512/5d/37 | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/content-v2/sha512/5d | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/content-v2/sha512 | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/content-v2/sha512/5d/37 | errno=17 (file exists) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/index-v5/10/b1 | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/index-v5/10 | errno=2 (no such file or directory) | PID 31 |
- PID 43 (from 31), exit 0: node install.js
| effected | socket | AF_INET SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK | ok | PID 43 |
| effected | bind | no address stated (AF_INET) | ok | PID 43 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 43 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 43 |
| effected | socket | AF_INET SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK | ok | PID 43 |
| effected | socket | AF_INET SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK | ok | PID 43 |
| effected | bind | no address stated (AF_INET) | ok | PID 43 |
| effected | send | sendmsg: the destination is not in the log; the message header changed during the call | ok | PID 43 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 43 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 43 |
| undetermined | connect | 203.0.113.7 port 443 (AF_INET) | errno=115 (operation now in progress) | PID 43 |
- PID 54 (from 16), exit 0: node -e require("plimsoll-fixture-network")
The canary package: 13 processes, 19 programs started, 562 files written
Receipt receipts/canary.intoto.json.gz: 1168 effected28 attempted0 undetermined and no entry the reader could not read as one event.
- PID 1, exit 0: no program load in the log
- PID 8 (from 1), exit 0: no program load in the log
7 calls listed
| effected | bind | ::1 (AF_INET6) | ok | PID 1 |
| effected | bind | ::ffff:7f00:1 (AF_INET6) | ok | PID 1 |
| effected | bind | port 9090 (AF_INET6) | ok | PID 1 |
| effected | listen | socket:[4] | ok | PID 1 |
| effected | bind | port 8080 (AF_INET6) | ok | PID 1 |
| effected | listen | socket:[5] | ok | PID 1 |
| attempted | socket | AF_INET SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK | errno=22 (invalid argument) | PID 1 |
- PID 9 (from 1), exit 0: cat /proc/mounts
- PID 11 (from 9), exit 0: no program load in the log
- PID 12 (from 11), exit 0: dmesg
- PID 13 (from 11), exit 0: head -n 1
- PID 14 (from 9), exit 0: cat /proc/sys/kernel/random/boot_id
- PID 15 (from 9), exit 0: grep ^Seccomp: /proc/self/status
3 calls listed
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 9 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 9 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 9 |
- PID 16 (from 1), exit 0: node /runner.mjs
- PID 17 (from 16), exit 0: mkdir -p /work/project
- PID 24 (from 16), exit 0: /usr/local/bin/node -e const fs=require("node:fs"); const p="/proc/"+process.ppid; const checks=[[p+"/fd/0",fs.constants.O_RDONLY],[p+"/fd/1",fs.const …
10 calls listed
| attempted | open for writing | /proc/16/fd/1 | errno=13 (permission denied) | PID 24 |
| attempted | mkdir | / | errno=17 (file exists) | PID 17 |
| attempted | mkdir | /work/ | errno=17 (file exists) | PID 17 |
| attempted | mkdir | /work/project | errno=17 (file exists) | PID 16 |
| attempted | mkdir | /work/project/packages/canary | errno=2 (no such file or directory) | PID 16 |
| attempted | mkdir | /work/project/packages/canary | errno=17 (file exists) | PID 16 |
| attempted | mkdir | /work/project/packages/canary | errno=17 (file exists) | PID 16 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 16 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 16 |
| attempted | connect | /var/run/nscd/socket (AF_UNIX) | errno=2 (no such file or directory) | PID 16 |
- PID 31 (from 16), exit 1: node /usr/local/bin/npm install --offline --no-audit --no-fund --foreground-scripts --install-links ./packages/canary
20 calls listed
| effected | connect | 127.0.0.1 port 65535 (AF_INET) | ok | PID 31 |
| effected | connect | ::1 port 65535 (AF_INET6) | ok | PID 31 |
| effected | bind | no address stated (AF_INET) | ok | PID 31 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 31 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 31 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 31 |
| effected | send | 127.0.0.1 port 53 (AF_INET) | ok | PID 31 |
| attempted | mkdir | /tmp/node-compile-cache/v22.23.4-x64-9ac5647c-61000 | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/project | errno=17 (file exists) | PID 31 |
| attempted | mkdir | /work/project/node_modules/plimsoll-fixture-canary | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/project/node_modules/plimsoll-fixture-canary | errno=17 (file exists) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/tmp | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/content-v2/sha512/c2/97 | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/content-v2/sha512/c2 | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/content-v2/sha512 | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/content-v2/sha512/c2/97 | errno=17 (file exists) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/index-v5/5d/5c | errno=2 (no such file or directory) | PID 31 |
| attempted | mkdir | /work/.npm/_cacache/index-v5/5d | errno=2 (no such file or directory) | PID 31 |
| attempted | rmdir | /work/project/node_modules | errno=39 (directory not empty) | PID 31 |
| attempted | rmdir | /work/project/node_modules/plimsoll-fixture-canary | errno=39 (directory not empty) | PID 31 |
- PID 43 (from 31), exit 1: node install.js
| attempted | open to create or empty | /root/.canary | errno=13 (permission denied) | PID 43 |
Most of what the receipts list is npm, not the package. npm turns on node's compile cache (node's module documentation (EXTERNAL · official docs ↗)), and in the quiet run 552 of the 560 files npm wrote are that cache. A package's own groups are short by comparison: that is what grouping by process is for.
The receipt's three sides, in plain words
Every open for writing, file change and network call in a receipt sits on exactly one of three sides, decided by what the call returned in the log.
Effected
The call returned success: it did what it asked. The network package's script sent 4 datagrams to port 53 of 127.0.0.1, the address the pod's name resolver is set to. Its source looks up canary.example, and a lookup shows in the log as a datagram to port 53, never as the name.
Attempted
The call returned an error. The canary script's open of /root/.canary answered errno=13 (permission denied), the error number and name the call returned: the code runs as user 61000 on a read-only root. Of the 77 attempted calls in the three receipts, 50 are a mkdir answering that the directory was already there (file exists) or that its parent was not there yet, and 18 are a connect to /var/run/nscd/socket, a name-service socket that does not exist in the pod. Attempted means the call failed, not that a policy stopped it, and never that the package did not try.
Undetermined
The log does not show the outcome. The network script's connect to 203.0.113.7 port 443 answered errno=115 (operation now in progress): node connects without waiting, and whether the connection ever completed is decided by calls the log does not follow. plimsoll refuses a pod unless a network policy denies its traffic out and this daemon's startup check saw all 7 outbound connections it tried from the pool's pods time out or find no route. The receipt does not lean on any of that: it says only that the connection was started.
Which run it was
plimsoll reads each claimed pod back from Kubernetes before it sends any code, and the run record names the pod by its UID, the unique ID the API server gave it when it was made. Kubernetes never gives that UID to another object in the cluster's history. runsc logs each container's spec when the pod starts, before any program in it runs, and the spec names the same UID. The receipt command verifies the run record's signature in the bundle (the harness (INTERNAL · glossary →)'s signed file of run records, one per line), requires the two UIDs to be equal, and names the record by its digest as the receipt's second subject.
instance k8s-pod-uid:fcbbc735-79ae-4871-b08f-3a8dab68675f
record b69b8960903c708887f2eaeecd140224e9a9306efd7d3cafd5ee35bcd1291205
container spec, pod UID fcbbc735-79ae-4871-b08f-3a8dab68675f
log 8f458f815dbe87f88031f6e8956cfc54e336ff452a4ab95b623f2629c356833e
instance k8s-pod-uid:6ff9c6f1-4b63-40a0-af88-869b1a087a1b
record bf86e832588ee1c77659c9bae0763b7e614aa40d51bceb525b80fcdcccf6013d
container spec, pod UID 6ff9c6f1-4b63-40a0-af88-869b1a087a1b
log 98f9a3e05af99afbd6e853613b30757033376c58946b36ee9b846ca4899a65e0
instance k8s-pod-uid:7f4001ae-cf30-4523-868f-50168cf8fd58
record 3400f04835e2a3395e3a8ba9f624586b5df21619cc959d8cfc1c08e2e15f1ef7
container spec, pod UID 7f4001ae-cf30-4523-868f-50168cf8fd58
log 13e70966358938126ca8e0fe77bcbe57e4c534d4916eff2487423fc8c51a33c2
The wrong run's record is refused. Signing the network run's log joined to the quiet run's record (bundle line 1). This and the commands below ran in the live run's scratch directory, where the receipts were not yet compressed and the key that signed them sat in secrets/:
$ dependency-receipt -log logs/network.boot.json.gz -key secrets/harness.key -out tampered/never-written.intoto.json -bundle bundle.jsonl -line 1 dependency-receipt: receipt: refused: the run record names instance "k8s-pod-uid:fcbbc735-79ae-4871-b08f-3a8dab68675f", the log names "k8s-pod-uid:6ff9c6f1-4b63-40a0-af88-869b1a087a1b" exit 1
Refused before anything was signed.
Changing a receipt or its log fails the check
The check opens a receipt under the signer's public key, rebuilds the whole claim from the log and the run record it names, and requires the two to be identical, byte for byte. Each receipt above checked. Two changed inputs, on the network run:
A connection deleted from the receipt. The receipt opened, the call connect 203.0.113.7 port 443 removed from its undetermined side, and the rest signed again with the same key: a signer who leaves a call out. The signature is good; the log still shows the call.
$ dependency-receipt -check tampered/network-connection-deleted.intoto.json -pub harness.pub -log logs/network.boot.json.gz -bundle bundle.jsonl -line 2 dependency-receipt: receipt: the signed receipt is not what its evidence rebuilds: the log shows something other than the receipt states exit 1
Refused: the log shows something other than the receipt states.
One byte of the log changed. Byte 1,676,748 of the 3,607,184-byte log, the last digit of the address the first time the log names it, changed from 7 to 8, so the log would say the package connected somewhere else:
…cket:[35], 0x7ed67f746660 {Family: AF_INET, Addr: 203.0.113.7→8, Port: 443}, 0x10)","level":"…
$ dependency-receipt -check receipts/network.intoto.json -pub harness.pub -log tampered/network-one-byte.boot.json -bundle bundle.jsonl -line 2 dependency-receipt: receipt: the signed receipt is not what its evidence rebuilds: the receipt is about log 98f9a3e05af99afbd6e853613b30757033376c58946b36ee9b846ca4899a65e0, the log given is a8ce7729abe09505e8b2e1ea24bf3988acd1a8df008a0ef65cc15d30d1d0b109 exit 1
Refused: the receipt is about another log.
What the log cannot show
A receipt is exactly as good as the log under it. These are its limits, stated in the receipt code's README; nothing on this page gets around them.
- The log is the signer's word. Whoever runs the node holds the log. The check proves the receipt says exactly what this log shows and names this run; it cannot prove runsc wrote this log for that pod.
- Behavior the package keeps for later. One install and one import is one observation. Code that waits, or checks whether it is watched, shows nothing.
- Names looked up in DNS show as datagrams to port 53, not the names.
- The destination of a datagram sent with
sendmsg, which node's UDP sockets use: the network script's datagram to 198.51.100.9 is in its receipt as one send with no destination. - Writes through shared memory mappings,
sendfile,splice,copy_file_rangeandfallocateare outside the calls the log keeps, so an open for writing can appear with no write after it. - Paths of calls that take no descriptor (
mkdir,rename,unlink,open) are read from the program's memory before the call reads them, so a second thread can change them in between, and a symlink in a folder of the path can make the call land elsewhere. Files written are named from descriptors and are not affected. - Starting a new program can close descriptors silently, so after one the reader leaves a descriptor's name unresolved rather than give it a new file's name.
- io_uring, Linux's interface for queued file and network work, which runsc leaves off unless started with
--iouring; the receipt is refused for a log made with it on. - Anything outside gVisor. plimsoll's providers that run each sandbox in a small virtual machine of its own (Kata Containers, E2B, Docker Cloud) have no such log, so no receipt.
- The whole pod is one observation. sandboxd, plimsoll's check, the runner and npm are in every receipt with the package; this page's grouping is a reading of the process tree, not part of the signed claim.
- This cluster is a local test setup. The pod network between plimsolld and sandboxd is kind's default, which is not encrypted, so these runs show the receipt, not a protected channel (plimsoll's agent-sandbox guide (EXTERNAL · source repo ↗)). The daemon was not in hardened mode (INTERNAL · glossary →), and the image is named by tag; each run's record states the image's digest as the node reported it.
Every receipt also signs this list of what it cannot see, so its limits travel with it:
- behavior the package keeps for later: one install and one import is one observation, and code that waits, or checks whether it is watched, shows nothing
- names looked up in DNS: a lookup shows as a datagram to port 53, not the name
- the destination of a datagram sent with sendmsg
- writes through shared memory mappings, sendfile, splice, copy_file_range and fallocate
- paths of calls that take no descriptor are copied from the program's memory before the call reads them, so a second thread can change them in between; files written are named from descriptors and are not affected
- the pod's whole life is one observation: a pod that served several calls shows all of them
- anything outside gVisor
How it ran, and how to check it
| Cluster | kind, Kubernetes v1.37.0, containerd://2.3.4, agent-sandbox's controller (the program that keeps the warm pool and hands out its pods) registry.k8s.io/agent-sandbox/agent-sandbox-controller:v1.0.7 |
|---|---|
| Observer | runsc release-20260907.0, network sandbox, 46 system calls logged, 16 bytes of written data kept per call; configuration digest 1469f8adb827c216bd3451bd590203169c3613c052b3f35988737f9ad8b32955 |
| Image | plimsoll/sandbox-agentsandbox:dev, as each record names it: agentsandbox-image:sha256:c7750b7916591611e93978029237cdb1cd3268d885e79b7b91624514ba2b72ff |
| plimsolld | built with go1.26.10 from commit 47b36cdd3d41ef6b3d8d29737309130579ef9e35: main at 1c9951d63ad6 and 1 commit past it (agentsandbox: a completed project run's result names its image too. runPlan returned the runner's report, a fr …), with 11 files not yet committed, none of them part of plimsolld (this example's own files and its documentation): docs/example-programs.md, examples/dependency-receipt/README.md, examples/dependency-receipt/live/cluster.yaml, examples/dependency-receipt/live/evidence.go, examples/dependency-receipt/live/live_test.go, examples/dependency-receipt/live/main.go, examples/dependency-receipt/live/page.go, examples/dependency-receipt/live/page.html.tmpl, examples/dependency-receipt/live/run.go, examples/dependency-receipt/live/tamper.go, scripts/public-manifest.txt |
| Startup check | As plimsolld logged it: tier kernel, the kernel log inside the pod began [ 0.000000] Starting gVisor..., a write was tried at 4,903 paths and succeeded only under /dev/shm, /tmp, /work; outbound: 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, tcp [2001:4860:4860::8888]:53 ENETUNREACH, tcp [2606:4700:4700::1111]:443 ENETUNREACH. This is the daemon's own log line, not a signed record. |
| Signing key | harness.pub, key ID 6a9724e384b8de2aadcebd12cd9d967c736214522fdb220d1de49bc73f56a8f4: it signed the bundle and the receipts |
| Bundle | bundle.jsonl: 4 lines, 3 runs and the closing checkpoint, sha256 cbc289183cd9b399b1ef6701649057e6c3bdc3a7740e6ca701da8befac740528 |
plimsolld's settings
SANDBOX_AGENTSANDBOX_API_SERVER | https://172.24.0.2:6443 |
SANDBOX_AGENTSANDBOX_IMAGE | plimsoll/sandbox-agentsandbox:dev |
SANDBOX_AGENTSANDBOX_NAMESPACE | plimsoll-dependency-receipt |
SANDBOX_AGENTSANDBOX_RUNSC_HANDLERS | runsc-receipt |
SANDBOX_AGENTSANDBOX_RUNTIME_CLASS | gvisor-receipt |
SANDBOX_AGENTSANDBOX_WARM_POOL | receipt |
SANDBOX_CPUS | 1 |
SANDBOX_MEMORY_MB | 512 |
SANDBOX_MIN_ISOLATION | kernel |
SANDBOX_PROVIDER | agentsandbox |
Besides these, the daemon read its secrets from files: the API server's certificate authority, a service-account token, sandboxd's token and the caller registry. plimsolld ran in a container on kind's network with a route to the pods, since it reaches each pod's sandboxd at the pod's address.
Files
- evidence.json: every number on this page
- quiet: receipts/quiet.intoto.json.gz (44 KB), logs/quiet.boot.json.gz (244 KB)
- network: receipts/network.intoto.json.gz (44 KB), logs/network.boot.json.gz (256 KB)
- canary: receipts/canary.intoto.json.gz (44 KB), logs/canary.boot.json.gz (250 KB)
- the receipt with a connection deleted: tampered/network-connection-deleted.intoto.json.gz
The receipts and logs are gzip-compressed. The receipt command reads a log either way and a receipt only as written. From the repository root:
go run ./examples/dependency-receipt/live -verify # this page's files, offline: starts nothing
gunzip -k docs/examples/dependency-receipt/receipts/network.intoto.json.gz
go run ./examples/dependency-receipt -check docs/examples/dependency-receipt/receipts/network.intoto.json \
-pub docs/examples/dependency-receipt/harness.pub -log docs/examples/dependency-receipt/logs/network.boot.json.gz \
-bundle docs/examples/dependency-receipt/bundle.jsonl -line 2
-verify checks the bundle's signatures and chain, checks every receipt against its log and record, requires both changed inputs and the wrong record to be refused, and requires this page to be exactly what evidence.json renders. Running everything again needs a kind cluster with agent-sandbox and the gvisor-receipt runtime class: go run ./examples/dependency-receipt/live makes a Kubernetes namespace (a named group of the cluster's objects) of its own and deletes it after.