Sandboxing

Workflows mix shell, agents, and inbox routing. That makes it easy to accidentally depend on mutable global state or to run untrusted code with full host access. Sandboxing is how Jaiph narrows those risks: it separates what runs where and, on supported Linux setups, makes rule checks harder to abuse for destructive filesystem writes.

Jaiph provides two independent layers:

  1. Rule-level read-only isolation — every rule runs in a subprocess. On Linux, when mount-namespace tooling is available, the filesystem can be remounted read-only inside that subprocess. Elsewhere, rules still run in a child shell so an exit inside a rule does not tear down the parent workflow, but the host filesystem may stay writable (see below).

  2. Docker container isolation — optional. The transpiled workflow runs inside a container that receives only generated Bash, the shell stdlib, and copied runtime modules. Jaiph sources, TypeScript, and Node from the host toolchain are not required inside the container for the run itself.

The layers stack: rule-level isolation still applies to rules executed inside Docker.

For general config syntax, allowed keys, and precedence with environment variables, see Configuration. Docker-related keys are documented here in detail.

Rule-level read-only isolation

Every rule block is emitted so the implementation runs under jaiph::execute_readonly (see Rules under Transpilation in the grammar doc). You do not configure this; the transpiler wires it automatically.

On Linux, when all of the following hold — unshare and sudo on PATH, passwordless sudo (sudo -n), and a working unshare -m — the rule body runs under:

sudo env JAIPH_PRECEDING_FILES="$JAIPH_PRECEDING_FILES" unshare -m bash -c '
  mount --make-rprivate /
  mount -o remount,ro /
  ... invoke the rule function ...
'

The mount namespace makes the filesystem read-only for the duration of the rule: reads work; creating, modifying, or deleting files on mounted filesystems should fail. JAIPH_PRECEDING_FILES is forwarded so agent-related behavior that depends on it still works under sudo.

Otherwise (typical macOS install, containers without usable namespaces, or missing passwordless sudo): the implementation falls back to a child bash that invokes the same function. Process boundaries remain (e.g. exit in a rule does not kill the workflow runner), but the filesystem is not forced read-only. Treat rules as non-mutating checks in your design; rely on Linux + the prerequisites above for enforcement.

All shell functions are exported into the child environment so rule bodies can call helpers and shims defined in the same module.

Docker container isolation

Beta. Docker sandboxing is functional but still under active development. Expect rough edges, breaking changes, and incomplete platform coverage. Feedback is welcome at https://github.com/jaiphlang/jaiph/issues.

Where Docker settings may live

runtime.* keys belong only in a module-level config { ... } block (top of the .jh file). They are not allowed inside a workflow-level config block (workflow blocks may only override agent.* and run.*). Putting runtime.* in a workflow-level block is a parse error.

Enabling Docker sandbox

Docker sandboxing is opt-in. Set runtime.docker_enabled = true in module-level config, or control enablement with JAIPH_DOCKER_ENABLED:

config {
  runtime.docker_enabled = true
}

If JAIPH_DOCKER_ENABLED is set in the environment, it overrides in-file runtime.docker_enabled: only the literal string true turns Docker on; false or any other value turns it off. If JAIPH_DOCKER_ENABLED is unset, the in-file value (default false) applies.

When Docker is enabled but the docker binary is not usable (docker info fails), the run fails with E_DOCKER_NOT_FOUND (no silent fallback).

Configuration keys

All Docker-related keys live under runtime.* in module-level config:

Key Type Default Description
runtime.docker_enabled boolean false Enable Docker sandbox for the run.
runtime.docker_image string "ubuntu:24.04" Container image to use.
runtime.docker_network string "default" Docker network mode.
runtime.docker_timeout integer 300 Maximum execution time in seconds (0 disables the timeout timer).
runtime.workspace string array [".:/jaiph/workspace:rw"] Mount specifications.

Each key enforces its expected type at parse time. Unknown config keys anywhere in config produce E_PARSE (message lists allowed keys).

Environment variable overrides

Following the JAIPH_* convention: JAIPH_DOCKER_ENABLED, JAIPH_DOCKER_IMAGE, JAIPH_DOCKER_NETWORK, JAIPH_DOCKER_TIMEOUT. Workspace mounts are not overridable via env.

Precedence: JAIPH_DOCKER_* environment variables → in-file config → defaults.

If JAIPH_DOCKER_TIMEOUT is set but not a valid integer, the default timeout (300) is used.

Mount parsing rules

Mount strings in runtime.workspace use these forms:

Host paths are resolved relative to the workspace root when building docker run -v arguments.

Workspace structure inside the container

/jaiph/
  generated/          # mounted read-only
    <script>.sh       # transpiled bash script(s); see below
    jaiph_stdlib.sh   # shell stdlib
    runtime/          # shell runtime modules
      events.sh
      test-mode.sh
      steps.sh
      inbox.sh
      prompt.sh
      sandbox.sh
  workspace/          # the mount targeting /jaiph/workspace (read-write root)
    .jaiph/
      runs/
        <YYYY-MM-DD>/
          <HH-MM-SS>-<source-file>/
            000001-<module>__<step>.out
            000002-<module>__<step>.out
            ...

The CLI also mounts the host directory containing the run meta file read-write at the same path inside the container so the wrapper can record exit status and paths.

Docker behavior

Dockerfile-based image detection

The runtime treats the image as explicitly configured if either runtime.docker_image appears in the file (any value) or JAIPH_DOCKER_IMAGE is set in the environment. In that case .jaiph/Dockerfile is not consulted.

When the image is not explicit (no in-file runtime.docker_image and no JAIPH_DOCKER_IMAGE):

  1. If .jaiph/Dockerfile exists in the workspace root, the runtime runs docker build, tags the result jaiph-runtime:latest, and uses that image.
  2. Otherwise it uses the configured image name (default ubuntu:24.04), pulling if needed.

Build failure → E_DOCKER_BUILD.

The repository’s example .jaiph/Dockerfile includes:

Agent environment variable forwarding

Besides variables forwarded as part of the normal JAIPH_* pass-through (except JAIPH_STDLIB, which the driver overrides), the following prefixes are forwarded for agent authentication and tooling:

Docker path remapping

When Docker mode is enabled, the CLI remaps workspace-related environment variables before passing them into the container so run artifacts land under the workspace mount.

Configure JAIPH_RUNS_DIR the same way as for a non-Docker run; remapping is automatic.

Example

Minimal workflow with Docker sandbox enabled (expects a config directory beside the workflow if you keep the extra read-only mount):

config {
  runtime.docker_enabled = true
  runtime.docker_image = "ubuntu:24.04"
  runtime.docker_timeout = 600
  runtime.workspace = [
    ".:/jaiph/workspace:rw",
    "config:config:ro"
  ]
}

workflow default {
  echo "Running inside Docker"
}