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
424 changes: 424 additions & 0 deletions backend/cortex_backend/execution/worker_protocol.py

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions docs/adr/0001-phase2-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- **Scope:** Provider-independent contracts plus a qualification-only fixed-function core
- **Source decision:** [Capability-tiered agentic execution harness](0001-capability-tiered-agentic-execution-harness.md)
- **Contract ADR:** [Phase 2 typed recipe and primitive contract](0001-phase2-recipe-contract.md)
- **Worker ADR:** [Phase 2 fixed recipe worker protocol and package boundary](0001-phase2-worker-protocol.md)

## Stage checklist

Expand All @@ -17,13 +18,14 @@
| 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. |
| Fixed worker protocol and package closure | **Complete (qualification-only)** | `worker_protocol.py` enforces bounded prepare/chunk/complete/cancel/collect state, canonical per-chunk hashes, whole-input claims, redacted output metadata, and no-capability bodies. `packaging/recipe_worker/recipe_worker.spec` builds the fixed `recipe_worker.exe`; the entrypoint exits `78` until native broker wiring exists. |
| 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`. |
| 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. |
| 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. |
| Suspended native launcher/resource policy | **Complete (disposable control spike)** | `native_launcher_qualification.py` creates a fixed suspended AppContainer child, applies and queries active-process, kill-on-close, CPU-time, memory, and no-breakaway Job Object policy before resume; real worker enforcement and broker binding remain blocked. |
| 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. |
| OS sandbox provider and provider-produced image outputs | **Blocked / release gate** | The package/protocol boundary is qualified, but the actual provider worker still needs a signed installed generation, native broker loop, LPAC/AppContainer policy, Job Object resource limits/accounting, live broker PID/token binding, watchdog, hostile decoder execution inside the sandbox, external review, and lifecycle wiring. |

## Security invariants

Expand Down Expand Up @@ -109,13 +111,14 @@ npm.cmd run build --prefix frontend
npm.cmd test --prefix frontend -- --run
```

**Validation result (2026-07-21):** 16 Phase 2 contract tests, 9 signed-manifest tests,
**Validation result (2026-07-23):** 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, 6 worker-provenance tests, 4
artifact-boundary tests, 17 recipe-provider tests, 6 worker-provenance tests, 7
worker-protocol tests, 4
native-launcher tests, and
5 sandbox-qualification tests
passed; the full Python suite
passed (219 tests total) with one
passed (226 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
76 changes: 76 additions & 0 deletions docs/adr/0001-phase2-worker-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# ADR-0001 Phase 2 fixed recipe worker protocol and package boundary

- **Status:** Protocol and packaging qualification complete; native execution blocked
- **Phase:** 2 - fixed-function image provider
- **Parent:** [Capability-tiered agentic execution harness](0001-capability-tiered-agentic-execution-harness.md)
- **Depends on:** [Signed worker provenance](0001-phase2-worker-provenance.md), [native broker adapter](0001-phase2-native-broker.md), and [native launcher](0001-phase2-native-launcher.md)
- **Scope:** Bounded worker messages, in-order byte streaming, fixed provider session state, and reproducible Windows package closure

## Decision

The worker protocol is transport-neutral and runs only inside an authenticated
native broker session. The broker remains responsible for framing, HMAC direction
keys, peer PID/token identity, installation principal, job ownership, and lifecycle
shutdown. The worker protocol validates the operation body after those checks.

One request follows this sequence:

1. `recipe.worker.prepare.v1` declares one parsed `ImageTransformPlan`, an opaque
artifact identifier, expected byte count, expected SHA-256, and an allowlisted
image MIME type.
2. One or more `recipe.worker.input_chunk.v1` messages append canonical base64
chunks in strict offset order. Each chunk has its own SHA-256 and is at most
48 KiB decoded.
3. `recipe.worker.input_complete.v1` repeats the whole-input size and digest. The
worker decodes only after both claims match the received bytes.
4. The fixed provider runs with the worker's cancellation callback and returns
only redacted metadata in `recipe.worker.result.v1`.
5. `recipe.worker.collect.v1` reads bounded output chunks. Each output chunk has a
digest and final marker; the host artifact boundary remains responsible for
quarantine, validation, hashing, and publication.

Cancellation is terminal for the request. Unknown operations, malformed bodies,
replayed offsets, identity mismatches, size/digest mismatches, provider failures,
and output offsets fail closed with stable categories. No worker message accepts a
filesystem path, shell command, executable, network target, token, or model source.

The package entrypoint is intentionally launch-refusing (exit code `78`) until the
reviewed native broker adapter is wired in. This prevents an unsigned or directly
started package from becoming a stdio, shell, or host-process fallback. The
PyInstaller definition and Windows build script qualify dependency closure and the
fixed `recipe_worker.exe` path only; they do not sign, install, or authorize the
worker.

## Security invariants

- The worker never opens a path or creates a transport.
- Input and output bytes are bounded by the provider ceilings; chunks are bounded
below the broker frame ceiling.
- Input chunks are canonical, contiguous, independently hashed, and committed only
after whole-stream size and digest verification.
- The session state machine permits one request at a time and never resumes after
cancellation or terminal failure.
- Output metadata is private and content-addressed; publication remains outside the
worker session.
- A package build is not a signed bundle. The release pipeline must create the
exact `recipe.manifest.v1` entry for `image_transform`/`recipe_worker.exe`, sign
it with the pinned key, install it through `SignedBundleInstaller`, and re-run
`verify_active_worker()`.

## Evidence

`tests/test_phase2_worker_protocol.py` covers successful in-order streaming and
collection, malformed operations, replay/order failure, claim mismatch,
cancellation terminality, and chunk tampering. On the controlled Windows host
(2026-07-23), the PyInstaller build produced
`dist/recipe-runtime/recipe_worker.exe` (11,320,824 bytes); direct launch returned
exit code `78` as required.

## Remaining blockers

This ADR does not authorize provider execution. The next stage must replace the
launch-refusing entrypoint with the reviewed native broker loop, launch the verified
worker suspended under AppContainer/LPAC and Job Object policy, bind the live broker
session to the actual worker PID and AppContainer token, and run the hostile decoder
and cancellation corpus inside that process. Lifecycle/UI enablement remains behind
those gates and external security review.
29 changes: 29 additions & 0 deletions packaging/build_recipe_worker.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
param(
[switch]$SkipDependencyInstall
)

$ErrorActionPreference = "Stop"
$RepositoryRoot = Split-Path -Parent $PSScriptRoot
Set-Location $RepositoryRoot

if (-not $IsWindows -and $PSVersionTable.PSEdition -eq "Core") {
throw "The recipe worker must be packaged on Windows."
}

if (-not $SkipDependencyInstall) {
python -m pip install --disable-pip-version-check -r requirements.txt
python -m pip install --disable-pip-version-check "pyinstaller>=6.14,<7"
}

Remove-Item -LiteralPath (Join-Path $RepositoryRoot "build\recipe_worker") -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath (Join-Path $RepositoryRoot "dist\recipe-runtime") -Recurse -Force -ErrorAction SilentlyContinue

python -m PyInstaller --noconfirm --clean packaging/recipe_worker/recipe_worker.spec

$Executable = Join-Path $RepositoryRoot "dist\recipe-runtime\recipe_worker.exe"
if (-not (Test-Path -LiteralPath $Executable -PathType Leaf)) {
throw "The recipe worker package did not produce recipe_worker.exe."
}

Write-Host "Recipe worker contract package ready: $Executable"
Write-Host "This output is intentionally unsigned and not launch-authorized."
26 changes: 26 additions & 0 deletions packaging/recipe_worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Recipe worker package

This directory defines the fixed-function worker package boundary. The package
entrypoint deliberately exits with status `78` until the reviewed native broker
adapter is added. That refusal is a safety property: a package build must not
silently become a host-process or stdio execution fallback.

Build it on a supported Windows machine with:

```powershell
powershell -ExecutionPolicy Bypass -File packaging/build_recipe_worker.ps1
```

The output is `dist/recipe-runtime/recipe_worker.exe`. It is a dependency-closure
artifact for qualification only and is **not signed or launch-authorized** by this
repository. Release packaging must, outside source control:

1. hash every immutable package entry;
2. create the exact `recipe.manifest.v1` entry with role `image_transform` and
path `recipe_worker.exe`;
3. sign the canonical manifest with the pinned release key; and
4. install it through `SignedBundleInstaller` before `verify_active_worker()` can
be considered.

The native launcher and live broker PID/AppContainer-token binding remain required
before this package can perform any transform.
24 changes: 24 additions & 0 deletions packaging/recipe_worker/recipe_worker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Fixed recipe worker package entrypoint.

This entrypoint intentionally refuses to run until the native broker adapter is
provided by the reviewed launcher stage. It exists so packaging, dependency
closure, and signed-bundle provenance can be qualified without creating an
unsafe stdio, shell, or host-process fallback.
"""

from __future__ import annotations

import sys


def main(argv: list[str] | None = None) -> int:
"""Refuse every launch until the native broker contract is wired in."""

del argv
# A non-zero deterministic exit is important: a packaged but unbound worker
# must never look healthy to a caller that accidentally starts it directly.
return 78


if __name__ == "__main__": # pragma: no cover - exercised by the packaged exe.
raise SystemExit(main(sys.argv[1:]))
51 changes: 51 additions & 0 deletions packaging/recipe_worker/recipe_worker.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""PyInstaller one-folder definition for the fixed recipe worker contract."""

from pathlib import Path


ROOT = Path(SPECPATH).resolve().parents[1]

a = Analysis(
[str(ROOT / "packaging" / "recipe_worker" / "recipe_worker.py")],
pathex=[str(ROOT), str(ROOT / "backend")],
binaries=[],
datas=[],
hiddenimports=[
"cortex_backend.execution.worker_protocol",
"cortex_backend.execution.recipe_provider",
"PIL",
"PIL.Image",
"PIL.ImageEnhance",
"PIL.ImageFile",
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)

pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
[],
name="recipe_worker",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
console=False,
exclude_binaries=True,
)

coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=False,
name="recipe-runtime",
)
Loading
Loading