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
3 changes: 3 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
- name: Run headless tests
run: python -m pytest

- name: Run artifact security qualification
run: python tools/execution_spikes/artifact_security_review.py --json --strict

- name: Compile application modules
run: python -m compileall -q main.py backend

Expand Down
62 changes: 53 additions & 9 deletions backend/cortex_backend/execution/artifact_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from dataclasses import dataclass
from hashlib import sha256
import json
import math
import os
from pathlib import Path
import re
Expand All @@ -38,6 +39,17 @@
b"7z\xbc\xaf\x27\x1c",
b"\x1f\x8b",
)
_ACTIVE_TEXT_MARKERS = (
b"<!doctype html",
b"<html",
b"<script",
b"<svg",
b"javascript:",
b"[internetshortcut]",
b"@echo off",
b"powershell ",
b"cmd.exe ",
)


class ArtifactBoundaryError(ValueError):
Expand Down Expand Up @@ -199,13 +211,44 @@ def _reject_json_constant(_value: str) -> Any:
raise _NonFiniteJSON("non-finite JSON number")


def _parse_finite_json_float(value: str) -> float:
"""Reject exponent overflow even when JSON does not spell ``Infinity``."""

parsed = float(value)
if not math.isfinite(parsed):
raise _NonFiniteJSON("non-finite JSON number")
return parsed


def _active_text_prefixes(prefix: bytes) -> tuple[bytes, ...]:
"""Return normalized UTF-8/UTF-16/UTF-32 prefixes for active-content checks."""

candidates = [prefix.lstrip(b"\xef\xbb\xbf \t\r\n").lower()]
for encoding in ("utf-32", "utf-16"):
try:
decoded = prefix.decode(encoding)
except UnicodeError:
continue
candidates.append(decoded.lstrip("\ufeff \t\r\n").lower().encode("utf-8"))
return tuple(candidates)


def _contains_active_text(prefixes: tuple[bytes, ...]) -> bool:
return any(
any(prefix.startswith(marker) for marker in _ACTIVE_TEXT_MARKERS)
or b"<svg" in prefix[:1024]
for prefix in prefixes
)


def sniff_artifact_mime(content: bytes) -> str:
"""Return a conservative MIME type or reject active/archive content."""

if not isinstance(content, bytes):
raise ArtifactBoundaryError("invalid_artifact")
prefix = content[:4096]
lower = prefix.lstrip(b"\xef\xbb\xbf \t\r\n").lower()
prefixes = _active_text_prefixes(prefix)
lower = prefixes[0]
if content.startswith(b"\x89PNG\r\n\x1a\n"):
return "image/png"
if content.startswith(b"\xff\xd8\xff"):
Expand All @@ -220,19 +263,18 @@ def sniff_artifact_mime(content: bytes) -> str:
raise ArtifactBoundaryError("invalid_artifact")
if any(content.startswith(magic) for magic in _ARCHIVE_MAGICS) or content[257:262] == b"ustar":
raise ArtifactBoundaryError("invalid_artifact")
if (
lower.startswith((b"<!doctype html", b"<html", b"<script", b"<svg", b"javascript:"))
or b"<svg" in lower[:1024]
):
raise ArtifactBoundaryError("invalid_artifact")
if lower.startswith((b"@echo off", b"powershell ", b"cmd.exe ")):
if _contains_active_text(prefixes):
raise ArtifactBoundaryError("invalid_artifact")
if _is_printable_text(content):
try:
parsed = json.loads(content.decode("utf-8"), parse_constant=_reject_json_constant)
parsed = json.loads(
content.decode("utf-8"),
parse_constant=_reject_json_constant,
parse_float=_parse_finite_json_float,
)
except _NonFiniteJSON:
raise ArtifactBoundaryError("invalid_artifact") from None
except (UnicodeDecodeError, json.JSONDecodeError):
except (UnicodeDecodeError, json.JSONDecodeError, ValueError, RecursionError):
return "text/plain"
if isinstance(parsed, (dict, list, str, int, float, bool)) or parsed is None:
return "application/json"
Expand Down Expand Up @@ -294,6 +336,8 @@ def copy_in(
) -> ExecutionArtifact:
"""Copy one explicitly granted source into the owner-scoped artifact store."""

if not isinstance(grant, ArtifactSourceGrant):
raise ArtifactBoundaryError("artifact_grant_invalid")
self._validate_retention(retention_seconds)
self._authorize_job(grant.job_id, grant.owner)
source = grant.source_path
Expand Down
14 changes: 12 additions & 2 deletions backend/cortex_backend/execution/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -895,13 +895,19 @@ def delete_artifact(self, artifact_id: str) -> None:
def read_artifact(self, artifact_id: str) -> bytes:
with self.connect() as connection:
row = connection.execute(
"SELECT path, sha256, expires_at FROM execution_artifacts WHERE artifact_id = ?",
"SELECT path, size, sha256, expires_at FROM execution_artifacts WHERE artifact_id = ?",
(artifact_id,),
).fetchone()
if row is None:
raise ExecutionRepositoryError("Artifact does not exist.")
if datetime.fromisoformat(row["expires_at"]) <= datetime.now(timezone.utc):
raise ExecutionRepositoryError("Artifact retention has expired.")
try:
expected_size = int(row["size"])
except (TypeError, ValueError):
raise ExecutionRepositoryError("Artifact integrity check failed.") from None
if not 0 <= expected_size <= self.max_artifact_bytes:
raise ExecutionRepositoryError("Artifact integrity check failed.")
original_path = Path(row["path"])
if _is_reparse_point(original_path):
raise ExecutionRepositoryError("Artifact path is unavailable.")
Expand All @@ -915,12 +921,16 @@ def read_artifact(self, artifact_id: str) -> bytes:
or _is_reparse_point(path)
or not stat.S_ISREG(info.st_mode)
or getattr(info, "st_nlink", 1) != 1
or int(info.st_size) != expected_size
):
raise ExecutionRepositoryError("Artifact path is unavailable.")
try:
content = path.read_bytes()
with path.open("rb") as stream:
content = stream.read(self.max_artifact_bytes + 1)
except OSError:
raise ExecutionRepositoryError("Artifact path is unavailable.") from None
if len(content) != expected_size or len(content) > self.max_artifact_bytes:
raise ExecutionRepositoryError("Artifact integrity check failed.")
if hashlib.sha256(content).hexdigest() != row["sha256"]:
raise ExecutionRepositoryError("Artifact integrity check failed.")
return content
Expand Down
11 changes: 7 additions & 4 deletions docs/adr/0001-capability-tiered-agentic-execution-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -953,9 +953,11 @@ pass without executing code.
complete one-folder dependency closure with inert resource entries and refuses to
produce metadata without an external key; signed-worker end-to-end broker
packaged-worker qualification now passes the fixed transform, hostile decoder,
and in-flight cancellation corpus. Collect opt-in aggregate reliability metrics,
never content, only after the remaining release gates close and the provider is
sandbox-qualified.
and in-flight cancellation corpus. The deterministic artifact-security corpus now
also qualifies owner/path binding, active-content rejection, exact claims,
quarantine, rollback, and repository integrity without user/model input. Collect
opt-in aggregate reliability metrics, never content, only after the remaining
release gates close and the provider is sandbox-qualified.

**Implementation gate:** parser, artifact-boundary, qualification-provider,
worker-provenance, sandbox-qualification, and native-launcher-policy regression
Expand All @@ -969,7 +971,8 @@ lifecycle health must pass before any provider is enabled.
The 2026-07-28 packaged qualification evidence closes the hostile decoder corpus,
signed provenance, broker identity, and in-flight cancellation portions of this
gate. The 2026-07-28 parser qualification also closes the deterministic parser-
fuzzing portion. Artifact security review, resource/watchdog accounting, external
fuzzing portion. The 2026-07-28 artifact-security qualification closes the
deterministic copy-in/publication review. Resource/watchdog accounting, external
review, and production lifecycle health remain open.

### Phase 3 — `scratch.auto.v1` arbitrary WebAssembly code
Expand Down
25 changes: 15 additions & 10 deletions docs/adr/0001-phase2-artifact-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ renamed, replaced, or deleted.

MIME is derived from bytes, never from an extension or model declaration. PNG, JPEG,
WebP, finite JSON, and ordinary UTF-8 text are recognized. Portable executables,
ELF/OLE files, shortcuts, shebang scripts, archives, active HTML/SVG/JavaScript,
and common shell/PowerShell launchers are rejected. Unknown non-active bytes may be
ELF/OLE files, shortcuts, shebang scripts, archives, active HTML/SVG/JavaScript
(including UTF-16/UTF-32 markup), and common shell/PowerShell launchers are rejected.
Exponent-overflow JSON and non-finite JSON constants are rejected, while oversized
numeric literals cannot escape as parser exceptions. Unknown non-active bytes may be
stored as `application/octet-stream`; this is metadata safety, not permission to decode
or execute them. Production image decoding remains behind the separate sandboxed
[recipe provider qualification gate](0001-phase2-recipe-provider.md).
Expand Down Expand Up @@ -82,7 +84,7 @@ of a path outside the configured artifact root.
### 5. Stable failure categories

The boundary exposes only categories such as `artifact_owner_mismatch`,
`artifact_path_invalid`, `artifact_reparse_point`, `artifact_hardlink_rejected`,
`artifact_grant_invalid`, `artifact_path_invalid`, `artifact_reparse_point`, `artifact_hardlink_rejected`,
`artifact_source_changed`, `artifact_too_large`, `invalid_artifact`,
`artifact_unclaimed_output`, `artifact_mime_mismatch`, `artifact_output_limit`,
`artifact_publish_failed`, and `artifact_cleanup_pending`. Raw paths and operating
Expand All @@ -91,15 +93,18 @@ system details do not cross the application boundary.
## Verification and evidence

`tests/test_phase2_artifact_boundary.py` covers owner binding, source preservation,
ADS/link/reparse rejection, source mutation, exact claims, quarantine, active/archive
rejection, executable rejection, non-finite JSON rejection, MIME mismatch, aggregate
limits, and all-or-nothing publication rollback. The repository tests continue to
cover hash/size/retention behavior. The complete matrix is recorded in the
[Phase 2 evidence log](0001-phase2-evidence.md).
ADS/link/reparse rejection, source mutation, sparse-file rejection, exact claims,
quarantine, active/archive rejection, executable rejection, finite-number checks,
MIME mismatch, aggregate limits, all-or-nothing publication rollback, and stored-size
integrity. The deterministic `tools/execution_spikes/artifact_security_review.py`
probe repeats the fixed 12-case corpus in a disposable temporary root and reports
`blocked` when a required host link primitive is unavailable. The complete matrix is
recorded in the [Phase 2 evidence log](0001-phase2-evidence.md).

## Explicit non-goals

This ADR does not authorize image decoding, thumbnail generation, archive extraction,
provider loading, code execution, model tool exposure, network access, or automatic
execution. The next gate is fixed-function provider qualification inside the already
planned OS sandbox, followed by a lifecycle health check and external security review.
execution. The artifact security review is complete; provider enablement remains
blocked on resource/watchdog accounting, external security review, and lifecycle
health gates in the parent ADR.
30 changes: 25 additions & 5 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, deterministic parser-fuzz qualification, signed-manifest verification, native broker transport, authenticated worker loop, signed bundle installation, trusted artifact boundary, signed worker launch/broker qualification, and packaged transform/hostile-decoder/cancellation qualification complete; resource/watchdog accounting, artifact security review, external review, and lifecycle release gates remain open
- **Status:** Typed contract, deterministic parser-fuzz qualification, signed-manifest verification, native broker transport, authenticated worker loop, signed bundle installation, trusted artifact boundary, artifact security review, signed worker launch/broker qualification, and packaged transform/hostile-decoder/cancellation qualification complete; resource/watchdog accounting, 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,10 +24,11 @@
| 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`. |
| Deterministic artifact security review | **Complete (qualification-only)** | `artifact_security_review.py --json --strict` passed the fixed 12-case disposable corpus (`artifact-boundary-review.v1`, digest `a748cc9f0a514c8d`): owner/path binding, link/hardlink rejection, active/non-finite content, exact claims/quarantine, rollback, and repository size integrity. Missing link primitives remain blocked. |
| 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/hostile/cancellation)** | `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 transform, truncated-PNG decoder rejection, active-SVG decoder rejection, and in-flight cancellation corpus. 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`, `input_chunk`, `input_complete`, `collect_output`, hostile decoder rejection, and in-flight cancellation acknowledgement are live-qualified inside the packaged worker. The remaining gate is artifact security review, resource/watchdog accounting, external review, and production 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`, `collect_output`, hostile decoder rejection, in-flight cancellation acknowledgement, and artifact security review are qualified. The remaining gate is resource/watchdog accounting, external review, and production lifecycle wiring. |

## Security invariants

Expand Down Expand Up @@ -103,6 +104,10 @@
28. Typed parser fuzzing uses a fixed seed, bounded iteration count, bounded payload
depth, and stable redacted error categories; an unexpected exception or budget
violation is a blocked qualification result.
29. Artifact security qualification uses fixed bytes and a disposable temporary root;
it checks owner/path binding, active-content and numeric safety, link rejection,
exact claims, quarantine, rollback, and stored-size integrity. The probe accepts
no user/model input and reports an unavailable required link primitive as blocked.

## Re-run target

Expand All @@ -122,6 +127,7 @@ python tools/execution_spikes/native_launcher_qualification.py
python tools/execution_spikes/recipe_sandbox_qualification.py --json --strict
python tools/execution_spikes/recipe_worker_e2e_qualification.py --json --strict
python tools/execution_spikes/recipe_parser_fuzz.py --json --strict
python tools/execution_spikes/artifact_security_review.py --json --strict
python -m compileall -q backend\cortex_backend\execution tests
python -m pytest -q
python tools/generate_contracts.py
Expand Down Expand Up @@ -170,7 +176,8 @@ residue. The optional `--case cancellation` path reproduces the cancellation
gate in isolation. These results close the packaged hostile-decoder and
cancellation qualification evidence, but do not enable the provider or close
resource/watchdog accounting, artifact security review, external review, or
production lifecycle health gates.
production lifecycle health gates; those were the remaining gates at the time of
this run.

**Parser fuzz qualification result (2026-07-28):** The deterministic
`recipe_parser_fuzz.py --iterations 2000 --seed 20260728 --json --strict` run
Expand All @@ -180,5 +187,18 @@ typed parsers, including unknown fields, malformed operations, non-mapping
values, oversized payloads, control/unicode text, and invalid optional values.
The corpus exposed and the parser fixed an uncaught `None` tolerance validator
failure for `check.v1`/`is_close`; the regression suite now locks that behavior
to `invalid_check`. Resource/watchdog accounting, artifact security review,
external review, and production lifecycle health remain open.
to `invalid_check`. At the time of this run, resource/watchdog accounting,
artifact security review, external review, and production lifecycle health
remained open.

**Artifact security review result (2026-07-28):** The deterministic
`artifact_security_review.py --json --strict` probe passed all 12 cases in the
fixed `artifact-boundary-review.v1` corpus (`a748cc9f0a514c8d`). The disposable
review covered source preservation and owner/ADS binding, hardlink/symlink
rejection, active UTF-8/UTF-16 and non-finite numeric content, exact output claims
and quarantine, all-or-nothing rollback, and repository stored-size integrity.
The review also fixed two fail-closed gaps: exponent-overflow JSON is rejected as
non-finite, and oversized JSON integers no longer leak a `ValueError`; UTF-16
active markup is rejected instead of being classified as inert binary. The focused
artifact suite now passes 24 boundary tests plus the reproducibility test. Resource/
watchdog accounting, external review, and production lifecycle health remain open.
Loading
Loading