Sandboxing — the model

A Jaiph workflow runs scripts, calls agents, and touches the filesystem on whatever machine jaiph run is invoked on. That power is the point — and also the risk: a careless or untrusted script can read files, exfiltrate secrets, and run arbitrary programs unless something constrains it.

Jaiph addresses this at two layers, each doing a different job:

This page explains the model: what each layer protects, what it deliberately does not protect, and why the design picks the trade-offs it does. The how-to of enabling or disabling Docker, the full configuration-key list, and the failure-mode codes live in their own how-to and reference pages — this page stays on the conceptual surface.

For the runtime implementation, see Architecture — Docker runtime helper.

Two layers, two jobs

Rules and Docker isolation are doing fundamentally different work, and it is worth keeping them separate:

Layer When it fires What it constrains What it does not constrain
Rules Compile time The set of step types allowed inside a rule body — no inline shell, no prompt, no const … = prompt, no send, no run async Anything a script does at runtime (rules can still call scripts via run)
Docker jaiph run launch time Filesystem reach, process isolation, capability surface, env-var exposure for every step in the workflow Network egress (default-on), agent credentials (forwarded by design), hooks (run on host)

Rules are about structure: by the time the compiler is done, a rule cannot contain a step type that mutates state in a surprising way. There is no OS sandbox around a rule body — when a rule calls a script, that script runs as a normal managed subprocess with the same access the workflow has. Treat rules as non-mutating checks by convention; do mutation in workflows.

Docker is about blast radius: it cannot stop a script from misbehaving, but it can keep that misbehavior inside a disposable container.

The three sandbox modes

When Docker is enabled, the CLI picks one of three sandbox primitives at launch. The mode controls how the workspace is presented to the container; the env allowlist, mount allowlist, and --security-opt no-new-privileges posture is the same across all three. Every mode starts from --cap-drop ALL; overlay mode adds back a small cap set for fuse-overlayfs (see What Docker protects against).

Overlay and copy are interchangeable from the user’s point of view — both produce the property that the host workspace is unmodified after a Docker run. Inplace explicitly opts out of that property in exchange for a tighter dev loop, and the CLI gates it behind a destructive-edit confirmation prompt before launch.

In every mode, run artifacts are written to a separate read-write mount at /jaiph/run (outside the workspace sandbox) so the artifact tree under .jaiph/runs/ persists on the host regardless of what happened inside the container.

What Docker protects against

The Docker sandbox is designed to contain damage from untrusted or semi-trusted workflow scripts. Its protections are:

What Docker does not protect against

Equally important is the list of things Docker is deliberately not claiming to defend:

This list exists because a sandbox that overclaims is worse than one that is honest about its scope. Jaiph treats the Docker boundary as a blast-radius reducer for workflow scripts, not as a credential vault or a network firewall.

Why opt-out, not opt-in

The default-on choice — Docker on unless the host sets JAIPH_UNSAFE=true or sets JAIPH_DOCKER_ENABLED to any value other than exact true — is deliberate. Workflows orchestrate agent and script code that is often pulled from a repository, edited by a model, or contributed by a third party. Making the safer posture the path of least resistance means a careless workflow gets contained by default and only escapes the container when a human types out the override.

A second, equally deliberate choice: enablement lives entirely in environment variables, not in in-file config. Module-level runtime.docker_* keys can tune image, network, and timeout, but nothing in a .jh file can turn Docker off — runtime.docker_enabled is rejected at parse time. That keeps the “host is in charge of sandbox enablement” property: pulling a workflow file from a less-trusted source cannot ship an off-switch with it.

The escape hatch — JAIPH_UNSAFE=true or jaiph run --unsafe — exists because some environments genuinely cannot run Docker (a sandboxed CI without nested virtualization, a developer iterating on the runtime itself). The choice to take that hatch should be visible and ergonomic, which is why it is a single explicit host-side switch rather than an in-file config knob.

Why jaiph test does not use Docker

The test runner runs in-process on the host. This is intentional: tests are a development feedback loop, they typically mock prompts and replace external calls, and Docker spawn overhead would harm the iteration cycle. Tests already get isolation from the things they care about (prompts, network) through the runtime’s mock infrastructure. The Docker boundary is for jaiph run, where the workflow is executing real scripts against real resources.

How sandboxing fits the rest of Jaiph

The Docker sandbox does not change workflow semantics. The runtime inside the container is the same NodeWorkflowRuntime AST interpreter that runs locally — the container runs jaiph run --raw, which spawns the internal __workflow-runner child the same way as host --raw execution (see Architecture — Docker runtime helper), same __JAIPH_EVENT__ stream on stderr, same run_summary.jsonl written under .jaiph/runs/. The only differences are where processes execute and what host resources they can reach.

That property is the point of the design: a workflow is the same workflow whether it runs sandboxed or not. The sandbox is a deployment decision, not a programming model.