One sandbox, five calls: an OpenShell session with a signed chain of records
A session keeps one sandbox for many calls: the files a call writes are there for the next call, and no process a call starts outlives it. This page is one real session on an NVIDIA OpenShell sandbox, run through plimsolld with the official Go client on 2026-10-04. A harness outside the daemon checked and signed every call's record, and the records form a chain: each names the digest of the one before it.
The calls
| # | What the call sent | What came back | ms | Record |
|---|---|---|---|---|
| 1 | project: Write lib.js and test.js, run the test. lib.js has a bug. files: lib.js, test.js step: node test.js |
completed, step exited 1 add(2, 3) = -1 but should be 5 |
89 | record f92bd3df8d62… previous (none) request 34174104c576… result 051833a2c440… |
| 2 | snippet: Fix the bug in place: the file the first call wrote is still there. const fs = require("fs");
const path = "/tmp/work/lib.js";
fs.writeFileSync(path, fs.readFileSync(path, "utf8").replace("a - b", "a + b"));
console.log("patched " + path); |
exit 0 patched /tmp/work/lib.js |
72 | record c10fb80babf1… previous f92bd3df8d62… request 52941fb847cb… result d5581c0de798… |
| 3 | project: Run the same test again. No files are sent: they persisted. step: node test.js |
completed, step exited 0 test passed: add(2, 3) = 5 |
139 | record ce75fda50e87… previous c10fb80babf1… request 1e06c1ba6c08… result 6688830180f5… |
| 4 | snippet: Leave a detached process behind, as careless or hostile code might. const { spawn } = require("child_process");
const child = spawn("sleep", ["600"], { detached: true, stdio: "ignore" });
child.unref();
console.log("started sleep 600 as pid " + child.pid + " and exited"); |
exit 0 started sleep 600 as pid 152 and exited |
68 | record d017ac7279c8… previous ce75fda50e87… request 8c0c97a3ddc7… result 7473f7e23227… |
| 5 | snippet: Look for that process, and read lib.js once more. const fs = require("fs");
const left = [];
for (const d of fs.readdirSync("/proc")) {
if (!/^[0-9]+$/.test(d)) continue;
let cmd = "";
try { cmd = fs.readFileSync("/proc/" + d + "/cmdline", "latin1").split("\0").join(" ").trim(); } catch { continue; }
if (cmd === "sleep 600") left.push(+d);
}
console.log("sleep 600 still running: " + (left.length ? left.join(", ") : "none"));
console.log("lib.js now: " + fs.readFileSync("/tmp/work/lib.js", "utf8").trim()); |
exit 0 sleep 600 still running: none lib.js now: module.exports = (a, b) => a + b; |
98 | record adf08a4e4ffc… previous d017ac7279c8… request eb4b6b7d706d… result 72bc024f2abe… |
Call 3 sent no files: the test and the patched library were still in the sandbox. Call 5 finds call 4's sleep 600 gone: after every call the sandbox's processes are swept.
The chain
Session c92f06904ddae5503b73cdfee68f984e2e0cd60285806c3287bde1fac264a4f8 is the SHA-256 of the session ID; the ID itself is a capability, sent only to the daemon, and never recorded or logged. The daemon's close: closed after 5 calls; last record adf08a4e4ffc058bcb8483fcc7557531f1891a89fcba84f5fdb12eebb2239ee2.
The verifier
| Bundle | Verdict | What the verifier said |
|---|---|---|
| The bundle as the harness wrote it | verified | 7 entries; one session of 5 calls, every record signed, chained and closed |
| The same bundle with call 3 removed | refused | entry 3: attest: broken bundle chain: the line says it is line 4 |
| The same bundle with one byte of call 2's output changed | refused | entry 2: attest: broken bundle chain: the line's link signs other content |
| The same bundle with its last two lines (the close and the checkpoint) cut | refused | attest: the bundle does not end with a checkpoint |
Every record is a DSSE envelope around an in-toto statement, signed with an Ed25519 key the harness held for this run (key ID 84338813b9ce78aa6c11cf59c1b5ef11f73971080b04c256a226fdded0afac50). The daemon holds no key: it only hashes. Every line of the bundle also carries a link the harness signed, naming the line before it, and the bundle ends with a checkpoint stating how many lines come before it, so a line removed, moved or added anywhere in the file, or a cut end, is refused even when no session chain covers it. The bundle and the public key sit beside this page as bundle.jsonl and harness.pub.
What happens around each call
- Before: the provider reads the sandbox and its effective policy back from the OpenShell gateway and ends the session on any difference, because any client of the gateway could change them between calls.
- After: a sweep kills every process except the sandbox's own two (OpenShell's supervisor and an idle main process), by process ID, and measures what the session left under
/tmpagainst its disk budget. The sweep's verdict is its exit status, which leftover code cannot forge on a host that restricts ptrace; the provider refuses a session on any other host. When the sweep cannot prove the sandbox clean, the sandbox is stopped and started, which ends every process; when that fails too, the session ends. - Idle: a session with no call for five minutes (the default) is stopped, not deleted: it holds no memory or CPU, keeps its files, and the next call starts it again, which took about 0.8 s when measured.
- End: at its lifetime (30 minutes by default), past its disk budget, or when code in the sandbox kills its main process. The owner's close collects how many calls ran and which record was last.
What this does not show
- A record is the daemon's statement. Its digests prove that the stored requests and results are the ones the daemon stated; the evidence fields (provider, tier, image (none stated), policy openshell-policy:sha256:d1c56801c3fcf0b1d8473339f3b625b1cbf43d5401c1268026551dd28a39d871) are configuration and provider evidence, never runtime attestation.
- A session is only as private as its gateway: any other client of the same gateway can reach the sandbox between calls. Give plimsoll a gateway, or an OpenShell workspace, of its own.
- Processes never persist, by design, so a long-running service inside a session (a development server) is not supported.
- The key here was made for this run and discarded. In use, the harness's key is the caller's, kept outside the daemon.
- The links prove the bundle is the whole file the harness wrote, not that the harness was handed every call the caller made;
plimsoll-attest verify -expectchecks the bundle against the caller's own list of its calls' trace IDs (the five calls here carry call-1 to call-5). A bundle cut back to an earlier checkpoint (one the harness was resumed after) verifies as whole: the caller's list catches that.
Reproduce: with an OpenShell gateway and its five SANDBOX_OPENSHELL_* settings in the environment, run go run ./examples/sessions in the plimsoll repository; the page, the bundle and the key are written from the run. Check the bundle: go run ./cmd/plimsoll-attest verify -pub docs/examples/sessions/harness.pub docs/examples/sessions/bundle.jsonl. plimsoll is an independent project, not affiliated with or endorsed by NVIDIA.