Skip to content

Module sandbox

Every module process runs confined: the coordinator side, the runner, services, probes and doctor, on every platform. This page is the contract: what a module process may and may not reach. Each platform enforces it with its own backend:

Platform Backend Details
macOS Seatbelt backends/macos.md
Linux Landlock + seccomp, plus a cgroup v2 leaf backends/linux.md
Windows AppContainer + Job Object backends/windows.md

A module never sees the backend. It declares what it needs in [sandbox], and an operator approves it.

Area Coordinator Runner Service, probe
its bundle, its runtime, the platform’s base system read, execute read, execute read, execute
work directory none read, write none
data directory (kept across jobs) read, write read, write read, write
private temporary directory read, write read, write (inside work) read, write (inside data)
approved host tools ([sandbox].tools) none read, execute read, execute
everything else: homes, other modules, the agent’s and coordinator’s state, credential stores, devices other than null, zero, random and urandom denied denied denied

Processes

  • A module process may spawn children, and children inherit the confinement.
  • It may signal only processes in its own confinement.
  • It never creates persistent or detached jobs (launchd, systemd, cron, scheduled tasks, services) and never leaves its process container.
  • It cannot rely on sandboxing itself again: tools such as Bazel or SwiftPM must skip their own sandbox.

Local IPC. The only channel is the job’s broker endpoint, when granted. There are no other sockets, pipes, D-Bus, mach services or Docker sockets. Name resolution and logging go through backend-listed system facilities.

Grants are whole directories or single files, resolved to canonical absolute paths at launch. There are no globs and no “deny inside allow” exceptions.

Hiding which files exist is not promised (Landlock cannot restrict stat). Keep secrets out of readable paths and out of the environment.

Identity. A module must not depend on its user, uid or SID, or on privileges.

Resources. The agent may enforce mem_gb and CPU as hard limits where the OS does (Linux cgroups, Windows Job Objects). Exceeding the memory limit is a job fault (oom).

Verification. The parent verifies confinement after every spawn and kills an unconfined process. No unconfined module process ever runs.

[sandbox]
contract = 1
net = { mode = "egress-allowlist", allow = ["api.example.org", "*.files.example.org:8443"] }
tools = [{ id = "java17", trust = "code-exec" }]
devices = { gpu = "compute" }
containers = [{ image = "docker.io/org/tool:1.2@sha256:<64 hex>", platform = "linux/amd64" }]
exec_writable = false
Grant What the process gets
net.mode = "none" (default) No network at all.
net.mode = "egress-allowlist" Only the allow hosts, host[:port] with port 443 by default and *. for subdomains, never IP addresses. Traffic goes through the agent’s local proxy, which enforces the list and refuses a name that resolves to a non-public address (loopback, link-local, private, multicast); oarbank_sdk.egress_proxy is the reference. The agent sets HTTPS_PROXY/HTTP_PROXY/ALL_PROXY; the backend blocks every other route.
net.mode = "egress-any" Outbound connections to any public address, plus DNS. A separately approved full-trust grant.
tools Read and execute the host paths that the operator’s tool registry maps the id to, per OS (e.g. java17 → a JDK directory), resolved to canonical paths; the process finds them in OARBANK_TOOLS_FILE. trust = "code-exec" flags tools that run arbitrary code (a JVM, an interpreter, a shell) in the approval UI. On Windows, any readable binary is executable.
devices.gpu = "compute" GPU compute through the platform’s APIs, with no display server. Flagged at approval: it widens the kernel surface.
containers The job’s broker endpoint, for these digest-pinned images only. A stage that runs containers reserves the agent-provided containers pool.
exec_writable = true Runners may execute files they wrote into the data or work directory, such as downloaded tools. Windows cannot enforce false without application control, so nodes report it as unavailable there.

Network rules, whatever the mode:

  • never loopback or link-local;
  • never listening;
  • never another unix socket or pipe.

Approval

  • A version whose [sandbox] requests anything cannot be enabled, canaried, pinned or promoted until an operator approves its exact requests. The approval is recorded by digest and audited.
  • A new version needs its own approval. A version that requests nothing needs none.

Placement. Every node reports, per capability, whether its backend enforces it: enforced, cooperative or unavailable. A module runs only where all of its grants, and the always-on rules above, are enforced. A node that cannot enforce them advertises SANDBOX_BACKEND_MISSING or CAPABILITY_NOT_ENFORCED for that module.

A runner never talks to Docker or Podman itself. The agent owns a container runtime that mounts only its own work and module-data directories:

Platform Runtime
macOS Colima, or Apple container
Linux rootless Podman, or Docker Engine
Windows an agent-owned WSL2 distribution running Podman

Each job whose module may run containers gets an endpoint, OARBANK_BROKER (unix:/path or npipe://./pipe/<name>), the only IPC its confinement allows. Requests are one JSON object per line, one request per connection. Use oarbank_sdk.broker.

Request Fields Answer
container.run image (an approved reference), platform, args[], entrypoint?, mounts[] {src, dst, ro}, env{}, workdir?, network (only with an egress grant), timeout_s, cpus?, mem_gb? {ok, exit_code, stdout_tail, stderr_tail, stdout_path, stderr_path, duration_s}; the full output is written into the work directory
container.pull image, platform {ok} once the image is present
status {ok, running, images[]}
any refusal {ok: false, error, detail}: image_not_approved, bad_mount, network_not_granted, runtime_unavailable, platform_unavailable

Mounts

  • Mount sources are PortablePaths inside the work directory, or data:<path> inside the data directory.
  • Destinations are POSIX absolute paths, for linux/* images.

The agent’s container run

  • Always: run --rm, the platform, the CPU and memory limits within the job’s reservation, the validated mounts and the job’s ownership labels.
  • Never: privileged mode, host networking, or another socket.

Ownership and filesystem behaviour

  • Files a container writes belong to the job’s identity: rootless mode, or a user namespace, on Linux.
  • Exec bits and case sensitivity follow the host filesystem.
  • Containers die with their job.

oarbank-sdk conform runs the runner suite under this host’s backend with the grants the manifest declares, plus a portability lint for every declared platform. A module that writes outside its directories, reads homes, or expects a tool it didn’t request fails there first.

Source: spec/sandbox.md in the oarbank-sdk repository