InitRunner

Runtime Sandbox

Since v2026.4.16, tool subprocesses run under kernel-level isolation, outside the initrunner process. This is distinct from the PEP 578 audit-hook sandbox (security.tools.audit_hooks_enabled), which runs inside the Python interpreter.

The audit-hook sandbox is defense-in-depth, not a containment boundary. Code running in the same interpreter can defeat it; for untrusted code the real boundary is the runtime sandbox (bwrap or docker) that runs it in a separate process. Since v2026.6.1, the audit hook closes two bypasses: write intent is now decoded from os.open() integer flags (so an os.open(path, O_WRONLY|O_CREAT) is checked against allowed_write_paths, not just open() mode strings), and the subprocess block now covers os.posix_spawn, os.exec*, os.fork, and os.forkpty in addition to subprocess.Popen and os.system. Violations are recorded to the audit chain as sandbox_violation events. See Security for the full audit-hook reference.

Pick one of three backends:

  • Bubblewrap — Linux user namespaces. No daemon, no Docker, no root. Default on Linux.
  • Docker Sandbox — Containers via the Docker daemon. Works on macOS, Windows, and Linux. Supports pinned images and bridge networking.
  • SSH Backend — Remote execution on an existing host via OpenSSH (since v2026.5.1). Not a kernel sandbox; use it to choose where code runs, not for containing untrusted code.

backend: auto tries bwrap on Linux and falls back to Docker. SSH is opt-in only (backend: ssh) because it needs an explicit remote host. This page is the shared config reference; the linked pages cover each backend's details, examples, and limits.

Configuration

security:
  sandbox:
    backend: auto                 # auto | bwrap | docker | ssh | none (default: none)
    network: none                 # none | bridge | host
    allowed_read_paths: []
    allowed_write_paths: []
    memory_limit: "256m"
    cpu_limit: 1.0
    read_only_rootfs: true
    bind_mounts: []
    env_passthrough: []
    docker:
      image: "python:3.12-slim"
      user: "auto"
      runtime: null               # null (runc) | runsc | kata-runtime | kata-qemu | kata-fc | kata-clh
      extra_args: []
    ssh:
      host: my-build-box          # required when backend: ssh
      remote_cwd: /srv/work       # optional; SSH login dir if unset
      identity_file: null         # optional override; falls back to ~/.ssh/config
      config_file: null           # optional override
      connect_timeout: 10
      control_persist: "60s"

backend

ValueBehavior
noneNo isolation. Tool subprocesses run on the host.
bwrapBubblewrap (Linux only). Lightweight user-namespace sandbox.
dockerDocker container. Requires a running Docker daemon.
sshRemote execution on a host via OpenSSH. Not a kernel sandbox. See SSH Backend.
autoPrefers bwrap on Linux, falls back to Docker. Never selects ssh (requires explicit host) and never falls to none.

Since v2026.6.1, the shell, python, and script tools log a warning at build time when backend: none (host execution), since model-driven commands then run with the initrunner process's own privileges. The shell tool also warns on an empty allowed_commands list, where every binary is permitted (an interpreter such as sh -c ... re-enables a full shell). Both are deliberate opt-outs, not errors.

network

Valuebwrapdocker
none--unshare-net (empty namespace)--network none
bridgeNot supported (raises error)--network bridge
hostHost network (no namespace)--network host

docker sub-config

  • image — Docker image (default python:3.12-slim).
  • user — Container user. "auto" maps to the current uid:gid when writable mounts exist. null runs as root.
  • runtime — Container runtime. null (default) uses Docker's default (runc). Other values: runsc (gVisor), kata-runtime, kata-qemu, kata-fc, kata-clh (Kata microVM hypervisor variants). Validated at preflight against the runtimes Docker has registered; an unregistered choice fails with a per-runtime install hint. Since v2026.5.2.
  • extra_args — Additional docker run flags. Changed in v2026.6.1: this is an allowlist; only resource and label flags are permitted, and mounts, host namespaces, capabilities, devices, and runtime overrides are rejected at load. --runtime is rejected here too; use runtime above instead. See extra_args validation for the full list.

ssh sub-config

  • host — required. Alias from ~/.ssh/config or user@hostname.
  • remote_cwd — working directory on the remote host. Defaults to the SSH login directory.
  • identity_file — optional IdentityFile override. Prefer setting this in ~/.ssh/config.
  • config_file — optional ~/.ssh/config path override.
  • connect_timeout — seconds for the initial connection (default 10).
  • control_persist — how long the multiplexed connection stays warm (default "60s", any OpenSSH duration string).

See SSH Backend for the full reference, the v1 limits (no bind_mounts, no python_exec yet), and the security posture warning.

For when to pick which backend, and what each runtime buys you, see Sandbox Comparison.

Backends at a glance

bubblewrapDockerSSH
PlatformLinux onlymacOS, Windows, LinuxAnywhere ssh is on PATH
DaemonNone (bwrap binary)Docker daemon requiredOpenSSH client; remote sshd
IsolationUser namespacesContainerNone (host-level on remote)
Startup costfork+execve~200–500ms per call~150–500ms cold, tens of ms warm (ControlMaster)
FilesystemHost /usr, /bin, /lib bound read-onlyPinned imageWhatever the remote host has
Network isolation--unshare-net--network noneNot enforced
Bridge networkingNot supported--network bridgeRejected at config load
Resource limitssystemd-run --user (skipped with a warning if unavailable)-m, --cpus, --pids-limitNone in v1
Bind mountsSupportedSupportedRejected at config load (v1)
Installapt install bubblewrapapt install docker.io or Docker Desktopapt install openssh-client

Mount validation

Mounts fall into two categories:

  • User-configured (allowed_read_paths, allowed_write_paths, bind_mounts from role YAML) — validated at load time against the role's permitted roots. A typo'd /etc mount fails before the role ever runs.
  • Tool-internal (e.g. python_exec writes code to /tmp and mounts it at /work/_run.py) — code-controlled, trusted, no validation.

Audit

Every sandboxed call logs a sandbox.exec security event:

FieldMeaning
backendwhich backend ran the command
argv0the command that ran
rcexit code
duration_mswall-clock time

Query with:

initrunner audit security-events --event-type sandbox.exec

Adaptive preflight

backend: auto and backend: bwrap run a functional probe before launching any tool:

bwrap --ro-bind /usr /usr -- /bin/true

If the probe fails, SandboxUnavailableError.remediation reads /proc/sys/kernel/unprivileged_userns_clone and /proc/sys/kernel/apparmor_restrict_unprivileged_userns and emits the specific fix — sysctl, AppArmor profile reinstall, or switch to backend: docker. The CLI renders the error as a clean Rich panel and exits with code 1.

initrunner doctor --role <file> now renders a sandbox row showing the resolved backend and readiness — Docker daemon reachable, bwrap probe passing, image pulled.

Migrating from security.docker

security.docker has been removed. A role still using it fails schema validation with migration instructions:

# Old (removed in v2026.4.16)
security:
  docker:
    enabled: true
    image: python:3.12-slim
    network: none

# New
security:
  sandbox:
    backend: docker
    network: none
    docker:
      image: python:3.12-slim

Bundle metadata

Published bundles declare supported_sandbox_backends in the manifest. initrunner install checks the host and warns when none of the listed backends is available. See OCI Distribution for the full manifest schema.

On this page