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
12 changes: 12 additions & 0 deletions backend/cortex_backend/execution/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@
NativeBrokerServerConfig,
build_pipe_sddl,
)
from .worker_provenance import (
EXPECTED_WORKER_PATH,
VerifiedRecipeWorker,
WorkerProvenanceError,
verify_active_worker,
verify_installed_worker,
)
from .recipes import (
CalculatorPlan,
CheckPlan,
Expand Down Expand Up @@ -155,6 +162,11 @@
"PeerIdentity",
"authorize_message",
"build_pipe_sddl",
"EXPECTED_WORKER_PATH",
"VerifiedRecipeWorker",
"WorkerProvenanceError",
"verify_active_worker",
"verify_installed_worker",
"decode_frame",
"decode_message",
"encode_frame",
Expand Down
202 changes: 202 additions & 0 deletions backend/cortex_backend/execution/worker_provenance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
"""Storage-only binding of an installed signed bundle to the image worker role.

This module is intentionally not a launcher. It accepts only an
``InstalledBundle`` returned by the signed installer, revalidates the immutable
generation, and proves that exactly one declared ``image_transform`` entry is the
fixed ``recipe_worker.exe`` file. It never imports, loads, decodes, or executes
the worker. Process isolation, native broker identity, resource limits, and
lifecycle enablement remain separate release gates.
"""

from __future__ import annotations

from dataclasses import dataclass
from hashlib import sha256
import json
from pathlib import Path
import re
from typing import Final

from .bundle_installer import (
BundleInstallError,
InstalledBundle,
SignedBundleInstaller,
)
from .manifest import (
MAX_BUNDLE_ENTRY_BYTES,
MAX_MANIFEST_BYTES,
ManifestVerificationError,
parse_signed_manifest,
verify_bundle_files,
)


EXPECTED_WORKER_PATH: Final[str] = "recipe_worker.exe"
_SAFE_CODE = re.compile(r"^[a-z][a-z0-9_]{0,63}$")


class WorkerProvenanceError(ValueError):
"""Stable worker provenance failure without paths or filesystem details."""

def __init__(self, code: str) -> None:
if _SAFE_CODE.fullmatch(code) is None:
raise ValueError("invalid worker provenance code")
self.code = code
super().__init__("The installed recipe worker failed provenance verification safely.")


@dataclass(frozen=True, slots=True)
class VerifiedRecipeWorker:
"""Verified metadata for a worker file; no executable handle is retained."""

bundle_root: Path
bundle_digest: str
key_id: str
worker_path: str
worker_sha256: str
worker_size: int
recipe_version: str


def _is_reparse_point(path: Path) -> bool:
if path.is_symlink():
return True
is_junction = getattr(path, "is_junction", None)
return bool(is_junction is not None and is_junction())


def _read_manifest(root: Path):
manifest_path = root / "manifest.json"
if _is_reparse_point(manifest_path):
raise WorkerProvenanceError("worker_manifest_reparse")
try:
with manifest_path.open("rb") as stream:
payload = stream.read(MAX_MANIFEST_BYTES + 1)
except OSError:
raise WorkerProvenanceError("worker_manifest_unavailable") from None
if len(payload) > MAX_MANIFEST_BYTES:
raise WorkerProvenanceError("worker_manifest_too_large")
try:
raw = json.loads(payload.decode("ascii"))
return parse_signed_manifest(raw)
except (UnicodeDecodeError, json.JSONDecodeError, ManifestVerificationError, ValueError):
raise WorkerProvenanceError("worker_manifest_invalid") from None


def _file_identity(path: Path) -> tuple[int, int, int, int]:
try:
stat = path.stat()
except OSError:
raise WorkerProvenanceError("worker_entrypoint_unavailable") from None
if _is_reparse_point(path):
raise WorkerProvenanceError("worker_entrypoint_reparse")
if not path.is_file() or getattr(stat, "st_nlink", 1) != 1:
raise WorkerProvenanceError("worker_entrypoint_invalid")
return (
int(stat.st_size),
int(stat.st_mtime_ns),
int(stat.st_ctime_ns),
int(getattr(stat, "st_ino", 0)),
)


def _read_worker(path: Path, expected_size: int, expected_digest: str) -> None:
before = _file_identity(path)
if before[0] != expected_size or expected_size > MAX_BUNDLE_ENTRY_BYTES:
raise WorkerProvenanceError("worker_entrypoint_size_mismatch")
digest = sha256()
total = 0
try:
with path.open("rb") as stream:
while True:
chunk = stream.read(min(1024 * 1024, MAX_BUNDLE_ENTRY_BYTES + 1 - total))
if not chunk:
break
total += len(chunk)
if total > expected_size:
raise WorkerProvenanceError("worker_entrypoint_size_mismatch")
digest.update(chunk)
except WorkerProvenanceError:
raise
except OSError:
raise WorkerProvenanceError("worker_entrypoint_unavailable") from None
after = _file_identity(path)
if before != after or total != expected_size:
raise WorkerProvenanceError("worker_entrypoint_changed")
if digest.hexdigest() != expected_digest:
raise WorkerProvenanceError("worker_entrypoint_hash_mismatch")


def verify_installed_worker(installed: InstalledBundle) -> VerifiedRecipeWorker:
"""Verify the exact image-worker role in an installer-returned generation."""

if not isinstance(installed, InstalledBundle):
raise TypeError("installed must be an InstalledBundle")
root = installed.bundle_root
if _is_reparse_point(root):
raise WorkerProvenanceError("worker_bundle_reparse")
try:
root = root.resolve(strict=True)
except (OSError, RuntimeError):
raise WorkerProvenanceError("worker_bundle_unavailable") from None
if not root.is_dir():
raise WorkerProvenanceError("worker_bundle_unavailable")

manifest = _read_manifest(root)
if manifest.manifest_digest() != installed.state.digest:
raise WorkerProvenanceError("worker_manifest_mismatch")
try:
verify_bundle_files(manifest, root)
except ManifestVerificationError:
raise WorkerProvenanceError("worker_bundle_integrity_failed") from None

worker_entries = [
entry for entry in manifest.entries if entry.entrypoint == "image_transform"
]
if len(worker_entries) == 0:
raise WorkerProvenanceError("worker_role_missing")
if len(worker_entries) != 1:
raise WorkerProvenanceError("worker_role_ambiguous")
entry = worker_entries[0]
if entry.bundle_path != EXPECTED_WORKER_PATH:
raise WorkerProvenanceError("worker_entrypoint_mismatch")

worker_path = root / EXPECTED_WORKER_PATH
try:
if not worker_path.resolve(strict=True).is_relative_to(root):
raise WorkerProvenanceError("worker_entrypoint_invalid")
except (OSError, RuntimeError):
raise WorkerProvenanceError("worker_entrypoint_unavailable") from None
_read_worker(worker_path, entry.size, entry.sha256)
return VerifiedRecipeWorker(
bundle_root=root,
bundle_digest=installed.state.digest,
key_id=manifest.key_id,
worker_path=EXPECTED_WORKER_PATH,
worker_sha256=entry.sha256,
worker_size=entry.size,
recipe_version=entry.version,
)


def verify_active_worker(installer: SignedBundleInstaller) -> VerifiedRecipeWorker:
"""Verify the active generation after the installer rechecks its signature."""

if not isinstance(installer, SignedBundleInstaller):
raise TypeError("installer must be a SignedBundleInstaller")
try:
installed = installer.status()
except BundleInstallError:
raise WorkerProvenanceError("worker_bundle_integrity_failed") from None
if installed is None:
raise WorkerProvenanceError("worker_bundle_unavailable")
return verify_installed_worker(installed)


__all__ = [
"EXPECTED_WORKER_PATH",
"VerifiedRecipeWorker",
"WorkerProvenanceError",
"verify_active_worker",
"verify_installed_worker",
]
17 changes: 10 additions & 7 deletions docs/adr/0001-capability-tiered-agentic-execution-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -944,13 +944,16 @@ pass without executing code.
- 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. 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.
composes AppContainer/Job Object controls and a fixed decoder corpus. Storage-only
signed worker provenance now binds exactly one image-transform role to the fixed
worker entrypoint, but native launch/resource/broker gates remain blocked; collect
opt-in aggregate reliability metrics, never content, only after the provider is
sandbox-qualified.

**Implementation gate:** parser, artifact-boundary, qualification-provider,
worker-provenance, 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
Expand Down
10 changes: 8 additions & 2 deletions docs/adr/0001-phase2-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
| Canonical plan identity | **Complete** | Validated plans expose stable canonical JSON and SHA-256 digests for future idempotency/signature binding. |
| Signed recipe manifest | **Complete (verification only)** | Ed25519 signature verification uses a pinned key-id allowlist; every declared bundle entry is path-, size-, and SHA-256-verified; monotonic updates and explicit rollback authorization are enforced. |
| Signed bundle installation/update | **Complete (storage-only)** | Digest-named immutable generations, exclusive staging, atomic activation state, chained keyring rotation, explicit rollback authorization, and previous-generation recovery are covered by installer tests. No provider is loaded. |
| Signed worker provenance binding | **Complete (storage-only)** | `verify_active_worker()` rechecks the active signed generation, binds exactly one `image_transform` role to `recipe_worker.exe`, revalidates byte identity, and rejects missing/ambiguous/mismatched/tampered/reparse entries without launching. |
| Authenticated broker contract | **Complete (transport-neutral)** | Bounded versioned frames, direction-specific HMAC keys, canonical messages, peer ACL/integrity policy, and owner-scoped authorization are covered by adversarial tests. |
| 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`. |
Expand Down Expand Up @@ -76,6 +77,9 @@
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.
22. Worker provenance is storage-only: only an installer-validated immutable
generation with one exact `image_transform`/`recipe_worker.exe` role and stable
byte identity can proceed to a future launcher; no executable is loaded here.

## Re-run target

Expand All @@ -87,6 +91,7 @@ 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_phase2_worker_provenance.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
Expand All @@ -100,9 +105,10 @@ 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, 17 recipe-provider tests, and 5 sandbox-qualification tests
artifact-boundary tests, 17 recipe-provider tests, 6 worker-provenance tests, and
5 sandbox-qualification tests
passed; the full Python suite
passed (209 tests total) with one
passed (215 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
Expand Down
7 changes: 4 additions & 3 deletions docs/adr/0001-phase2-recipe-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,10 @@ 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 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
The next gate is the [signed worker provenance binding](0001-phase2-worker-provenance.md)
followed by the disposable [Windows sandbox qualification harness](0001-phase2-sandbox-qualification.md).
The storage boundary now proves only the exact role and immutable bytes; 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.
2 changes: 1 addition & 1 deletion docs/adr/0001-phase2-sandbox-qualification.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ fallback.
| 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 |
| Signed worker provenance | Storage-only `verify_active_worker()` role binding plus fixed package precondition | **Storage gate complete; launch remains blocked** until a packaged executable and native launcher exist |
| 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 |

Expand Down
66 changes: 66 additions & 0 deletions docs/adr/0001-phase2-worker-provenance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# ADR-0001 Phase 2 signed worker provenance binding

- **Status:** Storage-only worker-role verifier complete; native launch remains blocked
- **Phase:** 2 - fixed-function image provider
- **Parent:** [Capability-tiered agentic execution harness](0001-capability-tiered-agentic-execution-harness.md)
- **Depends on:** [signed bundle installation](0001-phase2-bundle-installation.md), [signed recipe manifest](0001-phase2-signed-manifest.md), and [Windows sandbox qualification](0001-phase2-sandbox-qualification.md)
- **Scope:** Bind an installed signed generation to the fixed image-transform worker role without executing it.

## Decision

`backend/cortex_backend/execution/worker_provenance.py` adds a storage-only
verification boundary:

1. `verify_active_worker()` first asks `SignedBundleInstaller.status()` to
revalidate the active signed generation and exact immutable tree;
2. the manifest is read with a bounded ASCII read and its digest must match the
installer state;
3. every declared bundle byte is revalidated through `verify_bundle_files()`;
4. exactly one `image_transform` entry must exist and its path must be the fixed
`recipe_worker.exe` name;
5. the worker path must be an ordinary single-link file inside the generation,
and its size, timestamps, identity, and SHA-256 must remain stable across the
bounded read; and
6. only immutable metadata is returned. No executable handle, import, decode,
process creation, or provider instance is returned or performed.

The verifier is deliberately a second role-binding boundary after signature and
bundle installation. A valid signed bundle containing a different image entry,
multiple image roles, a changed worker, or a reparse/hardlink entry cannot be
treated as the worker. Stable `WorkerProvenanceError.code` values cross the
boundary; paths, bytes, OS errors, and signatures never do.

## Failure categories

The verifier uses bounded categories including `worker_bundle_unavailable`,
`worker_bundle_integrity_failed`, `worker_manifest_invalid`,
`worker_manifest_mismatch`, `worker_role_missing`, `worker_role_ambiguous`,
`worker_entrypoint_mismatch`, `worker_entrypoint_invalid`,
`worker_entrypoint_changed`, `worker_entrypoint_hash_mismatch`, and
`worker_entrypoint_reparse`/`worker_manifest_reparse` where applicable.

## Explicitly not implemented here

This boundary does not launch the worker or claim any OS isolation. The following
remain required before the provider can be enabled:

- a signed packaged executable at the fixed role path;
- a native suspended launcher with private staging and exact ACLs;
- AppContainer/LPAC policy, Job Object CPU/memory/breakaway limits, accounting,
watchdog, and full-tree cancellation;
- protected broker PID/token binding and bounded framed IPC;
- hostile decoder execution inside that worker; and
- external review and `ExecutionLifecycle` health-gated wiring.

Any missing control must leave the provider unavailable. The host process must
never decode or execute as a fallback.

## Verification

`tests/test_phase2_worker_provenance.py` proves a valid signed fixture binds to
the exact worker role and that no active generation, wrong path, ambiguous role,
or post-install tamper is accepted. Existing bundle-installer tests continue to
cover signature, byte, staging, hardlink, rollback, and recovery boundaries.

This stage is therefore **complete as a storage-only provenance substage**, not a
provider release approval.
Loading
Loading