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
26 changes: 26 additions & 0 deletions backend/cortex_backend/execution/native_broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import secrets
import struct
import sys
from time import sleep
from typing import Any, Callable, Literal

from cryptography.hazmat.primitives import hashes, serialization
Expand Down Expand Up @@ -249,6 +250,15 @@ def _load_win32() -> _Win32:
ctypes.c_void_p,
]
kernel32.ReadFile.restype = wintypes.BOOL
kernel32.PeekNamedPipe.argtypes = [
wintypes.HANDLE,
ctypes.c_void_p,
wintypes.DWORD,
ctypes.POINTER(wintypes.DWORD),
ctypes.POINTER(wintypes.DWORD),
ctypes.POINTER(wintypes.DWORD),
]
kernel32.PeekNamedPipe.restype = wintypes.BOOL
kernel32.WriteFile.argtypes = [
wintypes.HANDLE,
ctypes.c_void_p,
Expand Down Expand Up @@ -459,6 +469,22 @@ def __init__(self, win: _Win32, handle: Any) -> None:
def read(self, size: int) -> bytes:
if size <= 0 or size > 256 * 1024:
raise NativeBrokerError("native_pipe_read_invalid")
# ReadFile is synchronous. Polling availability first keeps a broker
# reader thread cancellation-live while a packaged provider transforms.
available = wintypes.DWORD()
while True:
if not self._win.kernel32.PeekNamedPipe(
self._handle,
None,
0,
None,
ctypes.byref(available),
None,
):
raise _pipe_error(ctypes.get_last_error())
if available.value:
break
sleep(0.005)
buffer = ctypes.create_string_buffer(size)
count = wintypes.DWORD()
if not self._win.kernel32.ReadFile(
Expand Down
25 changes: 15 additions & 10 deletions docs/adr/0001-phase2-evidence.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# ADR-0001 Phase 2 evidence log

- **Phase:** 2 — signed image recipes and calculator/check primitives
- **Status:** Typed contract, signed-manifest verification, native broker transport, authenticated worker loop, signed bundle installation, trusted artifact boundary, signed worker launch/broker qualification, and qualification-only provider core complete; the packaged provider transform and OS sandbox release gates remain open
- **Status:** Typed contract, signed-manifest verification, native broker transport, authenticated worker loop, signed bundle installation, trusted artifact boundary, signed worker launch/broker qualification, and packaged provider transform qualification complete; watchdog/hostile-decoder evidence, external review, and lifecycle release gates remain open
- **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)
Expand All @@ -24,9 +24,9 @@
| 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 (signed launch/broker; provider transform blocked)** | `recipe_worker_e2e_qualification.py` signs a disposable package with an in-memory key, installs/verifies one immutable generation, binds the live AppContainer identity to the broker, and exercises the packaged protocol. The fixed corpus passes through `input_chunk`; the provider `input_complete` transform times out and the harness fails closed. |
| Windows recipe sandbox qualification harness | **Complete (signed launch/broker/provider transform)** | `recipe_worker_e2e_qualification.py` signs a disposable package with an in-memory key, installs/verifies one immutable generation, binds the live AppContainer identity to the broker, and exercises the fixed PNG grayscale corpus through `collect_output`. The native broker uses bounded availability polling before reads so the cancellation reader remains live while the packaged provider transforms. |
| Suspended native launcher/resource policy | **Complete (factory + binder + ACL cleanup + qualification evidence)** | `NativeWin32ProcessFactory` grants only inherited read/execute access to the fresh AppContainer SID on the verified package root, applies and verifies Job Object policy before resume, and removes the per-launch ACE during cleanup. `NativeBrokerIdentityBinder` pins the live server to the worker PID/AppContainer SID and launcher cleanup closes it on failure. |
| OS sandbox provider and provider-produced image outputs | **Blocked / release gate** | Signed installation, provenance, AppContainer/job identity, broker handshake, `prepare`, and `input_chunk` are live-qualified. The actual provider transform still needs to complete inside the packaged worker, followed by watchdog/hostile-decoder evidence, external review, and lifecycle wiring. |
| OS sandbox provider and provider-produced image outputs | **Blocked / release gate** | Signed installation, provenance, AppContainer/job identity, broker handshake, `prepare`, `input_chunk`, `input_complete`, and `collect_output` are live-qualified inside the packaged worker. The remaining gate is watchdog/cancellation and hostile-decoder evidence, external review, and production lifecycle wiring. |

## Security invariants

Expand Down Expand Up @@ -93,6 +93,9 @@
25. The native launcher grants only a per-profile inherited read/execute ACE on the
verified package root, removes that ACE during worker cleanup, and never grants
package write/delete access; a qualification timeout is always a blocked result.
26. Native broker reads poll bounded pipe availability before synchronous reads so a
cancellation reader cannot starve the provider transform; pipe errors still fail
closed and do not bypass framing, authentication, or sequence checks.

## Re-run target

Expand Down Expand Up @@ -137,11 +140,13 @@ verified its complete 822-file closure (one `image_transform` role plus 821 iner
`resource` entries); no key or signed artifact was retained.

**Signed worker qualification result (2026-07-24):** The disposable
`recipe_worker_e2e_qualification.py --json --timeout-seconds 5` run passed
`recipe_worker_e2e_qualification.py --json --timeout-seconds 5 --strict` run passed
ephemeral signing/installation, active provenance verification, AppContainer/job
policy and identity binding, authenticated broker handshake, `prepare`, and
`input_chunk`. It blocked at `input_complete` with `worker_response_timeout` while
the packaged provider transform remained inside the worker. Bounded cleanup closed
the broker, binder, worker, and profile; no `recipe_worker.exe` process remained.
This is evidence for the launch/protocol boundary only and does not close the
provider or production lifecycle release gate.
policy and identity binding, authenticated broker handshake, `prepare`,
`input_chunk`, `input_complete`, and `collect_output`. The transport fix polls
`PeekNamedPipe` with a 5 ms bounded wait before synchronous reads, preventing the
worker's cancellation reader from starving the provider transform. Bounded cleanup
closed the broker, binder, worker, and profile; no `recipe_worker.exe` process
remained. This closes the packaged provider-transform qualification gate but does
not close the remaining watchdog/hostile-decoder, external-review, or lifecycle
release gates.
40 changes: 40 additions & 0 deletions tests/test_phase2_native_broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from dataclasses import dataclass
import ctypes
import os
import queue
import secrets
Expand All @@ -28,6 +29,7 @@
_HANDSHAKE_HEADER,
_HANDSHAKE_HELLO,
_HANDSHAKE_SERVER,
_PipeIO,
_PROCESS_QUERY_LIMITED_INFORMATION,
_close_handle,
_client_handshake,
Expand Down Expand Up @@ -232,6 +234,44 @@ def write(self, _payload: bytes) -> None:
assert closed == [True]


def test_pipe_io_waits_for_available_bytes_before_blocking_read():
class FakeKernel32:
def __init__(self) -> None:
self.peek_calls = 0

def PeekNamedPipe(
self,
_handle: object,
_buffer: object,
_buffer_size: int,
_bytes_read: object,
available: object,
_bytes_left: object,
) -> bool:
self.peek_calls += 1
available._obj.value = 0 if self.peek_calls == 1 else 3
return True

def ReadFile(
self,
_handle: object,
buffer: object,
_size: int,
count: object,
_overlapped: object,
) -> bool:
payload = b"abc"
ctypes.memmove(buffer, payload, len(payload))
count._obj.value = len(payload)
return True

kernel32 = FakeKernel32()
pipe = _PipeIO(type("Win", (), {"kernel32": kernel32})(), object())

assert pipe.read(16) == b"abc"
assert kernel32.peek_calls == 2


def test_native_configs_require_local_pipe_and_expected_process_binding():
with pytest.raises(ValueError):
NativeBrokerServerConfig(r"\\.\pipe\..\unsafe", _policy())
Expand Down
11 changes: 7 additions & 4 deletions tools/execution_spikes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,14 @@ It accepts no user files, model text, commands, or production trust material.
verification can take several minutes on Windows; the protocol timeout applies
after launch and is fail-closed.

The current evidence result is intentionally blocked at the provider transform:
The current evidence result passes the packaged provider transform:
signed installation, provenance, AppContainer/job identity binding, broker
handshake, `prepare`, and `input_chunk` pass, while `input_complete` times out
inside the packaged provider. The harness reports `worker_response_timeout`,
cleans up boundedly, and never reports a green result for this partial run.
handshake, `prepare`, `input_chunk`, `input_complete`, and `collect_output` all
complete through the authenticated broker. The native read path polls
`PeekNamedPipe` with a bounded 5 ms wait before `ReadFile`, keeping the worker's
cancellation reader live without starving the provider transform. The harness
still does not close the remaining watchdog/hostile-decoder, external-review, or
production-lifecycle release gates.

## What the probes prove

Expand Down
Loading