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.

How a receipt is made for a run plimsoll dispatched The harness asks plimsolld for a run and signs its run record into a bundle. plimsolld claims a pod from a warm pool and sends the install into it. Inside the pod, gVisor answers every system call, and runsc writes each one to a log on the node, outside the pod. The receipt is built from that log, joined to the run record by the pod's UID, and signed. The harness asks for each install; signs each run record one run result + run record plimsolld claims a pod, sends the steps Kubernetes node one pod, under gVisor npm install ./packages/network postinstall: node install.js node -e require(…) every system call goes to gVisor, not the host runsc writes each call to a log on the node outside the pod: the code cannot reach it The receipt, signed joined by the pod's UID effected attempted undetermined
The package's code runs in the pod. The record of what it asked for is written by gVisor's runsc on the node, outside the pod.
3 of 3installs dispatched by plimsoll, each to its own pod, with a receipt joined to the run's signed record by that pod's UID
203.0.113.7 port 443where the network package's install script started a connection; its outcome is not in the log, so the receipt says undetermined
/root/.canarythe file the canary package's install script tried to write; refused with errno=13 (permission denied), so the receipt says attempted
3 refusedthe three changed or mismatched inputs: a connection deleted from a receipt, one byte of a log changed, a log joined to another run's record

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.

PackageIts install scriptSteps (exit code, time)Pod UIDTierRun record (bundle line)runsc logReceipt
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.

The pod's start and sandboxd
The pod's first process, which runsc starts from the container spec, so the log shows no program load for it, and what it forked without loading a program. The first process bound ports 9090 and 8080, sandboxd's two ports in the template: it is sandboxd, agent-sandbox's daemon in the pod.
11 effected1 attempted0 undetermined0 written
  • 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
effectedbind::1 (AF_INET6)okPID 1
effectedbind::ffff:7f00:1 (AF_INET6)okPID 1
effectedbindport 9090 (AF_INET6)okPID 1
effectedlistensocket:[4]okPID 1
effectedbindport 8080 (AF_INET6)okPID 1
effectedlistensocket:[5]okPID 1
attemptedsocketAF_INET SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCKerrno=22 (invalid argument)PID 1
plimsoll's check of the pod
The command plimsoll sends before any of the run's code: it reads the kernel's first log line (gVisor's boot line), the boot ID, the seccomp mode (the kernel's filter on which calls a process may make) and the mount table.
4 effected3 attempted0 undetermined0 written
  • 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
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 9
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 9
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 9
plimsoll's runner
plimsoll's in-sandbox runner, node /runner.mjs: it writes the run's files, checks that a step cannot open its descriptors, then starts each step in its own shell.
9 effected9 attempted0 undetermined3 written
  • 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
attemptedopen for writing/proc/16/fd/1errno=13 (permission denied)PID 24
attemptedmkdir/errno=17 (file exists)PID 17
attemptedmkdir/work/errno=17 (file exists)PID 17
attemptedmkdir/work/projecterrno=17 (file exists)PID 16
attemptedmkdir/work/project/packages/quieterrno=2 (no such file or directory)PID 16
attemptedmkdir/work/project/packages/quieterrno=17 (file exists)PID 16
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 16
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 16
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 16
Step 1: npm install
npm itself, installing the package from its directory, offline. Its install script, if it has one, is the next group.
1140 effected11 attempted0 undetermined560 written, 552 of them node's compile cache
  • 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
effectedconnect127.0.0.1 port 65535 (AF_INET)okPID 31
effectedconnect::1 port 65535 (AF_INET6)okPID 31
effectedbindno address stated (AF_INET)okPID 31
effectedsend127.0.0.1 port 53 (AF_INET)okPID 31
effectedsend127.0.0.1 port 53 (AF_INET)okPID 31
effectedsend127.0.0.1 port 53 (AF_INET)okPID 31
effectedsend127.0.0.1 port 53 (AF_INET)okPID 31
attemptedmkdir/tmp/node-compile-cache/v22.23.4-x64-9ac5647c-61000errno=2 (no such file or directory)PID 31
attemptedmkdir/work/projecterrno=17 (file exists)PID 31
attemptedmkdir/work/project/node_modules/plimsoll-fixture-quieterrno=2 (no such file or directory)PID 31
attemptedmkdir/work/project/node_modules/plimsoll-fixture-quieterrno=17 (file exists)PID 31
attemptedmkdir/work/.npm/_cacache/tmperrno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/content-v2/sha512/ad/89errno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/content-v2/sha512/aderrno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/content-v2/sha512errno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/content-v2/sha512/ad/89errno=17 (file exists)PID 31
attemptedmkdir/work/.npm/_cacache/index-v5/fe/d1errno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/index-v5/feerrno=2 (no such file or directory)PID 31
Step 2: the import
node loading the installed package: this is the package's own code running at import time.
0 effected0 attempted0 undetermined0 written
  • 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.

The pod's start and sandboxd
The pod's first process, which runsc starts from the container spec, so the log shows no program load for it, and what it forked without loading a program. The first process bound ports 9090 and 8080, sandboxd's two ports in the template: it is sandboxd, agent-sandbox's daemon in the pod.
11 effected1 attempted0 undetermined0 written
  • 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
effectedbind::1 (AF_INET6)okPID 1
effectedbind::ffff:7f00:1 (AF_INET6)okPID 1
effectedbindport 9090 (AF_INET6)okPID 1
effectedlistensocket:[4]okPID 1
effectedbindport 8080 (AF_INET6)okPID 1
effectedlistensocket:[5]okPID 1
attemptedsocketAF_INET SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCKerrno=22 (invalid argument)PID 1
plimsoll's check of the pod
The command plimsoll sends before any of the run's code: it reads the kernel's first log line (gVisor's boot line), the boot ID, the seccomp mode (the kernel's filter on which calls a process may make) and the mount table.
4 effected3 attempted0 undetermined0 written
  • 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
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 9
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 9
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 9
plimsoll's runner
plimsoll's in-sandbox runner, node /runner.mjs: it writes the run's files, checks that a step cannot open its descriptors, then starts each step in its own shell.
10 effected10 attempted0 undetermined4 written
  • 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
attemptedopen for writing/proc/16/fd/1errno=13 (permission denied)PID 24
attemptedmkdir/errno=17 (file exists)PID 17
attemptedmkdir/work/errno=17 (file exists)PID 17
attemptedmkdir/work/projecterrno=17 (file exists)PID 16
attemptedmkdir/work/project/packages/networkerrno=2 (no such file or directory)PID 16
attemptedmkdir/work/project/packages/networkerrno=17 (file exists)PID 16
attemptedmkdir/work/project/packages/networkerrno=17 (file exists)PID 16
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 16
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 16
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 16
Step 1: npm install
npm itself, installing the package from its directory, offline. Its install script, if it has one, is the next group.
1141 effected11 attempted0 undetermined561 written, 552 of them node's compile cache
  • 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
effectedconnect127.0.0.1 port 65535 (AF_INET)okPID 31
effectedconnect::1 port 65535 (AF_INET6)okPID 31
effectedbindno address stated (AF_INET)okPID 31
effectedsend127.0.0.1 port 53 (AF_INET)okPID 31
effectedsend127.0.0.1 port 53 (AF_INET)okPID 31
effectedsend127.0.0.1 port 53 (AF_INET)okPID 31
effectedsend127.0.0.1 port 53 (AF_INET)okPID 31
attemptedmkdir/tmp/node-compile-cache/v22.23.4-x64-9ac5647c-61000errno=2 (no such file or directory)PID 31
attemptedmkdir/work/projecterrno=17 (file exists)PID 31
attemptedmkdir/work/project/node_modules/plimsoll-fixture-networkerrno=2 (no such file or directory)PID 31
attemptedmkdir/work/project/node_modules/plimsoll-fixture-networkerrno=17 (file exists)PID 31
attemptedmkdir/work/.npm/_cacache/tmperrno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/content-v2/sha512/5d/37errno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/content-v2/sha512/5derrno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/content-v2/sha512errno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/content-v2/sha512/5d/37errno=17 (file exists)PID 31
attemptedmkdir/work/.npm/_cacache/index-v5/10/b1errno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/index-v5/10errno=2 (no such file or directory)PID 31
The package's postinstall script
npm runs this when it installs the package: package.json says "postinstall": "node install.js". Everything here is the package's own code.
10 effected0 attempted1 undetermined0 written
  • PID 43 (from 31), exit 0: node install.js
effectedsocketAF_INET SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCKokPID 43
effectedbindno address stated (AF_INET)okPID 43
effectedsend127.0.0.1 port 53 (AF_INET)okPID 43
effectedsend127.0.0.1 port 53 (AF_INET)okPID 43
effectedsocketAF_INET SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCKokPID 43
effectedsocketAF_INET SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCKokPID 43
effectedbindno address stated (AF_INET)okPID 43
effectedsendsendmsg: the destination is not in the log; the message header changed during the callokPID 43
effectedsend127.0.0.1 port 53 (AF_INET)okPID 43
effectedsend127.0.0.1 port 53 (AF_INET)okPID 43
undeterminedconnect203.0.113.7 port 443 (AF_INET)errno=115 (operation now in progress)PID 43
Step 2: the import
node loading the installed package: this is the package's own code running at import time.
0 effected0 attempted0 undetermined0 written
  • 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.

The pod's start and sandboxd
The pod's first process, which runsc starts from the container spec, so the log shows no program load for it, and what it forked without loading a program. The first process bound ports 9090 and 8080, sandboxd's two ports in the template: it is sandboxd, agent-sandbox's daemon in the pod.
11 effected1 attempted0 undetermined0 written
  • 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
effectedbind::1 (AF_INET6)okPID 1
effectedbind::ffff:7f00:1 (AF_INET6)okPID 1
effectedbindport 9090 (AF_INET6)okPID 1
effectedlistensocket:[4]okPID 1
effectedbindport 8080 (AF_INET6)okPID 1
effectedlistensocket:[5]okPID 1
attemptedsocketAF_INET SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCKerrno=22 (invalid argument)PID 1
plimsoll's check of the pod
The command plimsoll sends before any of the run's code: it reads the kernel's first log line (gVisor's boot line), the boot ID, the seccomp mode (the kernel's filter on which calls a process may make) and the mount table.
4 effected3 attempted0 undetermined0 written
  • 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
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 9
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 9
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 9
plimsoll's runner
plimsoll's in-sandbox runner, node /runner.mjs: it writes the run's files, checks that a step cannot open its descriptors, then starts each step in its own shell.
10 effected10 attempted0 undetermined4 written
  • 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
attemptedopen for writing/proc/16/fd/1errno=13 (permission denied)PID 24
attemptedmkdir/errno=17 (file exists)PID 17
attemptedmkdir/work/errno=17 (file exists)PID 17
attemptedmkdir/work/projecterrno=17 (file exists)PID 16
attemptedmkdir/work/project/packages/canaryerrno=2 (no such file or directory)PID 16
attemptedmkdir/work/project/packages/canaryerrno=17 (file exists)PID 16
attemptedmkdir/work/project/packages/canaryerrno=17 (file exists)PID 16
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 16
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 16
attemptedconnect/var/run/nscd/socket (AF_UNIX)errno=2 (no such file or directory)PID 16
Step 1: npm install
npm itself, installing the package from its directory, offline. Its install script, if it has one, is the next group.
1143 effected13 attempted0 undetermined558 written, 552 of them node's compile cache
  • 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
effectedconnect127.0.0.1 port 65535 (AF_INET)okPID 31
effectedconnect::1 port 65535 (AF_INET6)okPID 31
effectedbindno address stated (AF_INET)okPID 31
effectedsend127.0.0.1 port 53 (AF_INET)okPID 31
effectedsend127.0.0.1 port 53 (AF_INET)okPID 31
effectedsend127.0.0.1 port 53 (AF_INET)okPID 31
effectedsend127.0.0.1 port 53 (AF_INET)okPID 31
attemptedmkdir/tmp/node-compile-cache/v22.23.4-x64-9ac5647c-61000errno=2 (no such file or directory)PID 31
attemptedmkdir/work/projecterrno=17 (file exists)PID 31
attemptedmkdir/work/project/node_modules/plimsoll-fixture-canaryerrno=2 (no such file or directory)PID 31
attemptedmkdir/work/project/node_modules/plimsoll-fixture-canaryerrno=17 (file exists)PID 31
attemptedmkdir/work/.npm/_cacache/tmperrno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/content-v2/sha512/c2/97errno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/content-v2/sha512/c2errno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/content-v2/sha512errno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/content-v2/sha512/c2/97errno=17 (file exists)PID 31
attemptedmkdir/work/.npm/_cacache/index-v5/5d/5cerrno=2 (no such file or directory)PID 31
attemptedmkdir/work/.npm/_cacache/index-v5/5derrno=2 (no such file or directory)PID 31
attemptedrmdir/work/project/node_moduleserrno=39 (directory not empty)PID 31
attemptedrmdir/work/project/node_modules/plimsoll-fixture-canaryerrno=39 (directory not empty)PID 31
The package's postinstall script
npm runs this when it installs the package: package.json says "postinstall": "node install.js". Everything here is the package's own code.
0 effected1 attempted0 undetermined0 written
  • PID 43 (from 31), exit 1: node install.js
attemptedopen to create or empty/root/.canaryerrno=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.

Run record, bundle line 1
instance k8s-pod-uid:fcbbc735-79ae-4871-b08f-3a8dab68675f
record b69b8960903c708887f2eaeecd140224e9a9306efd7d3cafd5ee35bcd1291205
=
runsc log of the quiet run
container spec, pod UID fcbbc735-79ae-4871-b08f-3a8dab68675f
log 8f458f815dbe87f88031f6e8956cfc54e336ff452a4ab95b623f2629c356833e
Run record, bundle line 2
instance k8s-pod-uid:6ff9c6f1-4b63-40a0-af88-869b1a087a1b
record bf86e832588ee1c77659c9bae0763b7e614aa40d51bceb525b80fcdcccf6013d
=
runsc log of the network run
container spec, pod UID 6ff9c6f1-4b63-40a0-af88-869b1a087a1b
log 98f9a3e05af99afbd6e853613b30757033376c58946b36ee9b846ca4899a65e0
Run record, bundle line 3
instance k8s-pod-uid:7f4001ae-cf30-4523-868f-50168cf8fd58
record 3400f04835e2a3395e3a8ba9f624586b5df21619cc959d8cfc1c08e2e15f1ef7
=
runsc log of the canary run
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.

Every receipt also signs this list of what it cannot see, so its limits travel with it:

How it ran, and how to check it

Clusterkind, 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
Observerrunsc release-20260907.0, network sandbox, 46 system calls logged, 16 bytes of written data kept per call; configuration digest 1469f8adb827c216bd3451bd590203169c3613c052b3f35988737f9ad8b32955
Imageplimsoll/sandbox-agentsandbox:dev, as each record names it: agentsandbox-image:sha256:c7750b7916591611e93978029237cdb1cd3268d885e79b7b91624514ba2b72ff
plimsolldbuilt 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 checkAs 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 keyharness.pub, key ID 6a9724e384b8de2aadcebd12cd9d967c736214522fdb220d1de49bc73f56a8f4: it signed the bundle and the receipts
Bundlebundle.jsonl: 4 lines, 3 runs and the closing checkpoint, sha256 cbc289183cd9b399b1ef6701649057e6c3bdc3a7740e6ca701da8befac740528

plimsolld's settings

SANDBOX_AGENTSANDBOX_API_SERVERhttps://172.24.0.2:6443
SANDBOX_AGENTSANDBOX_IMAGEplimsoll/sandbox-agentsandbox:dev
SANDBOX_AGENTSANDBOX_NAMESPACEplimsoll-dependency-receipt
SANDBOX_AGENTSANDBOX_RUNSC_HANDLERSrunsc-receipt
SANDBOX_AGENTSANDBOX_RUNTIME_CLASSgvisor-receipt
SANDBOX_AGENTSANDBOX_WARM_POOLreceipt
SANDBOX_CPUS1
SANDBOX_MEMORY_MB512
SANDBOX_MIN_ISOLATIONkernel
SANDBOX_PROVIDERagentsandbox

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

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.