# 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.

CONTRACT v1.8 #9–#12 also exposes this default behind `TrialExecutor`. Executors accept a versioned JSON `TrialSpecification` and return a provider-neutral `TrialResult`; the legacy `run_forked_trial` function still returns `TrialReport` unchanged. This is an adapter boundary, not a new security claim.

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

Run a forked trial through the default local TrialExecutor.

This compatibility surface preserves the v1.5 `TrialReport` return. New orchestration code may construct :class:`TrialSpecification` and call a :class:`TrialExecutor` directly for a provider-neutral result.

## Executor interface

Bases: `Protocol`

Execute a serialized trial behind declared isolation guarantees.

## `isolation_guarantees`

Return the adapter's honest host/process isolation claims.

## `execute(serialized_specification)`

Execute one validated serialized specification.

Versioned, JSON-serializable intent for one pinned trial.

## `to_json()`

Serialize this specification as canonical versioned JSON.

## `from_json(serialized)`

Parse and validate a versioned serialized specification.

Provider-neutral structured result returned by a TrialExecutor.

## `from_report(report, *, specification, isolation, artifacts=())`

Lift a legacy local report into the provider-neutral result.

## `to_report()`

Return the lossless legacy `run_forked_trial` report shape.

Store-authoritative work counts plus the requested limit set.

Reference to one artifact produced by an executor.

Location of the event/log authority for one trial.

Structured terminal failure information.

An executor's explicit process and host-isolation claims.

Default executor using the existing fresh-interpreter child path.

## `isolation_guarantees`

Declare crash isolation without claiming a security sandbox.

## `execute(serialized_specification)`

Run one specification through the behavior-preserving local path.

Deterministic executor double that records specs and returns fixtures.

## `isolation_guarantees`

Declare that the double performs no external execution.

## `execute(serialized_specification)`

Record a validated specification and return the next fixture.

Reusable adapter tests live at `activegraph.sandbox.conformance.TrialExecutorConformance`.

## 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.
