Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

How a job is boxed

The content of a job is hostile, and so is whatever opens it: a library that decodes an image or reads a model’s reply can be exploited by what it reads. So sandcastle never opens a job itself. Each job is opened in a box made for it alone, with nothing in it worth taking and no way out, and the box is gone when the job ends.

sandcastle never runs a capability itself. Every job and every check goes to a box of its own, which is the only place where the job’s content is opened: the input, the image, the model’s reply. sandcastle and the box speak in JSON lines on the box’s stdin and stdout, one message per line; sandcastle box <capability> is the box’s side.

Each box is a new rootless Podman container, podman run --rm -i, removed when it ends:

  • no network, a read-only root, and /tmp as its only writable folder, of box.tmp;
  • no folder of the machine mounted, no capabilities, no new privileges;
  • the user 65534, which is not the operator’s on the machine, and an empty environment;
  • box.memory with no swap on top, box.cpus, box.pids, and a time limit;
  • its stderr never reaches the journal.

Nothing of a job goes in an argument or in the environment. Each box is named sandcastle-box-<random> and labelled sandcastle.box=sandcastle, and sandcastle.role=core or sandcastle.role=check for who started it. When a box runs past its time, sandcastle removes it by name and confirms it is gone, since ending podman run leaves the box alive; no other box starts until it is. When run or once starts, it removes the core’s boxes a crash left behind, never the box of a check running alongside.

The images are sandcastle-ocr:<build> and sandcastle-claude:<build>. The build is the first 12 characters of the SHA-256 of the sandcastle binary, and each image holds that same binary, so a box of another build is refused. sandcastle version shows the build and the commit. The binary must be a static Linux binary (CGO_ENABLED=0).

The limits of each capability’s box can be lowered or raised in its box block; check warns of a limit above the default, which is outside the promise. No key opens a lock. The defaults are in config.yaml, for claude and for ocr.

  • sandcastle reads only a job’s id, capability and input, and passes the input on untouched. Other fields in the envelope, such as a destination or a url, have no effect.
  • Each capability has a max_input and a max_output, and a timeout for its box (the source’s job_timeout also applies). A line from the box that is not JSON, of an unknown type, one too many, larger than max_output, a box that runs out of time, memory or processes, or one that exits with another code than 0, fails the job.
  • A missing Podman, a box Podman cannot create (a missing image, for one) and a box of another build release the job, and the capability rests for its cooldown.
  • What the box says about a failure, and what it writes on stderr, is kept cut short in the job’s record and never in the journal.