Concepts
Two deadlines
One clock is PyBox's own. The other lives inside the box, so a program can't outlive PyBox, even if PyBox itself is killed.
The short version#
| Clock | Default | Starts | When it fires |
|---|---|---|---|
| Mac deadline (primary) | 2 s | Just before the program starts | PyBox asks the engine to stop that exact box, checks it stopped, removes it |
| Box timer (backup) | 3 s | When the box's first process starts | The timer kills the program itself, with no help from PyBox |
The box timer is always one second longer, so in normal operation PyBox's own deadline acts first and the timer never has to.
Why one clock isn't enough#
PyBox's normal cleanup lives in its own Python process. If that process is force-killed (by you, by the OS, by a crash), its cleanup code never runs. A program left behind could keep running forever.
So the box's very first process isn't Python. It's a tiny timer that starts Python as its child:
timeout -s KILL 3 python -I -B -u -
When the timer runs out, it kills Python. It doesn't need PyBox to be alive. In a recorded test, PyBox was force-killed about 0.1 seconds into an 8-second sleep. The program still stopped at 3.03 seconds, and PyBox's own command logs show it never sent a kill.
A second recorded test went further: the program tried to stop or pause the timer by signalling it. The timer still fired, at 3.03 seconds both times.
Choosing the time limit#
The default is 2 seconds. You can lower it, or raise it to at most 30 seconds when a job needs it:
$ pybox exec slow_job.py --timeout 20The box timer follows: 20 s on the Mac means 21 s in the box. In a recorded test, a program sleeping for 60 s with --timeout 30 was stopped by PyBox at 30.01 s. For AI agents, the server's --max-timeout sets the ceiling. See longer runs. The receipt shows both, as wall_timeout_s and guest_timer_s.
Other bounded waits#
Every step PyBox takes has its own short budget, so no single slow step can hang a run:
| Step | Budget |
|---|---|
| Create the box | 5 s |
| Start and attach | The run's own deadline (at least 2 s) |
| Inspect, query, stop | 2 s each |
| Remove | 3 s |
| All of cleanup together | 15 s (a single retry, after an interruption mid-cleanup, gets a fresh 15 s) |
What the receipt says about it#
The receipt never decides why a program ended from its exit code alone. Exit code 137 means "killed", but not by whom. So PyBox only labels the cause when it has its own evidence:
| Label | Means |
|---|---|
TARGET_EXIT_OBSERVED | The program ended by itself; PyBox didn't stop it |
SUPERVISOR_KILL_OBSERVED | PyBox sent a stop, the engine confirmed it, and the timestamps line up |
IN_CONTAINER_TIMER_CONSISTENT | PyBox sent nothing, and the program ended right when the box timer would. That's consistent with the timer, but not proof |
UNKNOWN | Anything else. PyBox says so instead of guessing |
Limits. Both clocks assume a healthy virtual machine that keeps getting CPU time. If your Mac sleeps or the VM is paused, time inside the box pauses too. These are strong practical limits, not hard real-time guarantees.