Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions docs/adr/0001-capability-tiered-agentic-execution-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -943,13 +943,18 @@ pass without executing code.
provider-independent until the sandbox qualification gate passes.
- The owner-bound copy-in, exact-claim output validation, quarantine, hashing, and
atomic publication boundary is implemented. The fixed-function provider core is
qualification-only; collect opt-in aggregate reliability metrics, never content,
after the provider is sandbox-qualified.

**Implementation gate:** parser, artifact-boundary, and qualification-provider adversarial
suites pass; no source overwrite is possible. **Release gate:** parser fuzzing, artifact
security review, hostile decoder corpus, OS sandbox qualification, and fixed-function
provider lifecycle health must pass before any provider is enabled.
qualification-only. The disposable Windows sandbox qualification harness now
composes AppContainer/Job Object controls and a fixed decoder corpus, but the
signed worker provenance gate remains blocked; collect opt-in aggregate reliability
metrics, never content, only after the provider is sandbox-qualified.

**Implementation gate:** parser, artifact-boundary, qualification-provider, and
sandbox-qualification regression suites pass; no source overwrite is possible. The
qualification harness must remain fail-closed when the signed worker is absent.
**Release gate:** parser fuzzing, artifact security review, hostile decoder corpus
executed inside the worker, OS sandbox qualification, signed provenance, broker
identity, resource/watchdog accounting, external review, and fixed-function provider
lifecycle health must pass before any provider is enabled.

### Phase 3 — `scratch.auto.v1` arbitrary WebAssembly code

Expand Down
17 changes: 13 additions & 4 deletions docs/adr/0001-phase2-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
| Native named-pipe adapter/DACL/peer-token binding | **Complete (transport-only)** | Protected local pipe, expected PID, OS token identity, X25519/HKDF handshake, direction keys, and close-on-error lifecycle are covered by native broker tests. |
| User-artifact copy-in, output validation, and publication | **Complete (boundary only)** | Explicit owner/turn grants, bounded stable snapshots, link/reparse/hardlink/sparse/ADS rejection, byte-derived MIME policy, exact output claims, quarantine, hash/size limits, atomic repository publication, rollback, and cleanup categories are covered by `tests/test_phase2_artifact_boundary.py`. |
| Fixed-function image provider core | **Complete (qualification-only)** | `RecipeImageProvider` validates allowlisted PNG/JPEG/WebP bytes, verifies/loads one frame with Pillow bomb/resource limits, applies only parsed steps, strips metadata, revalidates encoded output, checks cancellation, and remains disabled until external sandbox health passes. |
| OS sandbox provider and provider-produced image outputs | **Blocked / next gate** | The core has no process, AppContainer/LPAC, Job Object, broker, watchdog, or lifecycle route; hostile decoder qualification and external review remain required. |
| Windows recipe sandbox qualification harness | **Complete (qualification harness; worker gate blocked)** | `recipe_sandbox_qualification.py` composes out-of-process AppContainer isolation and Job Object cancellation with a fixed decoder corpus, then fails closed because the signed `recipe_worker.exe` bundle and trust-root launch verification are not shipped. |
| OS sandbox provider and provider-produced image outputs | **Blocked / release gate** | The actual provider worker still needs signed provenance, LPAC/AppContainer policy, Job Object resource limits/accounting, broker PID/token binding, watchdog, hostile decoder execution inside the sandbox, external review, and lifecycle wiring. |

## Security invariants

Expand Down Expand Up @@ -72,6 +73,9 @@
20. Provider startup requires an external available sandbox health result; dependency
or codec failure, cancellation, decoder failure, and output metadata/size failure
leave the provider disabled and return stable categories only.
21. The sandbox qualification harness never authorizes a provider launch from a
missing, unsigned, or merely present worker directory; it reports `blocked` and
never falls back to host-process decoding.

## Re-run target

Expand All @@ -83,6 +87,8 @@ python -m pytest tests/test_phase2_native_broker.py -q
python -m pytest tests/test_phase2_bundle_installer.py -q
python -m pytest tests/test_phase2_artifact_boundary.py -q
python -m pytest tests/test_phase2_recipe_provider.py -q
python -m pytest tests/test_recipe_sandbox_qualification.py -q
python tools/execution_spikes/recipe_sandbox_qualification.py --json --strict
python -m compileall -q backend\cortex_backend\execution tests
python -m pytest -q
python tools/generate_contracts.py
Expand All @@ -94,9 +100,12 @@ npm.cmd test --prefix frontend -- --run

**Validation result (2026-07-21):** 16 Phase 2 contract tests, 9 signed-manifest tests,
7 broker-contract tests, 9 native-broker tests, 7 bundle-installer tests, 16
artifact-boundary tests, and 17 recipe-provider tests passed; the full Python suite
passed (204 tests total) with one
artifact-boundary tests, 17 recipe-provider tests, and 5 sandbox-qualification tests
passed; the full Python suite
passed (209 tests total) with one
native-platform skip and one pre-existing `pytest-asyncio` deprecation warning.
Frontend lint, typecheck, production build, and all 39 frontend tests passed. Contract
generation, compileall, and `git diff --check` passed. No production execution
provider is enabled.
provider is enabled. The sandbox qualification command passed its AppContainer,
Job Object, cancellation, and fixed decoder checks but returned the expected
fail-closed `blocked` status because the signed worker bundle is not shipped.
6 changes: 4 additions & 2 deletions docs/adr/0001-phase2-recipe-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ allowlisted transforms, deterministic output hashes, metadata stripping, malform
active inputs, malformed JPEGs, animated WebP rejection, pixel/decoded-memory/output
limits, crop bounds, cancellation, stop behavior, and non-raiseable configuration caps.

The next gate is a separate Windows sandbox qualification: launch the provider out of
process under the signed bundle, AppContainer/LPAC and Job Object controls, restricted
The next gate is the disposable [Windows sandbox qualification harness](0001-phase2-sandbox-qualification.md),
which currently proves only the existing native isolation/cancellation controls and
fixed decoder corpus. The actual provider worker must still launch out of process
under a signed bundle, AppContainer/LPAC and Job Object controls, restricted
handles/environment, native broker identity, watchdog, and accounting; run the hostile
decoder corpus there; then wire it to `ExecutionLifecycle` only after external review.
95 changes: 95 additions & 0 deletions docs/adr/0001-phase2-sandbox-qualification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# ADR-0001 Phase 2 Windows recipe sandbox qualification

- **Status:** Qualification harness implemented; signed provider worker and release gate blocked
- **Phase:** 2 - fixed-function image provider
- **Parent:** [Capability-tiered agentic execution harness](0001-capability-tiered-agentic-execution-harness.md)
- **Depends on:** [Phase 2 recipe provider core](0001-phase2-recipe-provider.md), [signed bundle installation](0001-phase2-bundle-installation.md), [native broker adapter](0001-phase2-native-broker.md), and [trusted artifact boundary](0001-phase2-artifact-boundary.md)
- **Scope:** Disposable Windows control qualification only. No production provider or lifecycle route is enabled.

## Decision

The next gate is represented by `tools/execution_spikes/recipe_sandbox_qualification.py`.
The harness is deliberately fail-closed and has independent checks for:

1. it runs the reviewed zero-capability AppContainer isolation helper in a child
process and requires token identity, parent-file denial, loopback denial, and
bounded completion;
2. it runs the reviewed AppContainer/Job Object cancellation corpus in a child
process and requires full process-tree reaping after watchdog cancellation;
3. it exercises a fixed allowlisted/hostile decoder corpus against the
qualification-only Pillow core, while recording `sandboxed=false`; and
4. it requires the future fixed recipe worker package at the repository's fixed
packaging location;
5. it reports per-worker CPU/memory/breakaway/accounting controls as blocked until
the native launcher applies and verifies them; and
6. it reports broker PID/token binding as blocked until the launcher binds the
qualified transport to the actual worker.

The fourth check is intentionally blocked in this stage. A directory, executable,
self-reported digest, or unverified manifest cannot authorize a launch. The future
implementation must verify the packaged trust root, signed manifest, every declared
byte, worker identity, and runtime version before the native launcher is allowed to
start the worker. Until that exists, `provider_launch_authorized` is always false.

The harness accepts no command, source text, uploaded path, network target, or model
input. It invokes only fixed repository helpers and fixed bytes. It is not imported
by `backend/cortex_backend`, not a PyInstaller hidden import, and not an execution
fallback.

## Control matrix

| Control | Evidence in this stage | Release interpretation |
| --- | --- | --- |
| AppContainer token and zero-capability denials | `appcontainer_smoke.py`, child report `recipe_appcontainer_control` | Required prerequisite; does not prove LPAC policy or provider launch identity |
| Job Object kill-on-close and tree cancellation | `cancellation_corpus.py`, child report `recipe_cancellation_control` | Required prerequisite; resource limits and accounting remain open |
| Decoder hostile corpus | Fixed one-pixel PNG, truncated PNG, and active SVG against the core | Qualification-only evidence; not OS-sandbox evidence |
| Signed worker provenance | Fixed `packaging/recipe-runtime` location and exact `recipe_worker.exe` entrypoint | **Blocked** until packaged trust-root/signature/hash verification exists |
| Broker identity and framed IPC | Native broker transport tests and ADR | Must be bound to the actual worker PID/token before launch |
| Lifecycle enablement | `ExecutionLifecycle` remains disabled by default; provider is not exported | No provider can become reachable from the application |

No single green smoke result closes the gate. A missing, failed, or unverified
control produces `blocked` or `fail`, and no weaker host-process path is attempted.

## Required future worker qualification

The next implementation must add a signed, pinned worker bundle and a native
launcher that, per attempt:

1. verifies the installed immutable generation and image-worker entrypoint;
2. creates private staging and grants only the sandbox identity and required
system read/write handles;
3. starts the worker suspended under the intended LPAC/AppContainer policy;
4. applies Job Object kill-on-close, active-process, CPU-time, memory, and
breakaway restrictions and records accounting;
5. binds the protected broker pipe to the expected PID and OS token identity;
6. resumes only after all checks pass, enforces wall-clock/progress watchdogs,
and bounds every IPC frame; and
7. closes the job on completion or cancellation, validates output through the
trusted artifact boundary, and removes staging with recoverable cleanup state.

If any step cannot be applied or verified, the provider remains unavailable. The
host process must never decode the input as a fallback.

## Verification performed

On the controlled Windows host (2026-07-21):

```powershell
python tools/execution_spikes/recipe_sandbox_qualification.py --strict --json
```

The AppContainer and cancellation controls passed, and the fixed decoder corpus
passed. The overall result was intentionally `blocked` because the signed worker
package is not shipped. This is the expected result for the current stage.

Regression coverage is in `tests/test_recipe_sandbox_qualification.py`, including
missing/unsigned worker refusal, helper timeout/evidence failure, and the invariant
that a blocked worker gate never authorizes provider launch.

## Consequences

This stage provides reproducible evidence for the controls that already exist and
prevents accidental false-green qualification. It does not claim decoder isolation,
LPAC capability policy, resource enforcement, signed runtime provenance, native
worker identity, or production readiness. Those remain explicit blockers before
the provider can be wired to lifecycle or exposed to any model/UI route.
98 changes: 98 additions & 0 deletions tests/test_recipe_sandbox_qualification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Tests for the non-production recipe sandbox qualification gate."""

from __future__ import annotations

import json
import subprocess

import tools.execution_spikes.recipe_sandbox_qualification as qualification


def test_missing_signed_worker_blocks_without_authorizing_launch(tmp_path, monkeypatch):
monkeypatch.setattr(qualification, "EXPECTED_WORKER_ROOT", tmp_path / "missing")

result = qualification._probe_signed_worker_precondition()

assert result["status"] == "blocked"
assert result["details"]["signature_verified"] is False


def test_present_unsigned_worker_still_blocks(tmp_path, monkeypatch):
root = tmp_path / "recipe-runtime"
root.mkdir()
(root / "manifest.json").write_text("{}", encoding="ascii")
(root / qualification.EXPECTED_WORKER_ENTRYPOINT).write_bytes(b"not-a-signed-worker")
monkeypatch.setattr(qualification, "EXPECTED_WORKER_ROOT", root)

result = qualification._probe_signed_worker_precondition()

assert result["status"] == "blocked"
assert result["details"]["signature_verified"] is False
assert result["details"]["launch_refused"] is True


def test_fixed_helper_timeout_fails_closed(tmp_path):
helper = tmp_path / "helper.py"
helper.write_text("", encoding="ascii")

def timeout_runner(*args, **kwargs):
raise subprocess.TimeoutExpired("fixed-helper", 1)

result = qualification._run_fixed_helper(
helper,
"fixed_check",
1,
runner=timeout_runner,
)

assert result["name"] == "fixed_check"
assert result["status"] == "fail"


def test_fixed_helper_rejects_unexpected_evidence(tmp_path):
helper = tmp_path / "helper.py"
helper.write_text("", encoding="ascii")

def invalid_runner(*args, **kwargs):
return subprocess.CompletedProcess(
args=["fixed-helper"],
returncode=0,
stdout=json.dumps({"name": "wrong-check"}),
stderr="",
)

result = qualification._run_fixed_helper(
helper,
"fixed_check",
1,
runner=invalid_runner,
)

assert result["name"] == "fixed_check"
assert result["status"] == "fail"


def test_report_remains_blocked_when_worker_gate_is_blocked(monkeypatch):
monkeypatch.setattr(
qualification,
"_probe_os_controls",
lambda: [
{"name": "recipe_appcontainer_control", "status": "pass"},
{"name": "recipe_cancellation_control", "status": "pass"},
],
)
monkeypatch.setattr(
qualification,
"_probe_provider_core",
lambda: {"name": "recipe_decoder_corpus", "status": "pass"},
)
monkeypatch.setattr(
qualification,
"_probe_signed_worker_precondition",
lambda: {"name": "recipe_signed_worker_provenance", "status": "blocked"},
)

report = qualification.build_report()

assert report["qualification_status"] == "blocked"
assert report["provider_launch_authorized"] is False
22 changes: 22 additions & 0 deletions tools/execution_spikes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ required check that is blocked or fails:
python tools/execution_spikes/phase0_probe.py --json --job-smoke --ipc-smoke --appcontainer-smoke --guest-language-smoke --cancellation-smoke --wasi-smoke --strict
```

## Run the Phase 2 recipe sandbox qualification gate

The recipe harness runs only fixed repository helpers and fixed decoder bytes. It
does not accept model input and never falls back to host-process decoding:

```powershell
python tools/execution_spikes/recipe_sandbox_qualification.py --json
python tools/execution_spikes/recipe_sandbox_qualification.py --json --strict
```

The expected result at this stage is `qualification_status=blocked`: the native
AppContainer and Job Object controls may pass, but the signed `recipe_worker.exe`
bundle is not shipped yet. A blocked worker-provenance check is intentional and
must remain blocking until trust-root verification, native worker identity, and
resource/watchdog enforcement are implemented.

## What the probes prove

- `environment`: supported Windows host and interpreter metadata.
Expand All @@ -50,6 +66,9 @@ python tools/execution_spikes/phase0_probe.py --json --job-smoke --ipc-smoke --a
- `containment_cancellation_corpus`: starts a fixed AppContainer launcher that
creates a native descendant, closes the kill-on-close Job Object, and verifies
every observed process ID is reaped.
- `recipe_sandbox_qualification`: composes the native isolation/cancellation
controls with the qualification-only decoder corpus and a mandatory signed
worker provenance gate; it never authorizes a host-process fallback.
- `security_review`: records the conditional Phase 0 spike review and residual
blockers.
- `pyinstaller_package_preconditions`: all currently known one-folder package
Expand All @@ -70,3 +89,6 @@ Phase 0 gates.
4. Keep package/runtime experiments in a disposable environment and record exact
versions and hashes in the Phase 0 evidence log.
5. A blocked result is safer than a green result produced by a weaker fallback.
6. Do not add the recipe qualification harness or any worker bundle to application
imports, model tools, or PyInstaller hidden imports before its release gate is
explicitly closed.
Loading
Loading