Concepts
The receipt
Every run returns the same versioned record. Its job is to tell you not just what happened, but how PyBox knows it, and what it doesn't know.
Four kinds of fact#
The receipt (formally, the inspection record, schema version 1) is organized by where each fact came from, because that's what decides how much you can trust it.
| Section | What's in it | Trust it? |
|---|---|---|
configured | What PyBox asked for: limits, image, user, network | It's a request, not proof |
observed | What the container engine reported, plus counters from the Linux kernel | Yes. Independent of the program |
capture | What the program printed | No. A program can print anything |
unknown | What can't be determined from this run | Always present, never guessed away |
Two more sections describe PyBox's own actions: lifecycle (which deadlines fired, whether you cancelled) and cleanup (whether the box is confirmed gone). For runs that move files, files lists what went in and came back, with sizes and SHA-256s. Returned files count as the program's word, like stdout.
pybox exec prints a short view of this record in the terminal, grouped the same way: from the engine, from the kernel, from the program, and not known. pybox exec --json prints the record itself.
Why "the program said" is kept apart#
Imagine a program that prints All tests passed! and exits with code 0, while a child process it started was killed for using too much memory. PyBox recorded exactly that situation. The receipt shows the exit code 0 from the engine, the engine's own out-of-memory flag, and the kernel's count of one out-of-memory kill, side by side. Reading only the exit code, or only the output, would have told you the wrong story.
Output is also capped (64 KiB per stream by default; 1–8,192 bytes per stream over MCP) and marked when it was cut short.
The fields you'll use most#
| Field | Meaning |
|---|---|
observed.policy_verified_before_start | true if every rule was read back and matched before the program started |
observed.engine_state.ExitCode | The program's exit code, from the engine |
observed.engine_state.OOMKilled | The engine says the box ran out of memory |
observed.resources.metrics | Kernel counters (memory, processes, CPU): first, last and largest values seen |
observed.termination.mechanism | Why it ended, only when PyBox has evidence (labels) |
capture.stdout, capture.stderr | The program's output: untrusted |
capture.stdout_truncated | Output was longer than the cap and the rest was dropped |
lifecycle.timeouts | Which deadline fired, e.g. ["wall"] |
cleanup.status | REMOVED when the box was removed and confirmed gone (all statuses) |
explanations | Short, evidence-tagged reasons, e.g. OOM_KILL_COUNTER_OBSERVED |
What's always unknown#
- Final resource counters. The watcher reads the counters while the program runs, but the box can disappear between two reads. The receipt gives the last value it actually saw and says the final value is unknown.
- The exact cause of termination. Even a well-supported label is narrower than proof. A program could, in principle, kill itself at the exact moment a timer would have.
- Clock alignment. Your Mac, the container engine and the kernel each keep their own clock. PyBox never pretends they line up exactly.
Explanation codes#
| Code | Evidence behind it |
|---|---|
TARGET_EXIT_OBSERVED | The engine's final state |
DEADLINE_REACHED | PyBox's own clocks |
SUPERVISOR_KILL_ATTEMPTED | PyBox's recorded stop command |
OOM_REPORTED_BY_ENGINE | The engine's out-of-memory flag |
OOM_KILL_COUNTER_OBSERVED | The kernel's memory event counter |
TASK_LIMIT_EVENT_OBSERVED | The kernel's process-limit counter |
CPU_THROTTLING_OBSERVED | The kernel's CPU counter (slowing down, not stopping) |
CANCELLED | You interrupted the run |
RUNNER_ERROR | Something in PyBox itself went wrong; read the detail |
COUNTERS_UNAVAILABLE | The watcher couldn't collect counters |
POLICY_NOT_VERIFIED_BEFORE_START | The rules weren't confirmed, so the program didn't start |