Skip to content

Sandbox (trial isolation)

Subprocess fork-trial isolation (CONTRACT v1.5 #1). run_forked_trial runs candidate pack code against a fork of a saved run inside a fresh-interpreter child, materialized from artifacts pinned by a bundle hash — so the parent process stays out of the blast radius of a runaway (memory/CPU) or corrupt in-process state, and the bytes trialed are the bytes a proposal recorded.

For the design, the worked recorded-segment-replay example, the extra_packs cross-pack path, and the honest per-platform limits, see trial-isolation-design.md in the repository root.

Import path

The sandbox surface lives under activegraph.sandbox and is imported from there (from activegraph.sandbox import run_forked_trial, preflight); it is intentionally not re-exported at the top level.

Crash/state isolation, not a security sandbox

A fresh interpreter with rlimits stops runaway memory, CPU, and parent-state corruption. It does not confine syscalls, the network, or filesystem access — that is host territory (containers, seccomp). The env allow-list is closed (PATH/HOME/LANG plus an explicit env_passthrough); the child's PYTHONPATH is a computed code-location channel, not an ambient forward. The memory cap (RLIMIT_AS) is enforced on Linux and announced-unavailable (never crashed) on macOS/Windows, where the wall-clock kill and event budget remain the active nets.

Running a trial

Fork the parent at at_event and trial the candidate pack in a fresh subprocess. CONTRACT v1.5 #1.

The fork is created HERE, in the parent process, with full fork() semantics (lineage recorded, promote-block cut guard); the child receives only the fork's run id. scenario is "relative/path.py" or "relative/path.py::func" inside the CANDIDATE's pack root — the function (default main) is called with the fork's Runtime and drives the trial; empty means just run_until_idle(). The scenario contract is def main(rt): ....

The default is candidate-only isolation: the child loads nothing but the candidate, so the trial exercises it apart from every other pack's behaviors. extra_packs is the opt-in for cross-pack interaction trials (CONTRACT v1.5 #1 addendum 1b): each entry is materialized in the child exactly like the candidate — bundle hash verified before import, manifest schema + two-way surface check when required — and loaded, in order, BEFORE the candidate. Any extra pack failing its pins is materialization_failed for the whole trial; the scenario still resolves inside the candidate's root only.

Deterministic and key-free by default (max_llm_calls=0, empty environment pass-through). Returns a :class:TrialReport; never raises for in-trial failures — those are outcomes. Raises only for parent-side setup problems (bad store, bad fork point), with the same errors Runtime.load / fork() raise.

Startup preflight

Verify a trial child can START under the sandbox env on this box.

Spawns the child with a null job that exercises the full startup path a real trial hits — importing activegraph under the sandbox env AND applying resource limits (v1.7.1: a go/no-go gate that skipped limit application passed on macOS where every real trial then crashed on RLIMIT_AS). No fork, no store, no candidate.

Returns a tuple of degradation warnings — empty when every net applied cleanly, non-empty (and logged) when a net degraded on this platform (e.g. the memory cap on macOS). Raises :class:SandboxStartupError with the child's stderr tail if a child cannot start at all. So on macOS this PASSES with a memory-net warning rather than pass-then-crash; on a box that cannot import activegraph it RAISES with the real cause. Consumers call it once at boot to fail loud or learn what degraded before the first trial.

Pass the same limits a real trial will carry to probe that exact configuration; the default probes with a representative memory cap.

Bases: RuntimeError

A trial child could not START under the sandbox env.

Raised by :func:preflight when the child fails before it can run any trial — the common cause is the sandbox env being unable to import activegraph on this box (the exact restricted-env break the explicit package-path channel exists to prevent). The message carries the child's stderr tail so the cause is never opaque.

Inputs

Where the child materializes the candidate pack from.

expected_bundle_hash is the external pin (the §4 walk WITH manifest.toml, per the v1.4 bundle-hash amendment) — the child verifies it via activegraph.packs.manifest.verify_bundle_hash before importing anything, so the bytes trialed are the bytes the proposal recorded. manifest_required=True additionally runs load_manifest + verify_surface against the live Pack, making the trial child the first consumer of the full manifest chain end-to-end.

The trial's resource nets. Zero/None disables a given net.

max_llm_calls=0 (the default) means key-freedom is STRUCTURAL: the child configures no LLM provider at all, so a candidate with LLM behaviors fails loud at registration (MissingProviderError) rather than reaching a network. A positive cap is accepted and recorded for a future provider-wiring seam; the v1 child never configures a live provider either way. env_passthrough is the ONLY ambient parent environment forwarded beyond the closed allow-list (PATH/HOME/LANG) — the helper never forwards the environment wholesale, so parent API keys don't leak into candidate code by default. The child's PYTHONPATH is NOT an ambient forward: it is computed from the parent's resolved sys.path so the child can import its own code (see the module docstring's "two channels").

Result

What a trial produced. Store-derived numbers, child-signaled shape.

events_appended and behavior_failures are re-read from the fork's run in the store by the parent AFTER the child exits — the stdout tail is a signal only, and any disagreement resolves in favor of the store. Richer evidence (tracebacks, diffs) is read from the fork's log directly: Runtime.load(store, run_id= report.fork_run_id) then trace.failures() / diff().

warnings (v1.7.1) carries any resource net that DEGRADED on this platform — most notably the memory cap (RLIMIT_AS) on macOS, which the Darwin kernel refuses. A degraded net is announced, never silently skipped: the same strings are also folded into detail, and the parent logs each one.