diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..aef1393
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,71 @@
+name: Offline CI
+
+on:
+ push:
+ branches: [main, develop]
+ pull_request:
+ branches: [main, develop]
+ workflow_dispatch:
+
+jobs:
+ offline:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install package and dev tools
+ run: |
+ python -m pip install --upgrade pip
+ pip install -e ".[dev]"
+ - name: Format check (black)
+ run: black --check runner tests scripts
+ - name: Import sort check (isort)
+ run: isort --check-only runner tests scripts
+ - name: Lint (ruff, warnings as errors)
+ run: ruff check runner tests scripts
+ - name: Typecheck (mypy strict)
+ run: mypy runner
+ - name: Schema mirror digest check
+ run: python scripts/check_schema_mirrors.py
+ - name: Vendored schema instance validation
+ run: python scripts/validate_vendored_instances.py
+ - name: Unit / hermetic / adversarial / conformance
+ run: pytest tests -q --tb=short
+ - name: Optional external CLI gates (skip-only-when-absent)
+ run: python scripts/optional_external_validate.py
+ - name: Dependency audit (strict)
+ run: pip-audit --strict
+
+ morph-opt-in:
+ if: ${{ secrets.MORPH_API_KEY != '' }}
+ runs-on: ubuntu-latest
+ needs: offline
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - name: Install
+ run: |
+ python -m pip install --upgrade pip
+ pip install -e ".[dev]"
+ - name: Morph smoke (opt-in)
+ env:
+ MORPH_API_KEY: ${{ secrets.MORPH_API_KEY }}
+ run: |
+ mkdir -p replays
+ echo '{"type":"file","path":"/tmp/test.txt"}' > replays/demo-file.json
+ zip -j replays/demo-file.zip replays/demo-file.json
+ replay-runner run \
+ --snapshot morphvm-minimal \
+ --bundles "replays/demo-file.zip" \
+ --parallel 1 \
+ --timeout 300 \
+ --provider morph \
+ --out ./evidence-morph
diff --git a/.github/workflows/replay.yml b/.github/workflows/replay.yml
index 4b4b881..014a26c 100644
--- a/.github/workflows/replay.yml
+++ b/.github/workflows/replay.yml
@@ -1,148 +1,13 @@
-name: Replay Runner CI
+name: Morph smoke (legacy, disabled by default)
+
+# Retained for reference. Default CI is .github/workflows/ci.yml (offline).
+# This workflow is disabled; use ci.yml morph-opt-in job with MORPH_API_KEY.
on:
- push:
- branches: [ main, develop ]
- pull_request:
- branches: [ main, develop ]
workflow_dispatch:
- inputs:
- snapshot_id:
- description: 'Morph snapshot ID to test with'
- required: true
- default: 'morphvm-minimal'
- parallel_count:
- description: 'Number of parallel instances'
- required: false
- default: '2'
-
-env:
- MORPH_API_KEY: ${{ secrets.MORPH_API_KEY }}
jobs:
- test-replay-bundles:
- runs-on: ubuntu-latest
- strategy:
- matrix:
- python-version: [3.9, 3.10, 3.11]
- bundle: [demo-http, demo-tcp, demo-file]
- parallel: [1, 2]
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
- with:
- python-version: ${{ matrix.python-version }}
-
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -e ".[dev]"
-
- - name: Create demo bundles
- run: |
- mkdir -p replays
- # Create demo HTTP bundle
- echo '{"type": "http", "url": "https://httpbin.org/get"}' > replays/demo-http.json
- zip -j replays/demo-http.zip replays/demo-http.json
-
- # Create demo TCP bundle
- echo '{"type": "tcp", "host": "localhost", "port": 8080}' > replays/demo-tcp.json
- zip -j replays/demo-tcp.zip replays/demo-tcp.json
-
- # Create demo file bundle
- echo '{"type": "file", "path": "/tmp/test.txt"}' > replays/demo-file.json
- zip -j replays/demo-file.zip replays/demo-file.json
-
- - name: Run replay runner
- env:
- MORPH_API_KEY: ${{ secrets.MORPH_API_KEY }}
- run: |
- python -m runner.main \
- --snapshot ${{ github.event.inputs.snapshot_id || 'morphvm-minimal' }} \
- --bundles "replays/${{ matrix.bundle }}.zip" \
- --parallel ${{ matrix.parallel }} \
- --timeout 300 \
- --emit-cert \
- --out "./evidence-${{ matrix.bundle }}-${{ matrix.parallel }}"
-
- - name: Upload evidence artifacts
- uses: actions/upload-artifact@v3
- with:
- name: evidence-${{ matrix.bundle }}-${{ matrix.parallel }}
- path: |
- evidence-${{ matrix.bundle }}-${{ matrix.parallel }}/certs/
- evidence-${{ matrix.bundle }}-${{ matrix.parallel }}/logs/
- evidence-${{ matrix.bundle }}-${{ matrix.parallel }}/reports/
- retention-days: 30
-
- - name: Validate certificates
- run: |
- # Check if certificates were generated
- if [ -f "evidence-${{ matrix.bundle }}-${{ matrix.parallel }}/certs/cert_0.json" ]; then
- echo "✓ Certificate generated successfully"
- # Basic JSON validation
- python -c "import json; json.load(open('evidence-${{ matrix.bundle }}-${{ matrix.parallel }}/certs/cert_0.json'))"
- else
- echo "✗ No certificate generated"
- exit 1
- fi
-
- integration-test:
+ noop:
runs-on: ubuntu-latest
- needs: test-replay-bundles
-
steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Set up Python 3.11
- uses: actions/setup-python@v4
- with:
- python-version: 3.11
-
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -e ".[dev]"
-
- - name: Download all evidence artifacts
- uses: actions/download-artifact@v3
-
- - name: Run integration tests
- run: |
- # Test that all evidence directories contain expected files
- for dir in evidence-*; do
- echo "Checking $dir..."
- [ -d "$dir/certs" ] || exit 1
- [ -d "$dir/logs" ] || exit 1
- [ -d "$dir/reports" ] || exit 1
- [ -f "$dir/reports/index.json" ] || exit 1
- echo "✓ $dir structure is valid"
- done
-
- - name: Generate summary report
- run: |
- python -c "
- import json
- import glob
-
- summary = {
- 'total_runs': len(glob.glob('evidence-*')),
- 'timestamp': '$(date -u +%Y-%m-%dT%H:%M:%SZ)',
- 'status': 'success'
- }
-
- with open('ci-summary.json', 'w') as f:
- json.dump(summary, f, indent=2)
- "
-
- - name: Upload CI summary
- uses: actions/upload-artifact@v3
- with:
- name: ci-summary
- path: ci-summary.json
- retention-days: 90
+ - run: echo "Use ci.yml morph-opt-in instead"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6f66a28
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,49 @@
+# Byte-compiled / cache
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+*.egg-info/
+.eggs/
+dist/
+build/
+.mypy_cache/
+.ruff_cache/
+.pytest_cache/
+.coverage
+htmlcov/
+.tox/
+.venv/
+venv/
+env/
+
+# Evidence and local outputs
+/evidence/
+/evidence-*/
+branches/
+branches-smoke/
+diffs/
+replays/*.zip
+*.log
+
+# Secrets and local env
+.env
+.env.*
+!.env.example
+*.pem
+*.key
+credentials.json
+
+# IDE / OS
+.idea/
+.vscode/
+*.swp
+.DS_Store
+Thumbs.db
+
+# Release scratch
+release-bundle/
+*.whl
+!examples/**/*.json
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..5d4b619
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,50 @@
+# Changelog
+
+All notable changes to this project are documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+## [0.1.0] — 2026-07-24
+
+First public Alpha of the hermetic branch-N backend (MRR-ITE-00 through MRR-ITE-09).
+
+### Added
+
+- Hermetic branch-N backend (`replay-runner branch`) with `LocalFakeProvider` offline path
+- ExecutionProfile schema (`mrr.ExecutionProfile.v1`), canonical digest, secret-ref validation
+- Provider Protocol: Morph adapter + `LocalFakeProvider`
+- Hermeticity modes with fail-closed preflight evidence
+- Resource accounting with explicit `unavailable` metrics
+- Differential reports with three-valued absence (`absent ≠ equal`)
+- PCS `RuntimeReceipt.v0` emission; optional PF-Core gate for elevated claim classes
+- PIP `MorphReplayReport.v1` + `TransformationRecord` emission and joint fixtures
+- Always-on vendored JSON Schema instance validation (no external CLI required)
+- Offline CI (lint / typecheck / tests / schema mirrors); Morph smoke opt-in via `MORPH_API_KEY`
+- Security policy, threat model, release checksum script (`scripts/release_bundle.py`)
+- Optional `--ovk-check` (fail-closed when flag set and OVK not on PATH)
+- Branch CLI fail-closed on partial branches unless `--allow-partial`
+- `replay-runner validate` for schema mirrors and vendored instances
+- Release documentation: `NON_CLAIMS.md`, `CONTRIBUTING.md`, `docs/release-checklist.md`,
+ `fixtures/README.md`, public README / SECURITY / ADR polish
+
+### Changed
+
+- CLI is a Click group; legacy ZIP Morph path is `replay-runner run` (compat argv still works)
+- `requires-python` pinned to `>=3.9` (CI matrix 3.9–3.12)
+- Removed unused `aiofiles` / `asyncio-mqtt`; deleted `core_fixed.py`
+- `ReplayValidated` is claim-class metadata only; receipt status uses PCS enum
+ (`RuntimeObserved` / `RuntimeChecked`)
+- `HASH_EXCLUDED_FIELDS` aligned with pcs-core; domain digests via `extra_excluded`
+- HTTP callback flag rejected fail-closed (unimplemented / out of scope)
+
+### Non-claims
+
+Observational / differential evidence only. No causality, remediation ranking,
+environment-semantics generation, or campaign-level verifier assurance.
+See [NON_CLAIMS.md](NON_CLAIMS.md).
+
+[Unreleased]: https://github.com/SentinelOps-CI/morph-replay-runner/compare/v0.1.0...HEAD
+[0.1.0]: https://github.com/SentinelOps-CI/morph-replay-runner/releases/tag/v0.1.0
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..46bbe37
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,82 @@
+# Contributing
+
+## Prerequisites
+
+- Python 3.9–3.12 (CI matrix; 3.9 is the typed floor)
+- Git
+- Optional: `MORPH_API_KEY` only for live Morph (`replay-runner run` / `--provider morph`)
+- Optional: `pcs` / `post-incident` CLIs for deeper external validation extras
+
+## Install
+
+```bash
+python -m pip install --upgrade pip
+pip install -e ".[dev]"
+```
+
+Refresh generated fixtures/examples when you change generators:
+
+```bash
+python scripts/generate_fixtures.py
+```
+
+## Local quality gates
+
+Run the same offline path CI uses:
+
+```bash
+black --check runner tests scripts
+isort --check-only runner tests scripts
+ruff check runner tests scripts
+mypy runner
+python scripts/check_schema_mirrors.py
+python scripts/validate_vendored_instances.py
+pytest tests -q --tb=short
+python scripts/optional_external_validate.py
+pip-audit --strict
+```
+
+Or via the CLI wrapper:
+
+```bash
+replay-runner validate
+replay-runner validate --external-clis # only when pcs/post-incident are installed
+```
+
+Hermetic example (offline, no Morph):
+
+```bash
+replay-runner branch \
+ --incident examples/hermetic-branch-n/incident_bundle.json \
+ --snapshot examples/hermetic-branch-n/snapshot_ref.json \
+ --snapshot-payload examples/hermetic-branch-n/snapshot.payload \
+ --interventions examples/hermetic-branch-n/interventions \
+ --execution-profile examples/hermetic-branch-n/execution_profile.json \
+ --manifest examples/hermetic-branch-n/manifest.json \
+ --parallel 16 \
+ --provider local-fake \
+ --out ./branches
+```
+
+Release checksum bundle:
+
+```bash
+python scripts/release_bundle.py
+```
+
+Full command list: [docs/release-checklist.md](docs/release-checklist.md).
+
+## Standards
+
+- Prefer fail-closed behavior; never invent elevated claim classes or coerce absent values to equal.
+- Do not commit secrets, API keys, `.env` files, or partner plaintext.
+- Keep providers infrastructure-only (no causality / remediation / reward semantics).
+- Update ADRs under `docs/adr/` when contracts or claim boundaries change.
+- Keep [NON_CLAIMS.md](NON_CLAIMS.md) and README claim language aligned with implemented behavior.
+- Do not edit vendored `schemas/pcs/` or `schemas/pip/` in place; bump the pin and re-vendor (see [schemas/README.md](schemas/README.md)).
+
+## Pull requests
+
+- Describe intent and link the relevant MRR-ITE ADR when semantics change.
+- Include or update fixtures under `fixtures/` for new validation paths.
+- Keep default CI offline; Morph remains opt-in behind `MORPH_API_KEY`.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a3829e2
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,17 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/LICENSE-2.0
+
+Copyright 2026 SentinelOps-CI
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/NON_CLAIMS.md b/NON_CLAIMS.md
new file mode 100644
index 0000000..53b0bee
--- /dev/null
+++ b/NON_CLAIMS.md
@@ -0,0 +1,33 @@
+# Non-claims
+
+This repository emits **observational** and **differential** evidence only.
+Claim language is fail-closed: elevated labels require a check that actually ran.
+
+## This project does not claim
+
+- Causality, root-cause attribution, or blame
+- Remediation ranking, preferred branch, or policy certification
+- Environment-semantics generation inside providers
+- Campaign-level verifier assurance
+- That Morph Cloud (or any provider control plane) is honest beyond recorded digests and capability probes
+- That guest workloads are formally verified
+- That `observational` / `RuntimeObserved` evidence is formally verified
+- That HTTP callbacks, live SDE operation, or unimplemented surfaces are available
+
+## Status and claim-class boundaries
+
+| Label | Meaning in this repo |
+|-------|----------------------|
+| Branch `complete` / `partial` | Local observational run status under the scheduler |
+| PCS `RuntimeObserved` | Default `RuntimeReceipt.v0` status when no runtime check ran |
+| PCS `RuntimeChecked` | Receipt status only after a PF-Core / runtime check actually ran |
+| Claim class `ReplayValidated` | Manifest / environment / PF invocation metadata only — **not** a PCS receipt `status` enum member |
+| PIP Morph / transformation records | Structural linkage and digests; never promoted into PCS |
+
+Putting `ReplayValidated` on `RuntimeReceipt.status` is schema-invalid and rejected.
+
+## Related
+
+- [docs/adr/MRR-ITE-00-architecture.md](docs/adr/MRR-ITE-00-architecture.md)
+- [docs/adr/MRR-ITE-07-pcs-pf.md](docs/adr/MRR-ITE-07-pcs-pf.md)
+- [docs/security/threat-model.md](docs/security/threat-model.md)
diff --git a/README.md b/README.md
index ffb99ff..a41f452 100644
--- a/README.md
+++ b/README.md
@@ -1,310 +1,184 @@
-
-
-
-##############################################################################################
-# #
-# __ __ ___ ____ ____ _ _ #
-# | \/ |/ _ \| _ \| _ \| | | | #
-# | |\/| | | | | |_) | |_) | |_| | #
-# | | | | |_| | _ <| __/| _ | #
-# |_| |_|\___/|_| \_\_| |_| |_| #
-# #
-# ____ _____ ____ _ _ __ __ #
-# | _ \| ____| _ \| | / \\ \ / / #
-# | |_) | _| | |_) | | / _ \\ V / #
-# | _ <| |___| __/| |___ / ___ \| | #
-# |_| \_\_____|_| |_____/_/ \_\_| #
-# #
-# ____ _ _ _ _ _ _ _____ ____ #
-# | _ \| | | | \ | | \ | | ____| _ \ #
-# | |_) | | | | \| | \| | _| | |_) | #
-# | _ <| |_| | |\ | |\ | |___| _ < #
-# |_| \_\\___/|_| \_|_| \_|_____|_| \_\ #
-# #
-# #
-##############################################################################################
-
-
-**morph-replay-runner** is a command-line interface (CLI) tool designed to execute TRACE-REPLAY-KIT bundles with branch-N parallelism on Morph Cloud. This tool streamlines the process of running replay tasks, ensuring efficient and scalable execution with comprehensive evidence collection and CERT-V1 compliance.
-
-
-
----
-
-## Installation
-
-### Prerequisites
-
-- Python 3.9 or later
-- Morph Cloud API key
-- Access to Morph Cloud snapshots
-
-### Install from Source
+# morph-replay-runner
-```bash
-# Clone the repository
-git clone https://github.com/SentinelOps-CI/morph-replay-runner.git
-cd morph-replay-runner
+[](https://github.com/SentinelOps-CI/morph-replay-runner/actions/workflows/ci.yml)
+[](pyproject.toml)
+[](LICENSE)
-# Install in development mode
-pip install -e .
-```
+Hermetic, immutable **branch-N** execution backend for SentinelOps-CI.
+Produces observational PCS (`RuntimeReceipt.v0`) and PIP (Morph / transformation) evidence packs.
-### Install Dependencies
+Default path is offline via `LocalFakeProvider`. Live Morph Cloud is opt-in and requires `MORPH_API_KEY`.
-```bash
-# Install all dependencies
-pip install -r requirements.txt
+## Status
-# Or install with development tools
-pip install -e ".[dev]"
-```
+Version **0.1.0** (Alpha). Primary command: `replay-runner branch`.
+Legacy ZIP Morph path: `replay-runner run` (also accepts bare `--snapshot …` argv).
-## Quick Start
+What this release implements:
-```bash
-# Basic usage
-replay-runner \
- --snapshot morphvm-minimal \
- --bundles "./replays/*.zip" \
- --parallel 4 \
- --emit-cert \
- --out ./evidence
+- Hermetic branch-N scheduler with per-branch isolation and fail-closed hermeticity preflight
+- ExecutionProfile schema, canonical digest, secret-ref validation
+- Provider protocol: `LocalFakeProvider` (CI default) + Morph adapter (opt-in)
+- Resource accounting with explicit `unavailable` metrics
+- Differential reports (`absent ≠ equal`)
+- Always-on vendored JSON Schema validation; optional external `pcs` / `post-incident` gates
-# With HTTP callbacks
-replay-runner \
- --snapshot morphvm-minimal \
- --bundles "./examples/*.zip" \
- --parallel 2 \
- --http-callback \
- --http-port 8080 \
- --http-auth api_key
-
-# Asynchronous mode
-replay-runner \
- --snapshot morphvm-minimal \
- --bundles "./replays/*.zip" \
- --parallel 8 \
- --async \
- --timeout 1200
-```
+See [NON_CLAIMS.md](NON_CLAIMS.md) for claim boundaries. Architecture: [docs/adr/](docs/adr/README.md).
+
+## Requirements
-## Usage
+- Python **3.9+** (CI: 3.9–3.12)
+- Optional: `MORPH_API_KEY` for live Morph (`replay-runner run` / `--provider morph`)
+- Optional extras: `pcs`, `pip` (`post-incident`), `ovk` for deeper CLI gates
-### Command Line Options
+## Install
```bash
-replay-runner --help
+python -m pip install --upgrade pip
+pip install -e ".[dev]"
```
-**Required Options:**
-- `--snapshot`: Base snapshot ID or digest containing sidecar + replay tools
-- `--bundles`: Glob pattern for replay bundles (e.g., `./replays/*.zip`)
-
-**Optional Options:**
-- `-p, --parallel`: Number of parallel instances (default: 4)
-- `-t, --timeout`: Execution timeout in seconds (default: 600)
-- `--emit-cert/--no-emit-cert`: Emit CERT-V1 JSON certificates (default: True)
-- `-o, --out`: Output directory for evidence collection (default: `./evidence`)
-- `--async`: Use asynchronous execution mode
-- `--http-callback`: Enable HTTP callback service for demos
-- `--http-port`: Port for HTTP callback service (default: 8080)
-- `--http-auth`: HTTP callback authentication mode (`none` or `api_key`)
-
-### Basic Workflow
-
-1. **Prepare Replay Bundles**: Create TRACE-REPLAY-KIT compliant zip files
-2. **Set Environment**: Configure your Morph Cloud API key
-3. **Execute**: Run the tool with appropriate parameters
-4. **Collect Evidence**: Review generated certificates, logs, and reports
-
-## Configuration
-
-### Environment Variables
+Refresh fixtures/examples after generator changes:
```bash
-# Set your Morph Cloud API key
-export MORPH_API_KEY="your_api_key_here"
+python scripts/generate_fixtures.py
```
-### Configuration File
-
-The tool automatically detects and uses the `MORPH_API_KEY` environment variable. For advanced configuration, you can modify the `pyproject.toml` file.
-
-## Error Handling
-
-The tool utilizes standard Python exceptions such as `ValueError` and `Exception` to handle errors gracefully. Common error scenarios include:
-
-- **API Key Missing**: Ensure `MORPH_API_KEY` is set
-- **Snapshot Not Found**: Verify snapshot ID exists in Morph Cloud
-- **Bundle Format Issues**: Ensure bundles are valid TRACE-REPLAY-KIT format
-- **Network Issues**: Check connectivity to Morph Cloud
-
-## Project Structure
+## Quick start (hermetic, offline)
-```
-morph-replay-runner/
-├── runner/ # Main runner package
-│ ├── __init__.py # Package initialization
-│ ├── main.py # CLI entry point
-│ ├── core.py # Core execution logic
-│ └── models.py # Data models
-├── schemas/ # JSON schemas
-│ ├── cert_v1.json # CERT-V1 schema
-│ ├── trace_replay_kit.json # TRACE-REPLAY-KIT schema
-│ └── replay_runner.json # Internal schemas
-├── examples/ # Demo replay bundles
-│ ├── demo-http.json # HTTP demo
-│ ├── demo-tcp.json # TCP demo
-│ └── demo-file.json # File operations demo
-├── docker/ # Docker support
-│ ├── Dockerfile # Container image
-│ └── docker-compose.yml # Multi-service setup
-├── .github/workflows/ # CI/CD workflows
-│ └── replay.yml # Matrix testing workflow
-├── pyproject.toml # Project configuration
-├── requirements.txt # Dependencies
-└── README.md # This file
+```bash
+replay-runner branch \
+ --incident examples/hermetic-branch-n/incident_bundle.json \
+ --snapshot examples/hermetic-branch-n/snapshot_ref.json \
+ --snapshot-payload examples/hermetic-branch-n/snapshot.payload \
+ --interventions examples/hermetic-branch-n/interventions \
+ --execution-profile examples/hermetic-branch-n/execution_profile.json \
+ --manifest examples/hermetic-branch-n/manifest.json \
+ --parallel 16 \
+ --provider local-fake \
+ --out ./branches
```
-## Examples
-
-### Demo Bundles
-
-The project includes three example replay bundles:
-
-1. **HTTP Demo** (`examples/demo-http.json`): HTTP GET request to httpbin.org
-2. **TCP Demo** (`examples/demo-tcp.json`): TCP connection to localhost:8080
-3. **File Demo** (`examples/demo-file.json`): File operations in /tmp
-
-### Creating Custom Bundles
-
-```json
-{
- "version": "2.1.0",
- "manifest": {
- "bundle_id": "my-custom-replay",
- "created_at": "2025-01-01T00:00:00Z",
- "description": "Custom replay description",
- "tags": ["custom", "demo"],
- "author": "Your Name"
- },
- "replay_data": {
- "type": "http",
- "payload": {
- "method": "GET",
- "url": "https://api.example.com/endpoint"
- }
- }
-}
-```
+No Morph credentials required. Exit is non-zero if any branch is `partial` unless `--allow-partial` is set.
-## Docker Support
+## Morph opt-in (compatibility path)
-### Build and Run
+Requires `MORPH_API_KEY` in the environment. Without it, the GitHub Actions `morph-opt-in` job is skipped.
```bash
-# Build the Docker image
-docker build -f docker/Dockerfile -t morph-replay-runner .
-
-# Run with Docker
-docker run -e MORPH_API_KEY="your_key" morph-replay-runner --help
-```
-
-### Docker Compose
-
-```bash
-# Start services
-docker-compose -f docker/docker-compose.yml up
-
-# Run with mounted volumes
-docker-compose -f docker/docker-compose.yml run morph-replay-runner \
+export MORPH_API_KEY=... # never commit
+replay-runner run \
--snapshot morphvm-minimal \
- --bundles "/app/replays/*.zip"
+ --bundles "./replays/*.zip" \
+ --parallel 4 \
+ --provider morph \
+ --out ./evidence
```
-## CI/CD Pipeline
-
-The project includes a comprehensive GitHub Actions workflow that:
-
-- **Matrix Testing**: Tests across Python versions (3.9, 3.10, 3.11)
-- **Bundle Validation**: Tests different bundle types (HTTP, TCP, File)
-- **Parallel Execution**: Tests various parallel instance counts
-- **Evidence Collection**: Validates generated certificates and reports
-- **Artifact Publishing**: Uploads evidence packs for review
-
-### Workflow Triggers
-
-- **Push**: Automatically runs on pushes to `main` and `develop` branches
-- **Pull Request**: Runs on all PRs for quality assurance
-- **Manual Dispatch**: Can be triggered manually with custom parameters
+Legacy argv without a subcommand (`replay-runner --snapshot …`) still maps to `run`.
+`--http-callback` is rejected fail-closed (unimplemented / out of scope for hermetic branch-N).
+
+## CLI reference
+
+| Command | Purpose |
+|---------|---------|
+| `replay-runner branch` | Hermetic isolated branch-N execution (primary) |
+| `replay-runner run` | Legacy ZIP-bundle Morph path |
+| `replay-runner profile validate --file …` | Validate ExecutionProfile |
+| `replay-runner profile digest --file …` | Print profile digest |
+| `replay-runner diff --branches … --pairs a,b --out …` | Pairwise differential reports |
+| `replay-runner validate` | Schema mirrors + vendored instance checks |
+| `replay-runner validate --external-clis` | Also run pcs/post-incident when installed |
+
+Useful `branch` flags:
+
+- `--provider local-fake|morph` (default `local-fake`)
+- `--snapshot-payload` — raw bytes whose sha256 must match `snapshot_digest`
+- `--allow-partial` — accept explicit partial branches (default: fail closed)
+- `--ovk-check ` — shell out to OVK on PATH; fail closed if missing when set
+
+## Evidence layout
+
+Each `branches//` is an immutable evidence pack:
+
+```text
+execution_profile.json
+snapshot_ref.json
+intervention_record.json
+runtime_receipt.json # PCS RuntimeReceipt.v0
+morph_replay_report.json # PIP
+transformation_record.json # PIP
+resource_report.json
+terminal_state.json
+terminal_commitment.json
+branch_report.json
+teardown_receipt.json
+digests.json
+logs.txt
+status.json # complete | partial
+```
-## Testing
+Run root also includes `hermeticity_evidence.json` and `summary.json` (with `non_claims`).
-### Local Testing
+## Differential reports
```bash
-# Install development dependencies
-pip install -e ".[dev]"
-
-# Run tests
-pytest
-
-# Code quality checks
-black .
-isort .
-ruff check .
-mypy .
+replay-runner diff --branches ./branches --pairs branch-00,branch-01 --out ./diffs
```
-### CI Testing
-
-The CI pipeline automatically:
-- Creates demo bundles
-- Executes replay runner
-- Validates evidence collection
-- Generates summary reports
+Comparisons use `equal | different | absent_left | absent_right | absent_both`.
+Missing data is never coerced to equal. Outputs ban preferred-branch / causality language.
-## Output Structure
+## Offline quality gates
+```bash
+pytest tests -q
+python scripts/check_schema_mirrors.py
+python scripts/validate_vendored_instances.py
+python scripts/optional_external_validate.py # skips only when pcs/post-incident absent
+python scripts/release_bundle.py
```
-evidence/
-├── certs/ # CERT-V1 certificates
-│ ├── cert_0.json # Bundle 0 certificate
-│ ├── cert_1.json # Bundle 1 certificate
-│ └── ...
-├── logs/ # Execution logs
-│ ├── log_0.txt # Bundle 0 log
-│ ├── log_1.txt # Bundle 1 log
-│ └── ...
-└── reports/ # Summary reports
- └── index.json # Execution summary
-```
-
-## Contributing
-We welcome contributions to **morph-replay-runner**! To contribute:
+Full release command list: [docs/release-checklist.md](docs/release-checklist.md).
+Contributing: [CONTRIBUTING.md](CONTRIBUTING.md).
+
+## Layout
+
+```text
+runner/ # CLI, profile, hermeticity, branch, accounting, diff, evidence, providers
+schemas/mrr/ # repo-local domain schemas
+schemas/pcs/ # mirrored pcs-core (RuntimeReceipt.v0 + defs)
+schemas/pip/ # mirrored PIP Morph/lineage schemas
+fixtures/ # valid/invalid profiles, branch inputs, PIP linkage
+examples/hermetic-branch-n/
+docs/adr/ # MRR-ITE architecture decisions
+docs/security/ # threat model
+docs/baseline/ # historical ITE-00 measurement
+```
-1. **Fork** the repository
-2. **Create** a feature branch (`git checkout -b feature/amazing-feature`)
-3. **Commit** your changes (`git commit -m 'Add amazing feature'`)
-4. **Push** to the branch (`git push origin feature/amazing-feature`)
-5. **Open** a Pull Request
+## Pins
-### Development Guidelines
+| Layer | Pin |
+|-------|-----|
+| PCS | pcs-core `v0.1.0` schemas under `schemas/pcs/` |
+| PIP | post-incident-proofs schemas under `schemas/pip/` |
+| OVK | shell-out only; optional extra wheel `1.2.1` |
+| Morph | `morphcloud>=0.1.91` (live use opt-in via `MORPH_API_KEY`) |
-- Follow PEP 8 style guidelines
-- Add tests for new functionality
-- Update documentation as needed
-- Ensure all CI checks pass
+Mirror digests and refresh procedure: [schemas/README.md](schemas/README.md).
-### Code Quality Tools
+## Documentation
-- **Black**: Code formatting
-- **Isort**: Import sorting
-- **Ruff**: Linting and formatting
-- **MyPy**: Type checking
+| Doc | Topic |
+|-----|-------|
+| [NON_CLAIMS.md](NON_CLAIMS.md) | Fail-closed claim boundaries |
+| [SECURITY.md](SECURITY.md) | Disclosure and secret handling |
+| [CHANGELOG.md](CHANGELOG.md) | Version history |
+| [docs/security/threat-model.md](docs/security/threat-model.md) | Adversaries and trust boundaries |
+| [docs/adr/](docs/adr/) | MRR-ITE decisions |
+| [docs/baseline/MRR-ITE-00.md](docs/baseline/MRR-ITE-00.md) | Historical baseline |
+| [docs/release-checklist.md](docs/release-checklist.md) | Exact release commands |
## License
-**morph-replay-runner** is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for more details.
+Apache-2.0. See [LICENSE](LICENSE).
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..ca485ea
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,36 @@
+# Security Policy
+
+## Supported versions
+
+| Version | Supported |
+|---------|-----------|
+| 0.1.x | Yes |
+
+## Reporting a vulnerability
+
+Do not open a public issue for secret leakage, isolation bypasses, or claim-upgrade bugs.
+
+Preferred channels (in order):
+
+1. GitHub Security Advisories for this repository (Security tab → Advisories → Report a vulnerability), when enabled
+2. Email **security@sentinelops.ci** with affected commit/tag, reproduction steps, and impact
+
+Include a minimal reproduction when possible. Do not attach production credentials, partner plaintext, or live Morph API keys.
+
+Target response:
+
+- Acknowledgement within 2 business days
+- Triage update within 7 business days
+
+## Secret handling
+
+- Live Morph credentials are read from the environment variable `MORPH_API_KEY` only
+- ExecutionProfile accepts `secret_ref` identifiers; secret **values** in profiles are rejected fail-closed
+- Evidence emitters redact known secret material from logs and status payloads
+- Never commit `.env`, key files, or credentials; `.env` is gitignored
+- Default CI is offline (`LocalFakeProvider`); the `morph-opt-in` workflow job runs only when `MORPH_API_KEY` is configured as a repository secret
+
+## Threat model
+
+See [docs/security/threat-model.md](docs/security/threat-model.md) for assets, adversaries, trust boundaries, and non-claims.
+Architecture claim boundaries: [NON_CLAIMS.md](NON_CLAIMS.md).
diff --git a/docker/Dockerfile b/docker/Dockerfile
index e9f50ea..5a71572 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,27 +1,25 @@
FROM python:3.11-slim
-# Set working directory
WORKDIR /app
-# Install system dependencies
-RUN apt-get update && apt-get install -y \
+RUN apt-get update && apt-get install -y --no-install-recommends \
git \
curl \
&& rm -rf /var/lib/apt/lists/*
-# Copy requirements and install Python dependencies
-COPY pyproject.toml requirements.txt* ./
+# Copy project before editable install so package sources exist.
+COPY pyproject.toml README.md LICENSE ./
+COPY runner ./runner
+COPY schemas ./schemas
+COPY requirements.txt* ./
+
RUN pip install --no-cache-dir -e .
-# Copy source code
COPY . .
-# Create necessary directories
RUN mkdir -p /app/evidence/certs /app/evidence/logs /app/evidence/reports
-# Set environment variables
ENV PYTHONPATH=/app
ENV MORPH_API_KEY=""
-# Default command
CMD ["replay-runner", "--help"]
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index c939f51..5814ec8 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -1,4 +1,7 @@
-version: '3.8'
+version: "3.8"
+
+# Local Morph container helper. HTTP callback stub removed (fail-closed /
+# out of scope for hermetic branch-N). Use `replay-runner branch` offline.
services:
morph-replay-runner:
@@ -12,13 +15,3 @@ services:
- ../replays:/app/replays:ro
working_dir: /app
command: ["replay-runner", "--help"]
-
- # Example service for testing HTTP callbacks
- test-http:
- image: nginx:alpine
- ports:
- - "8080:80"
- volumes:
- - ./test-http.conf:/etc/nginx/conf.d/default.conf:ro
- depends_on:
- - morph-replay-runner
diff --git a/docs/adr/MRR-ITE-00-architecture.md b/docs/adr/MRR-ITE-00-architecture.md
new file mode 100644
index 0000000..efe23aa
--- /dev/null
+++ b/docs/adr/MRR-ITE-00-architecture.md
@@ -0,0 +1,76 @@
+# ADR MRR-ITE-00: Hermetic Branch-N Architecture Boundaries
+
+## Status
+
+Accepted
+
+## Date
+
+2026-07-24
+
+## Context
+
+`morph-replay-runner` began as a thin Morph Cloud ZIP-replay CLI. SentinelOps requires a hermetic, immutable, branch-N execution backend that emits observational evidence under PCS and PIP contracts without inventing research semantics inside providers.
+
+## Decision
+
+### Provider boundary
+
+Providers (`MorphCloudProvider`, `LocalFakeProvider`) implement only infrastructure operations: snapshot lookup, clone, execute, measure, extract artifacts, teardown, and hermeticity capability probes. Providers MUST NOT encode incident causality, remediation preference, reward semantics, or verifier policy interpretation.
+
+### Ownership
+
+| Concern | Owner module |
+|---------|--------------|
+| ExecutionProfile schema, digest, secret refs | `runner/profile/` |
+| Hermeticity modes + enforcement evidence | `runner/hermeticity/` |
+| BranchReplayManifest + scheduler | `runner/branch/` |
+| Resource accounting | `runner/accounting/` |
+| Differential reports | `runner/diff/` |
+| PCS/PIP emitters, digests, redaction | `runner/evidence/` |
+| OVK checker shell-out | `runner/ovk/` |
+| Legacy ZIP Morph path | `runner/legacy_core.py` (wrapper over prior `core.py` behavior) |
+
+### Claim classes
+
+| Layer | Allowed claim / label | When |
+|-------|----------------------|------|
+| Repo-local | observational branch status `complete` \| `partial` | always when written |
+| PCS | `RuntimeReceipt.v0` with status `RuntimeObserved` | every complete branch emission |
+| PCS | `RuntimeChecked` | only after an actual runtime check ran |
+| PF-Core | `ReplayValidated` / five-file emit | only when claim class applies and PF-shaped trace supplied |
+| PIP | `MorphReplayReport.v1` status `recorded` \| `mismatch` \| `indeterminate` | when PIP emitter runs |
+| OVK | digests/refs of checker results | only when OVK was shell-invoked |
+
+### Explicit non-claims
+
+- No causality or root-cause attribution
+- No remediation ranking or “preferred branch”
+- No environment semantics generation
+- No campaign-level verifier assurance
+- HTTP callback stubs are out of scope for hermetic branch-N; re-enabling without a real implementation MUST fail closed
+- CERT-V1 guest copies remain optional guest artifacts referenced by digest; they are not PCS substitutes
+
+Product-level text: [NON_CLAIMS.md](../../NON_CLAIMS.md).
+
+### Cleanup completed in later ITEs
+
+Tracked here for history; executed in MRR-ITE-02:
+
+- Removed unused duplicate `runner/core_fixed.py`
+- Dropped unused dependencies `aiofiles`, `asyncio-mqtt`
+- Simplified `docker/docker-compose.yml` (removed broken HTTP stub service)
+
+### Compatibility
+
+`replay-runner branch` is the primary hermetic path (MRR-ITE-04).
+Preserve `replay-runner --snapshot … --bundles …` as the `run` compatibility path.
+Default CI MUST run offline via `LocalFakeProvider`; Morph remains opt-in behind secrets.
+
+## Consequences
+
+Later ITEs introduce schemas and runtime behind these boundaries. Violations (providers encoding research semantics, claim upgrades without checks, secret values in profiles) are hard failures.
+
+## Rollback
+
+Revert docs/CI scaffolding introduced in ITE-00; no schema consumers at baseline time.
diff --git a/docs/adr/MRR-ITE-01-execution-profile.md b/docs/adr/MRR-ITE-01-execution-profile.md
new file mode 100644
index 0000000..0ef582a
--- /dev/null
+++ b/docs/adr/MRR-ITE-01-execution-profile.md
@@ -0,0 +1,25 @@
+# ADR MRR-ITE-01: ExecutionProfile
+
+## Status
+
+Accepted
+
+## Date
+
+2026-07-24
+
+## Context
+
+Branch-N runs need a single pinned execution contract (snapshot, images, network, budgets, secret refs) with a stable digest for evidence linkage.
+
+## Decision
+
+Introduce versioned `mrr.ExecutionProfile.v1` with canonical digest (PCS Canonical JSON v1), material pins required, and secrets by `secret_ref` only. Schema: `schemas/mrr/execution_profile.v1.json`. Loader: `runner/profile/`.
+
+## Non-claims
+
+Profile digests do not attest guest correctness or Morph control-plane honesty beyond recorded pins.
+
+## Consequences
+
+Invalid profiles (missing pins, secret values, wrong schema version) fail closed before scheduling.
diff --git a/docs/adr/MRR-ITE-02-providers.md b/docs/adr/MRR-ITE-02-providers.md
new file mode 100644
index 0000000..4dc8ad9
--- /dev/null
+++ b/docs/adr/MRR-ITE-02-providers.md
@@ -0,0 +1,25 @@
+# ADR MRR-ITE-02: Provider abstraction
+
+## Status
+
+Accepted
+
+## Date
+
+2026-07-24
+
+## Context
+
+Hard-wiring Morph in the CLI blocked offline CI and mixed infrastructure with research semantics.
+
+## Decision
+
+`ExecutionProvider` Protocol isolates infrastructure. `LocalFakeProvider` is the default offline CI backend. Morph is opt-in and fail-closed when required digests/capabilities are missing.
+
+## Cleanup
+
+Deleted `runner/core_fixed.py`; dropped unused `aiofiles` / `asyncio-mqtt`; simplified docker-compose (removed broken HTTP stub service).
+
+## Consequences
+
+Default `branch` provider is `local-fake`. Legacy `run` still defaults to `morph` and requires Morph credentials.
diff --git a/docs/adr/MRR-ITE-03-hermeticity.md b/docs/adr/MRR-ITE-03-hermeticity.md
new file mode 100644
index 0000000..6be612b
--- /dev/null
+++ b/docs/adr/MRR-ITE-03-hermeticity.md
@@ -0,0 +1,21 @@
+# ADR MRR-ITE-03: Hermeticity modes
+
+## Status
+
+Accepted
+
+## Date
+
+2026-07-24
+
+## Context
+
+Network and dependency assumptions must be explicit and enforceable before clone, with recorded evidence of enforcement or denial.
+
+## Decision
+
+Five modes (`no_network`, `allowlisted_network`, `recorded_response`, `partner_local`, `synthetic_dependency`) with `supports()` preflight. Unsupported modes fail before clone; evidence distinguishes `enforced` vs `unsupported`. Implementation: `runner/hermeticity/`.
+
+## Consequences
+
+Hermeticity evidence is written at the run root (`hermeticity_evidence.json`). Bypass attempts are hard failures, not warnings.
diff --git a/docs/adr/MRR-ITE-04-branch-scheduler.md b/docs/adr/MRR-ITE-04-branch-scheduler.md
new file mode 100644
index 0000000..000c550
--- /dev/null
+++ b/docs/adr/MRR-ITE-04-branch-scheduler.md
@@ -0,0 +1,21 @@
+# ADR MRR-ITE-04: Branch-N scheduler
+
+## Status
+
+Accepted
+
+## Date
+
+2026-07-24
+
+## Context
+
+Parallel branch execution must not share dirty sibling state; retries must reclone from the canonical snapshot.
+
+## Decision
+
+`BranchReplayManifest.v1` + scheduler clones each branch from the canonical snapshot, never from dirty siblings. Retries teardown and reclone. Independence unless cancellation policy says otherwise. `branch` CLI is the primary hermetic path; `run` remains compatibility. Implementation: `runner/branch/`.
+
+## Consequences
+
+Per-branch evidence packs are immutable once written. Process exits non-zero on any `partial` branch unless `--allow-partial`.
diff --git a/docs/adr/MRR-ITE-05-resources.md b/docs/adr/MRR-ITE-05-resources.md
new file mode 100644
index 0000000..7c4ed4e
--- /dev/null
+++ b/docs/adr/MRR-ITE-05-resources.md
@@ -0,0 +1,21 @@
+# ADR MRR-ITE-05: Resource accounting
+
+## Status
+
+Accepted
+
+## Date
+
+2026-07-24
+
+## Context
+
+Providers may be unable to observe some metrics; fabricating zeros would overclaim.
+
+## Decision
+
+Every metric carries `availability: observed|unavailable|not_applicable`. Unavailable metrics must have `value: null`. Budget enforcement fails closed on observed overruns. Schema: `schemas/mrr/resource_report.v1.json`.
+
+## Consequences
+
+Resource reports are validated before evidence write; unavailable metrics never silently become equal in diffs.
diff --git a/docs/adr/MRR-ITE-06-differential.md b/docs/adr/MRR-ITE-06-differential.md
new file mode 100644
index 0000000..36b7af4
--- /dev/null
+++ b/docs/adr/MRR-ITE-06-differential.md
@@ -0,0 +1,21 @@
+# ADR MRR-ITE-06: Differential reports
+
+## Status
+
+Accepted
+
+## Date
+
+2026-07-24
+
+## Context
+
+Pairwise branch comparison must not invent equality when a side is missing, and must not encode preferred-branch semantics.
+
+## Decision
+
+Pairwise reports with three-valued absence semantics (`equal | different | absent_left | absent_right | absent_both`). Ban preferred-branch / causality / remediation language in outputs. Schema: `schemas/mrr/differential_report.v1.json`. CLI: `replay-runner diff`.
+
+## Consequences
+
+Injected causal / remediation claim text outside `non_claims` fails validation. See [NON_CLAIMS.md](../../NON_CLAIMS.md).
diff --git a/docs/adr/MRR-ITE-07-pcs-pf.md b/docs/adr/MRR-ITE-07-pcs-pf.md
new file mode 100644
index 0000000..a19e8b3
--- /dev/null
+++ b/docs/adr/MRR-ITE-07-pcs-pf.md
@@ -0,0 +1,35 @@
+# ADR MRR-ITE-07: PCS / PF-Core outputs
+
+## Status
+
+Accepted (amended)
+
+## Date
+
+2026-07-24
+
+## Context
+
+Branch evidence must link to PCS `RuntimeReceipt.v0` without inventing elevated receipt statuses. PF-Core is optional and fail-closed.
+
+## Decision
+
+Emit `RuntimeReceipt.v0` with `RuntimeObserved` by default.
+
+`RuntimeChecked` is the only elevated **receipt status** used when a PF-Core /
+runtime check actually ran. `ReplayValidated` is a **claim class** (manifest /
+environment metadata + PF invocation record), not a PCS `artifact_status` enum
+member — putting it on `RuntimeReceipt.status` is schema-invalid.
+
+PF-Core via `pcs pf-core replay-trace` shell-out only when claim class is
+`RuntimeChecked` or `ReplayValidated`. Missing trace or missing `pcs` CLI fails
+closed (branch marked partial / error). CERT-V1 remains optional guest artifact,
+not a PCS substitute.
+
+Vendored `schemas/pcs/` instance validation always runs in emitters and CI;
+external `pcs validate` is an optional deeper gate when the CLI is installed.
+
+## Consequences
+
+Claim-class metadata may record `ReplayValidated`; receipt `status` never does.
+See [NON_CLAIMS.md](../../NON_CLAIMS.md).
diff --git a/docs/adr/MRR-ITE-08-pip.md b/docs/adr/MRR-ITE-08-pip.md
new file mode 100644
index 0000000..661b779
--- /dev/null
+++ b/docs/adr/MRR-ITE-08-pip.md
@@ -0,0 +1,21 @@
+# ADR MRR-ITE-08: PIP integration
+
+## Status
+
+Accepted
+
+## Date
+
+2026-07-24
+
+## Context
+
+Morph replay linkage for post-incident-proofs must stay observational and digest-linked to PCS, never promoted into PCS receipts.
+
+## Decision
+
+Emit `pip.MorphReplayReport.v1` and `TransformationRecord.v1` per branch. Cross-link PCS by digest only; never promote Morph reports into PCS. Joint fixtures under `fixtures/pip/valid_replay_linkage/`. Schemas mirrored under `schemas/pip/` with digest CI gate.
+
+## Consequences
+
+PIP emission is structural linkage only. Optional `post-incident` CLI gates skip when absent; vendored schema validation always runs.
diff --git a/docs/adr/MRR-ITE-09-security-release.md b/docs/adr/MRR-ITE-09-security-release.md
new file mode 100644
index 0000000..534c4a9
--- /dev/null
+++ b/docs/adr/MRR-ITE-09-security-release.md
@@ -0,0 +1,21 @@
+# ADR MRR-ITE-09: Security and release
+
+## Status
+
+Accepted
+
+## Date
+
+2026-07-24
+
+## Context
+
+Public release requires an honest secret model, isolation tests, disclosure route, and checksummed release artifacts — without overclaiming Morph trust or unimplemented surfaces.
+
+## Decision
+
+Isolation / cleanup / secret-erasure tests are mandatory gates. `SECURITY.md` + threat model published. Release checksum script covers schemas and example evidence (`scripts/release_bundle.py`). README and `NON_CLAIMS.md` claims match implemented behavior only. Default CI remains offline; Morph opt-in behind `MORPH_API_KEY`.
+
+## Consequences
+
+Release checklist: [docs/release-checklist.md](../release-checklist.md). Disclosure: [SECURITY.md](../../SECURITY.md).
diff --git a/docs/adr/README.md b/docs/adr/README.md
new file mode 100644
index 0000000..4df01bc
--- /dev/null
+++ b/docs/adr/README.md
@@ -0,0 +1,17 @@
+# Architecture Decision Records (MRR-ITE)
+
+| ADR | Title | Status |
+|-----|-------|--------|
+| [MRR-ITE-00](MRR-ITE-00-architecture.md) | Hermetic branch-N architecture boundaries | Accepted |
+| [MRR-ITE-01](MRR-ITE-01-execution-profile.md) | ExecutionProfile | Accepted |
+| [MRR-ITE-02](MRR-ITE-02-providers.md) | Provider abstraction | Accepted |
+| [MRR-ITE-03](MRR-ITE-03-hermeticity.md) | Hermeticity modes | Accepted |
+| [MRR-ITE-04](MRR-ITE-04-branch-scheduler.md) | Branch-N scheduler | Accepted |
+| [MRR-ITE-05](MRR-ITE-05-resources.md) | Resource accounting | Accepted |
+| [MRR-ITE-06](MRR-ITE-06-differential.md) | Differential reports | Accepted |
+| [MRR-ITE-07](MRR-ITE-07-pcs-pf.md) | PCS / PF-Core outputs | Accepted (amended) |
+| [MRR-ITE-08](MRR-ITE-08-pip.md) | PIP integration | Accepted |
+| [MRR-ITE-09](MRR-ITE-09-security-release.md) | Security and release | Accepted |
+
+Historical baseline measurement: [../baseline/MRR-ITE-00.md](../baseline/MRR-ITE-00.md).
+Claim boundaries: [../../NON_CLAIMS.md](../../NON_CLAIMS.md).
diff --git a/docs/baseline/MRR-ITE-00.md b/docs/baseline/MRR-ITE-00.md
new file mode 100644
index 0000000..22c5a90
--- /dev/null
+++ b/docs/baseline/MRR-ITE-00.md
@@ -0,0 +1,90 @@
+# MRR-ITE-00 Baseline
+
+## Status
+
+**Historical baseline measurement** for MRR-ITE-00 (2026-07-24).
+The gap inventory below describes the repository **at the base commit**, before
+subsequent ITEs. Implemented capabilities after 0.1.0 are documented in
+[docs/adr/](../adr/), [README.md](../../README.md), and [CHANGELOG.md](../../CHANGELOG.md).
+Do not treat the Pre-ITE inventory as current product status.
+
+## Identity
+
+| Field | Value |
+|-------|-------|
+| Base commit | `b3018d790e3e7c622b28641c0230a61bb3b78955` |
+| Base message | Update README with ASCII art and project details |
+| Measurement date | 2026-07-24 |
+| Host OS | Windows 10.0.26200 |
+| Python | 3.13.11 (local measurement host); package/`requires-python` and CI target **3.9–3.12** |
+| Package version | 0.1.0 (Alpha) |
+
+## Locked pins (sibling repos on measurement host)
+
+| Layer | Pin | Path / artifact |
+|-------|-----|-----------------|
+| PCS | `pcs-core` tag `v0.1.0` (`2f0ce059652408bce19bc89ba6660030020d5ea0`) | `C:\Users\mateo\pcs-core` |
+| PIP | `post-incident-proofs` commit `3cdfdf09c20f08ad5221d29607b5a9726295ad10` | `C:\Users\mateo\post-incident-proofs` |
+| OVK | wheel `1.2.1` | `C:\Users\mateo\ovk-v1.2.1-dist\open_verification_kernel-1.2.1-py3-none-any.whl` |
+| Morph | `morphcloud>=0.1.91` (optional live) | PyPI / Morph Cloud |
+
+## Tool versions (measurement host)
+
+| Tool | Version |
+|------|---------|
+| pydantic | 2.12.5 |
+| click | 8.2.1 |
+| jsonschema | 4.23.0 |
+| morphcloud | not installed on measurement host (live Morph remains opt-in) |
+
+## Baseline commands and results
+
+```text
+python --version
+# Python 3.13.11
+
+git rev-parse HEAD
+# b3018d790e3e7c622b28641c0230a61bb3b78955
+
+python -c "import runner; print(runner.__version__)"
+# 0.1.0 (after editable install)
+
+pytest tests/unit/test_import_smoke.py -q
+# (introduced in this ITE; must pass offline)
+```
+
+## Pre-ITE inventory (gaps at base commit — superseded by later ITEs)
+
+At `b3018d79…` the tree had:
+
+- Single Click command hard-wired to `MorphCloudClient` in `runner/core.py`
+- No provider abstraction, ExecutionProfile, hermeticity, branch-N scheduler, PCS, PIP, or resource accounting
+- No unit tests; CI is live Morph smoke only
+- `--timeout` / `--http-callback` are config-only stubs
+- Sync path reuses dirty Morph branches without reset
+- Dead file `runner/core_fixed.py`; unused deps `aiofiles`, `asyncio-mqtt`
+- Broken docker-compose reference to missing `test-http.conf`
+
+These gaps were addressed across MRR-ITE-01–09. Current behavior: hermetic `branch` + offline CI; `--http-callback` fail-closed.
+
+## Non-claims (baseline)
+
+This repository at the base commit does **not** claim:
+
+- Causality, remediation ranking, or environment-semantics generation
+- Campaign-level verifier assurance
+- Hermetic branch-N isolation
+- PCS `RuntimeReceipt` / PIP Morph report emission
+- Offline deterministic CI
+
+Evidence labels `runtime_observed` / `RuntimeChecked` / `ReplayValidated` are forbidden until the corresponding check actually runs (later ITEs).
+Current product non-claims: [NON_CLAIMS.md](../../NON_CLAIMS.md).
+
+## Quality scaffolding introduced here
+
+- Apache-2.0 `LICENSE`
+- `.gitignore`
+- Offline CI job (lint/typecheck/unit without Morph)
+- `requires-python >=3.9` aligned with README/CI
+- Import smoke tests
+- Architecture ADR
diff --git a/docs/release-checklist.md b/docs/release-checklist.md
new file mode 100644
index 0000000..1f9f6be
--- /dev/null
+++ b/docs/release-checklist.md
@@ -0,0 +1,123 @@
+# Release checklist
+
+Commands assume the repository root and an editable install (`pip install -e ".[dev]"`).
+Default CI is offline; Morph is opt-in.
+
+## 1. Install
+
+```bash
+python -m pip install --upgrade pip
+pip install -e ".[dev]"
+```
+
+Optional extras (not required for offline release gates):
+
+```bash
+pip install -e ".[pcs,pip,ovk]"
+```
+
+## 2. Lint and format
+
+```bash
+black --check runner tests scripts
+isort --check-only runner tests scripts
+ruff check runner tests scripts
+```
+
+## 3. Typecheck
+
+```bash
+mypy runner
+```
+
+## 4. Tests
+
+```bash
+pytest tests -q --tb=short
+```
+
+## 5. Schema mirror digests
+
+```bash
+python scripts/check_schema_mirrors.py
+```
+
+## 6. Vendored instance validation
+
+Always-on offline gate (no external CLIs required):
+
+```bash
+python scripts/validate_vendored_instances.py
+```
+
+Equivalent CLI entry:
+
+```bash
+replay-runner validate
+```
+
+## 7. Optional external validate
+
+Skips only when `pcs` / `post-incident` are absent; fails closed when present and non-zero:
+
+```bash
+python scripts/optional_external_validate.py
+# or:
+replay-runner validate --external-clis
+```
+
+## 8. Dependency audit
+
+```bash
+pip-audit --strict
+```
+
+## 9. Hermetic example smoke (offline)
+
+```bash
+replay-runner branch \
+ --incident examples/hermetic-branch-n/incident_bundle.json \
+ --snapshot examples/hermetic-branch-n/snapshot_ref.json \
+ --snapshot-payload examples/hermetic-branch-n/snapshot.payload \
+ --interventions examples/hermetic-branch-n/interventions \
+ --execution-profile examples/hermetic-branch-n/execution_profile.json \
+ --manifest examples/hermetic-branch-n/manifest.json \
+ --parallel 16 \
+ --provider local-fake \
+ --out ./branches
+```
+
+Expect exit 0 with all branches `complete` unless `--allow-partial` is intentional.
+
+## 10. Release checksum bundle
+
+```bash
+python scripts/release_bundle.py
+```
+
+Writes `release-bundle/checksums.json` (gitignored scratch) covering schemas, the hermetic example tree, and PIP linkage fixtures.
+
+## 11. Docs and claim surface
+
+Confirm before tagging:
+
+- [README.md](../README.md), [NON_CLAIMS.md](../NON_CLAIMS.md), [CHANGELOG.md](../CHANGELOG.md), [SECURITY.md](../SECURITY.md) match implemented behavior
+- Version in `pyproject.toml`, `runner.__version__`, and CLI `--version` agree
+- No TODOs or aspirational Morph/HTTP-callback marketing in shipped docs
+- License Apache-2.0 referenced from README and `pyproject.toml`
+
+## 12. Morph opt-in (optional, not a release blocker)
+
+Requires repository secret / environment `MORPH_API_KEY`. Without it, the GitHub Actions `morph-opt-in` job is skipped.
+
+```bash
+export MORPH_API_KEY=... # never commit
+replay-runner run \
+ --snapshot morphvm-minimal \
+ --bundles "./replays/*.zip" \
+ --parallel 1 \
+ --provider morph \
+ --out ./evidence-morph
+```
+
+`--http-callback` is rejected fail-closed (unimplemented / out of scope).
diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md
new file mode 100644
index 0000000..e1a22f1
--- /dev/null
+++ b/docs/security/threat-model.md
@@ -0,0 +1,55 @@
+# Threat Model — morph-replay-runner
+
+Date: 2026-07-24 · Scope: version 0.1.x hermetic branch-N + legacy Morph `run` path
+
+## Assets
+
+- Snapshot bytes and digests
+- ExecutionProfile pins and digests
+- Per-branch evidence packs (PCS / PIP)
+- Secret references (identifiers only)
+- Provider credentials (`MORPH_API_KEY` env)
+- Vendored schema mirrors (`SCHEMA_MIRROR.json` digests)
+
+## Adversaries
+
+| Adversary | Goal |
+|-----------|------|
+| Malicious or malformed incident / profile / manifest inputs | Crash parsers, inject secret values, upgrade claim classes without checks |
+| Cross-branch pollution | Leak files, resume dirty state, forge equal diffs from absent data |
+| Compromised or dishonest Morph control plane | Misreport execute/measure results; treated as untrusted for claim upgrades |
+| Supply-chain schema drift | Silently change PCS/PIP contracts without digest gate failure |
+| CI / operator misconfiguration | Accidental live Morph spend or credential commit |
+
+## Trust boundaries
+
+1. **CLI / parsers** — untrusted JSON inputs; validated before the scheduler kernel
+2. **Scheduler kernel** — receives typed profile / manifest only; owns isolation and retries
+3. **Providers** — infrastructure only (clone / execute / measure / teardown / capability probes); Morph is untrusted for claim upgrades
+4. **Evidence emitters** — observational labels only unless a check actually ran; vendored schema validation fail-closed before write
+5. **External CLIs** (`pcs`, `post-incident`, OVK) — optional deeper gates; never implied by default offline CI
+
+## Threats and mitigations
+
+| Threat | Mitigation |
+|--------|------------|
+| Cross-branch file bleed | Per-branch FS roots; clone from canonical snapshot only; isolation tests |
+| Dirty retry resume | Retry tears down and reclones from original snapshot |
+| Secret materialization | Profile rejects secret values; redaction on logs; adversarial tests |
+| Hermeticity bypass | `supports(mode)` preflight fail-closed before clone |
+| Claim upgrade without check | Elevated claim classes require PF-Core trace + CLI; receipt status never invents `ReplayValidated` |
+| Supply-chain schema drift | `SCHEMA_MIRROR.json` digest CI gate + always-on vendored instance validation |
+| Live cloud in default CI | Offline `LocalFakeProvider` default; Morph opt-in via secret |
+| HTTP callback confusion | `--http-callback` rejected fail-closed (unimplemented / out of scope) |
+| Absent coerced to equal | Differential reports use three-valued absence semantics |
+
+## Non-goals / non-claims
+
+- Protecting against a compromised Morph control plane beyond documented assumptions
+- Formal verification of guest workloads
+- Campaign-level verifier assurance
+- Causality, remediation ranking, or environment-semantics generation
+- Production-grade non-repudiation or signing beyond recorded digests
+
+Full product non-claims: [NON_CLAIMS.md](../../NON_CLAIMS.md).
+Disclosure: [SECURITY.md](../../SECURITY.md).
diff --git a/examples/hermetic-branch-n/README.md b/examples/hermetic-branch-n/README.md
new file mode 100644
index 0000000..c1a111b
--- /dev/null
+++ b/examples/hermetic-branch-n/README.md
@@ -0,0 +1,76 @@
+# Hermetic branch-N example
+
+Offline reproducible example (16 branches) for `LocalFakeProvider`.
+No Morph credentials required.
+
+Inputs are generated by `python scripts/generate_fixtures.py` from the repo root
+(snapshot payload, profile digests, interventions, and manifest stay in sync with `fixtures/branch/`).
+
+## Prerequisites
+
+```bash
+pip install -e ".[dev]"
+```
+
+## Run
+
+From the **repository root**:
+
+```bash
+replay-runner branch \
+ --incident examples/hermetic-branch-n/incident_bundle.json \
+ --snapshot examples/hermetic-branch-n/snapshot_ref.json \
+ --snapshot-payload examples/hermetic-branch-n/snapshot.payload \
+ --interventions examples/hermetic-branch-n/interventions \
+ --execution-profile examples/hermetic-branch-n/execution_profile.json \
+ --manifest examples/hermetic-branch-n/manifest.json \
+ --parallel 16 \
+ --provider local-fake \
+ --out ./branches-out
+```
+
+From this directory (paths relative to cwd):
+
+```bash
+replay-runner branch \
+ --incident incident_bundle.json \
+ --snapshot snapshot_ref.json \
+ --snapshot-payload snapshot.payload \
+ --interventions interventions \
+ --execution-profile execution_profile.json \
+ --manifest manifest.json \
+ --parallel 16 \
+ --provider local-fake \
+ --out ./branches-out
+```
+
+Expect exit 0 with 16 `complete` branches. Partial branches fail the process unless `--allow-partial` is set.
+
+## Outputs
+
+`branches-out/summary.json` plus per-branch packs under `branches-out//`
+(`runtime_receipt.json`, PIP reports, `resource_report.json`, `status.json`, digests, logs).
+See the Evidence layout section in the root [README.md](../../README.md).
+
+## Differential sample
+
+```bash
+replay-runner diff \
+ --branches ./branches-out \
+ --pairs branch-00,branch-01 \
+ --out ./diffs-out
+```
+
+## Checksums
+
+From the repository root:
+
+```bash
+python scripts/release_bundle.py
+```
+
+Writes `release-bundle/checksums.json` covering this example tree, domain/PCS/PIP schemas, and PIP linkage fixtures.
+
+## Non-claims
+
+Outputs are observational / differential only. See [NON_CLAIMS.md](../../NON_CLAIMS.md).
diff --git a/examples/hermetic-branch-n/execution_profile.json b/examples/hermetic-branch-n/execution_profile.json
new file mode 100644
index 0000000..7df8c56
--- /dev/null
+++ b/examples/hermetic-branch-n/execution_profile.json
@@ -0,0 +1,61 @@
+{
+ "budgets": {
+ "accelerator_count": 0,
+ "api_call_budget": 0,
+ "cpu_cores": 2,
+ "memory_mib": 1024,
+ "storage_mib": 2048,
+ "token_budget": 0,
+ "wall_time_ms": 60000
+ },
+ "clock_policy": {
+ "fixed_epoch_ms": 1700000000000,
+ "mode": "frozen"
+ },
+ "container_image_digests": [
+ "sha256:6105d6cc76af400325e94d588ce511be5bfdbb73b437dc51eca43917d7a43e3d"
+ ],
+ "dependency_lock_digest": "sha256:0c030586945fe504b604ecc2e875c38ede400cd5cd73da9730302162e6b02c6f",
+ "environment": {
+ "profile_name": "hermetic-demo",
+ "profile_version": "1.0.0"
+ },
+ "integrity": {
+ "artifact_digest": "sha256:130d846c41fc26fa002d68e1a854faaeabeaf429069807437b2c14f761eada07",
+ "canonicalization_version": "v1"
+ },
+ "network_policy": {
+ "allowlist_hosts": [],
+ "mode": "no_network",
+ "partner_local_paths": [],
+ "synthetic_dependency_digests": []
+ },
+ "os_runtime": {
+ "os": "linux",
+ "runtime": "python3.11"
+ },
+ "output_retention": {
+ "mode": "retain",
+ "retention_days": 30
+ },
+ "profile_digest": "sha256:130d846c41fc26fa002d68e1a854faaeabeaf429069807437b2c14f761eada07",
+ "profile_id": "profile-demo",
+ "random_seed_policy": {
+ "seed": 42
+ },
+ "schema_version": "mrr.ExecutionProfile.v1",
+ "secret_refs": [
+ {
+ "purpose": "optional-morph",
+ "secret_ref": "vault/morph/api"
+ }
+ ],
+ "snapshot": {
+ "snapshot_digest": "sha256:9047a18b16b4542b7388faa6d97a47a79c793d0fa280013bb0587fa35f932785",
+ "snapshot_id": "snap-demo"
+ },
+ "source_commit": "b3018d790e3e7c622b28641c0230a61bb3b78955",
+ "tool_versions": {
+ "replay-runner": "0.1.0"
+ }
+}
diff --git a/examples/hermetic-branch-n/incident_bundle.json b/examples/hermetic-branch-n/incident_bundle.json
new file mode 100644
index 0000000..27cfb43
--- /dev/null
+++ b/examples/hermetic-branch-n/incident_bundle.json
@@ -0,0 +1,4 @@
+{
+ "incident_id": "inc-demo",
+ "incident_digest": "sha256:cad4eaad4523d1b78d9de32b78b3a873380aad8a75c2646fffe921e182f01907"
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-00.json b/examples/hermetic-branch-n/interventions/branch-00.json
new file mode 100644
index 0000000..1533b6b
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-00.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-00",
+ "intervention_id": "iv-00",
+ "artifact_paths": [
+ "branch-00.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-00"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-01.json b/examples/hermetic-branch-n/interventions/branch-01.json
new file mode 100644
index 0000000..59e1454
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-01.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-01",
+ "intervention_id": "iv-01",
+ "artifact_paths": [
+ "branch-01.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-01"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-02.json b/examples/hermetic-branch-n/interventions/branch-02.json
new file mode 100644
index 0000000..f2abc91
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-02.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-02",
+ "intervention_id": "iv-02",
+ "artifact_paths": [
+ "branch-02.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-02"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-03.json b/examples/hermetic-branch-n/interventions/branch-03.json
new file mode 100644
index 0000000..ecd6edc
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-03.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-03",
+ "intervention_id": "iv-03",
+ "artifact_paths": [
+ "branch-03.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-03"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-04.json b/examples/hermetic-branch-n/interventions/branch-04.json
new file mode 100644
index 0000000..00cebf6
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-04.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-04",
+ "intervention_id": "iv-04",
+ "artifact_paths": [
+ "branch-04.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-04"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-05.json b/examples/hermetic-branch-n/interventions/branch-05.json
new file mode 100644
index 0000000..8524123
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-05.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-05",
+ "intervention_id": "iv-05",
+ "artifact_paths": [
+ "branch-05.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-05"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-06.json b/examples/hermetic-branch-n/interventions/branch-06.json
new file mode 100644
index 0000000..1b53696
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-06.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-06",
+ "intervention_id": "iv-06",
+ "artifact_paths": [
+ "branch-06.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-06"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-07.json b/examples/hermetic-branch-n/interventions/branch-07.json
new file mode 100644
index 0000000..cd69b03
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-07.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-07",
+ "intervention_id": "iv-07",
+ "artifact_paths": [
+ "branch-07.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-07"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-08.json b/examples/hermetic-branch-n/interventions/branch-08.json
new file mode 100644
index 0000000..1de90ba
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-08.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-08",
+ "intervention_id": "iv-08",
+ "artifact_paths": [
+ "branch-08.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-08"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-09.json b/examples/hermetic-branch-n/interventions/branch-09.json
new file mode 100644
index 0000000..246816d
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-09.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-09",
+ "intervention_id": "iv-09",
+ "artifact_paths": [
+ "branch-09.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-09"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-10.json b/examples/hermetic-branch-n/interventions/branch-10.json
new file mode 100644
index 0000000..df08633
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-10.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-10",
+ "intervention_id": "iv-10",
+ "artifact_paths": [
+ "branch-10.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-10"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-11.json b/examples/hermetic-branch-n/interventions/branch-11.json
new file mode 100644
index 0000000..0b809d1
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-11.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-11",
+ "intervention_id": "iv-11",
+ "artifact_paths": [
+ "branch-11.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-11"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-12.json b/examples/hermetic-branch-n/interventions/branch-12.json
new file mode 100644
index 0000000..6462095
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-12.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-12",
+ "intervention_id": "iv-12",
+ "artifact_paths": [
+ "branch-12.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-12"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-13.json b/examples/hermetic-branch-n/interventions/branch-13.json
new file mode 100644
index 0000000..4b182e7
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-13.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-13",
+ "intervention_id": "iv-13",
+ "artifact_paths": [
+ "branch-13.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-13"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-14.json b/examples/hermetic-branch-n/interventions/branch-14.json
new file mode 100644
index 0000000..41dffbe
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-14.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-14",
+ "intervention_id": "iv-14",
+ "artifact_paths": [
+ "branch-14.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-14"
+ ]
+}
diff --git a/examples/hermetic-branch-n/interventions/branch-15.json b/examples/hermetic-branch-n/interventions/branch-15.json
new file mode 100644
index 0000000..578674e
--- /dev/null
+++ b/examples/hermetic-branch-n/interventions/branch-15.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-15",
+ "intervention_id": "iv-15",
+ "artifact_paths": [
+ "branch-15.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-15"
+ ]
+}
diff --git a/examples/hermetic-branch-n/manifest.json b/examples/hermetic-branch-n/manifest.json
new file mode 100644
index 0000000..e1e12c3
--- /dev/null
+++ b/examples/hermetic-branch-n/manifest.json
@@ -0,0 +1,250 @@
+{
+ "schema_version": "mrr.BranchReplayManifest.v1",
+ "manifest_id": "manifest-demo",
+ "incident_ref": {
+ "incident_id": "inc-demo",
+ "incident_digest": "sha256:cad4eaad4523d1b78d9de32b78b3a873380aad8a75c2646fffe921e182f01907"
+ },
+ "snapshot_ref": {
+ "snapshot_id": "snap-demo",
+ "snapshot_digest": "sha256:9047a18b16b4542b7388faa6d97a47a79c793d0fa280013bb0587fa35f932785"
+ },
+ "execution_profile_digest": "sha256:130d846c41fc26fa002d68e1a854faaeabeaf429069807437b2c14f761eada07",
+ "branches": [
+ {
+ "branch_id": "branch-00",
+ "intervention": {
+ "intervention_id": "iv-00",
+ "artifact_paths": [
+ "branch-00.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-00"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-01",
+ "intervention": {
+ "intervention_id": "iv-01",
+ "artifact_paths": [
+ "branch-01.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-01"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-02",
+ "intervention": {
+ "intervention_id": "iv-02",
+ "artifact_paths": [
+ "branch-02.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-02"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-03",
+ "intervention": {
+ "intervention_id": "iv-03",
+ "artifact_paths": [
+ "branch-03.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-03"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-04",
+ "intervention": {
+ "intervention_id": "iv-04",
+ "artifact_paths": [
+ "branch-04.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-04"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-05",
+ "intervention": {
+ "intervention_id": "iv-05",
+ "artifact_paths": [
+ "branch-05.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-05"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-06",
+ "intervention": {
+ "intervention_id": "iv-06",
+ "artifact_paths": [
+ "branch-06.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-06"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-07",
+ "intervention": {
+ "intervention_id": "iv-07",
+ "artifact_paths": [
+ "branch-07.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-07"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-08",
+ "intervention": {
+ "intervention_id": "iv-08",
+ "artifact_paths": [
+ "branch-08.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-08"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-09",
+ "intervention": {
+ "intervention_id": "iv-09",
+ "artifact_paths": [
+ "branch-09.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-09"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-10",
+ "intervention": {
+ "intervention_id": "iv-10",
+ "artifact_paths": [
+ "branch-10.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-10"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-11",
+ "intervention": {
+ "intervention_id": "iv-11",
+ "artifact_paths": [
+ "branch-11.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-11"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-12",
+ "intervention": {
+ "intervention_id": "iv-12",
+ "artifact_paths": [
+ "branch-12.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-12"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-13",
+ "intervention": {
+ "intervention_id": "iv-13",
+ "artifact_paths": [
+ "branch-13.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-13"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-14",
+ "intervention": {
+ "intervention_id": "iv-14",
+ "artifact_paths": [
+ "branch-14.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-14"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-15",
+ "intervention": {
+ "intervention_id": "iv-15",
+ "artifact_paths": [
+ "branch-15.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-15"
+ ],
+ "claim_class": "RuntimeObserved"
+ }
+ ],
+ "retry_policy": {
+ "max_retries": 1,
+ "retry_on": [
+ "TIMEOUT",
+ "ERROR"
+ ]
+ },
+ "cancellation_policy": {
+ "cancel_others_on_failure": false
+ },
+ "branch_independence": true
+}
diff --git a/examples/hermetic-branch-n/snapshot.payload b/examples/hermetic-branch-n/snapshot.payload
new file mode 100644
index 0000000..c19da04
--- /dev/null
+++ b/examples/hermetic-branch-n/snapshot.payload
@@ -0,0 +1 @@
+mrr-local-fake-snapshot-v1
diff --git a/examples/hermetic-branch-n/snapshot_ref.json b/examples/hermetic-branch-n/snapshot_ref.json
new file mode 100644
index 0000000..9c133d2
--- /dev/null
+++ b/examples/hermetic-branch-n/snapshot_ref.json
@@ -0,0 +1,4 @@
+{
+ "snapshot_id": "snap-demo",
+ "snapshot_digest": "sha256:9047a18b16b4542b7388faa6d97a47a79c793d0fa280013bb0587fa35f932785"
+}
diff --git a/fixtures/README.md b/fixtures/README.md
new file mode 100644
index 0000000..09a8692
--- /dev/null
+++ b/fixtures/README.md
@@ -0,0 +1,29 @@
+# Fixtures
+
+Offline fixtures for unit, hermetic, adversarial, and schema-conformance tests.
+Regenerate shared trees with:
+
+```bash
+python scripts/generate_fixtures.py
+```
+
+## Layout
+
+| Path | Role |
+|------|------|
+| `profile/valid_profile.json` | Valid `mrr.ExecutionProfile.v1` |
+| `profile/invalid_*.json` | Fail-closed profile validation cases (missing pin, secret value, schema version) |
+| `branch/` | Hermetic branch-N inputs: incident, snapshot ref + payload, 16 interventions, manifest |
+| `branch/manifest_digest_drift.json` | Negative case for digest drift |
+| `pip/valid_replay_linkage/` | Joint PIP lineage + Morph replay report instances |
+
+Empty reserved directories (`diff/`, `hermeticity/`, `pcs/`, `resources/`) may appear locally; authoritative committed fixtures are the paths above.
+
+## Validation
+
+```bash
+python scripts/validate_vendored_instances.py
+replay-runner profile validate --file fixtures/profile/valid_profile.json
+```
+
+Public fixtures use synthetic digests only. Do not add credentials or partner plaintext.
diff --git a/fixtures/branch/incident_bundle.json b/fixtures/branch/incident_bundle.json
new file mode 100644
index 0000000..27cfb43
--- /dev/null
+++ b/fixtures/branch/incident_bundle.json
@@ -0,0 +1,4 @@
+{
+ "incident_id": "inc-demo",
+ "incident_digest": "sha256:cad4eaad4523d1b78d9de32b78b3a873380aad8a75c2646fffe921e182f01907"
+}
diff --git a/fixtures/branch/interventions/branch-00.json b/fixtures/branch/interventions/branch-00.json
new file mode 100644
index 0000000..1533b6b
--- /dev/null
+++ b/fixtures/branch/interventions/branch-00.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-00",
+ "intervention_id": "iv-00",
+ "artifact_paths": [
+ "branch-00.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-00"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-01.json b/fixtures/branch/interventions/branch-01.json
new file mode 100644
index 0000000..59e1454
--- /dev/null
+++ b/fixtures/branch/interventions/branch-01.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-01",
+ "intervention_id": "iv-01",
+ "artifact_paths": [
+ "branch-01.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-01"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-02.json b/fixtures/branch/interventions/branch-02.json
new file mode 100644
index 0000000..f2abc91
--- /dev/null
+++ b/fixtures/branch/interventions/branch-02.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-02",
+ "intervention_id": "iv-02",
+ "artifact_paths": [
+ "branch-02.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-02"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-03.json b/fixtures/branch/interventions/branch-03.json
new file mode 100644
index 0000000..ecd6edc
--- /dev/null
+++ b/fixtures/branch/interventions/branch-03.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-03",
+ "intervention_id": "iv-03",
+ "artifact_paths": [
+ "branch-03.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-03"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-04.json b/fixtures/branch/interventions/branch-04.json
new file mode 100644
index 0000000..00cebf6
--- /dev/null
+++ b/fixtures/branch/interventions/branch-04.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-04",
+ "intervention_id": "iv-04",
+ "artifact_paths": [
+ "branch-04.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-04"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-05.json b/fixtures/branch/interventions/branch-05.json
new file mode 100644
index 0000000..8524123
--- /dev/null
+++ b/fixtures/branch/interventions/branch-05.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-05",
+ "intervention_id": "iv-05",
+ "artifact_paths": [
+ "branch-05.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-05"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-06.json b/fixtures/branch/interventions/branch-06.json
new file mode 100644
index 0000000..1b53696
--- /dev/null
+++ b/fixtures/branch/interventions/branch-06.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-06",
+ "intervention_id": "iv-06",
+ "artifact_paths": [
+ "branch-06.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-06"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-07.json b/fixtures/branch/interventions/branch-07.json
new file mode 100644
index 0000000..cd69b03
--- /dev/null
+++ b/fixtures/branch/interventions/branch-07.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-07",
+ "intervention_id": "iv-07",
+ "artifact_paths": [
+ "branch-07.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-07"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-08.json b/fixtures/branch/interventions/branch-08.json
new file mode 100644
index 0000000..1de90ba
--- /dev/null
+++ b/fixtures/branch/interventions/branch-08.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-08",
+ "intervention_id": "iv-08",
+ "artifact_paths": [
+ "branch-08.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-08"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-09.json b/fixtures/branch/interventions/branch-09.json
new file mode 100644
index 0000000..246816d
--- /dev/null
+++ b/fixtures/branch/interventions/branch-09.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-09",
+ "intervention_id": "iv-09",
+ "artifact_paths": [
+ "branch-09.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-09"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-10.json b/fixtures/branch/interventions/branch-10.json
new file mode 100644
index 0000000..df08633
--- /dev/null
+++ b/fixtures/branch/interventions/branch-10.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-10",
+ "intervention_id": "iv-10",
+ "artifact_paths": [
+ "branch-10.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-10"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-11.json b/fixtures/branch/interventions/branch-11.json
new file mode 100644
index 0000000..0b809d1
--- /dev/null
+++ b/fixtures/branch/interventions/branch-11.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-11",
+ "intervention_id": "iv-11",
+ "artifact_paths": [
+ "branch-11.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-11"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-12.json b/fixtures/branch/interventions/branch-12.json
new file mode 100644
index 0000000..6462095
--- /dev/null
+++ b/fixtures/branch/interventions/branch-12.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-12",
+ "intervention_id": "iv-12",
+ "artifact_paths": [
+ "branch-12.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-12"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-13.json b/fixtures/branch/interventions/branch-13.json
new file mode 100644
index 0000000..4b182e7
--- /dev/null
+++ b/fixtures/branch/interventions/branch-13.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-13",
+ "intervention_id": "iv-13",
+ "artifact_paths": [
+ "branch-13.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-13"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-14.json b/fixtures/branch/interventions/branch-14.json
new file mode 100644
index 0000000..41dffbe
--- /dev/null
+++ b/fixtures/branch/interventions/branch-14.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-14",
+ "intervention_id": "iv-14",
+ "artifact_paths": [
+ "branch-14.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-14"
+ ]
+}
diff --git a/fixtures/branch/interventions/branch-15.json b/fixtures/branch/interventions/branch-15.json
new file mode 100644
index 0000000..578674e
--- /dev/null
+++ b/fixtures/branch/interventions/branch-15.json
@@ -0,0 +1,11 @@
+{
+ "branch_id": "branch-15",
+ "intervention_id": "iv-15",
+ "artifact_paths": [
+ "branch-15.json"
+ ],
+ "action_sequence": [
+ "replay",
+ "branch-15"
+ ]
+}
diff --git a/fixtures/branch/manifest.json b/fixtures/branch/manifest.json
new file mode 100644
index 0000000..e1e12c3
--- /dev/null
+++ b/fixtures/branch/manifest.json
@@ -0,0 +1,250 @@
+{
+ "schema_version": "mrr.BranchReplayManifest.v1",
+ "manifest_id": "manifest-demo",
+ "incident_ref": {
+ "incident_id": "inc-demo",
+ "incident_digest": "sha256:cad4eaad4523d1b78d9de32b78b3a873380aad8a75c2646fffe921e182f01907"
+ },
+ "snapshot_ref": {
+ "snapshot_id": "snap-demo",
+ "snapshot_digest": "sha256:9047a18b16b4542b7388faa6d97a47a79c793d0fa280013bb0587fa35f932785"
+ },
+ "execution_profile_digest": "sha256:130d846c41fc26fa002d68e1a854faaeabeaf429069807437b2c14f761eada07",
+ "branches": [
+ {
+ "branch_id": "branch-00",
+ "intervention": {
+ "intervention_id": "iv-00",
+ "artifact_paths": [
+ "branch-00.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-00"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-01",
+ "intervention": {
+ "intervention_id": "iv-01",
+ "artifact_paths": [
+ "branch-01.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-01"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-02",
+ "intervention": {
+ "intervention_id": "iv-02",
+ "artifact_paths": [
+ "branch-02.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-02"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-03",
+ "intervention": {
+ "intervention_id": "iv-03",
+ "artifact_paths": [
+ "branch-03.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-03"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-04",
+ "intervention": {
+ "intervention_id": "iv-04",
+ "artifact_paths": [
+ "branch-04.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-04"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-05",
+ "intervention": {
+ "intervention_id": "iv-05",
+ "artifact_paths": [
+ "branch-05.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-05"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-06",
+ "intervention": {
+ "intervention_id": "iv-06",
+ "artifact_paths": [
+ "branch-06.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-06"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-07",
+ "intervention": {
+ "intervention_id": "iv-07",
+ "artifact_paths": [
+ "branch-07.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-07"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-08",
+ "intervention": {
+ "intervention_id": "iv-08",
+ "artifact_paths": [
+ "branch-08.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-08"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-09",
+ "intervention": {
+ "intervention_id": "iv-09",
+ "artifact_paths": [
+ "branch-09.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-09"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-10",
+ "intervention": {
+ "intervention_id": "iv-10",
+ "artifact_paths": [
+ "branch-10.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-10"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-11",
+ "intervention": {
+ "intervention_id": "iv-11",
+ "artifact_paths": [
+ "branch-11.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-11"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-12",
+ "intervention": {
+ "intervention_id": "iv-12",
+ "artifact_paths": [
+ "branch-12.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-12"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-13",
+ "intervention": {
+ "intervention_id": "iv-13",
+ "artifact_paths": [
+ "branch-13.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-13"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-14",
+ "intervention": {
+ "intervention_id": "iv-14",
+ "artifact_paths": [
+ "branch-14.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-14"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-15",
+ "intervention": {
+ "intervention_id": "iv-15",
+ "artifact_paths": [
+ "branch-15.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-15"
+ ],
+ "claim_class": "RuntimeObserved"
+ }
+ ],
+ "retry_policy": {
+ "max_retries": 1,
+ "retry_on": [
+ "TIMEOUT",
+ "ERROR"
+ ]
+ },
+ "cancellation_policy": {
+ "cancel_others_on_failure": false
+ },
+ "branch_independence": true
+}
diff --git a/fixtures/branch/manifest_digest_drift.json b/fixtures/branch/manifest_digest_drift.json
new file mode 100644
index 0000000..18d9dc4
--- /dev/null
+++ b/fixtures/branch/manifest_digest_drift.json
@@ -0,0 +1,250 @@
+{
+ "schema_version": "mrr.BranchReplayManifest.v1",
+ "manifest_id": "manifest-demo",
+ "incident_ref": {
+ "incident_id": "inc-demo",
+ "incident_digest": "sha256:cad4eaad4523d1b78d9de32b78b3a873380aad8a75c2646fffe921e182f01907"
+ },
+ "snapshot_ref": {
+ "snapshot_id": "snap-demo",
+ "snapshot_digest": "sha256:9047a18b16b4542b7388faa6d97a47a79c793d0fa280013bb0587fa35f932785"
+ },
+ "execution_profile_digest": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
+ "branches": [
+ {
+ "branch_id": "branch-00",
+ "intervention": {
+ "intervention_id": "iv-00",
+ "artifact_paths": [
+ "branch-00.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-00"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-01",
+ "intervention": {
+ "intervention_id": "iv-01",
+ "artifact_paths": [
+ "branch-01.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-01"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-02",
+ "intervention": {
+ "intervention_id": "iv-02",
+ "artifact_paths": [
+ "branch-02.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-02"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-03",
+ "intervention": {
+ "intervention_id": "iv-03",
+ "artifact_paths": [
+ "branch-03.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-03"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-04",
+ "intervention": {
+ "intervention_id": "iv-04",
+ "artifact_paths": [
+ "branch-04.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-04"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-05",
+ "intervention": {
+ "intervention_id": "iv-05",
+ "artifact_paths": [
+ "branch-05.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-05"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-06",
+ "intervention": {
+ "intervention_id": "iv-06",
+ "artifact_paths": [
+ "branch-06.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-06"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-07",
+ "intervention": {
+ "intervention_id": "iv-07",
+ "artifact_paths": [
+ "branch-07.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-07"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-08",
+ "intervention": {
+ "intervention_id": "iv-08",
+ "artifact_paths": [
+ "branch-08.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-08"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-09",
+ "intervention": {
+ "intervention_id": "iv-09",
+ "artifact_paths": [
+ "branch-09.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-09"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-10",
+ "intervention": {
+ "intervention_id": "iv-10",
+ "artifact_paths": [
+ "branch-10.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-10"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-11",
+ "intervention": {
+ "intervention_id": "iv-11",
+ "artifact_paths": [
+ "branch-11.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-11"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-12",
+ "intervention": {
+ "intervention_id": "iv-12",
+ "artifact_paths": [
+ "branch-12.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-12"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-13",
+ "intervention": {
+ "intervention_id": "iv-13",
+ "artifact_paths": [
+ "branch-13.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-13"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-14",
+ "intervention": {
+ "intervention_id": "iv-14",
+ "artifact_paths": [
+ "branch-14.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-14"
+ ],
+ "claim_class": "RuntimeObserved"
+ },
+ {
+ "branch_id": "branch-15",
+ "intervention": {
+ "intervention_id": "iv-15",
+ "artifact_paths": [
+ "branch-15.json"
+ ]
+ },
+ "action_sequence": [
+ "replay",
+ "branch-15"
+ ],
+ "claim_class": "RuntimeObserved"
+ }
+ ],
+ "retry_policy": {
+ "max_retries": 1,
+ "retry_on": [
+ "TIMEOUT",
+ "ERROR"
+ ]
+ },
+ "cancellation_policy": {
+ "cancel_others_on_failure": false
+ },
+ "branch_independence": true
+}
diff --git a/fixtures/branch/snapshot.payload b/fixtures/branch/snapshot.payload
new file mode 100644
index 0000000..c19da04
--- /dev/null
+++ b/fixtures/branch/snapshot.payload
@@ -0,0 +1 @@
+mrr-local-fake-snapshot-v1
diff --git a/fixtures/branch/snapshot_ref.json b/fixtures/branch/snapshot_ref.json
new file mode 100644
index 0000000..9c133d2
--- /dev/null
+++ b/fixtures/branch/snapshot_ref.json
@@ -0,0 +1,4 @@
+{
+ "snapshot_id": "snap-demo",
+ "snapshot_digest": "sha256:9047a18b16b4542b7388faa6d97a47a79c793d0fa280013bb0587fa35f932785"
+}
diff --git a/fixtures/pip/valid_replay_linkage/lineage_bundle.json b/fixtures/pip/valid_replay_linkage/lineage_bundle.json
new file mode 100644
index 0000000..072da5d
--- /dev/null
+++ b/fixtures/pip/valid_replay_linkage/lineage_bundle.json
@@ -0,0 +1,64 @@
+{
+ "schema_version": "pip.LineageBundle.v1",
+ "bundle_id": "lin-mrr-001",
+ "incident_id": "inc-demo",
+ "nodes": [
+ {
+ "node_id": "n0",
+ "artifact_digest": "sha256:289c58f19e2b126468dae353a63ea5b6d41161e8ba7ae3eada37643fb0e2b66f",
+ "role": "source"
+ },
+ {
+ "node_id": "n1",
+ "artifact_digest": "sha256:8b82eb9475941110dc4bb6225594876cfabbf4676dafbeebe764c9021352df1a",
+ "role": "intermediate"
+ },
+ {
+ "node_id": "n2",
+ "artifact_digest": "sha256:985a5e7b20cd925ba65f3c58921a7b5d4343000f7f19e2cf6532320d8fea0ca5",
+ "role": "replay"
+ }
+ ],
+ "edges": [
+ {
+ "from_node_id": "n0",
+ "to_node_id": "n1",
+ "transformation_id": "t-norm"
+ },
+ {
+ "from_node_id": "n1",
+ "to_node_id": "n2",
+ "transformation_id": "t-replay"
+ }
+ ],
+ "transformations": [
+ {
+ "schema_version": "pip.TransformationRecord.v1",
+ "transformation_id": "t-norm",
+ "transformation_type": "normalization",
+ "input_artifact_digests": [
+ "sha256:289c58f19e2b126468dae353a63ea5b6d41161e8ba7ae3eada37643fb0e2b66f"
+ ],
+ "output_artifact_digest": "sha256:8b82eb9475941110dc4bb6225594876cfabbf4676dafbeebe764c9021352df1a",
+ "implementation_id": "morph-replay-runner",
+ "implementation_version": "0.1.0",
+ "container_digest": "sha256:a42d519714d616e9411dbceec4b52808bd6b1ee53e6f6497a281d655357d8b71",
+ "source_commit": "b3018d790e3e7c622b28641c0230a61bb3b78955",
+ "record_digest": "sha256:3be6704ac3c5387837079e6cc4875ede904bbd16b3937028ab6284290e2e71c7"
+ },
+ {
+ "schema_version": "pip.TransformationRecord.v1",
+ "transformation_id": "t-replay",
+ "transformation_type": "synthetic_variant_derivation",
+ "input_artifact_digests": [
+ "sha256:8b82eb9475941110dc4bb6225594876cfabbf4676dafbeebe764c9021352df1a"
+ ],
+ "output_artifact_digest": "sha256:985a5e7b20cd925ba65f3c58921a7b5d4343000f7f19e2cf6532320d8fea0ca5",
+ "implementation_id": "morph-replay-runner",
+ "implementation_version": "0.1.0",
+ "container_digest": "sha256:a42d519714d616e9411dbceec4b52808bd6b1ee53e6f6497a281d655357d8b71",
+ "source_commit": "b3018d790e3e7c622b28641c0230a61bb3b78955",
+ "record_digest": "sha256:363f28a2711ee54e606b3cd632208e4e4713d0e526b00966fb137449b32bebeb"
+ }
+ ]
+}
diff --git a/fixtures/pip/valid_replay_linkage/morph_replay_report.json b/fixtures/pip/valid_replay_linkage/morph_replay_report.json
new file mode 100644
index 0000000..19a7d4b
--- /dev/null
+++ b/fixtures/pip/valid_replay_linkage/morph_replay_report.json
@@ -0,0 +1,16 @@
+{
+ "schema_version": "pip.MorphReplayReport.v1",
+ "replay_id": "morph-replay-mrr-001",
+ "branch_id": "branch-00",
+ "replay_identity_digest": "sha256:985a5e7b20cd925ba65f3c58921a7b5d4343000f7f19e2cf6532320d8fea0ca5",
+ "input_artifact_digests": [
+ "sha256:8b82eb9475941110dc4bb6225594876cfabbf4676dafbeebe764c9021352df1a"
+ ],
+ "output_artifact_digests": [
+ "sha256:985a5e7b20cd925ba65f3c58921a7b5d4343000f7f19e2cf6532320d8fea0ca5"
+ ],
+ "status": "recorded",
+ "lineage_node_ids": [
+ "n2"
+ ]
+}
diff --git a/fixtures/profile/invalid_missing_pin.json b/fixtures/profile/invalid_missing_pin.json
new file mode 100644
index 0000000..c17eaf2
--- /dev/null
+++ b/fixtures/profile/invalid_missing_pin.json
@@ -0,0 +1,55 @@
+{
+ "schema_version": "mrr.ExecutionProfile.v1",
+ "profile_id": "profile-demo",
+ "snapshot": {
+ "snapshot_id": "snap-demo",
+ "snapshot_digest": "sha256:9047a18b16b4542b7388faa6d97a47a79c793d0fa280013bb0587fa35f932785"
+ },
+ "container_image_digests": [
+ "sha256:6105d6cc76af400325e94d588ce511be5bfdbb73b437dc51eca43917d7a43e3d"
+ ],
+ "environment": {
+ "profile_name": "hermetic-demo",
+ "profile_version": "1.0.0"
+ },
+ "os_runtime": {
+ "os": "linux",
+ "runtime": "python3.11"
+ },
+ "network_policy": {
+ "mode": "no_network"
+ },
+ "clock_policy": {
+ "mode": "frozen",
+ "fixed_epoch_ms": 1700000000000
+ },
+ "random_seed_policy": {
+ "seed": 42
+ },
+ "tool_versions": {
+ "replay-runner": "0.1.0"
+ },
+ "budgets": {
+ "cpu_cores": 2,
+ "memory_mib": 1024,
+ "accelerator_count": 0,
+ "storage_mib": 2048,
+ "wall_time_ms": 60000,
+ "token_budget": 0,
+ "api_call_budget": 0
+ },
+ "secret_refs": [
+ {
+ "secret_ref": "vault/morph/api",
+ "purpose": "optional-morph"
+ }
+ ],
+ "output_retention": {
+ "mode": "retain",
+ "retention_days": 30
+ },
+ "source_commit": "b3018d790e3e7c622b28641c0230a61bb3b78955",
+ "integrity": {
+ "canonicalization_version": "v1"
+ }
+}
diff --git a/fixtures/profile/invalid_schema_version.json b/fixtures/profile/invalid_schema_version.json
new file mode 100644
index 0000000..5816242
--- /dev/null
+++ b/fixtures/profile/invalid_schema_version.json
@@ -0,0 +1,56 @@
+{
+ "schema_version": "mrr.ExecutionProfile.v999",
+ "profile_id": "profile-demo",
+ "snapshot": {
+ "snapshot_id": "snap-demo",
+ "snapshot_digest": "sha256:9047a18b16b4542b7388faa6d97a47a79c793d0fa280013bb0587fa35f932785"
+ },
+ "container_image_digests": [
+ "sha256:6105d6cc76af400325e94d588ce511be5bfdbb73b437dc51eca43917d7a43e3d"
+ ],
+ "environment": {
+ "profile_name": "hermetic-demo",
+ "profile_version": "1.0.0"
+ },
+ "os_runtime": {
+ "os": "linux",
+ "runtime": "python3.11"
+ },
+ "dependency_lock_digest": "sha256:0c030586945fe504b604ecc2e875c38ede400cd5cd73da9730302162e6b02c6f",
+ "network_policy": {
+ "mode": "no_network"
+ },
+ "clock_policy": {
+ "mode": "frozen",
+ "fixed_epoch_ms": 1700000000000
+ },
+ "random_seed_policy": {
+ "seed": 42
+ },
+ "tool_versions": {
+ "replay-runner": "0.1.0"
+ },
+ "budgets": {
+ "cpu_cores": 2,
+ "memory_mib": 1024,
+ "accelerator_count": 0,
+ "storage_mib": 2048,
+ "wall_time_ms": 60000,
+ "token_budget": 0,
+ "api_call_budget": 0
+ },
+ "secret_refs": [
+ {
+ "secret_ref": "vault/morph/api",
+ "purpose": "optional-morph"
+ }
+ ],
+ "output_retention": {
+ "mode": "retain",
+ "retention_days": 30
+ },
+ "source_commit": "b3018d790e3e7c622b28641c0230a61bb3b78955",
+ "integrity": {
+ "canonicalization_version": "v1"
+ }
+}
diff --git a/fixtures/profile/invalid_secret_value.json b/fixtures/profile/invalid_secret_value.json
new file mode 100644
index 0000000..7d48481
--- /dev/null
+++ b/fixtures/profile/invalid_secret_value.json
@@ -0,0 +1,57 @@
+{
+ "schema_version": "mrr.ExecutionProfile.v1",
+ "profile_id": "profile-demo",
+ "snapshot": {
+ "snapshot_id": "snap-demo",
+ "snapshot_digest": "sha256:9047a18b16b4542b7388faa6d97a47a79c793d0fa280013bb0587fa35f932785"
+ },
+ "container_image_digests": [
+ "sha256:6105d6cc76af400325e94d588ce511be5bfdbb73b437dc51eca43917d7a43e3d"
+ ],
+ "environment": {
+ "profile_name": "hermetic-demo",
+ "profile_version": "1.0.0"
+ },
+ "os_runtime": {
+ "os": "linux",
+ "runtime": "python3.11"
+ },
+ "dependency_lock_digest": "sha256:0c030586945fe504b604ecc2e875c38ede400cd5cd73da9730302162e6b02c6f",
+ "network_policy": {
+ "mode": "no_network"
+ },
+ "clock_policy": {
+ "mode": "frozen",
+ "fixed_epoch_ms": 1700000000000
+ },
+ "random_seed_policy": {
+ "seed": 42
+ },
+ "tool_versions": {
+ "replay-runner": "0.1.0"
+ },
+ "budgets": {
+ "cpu_cores": 2,
+ "memory_mib": 1024,
+ "accelerator_count": 0,
+ "storage_mib": 2048,
+ "wall_time_ms": 60000,
+ "token_budget": 0,
+ "api_call_budget": 0
+ },
+ "secret_refs": [
+ {
+ "secret_ref": "vault/morph/api",
+ "purpose": "optional-morph"
+ }
+ ],
+ "output_retention": {
+ "mode": "retain",
+ "retention_days": 30
+ },
+ "source_commit": "b3018d790e3e7c622b28641c0230a61bb3b78955",
+ "integrity": {
+ "canonicalization_version": "v1"
+ },
+ "api_key": "sk-secret-value"
+}
diff --git a/fixtures/profile/valid_profile.json b/fixtures/profile/valid_profile.json
new file mode 100644
index 0000000..7df8c56
--- /dev/null
+++ b/fixtures/profile/valid_profile.json
@@ -0,0 +1,61 @@
+{
+ "budgets": {
+ "accelerator_count": 0,
+ "api_call_budget": 0,
+ "cpu_cores": 2,
+ "memory_mib": 1024,
+ "storage_mib": 2048,
+ "token_budget": 0,
+ "wall_time_ms": 60000
+ },
+ "clock_policy": {
+ "fixed_epoch_ms": 1700000000000,
+ "mode": "frozen"
+ },
+ "container_image_digests": [
+ "sha256:6105d6cc76af400325e94d588ce511be5bfdbb73b437dc51eca43917d7a43e3d"
+ ],
+ "dependency_lock_digest": "sha256:0c030586945fe504b604ecc2e875c38ede400cd5cd73da9730302162e6b02c6f",
+ "environment": {
+ "profile_name": "hermetic-demo",
+ "profile_version": "1.0.0"
+ },
+ "integrity": {
+ "artifact_digest": "sha256:130d846c41fc26fa002d68e1a854faaeabeaf429069807437b2c14f761eada07",
+ "canonicalization_version": "v1"
+ },
+ "network_policy": {
+ "allowlist_hosts": [],
+ "mode": "no_network",
+ "partner_local_paths": [],
+ "synthetic_dependency_digests": []
+ },
+ "os_runtime": {
+ "os": "linux",
+ "runtime": "python3.11"
+ },
+ "output_retention": {
+ "mode": "retain",
+ "retention_days": 30
+ },
+ "profile_digest": "sha256:130d846c41fc26fa002d68e1a854faaeabeaf429069807437b2c14f761eada07",
+ "profile_id": "profile-demo",
+ "random_seed_policy": {
+ "seed": 42
+ },
+ "schema_version": "mrr.ExecutionProfile.v1",
+ "secret_refs": [
+ {
+ "purpose": "optional-morph",
+ "secret_ref": "vault/morph/api"
+ }
+ ],
+ "snapshot": {
+ "snapshot_digest": "sha256:9047a18b16b4542b7388faa6d97a47a79c793d0fa280013bb0587fa35f932785",
+ "snapshot_id": "snap-demo"
+ },
+ "source_commit": "b3018d790e3e7c622b28641c0230a61bb3b78955",
+ "tool_versions": {
+ "replay-runner": "0.1.0"
+ }
+}
diff --git a/morph_replay_runner.egg-info/PKG-INFO b/morph_replay_runner.egg-info/PKG-INFO
deleted file mode 100644
index 68c4e8a..0000000
--- a/morph_replay_runner.egg-info/PKG-INFO
+++ /dev/null
@@ -1,35 +0,0 @@
-Metadata-Version: 2.4
-Name: morph-replay-runner
-Version: 0.1.0
-Summary: CLI tool for running TRACE-REPLAY-KIT bundles with branch-N parallelism on Morph Cloud
-Author-email: SentinelOps-CI
-License: Apache-2.0
-Project-URL: Homepage, https://github.com/SentinelOps-CI/morph-replay-runner
-Project-URL: Repository, https://github.com/SentinelOps-CI/morph-replay-runner
-Project-URL: Issues, https://github.com/SentinelOps-CI/morph-replay-runner/issues
-Classifier: Development Status :: 3 - Alpha
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: Apache Software License
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.8
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
-Classifier: Programming Language :: Python :: 3.12
-Requires-Python: >=3.8
-Description-Content-Type: text/markdown
-Requires-Dist: morphcloud>=0.1.91
-Requires-Dist: click>=8.0.0
-Requires-Dist: pydantic>=2.0.0
-Requires-Dist: rich>=13.0.0
-Requires-Dist: aiofiles>=23.0.0
-Requires-Dist: asyncio-mqtt>=0.16.0
-Provides-Extra: dev
-Requires-Dist: pytest>=7.0.0; extra == "dev"
-Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
-Requires-Dist: black>=23.0.0; extra == "dev"
-Requires-Dist: isort>=5.12.0; extra == "dev"
-Requires-Dist: mypy>=1.0.0; extra == "dev"
-Requires-Dist: ruff>=0.1.0; extra == "dev"
-
-
diff --git a/morph_replay_runner.egg-info/SOURCES.txt b/morph_replay_runner.egg-info/SOURCES.txt
deleted file mode 100644
index 9c79c99..0000000
--- a/morph_replay_runner.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,13 +0,0 @@
-README.md
-pyproject.toml
-morph_replay_runner.egg-info/PKG-INFO
-morph_replay_runner.egg-info/SOURCES.txt
-morph_replay_runner.egg-info/dependency_links.txt
-morph_replay_runner.egg-info/entry_points.txt
-morph_replay_runner.egg-info/requires.txt
-morph_replay_runner.egg-info/top_level.txt
-runner/__init__.py
-runner/core.py
-runner/core_fixed.py
-runner/main.py
-runner/models.py
\ No newline at end of file
diff --git a/morph_replay_runner.egg-info/dependency_links.txt b/morph_replay_runner.egg-info/dependency_links.txt
deleted file mode 100644
index 8b13789..0000000
--- a/morph_replay_runner.egg-info/dependency_links.txt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/morph_replay_runner.egg-info/entry_points.txt b/morph_replay_runner.egg-info/entry_points.txt
deleted file mode 100644
index 3b2bb09..0000000
--- a/morph_replay_runner.egg-info/entry_points.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-[console_scripts]
-replay-runner = runner.main:main
diff --git a/morph_replay_runner.egg-info/requires.txt b/morph_replay_runner.egg-info/requires.txt
deleted file mode 100644
index 1c3bb7b..0000000
--- a/morph_replay_runner.egg-info/requires.txt
+++ /dev/null
@@ -1,14 +0,0 @@
-morphcloud>=0.1.91
-click>=8.0.0
-pydantic>=2.0.0
-rich>=13.0.0
-aiofiles>=23.0.0
-asyncio-mqtt>=0.16.0
-
-[dev]
-pytest>=7.0.0
-pytest-asyncio>=0.21.0
-black>=23.0.0
-isort>=5.12.0
-mypy>=1.0.0
-ruff>=0.1.0
diff --git a/morph_replay_runner.egg-info/top_level.txt b/morph_replay_runner.egg-info/top_level.txt
deleted file mode 100644
index 6ad2d8a..0000000
--- a/morph_replay_runner.egg-info/top_level.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-runner
-schemas
diff --git a/pyproject.toml b/pyproject.toml
index f68b432..8e82fcc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,41 +5,66 @@ build-backend = "setuptools.build_meta"
[project]
name = "morph-replay-runner"
version = "0.1.0"
-description = "CLI tool for running TRACE-REPLAY-KIT bundles with branch-N parallelism on Morph Cloud"
+description = "Hermetic branch-N execution backend for SentinelOps-CI with observational PCS/PIP evidence"
authors = [
{name = "SentinelOps-CI", email = "team@sentinelops.ci"}
]
readme = "README.md"
license = {text = "Apache-2.0"}
-requires-python = ">=3.8"
+requires-python = ">=3.9"
+keywords = [
+ "sentinelops",
+ "hermetic",
+ "branch-n",
+ "pcs",
+ "pip",
+ "morph",
+ "replay",
+ "evidence",
+]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
+ "Intended Audience :: Science/Research",
"License :: OSI Approved :: Apache Software License",
+ "Operating System :: OS Independent",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Topic :: Security",
+ "Topic :: Software Development :: Quality Assurance",
+ "Typing :: Typed",
]
dependencies = [
"morphcloud>=0.1.91",
"click>=8.0.0",
"pydantic>=2.0.0",
"rich>=13.0.0",
- "aiofiles>=23.0.0",
- "asyncio-mqtt>=0.16.0",
+ "jsonschema[format]>=4.18.0",
+ "referencing>=0.30.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
- "pytest-asyncio>=0.21.0",
"black>=23.0.0",
"isort>=5.12.0",
"mypy>=1.0.0",
"ruff>=0.1.0",
+ "types-jsonschema>=4.0.0",
+ "pip-audit>=2.6.0",
+]
+pcs = [
+ "pcs-core>=0.1.0",
+]
+pip = [
+ "post-incident>=0.1.0",
+]
+ovk = [
+ "open-verification-kernel==1.2.1",
]
[project.scripts]
@@ -49,27 +74,75 @@ replay-runner = "runner.main:main"
Homepage = "https://github.com/SentinelOps-CI/morph-replay-runner"
Repository = "https://github.com/SentinelOps-CI/morph-replay-runner"
Issues = "https://github.com/SentinelOps-CI/morph-replay-runner/issues"
+Changelog = "https://github.com/SentinelOps-CI/morph-replay-runner/blob/main/CHANGELOG.md"
+Documentation = "https://github.com/SentinelOps-CI/morph-replay-runner#readme"
+"Security Policy" = "https://github.com/SentinelOps-CI/morph-replay-runner/blob/main/SECURITY.md"
[tool.setuptools.packages.find]
where = ["."]
-include = ["runner*", "schemas*"]
+include = ["runner*"]
+
+[tool.setuptools.package-data]
+runner = ["py.typed"]
[tool.black]
line-length = 88
-target-version = ['py38']
+target-version = ['py39']
+extend-exclude = '''
+/(
+ \.mypy_cache
+ | \.ruff_cache
+ | \.pytest_cache
+ | branches-smoke
+ | release-bundle
+ | evidence
+)/
+'''
[tool.isort]
profile = "black"
line_length = 88
+skip_glob = [".mypy_cache/*", "branches-smoke/*", "release-bundle/*"]
[tool.mypy]
-python_version = "3.8"
+python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
+warn_unused_ignores = true
disallow_untyped_defs = true
+disallow_any_generics = true
+no_implicit_optional = true
+strict = true
+warn_redundant_casts = true
+mypy_path = "."
+packages = ["runner"]
+
+[[tool.mypy.overrides]]
+module = ["morphcloud.*"]
+ignore_missing_imports = true
[tool.ruff]
-target-version = "py38"
+target-version = "py39"
line-length = 88
-select = ["E", "F", "B", "I", "N", "UP", "W", "C4", "SIM", "ARG", "PIE", "T20", "Q", "RSE", "RET", "SLF", "SLOT", "TCH", "TID", "TCH", "ARG", "PIE", "SIM", "LOG", "RUF"]
-ignore = ["E501", "B008", "C901"]
+extend-exclude = [".mypy_cache", "branches-smoke", "release-bundle", "evidence"]
+
+[tool.ruff.lint]
+select = ["E", "F", "B", "I", "UP", "W", "C4", "SIM", "ARG", "PIE", "RUF"]
+ignore = [
+ "E501",
+ "B008",
+ "C901",
+ "UP007", # Optional[] kept for explicit 3.9-friendly annotations
+ "UP006",
+]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+addopts = "-q"
+asyncio_default_fixture_loop_scope = "function"
+filterwarnings = [
+ "error",
+ "ignore::DeprecationWarning:morphcloud.*",
+ "ignore::DeprecationWarning:pydantic.*",
+ "ignore::pytest.PytestDeprecationWarning",
+]
diff --git a/requirements.txt b/requirements.txt
index 22efc0f..0ab3254 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,8 @@
+# Runtime pins for editable / Docker installs.
+# Canonical metadata lives in pyproject.toml.
morphcloud>=0.1.91
click>=8.0.0
pydantic>=2.0.0
rich>=13.0.0
-aiofiles>=23.0.0
-asyncio-mqtt>=0.16.0
+jsonschema[format]>=4.18.0
+referencing>=0.30.0
diff --git a/runner/__init__.py b/runner/__init__.py
index 2cf9dd4..2ac7de1 100644
--- a/runner/__init__.py
+++ b/runner/__init__.py
@@ -1,4 +1,4 @@
-"""Morph Replay Runner - CLI tool for running TRACE-REPLAY-KIT bundles with branch-N parallelism on Morph Cloud."""
+"""Hermetic branch-N replay runner with observational PCS/PIP evidence."""
__version__ = "0.1.0"
__author__ = "SentinelOps-CI"
diff --git a/runner/accounting/__init__.py b/runner/accounting/__init__.py
new file mode 100644
index 0000000..3348f5e
--- /dev/null
+++ b/runner/accounting/__init__.py
@@ -0,0 +1,15 @@
+"""Accounting package."""
+
+from runner.accounting.report import (
+ BudgetExceededError,
+ assert_unavailable_not_fabricated,
+ enforce_budgets,
+ sample_to_report,
+)
+
+__all__ = [
+ "BudgetExceededError",
+ "assert_unavailable_not_fabricated",
+ "enforce_budgets",
+ "sample_to_report",
+]
diff --git a/runner/accounting/report.py b/runner/accounting/report.py
new file mode 100644
index 0000000..79c3f5f
--- /dev/null
+++ b/runner/accounting/report.py
@@ -0,0 +1,75 @@
+"""Resource accounting: never invent unavailable metrics."""
+
+from __future__ import annotations
+
+from typing import Any, Optional
+
+from runner.profile.schema import Budgets
+from runner.providers.base import MetricValue, ResourceSample
+
+
+class BudgetExceededError(RuntimeError):
+ """Observed metric exceeds profile budget."""
+
+
+def metric_to_dict(metric: MetricValue) -> dict[str, Any]:
+ return {
+ "value": metric.value,
+ "unit": metric.unit,
+ "availability": metric.availability,
+ "source": metric.source,
+ }
+
+
+def sample_to_report(branch_id: str, sample: ResourceSample) -> dict[str, Any]:
+ return {
+ "schema_version": "mrr.ResourceReport.v1",
+ "branch_id": branch_id,
+ "metrics": {
+ key: metric_to_dict(value) for key, value in sorted(sample.metrics.items())
+ },
+ "budget_violations": [],
+ }
+
+
+def _observed_number(metric: Optional[MetricValue]) -> Optional[float]:
+ if metric is None or metric.availability != "observed":
+ return None
+ if isinstance(metric.value, (int, float)):
+ return float(metric.value)
+ return None
+
+
+def enforce_budgets(sample: ResourceSample, budgets: Budgets) -> list[str]:
+ """Return violation messages; raises if any observed metric exceeds budget."""
+ violations: list[str] = []
+ checks = [
+ ("wall_time_ms", budgets.wall_time_ms),
+ ("memory_mib", budgets.memory_mib),
+ ("model_tokens", budgets.token_budget),
+ ("external_api_calls", budgets.api_call_budget),
+ ]
+ for key, limit in checks:
+ metric = sample.metrics.get(key)
+ value = _observed_number(metric)
+ if value is None:
+ continue
+ if value > float(limit):
+ violations.append(f"{key} observed {value} exceeds budget {limit}")
+ storage = _observed_number(sample.metrics.get("storage_bytes"))
+ if storage is not None and storage > budgets.storage_mib * 1024 * 1024:
+ violations.append(
+ f"storage_bytes observed {storage} exceeds budget "
+ f"{budgets.storage_mib} MiB"
+ )
+ if violations:
+ raise BudgetExceededError("; ".join(violations))
+ return violations
+
+
+def assert_unavailable_not_fabricated(sample: ResourceSample) -> None:
+ for name, metric in sample.metrics.items():
+ if metric.availability == "unavailable" and metric.value is not None:
+ raise ValueError(
+ f"metric {name} marked unavailable but has value {metric.value!r}"
+ )
diff --git a/runner/branch/__init__.py b/runner/branch/__init__.py
new file mode 100644
index 0000000..5059307
--- /dev/null
+++ b/runner/branch/__init__.py
@@ -0,0 +1,19 @@
+"""Branch package."""
+
+from runner.branch.manifest import (
+ BranchReplayManifest,
+ ManifestValidationError,
+ load_manifest,
+ validate_manifest,
+)
+from runner.branch.scheduler import BranchScheduler, SchedulerConfig, run_branch_job
+
+__all__ = [
+ "BranchReplayManifest",
+ "BranchScheduler",
+ "ManifestValidationError",
+ "SchedulerConfig",
+ "load_manifest",
+ "run_branch_job",
+ "validate_manifest",
+]
diff --git a/runner/branch/manifest.py b/runner/branch/manifest.py
new file mode 100644
index 0000000..c575bc5
--- /dev/null
+++ b/runner/branch/manifest.py
@@ -0,0 +1,85 @@
+"""BranchReplayManifest models."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Any, Literal, Optional
+
+from pydantic import BaseModel, Field
+
+SCHEMA_VERSION = "mrr.BranchReplayManifest.v1"
+
+
+class ManifestValidationError(ValueError):
+ """Fail-closed manifest validation."""
+
+
+class IncidentRef(BaseModel):
+ incident_id: str
+ incident_digest: str = Field(..., pattern=r"^sha256:[a-f0-9]{64}$")
+
+
+class SnapshotRefModel(BaseModel):
+ snapshot_id: str
+ snapshot_digest: str = Field(..., pattern=r"^sha256:[a-f0-9]{64}$")
+
+
+class InterventionSpec(BaseModel):
+ intervention_id: str
+ artifact_paths: list[str] = Field(default_factory=list)
+ digest: Optional[str] = Field(default=None, pattern=r"^sha256:[a-f0-9]{64}$")
+
+
+class BranchSpec(BaseModel):
+ branch_id: str
+ intervention: InterventionSpec
+ action_sequence: list[str] = Field(..., min_length=1)
+ policy_checkpoint: Optional[str] = None
+ verifier_profiles: list[str] = Field(default_factory=list)
+ expected_output_classes: list[str] = Field(default_factory=list)
+ claim_class: Literal[
+ "runtime_observed",
+ "RuntimeObserved",
+ "RuntimeChecked",
+ "ReplayValidated",
+ ] = "RuntimeObserved"
+
+
+class RetryPolicy(BaseModel):
+ max_retries: int = Field(..., ge=0)
+ retry_on: list[Literal["TIMEOUT", "ERROR", "FAILED"]] = Field(default_factory=list)
+
+
+class CancellationPolicy(BaseModel):
+ cancel_others_on_failure: bool = False
+
+
+class BranchReplayManifest(BaseModel):
+ schema_version: Literal["mrr.BranchReplayManifest.v1"] = (
+ "mrr.BranchReplayManifest.v1"
+ )
+ manifest_id: str
+ incident_ref: IncidentRef
+ snapshot_ref: SnapshotRefModel
+ execution_profile_digest: str = Field(..., pattern=r"^sha256:[a-f0-9]{64}$")
+ branches: list[BranchSpec] = Field(..., min_length=1)
+ retry_policy: RetryPolicy
+ cancellation_policy: CancellationPolicy
+ branch_independence: bool = True
+
+
+def load_manifest(path: Path | str) -> BranchReplayManifest:
+ raw = json.loads(Path(path).read_text(encoding="utf-8"))
+ return validate_manifest(raw)
+
+
+def validate_manifest(raw: dict[str, Any]) -> BranchReplayManifest:
+ if raw.get("schema_version") != SCHEMA_VERSION:
+ raise ManifestValidationError(
+ f"unknown schema_version {raw.get('schema_version')!r}"
+ )
+ try:
+ return BranchReplayManifest.model_validate(raw)
+ except Exception as exc:
+ raise ManifestValidationError(str(exc)) from exc
diff --git a/runner/branch/scheduler.py b/runner/branch/scheduler.py
new file mode 100644
index 0000000..50649dc
--- /dev/null
+++ b/runner/branch/scheduler.py
@@ -0,0 +1,498 @@
+"""Isolated branch-N scheduler."""
+
+from __future__ import annotations
+
+import json
+import shutil
+import traceback
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from dataclasses import dataclass
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Any, Optional
+
+from runner.accounting.report import (
+ BudgetExceededError,
+ assert_unavailable_not_fabricated,
+ enforce_budgets,
+ sample_to_report,
+)
+from runner.branch.manifest import (
+ BranchReplayManifest,
+ BranchSpec,
+ CancellationPolicy,
+ IncidentRef,
+ InterventionSpec,
+ RetryPolicy,
+ SnapshotRefModel,
+ load_manifest,
+)
+from runner.evidence import (
+ assert_no_secret_material,
+ emit_morph_replay_report,
+ emit_runtime_receipt,
+ emit_transformation_record,
+ maybe_pf_core_replay_trace,
+ redact_text,
+ terminal_state_commitment,
+ write_json,
+)
+from runner.hashing import sha256_file, sha256_text
+from runner.hermeticity.modes import (
+ EnforcementStatus,
+ HermeticityError,
+ HermeticityEvidence,
+ policy_from_profile_network,
+)
+from runner.profile.schema import ExecutionProfile, load_profile, profile_digest
+from runner.providers.base import (
+ ExecRequest,
+ ExecResult,
+ ExecStatus,
+ ExecutionProvider,
+ SnapshotHandle,
+ SnapshotRef,
+ TeardownReceipt,
+)
+
+
+def _iso_now() -> str:
+ return (
+ datetime.now(timezone.utc)
+ .replace(microsecond=0)
+ .isoformat()
+ .replace("+00:00", "Z")
+ )
+
+
+@dataclass
+class SchedulerConfig:
+ parallel: int
+ out_dir: Path
+ interventions_dir: Optional[Path] = None
+ container_digest: str = (
+ "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ )
+
+
+class BranchScheduler:
+ """Clone-from-canonical-snapshot branch-N executor."""
+
+ def __init__(
+ self,
+ provider: ExecutionProvider,
+ profile: ExecutionProfile,
+ manifest: BranchReplayManifest,
+ config: SchedulerConfig,
+ ) -> None:
+ self.provider = provider
+ self.profile = profile
+ self.manifest = manifest
+ self.config = config
+ digest = profile.profile_digest or profile_digest(profile)
+ if digest != manifest.execution_profile_digest:
+ raise HermeticityError(
+ "execution profile digest drift vs manifest pin: "
+ f"{digest} != {manifest.execution_profile_digest}"
+ )
+ self.profile_digest = digest
+ self.policy = policy_from_profile_network(profile.network_policy)
+
+ def preflight(self) -> HermeticityEvidence:
+ mode = self.policy.mode
+ if not self.provider.supports(mode):
+ evidence = HermeticityEvidence(
+ mode=mode,
+ status=EnforcementStatus.UNSUPPORTED,
+ policy_digest=self.policy.policy_digest(),
+ probe_result={"provider": self.provider.name, "supports": False},
+ message=f"provider {self.provider.name} cannot enforce {mode.value}",
+ )
+ raise HermeticityError(evidence.message)
+ return HermeticityEvidence(
+ mode=mode,
+ status=EnforcementStatus.ENFORCED,
+ policy_digest=self.policy.policy_digest(),
+ probe_result={"provider": self.provider.name, "supports": True},
+ message="hermeticity mode supported",
+ )
+
+ def run(self) -> dict[str, Any]:
+ evidence = self.preflight()
+ self.config.out_dir.mkdir(parents=True, exist_ok=True)
+ write_json(
+ self.config.out_dir / "hermeticity_evidence.json", evidence.to_dict()
+ )
+
+ snapshot = self.provider.lookup_snapshot(
+ SnapshotRef(
+ snapshot_id=self.manifest.snapshot_ref.snapshot_id,
+ snapshot_digest=self.manifest.snapshot_ref.snapshot_digest,
+ )
+ )
+ write_json(
+ self.config.out_dir / "snapshot_ref.json",
+ {
+ "snapshot_id": snapshot.snapshot_id,
+ "snapshot_digest": snapshot.snapshot_digest,
+ },
+ )
+
+ results: dict[str, Any] = {}
+ branches = list(self.manifest.branches)
+ with ThreadPoolExecutor(max_workers=max(1, self.config.parallel)) as pool:
+ futures = {
+ pool.submit(self._run_branch, spec, snapshot): spec for spec in branches
+ }
+ for future in as_completed(futures):
+ spec = futures[future]
+ try:
+ result = future.result()
+ except Exception as exc:
+ result = {
+ "branch_id": spec.branch_id,
+ "status": "partial",
+ "terminal": "ERROR",
+ "error": str(exc),
+ "traceback": traceback.format_exc(),
+ }
+ err_dir = self.config.out_dir / spec.branch_id
+ err_dir.mkdir(parents=True, exist_ok=True)
+ write_json(err_dir / "status.json", result)
+ results[spec.branch_id] = result
+ if (
+ self.manifest.cancellation_policy.cancel_others_on_failure
+ and result.get("terminal") in {"FAILED", "ERROR", "TIMEOUT"}
+ ):
+ cancel = getattr(self.provider, "cancel", None)
+ if callable(cancel):
+ for other in branches:
+ if other.branch_id != spec.branch_id:
+ cancel(other.branch_id)
+
+ summary = {
+ "schema_version": "mrr.BranchRunSummary.v1",
+ "manifest_id": self.manifest.manifest_id,
+ "profile_digest": self.profile_digest,
+ "snapshot_digest": snapshot.snapshot_digest,
+ "branches": results,
+ "non_claims": [
+ "Observational/differential evidence only.",
+ "No causality or remediation ranking.",
+ ],
+ }
+ write_json(self.config.out_dir / "summary.json", summary)
+ return summary
+
+ def _resolve_intervention_paths(self, spec: BranchSpec) -> list[str]:
+ paths: list[str] = []
+ for rel in spec.intervention.artifact_paths:
+ candidate = Path(rel)
+ if not candidate.is_file() and self.config.interventions_dir is not None:
+ candidate = self.config.interventions_dir / rel
+ if not candidate.is_file():
+ raise FileNotFoundError(f"intervention artifact missing: {rel}")
+ paths.append(str(candidate.resolve()))
+ return paths
+
+ def _run_branch(self, spec: BranchSpec, snapshot: SnapshotHandle) -> dict[str, Any]:
+ branch_out = self.config.out_dir / spec.branch_id
+ if branch_out.exists():
+ shutil.rmtree(branch_out)
+ branch_out.mkdir(parents=True)
+
+ profile_payload = self.profile.model_dump(mode="json", exclude_none=True)
+ profile_payload["profile_digest"] = self.profile_digest
+ write_json(branch_out / "execution_profile.json", profile_payload)
+ write_json(
+ branch_out / "snapshot_ref.json",
+ {
+ "snapshot_id": snapshot.snapshot_id,
+ "snapshot_digest": snapshot.snapshot_digest,
+ },
+ )
+
+ intervention_paths = self._resolve_intervention_paths(spec)
+ intervention_record = {
+ "intervention_id": spec.intervention.intervention_id,
+ "artifact_paths": [Path(p).name for p in intervention_paths],
+ "digests": {Path(p).name: sha256_file(p) for p in intervention_paths},
+ }
+ write_json(branch_out / "intervention_record.json", intervention_record)
+
+ started_at = _iso_now()
+ attempt = 0
+ exec_result: Optional[ExecResult] = None
+ resource_report: dict[str, Any] = {}
+ teardown: Optional[TeardownReceipt] = None
+
+ while attempt <= self.manifest.retry_policy.max_retries:
+ handle = self.provider.clone(
+ snapshot, branch_id=f"{spec.branch_id}__attempt{attempt}"
+ )
+ try:
+ request = ExecRequest(
+ command=list(spec.action_sequence),
+ timeout_ms=self.profile.budgets.wall_time_ms,
+ env={},
+ intervention_paths=intervention_paths,
+ )
+ exec_result = self.provider.execute(handle, request)
+ sample = self.provider.measure(handle)
+ assert_unavailable_not_fabricated(sample)
+ try:
+ enforce_budgets(sample, self.profile.budgets)
+ violations: list[str] = []
+ except BudgetExceededError as exc:
+ violations = str(exc).split("; ")
+ exec_result = ExecResult(
+ status=ExecStatus.FAILED,
+ exit_code=3,
+ stdout=exec_result.stdout,
+ stderr=str(exc),
+ wall_time_ms=exec_result.wall_time_ms,
+ terminal_state={
+ "reason": "BUDGET_EXCEEDED",
+ "error": str(exc),
+ },
+ )
+ resource_report = sample_to_report(spec.branch_id, sample)
+ resource_report["budget_violations"] = violations
+ finally:
+ teardown = self.provider.teardown(handle)
+
+ assert exec_result is not None
+ if exec_result.status == ExecStatus.PASSED:
+ break
+ if exec_result.status.value not in self.manifest.retry_policy.retry_on:
+ break
+ attempt += 1
+
+ assert exec_result is not None
+ assert teardown is not None
+ ended_at = _iso_now()
+ terminal_status = exec_result.status.value
+ status_label = (
+ "complete" if exec_result.status == ExecStatus.PASSED else "partial"
+ )
+
+ terminal = dict(exec_result.terminal_state or {})
+ terminal.setdefault("status", terminal_status)
+ terminal_commitment = terminal_state_commitment(terminal)
+ write_json(branch_out / "terminal_state.json", terminal)
+ write_json(
+ branch_out / "terminal_commitment.json", {"digest": terminal_commitment}
+ )
+ write_json(branch_out / "resource_report.json", resource_report)
+ write_json(
+ branch_out / "teardown_receipt.json",
+ {
+ "branch_id": teardown.branch_id,
+ "cleaned": teardown.cleaned,
+ "residual_paths": teardown.residual_paths,
+ "secrets_scrubbed": teardown.secrets_scrubbed,
+ "details": teardown.details,
+ },
+ )
+
+ logs = redact_text(
+ f"STDOUT:\n{exec_result.stdout}\n\nSTDERR:\n{exec_result.stderr}\n"
+ )
+ (branch_out / "logs.txt").write_text(logs, encoding="utf-8")
+ log_digest = sha256_text(logs)
+
+ input_hashes = {
+ "execution_profile": self.profile_digest,
+ "snapshot": snapshot.snapshot_digest,
+ "intervention": sha256_text(
+ json.dumps(intervention_record, sort_keys=True)
+ ),
+ }
+ output_hashes = {
+ "terminal_state": terminal_commitment,
+ "logs": log_digest,
+ }
+
+ from runner.evidence.pcs import EvidenceError, receipt_status_for_claim_class
+ from runner.evidence.validate import SchemaValidationError, validate_instance
+
+ pf_check_ran = False
+ if spec.claim_class in {"RuntimeChecked", "ReplayValidated"}:
+ # Fail closed: elevated claim classes require PF-Core trace + CLI.
+ maybe_pf_core_replay_trace(
+ claim_class=spec.claim_class,
+ trace_path=branch_out / "pf_trace.json",
+ out_dir=branch_out / "pf_core",
+ )
+ pf_check_ran = True
+ receipt_status = receipt_status_for_claim_class(
+ spec.claim_class, check_ran=pf_check_ran
+ )
+ claim_semantics = (
+ "runtime_checked"
+ if receipt_status == "RuntimeChecked"
+ else "runtime_observed"
+ )
+
+ try:
+ validate_instance("mrr.ResourceReport.v1", resource_report)
+ except SchemaValidationError as exc:
+ raise EvidenceError(str(exc)) from exc
+
+ receipt = emit_runtime_receipt(
+ receipt_id=f"receipt-{spec.branch_id}",
+ run_id=f"run-{self.manifest.manifest_id}-{spec.branch_id}",
+ started_at=started_at,
+ ended_at=ended_at,
+ run_outcome="passed" if status_label == "complete" else "failed",
+ final_reason_code=terminal_status,
+ source_commit=self.profile.source_commit,
+ input_hashes=input_hashes,
+ output_hashes=output_hashes,
+ environment={
+ "provider": self.provider.name,
+ "claim_class": spec.claim_class,
+ "claim_semantics": claim_semantics,
+ },
+ status=receipt_status,
+ )
+ write_json(branch_out / "runtime_receipt.json", receipt)
+
+ morph_report = emit_morph_replay_report(
+ replay_id=f"replay-{self.manifest.manifest_id}",
+ branch_id=spec.branch_id,
+ replay_identity_digest=terminal_commitment,
+ input_artifact_digests=[self.profile_digest, snapshot.snapshot_digest],
+ output_artifact_digests=[terminal_commitment],
+ status="recorded" if status_label == "complete" else "indeterminate",
+ )
+ write_json(branch_out / "morph_replay_report.json", morph_report)
+
+ transform = emit_transformation_record(
+ transformation_id=f"t-{spec.branch_id}",
+ transformation_type="synthetic_variant_derivation",
+ input_artifact_digests=[snapshot.snapshot_digest],
+ output_artifact_digest=terminal_commitment,
+ implementation_id="morph-replay-runner",
+ implementation_version="0.1.0",
+ container_digest=self.config.container_digest,
+ source_commit=self.profile.source_commit,
+ notes="branch replay transform",
+ )
+ write_json(branch_out / "transformation_record.json", transform)
+
+ branch_report = {
+ "branch_id": spec.branch_id,
+ "process_action": list(spec.action_sequence),
+ "authorization": None,
+ "side_effect": None,
+ "reward": None,
+ "verifier_decision": None,
+ "unresolved_external_dependency": None,
+ "attempts": attempt + 1,
+ "teardown_cleaned": teardown.cleaned,
+ }
+ write_json(branch_out / "branch_report.json", branch_report)
+
+ digests = {
+ "execution_profile": self.profile_digest,
+ "runtime_receipt": receipt["signature_or_digest"],
+ "terminal_state": terminal_commitment,
+ "logs": log_digest,
+ }
+ write_json(branch_out / "digests.json", digests)
+ status_payload = {
+ "status": status_label,
+ "terminal": terminal_status,
+ "branch_id": spec.branch_id,
+ "complete_evidence": status_label == "complete",
+ }
+ write_json(branch_out / "status.json", status_payload)
+ assert_no_secret_material(status_payload)
+ assert_no_secret_material(receipt)
+
+ return {
+ "branch_id": spec.branch_id,
+ "status": status_label,
+ "terminal": terminal_status,
+ "digests": digests,
+ "attempts": attempt + 1,
+ }
+
+
+def run_branch_job(
+ *,
+ provider: ExecutionProvider,
+ profile_path: Path,
+ manifest_path: Optional[Path] = None,
+ incident_path: Optional[Path] = None,
+ snapshot_path: Optional[Path] = None,
+ interventions_dir: Optional[Path] = None,
+ parallel: int = 4,
+ out_dir: Path,
+ manifest: Optional[BranchReplayManifest] = None,
+) -> dict[str, Any]:
+ profile = load_profile(profile_path)
+ if manifest is None:
+ if manifest_path is not None:
+ manifest = load_manifest(manifest_path)
+ else:
+ if incident_path is None or snapshot_path is None:
+ raise ValueError("manifest or incident+snapshot required")
+ if interventions_dir is None:
+ raise ValueError("--interventions required when manifest omitted")
+ incident = json.loads(incident_path.read_text(encoding="utf-8"))
+ snapshot = json.loads(snapshot_path.read_text(encoding="utf-8"))
+ branch_specs: list[BranchSpec] = []
+ for path in sorted(interventions_dir.glob("*.json")):
+ data = json.loads(path.read_text(encoding="utf-8"))
+ branch_specs.append(
+ BranchSpec(
+ branch_id=data["branch_id"],
+ intervention=InterventionSpec(
+ intervention_id=data.get("intervention_id", path.stem),
+ artifact_paths=data.get("artifact_paths", [path.name]),
+ ),
+ action_sequence=data.get("action_sequence", ["replay"]),
+ )
+ )
+ if not branch_specs:
+ raise ValueError("no intervention specs found")
+ digest = profile.profile_digest or profile_digest(profile)
+ manifest = BranchReplayManifest(
+ manifest_id=incident.get("incident_id", "incident"),
+ incident_ref=IncidentRef(
+ incident_id=incident["incident_id"],
+ incident_digest=incident["incident_digest"],
+ ),
+ snapshot_ref=SnapshotRefModel(
+ snapshot_id=snapshot["snapshot_id"],
+ snapshot_digest=snapshot["snapshot_digest"],
+ ),
+ execution_profile_digest=digest,
+ branches=branch_specs,
+ retry_policy=RetryPolicy(max_retries=0, retry_on=["TIMEOUT", "ERROR"]),
+ cancellation_policy=CancellationPolicy(cancel_others_on_failure=False),
+ branch_independence=True,
+ )
+
+ from runner.providers.local_fake import LocalFakeProvider
+
+ if isinstance(provider, LocalFakeProvider):
+ if provider.hermeticity is None:
+ provider.hermeticity = policy_from_profile_network(profile.network_policy)
+ provider.fixed_epoch_ms = profile.clock_policy.fixed_epoch_ms
+ provider.seed = profile.random_seed_policy.seed
+
+ scheduler = BranchScheduler(
+ provider,
+ profile,
+ manifest,
+ SchedulerConfig(
+ parallel=parallel,
+ out_dir=out_dir,
+ interventions_dir=interventions_dir,
+ container_digest=profile.container_image_digests[0],
+ ),
+ )
+ return scheduler.run()
diff --git a/runner/core.py b/runner/core.py
index fadf331..ed6a45a 100644
--- a/runner/core.py
+++ b/runner/core.py
@@ -1,15 +1,16 @@
-"""Core runner logic for executing replay bundles on Morph Cloud."""
+"""Core runner logic for executing replay bundles on Morph Cloud."""
+
+from __future__ import annotations
import asyncio
import json
import os
import time
-from typing import List
+from datetime import datetime, timezone
+from typing import Any, Union
from morphcloud.api import MorphCloudClient
-# Standard exceptions for error handling
-
from .models import (
ExecutionResult,
ExecutionSummary,
@@ -21,18 +22,17 @@
class ReplayRunner:
"""Main runner class for executing replay bundles on Morph Cloud."""
- def __init__(self, config: RunnerConfig):
+ def __init__(self, config: RunnerConfig) -> None:
"""Initialize the replay runner."""
self.config = config
self.client = MorphCloudClient()
self.summary = ExecutionSummary()
- # Ensure output directories exist
os.makedirs(f"{config.output_directory}/certs", exist_ok=True)
os.makedirs(f"{config.output_directory}/logs", exist_ok=True)
os.makedirs(f"{config.output_directory}/reports", exist_ok=True)
- def run_sync(self, bundle_paths: List[str]) -> ExecutionSummary:
+ def run_sync(self, bundle_paths: list[str]) -> ExecutionSummary:
"""Run replay bundles synchronously."""
bundles = [ReplayBundle.from_path(path) for path in bundle_paths]
@@ -40,59 +40,49 @@ def run_sync(self, bundle_paths: List[str]) -> ExecutionSummary:
print(f"Using snapshot: {self.config.snapshot_id}")
print(f"Parallel instances: {self.config.parallel_count}")
- # Get base snapshot
try:
base_snapshot = self.client.snapshots.get(self.config.snapshot_id)
print(f"✓ Base snapshot loaded: {base_snapshot.id}")
- except Exception as e:
- print(f"✗ Failed to load snapshot: {e}")
+ except Exception as exc:
+ print(f"✗ Failed to load snapshot: {exc}")
return self.summary
- # Start base instance
try:
base_instance = self.client.instances.start(snapshot_id=base_snapshot.id)
base_instance.wait_until_ready()
print(f"✓ Base instance started: {base_instance.id}")
- except Exception as e:
- print(f"✗ Failed to start base instance: {e}")
+ except Exception as exc:
+ print(f"✗ Failed to start base instance: {exc}")
return self.summary
+ branches: list[Any] = []
try:
- # Create branch instances
- branches = base_instance.branch(count=self.config.parallel_count)
+ branches = list(base_instance.branch(count=self.config.parallel_count))
print(f"✓ Created {len(branches)} branch instances")
- # Execute bundles in parallel
- results = []
for i, bundle in enumerate(bundles):
branch_idx = i % len(branches)
branch = branches[branch_idx]
-
result = self._execute_bundle_sync(branch, bundle, i)
- results.append(result)
self.summary.add_result(result)
-
print(f"Bundle {i+1}/{len(bundles)}: {result.status}")
- # Generate summary report
self._generate_summary_report()
-
finally:
- # Cleanup instances
print("Cleaning up instances...")
for branch in branches:
try:
branch.stop()
- except Exception:
- pass
+ except Exception as cleanup_exc:
+ print(f"WARNING: branch stop failed during cleanup: {cleanup_exc}")
try:
base_instance.stop()
- except Exception:
- pass
+ except Exception as cleanup_exc:
+ print(f"WARNING: base stop failed during cleanup: {cleanup_exc}")
return self.summary
- async def run_async(self, bundle_paths: List[str]) -> ExecutionSummary:
+ async def run_async(self, bundle_paths: list[str]) -> ExecutionSummary:
"""Run replay bundles asynchronously."""
bundles = [ReplayBundle.from_path(path) for path in bundle_paths]
@@ -100,46 +90,40 @@ async def run_async(self, bundle_paths: List[str]) -> ExecutionSummary:
print(f"Using snapshot: {self.config.snapshot_id}")
print(f"Parallel instances: {self.config.parallel_count}")
- # Get base snapshot
try:
base_snapshot = await self.client.snapshots.aget(self.config.snapshot_id)
print(f"✓ Base snapshot loaded: {base_snapshot.id}")
- except Exception as e:
- print(f"✗ Failed to load snapshot: {e}")
+ except Exception as exc:
+ print(f"✗ Failed to load snapshot: {exc}")
return self.summary
- # Start base instance
try:
base_instance = await self.client.instances.astart(
snapshot_id=base_snapshot.id
)
await base_instance.await_until_ready()
print(f"✓ Base instance started: {base_instance.id}")
- except Exception as e:
- print(f"✗ Failed to start base instance: {e}")
+ except Exception as exc:
+ print(f"✗ Failed to start base instance: {exc}")
return self.summary
+ branches: list[Any] = []
try:
- # Create branch instances
- branches = base_instance.branch(count=self.config.parallel_count)
+ branches = list(base_instance.branch(count=self.config.parallel_count))
print(f"✓ Created {len(branches)} branch instances")
- # Execute bundles in parallel
tasks = []
for i, bundle in enumerate(bundles):
branch_idx = i % len(branches)
branch = branches[branch_idx]
+ tasks.append(self._execute_bundle_async(branch, bundle, i))
- task = self._execute_bundle_async(branch, bundle, i)
- tasks.append(task)
-
- # Wait for all executions to complete
- results = await asyncio.gather(*tasks, return_exceptions=True)
+ results: list[Union[ExecutionResult, BaseException]] = await asyncio.gather(
+ *tasks, return_exceptions=True
+ )
- # Process results
for i, result in enumerate(results):
- if isinstance(result, Exception):
- # Create error result
+ if isinstance(result, BaseException):
error_result = ExecutionResult(
bundle_path=str(bundles[i].path),
status="ERROR",
@@ -147,41 +131,36 @@ async def run_async(self, bundle_paths: List[str]) -> ExecutionSummary:
error_message=str(result),
)
self.summary.add_result(error_result)
+ print(f"Bundle {i+1}/{len(bundles)}: ERROR")
else:
self.summary.add_result(result)
+ print(f"Bundle {i+1}/{len(bundles)}: {result.status}")
- print(f"Bundle {i+1}/{len(bundles)}: {result.status}")
-
- # Generate summary report
await self._generate_summary_report_async()
-
finally:
- # Cleanup instances
print("Cleaning up instances...")
for branch in branches:
try:
await branch.astop()
- except Exception:
- pass
+ except Exception as cleanup_exc:
+ print(f"WARNING: branch astop failed during cleanup: {cleanup_exc}")
try:
await base_instance.astop()
- except Exception:
- pass
+ except Exception as cleanup_exc:
+ print(f"WARNING: base astop failed during cleanup: {cleanup_exc}")
return self.summary
def _execute_bundle_sync(
- self, instance, bundle: ReplayBundle, bundle_index: int
+ self, instance: Any, bundle: ReplayBundle, bundle_index: int
) -> ExecutionResult:
"""Execute a single bundle synchronously."""
start_time = time.time()
try:
- # Copy bundle to instance
remote_path = f"/tmp/replay_{bundle_index}.zip"
instance.copy(str(bundle.path), remote_path)
- # Execute replay command
if self.config.emit_cert:
cmd = (
f"replay --in {remote_path} "
@@ -192,21 +171,17 @@ def _execute_bundle_sync(
with instance.ssh() as ssh:
result = ssh.run(cmd)
-
- # Collect output
stdout = result.stdout
stderr = result.stderr
exit_code = result.exit_code
- # Determine status
if exit_code == 0:
status = "PASS"
- elif result.timed_out:
+ elif getattr(result, "timed_out", False):
status = "TIMEOUT"
else:
status = "FAIL"
- # Copy certificate if generated
cert_path = None
if self.config.emit_cert and exit_code == 0:
local_cert_path = (
@@ -216,18 +191,14 @@ def _execute_bundle_sync(
try:
instance.copy(f"/tmp/cert_{bundle_index}.json", local_cert_path)
cert_path = local_cert_path
- except Exception as e:
- print(f"Warning: Failed to copy cert: {e}")
+ except Exception as exc:
+ print(f"Warning: Failed to copy cert: {exc}")
- # Save log
- log_path = (
- f"{self.config.output_directory}/logs/" f"log_{bundle_index}.txt"
- )
- with open(log_path, "w") as f:
- f.write(f"STDOUT:\n{stdout}\n\nSTDERR:\n{stderr}\n")
+ log_path = f"{self.config.output_directory}/logs/log_{bundle_index}.txt"
+ with open(log_path, "w", encoding="utf-8") as handle:
+ handle.write(f"STDOUT:\n{stdout}\n\nSTDERR:\n{stderr}\n")
execution_time_ms = int((time.time() - start_time) * 1000)
-
return ExecutionResult(
bundle_path=str(bundle.path),
bundle_hash=bundle.hash,
@@ -235,106 +206,33 @@ def _execute_bundle_sync(
execution_time_ms=execution_time_ms,
cert_path=cert_path,
log_path=log_path,
- instance_id=instance.id,
+ instance_id=getattr(instance, "id", None),
error_message=None if status == "PASS" else stderr,
)
-
- except Exception as e:
+ except Exception as exc:
execution_time_ms = int((time.time() - start_time) * 1000)
return ExecutionResult(
bundle_path=str(bundle.path),
bundle_hash=bundle.hash,
status="ERROR",
execution_time_ms=execution_time_ms,
- error_message=str(e),
+ error_message=str(exc),
)
async def _execute_bundle_async(
- self, instance, bundle: ReplayBundle, bundle_index: int
+ self, instance: Any, bundle: ReplayBundle, bundle_index: int
) -> ExecutionResult:
- """Execute a single bundle asynchronously."""
- start_time = time.time()
-
- try:
- # Copy bundle to instance
- remote_path = f"/tmp/replay_{bundle_index}.zip"
- instance.copy(str(bundle.path), remote_path)
-
- # Execute replay command
- if self.config.emit_cert:
- cmd = (
- f"replay --in {remote_path} "
- f"--emit /tmp/cert_{bundle_index}.json"
- )
- else:
- cmd = f"replay --in {remote_path}"
+ """Execute a single bundle asynchronously (SSH still sync via Morph SDK)."""
+ return await asyncio.to_thread(
+ self._execute_bundle_sync, instance, bundle, bundle_index
+ )
- with instance.ssh() as ssh:
- result = ssh.run(cmd)
-
- # Collect output
- stdout = result.stdout
- stderr = result.stderr
- exit_code = result.exit_code
-
- # Determine status
- if exit_code == 0:
- status = "PASS"
- elif result.timed_out:
- status = "TIMEOUT"
- else:
- status = "FAIL"
-
- # Copy certificate if generated
- cert_path = None
- if self.config.emit_cert and exit_code == 0:
- local_cert_path = (
- f"{self.config.output_directory}/certs/"
- f"cert_{bundle_index}.json"
- )
- try:
- instance.copy(f"/tmp/cert_{bundle_index}.json", local_cert_path)
- cert_path = local_cert_path
- except Exception as e:
- print(f"Warning: Failed to copy cert: {e}")
-
- # Save log
- log_path = (
- f"{self.config.output_directory}/logs/" f"log_{bundle_index}.txt"
- )
- with open(log_path, "w") as f:
- f.write(f"STDOUT:\n{stdout}\n\nSTDERR:\n{stderr}\n")
-
- execution_time_ms = int((time.time() - start_time) * 1000)
-
- return ExecutionResult(
- bundle_path=str(bundle.path),
- bundle_hash=bundle.hash,
- status=status,
- execution_time_ms=execution_time_ms,
- cert_path=cert_path,
- log_path=log_path,
- instance_id=instance.id,
- error_message=None if status == "PASS" else stderr,
- )
-
- except Exception as e:
- execution_time_ms = int((time.time() - start_time) * 1000)
- return ExecutionResult(
- bundle_path=str(bundle.path),
- bundle_hash=bundle.hash,
- status="ERROR",
- execution_time_ms=execution_time_ms,
- error_message=str(e),
- )
-
- def _generate_summary_report(self):
+ def _generate_summary_report(self) -> None:
"""Generate summary report synchronously."""
- self.summary.end_time = None # Will be set by validator
-
+ self.summary.end_time = datetime.now(timezone.utc).replace(tzinfo=None)
report_path = f"{self.config.output_directory}/reports/index.json"
- with open(report_path, "w") as f:
- json.dump(self.summary.dict(), f, indent=2, default=str)
+ with open(report_path, "w", encoding="utf-8") as handle:
+ json.dump(self.summary.model_dump(), handle, indent=2, default=str)
print("\nExecution Summary:")
print(f" Total bundles: {self.summary.total_bundles}")
@@ -342,22 +240,9 @@ def _generate_summary_report(self):
print(f" Failed: {self.summary.failed}")
print(f" Timed out: {self.summary.timed_out}")
print(f" Success rate: {self.summary.success_rate:.1f}%")
- print(f" Total time: " f"{self.summary.total_execution_time_ms/1000:.1f}s")
+ print(f" Total time: {self.summary.total_execution_time_ms/1000:.1f}s")
print(f" Report saved to: {report_path}")
- async def _generate_summary_report_async(self):
+ async def _generate_summary_report_async(self) -> None:
"""Generate summary report asynchronously."""
- self.summary.end_time = None # Will be set by validator
-
- report_path = f"{self.config.output_directory}/reports/index.json"
- with open(report_path, "w") as f:
- json.dump(self.summary.dict(), f, indent=2, default=str)
-
- print("\nExecution Summary:")
- print(f" Total bundles: {self.summary.total_bundles}")
- print(f" Successful: {self.summary.successful}")
- print(f" Failed: {self.summary.failed}")
- print(f" Timed out: {self.summary.timed_out}")
- print(f" Success rate: {self.summary.success_rate:.1f}%")
- print(f" Total time: " f"{self.summary.total_execution_time_ms/1000:.1f}s")
- print(f" Report saved to: {report_path}")
+ await asyncio.to_thread(self._generate_summary_report)
diff --git a/runner/core_fixed.py b/runner/core_fixed.py
deleted file mode 100644
index a9d9c7f..0000000
--- a/runner/core_fixed.py
+++ /dev/null
@@ -1,362 +0,0 @@
-"""Core runner logic for executing replay bundles on Morph Cloud."""
-
-import asyncio
-import json
-import os
-import time
-from typing import List
-
-from morphcloud.api import MorphCloudClient
-from morphcloud.exceptions import MorphCloudError
-
-from .models import (
- ExecutionResult,
- ExecutionSummary,
- ReplayBundle,
- RunnerConfig,
-)
-
-
-class ReplayRunner:
- """Main runner class for executing replay bundles on Morph Cloud."""
-
- def __init__(self, config: RunnerConfig):
- """Initialize the replay runner."""
- self.config = config
- self.client = MorphCloudClient()
- self.summary = ExecutionSummary()
-
- # Ensure output directories exist
- os.makedirs(f"{config.output_directory}/certs", exist_ok=True)
- os.makedirs(f"{config.output_directory}/logs", exist_ok=True)
- os.makedirs(f"{config.output_directory}/reports", exist_ok=True)
-
- def run_sync(self, bundle_paths: List[str]) -> ExecutionSummary:
- """Run replay bundles synchronously."""
- bundles = [ReplayBundle.from_path(path) for path in bundle_paths]
-
- print(f"Starting replay execution for {len(bundles)} bundles...")
- print(f"Using snapshot: {self.config.snapshot_id}")
- print(f"Parallel instances: {self.config.parallel_count}")
-
- # Get base snapshot
- try:
- base_snapshot = self.client.snapshots.get(self.config.snapshot_id)
- print(f"✓ Base snapshot loaded: {base_snapshot.id}")
- except MorphCloudError as e:
- print(f"✗ Failed to load snapshot: {e}")
- return self.summary
-
- # Start base instance
- try:
- base_instance = self.client.instances.start(snapshot_id=base_snapshot.id)
- base_instance.wait_until_ready()
- print(f"✓ Base instance started: {base_instance.id}")
- except MorphCloudError as e:
- print(f"✗ Failed to start base instance: {e}")
- return self.summary
-
- try:
- # Create branch instances
- branches = base_instance.branch(count=self.config.parallel_count)
- print(f"✓ Created {len(branches)} branch instances")
-
- # Execute bundles in parallel
- results = []
- for i, bundle in enumerate(bundles):
- branch_idx = i % len(branches)
- branch = branches[branch_idx]
-
- result = self._execute_bundle_sync(branch, bundle, i)
- results.append(result)
- self.summary.add_result(result)
-
- print(f"Bundle {i+1}/{len(bundles)}: {result.status}")
-
- # Generate summary report
- self._generate_summary_report()
-
- finally:
- # Cleanup instances
- print("Cleaning up instances...")
- for branch in branches:
- try:
- branch.stop()
- except Exception:
- pass
- try:
- base_instance.stop()
- except Exception:
- pass
-
- return self.summary
-
- async def run_async(self, bundle_paths: List[str]) -> ExecutionSummary:
- """Run replay bundles asynchronously."""
- bundles = [ReplayBundle.from_path(path) for path in bundle_paths]
-
- print(f"Starting async replay execution for {len(bundles)} bundles...")
- print(f"Using snapshot: {self.config.snapshot_id}")
- print(f"Parallel instances: {self.config.parallel_count}")
-
- # Get base snapshot
- try:
- base_snapshot = await self.client.snapshots.aget(self.config.snapshot_id)
- print(f"✓ Base snapshot loaded: {base_snapshot.id}")
- except MorphCloudError as e:
- print(f"✗ Failed to load snapshot: {e}")
- return self.summary
-
- # Start base instance
- try:
- base_instance = await self.client.instances.astart(
- snapshot_id=base_snapshot.id
- )
- await base_instance.await_until_ready()
- print(f"✓ Base instance started: {base_instance.id}")
- except MorphCloudError as e:
- print(f"✗ Failed to start base instance: {e}")
- return self.summary
-
- try:
- # Create branch instances
- branches = base_instance.branch(count=self.config.parallel_count)
- print(f"✓ Created {len(branches)} branch instances")
-
- # Execute bundles in parallel
- tasks = []
- for i, bundle in enumerate(bundles):
- branch_idx = i % len(branches)
- branch = branches[branch_idx]
-
- task = self._execute_bundle_async(branch, bundle, i)
- tasks.append(task)
-
- # Wait for all executions to complete
- results = await asyncio.gather(*tasks, return_exceptions=True)
-
- # Process results
- for i, result in enumerate(results):
- if isinstance(result, Exception):
- # Create error result
- error_result = ExecutionResult(
- bundle_path=str(bundles[i].path),
- status="ERROR",
- execution_time_ms=0,
- error_message=str(result),
- )
- self.summary.add_result(error_result)
- else:
- self.summary.add_result(result)
-
- print(f"Bundle {i+1}/{len(bundles)}: {result.status}")
-
- # Generate summary report
- await self._generate_summary_report_async()
-
- finally:
- # Cleanup instances
- print("Cleaning up instances...")
- for branch in branches:
- try:
- await branch.astop()
- except Exception:
- pass
- try:
- await base_instance.astop()
- except Exception:
- pass
-
- return self.summary
-
- def _execute_bundle_sync(
- self, instance, bundle: ReplayBundle, bundle_index: int
- ) -> ExecutionResult:
- """Execute a single bundle synchronously."""
- start_time = time.time()
-
- try:
- # Copy bundle to instance
- remote_path = f"/tmp/replay_{bundle_index}.zip"
- instance.copy(str(bundle.path), remote_path)
-
- # Execute replay command
- if self.config.emit_cert:
- cmd = (
- f"replay --in {remote_path} "
- f"--emit /tmp/cert_{bundle_index}.json"
- )
- else:
- cmd = f"replay --in {remote_path}"
-
- with instance.ssh() as ssh:
- result = ssh.run(cmd)
-
- # Collect output
- stdout = result.stdout
- stderr = result.stderr
- exit_code = result.exit_code
-
- # Determine status
- if exit_code == 0:
- status = "PASS"
- elif result.timed_out:
- status = "TIMEOUT"
- else:
- status = "FAIL"
-
- # Copy certificate if generated
- cert_path = None
- if self.config.emit_cert and exit_code == 0:
- local_cert_path = (
- f"{self.config.output_directory}/certs/"
- f"cert_{bundle_index}.json"
- )
- try:
- instance.copy(f"/tmp/cert_{bundle_index}.json", local_cert_path)
- cert_path = local_cert_path
- except Exception as e:
- print(f"Warning: Failed to copy cert: {e}")
-
- # Save log
- log_path = (
- f"{self.config.output_directory}/logs/" f"log_{bundle_index}.txt"
- )
- with open(log_path, "w") as f:
- f.write(f"STDOUT:\n{stdout}\n\nSTDERR:\n{stderr}\n")
-
- execution_time_ms = int((time.time() - start_time) * 1000)
-
- return ExecutionResult(
- bundle_path=str(bundle.path),
- bundle_hash=bundle.hash,
- status=status,
- execution_time_ms=execution_time_ms,
- cert_path=cert_path,
- log_path=log_path,
- instance_id=instance.id,
- error_message=None if status == "PASS" else stderr,
- )
-
- except Exception as e:
- execution_time_ms = int((time.time() - start_time) * 1000)
- return ExecutionResult(
- bundle_path=str(bundle.path),
- bundle_hash=bundle.hash,
- status="ERROR",
- execution_time_ms=execution_time_ms,
- error_message=str(e),
- )
-
- async def _execute_bundle_async(
- self, instance, bundle: ReplayBundle, bundle_index: int
- ) -> ExecutionResult:
- """Execute a single bundle asynchronously."""
- start_time = time.time()
-
- try:
- # Copy bundle to instance
- remote_path = f"/tmp/replay_{bundle_index}.zip"
- instance.copy(str(bundle.path), remote_path)
-
- # Execute replay command
- if self.config.emit_cert:
- cmd = (
- f"replay --in {remote_path} "
- f"--emit /tmp/cert_{bundle_index}.json"
- )
- else:
- cmd = f"replay --in {remote_path}"
-
- with instance.ssh() as ssh:
- result = ssh.run(cmd)
-
- # Collect output
- stdout = result.stdout
- stderr = result.stderr
- exit_code = result.exit_code
-
- # Determine status
- if exit_code == 0:
- status = "PASS"
- elif result.timed_out:
- status = "TIMEOUT"
- else:
- status = "FAIL"
-
- # Copy certificate if generated
- cert_path = None
- if self.config.emit_cert and exit_code == 0:
- local_cert_path = (
- f"{self.config.output_directory}/certs/"
- f"cert_{bundle_index}.json"
- )
- try:
- instance.copy(f"/tmp/cert_{bundle_index}.json", local_cert_path)
- cert_path = local_cert_path
- except Exception as e:
- print(f"Warning: Failed to copy cert: {e}")
-
- # Save log
- log_path = (
- f"{self.config.output_directory}/logs/" f"log_{bundle_index}.txt"
- )
- with open(log_path, "w") as f:
- f.write(f"STDOUT:\n{stdout}\n\nSTDERR:\n{stderr}\n")
-
- execution_time_ms = int((time.time() - start_time) * 1000)
-
- return ExecutionResult(
- bundle_path=str(bundle.path),
- bundle_hash=bundle.hash,
- status=status,
- execution_time_ms=execution_time_ms,
- cert_path=cert_path,
- log_path=log_path,
- instance_id=instance.id,
- error_message=None if status == "PASS" else stderr,
- )
-
- except Exception as e:
- execution_time_ms = int((time.time() - start_time) * 1000)
- return ExecutionResult(
- bundle_path=str(bundle.path),
- bundle_hash=bundle.hash,
- status="ERROR",
- execution_time_ms=execution_time_ms,
- error_message=str(e),
- )
-
- def _generate_summary_report(self):
- """Generate summary report synchronously."""
- self.summary.end_time = None # Will be set by validator
-
- report_path = f"{self.config.output_directory}/reports/index.json"
- with open(report_path, "w") as f:
- json.dump(self.summary.dict(), f, indent=2, default=str)
-
- print("\nExecution Summary:")
- print(f" Total bundles: {self.summary.total_bundles}")
- print(f" Successful: {self.summary.successful}")
- print(f" Failed: {self.summary.failed}")
- print(f" Timed out: {self.summary.timed_out}")
- print(f" Success rate: {self.summary.success_rate:.1f}%")
- print(f" Total time: " f"{self.summary.total_execution_time_ms/1000:.1f}s")
- print(f" Report saved to: {report_path}")
-
- async def _generate_summary_report_async(self):
- """Generate summary report asynchronously."""
- self.summary.end_time = None # Will be set by validator
-
- report_path = f"{self.config.output_directory}/reports/index.json"
- with open(report_path, "w") as f:
- json.dump(self.summary.dict(), f, indent=2, default=str)
-
- print("\nExecution Summary:")
- print(f" Total bundles: {self.summary.total_bundles}")
- print(f" Successful: {self.summary.successful}")
- print(f" Failed: {self.summary.failed}")
- print(f" Timed out: {self.summary.timed_out}")
- print(f" Success rate: {self.summary.success_rate:.1f}%")
- print(f" Total time: " f"{self.summary.total_execution_time_ms/1000:.1f}s")
- print(f" Report saved to: {report_path}")
diff --git a/runner/diff/__init__.py b/runner/diff/__init__.py
new file mode 100644
index 0000000..6c25963
--- /dev/null
+++ b/runner/diff/__init__.py
@@ -0,0 +1,17 @@
+"""Diff package."""
+
+from runner.diff.report import (
+ NON_CLAIMS,
+ build_differential_report,
+ compare_values,
+ diff_branch_dirs,
+ parse_pairs,
+)
+
+__all__ = [
+ "NON_CLAIMS",
+ "build_differential_report",
+ "compare_values",
+ "diff_branch_dirs",
+ "parse_pairs",
+]
diff --git a/runner/diff/report.py b/runner/diff/report.py
new file mode 100644
index 0000000..177f73b
--- /dev/null
+++ b/runner/diff/report.py
@@ -0,0 +1,171 @@
+"""Differential reports with three-valued absent≠equal semantics."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Any, Literal
+
+from runner.hashing import canonical_hash
+
+ComparisonResult = Literal[
+ "equal", "different", "absent_left", "absent_right", "absent_both"
+]
+
+COMPARISON_KEYS = (
+ "terminal_state",
+ "process_action",
+ "authorization",
+ "side_effect",
+ "reward",
+ "verifier_decision",
+ "resource",
+ "unresolved_external_dependency",
+)
+
+NON_CLAIMS = [
+ "No preferred branch.",
+ "No causality attribution.",
+ "No remediation ranking.",
+]
+
+
+def compare_values(left: Any, right: Any) -> dict[str, Any]:
+ left_absent = left is None
+ right_absent = right is None
+ if left_absent and right_absent:
+ result: ComparisonResult = "absent_both"
+ elif left_absent:
+ result = "absent_left"
+ elif right_absent:
+ result = "absent_right"
+ elif left == right:
+ result = "equal"
+ else:
+ result = "different"
+ payload: dict[str, Any] = {"result": result}
+ if not left_absent:
+ payload["left"] = left
+ if not right_absent:
+ payload["right"] = right
+ return payload
+
+
+def load_branch_view(branch_dir: Path) -> dict[str, Any]:
+ status_path = branch_dir / "status.json"
+ status = (
+ json.loads(status_path.read_text(encoding="utf-8"))
+ if status_path.is_file()
+ else {}
+ )
+ terminal_path = branch_dir / "terminal_state.json"
+ terminal = (
+ json.loads(terminal_path.read_text(encoding="utf-8"))
+ if terminal_path.is_file()
+ else None
+ )
+ resources_path = branch_dir / "resource_report.json"
+ resources = (
+ json.loads(resources_path.read_text(encoding="utf-8"))
+ if resources_path.is_file()
+ else None
+ )
+ branch_report_path = branch_dir / "branch_report.json"
+ branch_report = (
+ json.loads(branch_report_path.read_text(encoding="utf-8"))
+ if branch_report_path.is_file()
+ else {}
+ )
+ return {
+ "terminal_state": terminal,
+ "process_action": branch_report.get("process_action"),
+ "authorization": branch_report.get("authorization"),
+ "side_effect": branch_report.get("side_effect"),
+ "reward": branch_report.get("reward"),
+ "verifier_decision": branch_report.get("verifier_decision"),
+ "resource": resources,
+ "unresolved_external_dependency": branch_report.get(
+ "unresolved_external_dependency"
+ ),
+ "status": status,
+ }
+
+
+def build_differential_report(
+ left_id: str,
+ right_id: str,
+ left: dict[str, Any],
+ right: dict[str, Any],
+) -> dict[str, Any]:
+ comparisons = {
+ key: compare_values(left.get(key), right.get(key)) for key in COMPARISON_KEYS
+ }
+ body = {
+ "schema_version": "mrr.DifferentialReport.v1",
+ "left_branch_id": left_id,
+ "right_branch_id": right_id,
+ "comparisons": comparisons,
+ "non_claims": list(NON_CLAIMS),
+ }
+ digest = canonical_hash(
+ body,
+ enforce_number_policy=True,
+ extra_excluded=frozenset({"report_digest"}),
+ )
+ body["report_digest"] = digest
+ from runner.evidence.validate import SchemaValidationError, validate_instance
+
+ try:
+ validate_instance("mrr.DifferentialReport.v1", body)
+ except SchemaValidationError as exc:
+ raise ValueError(str(exc)) from exc
+ return body
+
+
+def diff_branch_dirs(
+ branches_root: Path,
+ pairs: list[tuple[str, str]],
+ out_dir: Path,
+) -> list[dict[str, Any]]:
+ out_dir.mkdir(parents=True, exist_ok=True)
+ reports: list[dict[str, Any]] = []
+ for left_id, right_id in pairs:
+ left = load_branch_view(branches_root / left_id)
+ right = load_branch_view(branches_root / right_id)
+ report = build_differential_report(left_id, right_id, left, right)
+ # Reject causal / remediation claims if injected outside non_claims.
+ comparisons_blob = json.dumps(report.get("comparisons", {})).lower()
+ for banned in ("preferred_branch", "root_cause", "remediation_rank"):
+ if banned in comparisons_blob:
+ raise ValueError(
+ f"differential report contains banned language: {banned}"
+ )
+ prose = json.dumps(
+ {k: v for k, v in report.items() if k != "non_claims"}
+ ).lower()
+ for banned in ("root cause is", "preferred branch is", "remediate by"):
+ if banned in prose:
+ raise ValueError(
+ f"differential report contains banned language: {banned}"
+ )
+ path = out_dir / f"{left_id}__{right_id}.json"
+ path.write_text(
+ json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8"
+ )
+ reports.append(report)
+ return reports
+
+
+def parse_pairs(raw: str) -> list[tuple[str, str]]:
+ pairs: list[tuple[str, str]] = []
+ for part in raw.split(";"):
+ part = part.strip()
+ if not part:
+ continue
+ if "," not in part:
+ raise ValueError(f"invalid pair {part!r}; expected a,b")
+ left, right = part.split(",", 1)
+ pairs.append((left.strip(), right.strip()))
+ if not pairs:
+ raise ValueError("no pairs provided")
+ return pairs
diff --git a/runner/evidence/__init__.py b/runner/evidence/__init__.py
new file mode 100644
index 0000000..82eb1d9
--- /dev/null
+++ b/runner/evidence/__init__.py
@@ -0,0 +1,51 @@
+"""Evidence package: digests and redaction helpers."""
+
+from __future__ import annotations
+
+import re
+from typing import Any
+
+from runner.evidence.pcs import (
+ emit_runtime_receipt,
+ maybe_pf_core_replay_trace,
+ receipt_status_for_claim_class,
+ terminal_state_commitment,
+ write_json,
+)
+from runner.evidence.pip import emit_morph_replay_report, emit_transformation_record
+from runner.evidence.validate import SchemaValidationError, validate_instance
+
+SECRET_PATTERNS = (
+ re.compile(r"(?i)(api[_-]?key|token|password|secret)\s*[:=]\s*\S+"),
+ re.compile(r"sk-[A-Za-z0-9]{10,}"),
+ re.compile(r"Bearer\s+[A-Za-z0-9\-._~+/]+=*"),
+)
+
+
+def redact_text(text: str) -> str:
+ redacted = text
+ for pattern in SECRET_PATTERNS:
+ redacted = pattern.sub("[REDACTED]", redacted)
+ return redacted
+
+
+def assert_no_secret_material(payload: Any) -> None:
+ blob = str(payload).lower()
+ for needle in ("sk-", "bearer ", "api_key=", "password="):
+ if needle in blob:
+ raise ValueError(f"secret material leaked into evidence: {needle}")
+
+
+__all__ = [
+ "SchemaValidationError",
+ "assert_no_secret_material",
+ "emit_morph_replay_report",
+ "emit_runtime_receipt",
+ "emit_transformation_record",
+ "maybe_pf_core_replay_trace",
+ "receipt_status_for_claim_class",
+ "redact_text",
+ "terminal_state_commitment",
+ "validate_instance",
+ "write_json",
+]
diff --git a/runner/evidence/pcs.py b/runner/evidence/pcs.py
new file mode 100644
index 0000000..634bc47
--- /dev/null
+++ b/runner/evidence/pcs.py
@@ -0,0 +1,175 @@
+"""PCS RuntimeReceipt.v0 emitter and optional PF-Core gate."""
+
+from __future__ import annotations
+
+import json
+import subprocess
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Any, Optional
+
+from runner.evidence.validate import SchemaValidationError, validate_instance
+from runner.hashing import canonical_hash, sha256_bytes, sha256_text
+
+SOURCE_REPO = "https://github.com/SentinelOps-CI/morph-replay-runner"
+PRODUCER = "morph-replay-runner"
+PRODUCER_VERSION = "0.1.0"
+
+# PCS common.defs artifact_status — ReplayValidated is a PF claim class, not a
+# RuntimeReceipt.status enum member.
+_RECEIPT_STATUSES = frozenset({"RuntimeObserved", "RuntimeChecked", "Draft"})
+
+
+class EvidenceError(RuntimeError):
+ """Fail-closed evidence emission error."""
+
+
+def _iso_now() -> str:
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace(
+ "+00:00", "Z"
+ )
+
+
+def receipt_status_for_claim_class(claim_class: str, *, check_ran: bool) -> str:
+ """Map branch claim class to a schema-legal RuntimeReceipt.status.
+
+ ``ReplayValidated`` is recorded in environment metadata / PF invocation
+ evidence; the receipt status upgrades only to ``RuntimeChecked`` when the
+ PF-Core check actually ran.
+ """
+ if not check_ran:
+ return "RuntimeObserved"
+ if claim_class in {"RuntimeChecked", "ReplayValidated"}:
+ return "RuntimeChecked"
+ if claim_class == "RuntimeObserved":
+ return "RuntimeObserved"
+ raise EvidenceError(f"unsupported claim_class for receipt: {claim_class}")
+
+
+def emit_runtime_receipt(
+ *,
+ receipt_id: str,
+ run_id: str,
+ started_at: str,
+ ended_at: str,
+ run_outcome: str,
+ final_reason_code: str,
+ source_commit: str,
+ input_hashes: dict[str, str],
+ output_hashes: dict[str, str],
+ environment: Optional[dict[str, str]] = None,
+ status: str = "RuntimeObserved",
+ events_hash: Optional[str] = None,
+ policy_hash: Optional[str] = None,
+ trace_hash: Optional[str] = None,
+ released: bool = False,
+ local_dev: bool = True,
+) -> dict[str, Any]:
+ """Emit RuntimeReceipt.v0 with observational semantics.
+
+ Status defaults to RuntimeObserved. RuntimeChecked only when the
+ corresponding check actually ran. ReplayValidated is never a receipt
+ status (PCS enum); use environment claim metadata instead.
+ """
+ if status not in _RECEIPT_STATUSES:
+ raise EvidenceError(
+ f"disallowed receipt status: {status} "
+ f"(allowed={sorted(_RECEIPT_STATUSES)}; "
+ "ReplayValidated is a PF claim class, not a RuntimeReceipt status)"
+ )
+
+ empty = sha256_text("")
+ body: dict[str, Any] = {
+ "receipt_id": receipt_id,
+ "schema_version": "v0",
+ "run_id": run_id,
+ "environment": environment or {"provider": "local-fake"},
+ "started_at": started_at,
+ "ended_at": ended_at,
+ "status": status,
+ "run_outcome": run_outcome,
+ "final_reason_code": final_reason_code,
+ "released": released,
+ "events_hash": events_hash or empty,
+ "policy_hash": policy_hash or empty,
+ "trace_hash": trace_hash or empty,
+ "producer": PRODUCER,
+ "producer_version": PRODUCER_VERSION,
+ "source_repo": SOURCE_REPO,
+ "source_commit": source_commit,
+ "local_dev": local_dev,
+ "input_hashes": dict(sorted(input_hashes.items())),
+ "output_hashes": dict(sorted(output_hashes.items())),
+ }
+ digest = canonical_hash(body, enforce_number_policy=True)
+ body["signature_or_digest"] = digest
+ try:
+ validate_instance("RuntimeReceipt.v0", body)
+ except SchemaValidationError as exc:
+ raise EvidenceError(str(exc)) from exc
+ return body
+
+
+def maybe_pf_core_replay_trace(
+ *,
+ claim_class: str,
+ trace_path: Optional[Path],
+ out_dir: Path,
+) -> Optional[dict[str, Any]]:
+ """Invoke ``pcs pf-core replay-trace`` only when claim class applies.
+
+ Never upgrades claim class. Returns subprocess metadata or None if skipped.
+ """
+ if claim_class not in {"ReplayValidated", "RuntimeChecked"}:
+ return None
+ if trace_path is None or not trace_path.is_file():
+ raise EvidenceError(
+ f"claim_class {claim_class} requires PF-Core-shaped trace file"
+ )
+ out_dir.mkdir(parents=True, exist_ok=True)
+ cmd = [
+ "pcs",
+ "pf-core",
+ "replay-trace",
+ str(trace_path),
+ "--out",
+ str(out_dir),
+ ]
+ try:
+ completed = subprocess.run(
+ cmd,
+ check=False,
+ capture_output=True,
+ text=True,
+ )
+ except FileNotFoundError as exc:
+ raise EvidenceError(
+ "pcs CLI not found; cannot satisfy PF-Core claim class"
+ ) from exc
+ record = {
+ "command": cmd,
+ "exit_code": completed.returncode,
+ "stdout_digest": sha256_text(completed.stdout or ""),
+ "stderr_digest": sha256_text(completed.stderr or ""),
+ "claim_class": claim_class,
+ "upgraded": False,
+ }
+ (out_dir / "pf_core_invocation.json").write_text(
+ json.dumps(record, indent=2, sort_keys=True) + "\n", encoding="utf-8"
+ )
+ if completed.returncode != 0:
+ raise EvidenceError(
+ f"pcs pf-core replay-trace failed with exit {completed.returncode}"
+ )
+ return record
+
+
+def terminal_state_commitment(terminal: dict[str, Any]) -> str:
+ return canonical_hash(terminal, enforce_number_policy=True)
+
+
+def write_json(path: Path, payload: dict[str, Any]) -> str:
+ data = json.dumps(payload, indent=2, sort_keys=True) + "\n"
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(data, encoding="utf-8")
+ return sha256_bytes(data.encode("utf-8"))
diff --git a/runner/evidence/pip.py b/runner/evidence/pip.py
new file mode 100644
index 0000000..e0b4e70
--- /dev/null
+++ b/runner/evidence/pip.py
@@ -0,0 +1,78 @@
+"""PIP MorphReplayReport + TransformationRecord emitters."""
+
+from __future__ import annotations
+
+from typing import Any, Literal, Optional
+
+from runner.evidence.validate import SchemaValidationError, validate_instance
+from runner.hashing import canonical_hash
+
+MorphStatus = Literal["recorded", "mismatch", "indeterminate"]
+
+
+class PipEvidenceError(ValueError):
+ """Fail-closed PIP emission error."""
+
+
+def emit_morph_replay_report(
+ *,
+ replay_id: str,
+ branch_id: str,
+ replay_identity_digest: str,
+ input_artifact_digests: list[str],
+ output_artifact_digests: list[str],
+ status: MorphStatus = "recorded",
+ lineage_node_ids: Optional[list[str]] = None,
+) -> dict[str, Any]:
+ if not output_artifact_digests:
+ raise PipEvidenceError("output_artifact_digests must be non-empty")
+ body: dict[str, Any] = {
+ "schema_version": "pip.MorphReplayReport.v1",
+ "replay_id": replay_id,
+ "branch_id": branch_id,
+ "replay_identity_digest": replay_identity_digest,
+ "input_artifact_digests": list(input_artifact_digests),
+ "output_artifact_digests": list(output_artifact_digests),
+ "status": status,
+ "lineage_node_ids": list(lineage_node_ids or []),
+ }
+ try:
+ validate_instance("pip.MorphReplayReport.v1", body)
+ except SchemaValidationError as exc:
+ raise PipEvidenceError(str(exc)) from exc
+ return body
+
+
+def emit_transformation_record(
+ *,
+ transformation_id: str,
+ transformation_type: str,
+ input_artifact_digests: list[str],
+ output_artifact_digest: str,
+ implementation_id: str,
+ implementation_version: str,
+ container_digest: str,
+ source_commit: str,
+ notes: Optional[str] = None,
+) -> dict[str, Any]:
+ body: dict[str, Any] = {
+ "schema_version": "pip.TransformationRecord.v1",
+ "transformation_id": transformation_id,
+ "transformation_type": transformation_type,
+ "input_artifact_digests": list(input_artifact_digests),
+ "output_artifact_digest": output_artifact_digest,
+ "implementation_id": implementation_id,
+ "implementation_version": implementation_version,
+ "container_digest": container_digest,
+ "source_commit": source_commit,
+ }
+ if notes is not None:
+ body["notes"] = notes
+ body["record_digest"] = canonical_hash(
+ body, enforce_number_policy=True, extra_excluded=frozenset({"record_digest"})
+ )
+ try:
+ validate_instance("pip.TransformationRecord.v1", body)
+ except SchemaValidationError as exc:
+ raise PipEvidenceError(str(exc)) from exc
+ return body
diff --git a/runner/evidence/validate.py b/runner/evidence/validate.py
new file mode 100644
index 0000000..4a3c9e2
--- /dev/null
+++ b/runner/evidence/validate.py
@@ -0,0 +1,143 @@
+"""Vendored JSON Schema validation (always-on; no external CLI required).
+
+Mirrors pcs-core registry construction so CI can fail closed on instance
+conformance without ``pcs validate`` / ``post-incident replay-check``.
+External CLIs remain optional deeper semantic gates when installed.
+"""
+
+from __future__ import annotations
+
+import json
+from functools import lru_cache
+from pathlib import Path
+from typing import Any, Literal
+
+from jsonschema import Draft202012Validator, FormatChecker
+from jsonschema.exceptions import SchemaError
+from referencing import Registry, Resource
+from referencing.jsonschema import DRAFT202012
+
+ArtifactKind = Literal[
+ "RuntimeReceipt.v0",
+ "pip.MorphReplayReport.v1",
+ "pip.TransformationRecord.v1",
+ "pip.LineageBundle.v1",
+ "mrr.ExecutionProfile.v1",
+ "mrr.BranchReplayManifest.v1",
+ "mrr.DifferentialReport.v1",
+ "mrr.ResourceReport.v1",
+]
+
+
+class SchemaValidationError(ValueError):
+ """Instance failed vendored JSON Schema validation."""
+
+
+def repo_root() -> Path:
+ """Return repository root containing ``schemas/`` (cwd or parents)."""
+ here = Path(__file__).resolve()
+ for candidate in (here.parents[2], Path.cwd(), *Path.cwd().parents):
+ if (candidate / "schemas").is_dir():
+ return candidate
+ raise SchemaValidationError(
+ "schemas/ directory not found; run from repo root or install package data"
+ )
+
+
+def schemas_root() -> Path:
+ return repo_root() / "schemas"
+
+
+SCHEMA_FILES: dict[ArtifactKind, Path] = {
+ "RuntimeReceipt.v0": Path("pcs/RuntimeReceipt.v0.schema.json"),
+ "pip.MorphReplayReport.v1": Path("pip/MorphReplayReport.schema.json"),
+ "pip.TransformationRecord.v1": Path("pip/TransformationRecord.schema.json"),
+ "pip.LineageBundle.v1": Path("pip/LineageBundle.schema.json"),
+ "mrr.ExecutionProfile.v1": Path("mrr/execution_profile.v1.json"),
+ "mrr.BranchReplayManifest.v1": Path("mrr/branch_replay_manifest.v1.json"),
+ "mrr.DifferentialReport.v1": Path("mrr/differential_report.v1.json"),
+ "mrr.ResourceReport.v1": Path("mrr/resource_report.v1.json"),
+}
+
+
+def _load_json(path: Path) -> dict[str, Any]:
+ data = json.loads(path.read_text(encoding="utf-8"))
+ if not isinstance(data, dict):
+ raise SchemaValidationError(f"schema root must be object: {path}")
+ return data
+
+
+@lru_cache(maxsize=1)
+def _build_registry() -> Registry:
+ root = schemas_root()
+ resources: list[tuple[str, Resource[Any]]] = []
+ for path in sorted(root.rglob("*.json")):
+ if path.name == "SCHEMA_MIRROR.json":
+ continue
+ schema = _load_json(path)
+ resource = Resource.from_contents(schema, default_specification=DRAFT202012)
+ resources.append((path.as_uri(), resource))
+ resources.append((path.name, resource))
+ schema_id = schema.get("$id")
+ if isinstance(schema_id, str) and schema_id:
+ resources.append((schema_id, resource))
+ # Relative $ref targets used by PCS (e.g. common.defs.json#/...)
+ resources.append((path.name.split("/")[-1], resource))
+ return Registry().with_resources(resources)
+
+
+def _format_checker() -> FormatChecker:
+ checker = FormatChecker()
+ # Require format assertions when checkers exist (fail closed on uri/date-time).
+ required = {"date-time", "uri"}
+ missing = required - set(checker.checkers)
+ if missing:
+ raise SchemaValidationError(
+ "jsonschema FormatChecker missing required formats: "
+ f"{sorted(missing)}; install jsonschema[format]"
+ )
+ return checker
+
+
+@lru_cache(maxsize=16)
+def _validator_for(kind: ArtifactKind) -> Draft202012Validator:
+ rel = SCHEMA_FILES[kind]
+ path = schemas_root() / rel
+ if not path.is_file():
+ raise SchemaValidationError(f"missing vendored schema: {path}")
+ schema = _load_json(path)
+ try:
+ Draft202012Validator.check_schema(schema)
+ except SchemaError as exc:
+ raise SchemaValidationError(f"invalid schema {path}: {exc}") from exc
+ return Draft202012Validator(
+ schema,
+ registry=_build_registry(),
+ format_checker=_format_checker(),
+ )
+
+
+def validate_instance(kind: ArtifactKind, instance: dict[str, Any]) -> None:
+ """Fail closed if ``instance`` does not conform to vendored schema ``kind``."""
+ if not isinstance(instance, dict):
+ raise SchemaValidationError(f"{kind}: instance must be a JSON object")
+ validator = _validator_for(kind)
+ errors = sorted(validator.iter_errors(instance), key=lambda e: list(e.path))
+ if errors:
+ messages = "; ".join(
+ f"{'/'.join(str(p) for p in err.path) or '$'}: {err.message}"
+ for err in errors[:12]
+ )
+ raise SchemaValidationError(f"{kind} schema validation failed: {messages}")
+
+
+def validate_json_file(kind: ArtifactKind, path: Path) -> dict[str, Any]:
+ data = _load_json(path)
+ validate_instance(kind, data)
+ return data
+
+
+def clear_schema_caches() -> None:
+ """Test helper to rebuild registry after schema tree changes."""
+ _build_registry.cache_clear()
+ _validator_for.cache_clear()
diff --git a/runner/hashing.py b/runner/hashing.py
new file mode 100644
index 0000000..d950a43
--- /dev/null
+++ b/runner/hashing.py
@@ -0,0 +1,152 @@
+"""PCS Canonical JSON v1 hashing (algorithm-compatible with pcs-core).
+
+Do not invent a second hash. ``HASH_EXCLUDED_FIELDS`` matches pcs-core
+(``signature_or_digest``, ``artifact_digest``, ``signature``). Domain digests
+such as ``profile_digest`` / ``record_digest`` / ``report_digest`` must be
+passed via ``extra_excluded``.
+"""
+from __future__ import annotations
+
+import hashlib
+import json
+import math
+from typing import Any
+
+SIGNATURE_FIELD = "signature_or_digest"
+ARTIFACT_DIGEST_FIELD = "artifact_digest"
+SIGNATURE_OBJECT_FIELD = "signature"
+# Match pcs-core HASH_EXCLUDED_FIELDS exactly. Domain digests (profile_digest,
+# record_digest, report_digest, integrity) must be passed via extra_excluded.
+HASH_EXCLUDED_FIELDS = frozenset(
+ {
+ SIGNATURE_FIELD,
+ ARTIFACT_DIGEST_FIELD,
+ SIGNATURE_OBJECT_FIELD,
+ }
+)
+
+CANONICALIZATION_VERSION = "v1"
+SAFE_INTEGER_MIN = -9007199254740991
+SAFE_INTEGER_MAX = 9007199254740991
+REJECTION_FLOAT_PROHIBITED = "float_prohibited"
+REJECTION_INTEGER_OUT_OF_RANGE = "integer_out_of_range"
+REJECTION_NEGATIVE_ZERO = "negative_zero"
+
+
+class CanonicalizationError(ValueError):
+ """Raised when a value cannot be represented under Canonical JSON v1 rules."""
+
+ def __init__(self, code: str, message: str, *, path: str = "$") -> None:
+ self.code = code
+ self.path = path
+ super().__init__(message)
+
+
+def assert_canonical_number_policy(value: Any, *, path: str = "$") -> None:
+ """Enforce Canonical JSON v1 number policy."""
+ if isinstance(value, bool):
+ return
+ if isinstance(value, int):
+ if value < SAFE_INTEGER_MIN or value > SAFE_INTEGER_MAX:
+ raise CanonicalizationError(
+ REJECTION_INTEGER_OUT_OF_RANGE,
+ f"{path}: integer {value} outside safe-integer range",
+ path=path,
+ )
+ return
+ if isinstance(value, float):
+ if value == 0.0 and math.copysign(1.0, value) < 0.0:
+ raise CanonicalizationError(
+ REJECTION_NEGATIVE_ZERO,
+ f"{path}: negative zero is prohibited",
+ path=path,
+ )
+ raise CanonicalizationError(
+ REJECTION_FLOAT_PROHIBITED,
+ f"{path}: float values are prohibited; use decimal strings",
+ path=path,
+ )
+ if isinstance(value, dict):
+ for key, child in value.items():
+ assert_canonical_number_policy(child, path=f"{path}.{key}")
+ return
+ if isinstance(value, list):
+ for index, child in enumerate(value):
+ assert_canonical_number_policy(child, path=f"{path}[{index}]")
+
+
+def _sort_keys(value: Any) -> Any:
+ if isinstance(value, dict):
+ return {k: _sort_keys(value[k]) for k in sorted(value)}
+ if isinstance(value, list):
+ return [_sort_keys(item) for item in value]
+ return value
+
+
+def canonicalize_for_hash(
+ data: dict[str, Any],
+ *,
+ enforce_number_policy: bool = False,
+ extra_excluded: frozenset[str] | None = None,
+) -> dict[str, Any]:
+ """Return a copy suitable for hashing (integrity fields removed, keys sorted)."""
+ excluded = (
+ HASH_EXCLUDED_FIELDS
+ if extra_excluded is None
+ else (HASH_EXCLUDED_FIELDS | extra_excluded)
+ )
+ payload = {k: v for k, v in data.items() if k not in excluded}
+ if enforce_number_policy:
+ assert_canonical_number_policy(payload)
+ sorted_payload = _sort_keys(payload)
+ assert isinstance(sorted_payload, dict)
+ return sorted_payload
+
+
+def canonical_json_bytes(
+ data: dict[str, Any],
+ *,
+ enforce_number_policy: bool = False,
+ extra_excluded: frozenset[str] | None = None,
+) -> bytes:
+ canonical = canonicalize_for_hash(
+ data,
+ enforce_number_policy=enforce_number_policy,
+ extra_excluded=extra_excluded,
+ )
+ return json.dumps(canonical, separators=(",", ":"), ensure_ascii=False).encode(
+ "utf-8"
+ )
+
+
+def canonical_hash(
+ data: dict[str, Any],
+ *,
+ enforce_number_policy: bool = False,
+ extra_excluded: frozenset[str] | None = None,
+) -> str:
+ digest = hashlib.sha256(
+ canonical_json_bytes(
+ data,
+ enforce_number_policy=enforce_number_policy,
+ extra_excluded=extra_excluded,
+ )
+ ).hexdigest()
+ return f"sha256:{digest}"
+
+
+def sha256_file(path: str) -> str:
+ """Return ``sha256:`` for file bytes."""
+ h = hashlib.sha256()
+ with open(path, "rb") as handle:
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
+ h.update(chunk)
+ return f"sha256:{h.hexdigest()}"
+
+
+def sha256_bytes(data: bytes) -> str:
+ return f"sha256:{hashlib.sha256(data).hexdigest()}"
+
+
+def sha256_text(text: str) -> str:
+ return sha256_bytes(text.encode("utf-8"))
diff --git a/runner/hermeticity/__init__.py b/runner/hermeticity/__init__.py
new file mode 100644
index 0000000..0ad5378
--- /dev/null
+++ b/runner/hermeticity/__init__.py
@@ -0,0 +1,23 @@
+"""Hermeticity package."""
+
+from runner.hermeticity.modes import (
+ EnforcementStatus,
+ HermeticityError,
+ HermeticityEvidence,
+ HermeticityMode,
+ HermeticityPolicy,
+ host_allowed,
+ path_within_partner,
+ policy_from_profile_network,
+)
+
+__all__ = [
+ "EnforcementStatus",
+ "HermeticityError",
+ "HermeticityEvidence",
+ "HermeticityMode",
+ "HermeticityPolicy",
+ "host_allowed",
+ "path_within_partner",
+ "policy_from_profile_network",
+]
diff --git a/runner/hermeticity/modes.py b/runner/hermeticity/modes.py
new file mode 100644
index 0000000..d9f6af1
--- /dev/null
+++ b/runner/hermeticity/modes.py
@@ -0,0 +1,116 @@
+"""Hermeticity modes, enforcement, and evidence."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Any, Literal, Optional
+
+from runner.hashing import canonical_hash, sha256_text
+
+
+class HermeticityMode(str, Enum):
+ NO_NETWORK = "no_network"
+ ALLOWLISTED_NETWORK = "allowlisted_network"
+ RECORDED_RESPONSE = "recorded_response"
+ PARTNER_LOCAL = "partner_local"
+ SYNTHETIC_DEPENDENCY = "synthetic_dependency"
+
+
+class EnforcementStatus(str, Enum):
+ ENFORCED = "enforced"
+ UNSUPPORTED = "unsupported"
+ DENIED = "denied"
+
+
+@dataclass(frozen=True)
+class HermeticityPolicy:
+ mode: HermeticityMode
+ allowlist_hosts: tuple[str, ...] = ()
+ cassette_digest: Optional[str] = None
+ partner_local_paths: tuple[str, ...] = ()
+ synthetic_dependency_digests: tuple[str, ...] = ()
+
+ def policy_digest(self) -> str:
+ payload = {
+ "mode": self.mode.value,
+ "allowlist_hosts": list(self.allowlist_hosts),
+ "cassette_digest": self.cassette_digest,
+ "partner_local_paths": list(self.partner_local_paths),
+ "synthetic_dependency_digests": list(self.synthetic_dependency_digests),
+ }
+ return canonical_hash(payload, enforce_number_policy=True)
+
+
+@dataclass
+class HermeticityEvidence:
+ mode: HermeticityMode
+ status: EnforcementStatus
+ policy_digest: str
+ probe_result: dict[str, Any] = field(default_factory=dict)
+ message: str = ""
+
+ def to_dict(self) -> dict[str, Any]:
+ return {
+ "mode": self.mode.value,
+ "status": self.status.value,
+ "policy_digest": self.policy_digest,
+ "probe_result": self.probe_result,
+ "message": self.message,
+ }
+
+
+class HermeticityError(RuntimeError):
+ """Fail-closed hermeticity error before or during execution."""
+
+
+def policy_from_profile_network(network: Any) -> HermeticityPolicy:
+ mode = HermeticityMode(network.mode)
+ return HermeticityPolicy(
+ mode=mode,
+ allowlist_hosts=tuple(network.allowlist_hosts or []),
+ cassette_digest=network.cassette_digest,
+ partner_local_paths=tuple(network.partner_local_paths or []),
+ synthetic_dependency_digests=tuple(network.synthetic_dependency_digests or []),
+ )
+
+
+def host_allowed(host: str, policy: HermeticityPolicy) -> bool:
+ if policy.mode == HermeticityMode.NO_NETWORK:
+ return False
+ if policy.mode == HermeticityMode.ALLOWLISTED_NETWORK:
+ return host in policy.allowlist_hosts
+ if policy.mode == HermeticityMode.RECORDED_RESPONSE:
+ return False
+ if policy.mode == HermeticityMode.PARTNER_LOCAL:
+ return False
+ if policy.mode == HermeticityMode.SYNTHETIC_DEPENDENCY:
+ return False
+ return False
+
+
+def path_within_partner(path: str, policy: HermeticityPolicy) -> bool:
+ if policy.mode != HermeticityMode.PARTNER_LOCAL:
+ return False
+ from pathlib import Path
+
+ target = Path(path).resolve()
+ for allowed in policy.partner_local_paths:
+ root = Path(allowed).resolve()
+ if target == root:
+ return True
+ try:
+ target.relative_to(root)
+ return True
+ except ValueError:
+ continue
+ return False
+
+
+def evidence_digest(evidence: HermeticityEvidence) -> str:
+ return sha256_text(
+ f"{evidence.mode.value}|{evidence.status.value}|{evidence.policy_digest}"
+ )
+
+
+CapabilityResult = Literal["supported", "unsupported"]
diff --git a/runner/legacy_core.py b/runner/legacy_core.py
new file mode 100644
index 0000000..e8fbaf6
--- /dev/null
+++ b/runner/legacy_core.py
@@ -0,0 +1,8 @@
+"""Legacy ZIP Morph path preserved as compatibility wrapper."""
+
+from __future__ import annotations
+
+# Re-export prior core implementation under a stable name for migration.
+from runner.core import ReplayRunner
+
+__all__ = ["ReplayRunner"]
diff --git a/runner/main.py b/runner/main.py
index 45c4338..c375e70 100644
--- a/runner/main.py
+++ b/runner/main.py
@@ -1,181 +1,385 @@
-"""Main CLI entry point for the Morph Replay Runner."""
+"""CLI entry point: run (compat), branch, profile, diff, validate."""
+
+from __future__ import annotations
import asyncio
import glob
+import hashlib
+import json
import sys
+import tempfile
+from pathlib import Path
import click
from rich.console import Console
-from .core import ReplayRunner
-from .models import RunnerConfig
+from runner.diff import diff_branch_dirs, parse_pairs
+from runner.hermeticity import HermeticityError
+from runner.legacy_core import ReplayRunner
+from runner.models import HttpCallbackConfig, RunnerConfig
+from runner.profile import ProfileValidationError, load_profile, profile_digest
+from runner.providers import ProviderError, get_provider
console = Console()
-@click.command()
-@click.option(
- "--snapshot",
- required=True,
- help="Base snapshot ID or digest containing sidecar + replay tools",
-)
-@click.option(
- "--bundles",
- required=True,
- help="Glob pattern for replay bundles (e.g., ./replays/*.zip)",
-)
-@click.option(
- "--parallel",
- "-p",
- default=4,
- type=int,
- help="Number of parallel instances (default: 4)",
-)
-@click.option(
- "--timeout",
- "-t",
- default=600,
- type=int,
- help="Execution timeout in seconds (default: 600)",
-)
-@click.option(
- "--emit-cert/--no-emit-cert",
- default=True,
- help="Emit CERT-V1 JSON certificates (default: True)",
-)
-@click.option(
- "--out",
- "-o",
- default="./evidence",
- help="Output directory for evidence collection (default: ./evidence)",
-)
-@click.option(
- "--async", "use_async", is_flag=True, help="Use asynchronous execution mode"
-)
-@click.option(
- "--http-callback", is_flag=True, help="Enable HTTP callback service for demos"
-)
-@click.option(
- "--http-port",
- default=8080,
- type=int,
- help="Port for HTTP callback service (default: 8080)",
-)
+def _compat_argv(argv: list[str]) -> list[str]:
+ """Preserve ``replay-runner --snapshot …`` by inserting ``run`` when needed."""
+ if not argv:
+ return argv
+ commands = {
+ "run",
+ "branch",
+ "profile",
+ "diff",
+ "validate",
+ "--help",
+ "-h",
+ "--version",
+ }
+ if argv[0] in commands:
+ return argv
+ if argv[0].startswith("-"):
+ return ["run", *argv]
+ return argv
+
+
+@click.group()
+@click.version_option(version="0.1.0")
+def cli() -> None:
+ """Hermetic branch-N replay runner (observational evidence only)."""
+
+
+@cli.command("run")
+@click.option("--snapshot", required=True, help="Base snapshot ID or digest")
+@click.option("--bundles", required=True, help="Glob for replay bundles (*.zip)")
+@click.option("--parallel", "-p", default=4, type=int)
+@click.option("--timeout", "-t", default=600, type=int)
+@click.option("--emit-cert/--no-emit-cert", default=True)
+@click.option("--out", "-o", default="./evidence")
+@click.option("--async", "use_async", is_flag=True)
+@click.option("--http-callback", is_flag=True)
+@click.option("--http-port", default=8080, type=int)
@click.option(
"--http-auth",
default="none",
type=click.Choice(["none", "api_key"]),
- help="HTTP callback authentication mode (default: none)",
)
-@click.version_option(version="0.1.0")
-def main(
- snapshot,
- bundles,
- parallel,
- timeout,
- emit_cert,
- out,
- use_async,
- http_callback,
- http_port,
- http_auth,
-):
- """Morph Replay Runner - Execute TRACE-REPLAY-KIT bundles with branch-N parallelism."""
-
- # Validate inputs
+@click.option(
+ "--provider",
+ default="morph",
+ type=click.Choice(["morph", "local-fake"]),
+ help="Execution provider (default morph for legacy run path)",
+)
+def run_cmd(
+ snapshot: str,
+ bundles: str,
+ parallel: int,
+ timeout: int,
+ emit_cert: bool,
+ out: str,
+ use_async: bool,
+ http_callback: bool,
+ http_port: int,
+ http_auth: str,
+ provider: str,
+) -> None:
+ """Compatibility ZIP-bundle Morph path (legacy)."""
+ if http_callback:
+ console.print(
+ "[red]HTTP callback is out of scope for hermetic branch-N and "
+ "is not implemented; refusing to run (fail-closed).[/red]"
+ )
+ sys.exit(2)
+
+ if provider != "morph":
+ console.print(
+ "[red]Legacy run path requires --provider morph; "
+ "use `replay-runner branch` for local-fake.[/red]"
+ )
+ sys.exit(2)
+
if parallel < 1 or parallel > 100:
console.print("[red]Error: Parallel count must be between 1 and 100[/red]")
sys.exit(1)
-
if timeout < 60:
console.print("[red]Error: Timeout must be at least 60 seconds[/red]")
sys.exit(1)
- # Resolve bundle paths
- bundle_paths = glob.glob(bundles)
+ bundle_paths = [p for p in glob.glob(bundles) if p.endswith(".zip")]
if not bundle_paths:
- console.print(f"[red]Error: No bundles found matching pattern: {bundles}[/red]")
- sys.exit(1)
-
- # Filter for zip files
- bundle_paths = [p for p in bundle_paths if p.endswith(".zip")]
- if not bundle_paths:
- console.print(
- f"[red]Error: No .zip files found matching pattern: {bundles}[/red]"
- )
+ console.print(f"[red]Error: No .zip bundles matching {bundles}[/red]")
sys.exit(1)
- console.print(f"[green]Found {len(bundle_paths)} replay bundles[/green]")
- for path in bundle_paths:
- console.print(f" [blue]{path}[/blue]")
-
- # Create configuration
config = RunnerConfig(
snapshot_id=snapshot,
parallel_count=parallel,
timeout_seconds=timeout,
emit_cert=emit_cert,
output_directory=out,
- http_callback={
- "enabled": http_callback,
- "auth_mode": http_auth,
- "port": http_port,
- },
+ http_callback=HttpCallbackConfig(
+ enabled=False, auth_mode=http_auth, port=http_port
+ ),
)
-
- # Create runner
runner = ReplayRunner(config)
-
- # Display configuration
- console.print("\n[bold]Configuration:[/bold]")
- console.print(f" Snapshot: [blue]{snapshot}[/blue]")
- console.print(f" Parallel instances: [blue]{parallel}[/blue]")
- console.print(f" Timeout: [blue]{timeout}s[/blue]")
- console.print(f" Emit certificates: [blue]{emit_cert}[/blue]")
- console.print(f" Output directory: [blue]{out}[/blue]")
- console.print(f" HTTP callback: [blue]{http_callback}[/blue]")
- if http_callback:
- console.print(f" HTTP port: [blue]{http_port}[/blue]")
- console.print(f" HTTP auth: [blue]{http_auth}[/blue]")
-
- # Execute
try:
if use_async:
- console.print("\n[bold]Starting asynchronous execution...[/bold]")
summary = asyncio.run(runner.run_async(bundle_paths))
else:
- console.print("\n[bold]Starting synchronous execution...[/bold]")
summary = runner.run_sync(bundle_paths)
-
- # Display final results
- console.print("\n[bold green]Execution completed![/bold green]")
- console.print(f" Total bundles: [blue]{summary.total_bundles}[/blue]")
- console.print(f" Successful: [green]{summary.successful}[/green]")
- console.print(f" Failed: [red]{summary.failed}[/red]")
- console.print(f" Timed out: [yellow]{summary.timed_out}[/yellow]")
- console.print(f" Success rate: [blue]{summary.success_rate:.1f}%[/blue]")
console.print(
- f" Total time: "
- f"[blue]{summary.total_execution_time_ms/1000:.1f}s[/blue]"
+ f"successful={summary.successful} failed={summary.failed} "
+ f"timed_out={summary.timed_out}"
)
+ if summary.failed or summary.timed_out:
+ sys.exit(1)
+ except Exception as exc:
+ console.print(f"[red]Execution failed: {exc}[/red]")
+ sys.exit(1)
- # Check for failures
- if summary.failed > 0 or summary.timed_out > 0:
- console.print(
- "\n[yellow]Some bundles failed or timed out. "
- "Check logs for details.[/yellow]"
+
+@cli.command("branch")
+@click.option("--incident", type=click.Path(exists=True, path_type=Path), required=True)
+@click.option("--snapshot", type=click.Path(exists=True, path_type=Path), required=True)
+@click.option(
+ "--interventions",
+ type=click.Path(exists=True, file_okay=False, path_type=Path),
+ required=True,
+)
+@click.option(
+ "--execution-profile",
+ type=click.Path(exists=True, path_type=Path),
+ required=True,
+)
+@click.option("--manifest", type=click.Path(exists=True, path_type=Path), default=None)
+@click.option("--parallel", default=16, type=int)
+@click.option("--out", type=click.Path(path_type=Path), required=True)
+@click.option(
+ "--provider",
+ default="local-fake",
+ type=click.Choice(["local-fake", "morph"]),
+)
+@click.option("--provider-root", type=click.Path(path_type=Path), default=None)
+@click.option(
+ "--snapshot-payload",
+ type=click.Path(exists=True, path_type=Path),
+ default=None,
+ help="Raw snapshot bytes whose sha256 must match snapshot_digest",
+)
+@click.option(
+ "--allow-partial/--fail-on-partial",
+ default=False,
+ help="Exit 0 when any branch is partial (default: fail closed)",
+)
+@click.option(
+ "--ovk-check",
+ type=click.Path(exists=True, path_type=Path),
+ default=None,
+ help="If set, shell out to OVK checker on PATH (fail-closed if missing)",
+)
+def branch_cmd(
+ incident: Path,
+ snapshot: Path,
+ interventions: Path,
+ execution_profile: Path,
+ manifest: Path | None,
+ parallel: int,
+ out: Path,
+ provider: str,
+ provider_root: Path | None,
+ snapshot_payload: Path | None,
+ allow_partial: bool,
+ ovk_check: Path | None,
+) -> None:
+ """Hermetic isolated branch-N execution."""
+ from runner.branch import run_branch_job
+
+ tmp: tempfile.TemporaryDirectory[str] | None = None
+ try:
+ if provider == "local-fake":
+ root = provider_root
+ if root is None:
+ tmp = tempfile.TemporaryDirectory(prefix="mrr-fake-")
+ root = Path(tmp.name)
+ snap_doc = json.loads(snapshot.read_text(encoding="utf-8"))
+ payload_path = snapshot_payload or snapshot.with_name(
+ snapshot.stem + ".payload"
+ )
+ if not payload_path.is_file():
+ raise ProviderError(
+ f"snapshot payload required at {payload_path} "
+ "(bytes must hash to snapshot_digest)"
+ )
+ raw = payload_path.read_bytes()
+ digest = "sha256:" + hashlib.sha256(raw).hexdigest()
+ if digest != snap_doc["snapshot_digest"]:
+ raise ProviderError(
+ f"snapshot payload digest {digest} != pinned "
+ f"{snap_doc['snapshot_digest']}"
+ )
+ provider_obj = get_provider("local-fake", root=str(root))
+ provider_obj.register_snapshot( # type: ignore[attr-defined]
+ snap_doc["snapshot_id"], raw, digest=digest
)
- sys.exit(1)
else:
- console.print("\n[green]All bundles executed successfully![/green]")
- sys.exit(0)
-
- except KeyboardInterrupt:
- console.print("\n[yellow]Execution interrupted by user[/yellow]")
- sys.exit(130)
- except Exception as e:
- console.print(f"\n[red]Execution failed: {e}[/red]")
+ provider_obj = get_provider("morph")
+
+ summary = run_branch_job(
+ provider=provider_obj,
+ profile_path=execution_profile,
+ manifest_path=manifest,
+ incident_path=incident,
+ snapshot_path=snapshot,
+ interventions_dir=interventions,
+ parallel=parallel,
+ out_dir=out,
+ )
+ if ovk_check is not None:
+ from runner.ovk import OvkError, run_ovk_check
+
+ try:
+ run_ovk_check(target=ovk_check, out_dir=out / "ovk")
+ except OvkError as exc:
+ console.print(f"[red]OVK check failed: {exc}[/red]")
+ sys.exit(2)
+
+ branches = summary.get("branches", {})
+ incomplete = [
+ bid
+ for bid, result in branches.items()
+ if result.get("status") != "complete"
+ ]
+ console.print(
+ json.dumps(
+ {
+ "branches": list(branches.keys()),
+ "complete": len(branches) - len(incomplete),
+ "partial": len(incomplete),
+ }
+ )
+ )
+ if incomplete and not allow_partial:
+ console.print(
+ f"[red]fail-closed: {len(incomplete)} branch(es) not complete "
+ f"({', '.join(sorted(incomplete))}); "
+ "pass --allow-partial to accept explicit partial records[/red]"
+ )
+ sys.exit(3)
+ except (HermeticityError, ProviderError, ProfileValidationError, ValueError) as exc:
+ console.print(f"[red]{exc}[/red]")
+ sys.exit(2)
+ except Exception as exc:
+ console.print(f"[red]branch failed: {exc}[/red]")
sys.exit(1)
+ finally:
+ if tmp is not None:
+ tmp.cleanup()
+
+
+@cli.group("profile")
+def profile_group() -> None:
+ """ExecutionProfile utilities."""
+
+
+@profile_group.command("validate")
+@click.option(
+ "--file",
+ "file_path",
+ type=click.Path(exists=True, path_type=Path),
+ required=True,
+)
+def profile_validate(file_path: Path) -> None:
+ try:
+ profile = load_profile(file_path)
+ except ProfileValidationError as exc:
+ console.print(f"[red]invalid: {exc}[/red]")
+ sys.exit(2)
+ console.print(f"valid digest={profile.profile_digest}")
+
+
+@profile_group.command("digest")
+@click.option(
+ "--file",
+ "file_path",
+ type=click.Path(exists=True, path_type=Path),
+ required=True,
+)
+def profile_digest_cmd(file_path: Path) -> None:
+ try:
+ profile = load_profile(file_path)
+ except ProfileValidationError as exc:
+ console.print(f"[red]invalid: {exc}[/red]")
+ sys.exit(2)
+ console.print(profile.profile_digest or profile_digest(profile))
+
+
+@cli.command("diff")
+@click.option(
+ "--branches",
+ type=click.Path(exists=True, file_okay=False, path_type=Path),
+ required=True,
+)
+@click.option("--pairs", required=True, help="Pairs as a,b;c,d")
+@click.option("--out", type=click.Path(path_type=Path), required=True)
+def diff_cmd(branches: Path, pairs: str, out: Path) -> None:
+ try:
+ parsed = parse_pairs(pairs)
+ reports = diff_branch_dirs(branches, parsed, out)
+ except Exception as exc:
+ console.print(f"[red]diff failed: {exc}[/red]")
+ sys.exit(2)
+ console.print(f"wrote {len(reports)} differential report(s) to {out}")
+
+
+@cli.command("validate")
+@click.option("--schema-mirrors/--no-schema-mirrors", default=True)
+@click.option(
+ "--fixtures/--no-fixtures",
+ default=True,
+ help="Validate fixture/example instances against vendored schemas",
+)
+@click.option(
+ "--external-clis/--no-external-clis",
+ default=False,
+ help="Also run pcs/post-incident CLIs when installed (skip-only-when-absent)",
+)
+def validate_cmd(schema_mirrors: bool, fixtures: bool, external_clis: bool) -> None:
+ import runpy
+ import subprocess
+
+ root = Path(__file__).resolve().parents[1]
+ if schema_mirrors:
+ script = root / "scripts" / "check_schema_mirrors.py"
+ sys.argv = [str(script)]
+ try:
+ runpy.run_path(str(script), run_name="__main__")
+ except SystemExit as exc:
+ if exc.code not in (0, None):
+ raise
+ if fixtures:
+ fixture_script = root / "scripts" / "validate_vendored_instances.py"
+ sys.argv = [str(fixture_script)]
+ try:
+ runpy.run_path(str(fixture_script), run_name="__main__")
+ except SystemExit as exc:
+ if exc.code not in (0, None):
+ raise
+ if external_clis:
+ optional = root / "scripts" / "optional_external_validate.py"
+ completed = subprocess.run(
+ [sys.executable, str(optional)], check=False, capture_output=True, text=True
+ )
+ console.print(completed.stdout or "")
+ if completed.stderr:
+ console.print(completed.stderr)
+ if completed.returncode not in (0,):
+ sys.exit(completed.returncode)
+ console.print("ok")
+
+
+def main() -> None:
+ sys.argv = [sys.argv[0], *_compat_argv(sys.argv[1:])]
+ cli.main(prog_name="replay-runner")
if __name__ == "__main__":
diff --git a/runner/models.py b/runner/models.py
index ea27dc8..511fdb6 100644
--- a/runner/models.py
+++ b/runner/models.py
@@ -1,12 +1,14 @@
-"""Data models for the Morph Replay Runner."""
+"""Data models for the Morph Replay Runner (legacy ZIP path)."""
+
+from __future__ import annotations
import hashlib
import os
-from datetime import datetime
+from datetime import datetime, timezone
from pathlib import Path
-from typing import List, Optional
+from typing import Optional
-from pydantic import BaseModel, Field, validator
+from pydantic import BaseModel, Field, field_validator, model_validator
class HttpCallbackConfig(BaseModel):
@@ -29,11 +31,12 @@ class RunnerConfig(BaseModel):
output_directory: str = "./evidence"
http_callback: HttpCallbackConfig = Field(default_factory=HttpCallbackConfig)
- @validator("output_directory")
- def validate_output_directory(cls, v):
+ @field_validator("output_directory")
+ @classmethod
+ def validate_output_directory(cls, value: str) -> str:
"""Ensure output directory exists and is writable."""
- os.makedirs(v, exist_ok=True)
- return v
+ os.makedirs(value, exist_ok=True)
+ return value
class ExecutionResult(BaseModel):
@@ -49,21 +52,21 @@ class ExecutionResult(BaseModel):
error_message: Optional[str] = None
http_service_url: Optional[str] = None
- @validator("bundle_hash", pre=True, always=True)
- def compute_bundle_hash(cls, v, values):
+ @model_validator(mode="after")
+ def compute_bundle_hash(self) -> ExecutionResult:
"""Compute SHA-256 hash of the bundle file if not provided."""
- if v is None and "bundle_path" in values:
- bundle_path = values["bundle_path"]
- if os.path.exists(bundle_path):
- with open(bundle_path, "rb") as f:
- return hashlib.sha256(f.read()).hexdigest()
- return v
+ if self.bundle_hash is None and os.path.exists(self.bundle_path):
+ with open(self.bundle_path, "rb") as handle:
+ self.bundle_hash = hashlib.sha256(handle.read()).hexdigest()
+ return self
class ExecutionSummary(BaseModel):
"""Summary of all replay executions."""
- start_time: datetime = Field(default_factory=datetime.utcnow)
+ start_time: datetime = Field(
+ default_factory=lambda: datetime.now(timezone.utc).replace(tzinfo=None)
+ )
end_time: Optional[datetime] = None
total_bundles: int = 0
successful: int = 0
@@ -71,14 +74,14 @@ class ExecutionSummary(BaseModel):
timed_out: int = 0
total_execution_time_ms: int = 0
average_execution_time_ms: float = 0.0
- results: List[ExecutionResult] = Field(default_factory=list)
+ results: list[ExecutionResult] = Field(default_factory=list)
- @validator("end_time", pre=True, always=True)
- def set_end_time(cls, v):
+ @model_validator(mode="after")
+ def set_end_time(self) -> ExecutionSummary:
"""Set end time if not provided."""
- if v is None:
- return datetime.utcnow()
- return v
+ if self.end_time is None:
+ self.end_time = datetime.now(timezone.utc).replace(tzinfo=None)
+ return self
@property
def success_rate(self) -> float:
@@ -87,12 +90,11 @@ def success_rate(self) -> float:
return 0.0
return (self.successful / self.total_bundles) * 100
- def add_result(self, result: ExecutionResult):
+ def add_result(self, result: ExecutionResult) -> None:
"""Add an execution result and update summary statistics."""
self.results.append(result)
self.total_bundles = len(self.results)
- # Update counters
if result.status == "PASS":
self.successful += 1
elif result.status == "FAIL":
@@ -100,7 +102,6 @@ def add_result(self, result: ExecutionResult):
elif result.status == "TIMEOUT":
self.timed_out += 1
- # Update timing statistics
self.total_execution_time_ms += result.execution_time_ms
self.average_execution_time_ms = (
self.total_execution_time_ms / self.total_bundles
@@ -116,15 +117,15 @@ class ReplayBundle(BaseModel):
created_at: datetime
@classmethod
- def from_path(cls, bundle_path: str) -> "ReplayBundle":
+ def from_path(cls, bundle_path: str) -> ReplayBundle:
"""Create a ReplayBundle from a file path."""
path = Path(bundle_path)
if not path.exists():
raise FileNotFoundError(f"Bundle not found: {bundle_path}")
stat = path.stat()
- with open(path, "rb") as f:
- bundle_hash = hashlib.sha256(f.read()).hexdigest()
+ with open(path, "rb") as handle:
+ bundle_hash = hashlib.sha256(handle.read()).hexdigest()
return cls(
path=path,
diff --git a/runner/ovk/__init__.py b/runner/ovk/__init__.py
new file mode 100644
index 0000000..b60d134
--- /dev/null
+++ b/runner/ovk/__init__.py
@@ -0,0 +1,53 @@
+"""OVK shell-out only; do not vendor OVK schemas."""
+
+from __future__ import annotations
+
+import json
+import shutil
+import subprocess
+from pathlib import Path
+from typing import Any, Optional
+
+from runner.hashing import sha256_file, sha256_text
+
+
+class OvkError(RuntimeError):
+ """OVK invocation failure."""
+
+
+def resolve_ovk_cli() -> str:
+ path = shutil.which("ovk") or shutil.which("open-verification-kernel")
+ if path is None:
+ raise OvkError("OVK CLI not found on PATH")
+ return path
+
+
+def run_ovk_check(
+ *,
+ target: Path,
+ profile: Optional[str] = None,
+ out_dir: Optional[Path] = None,
+) -> dict[str, Any]:
+ """Shell out to OVK checker; return digests/refs only."""
+ cli = resolve_ovk_cli()
+ cmd = [cli, "check", str(target)]
+ if profile:
+ cmd.extend(["--profile", profile])
+ completed = subprocess.run(cmd, check=False, capture_output=True, text=True)
+ record: dict[str, Any] = {
+ "tool": "ovk",
+ "tool_version_pin": "1.2.1",
+ "command": cmd,
+ "exit_code": completed.returncode,
+ "stdout_digest": sha256_text(completed.stdout or ""),
+ "stderr_digest": sha256_text(completed.stderr or ""),
+ "target_digest": sha256_file(str(target)) if target.is_file() else None,
+ }
+ if out_dir is not None:
+ out_dir.mkdir(parents=True, exist_ok=True)
+ (out_dir / "ovk_invocation.json").write_text(
+ json.dumps(record, indent=2, sort_keys=True) + "\n", encoding="utf-8"
+ )
+ if completed.returncode != 0:
+ raise OvkError(f"OVK check failed with exit {completed.returncode}")
+ return record
diff --git a/runner/profile/__init__.py b/runner/profile/__init__.py
new file mode 100644
index 0000000..d480309
--- /dev/null
+++ b/runner/profile/__init__.py
@@ -0,0 +1,21 @@
+"""ExecutionProfile package."""
+
+from runner.profile.schema import (
+ SCHEMA_VERSION,
+ ExecutionProfile,
+ ProfileValidationError,
+ load_profile,
+ profile_digest,
+ validate_profile,
+ write_profile_with_digest,
+)
+
+__all__ = [
+ "SCHEMA_VERSION",
+ "ExecutionProfile",
+ "ProfileValidationError",
+ "load_profile",
+ "profile_digest",
+ "validate_profile",
+ "write_profile_with_digest",
+]
diff --git a/runner/profile/schema.py b/runner/profile/schema.py
new file mode 100644
index 0000000..d8024e0
--- /dev/null
+++ b/runner/profile/schema.py
@@ -0,0 +1,258 @@
+"""ExecutionProfile models, validation, and digests."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Any, Literal, Optional
+
+from pydantic import BaseModel, Field, field_validator, model_validator
+
+from runner.hashing import canonical_hash
+
+SCHEMA_VERSION = "mrr.ExecutionProfile.v1"
+FORBIDDEN_SECRET_KEYS = frozenset(
+ {
+ "secret",
+ "secrets",
+ "password",
+ "token",
+ "api_key",
+ "apikey",
+ "private_key",
+ "secret_value",
+ "credential",
+ "credentials",
+ }
+)
+
+
+class ProfileValidationError(ValueError):
+ """Fail-closed profile validation error."""
+
+
+class SnapshotPin(BaseModel):
+ snapshot_id: str = Field(..., min_length=1)
+ snapshot_digest: str = Field(..., pattern=r"^sha256:[a-f0-9]{64}$")
+
+
+class EnvironmentPin(BaseModel):
+ profile_name: str = Field(..., min_length=1)
+ profile_version: str = Field(..., min_length=1)
+
+
+class OsRuntime(BaseModel):
+ os: str = Field(..., min_length=1)
+ runtime: str = Field(..., min_length=1)
+
+
+class NetworkPolicy(BaseModel):
+ mode: Literal[
+ "no_network",
+ "allowlisted_network",
+ "recorded_response",
+ "partner_local",
+ "synthetic_dependency",
+ ]
+ allowlist_hosts: list[str] = Field(default_factory=list)
+ cassette_digest: Optional[str] = Field(
+ default=None, pattern=r"^sha256:[a-f0-9]{64}$"
+ )
+ partner_local_paths: list[str] = Field(default_factory=list)
+ synthetic_dependency_digests: list[str] = Field(default_factory=list)
+
+ @model_validator(mode="after")
+ def _mode_pins(self) -> NetworkPolicy:
+ if self.mode == "allowlisted_network" and not self.allowlist_hosts:
+ raise ProfileValidationError("allowlisted_network requires allowlist_hosts")
+ if self.mode == "recorded_response" and not self.cassette_digest:
+ raise ProfileValidationError("recorded_response requires cassette_digest")
+ if self.mode == "partner_local" and not self.partner_local_paths:
+ raise ProfileValidationError("partner_local requires partner_local_paths")
+ if (
+ self.mode == "synthetic_dependency"
+ and not self.synthetic_dependency_digests
+ ):
+ raise ProfileValidationError(
+ "synthetic_dependency requires synthetic_dependency_digests"
+ )
+ return self
+
+
+class ClockPolicy(BaseModel):
+ mode: Literal["frozen", "offset"]
+ fixed_epoch_ms: int = Field(..., ge=0)
+
+
+class RandomSeedPolicy(BaseModel):
+ seed: int = Field(..., ge=0)
+
+
+class ModelPin(BaseModel):
+ endpoint: Optional[str] = None
+ version: Optional[str] = None
+ local_checkpoint_digest: Optional[str] = Field(
+ default=None, pattern=r"^sha256:[a-f0-9]{64}$"
+ )
+
+ @model_validator(mode="after")
+ def _require_pin(self) -> ModelPin:
+ if not any([self.endpoint, self.version, self.local_checkpoint_digest]):
+ raise ProfileValidationError(
+ "model requires endpoint/version or local_checkpoint_digest"
+ )
+ return self
+
+
+class Budgets(BaseModel):
+ cpu_cores: int = Field(..., ge=1)
+ memory_mib: int = Field(..., ge=1)
+ accelerator_count: int = Field(default=0, ge=0)
+ storage_mib: int = Field(..., ge=1)
+ wall_time_ms: int = Field(..., ge=1)
+ token_budget: int = Field(..., ge=0)
+ api_call_budget: int = Field(..., ge=0)
+
+
+class SecretRef(BaseModel):
+ secret_ref: str = Field(..., min_length=1, pattern=r"^[A-Za-z0-9_./:-]+$")
+ purpose: Optional[str] = None
+
+ @field_validator("secret_ref")
+ @classmethod
+ def _no_embedded_secret(cls, value: str) -> str:
+ lowered = value.lower()
+ if any(token in lowered for token in ("=", "bearer ", "sk-")):
+ raise ProfileValidationError(
+ "secret_ref must be an identifier only; values rejected"
+ )
+ return value
+
+
+class OutputRetention(BaseModel):
+ mode: Literal["ephemeral", "retain"]
+ retention_days: int = Field(..., ge=0)
+
+
+class IntegrityEnvelope(BaseModel):
+ canonicalization_version: Literal["v1"]
+ artifact_digest: Optional[str] = Field(
+ default=None, pattern=r"^sha256:[a-f0-9]{64}$"
+ )
+
+
+class ExecutionProfile(BaseModel):
+ schema_version: Literal["mrr.ExecutionProfile.v1"] = "mrr.ExecutionProfile.v1"
+ profile_id: str = Field(..., min_length=1)
+ profile_digest: Optional[str] = Field(
+ default=None, pattern=r"^sha256:[a-f0-9]{64}$"
+ )
+ snapshot: SnapshotPin
+ container_image_digests: list[str] = Field(..., min_length=1)
+ environment: EnvironmentPin
+ os_runtime: OsRuntime
+ dependency_lock_digest: str = Field(..., pattern=r"^sha256:[a-f0-9]{64}$")
+ network_policy: NetworkPolicy
+ clock_policy: ClockPolicy
+ random_seed_policy: RandomSeedPolicy
+ tool_versions: dict[str, str] = Field(..., min_length=1)
+ model: Optional[ModelPin] = None
+ budgets: Budgets
+ secret_refs: list[SecretRef] = Field(default_factory=list)
+ output_retention: OutputRetention
+ source_commit: str = Field(..., pattern=r"^[0-9a-f]{40}$")
+ integrity: IntegrityEnvelope
+
+ @field_validator("container_image_digests")
+ @classmethod
+ def _digest_items(cls, values: list[str]) -> list[str]:
+ for item in values:
+ if not item.startswith("sha256:") or len(item) != 71:
+ raise ProfileValidationError(f"invalid image digest: {item}")
+ return values
+
+ @field_validator("tool_versions")
+ @classmethod
+ def _tool_versions_nonempty(cls, values: dict[str, str]) -> dict[str, str]:
+ if not values:
+ raise ProfileValidationError("tool_versions must be non-empty")
+ for key, value in values.items():
+ if not value:
+ raise ProfileValidationError(f"tool_versions[{key}] empty")
+ return values
+
+
+def _reject_secret_values(obj: Any, path: str = "$") -> None:
+ if isinstance(obj, dict):
+ for key, value in obj.items():
+ key_l = str(key).lower()
+ if key_l in FORBIDDEN_SECRET_KEYS:
+ raise ProfileValidationError(
+ f"secret values forbidden at {path}.{key}; use secret_refs only"
+ )
+ if key_l in {"secret_ref", "secret_refs"}:
+ _reject_secret_values(value, f"{path}.{key}")
+ continue
+ if isinstance(value, str) and key_l.endswith(
+ ("_secret", "_token", "_password")
+ ):
+ raise ProfileValidationError(f"secret values forbidden at {path}.{key}")
+ _reject_secret_values(value, f"{path}.{key}")
+ elif isinstance(obj, list):
+ for index, item in enumerate(obj):
+ _reject_secret_values(item, f"{path}[{index}]")
+
+
+def profile_digest(profile: ExecutionProfile | dict[str, Any]) -> str:
+ """SHA-256 over Canonical JSON v1 with integrity/digest fields stripped."""
+ if isinstance(profile, ExecutionProfile):
+ data = profile.model_dump(mode="json", exclude_none=True)
+ else:
+ data = dict(profile)
+ return canonical_hash(
+ data,
+ enforce_number_policy=True,
+ extra_excluded=frozenset({"profile_digest", "integrity"}),
+ )
+
+
+def load_profile(path: Path | str) -> ExecutionProfile:
+ raw_path = Path(path)
+ raw = json.loads(raw_path.read_text(encoding="utf-8"))
+ return validate_profile(raw)
+
+
+def validate_profile(raw: dict[str, Any]) -> ExecutionProfile:
+ if not isinstance(raw, dict):
+ raise ProfileValidationError("profile must be a JSON object")
+ version = raw.get("schema_version")
+ if version != SCHEMA_VERSION:
+ raise ProfileValidationError(
+ f"unknown schema_version {version!r}; expected {SCHEMA_VERSION}"
+ )
+ _reject_secret_values(raw)
+ try:
+ profile = ExecutionProfile.model_validate(raw)
+ except Exception as exc:
+ raise ProfileValidationError(str(exc)) from exc
+
+ digest = profile_digest(profile)
+ if profile.profile_digest is not None and profile.profile_digest != digest:
+ raise ProfileValidationError(
+ f"profile_digest drift: declared {profile.profile_digest}, computed {digest}"
+ )
+ return profile.model_copy(update={"profile_digest": digest})
+
+
+def write_profile_with_digest(profile: ExecutionProfile, path: Path | str) -> str:
+ digest = profile_digest(profile)
+ payload = profile.model_dump(mode="json", exclude_none=True)
+ payload["profile_digest"] = digest
+ payload["integrity"] = {
+ "canonicalization_version": "v1",
+ "artifact_digest": digest,
+ }
+ Path(path).write_text(
+ json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"
+ )
+ return digest
diff --git a/runner/providers/__init__.py b/runner/providers/__init__.py
new file mode 100644
index 0000000..9428c1d
--- /dev/null
+++ b/runner/providers/__init__.py
@@ -0,0 +1,35 @@
+"""Provider factory and exports."""
+
+from __future__ import annotations
+
+from typing import Any, Optional
+
+from runner.hermeticity.modes import HermeticityPolicy
+from runner.providers.base import ExecutionProvider, ProviderError
+from runner.providers.local_fake import LocalFakeProvider
+from runner.providers.morph import MorphCloudProvider
+
+
+def get_provider(
+ name: str,
+ *,
+ root: Optional[str] = None,
+ hermeticity: Optional[HermeticityPolicy] = None,
+ **kwargs: Any,
+) -> ExecutionProvider:
+ if name in {"local-fake", "local_fake", "fake"}:
+ if root is None:
+ raise ProviderError("local-fake provider requires root=")
+ return LocalFakeProvider(root, hermeticity=hermeticity, **kwargs)
+ if name in {"morph", "morphcloud"}:
+ return MorphCloudProvider(hermeticity=hermeticity, **kwargs)
+ raise ProviderError(f"unknown provider: {name}")
+
+
+__all__ = [
+ "ExecutionProvider",
+ "LocalFakeProvider",
+ "MorphCloudProvider",
+ "ProviderError",
+ "get_provider",
+]
diff --git a/runner/providers/base.py b/runner/providers/base.py
new file mode 100644
index 0000000..4914d97
--- /dev/null
+++ b/runner/providers/base.py
@@ -0,0 +1,118 @@
+"""Provider protocol types and ExecutionProvider Protocol."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Any, Literal, Optional, Protocol, runtime_checkable
+
+from runner.hermeticity.modes import HermeticityMode
+
+
+class ProviderError(RuntimeError):
+ """Fail-closed provider error."""
+
+
+@dataclass(frozen=True)
+class SnapshotRef:
+ snapshot_id: str
+ snapshot_digest: Optional[str] = None
+
+
+@dataclass(frozen=True)
+class SnapshotHandle:
+ snapshot_id: str
+ snapshot_digest: str
+ metadata: dict[str, Any] = field(default_factory=dict)
+
+
+@dataclass(frozen=True)
+class BranchHandle:
+ branch_id: str
+ snapshot_id: str
+ snapshot_digest: str
+ root_path: str
+ provider_ref: str
+
+
+@dataclass(frozen=True)
+class ExecRequest:
+ command: list[str]
+ timeout_ms: int
+ env: dict[str, str] = field(default_factory=dict)
+ workdir: Optional[str] = None
+ intervention_paths: list[str] = field(default_factory=list)
+
+
+class ExecStatus(str, Enum):
+ PASSED = "PASSED"
+ FAILED = "FAILED"
+ TIMEOUT = "TIMEOUT"
+ CANCELLED = "CANCELLED"
+ ERROR = "ERROR"
+
+
+@dataclass
+class ExecResult:
+ status: ExecStatus
+ exit_code: int
+ stdout: str
+ stderr: str
+ wall_time_ms: int
+ terminal_state: dict[str, Any] = field(default_factory=dict)
+ artifacts: dict[str, str] = field(default_factory=dict)
+
+
+MetricAvailability = Literal["observed", "unavailable", "not_applicable"]
+
+
+@dataclass(frozen=True)
+class MetricValue:
+ value: Optional[float | int | str]
+ unit: str
+ availability: MetricAvailability
+ source: Literal["provider", "runner", "guest"]
+
+
+@dataclass
+class ResourceSample:
+ metrics: dict[str, MetricValue]
+
+
+@dataclass
+class ArtifactBag:
+ files: dict[str, bytes]
+ digests: dict[str, str]
+
+
+@dataclass
+class TeardownReceipt:
+ branch_id: str
+ cleaned: bool
+ residual_paths: list[str] = field(default_factory=list)
+ secrets_scrubbed: bool = True
+ details: dict[str, Any] = field(default_factory=dict)
+
+
+@runtime_checkable
+class ExecutionProvider(Protocol):
+ """Backend-neutral execution provider."""
+
+ name: str
+
+ def supports(self, mode: HermeticityMode) -> bool:
+ """Return True if the provider can enforce the hermeticity mode."""
+
+ def lookup_snapshot(self, ref: SnapshotRef) -> SnapshotHandle: ...
+
+ def clone(self, snapshot: SnapshotHandle, *, branch_id: str) -> BranchHandle: ...
+
+ def execute(self, branch: BranchHandle, request: ExecRequest) -> ExecResult: ...
+
+ def measure(self, branch: BranchHandle) -> ResourceSample: ...
+
+ def extract_artifacts(
+ self, branch: BranchHandle, paths: list[str]
+ ) -> ArtifactBag: ...
+
+ def teardown(self, branch: BranchHandle) -> TeardownReceipt: ...
diff --git a/runner/providers/local_fake.py b/runner/providers/local_fake.py
new file mode 100644
index 0000000..92432ec
--- /dev/null
+++ b/runner/providers/local_fake.py
@@ -0,0 +1,471 @@
+"""Deterministic LocalFakeProvider for offline hermetic branch-N execution."""
+
+from __future__ import annotations
+
+import json
+import shutil
+import threading
+import time
+from pathlib import Path
+from typing import Any, Optional
+
+from runner.hashing import sha256_bytes, sha256_file, sha256_text
+from runner.hermeticity.modes import (
+ HermeticityError,
+ HermeticityMode,
+ HermeticityPolicy,
+ host_allowed,
+ path_within_partner,
+)
+from runner.providers.base import (
+ ArtifactBag,
+ BranchHandle,
+ ExecRequest,
+ ExecResult,
+ ExecStatus,
+ ExecutionProvider,
+ MetricValue,
+ ProviderError,
+ ResourceSample,
+ SnapshotHandle,
+ SnapshotRef,
+ TeardownReceipt,
+)
+
+
+class LocalFakeProvider:
+ """Filesystem sandbox provider with deterministic clocks/seeds and isolation.
+
+ Supports >=16 concurrent branches. No outbound network unless hermeticity
+ mode allows recorded fixtures or allowlisted hosts (simulated only).
+ """
+
+ name = "local-fake"
+
+ def __init__(
+ self,
+ root: Path | str,
+ *,
+ hermeticity: Optional[HermeticityPolicy] = None,
+ fixed_epoch_ms: int = 1_700_000_000_000,
+ seed: int = 0,
+ accelerator_configured: bool = False,
+ ) -> None:
+ self.root = Path(root)
+ self.root.mkdir(parents=True, exist_ok=True)
+ self.snapshots_dir = self.root / "snapshots"
+ self.branches_dir = self.root / "branches"
+ self.snapshots_dir.mkdir(exist_ok=True)
+ self.branches_dir.mkdir(exist_ok=True)
+ self.hermeticity = hermeticity
+ self.fixed_epoch_ms = fixed_epoch_ms
+ self.seed = seed
+ self.accelerator_configured = accelerator_configured
+ self._lock = threading.RLock()
+ self._branch_state: dict[str, dict[str, Any]] = {}
+ self._network_attempts: list[dict[str, Any]] = []
+
+ def register_snapshot(
+ self, snapshot_id: str, content: bytes, *, digest: Optional[str] = None
+ ) -> SnapshotHandle:
+ snap_dir = self.snapshots_dir / snapshot_id
+ snap_dir.mkdir(parents=True, exist_ok=True)
+ payload = snap_dir / "root.bin"
+ payload.write_bytes(content)
+ computed = sha256_bytes(content)
+ if digest is not None and digest != computed:
+ raise ProviderError(
+ f"snapshot digest mismatch for {snapshot_id}: {digest} != {computed}"
+ )
+ meta = {
+ "snapshot_id": snapshot_id,
+ "snapshot_digest": computed,
+ "size": len(content),
+ }
+ (snap_dir / "meta.json").write_text(
+ json.dumps(meta, sort_keys=True) + "\n", encoding="utf-8"
+ )
+ return SnapshotHandle(
+ snapshot_id=snapshot_id, snapshot_digest=computed, metadata=meta
+ )
+
+ def supports(self, mode: HermeticityMode) -> bool:
+ return mode in {
+ HermeticityMode.NO_NETWORK,
+ HermeticityMode.ALLOWLISTED_NETWORK,
+ HermeticityMode.RECORDED_RESPONSE,
+ HermeticityMode.PARTNER_LOCAL,
+ HermeticityMode.SYNTHETIC_DEPENDENCY,
+ }
+
+ def lookup_snapshot(self, ref: SnapshotRef) -> SnapshotHandle:
+ snap_dir = self.snapshots_dir / ref.snapshot_id
+ meta_path = snap_dir / "meta.json"
+ payload = snap_dir / "root.bin"
+ if not meta_path.is_file() or not payload.is_file():
+ raise ProviderError(f"snapshot not found: {ref.snapshot_id}")
+ meta = json.loads(meta_path.read_text(encoding="utf-8"))
+ digest = meta["snapshot_digest"]
+ if ref.snapshot_digest is not None and ref.snapshot_digest != digest:
+ raise ProviderError(
+ f"snapshot digest mismatch: profile requires {ref.snapshot_digest}, "
+ f"provider has {digest}"
+ )
+ recomputed = sha256_file(str(payload))
+ if recomputed != digest:
+ raise ProviderError(f"snapshot content drift for {ref.snapshot_id}")
+ return SnapshotHandle(
+ snapshot_id=ref.snapshot_id, snapshot_digest=digest, metadata=meta
+ )
+
+ def clone(self, snapshot: SnapshotHandle, *, branch_id: str) -> BranchHandle:
+ with self._lock:
+ src = self.snapshots_dir / snapshot.snapshot_id / "root.bin"
+ if not src.is_file():
+ raise ProviderError(f"missing snapshot bytes: {snapshot.snapshot_id}")
+ branch_root = self.branches_dir / branch_id
+ if branch_root.exists():
+ raise ProviderError(f"branch already exists: {branch_id}")
+ branch_root.mkdir(parents=True)
+ dest = branch_root / "root.bin"
+ shutil.copyfile(src, dest)
+ fs = branch_root / "fs"
+ fs.mkdir()
+ (fs / "SNAPSHOT_ID").write_text(snapshot.snapshot_id, encoding="utf-8")
+ (fs / "SNAPSHOT_DIGEST").write_text(
+ snapshot.snapshot_digest, encoding="utf-8"
+ )
+ (fs / "BRANCH_ID").write_text(branch_id, encoding="utf-8")
+ (fs / "SEED").write_text(str(self.seed), encoding="utf-8")
+ (fs / "CLOCK_EPOCH_MS").write_text(
+ str(self.fixed_epoch_ms), encoding="utf-8"
+ )
+ self._branch_state[branch_id] = {
+ "created_at_ms": self.fixed_epoch_ms,
+ "wall_ms": 0,
+ "cpu_ms": 0,
+ "storage_bytes": dest.stat().st_size,
+ "tool_calls": 0,
+ "api_calls": 0,
+ "network_bytes": 0,
+ "retries": 0,
+ "failed_processes": 0,
+ "verifier_invocations": 0,
+ "cancelled": False,
+ }
+ return BranchHandle(
+ branch_id=branch_id,
+ snapshot_id=snapshot.snapshot_id,
+ snapshot_digest=snapshot.snapshot_digest,
+ root_path=str(branch_root),
+ provider_ref=f"local-fake:{branch_id}",
+ )
+
+ def _enforce_pre_exec(self, request: ExecRequest) -> None:
+ policy = self.hermeticity
+ if policy is None:
+ return
+ for key, value in request.env.items():
+ if key.startswith("HTTP") or key.endswith("_URL"):
+ host = value
+ if "://" in value:
+ host = value.split("://", 1)[1].split("/", 1)[0]
+ allowed = host_allowed(host, policy)
+ self._network_attempts.append(
+ {"host": host, "allowed": allowed, "mode": policy.mode.value}
+ )
+ if not allowed and policy.mode in {
+ HermeticityMode.NO_NETWORK,
+ HermeticityMode.ALLOWLISTED_NETWORK,
+ HermeticityMode.RECORDED_RESPONSE,
+ HermeticityMode.PARTNER_LOCAL,
+ HermeticityMode.SYNTHETIC_DEPENDENCY,
+ }:
+ raise HermeticityError(
+ f"network denied by hermeticity mode {policy.mode.value}: {host}"
+ )
+ if policy.mode == HermeticityMode.PARTNER_LOCAL:
+ for path in request.intervention_paths:
+ if not path_within_partner(path, policy):
+ raise HermeticityError(
+ f"path outside partner_local boundary: {path}"
+ )
+ if policy.mode == HermeticityMode.SYNTHETIC_DEPENDENCY:
+ # Only declared synthetic digests may be referenced via env SYNTHETIC_DEP
+ declared = set(policy.synthetic_dependency_digests)
+ synth = request.env.get("SYNTHETIC_DEP_DIGEST")
+ if synth and synth not in declared:
+ raise HermeticityError(
+ f"undeclared synthetic dependency digest: {synth}"
+ )
+ if policy.mode == HermeticityMode.RECORDED_RESPONSE:
+ cassette = request.env.get("CASSETTE_DIGEST")
+ if cassette and cassette != policy.cassette_digest:
+ raise HermeticityError("cassette digest mismatch")
+
+ def execute(self, branch: BranchHandle, request: ExecRequest) -> ExecResult:
+ with self._lock:
+ state = self._branch_state.get(branch.branch_id)
+ if state is None:
+ raise ProviderError(f"unknown branch: {branch.branch_id}")
+ if state.get("cancelled"):
+ return ExecResult(
+ status=ExecStatus.CANCELLED,
+ exit_code=130,
+ stdout="",
+ stderr="cancelled",
+ wall_time_ms=0,
+ terminal_state={"reason": "CANCELLED"},
+ )
+
+ start = time.perf_counter()
+ try:
+ self._enforce_pre_exec(request)
+ except HermeticityError as exc:
+ state["failed_processes"] += 1
+ return ExecResult(
+ status=ExecStatus.FAILED,
+ exit_code=2,
+ stdout="",
+ stderr=str(exc),
+ wall_time_ms=0,
+ terminal_state={"reason": "HERMETICITY_DENIED", "error": str(exc)},
+ )
+
+ branch_root = Path(branch.root_path)
+ fs = branch_root / "fs"
+ for src in request.intervention_paths:
+ src_path = Path(src)
+ if not src_path.is_file():
+ state["failed_processes"] += 1
+ return ExecResult(
+ status=ExecStatus.ERROR,
+ exit_code=1,
+ stdout="",
+ stderr=f"intervention missing: {src}",
+ wall_time_ms=0,
+ terminal_state={"reason": "INTERVENTION_MISSING"},
+ )
+ dest = fs / "interventions" / src_path.name
+ dest.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copyfile(src_path, dest)
+
+ # Deterministic simulated work proportional to seed + command length
+ work_units = self.seed + sum(len(part) for part in request.command)
+ simulated_ms = min(request.timeout_ms, 1 + (work_units % 50))
+ if request.timeout_ms <= 0:
+ return ExecResult(
+ status=ExecStatus.TIMEOUT,
+ exit_code=124,
+ stdout="",
+ stderr="timeout",
+ wall_time_ms=0,
+ terminal_state={"reason": "TIMEOUT"},
+ )
+ if simulated_ms >= request.timeout_ms and request.command[:1] == [
+ "__timeout__"
+ ]:
+ state["failed_processes"] += 1
+ return ExecResult(
+ status=ExecStatus.TIMEOUT,
+ exit_code=124,
+ stdout="",
+ stderr="timeout",
+ wall_time_ms=request.timeout_ms,
+ terminal_state={"reason": "TIMEOUT"},
+ )
+
+ result_path = fs / "result.json"
+ terminal = {
+ "branch_id": branch.branch_id,
+ "snapshot_digest": branch.snapshot_digest,
+ "command": list(request.command),
+ "seed": self.seed,
+ "clock_epoch_ms": self.fixed_epoch_ms,
+ "status": "PASSED",
+ }
+ if request.command[:1] == ["__fail__"]:
+ terminal["status"] = "FAILED"
+ state["failed_processes"] += 1
+ status = ExecStatus.FAILED
+ exit_code = 1
+ stderr = "forced failure"
+ else:
+ status = ExecStatus.PASSED
+ exit_code = 0
+ stderr = ""
+
+ result_path.write_text(
+ json.dumps(terminal, sort_keys=True) + "\n", encoding="utf-8"
+ )
+ log_path = fs / "stdout.log"
+ log_path.write_text(
+ f"cmd={' '.join(request.command)}\nstatus={terminal['status']}\n",
+ encoding="utf-8",
+ )
+ elapsed_ms = int((time.perf_counter() - start) * 1000) or simulated_ms
+ state["wall_ms"] += elapsed_ms
+ state["cpu_ms"] += max(1, elapsed_ms // 2)
+ state["tool_calls"] += 1
+ state["storage_bytes"] = sum(
+ p.stat().st_size for p in branch_root.rglob("*") if p.is_file()
+ )
+ return ExecResult(
+ status=status,
+ exit_code=exit_code,
+ stdout=log_path.read_text(encoding="utf-8"),
+ stderr=stderr,
+ wall_time_ms=elapsed_ms,
+ terminal_state=terminal,
+ artifacts={
+ "result.json": str(result_path),
+ "stdout.log": str(log_path),
+ },
+ )
+
+ def measure(self, branch: BranchHandle) -> ResourceSample:
+ state = self._branch_state.get(branch.branch_id)
+ if state is None:
+ raise ProviderError(f"unknown branch: {branch.branch_id}")
+ accel: MetricValue
+ if self.accelerator_configured:
+ accel = MetricValue(
+ value=0, unit="count", availability="observed", source="provider"
+ )
+ else:
+ accel = MetricValue(
+ value=None,
+ unit="count",
+ availability="unavailable",
+ source="provider",
+ )
+ return ResourceSample(
+ metrics={
+ "wall_time_ms": MetricValue(
+ value=state["wall_ms"],
+ unit="ms",
+ availability="observed",
+ source="provider",
+ ),
+ "cpu_time_ms": MetricValue(
+ value=state["cpu_ms"],
+ unit="ms",
+ availability="observed",
+ source="provider",
+ ),
+ "memory_mib": MetricValue(
+ value=64,
+ unit="MiB",
+ availability="observed",
+ source="provider",
+ ),
+ "accelerator_alloc": accel,
+ "accelerator_use": accel,
+ "model_tokens": MetricValue(
+ value=0,
+ unit="tokens",
+ availability="observed",
+ source="provider",
+ ),
+ "tool_calls": MetricValue(
+ value=state["tool_calls"],
+ unit="count",
+ availability="observed",
+ source="provider",
+ ),
+ "external_api_calls": MetricValue(
+ value=state["api_calls"],
+ unit="count",
+ availability="observed",
+ source="provider",
+ ),
+ "network_bytes": MetricValue(
+ value=state["network_bytes"],
+ unit="bytes",
+ availability="observed",
+ source="provider",
+ ),
+ "storage_bytes": MetricValue(
+ value=state["storage_bytes"],
+ unit="bytes",
+ availability="observed",
+ source="provider",
+ ),
+ "retries": MetricValue(
+ value=state["retries"],
+ unit="count",
+ availability="observed",
+ source="provider",
+ ),
+ "failed_processes": MetricValue(
+ value=state["failed_processes"],
+ unit="count",
+ availability="observed",
+ source="provider",
+ ),
+ "verifier_invocations": MetricValue(
+ value=state["verifier_invocations"],
+ unit="count",
+ availability="observed",
+ source="provider",
+ ),
+ }
+ )
+
+ def extract_artifacts(self, branch: BranchHandle, paths: list[str]) -> ArtifactBag:
+ branch_root = Path(branch.root_path)
+ files: dict[str, bytes] = {}
+ digests: dict[str, str] = {}
+ for rel in paths:
+ path = branch_root / rel
+ if not path.is_file():
+ raise ProviderError(f"artifact missing: {rel}")
+ data = path.read_bytes()
+ files[rel] = data
+ digests[rel] = sha256_bytes(data)
+ return ArtifactBag(files=files, digests=digests)
+
+ def teardown(self, branch: BranchHandle) -> TeardownReceipt:
+ with self._lock:
+ branch_root = Path(branch.root_path)
+ residual: list[str] = []
+ cleaned = False
+ if branch_root.exists():
+ shutil.rmtree(branch_root)
+ cleaned = not branch_root.exists()
+ if not cleaned:
+ residual.append(str(branch_root))
+ self._branch_state.pop(branch.branch_id, None)
+ return TeardownReceipt(
+ branch_id=branch.branch_id,
+ cleaned=cleaned,
+ residual_paths=residual,
+ secrets_scrubbed=True,
+ details={"provider": self.name},
+ )
+
+ def cancel(self, branch_id: str) -> None:
+ with self._lock:
+ state = self._branch_state.get(branch_id)
+ if state is not None:
+ state["cancelled"] = True
+
+ def network_attempt_log(self) -> list[dict[str, Any]]:
+ return list(self._network_attempts)
+
+ def enforce_capability_or_fail(self, mode: HermeticityMode) -> None:
+ if not self.supports(mode):
+ raise HermeticityError(
+ f"provider {self.name} does not support hermeticity mode {mode.value}"
+ )
+
+
+def ensure_protocol(provider: LocalFakeProvider) -> ExecutionProvider:
+ """Type helper asserting LocalFakeProvider satisfies ExecutionProvider."""
+ return provider
+
+
+def branch_isolation_marker(branch: BranchHandle) -> str:
+ return sha256_text(
+ f"{branch.branch_id}:{branch.snapshot_digest}:{branch.root_path}"
+ )
diff --git a/runner/providers/morph.py b/runner/providers/morph.py
new file mode 100644
index 0000000..a2efae2
--- /dev/null
+++ b/runner/providers/morph.py
@@ -0,0 +1,257 @@
+"""Morph Cloud execution provider adapter."""
+
+from __future__ import annotations
+
+import os
+import time
+from typing import Any, Optional
+
+from runner.hermeticity.modes import (
+ HermeticityError,
+ HermeticityMode,
+ HermeticityPolicy,
+)
+from runner.providers.base import (
+ ArtifactBag,
+ BranchHandle,
+ ExecRequest,
+ ExecResult,
+ ExecStatus,
+ MetricValue,
+ ProviderError,
+ ResourceSample,
+ SnapshotHandle,
+ SnapshotRef,
+ TeardownReceipt,
+)
+
+
+class MorphCloudProvider:
+ """Morph Cloud adapter. Live cloud only; not used in default offline CI."""
+
+ name = "morph"
+
+ SUPPORTED_METRICS = frozenset(
+ {
+ "wall_time_ms",
+ "tool_calls",
+ "failed_processes",
+ "retries",
+ }
+ )
+ UNSUPPORTED_METRICS = frozenset(
+ {
+ "cpu_time_ms",
+ "memory_mib",
+ "accelerator_alloc",
+ "accelerator_use",
+ "model_tokens",
+ "external_api_calls",
+ "network_bytes",
+ "storage_bytes",
+ "verifier_invocations",
+ }
+ )
+
+ def __init__(
+ self,
+ *,
+ api_key: Optional[str] = None,
+ hermeticity: Optional[HermeticityPolicy] = None,
+ require_snapshot_digest: bool = True,
+ client: Any = None,
+ ) -> None:
+ self.api_key = api_key or os.environ.get("MORPH_API_KEY")
+ if not self.api_key and client is None:
+ raise ProviderError("MORPH_API_KEY required for MorphCloudProvider")
+ self.hermeticity = hermeticity
+ self.require_snapshot_digest = require_snapshot_digest
+ self._client = client
+ self._instances: dict[str, Any] = {}
+ self._base_instance: Any = None
+
+ def _get_client(self) -> Any:
+ if self._client is not None:
+ return self._client
+ try:
+ from morphcloud.api import MorphCloudClient
+ except ImportError as exc:
+ raise ProviderError(
+ "morphcloud package is required for MorphCloudProvider"
+ ) from exc
+ self._client = MorphCloudClient()
+ return self._client
+
+ def supports(self, mode: HermeticityMode) -> bool:
+ return mode == HermeticityMode.NO_NETWORK
+
+ def lookup_snapshot(self, ref: SnapshotRef) -> SnapshotHandle:
+ client = self._get_client()
+ try:
+ snap = client.snapshots.get(ref.snapshot_id)
+ except Exception as exc:
+ raise ProviderError(f"Morph snapshot lookup failed: {exc}") from exc
+ digest = getattr(snap, "digest", None) or getattr(snap, "sha256", None)
+ if digest is None:
+ if ref.snapshot_digest is None:
+ raise ProviderError(
+ "Morph snapshot has no digest and snapshot_ref.snapshot_digest "
+ "is unset; refusing fabricated zero-digest (fail-closed)"
+ )
+ if self.require_snapshot_digest:
+ raise ProviderError(
+ "profile requires snapshot digest but Morph did not return one"
+ )
+ # Pin from the caller-supplied ref only when Morph omits digest and
+ # require_snapshot_digest is False (explicit opt-out of Morph verify).
+ digest = ref.snapshot_digest
+ if not str(digest).startswith("sha256:"):
+ digest = f"sha256:{digest}"
+ if ref.snapshot_digest is not None and digest != ref.snapshot_digest:
+ raise ProviderError(
+ f"Morph snapshot digest mismatch: {digest} != {ref.snapshot_digest}"
+ )
+ return SnapshotHandle(
+ snapshot_id=getattr(snap, "id", ref.snapshot_id),
+ snapshot_digest=str(digest),
+ metadata={"provider": "morph"},
+ )
+
+ def clone(self, snapshot: SnapshotHandle, *, branch_id: str) -> BranchHandle:
+ client = self._get_client()
+ if self._base_instance is None:
+ try:
+ base = client.instances.start(snapshot_id=snapshot.snapshot_id)
+ base.wait_until_ready()
+ self._base_instance = base
+ except Exception as exc:
+ raise ProviderError(f"Morph base instance start failed: {exc}") from exc
+ try:
+ branches = self._base_instance.branch(count=1)
+ instance = branches[0]
+ except Exception as exc:
+ raise ProviderError(f"Morph branch failed: {exc}") from exc
+ self._instances[branch_id] = instance
+ return BranchHandle(
+ branch_id=branch_id,
+ snapshot_id=snapshot.snapshot_id,
+ snapshot_digest=snapshot.snapshot_digest,
+ root_path=f"morph://{getattr(instance, 'id', branch_id)}",
+ provider_ref=str(getattr(instance, "id", branch_id)),
+ )
+
+ def execute(self, branch: BranchHandle, request: ExecRequest) -> ExecResult:
+ if self.hermeticity is not None and not self.supports(self.hermeticity.mode):
+ raise HermeticityError(
+ f"Morph cannot enforce hermeticity mode {self.hermeticity.mode.value}"
+ )
+ instance = self._instances.get(branch.branch_id)
+ if instance is None:
+ raise ProviderError(f"unknown Morph branch: {branch.branch_id}")
+ start = time.time()
+ remote = f"/tmp/replay_{branch.branch_id}.zip"
+ try:
+ for path in request.intervention_paths:
+ instance.copy(path, remote)
+ cmd = " ".join(request.command)
+ with instance.ssh() as ssh:
+ result = ssh.run(cmd)
+ wall_ms = int((time.time() - start) * 1000)
+ timed_out = bool(getattr(result, "timed_out", False))
+ exit_code = int(getattr(result, "exit_code", 1))
+ if timed_out or wall_ms > request.timeout_ms:
+ status = ExecStatus.TIMEOUT
+ elif exit_code == 0:
+ status = ExecStatus.PASSED
+ else:
+ status = ExecStatus.FAILED
+ return ExecResult(
+ status=status,
+ exit_code=exit_code,
+ stdout=str(getattr(result, "stdout", "")),
+ stderr=str(getattr(result, "stderr", "")),
+ wall_time_ms=wall_ms,
+ terminal_state={
+ "reason": status.value,
+ "instance_id": branch.provider_ref,
+ },
+ )
+ except Exception as exc:
+ return ExecResult(
+ status=ExecStatus.ERROR,
+ exit_code=1,
+ stdout="",
+ stderr=str(exc),
+ wall_time_ms=int((time.time() - start) * 1000),
+ terminal_state={"reason": "ERROR", "error": str(exc)},
+ )
+
+ def measure(self, branch: BranchHandle) -> ResourceSample:
+ _ = branch
+ metrics: dict[str, MetricValue] = {}
+ for name in sorted(self.SUPPORTED_METRICS | self.UNSUPPORTED_METRICS):
+ if name in self.SUPPORTED_METRICS:
+ metrics[name] = MetricValue(
+ value=0,
+ unit="ms" if name.endswith("_ms") else "count",
+ availability="observed",
+ source="provider",
+ )
+ else:
+ metrics[name] = MetricValue(
+ value=None,
+ unit="count",
+ availability="unavailable",
+ source="provider",
+ )
+ metrics["wall_time_ms"] = MetricValue(
+ value=None,
+ unit="ms",
+ availability="unavailable",
+ source="provider",
+ )
+ return ResourceSample(metrics=metrics)
+
+ def extract_artifacts(self, branch: BranchHandle, paths: list[str]) -> ArtifactBag:
+ instance = self._instances.get(branch.branch_id)
+ if instance is None:
+ raise ProviderError(f"unknown Morph branch: {branch.branch_id}")
+ files: dict[str, bytes] = {}
+ digests: dict[str, str] = {}
+ from runner.hashing import sha256_bytes
+
+ for remote in paths:
+ local = f"/tmp/mrr_extract_{branch.branch_id}_{os.path.basename(remote)}"
+ try:
+ instance.copy(remote, local)
+ with open(local, "rb") as handle:
+ data = handle.read()
+ except Exception as exc:
+ raise ProviderError(f"extract failed for {remote}: {exc}") from exc
+ files[remote] = data
+ digests[remote] = sha256_bytes(data)
+ return ArtifactBag(files=files, digests=digests)
+
+ def teardown(self, branch: BranchHandle) -> TeardownReceipt:
+ instance = self._instances.pop(branch.branch_id, None)
+ cleaned = True
+ residual: list[str] = []
+ if instance is not None:
+ try:
+ instance.stop()
+ except Exception:
+ cleaned = False
+ residual.append(branch.provider_ref)
+ if not self._instances and self._base_instance is not None:
+ try:
+ self._base_instance.stop()
+ except Exception:
+ cleaned = False
+ self._base_instance = None
+ return TeardownReceipt(
+ branch_id=branch.branch_id,
+ cleaned=cleaned,
+ residual_paths=residual,
+ secrets_scrubbed=True,
+ details={"provider": self.name},
+ )
diff --git a/runner/py.typed b/runner/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/schemas/README.md b/schemas/README.md
index 33f5b07..fc32019 100644
--- a/schemas/README.md
+++ b/schemas/README.md
@@ -1,30 +1,72 @@
-# Vendored Schemas
+# Vendored / mirrored schemas
-This directory contains vendored schemas from external projects, pinned by specific tags for reproducibility.
+Pinned PCS and PIP contracts live here so offline CI can validate instances
+without requiring `pcs` or `post-incident` CLIs. Digests are recorded in each
+directory's `SCHEMA_MIRROR.json` and checked by `scripts/check_schema_mirrors.py`.
-## CERT-V1 Schema
+## Layout
-**Source**: [CERT-V1 specification](https://github.com/cert-org/cert-v1)
-**Pinned Version**: v1.0.0
-**Purpose**: Defines the certificate format for replay execution results
+| Path | Source | Role |
+|------|--------|------|
+| `mrr/` | this repo | ExecutionProfile, BranchReplayManifest, DifferentialReport, ResourceReport |
+| `pcs/` | [pcs-core](https://github.com/SentinelOps-CI/pcs-core) tag `v0.1.0` | `RuntimeReceipt.v0`, `common.defs`, ArtifactIntegrity |
+| `pip/` | [post-incident-proofs](https://github.com/SentinelOps-CI/post-incident-proofs) | MorphReplayReport, TransformationRecord, LineageBundle |
+| `cert_v1.json` / `trace_replay_kit.json` / `replay_runner.json` | legacy guest/docs | Optional guest CERT / historical TRACE; not PCS substitutes |
-## TRACE-REPLAY-KIT Schema
+## Current pins
-**Source**: [TRACE-REPLAY-KIT](https://github.com/trace-replay-kit/spec)
-**Pinned Version**: v2.1.0
-**Purpose**: Defines the replay bundle format and execution interface
+| Mirror | Manifest | Pin |
+|--------|----------|-----|
+| PCS | `pcs/SCHEMA_MIRROR.json` | tag `v0.1.0`, commit `2f0ce059652408bce19bc89ba6660030020d5ea0` |
+| PIP | `pip/SCHEMA_MIRROR.json` | commit `3cdfdf09c20f08ad5221d29607b5a9726295ad10` |
-## Schema Files
+Do **not** edit vendored files under `pcs/` or `pip/` in place. Bump the pin, re-copy, and update `local_digests`.
-- `cert_v1.json` - CERT-V1 JSON schema definition
-- `trace_replay_kit.json` - TRACE-REPLAY-KIT bundle schema
-- `replay_runner.json` - Internal schema for runner configuration and reports
+## Validation
-## Updating Schemas
+1. Mirror digests: `python scripts/check_schema_mirrors.py`
+2. Instance conformance (always-on): `python scripts/validate_vendored_instances.py`
+3. Optional deeper CLI gates when installed: `python scripts/optional_external_validate.py`
-To update to newer versions:
+Emitters call `runner.evidence.validate.validate_instance` fail-closed before writing evidence.
-1. Download the latest schema from the source repository
-2. Update the version tag in this README
-3. Test compatibility with existing code
-4. Update any validation logic if needed
+## How to refresh a mirror
+
+From a checked-out sibling checkout at the desired pin:
+
+```bash
+# PCS (example paths; adjust to your local checkout)
+cp /path/to/pcs-core/schemas/RuntimeReceipt.v0.schema.json schemas/pcs/
+cp /path/to/pcs-core/schemas/common.defs.json schemas/pcs/
+cp /path/to/pcs-core/schemas/ArtifactIntegrity.v1.schema.json schemas/pcs/
+
+# PIP Morph / lineage schemas used by this runner
+cp /path/to/post-incident-proofs/schemas/pip/v1/LineageBundle.schema.json schemas/pip/
+cp /path/to/post-incident-proofs/schemas/pip/v1/MorphReplayReport.schema.json schemas/pip/
+cp /path/to/post-incident-proofs/schemas/pip/v1/TransformationRecord.schema.json schemas/pip/
+```
+
+Then recompute digests and update the corresponding `SCHEMA_MIRROR.json`:
+
+```bash
+python - <<'PY'
+import hashlib, json, pathlib
+root = pathlib.Path("schemas/pcs") # or schemas/pip
+files = json.loads((root / "SCHEMA_MIRROR.json").read_text())["vendored_files"]
+digests = {
+ name: "sha256:" + hashlib.sha256((root / name).read_bytes()).hexdigest()
+ for name in files
+}
+print(json.dumps(digests, indent=2))
+PY
+```
+
+Update `source_tag` / `pinned_commit` / `pin_rationale` in the manifest, then:
+
+```bash
+python scripts/check_schema_mirrors.py
+python scripts/validate_vendored_instances.py
+```
+
+Mirroring schemas does not imply PF-Core / OVK runtime integration on the default path.
+See [NON_CLAIMS.md](../NON_CLAIMS.md).
diff --git a/schemas/mrr/branch_replay_manifest.v1.json b/schemas/mrr/branch_replay_manifest.v1.json
new file mode 100644
index 0000000..9bfc89d
--- /dev/null
+++ b/schemas/mrr/branch_replay_manifest.v1.json
@@ -0,0 +1,125 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://morph-replay-runner.local/schemas/mrr/branch_replay_manifest.v1.json",
+ "title": "BranchReplayManifest",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "schema_version",
+ "manifest_id",
+ "incident_ref",
+ "snapshot_ref",
+ "execution_profile_digest",
+ "branches",
+ "retry_policy",
+ "cancellation_policy",
+ "branch_independence"
+ ],
+ "properties": {
+ "schema_version": { "const": "mrr.BranchReplayManifest.v1" },
+ "manifest_id": { "type": "string", "minLength": 1 },
+ "incident_ref": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["incident_id", "incident_digest"],
+ "properties": {
+ "incident_id": { "type": "string", "minLength": 1 },
+ "incident_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ }
+ }
+ },
+ "snapshot_ref": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["snapshot_id", "snapshot_digest"],
+ "properties": {
+ "snapshot_id": { "type": "string", "minLength": 1 },
+ "snapshot_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ }
+ }
+ },
+ "execution_profile_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "branches": {
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["branch_id", "intervention", "action_sequence"],
+ "properties": {
+ "branch_id": { "type": "string", "minLength": 1 },
+ "intervention": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["intervention_id", "artifact_paths"],
+ "properties": {
+ "intervention_id": { "type": "string", "minLength": 1 },
+ "artifact_paths": {
+ "type": "array",
+ "items": { "type": "string", "minLength": 1 }
+ },
+ "digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ }
+ }
+ },
+ "action_sequence": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "type": "string", "minLength": 1 }
+ },
+ "policy_checkpoint": { "type": "string" },
+ "verifier_profiles": {
+ "type": "array",
+ "items": { "type": "string", "minLength": 1 }
+ },
+ "expected_output_classes": {
+ "type": "array",
+ "items": { "type": "string", "minLength": 1 }
+ },
+ "claim_class": {
+ "type": "string",
+ "enum": [
+ "runtime_observed",
+ "RuntimeObserved",
+ "RuntimeChecked",
+ "ReplayValidated"
+ ]
+ }
+ }
+ }
+ },
+ "retry_policy": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["max_retries", "retry_on"],
+ "properties": {
+ "max_retries": { "type": "integer", "minimum": 0 },
+ "retry_on": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["TIMEOUT", "ERROR", "FAILED"]
+ }
+ }
+ }
+ },
+ "cancellation_policy": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["cancel_others_on_failure"],
+ "properties": {
+ "cancel_others_on_failure": { "type": "boolean" }
+ }
+ },
+ "branch_independence": { "type": "boolean" }
+ }
+}
diff --git a/schemas/mrr/differential_report.v1.json b/schemas/mrr/differential_report.v1.json
new file mode 100644
index 0000000..33bf79c
--- /dev/null
+++ b/schemas/mrr/differential_report.v1.json
@@ -0,0 +1,73 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://morph-replay-runner.local/schemas/mrr/differential_report.v1.json",
+ "title": "DifferentialReport",
+ "description": "Pairwise differential state. absent ≠ equal. No preferred-branch or causality language.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "schema_version",
+ "left_branch_id",
+ "right_branch_id",
+ "comparisons",
+ "report_digest"
+ ],
+ "properties": {
+ "schema_version": { "const": "mrr.DifferentialReport.v1" },
+ "left_branch_id": { "type": "string", "minLength": 1 },
+ "right_branch_id": { "type": "string", "minLength": 1 },
+ "comparisons": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "terminal_state",
+ "process_action",
+ "authorization",
+ "side_effect",
+ "reward",
+ "verifier_decision",
+ "resource",
+ "unresolved_external_dependency"
+ ],
+ "properties": {
+ "terminal_state": { "$ref": "#/$defs/field_comparison" },
+ "process_action": { "$ref": "#/$defs/field_comparison" },
+ "authorization": { "$ref": "#/$defs/field_comparison" },
+ "side_effect": { "$ref": "#/$defs/field_comparison" },
+ "reward": { "$ref": "#/$defs/field_comparison" },
+ "verifier_decision": { "$ref": "#/$defs/field_comparison" },
+ "resource": { "$ref": "#/$defs/field_comparison" },
+ "unresolved_external_dependency": { "$ref": "#/$defs/field_comparison" }
+ }
+ },
+ "report_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "non_claims": {
+ "type": "array",
+ "items": { "type": "string" }
+ }
+ },
+ "$defs": {
+ "field_comparison": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["result"],
+ "properties": {
+ "result": {
+ "type": "string",
+ "enum": [
+ "equal",
+ "different",
+ "absent_left",
+ "absent_right",
+ "absent_both"
+ ]
+ },
+ "left": {},
+ "right": {}
+ }
+ }
+ }
+}
diff --git a/schemas/mrr/execution_profile.v1.json b/schemas/mrr/execution_profile.v1.json
new file mode 100644
index 0000000..2b86e53
--- /dev/null
+++ b/schemas/mrr/execution_profile.v1.json
@@ -0,0 +1,204 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://morph-replay-runner.local/schemas/mrr/execution_profile.v1.json",
+ "title": "ExecutionProfile",
+ "description": "Pinned hermetic execution profile. Secrets by reference only.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "schema_version",
+ "profile_id",
+ "snapshot",
+ "container_image_digests",
+ "environment",
+ "os_runtime",
+ "dependency_lock_digest",
+ "network_policy",
+ "clock_policy",
+ "random_seed_policy",
+ "tool_versions",
+ "budgets",
+ "secret_refs",
+ "output_retention",
+ "source_commit",
+ "integrity"
+ ],
+ "properties": {
+ "schema_version": { "const": "mrr.ExecutionProfile.v1" },
+ "profile_id": { "type": "string", "minLength": 1 },
+ "profile_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$",
+ "description": "Optional precomputed digest; recomputed on validate."
+ },
+ "snapshot": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["snapshot_id", "snapshot_digest"],
+ "properties": {
+ "snapshot_id": { "type": "string", "minLength": 1 },
+ "snapshot_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ }
+ }
+ },
+ "container_image_digests": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
+ },
+ "environment": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["profile_name", "profile_version"],
+ "properties": {
+ "profile_name": { "type": "string", "minLength": 1 },
+ "profile_version": { "type": "string", "minLength": 1 }
+ }
+ },
+ "os_runtime": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["os", "runtime"],
+ "properties": {
+ "os": { "type": "string", "minLength": 1 },
+ "runtime": { "type": "string", "minLength": 1 }
+ }
+ },
+ "dependency_lock_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "network_policy": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["mode"],
+ "properties": {
+ "mode": {
+ "type": "string",
+ "enum": [
+ "no_network",
+ "allowlisted_network",
+ "recorded_response",
+ "partner_local",
+ "synthetic_dependency"
+ ]
+ },
+ "allowlist_hosts": {
+ "type": "array",
+ "items": { "type": "string", "minLength": 1 }
+ },
+ "cassette_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "partner_local_paths": {
+ "type": "array",
+ "items": { "type": "string", "minLength": 1 }
+ },
+ "synthetic_dependency_digests": {
+ "type": "array",
+ "items": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
+ }
+ }
+ },
+ "clock_policy": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["mode", "fixed_epoch_ms"],
+ "properties": {
+ "mode": { "type": "string", "enum": ["frozen", "offset"] },
+ "fixed_epoch_ms": { "type": "integer", "minimum": 0 }
+ }
+ },
+ "random_seed_policy": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["seed"],
+ "properties": {
+ "seed": { "type": "integer", "minimum": 0 }
+ }
+ },
+ "tool_versions": {
+ "type": "object",
+ "additionalProperties": { "type": "string", "minLength": 1 },
+ "minProperties": 1
+ },
+ "model": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "endpoint": { "type": "string", "minLength": 1 },
+ "version": { "type": "string", "minLength": 1 },
+ "local_checkpoint_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ }
+ }
+ },
+ "budgets": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "cpu_cores",
+ "memory_mib",
+ "storage_mib",
+ "wall_time_ms",
+ "token_budget",
+ "api_call_budget"
+ ],
+ "properties": {
+ "cpu_cores": { "type": "integer", "minimum": 1 },
+ "memory_mib": { "type": "integer", "minimum": 1 },
+ "accelerator_count": { "type": "integer", "minimum": 0 },
+ "storage_mib": { "type": "integer", "minimum": 1 },
+ "wall_time_ms": { "type": "integer", "minimum": 1 },
+ "token_budget": { "type": "integer", "minimum": 0 },
+ "api_call_budget": { "type": "integer", "minimum": 0 }
+ }
+ },
+ "secret_refs": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["secret_ref"],
+ "properties": {
+ "secret_ref": {
+ "type": "string",
+ "minLength": 1,
+ "pattern": "^[A-Za-z0-9_./:-]+$",
+ "description": "Identifier only; secret values are rejected."
+ },
+ "purpose": { "type": "string" }
+ }
+ }
+ },
+ "output_retention": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["mode", "retention_days"],
+ "properties": {
+ "mode": { "type": "string", "enum": ["ephemeral", "retain"] },
+ "retention_days": { "type": "integer", "minimum": 0 }
+ }
+ },
+ "source_commit": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{40}$"
+ },
+ "integrity": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["canonicalization_version"],
+ "properties": {
+ "canonicalization_version": { "const": "v1" },
+ "artifact_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ }
+ }
+ }
+ }
+}
diff --git a/schemas/mrr/resource_report.v1.json b/schemas/mrr/resource_report.v1.json
new file mode 100644
index 0000000..c0e443a
--- /dev/null
+++ b/schemas/mrr/resource_report.v1.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://morph-replay-runner.local/schemas/mrr/resource_report.v1.json",
+ "title": "ResourceReport",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["schema_version", "branch_id", "metrics"],
+ "properties": {
+ "schema_version": { "const": "mrr.ResourceReport.v1" },
+ "branch_id": { "type": "string", "minLength": 1 },
+ "metrics": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["value", "unit", "availability", "source"],
+ "properties": {
+ "value": {
+ "type": ["number", "string", "null"]
+ },
+ "unit": { "type": "string", "minLength": 1 },
+ "availability": {
+ "type": "string",
+ "enum": ["observed", "unavailable", "not_applicable"]
+ },
+ "source": {
+ "type": "string",
+ "enum": ["provider", "runner", "guest"]
+ }
+ }
+ }
+ },
+ "budget_violations": {
+ "type": "array",
+ "items": { "type": "string" }
+ }
+ }
+}
diff --git a/schemas/pcs/ArtifactIntegrity.v1.schema.json b/schemas/pcs/ArtifactIntegrity.v1.schema.json
new file mode 100644
index 0000000..f4c0fd6
--- /dev/null
+++ b/schemas/pcs/ArtifactIntegrity.v1.schema.json
@@ -0,0 +1,28 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://pcs.sentinelops.ci/schemas/ArtifactIntegrity.v1.schema.json",
+ "title": "ArtifactIntegrity.v1",
+ "description": "Normative v1 integrity envelope separating content digest from cryptographic signature. Domain-separated signing message: PCS:::.",
+ "type": "object",
+ "required": [
+ "schema_version",
+ "artifact_type",
+ "canonicalization_version",
+ "artifact_digest",
+ "signature"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "schema_version": { "$ref": "common.defs.json#/$defs/schema_version_v1" },
+ "artifact_type": { "type": "string", "minLength": 1 },
+ "canonicalization_version": {
+ "$ref": "common.defs.json#/$defs/canonicalization_version"
+ },
+ "artifact_digest": { "$ref": "common.defs.json#/$defs/hex_digest" },
+ "signature": { "$ref": "common.defs.json#/$defs/ed25519_signature_envelope" },
+ "signature_or_digest": {
+ "description": "Forbidden on v1 integrity envelopes; retained only for documentation of v0 compatibility readers.",
+ "not": {}
+ }
+ }
+}
diff --git a/schemas/pcs/RuntimeReceipt.v0.schema.json b/schemas/pcs/RuntimeReceipt.v0.schema.json
new file mode 100644
index 0000000..98a034c
--- /dev/null
+++ b/schemas/pcs/RuntimeReceipt.v0.schema.json
@@ -0,0 +1,55 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://pcs.sentinelops.ci/schemas/RuntimeReceipt.v0.schema.json",
+ "title": "RuntimeReceipt.v0",
+ "type": "object",
+ "required": [
+ "receipt_id",
+ "schema_version",
+ "run_id",
+ "environment",
+ "started_at",
+ "ended_at",
+ "status",
+ "run_outcome",
+ "final_reason_code",
+ "released",
+ "events_hash",
+ "policy_hash",
+ "trace_hash",
+ "producer",
+ "producer_version",
+ "source_repo",
+ "source_commit",
+ "input_hashes",
+ "output_hashes",
+ "signature_or_digest"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "receipt_id": { "type": "string", "minLength": 1 },
+ "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" },
+ "run_id": { "type": "string", "minLength": 1 },
+ "environment": {
+ "type": "object",
+ "additionalProperties": { "type": "string" }
+ },
+ "started_at": { "$ref": "common.defs.json#/$defs/iso8601_datetime" },
+ "ended_at": { "$ref": "common.defs.json#/$defs/iso8601_datetime" },
+ "status": { "$ref": "common.defs.json#/$defs/artifact_status" },
+ "run_outcome": { "type": "string", "enum": ["passed", "failed"] },
+ "final_reason_code": { "type": "string", "minLength": 1 },
+ "released": { "type": "boolean" },
+ "events_hash": { "$ref": "common.defs.json#/$defs/hex_digest" },
+ "policy_hash": { "$ref": "common.defs.json#/$defs/hex_digest" },
+ "trace_hash": { "$ref": "common.defs.json#/$defs/hex_digest" },
+ "producer": { "type": "string", "minLength": 1 },
+ "producer_version": { "type": "string", "minLength": 1 },
+ "source_repo": { "type": "string", "format": "uri" },
+ "source_commit": { "type": "string", "minLength": 7 },
+ "local_dev": { "type": "boolean" },
+ "input_hashes": { "$ref": "common.defs.json#/$defs/hash_map" },
+ "output_hashes": { "$ref": "common.defs.json#/$defs/hash_map" },
+ "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" }
+ }
+}
diff --git a/schemas/pcs/SCHEMA_MIRROR.json b/schemas/pcs/SCHEMA_MIRROR.json
new file mode 100644
index 0000000..cda364b
--- /dev/null
+++ b/schemas/pcs/SCHEMA_MIRROR.json
@@ -0,0 +1,22 @@
+{
+ "mirror_name": "pcs-core",
+ "source_repo": "https://github.com/SentinelOps-CI/pcs-core",
+ "source_tag": "v0.1.0",
+ "pinned_commit": "2f0ce059652408bce19bc89ba6660030020d5ea0",
+ "pin_rationale": "pcs-core tag v0.1.0 for RuntimeReceipt.v0; schemas mirrored byte-for-byte for offline validation.",
+ "vendored_files": [
+ "RuntimeReceipt.v0.schema.json",
+ "common.defs.json",
+ "ArtifactIntegrity.v1.schema.json"
+ ],
+ "local_digests": {
+ "RuntimeReceipt.v0.schema.json": "sha256:382506ac0b923a0ee88a9bdadf47827e2aa9afe9c2c50b10f7f7e73df8fc1241",
+ "common.defs.json": "sha256:f1eabfec01af7626347da861e8c95675faf4fd90f05d98b5ecd5b4c0d6b4e2a1",
+ "ArtifactIntegrity.v1.schema.json": "sha256:2468607a3e42d60c9ff0c1b4f461df81207581a3aa81e8dd3544ff197a052f54"
+ },
+ "sync_policy": "Fail CI if local file digests drift from local_digests. Do not edit vendored schemas in-place; bump pin and re-vendor.",
+ "non_claims": [
+ "Mirroring schemas does not imply PF-Core/OVK runtime integration on the default Morph path.",
+ "Runner emits observational RuntimeReceipt.v0 only; claim class is never upgraded."
+ ]
+}
diff --git a/schemas/pcs/common.defs.json b/schemas/pcs/common.defs.json
new file mode 100644
index 0000000..dccc394
--- /dev/null
+++ b/schemas/pcs/common.defs.json
@@ -0,0 +1,558 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://pcs.sentinelops.ci/schemas/common.defs.json",
+ "title": "PCS Common Definitions",
+ "$defs": {
+ "schema_version": {
+ "type": "string",
+ "pattern": "^v0$"
+ },
+ "iso8601_datetime": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "hex_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "relative_posix_path": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 4096,
+ "description": "Relative POSIX path: no absolute roots, drive letters, UNC, empty segments, or parent (..) traversal.",
+ "pattern": "^(?!/)(?![A-Za-z]:)(?!\\\\\\\\)(?!.*(?:^|/)\\.\\.(?:/|$))[A-Za-z0-9._-]+(?:/[A-Za-z0-9._-]+)*$"
+ },
+ "canonicalization_version": {
+ "type": "string",
+ "const": "v1",
+ "description": "PCS Canonical JSON algorithm version used for artifact_digest / signature_or_digest."
+ },
+ "schema_version_v1": {
+ "type": "string",
+ "pattern": "^v1$"
+ },
+ "ed25519_signature_envelope": {
+ "type": "object",
+ "required": ["algorithm", "key_id", "signed_at", "value"],
+ "additionalProperties": false,
+ "properties": {
+ "algorithm": { "const": "ed25519" },
+ "key_id": { "type": "string", "minLength": 1, "maxLength": 256 },
+ "signed_at": { "$ref": "#/$defs/iso8601_datetime" },
+ "value": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Base64url or hex encoding of the ed25519 signature over the domain-separated message."
+ }
+ }
+ },
+ "uuid": {
+ "type": "string",
+ "format": "uuid"
+ },
+ "duration": {
+ "type": "string",
+ "format": "duration"
+ },
+ "hostname": {
+ "type": "string",
+ "format": "hostname"
+ },
+ "email": {
+ "type": "string",
+ "format": "email"
+ },
+ "kernel_file_entry": {
+ "type": "object",
+ "required": ["path", "sha256"],
+ "additionalProperties": false,
+ "properties": {
+ "path": { "$ref": "#/$defs/relative_posix_path" },
+ "sha256": { "$ref": "#/$defs/hex_digest" }
+ }
+ },
+ "artifact_status": {
+ "type": "string",
+ "enum": [
+ "Draft",
+ "Extracted",
+ "HumanReviewed",
+ "Formalized",
+ "ProofPending",
+ "ProofChecked",
+ "CertificatePending",
+ "CertificateChecked",
+ "RuntimeObserved",
+ "RuntimeChecked",
+ "Rejected",
+ "EmpiricalOnly",
+ "Deprecated",
+ "Stale"
+ ]
+ },
+ "trace_certificate_status": {
+ "type": "string",
+ "enum": ["CertificatePending", "CertificateChecked", "Rejected", "Stale"]
+ },
+ "producer_metadata": {
+ "type": "object",
+ "required": [
+ "schema_version",
+ "created_at",
+ "producer",
+ "producer_version",
+ "source_repo",
+ "source_commit",
+ "status",
+ "signature_or_digest"
+ ],
+ "properties": {
+ "schema_version": { "$ref": "#/$defs/schema_version" },
+ "created_at": { "$ref": "#/$defs/iso8601_datetime" },
+ "producer": { "type": "string", "minLength": 1 },
+ "producer_version": { "type": "string", "minLength": 1 },
+ "source_repo": { "type": "string", "format": "uri" },
+ "source_commit": { "type": "string", "minLength": 7 },
+ "status": { "$ref": "#/$defs/artifact_status" },
+ "signature_or_digest": { "$ref": "#/$defs/hex_digest" }
+ }
+ },
+ "hash_map": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#/$defs/hex_digest" }
+ },
+ "ref_list": {
+ "type": "array",
+ "items": { "type": "string", "minLength": 1 }
+ },
+ "git_commit": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{40}$"
+ },
+ "release_status": {
+ "type": "string",
+ "enum": ["Draft", "Validated", "Rejected", "Stale", "Deprecated"]
+ },
+ "handoff_kind": {
+ "type": "string",
+ "enum": [
+ "runtime_to_certificate",
+ "certificate_to_bundle",
+ "bundle_to_verifier",
+ "signed_bundle_to_memory",
+ "release_chain"
+ ]
+ },
+ "handoff_status": {
+ "type": "string",
+ "enum": ["Draft", "Validated", "Rejected", "Stale", "Deprecated"]
+ },
+ "release_chain_validation_status": {
+ "type": "string",
+ "enum": ["ProofChecked", "Rejected", "Stale"]
+ },
+ "validation_check_status": {
+ "type": "string",
+ "enum": ["passed", "failed", "warning", "skipped"]
+ },
+ "registry_check_coverage_status": {
+ "type": "string",
+ "enum": ["deferred", "skipped"]
+ },
+ "enforcement_location": {
+ "type": "string",
+ "enum": ["artifact_validate", "release_chain", "consumer", "registry_metadata"]
+ },
+ "deferred_registry_check": {
+ "type": "object",
+ "required": [
+ "registry_ref",
+ "status",
+ "enforcement_location",
+ "responsible_component",
+ "reason"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "registry_ref": { "type": "string", "minLength": 1 },
+ "status": { "$ref": "#/$defs/registry_check_coverage_status" },
+ "enforcement_location": { "$ref": "#/$defs/enforcement_location" },
+ "responsible_component": { "type": "string", "minLength": 1 },
+ "reason": { "type": "string", "minLength": 1 }
+ }
+ },
+ "producer_repo_pin": {
+ "type": "object",
+ "required": ["repo", "commit"],
+ "additionalProperties": false,
+ "properties": {
+ "repo": { "type": "string", "format": "uri" },
+ "commit": { "$ref": "#/$defs/git_commit" },
+ "local_dev": { "type": "boolean" }
+ }
+ },
+ "manifest_artifact_entry": {
+ "type": "object",
+ "required": [
+ "artifact_type",
+ "schema",
+ "producer",
+ "source_repo",
+ "source_commit",
+ "sha256"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "artifact_type": { "type": "string", "minLength": 1 },
+ "schema": { "type": "string", "minLength": 1 },
+ "producer": { "type": "string", "minLength": 1 },
+ "source_repo": { "type": "string", "format": "uri" },
+ "source_commit": { "$ref": "#/$defs/git_commit" },
+ "sha256": { "$ref": "#/$defs/hex_digest" },
+ "local_dev": { "type": "boolean" }
+ }
+ },
+ "handoff_artifact_ref": {
+ "type": "object",
+ "required": ["artifact_type"],
+ "additionalProperties": false,
+ "properties": {
+ "artifact_type": { "type": "string", "minLength": 1 },
+ "sha256": { "$ref": "#/$defs/hex_digest" }
+ }
+ },
+ "semantic_check_severity": {
+ "type": "string",
+ "enum": [
+ "required",
+ "optional",
+ "warning_only",
+ "release_blocking",
+ "producer_responsible",
+ "consumer_responsible",
+ "validator_responsible"
+ ]
+ },
+ "registry_semantic_check": {
+ "type": "object",
+ "required": [
+ "check_id",
+ "severity",
+ "responsible_component",
+ "execution_required_in_release_mode",
+ "allowed_to_skip"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "check_id": { "type": "string", "minLength": 1 },
+ "severity": { "$ref": "#/$defs/semantic_check_severity" },
+ "responsible_component": { "type": "string", "minLength": 1 },
+ "execution_required_in_release_mode": { "type": "boolean" },
+ "allowed_to_skip": { "type": "boolean" }
+ }
+ },
+ "release_validation_check": {
+ "type": "object",
+ "required": [
+ "check_id",
+ "description",
+ "status",
+ "details",
+ "registry_check_refs",
+ "responsible_component"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "check_id": { "type": "string", "minLength": 1 },
+ "description": { "type": "string", "minLength": 1 },
+ "status": { "$ref": "#/$defs/validation_check_status" },
+ "details": { "type": "object" },
+ "registry_check_refs": {
+ "type": "array",
+ "items": { "type": "string", "minLength": 1 }
+ },
+ "responsible_component": { "type": "string", "minLength": 1 }
+ }
+ },
+ "chain_root": {
+ "type": "object",
+ "required": [
+ "trace_hash",
+ "certificate_id",
+ "certified_bundle_hash",
+ "signed_bundle_hash"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "trace_hash": { "$ref": "#/$defs/hex_digest" },
+ "certificate_id": { "type": "string", "minLength": 1 },
+ "certified_bundle_hash": { "$ref": "#/$defs/hex_digest" },
+ "signed_bundle_hash": { "$ref": "#/$defs/hex_digest" }
+ }
+ },
+ "manifest_path_ref": {
+ "type": "object",
+ "required": ["path", "sha256"],
+ "additionalProperties": false,
+ "properties": {
+ "path": { "type": "string", "minLength": 1 },
+ "sha256": { "$ref": "#/$defs/hex_digest" }
+ }
+ },
+ "limitations_notice": {
+ "type": "string",
+ "minLength": 1
+ },
+ "formal_check": {
+ "type": "object",
+ "required": ["check_id", "status", "artifact"],
+ "additionalProperties": false,
+ "properties": {
+ "check_id": { "type": "string", "minLength": 1 },
+ "status": { "$ref": "#/$defs/validation_check_status" },
+ "artifact": { "type": "string", "minLength": 1 },
+ "lean_theorem": { "type": "string", "minLength": 1 }
+ }
+ },
+ "benchmark_case_kind": {
+ "type": "string",
+ "enum": [
+ "valid_release",
+ "invalid_hash_mismatch",
+ "invalid_certificate",
+ "invalid_handoff",
+ "invalid_registry",
+ "invalid_formal_check",
+ "invalid_import",
+ "invalid_render",
+ "stale_release"
+ ]
+ },
+ "benchmark_responsible_component": {
+ "type": "string",
+ "enum": [
+ "runtime_producer",
+ "certificate_producer",
+ "verifier",
+ "registry",
+ "formal_kernel",
+ "scientific_memory",
+ "release_manifest",
+ "handoff",
+ "hashing",
+ "unknown"
+ ]
+ },
+ "benchmark_repair_hint_kind": {
+ "type": "string",
+ "enum": [
+ "align_provenance",
+ "align_hash",
+ "align_certificate_id",
+ "align_handoff",
+ "fix_registry_entry",
+ "rerun_verification",
+ "rerun_formal_check",
+ "fix_import_report",
+ "fix_render",
+ "none",
+ "unknown"
+ ]
+ },
+ "benchmark_metric_name": {
+ "type": "string",
+ "enum": [
+ "release_reproducibility",
+ "failure_localization",
+ "certificate_completeness",
+ "registry_coverage",
+ "formal_check_coverage",
+ "scientific_memory_interpretability",
+ "repair_hint_quality",
+ "cross_domain_portability"
+ ]
+ },
+ "benchmark_metric_applicability": {
+ "type": "string",
+ "enum": [
+ "measured",
+ "not_applicable",
+ "insufficient_cases",
+ "skipped",
+ "failed_to_measure"
+ ]
+ },
+ "benchmark_metric_id": {
+ "type": "string",
+ "enum": [
+ "release_reproducibility_score",
+ "failure_localization_accuracy",
+ "certificate_completeness_score",
+ "registry_coverage_score",
+ "formal_check_coverage_score",
+ "scientific_memory_interpretability_score",
+ "repair_hint_quality_score",
+ "cross_domain_portability_score"
+ ]
+ },
+ "benchmark_producer_id": {
+ "type": "string",
+ "enum": [
+ "pcs-core",
+ "pcs-bench",
+ "provability-fabric",
+ "certifyedge",
+ "labtrust-gym",
+ "scientific-memory"
+ ]
+ },
+ "explain_quality_section_id": {
+ "type": "string",
+ "enum": [
+ "provenance",
+ "hashes",
+ "handoffs",
+ "verification",
+ "formal_checks",
+ "limitations",
+ "lineage",
+ "repair_hints"
+ ]
+ },
+ "benchmark_observed_status": {
+ "type": "string",
+ "enum": ["passed", "failed", "skipped", "error"]
+ },
+ "benchmark_system_outcome": {
+ "type": "string",
+ "enum": [
+ "admitted",
+ "rejected",
+ "stale",
+ "import_failed",
+ "render_failed",
+ "query_failed",
+ "comparison_failed",
+ "formal_failed",
+ "certificate_rejected",
+ "unknown"
+ ]
+ },
+ "nullable_benchmark_system_outcome": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "#/$defs/benchmark_system_outcome" }
+ ]
+ },
+ "benchmark_detection_layer": {
+ "type": "string",
+ "enum": [
+ "runtime",
+ "certificate",
+ "verifier",
+ "registry",
+ "formal_kernel",
+ "scientific_memory",
+ "release_manifest",
+ "handoff",
+ "hashing",
+ "labtrust",
+ "certifyedge",
+ "provability_fabric",
+ "pcs_core",
+ "unknown"
+ ]
+ },
+ "metric_summary_applicability": {
+ "type": "string",
+ "enum": [
+ "measured",
+ "not_applicable",
+ "insufficient_cases",
+ "skipped",
+ "failed_to_measure"
+ ]
+ },
+ "system_admission_outcome": {
+ "type": "string",
+ "enum": ["admitted", "rejected", "deferred", "not_evaluated"]
+ },
+ "release_chain_status": {
+ "type": "string",
+ "enum": ["valid", "invalid", "not_applicable"]
+ },
+ "certificate_status_value": {
+ "type": "string",
+ "enum": ["CertificateChecked", "Rejected", "Stale", "not_applicable"]
+ },
+ "scientific_memory_import_status": {
+ "type": "string",
+ "enum": ["passed", "failed", "not_applicable"]
+ },
+ "scientific_memory_render_status": {
+ "type": "string",
+ "enum": ["rendered", "incomplete", "not_applicable"]
+ },
+ "nullable_benchmark_responsible_component": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "#/$defs/benchmark_responsible_component" }
+ ]
+ },
+ "nullable_benchmark_repair_hint_kind": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "#/$defs/benchmark_repair_hint_kind" }
+ ]
+ },
+ "benchmark_input_artifact_ref": {
+ "type": "object",
+ "required": ["path"],
+ "additionalProperties": false,
+ "properties": {
+ "path": { "type": "string", "minLength": 1 },
+ "artifact_type": { "type": "string", "minLength": 1 },
+ "role": { "type": "string", "minLength": 1 }
+ }
+ },
+ "benchmark_artifact_ref_type": {
+ "type": "string",
+ "enum": [
+ "BenchmarkCase.v0",
+ "BenchmarkRun.v0",
+ "CoverageReport.v0",
+ "FailureLocalizationResult.v0",
+ "ExplainQualityReport.v0",
+ "ProfileCoverageReport.v0",
+ "MetricSummary.v0"
+ ]
+ },
+ "benchmark_artifact_ref_role": {
+ "type": "string",
+ "enum": ["producer_export", "ingest_bundle", "primary"]
+ },
+ "benchmark_command_entry": {
+ "type": "object",
+ "required": ["command", "exit_code"],
+ "additionalProperties": false,
+ "properties": {
+ "command": { "type": "string", "minLength": 1 },
+ "exit_code": { "type": "integer" },
+ "stdout_digest": { "$ref": "#/$defs/hex_digest" }
+ }
+ },
+ "conformance_run_ref": {
+ "type": "object",
+ "required": ["suite", "run_id"],
+ "additionalProperties": false,
+ "properties": {
+ "suite": { "type": "string", "minLength": 1 },
+ "run_id": { "type": "string", "minLength": 1 },
+ "status": {
+ "type": "string",
+ "enum": ["passed", "failed"]
+ }
+ }
+ }
+ }
+}
diff --git a/schemas/pip/LineageBundle.schema.json b/schemas/pip/LineageBundle.schema.json
new file mode 100644
index 0000000..2df15a1
--- /dev/null
+++ b/schemas/pip/LineageBundle.schema.json
@@ -0,0 +1,59 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://post-incident-proofs.local/schemas/pip/v1/LineageBundle.schema.json",
+ "title": "LineageBundle",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "schema_version",
+ "bundle_id",
+ "incident_id",
+ "nodes",
+ "edges",
+ "transformations"
+ ],
+ "properties": {
+ "schema_version": { "const": "pip.LineageBundle.v1" },
+ "bundle_id": { "type": "string", "minLength": 1 },
+ "incident_id": { "type": "string", "minLength": 1 },
+ "nodes": {
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["node_id", "artifact_digest", "role"],
+ "properties": {
+ "node_id": { "type": "string", "minLength": 1 },
+ "artifact_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "role": {
+ "type": "string",
+ "enum": ["source", "intermediate", "output", "replay"]
+ }
+ }
+ }
+ },
+ "edges": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["from_node_id", "to_node_id", "transformation_id"],
+ "properties": {
+ "from_node_id": { "type": "string", "minLength": 1 },
+ "to_node_id": { "type": "string", "minLength": 1 },
+ "transformation_id": { "type": "string", "minLength": 1 }
+ }
+ }
+ },
+ "transformations": {
+ "type": "array",
+ "items": {
+ "$ref": "TransformationRecord.schema.json"
+ }
+ }
+ }
+}
diff --git a/schemas/pip/MorphReplayReport.schema.json b/schemas/pip/MorphReplayReport.schema.json
new file mode 100644
index 0000000..867891a
--- /dev/null
+++ b/schemas/pip/MorphReplayReport.schema.json
@@ -0,0 +1,43 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://post-incident-proofs.local/schemas/pip/v1/MorphReplayReport.schema.json",
+ "title": "MorphReplayReport",
+ "description": "Local PIP subset of Morph branch-replay report fields. Does not vendor Morph runtime.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "schema_version",
+ "replay_id",
+ "branch_id",
+ "replay_identity_digest",
+ "input_artifact_digests",
+ "output_artifact_digests",
+ "status"
+ ],
+ "properties": {
+ "schema_version": { "const": "pip.MorphReplayReport.v1" },
+ "replay_id": { "type": "string", "minLength": 1 },
+ "branch_id": { "type": "string", "minLength": 1 },
+ "replay_identity_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "input_artifact_digests": {
+ "type": "array",
+ "items": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
+ },
+ "output_artifact_digests": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
+ },
+ "status": {
+ "type": "string",
+ "enum": ["recorded", "mismatch", "indeterminate"]
+ },
+ "lineage_node_ids": {
+ "type": "array",
+ "items": { "type": "string", "minLength": 1 }
+ }
+ }
+}
diff --git a/schemas/pip/SCHEMA_MIRROR.json b/schemas/pip/SCHEMA_MIRROR.json
new file mode 100644
index 0000000..4b6fd63
--- /dev/null
+++ b/schemas/pip/SCHEMA_MIRROR.json
@@ -0,0 +1,19 @@
+{
+ "mirror_name": "post-incident-proofs",
+ "source_repo": "https://github.com/SentinelOps-CI/post-incident-proofs",
+ "pinned_commit": "3cdfdf09c20f08ad5221d29607b5a9726295ad10",
+ "vendored_files": [
+ "LineageBundle.schema.json",
+ "MorphReplayReport.schema.json",
+ "TransformationRecord.schema.json"
+ ],
+ "local_digests": {
+ "LineageBundle.schema.json": "sha256:1b560005b7479f7c55484dfadcaf172ecb72fe4b995d394c633ed1af406e0463",
+ "MorphReplayReport.schema.json": "sha256:afca63d0df978f697eb8b60e4e1d6df08fa0e55059df0e13eaa185d3603a41fd",
+ "TransformationRecord.schema.json": "sha256:863c9844dbf67892f1bbc41d769d3b7a591d2fa8e05c1ba664fb5ffd78cc93d5"
+ },
+ "sync_policy": "Fail CI on digest drift. Cross-link PCS by digest only; do not promote Morph reports into PCS.",
+ "non_claims": [
+ "PIP emission is observational/differential linkage only."
+ ]
+}
diff --git a/schemas/pip/TransformationRecord.schema.json b/schemas/pip/TransformationRecord.schema.json
new file mode 100644
index 0000000..841f299
--- /dev/null
+++ b/schemas/pip/TransformationRecord.schema.json
@@ -0,0 +1,62 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://post-incident-proofs.local/schemas/pip/v1/TransformationRecord.schema.json",
+ "title": "TransformationRecord",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "schema_version",
+ "transformation_id",
+ "transformation_type",
+ "input_artifact_digests",
+ "output_artifact_digest",
+ "implementation_id",
+ "implementation_version",
+ "container_digest",
+ "source_commit",
+ "record_digest"
+ ],
+ "properties": {
+ "schema_version": { "const": "pip.TransformationRecord.v1" },
+ "transformation_id": { "type": "string", "minLength": 1 },
+ "transformation_type": {
+ "type": "string",
+ "enum": [
+ "redaction",
+ "normalization",
+ "event_filtering",
+ "state_projection",
+ "trace_minimization",
+ "dependency_substitution",
+ "timestamp_normalization",
+ "environment_conversion",
+ "synthetic_variant_derivation"
+ ]
+ },
+ "input_artifact_digests": {
+ "type": "array",
+ "minItems": 1,
+ "items": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" }
+ },
+ "output_artifact_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "implementation_id": { "type": "string", "minLength": 1 },
+ "implementation_version": { "type": "string", "minLength": 1 },
+ "container_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$"
+ },
+ "source_commit": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{40}$"
+ },
+ "record_digest": {
+ "type": "string",
+ "pattern": "^sha256:[a-f0-9]{64}$",
+ "description": "Content digest of this record excluding record_digest itself; used for append-only immutability."
+ },
+ "notes": { "type": "string" }
+ }
+}
diff --git a/scripts/check_schema_mirrors.py b/scripts/check_schema_mirrors.py
new file mode 100644
index 0000000..1bcb3d4
--- /dev/null
+++ b/scripts/check_schema_mirrors.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+"""Fail closed if vendored schema digests drift from SCHEMA_MIRROR manifests."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def check_mirror(mirror_path: Path, schema_dir: Path) -> list[str]:
+ errors: list[str] = []
+ mirror = json.loads(mirror_path.read_text(encoding="utf-8"))
+ local = mirror.get("local_digests") or {}
+ for name, expected in local.items():
+ path = schema_dir / name
+ if not path.is_file():
+ errors.append(f"missing vendored schema: {path}")
+ continue
+ digest = "sha256:" + hashlib.sha256(path.read_bytes()).hexdigest()
+ if digest != expected:
+ errors.append(f"digest drift for {name}: expected {expected}, got {digest}")
+ return errors
+
+
+def main() -> int:
+ errors: list[str] = []
+ errors.extend(
+ check_mirror(ROOT / "schemas/pcs/SCHEMA_MIRROR.json", ROOT / "schemas/pcs")
+ )
+ errors.extend(
+ check_mirror(ROOT / "schemas/pip/SCHEMA_MIRROR.json", ROOT / "schemas/pip")
+ )
+ if errors:
+ for err in errors:
+ print(err, file=sys.stderr)
+ return 1
+ print("schema mirrors OK")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/generate_fixtures.py b/scripts/generate_fixtures.py
new file mode 100644
index 0000000..bfb9cef
--- /dev/null
+++ b/scripts/generate_fixtures.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python3
+"""Generate deterministic fixtures and example trees."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+from pathlib import Path
+
+from runner.profile.schema import (
+ profile_digest,
+ validate_profile,
+ write_profile_with_digest,
+)
+
+ROOT = Path(__file__).resolve().parents[1]
+COMMIT = "b3018d790e3e7c622b28641c0230a61bb3b78955"
+
+
+def main() -> None:
+ payload = b"mrr-local-fake-snapshot-v1\n"
+ snap_digest = "sha256:" + hashlib.sha256(payload).hexdigest()
+ (ROOT / "fixtures/branch/snapshot.payload").write_bytes(payload)
+ snap = {"snapshot_id": "snap-demo", "snapshot_digest": snap_digest}
+ for path in (
+ ROOT / "fixtures/branch/snapshot_ref.json",
+ ROOT / "examples/hermetic-branch-n/snapshot_ref.json",
+ ):
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(json.dumps(snap, indent=2) + "\n", encoding="utf-8")
+ for path in (
+ ROOT / "fixtures/branch/snapshot.payload",
+ ROOT / "examples/hermetic-branch-n/snapshot.payload",
+ ):
+ path.write_bytes(payload)
+
+ incident = {
+ "incident_id": "inc-demo",
+ "incident_digest": "sha256:" + hashlib.sha256(b"inc-demo").hexdigest(),
+ }
+ for path in (
+ ROOT / "fixtures/branch/incident_bundle.json",
+ ROOT / "examples/hermetic-branch-n/incident_bundle.json",
+ ):
+ path.write_text(json.dumps(incident, indent=2) + "\n", encoding="utf-8")
+
+ lock = "sha256:" + hashlib.sha256(b"lock").hexdigest()
+ img = "sha256:" + hashlib.sha256(b"image").hexdigest()
+ base_profile = {
+ "schema_version": "mrr.ExecutionProfile.v1",
+ "profile_id": "profile-demo",
+ "snapshot": snap,
+ "container_image_digests": [img],
+ "environment": {"profile_name": "hermetic-demo", "profile_version": "1.0.0"},
+ "os_runtime": {"os": "linux", "runtime": "python3.11"},
+ "dependency_lock_digest": lock,
+ "network_policy": {"mode": "no_network"},
+ "clock_policy": {"mode": "frozen", "fixed_epoch_ms": 1700000000000},
+ "random_seed_policy": {"seed": 42},
+ "tool_versions": {"replay-runner": "0.1.0"},
+ "budgets": {
+ "cpu_cores": 2,
+ "memory_mib": 1024,
+ "accelerator_count": 0,
+ "storage_mib": 2048,
+ "wall_time_ms": 60000,
+ "token_budget": 0,
+ "api_call_budget": 0,
+ },
+ "secret_refs": [{"secret_ref": "vault/morph/api", "purpose": "optional-morph"}],
+ "output_retention": {"mode": "retain", "retention_days": 30},
+ "source_commit": COMMIT,
+ "integrity": {"canonicalization_version": "v1"},
+ }
+
+ profile = validate_profile(base_profile)
+ digest = profile_digest(profile)
+ write_profile_with_digest(profile, ROOT / "fixtures/profile/valid_profile.json")
+ write_profile_with_digest(
+ profile, ROOT / "examples/hermetic-branch-n/execution_profile.json"
+ )
+
+ bad = dict(base_profile)
+ bad["api_key"] = "sk-secret-value"
+ (ROOT / "fixtures/profile/invalid_secret_value.json").write_text(
+ json.dumps(bad, indent=2) + "\n", encoding="utf-8"
+ )
+
+ bad2 = dict(base_profile)
+ bad2["schema_version"] = "mrr.ExecutionProfile.v999"
+ (ROOT / "fixtures/profile/invalid_schema_version.json").write_text(
+ json.dumps(bad2, indent=2) + "\n", encoding="utf-8"
+ )
+
+ bad3 = dict(base_profile)
+ del bad3["dependency_lock_digest"]
+ (ROOT / "fixtures/profile/invalid_missing_pin.json").write_text(
+ json.dumps(bad3, indent=2) + "\n", encoding="utf-8"
+ )
+
+ int_dir = ROOT / "fixtures/branch/interventions"
+ ex_int = ROOT / "examples/hermetic-branch-n/interventions"
+ int_dir.mkdir(parents=True, exist_ok=True)
+ ex_int.mkdir(parents=True, exist_ok=True)
+ branches = []
+ for i in range(16):
+ bid = f"branch-{i:02d}"
+ body = {
+ "branch_id": bid,
+ "intervention_id": f"iv-{i:02d}",
+ "artifact_paths": [f"{bid}.json"],
+ "action_sequence": ["replay", bid],
+ }
+ for directory in (int_dir, ex_int):
+ (directory / f"{bid}.json").write_text(
+ json.dumps(body, indent=2) + "\n", encoding="utf-8"
+ )
+ branches.append(
+ {
+ "branch_id": bid,
+ "intervention": {
+ "intervention_id": f"iv-{i:02d}",
+ "artifact_paths": [f"{bid}.json"],
+ },
+ "action_sequence": ["replay", bid],
+ "claim_class": "RuntimeObserved",
+ }
+ )
+
+ manifest = {
+ "schema_version": "mrr.BranchReplayManifest.v1",
+ "manifest_id": "manifest-demo",
+ "incident_ref": incident,
+ "snapshot_ref": snap,
+ "execution_profile_digest": digest,
+ "branches": branches,
+ "retry_policy": {"max_retries": 1, "retry_on": ["TIMEOUT", "ERROR"]},
+ "cancellation_policy": {"cancel_others_on_failure": False},
+ "branch_independence": True,
+ }
+ for path in (
+ ROOT / "fixtures/branch/manifest.json",
+ ROOT / "examples/hermetic-branch-n/manifest.json",
+ ):
+ path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
+
+ drift = dict(manifest)
+ drift["execution_profile_digest"] = "sha256:" + ("b" * 64)
+ (ROOT / "fixtures/branch/manifest_digest_drift.json").write_text(
+ json.dumps(drift, indent=2) + "\n", encoding="utf-8"
+ )
+ print("profile_digest", digest)
+ print("snapshot_digest", snap_digest)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/optional_external_validate.py b/scripts/optional_external_validate.py
new file mode 100644
index 0000000..d52ff0e
--- /dev/null
+++ b/scripts/optional_external_validate.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+"""Optional deeper semantic gates via sibling CLIs.
+
+Skip-only-when-absent is JUSTIFIED: default CI already enforces conformance via
+vendored JSON Schema instance validation (``validate_vendored_instances.py``).
+When ``pcs`` / ``post-incident`` are on PATH (or installed via extras), this
+script runs their CLI checks and fails closed on non-zero exit.
+"""
+
+from __future__ import annotations
+
+import shutil
+import subprocess
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+def _run(cmd: list[str]) -> int:
+ print("+", " ".join(cmd))
+ completed = subprocess.run(cmd, check=False, cwd=str(ROOT))
+ return int(completed.returncode)
+
+
+def main() -> int:
+ skipped: list[str] = []
+ failed = 0
+
+ pcs = shutil.which("pcs")
+ if pcs:
+ # Prefer a previously emitted receipt; otherwise emit one for pcs validate.
+ receipt_candidates = list(
+ (ROOT / "branches-smoke").glob("*/runtime_receipt.json")
+ )
+ if receipt_candidates:
+ code = _run([pcs, "validate", str(receipt_candidates[0])])
+ if code != 0:
+ failed += 1
+ else:
+ print(
+ "pcs present but no runtime_receipt fixture to validate; "
+ "vendored schema gate remains authoritative"
+ )
+ skipped.append("pcs-validate-no-receipt")
+ else:
+ skipped.append("pcs-cli-absent")
+ print(
+ "SKIP pcs validate: CLI not on PATH "
+ "(justified — vendored RuntimeReceipt.v0 schema validation always runs)"
+ )
+
+ post = shutil.which("post-incident")
+ if post:
+ lineage = ROOT / "fixtures/pip/valid_replay_linkage/lineage_bundle.json"
+ report = ROOT / "fixtures/pip/valid_replay_linkage/morph_replay_report.json"
+ code = _run([post, "lineage", "validate", str(lineage)])
+ if code != 0:
+ failed += 1
+ # replay-check may require a release_dir layout; attempt and fail closed.
+ release_dir = ROOT / "fixtures/pip/valid_replay_linkage"
+ code = _run(
+ [
+ post,
+ "replay-check",
+ str(release_dir),
+ "--replay-report",
+ str(report),
+ ]
+ )
+ if code != 0:
+ failed += 1
+ else:
+ skipped.append("post-incident-cli-absent")
+ print(
+ "SKIP post-incident replay-check: CLI not on PATH "
+ "(justified — vendored PIP schema + linkage tests always run)"
+ )
+
+ print(f"optional_external_validate skipped={skipped} failed={failed}")
+ return 1 if failed else 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/release_bundle.py b/scripts/release_bundle.py
new file mode 100644
index 0000000..89c73f1
--- /dev/null
+++ b/scripts/release_bundle.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+"""Build a release checksum bundle for schemas and example evidence."""
+
+from __future__ import annotations
+
+import hashlib
+import json
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+OUT = ROOT / "release-bundle"
+
+
+def file_digest(path: Path) -> str:
+ return "sha256:" + hashlib.sha256(path.read_bytes()).hexdigest()
+
+
+def main() -> None:
+ OUT.mkdir(parents=True, exist_ok=True)
+ entries: dict[str, str] = {}
+ for pattern in (
+ "schemas/mrr/*.json",
+ "schemas/pcs/*.json",
+ "schemas/pip/*.json",
+ "examples/hermetic-branch-n/**/*",
+ "fixtures/pip/valid_replay_linkage/*",
+ ):
+ for path in ROOT.glob(pattern):
+ if path.is_file():
+ rel = path.relative_to(ROOT).as_posix()
+ entries[rel] = file_digest(path)
+ payload = {
+ "schema_version": "mrr.ReleaseChecksums.v1",
+ "files": dict(sorted(entries.items())),
+ }
+ (OUT / "checksums.json").write_text(
+ json.dumps(payload, indent=2) + "\n", encoding="utf-8"
+ )
+ print(f"wrote {OUT / 'checksums.json'} ({len(entries)} files)")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/validate_vendored_instances.py b/scripts/validate_vendored_instances.py
new file mode 100644
index 0000000..3f7b921
--- /dev/null
+++ b/scripts/validate_vendored_instances.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+"""Validate fixture/example instances against vendored JSON Schemas.
+
+Always-on offline gate — does not require pcs / post-incident CLIs.
+"""
+
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+from typing import Any
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+
+from runner.evidence.pcs import emit_runtime_receipt # noqa: E402
+from runner.evidence.pip import ( # noqa: E402
+ emit_morph_replay_report,
+ emit_transformation_record,
+)
+from runner.evidence.validate import ( # noqa: E402
+ ArtifactKind,
+ SchemaValidationError,
+ validate_instance,
+ validate_json_file,
+)
+from runner.profile import load_profile # noqa: E402
+
+
+def _load(path: Path) -> dict[str, Any]:
+ data = json.loads(path.read_text(encoding="utf-8"))
+ if not isinstance(data, dict):
+ raise SchemaValidationError(f"{path}: expected object")
+ return data
+
+
+def main() -> int:
+ errors: list[str] = []
+
+ def check(kind: ArtifactKind, path: Path) -> None:
+ try:
+ validate_json_file(kind, path)
+ print(f"ok {kind} {path.relative_to(ROOT)}")
+ except (SchemaValidationError, OSError, json.JSONDecodeError) as exc:
+ errors.append(f"{path}: {exc}")
+ print(f"FAIL {kind} {path.relative_to(ROOT)}: {exc}", file=sys.stderr)
+
+ for path in [
+ ROOT / "fixtures/profile/valid_profile.json",
+ ROOT / "examples/hermetic-branch-n/execution_profile.json",
+ ]:
+ try:
+ profile = load_profile(path)
+ payload = profile.model_dump(mode="json", exclude_none=True)
+ validate_instance("mrr.ExecutionProfile.v1", payload)
+ print(f"ok mrr.ExecutionProfile.v1 {path.relative_to(ROOT)}")
+ except Exception as exc:
+ errors.append(f"{path}: {exc}")
+ print(f"FAIL profile {path}: {exc}", file=sys.stderr)
+
+ check("mrr.BranchReplayManifest.v1", ROOT / "fixtures/branch/manifest.json")
+ check(
+ "mrr.BranchReplayManifest.v1",
+ ROOT / "examples/hermetic-branch-n/manifest.json",
+ )
+
+ pip_root = ROOT / "fixtures/pip/valid_replay_linkage"
+ check("pip.LineageBundle.v1", pip_root / "lineage_bundle.json")
+ check("pip.MorphReplayReport.v1", pip_root / "morph_replay_report.json")
+
+ lineage = _load(pip_root / "lineage_bundle.json")
+ for transform in lineage.get("transformations", []):
+ try:
+ validate_instance("pip.TransformationRecord.v1", transform)
+ except SchemaValidationError as exc:
+ errors.append(f"lineage transform: {exc}")
+
+ try:
+ receipt = emit_runtime_receipt(
+ receipt_id="ci-receipt",
+ run_id="ci-run",
+ started_at="2026-07-24T00:00:00Z",
+ ended_at="2026-07-24T00:00:01Z",
+ run_outcome="passed",
+ final_reason_code="PASSED",
+ source_commit="b3018d790e3e7c622b28641c0230a61bb3b78955",
+ input_hashes={"a": "sha256:" + "a" * 64},
+ output_hashes={"b": "sha256:" + "b" * 64},
+ )
+ validate_instance("RuntimeReceipt.v0", receipt)
+ print("ok RuntimeReceipt.v0 (emitted)")
+ emit_morph_replay_report(
+ replay_id="ci-replay",
+ branch_id="branch-00",
+ replay_identity_digest="sha256:" + "d" * 64,
+ input_artifact_digests=["sha256:" + "a" * 64],
+ output_artifact_digests=["sha256:" + "d" * 64],
+ status="recorded",
+ )
+ emit_transformation_record(
+ transformation_id="t-ci",
+ transformation_type="normalization",
+ input_artifact_digests=["sha256:" + "a" * 64],
+ output_artifact_digest="sha256:" + "b" * 64,
+ implementation_id="morph-replay-runner",
+ implementation_version="0.1.0",
+ container_digest="sha256:" + "c" * 64,
+ source_commit="b3018d790e3e7c622b28641c0230a61bb3b78955",
+ )
+ print("ok PIP emitters (emitted)")
+ except Exception as exc:
+ errors.append(f"emitter round-trip: {exc}")
+ print(f"FAIL emitter round-trip: {exc}", file=sys.stderr)
+
+ if errors:
+ print(f"{len(errors)} schema instance validation error(s)", file=sys.stderr)
+ return 1
+ print("vendored instance validation passed")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..ba69e09
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+"""Pytest package marker."""
diff --git a/tests/adversarial/__init__.py b/tests/adversarial/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/adversarial/test_secrets_isolation.py b/tests/adversarial/test_secrets_isolation.py
new file mode 100644
index 0000000..7802bad
--- /dev/null
+++ b/tests/adversarial/test_secrets_isolation.py
@@ -0,0 +1,70 @@
+"""Adversarial secret and isolation tests."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+import pytest
+
+from runner.branch import run_branch_job
+from runner.evidence import assert_no_secret_material, redact_text
+from runner.profile import ProfileValidationError, validate_profile
+from runner.providers.local_fake import LocalFakeProvider
+
+
+def test_redaction_strips_tokens() -> None:
+ text = "Authorization: Bearer abcdefTOKEN api_key=supersecret"
+ redacted = redact_text(text)
+ assert "supersecret" not in redacted
+ assert "abcdefTOKEN" not in redacted or "[REDACTED]" in redacted
+
+
+def test_secret_refs_never_materialize(tmp_path: Path) -> None:
+ snap = json.loads(
+ Path("fixtures/branch/snapshot_ref.json").read_text(encoding="utf-8")
+ )
+ payload = Path("fixtures/branch/snapshot.payload").read_bytes()
+ provider = LocalFakeProvider(tmp_path / "prov")
+ provider.register_snapshot(
+ snap["snapshot_id"], payload, digest=snap["snapshot_digest"]
+ )
+ out = tmp_path / "branches"
+ run_branch_job(
+ provider=provider,
+ profile_path=Path("fixtures/profile/valid_profile.json"),
+ manifest_path=Path("fixtures/branch/manifest.json"),
+ interventions_dir=Path("fixtures/branch/interventions"),
+ parallel=4,
+ out_dir=out,
+ )
+ for path in out.rglob("*.json"):
+ blob = path.read_text(encoding="utf-8")
+ assert "sk-" not in blob.lower()
+ assert "password=" not in blob.lower()
+ assert_no_secret_material(
+ json.loads(blob) if blob.strip().startswith("{") else blob
+ )
+
+
+def test_profile_rejects_embedded_secret_ref_value() -> None:
+ raw = json.loads(
+ Path("fixtures/profile/valid_profile.json").read_text(encoding="utf-8")
+ )
+ raw["secret_refs"] = [{"secret_ref": "token=Bearer sk-abc"}]
+ with pytest.raises(ProfileValidationError):
+ validate_profile(raw)
+
+
+def test_no_cross_branch_file_bleed(tmp_path: Path) -> None:
+ provider = LocalFakeProvider(tmp_path)
+ snap = provider.register_snapshot("s", b"shared")
+ h1 = provider.clone(snap, branch_id="b1")
+ h2 = provider.clone(snap, branch_id="b2")
+ secret = Path(h1.root_path) / "fs" / "secret.txt"
+ secret.write_text("tenant-a-secret", encoding="utf-8")
+ assert not (Path(h2.root_path) / "fs" / "secret.txt").exists()
+ provider.teardown(h1)
+ provider.teardown(h2)
+ assert not Path(h1.root_path).exists()
+ assert not Path(h2.root_path).exists()
diff --git a/tests/conformance/__init__.py b/tests/conformance/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conformance/test_pip_linkage.py b/tests/conformance/test_pip_linkage.py
new file mode 100644
index 0000000..ac7cf59
--- /dev/null
+++ b/tests/conformance/test_pip_linkage.py
@@ -0,0 +1,90 @@
+"""PIP emission and linkage tests."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+import pytest
+
+from runner.evidence.pip import (
+ PipEvidenceError,
+ emit_morph_replay_report,
+ emit_transformation_record,
+)
+from runner.evidence.validate import SchemaValidationError, validate_instance
+from runner.hashing import canonical_hash
+
+
+def test_transformation_record_digest() -> None:
+ record = emit_transformation_record(
+ transformation_id="t1",
+ transformation_type="normalization",
+ input_artifact_digests=["sha256:" + "a" * 64],
+ output_artifact_digest="sha256:" + "b" * 64,
+ implementation_id="morph-replay-runner",
+ implementation_version="0.1.0",
+ container_digest="sha256:" + "c" * 64,
+ source_commit="b3018d790e3e7c622b28641c0230a61bb3b78955",
+ )
+ recomputed = canonical_hash(
+ {k: v for k, v in record.items() if k != "record_digest"},
+ enforce_number_policy=True,
+ extra_excluded=frozenset({"record_digest"}),
+ )
+ assert record["record_digest"] == recomputed
+ validate_instance("pip.TransformationRecord.v1", record)
+
+
+def test_morph_report_status_enum() -> None:
+ report = emit_morph_replay_report(
+ replay_id="r",
+ branch_id="b",
+ replay_identity_digest="sha256:" + "d" * 64,
+ input_artifact_digests=["sha256:" + "a" * 64],
+ output_artifact_digests=["sha256:" + "d" * 64],
+ status="recorded",
+ )
+ assert report["schema_version"] == "pip.MorphReplayReport.v1"
+ validate_instance("pip.MorphReplayReport.v1", report)
+
+
+def test_joint_fixture_linkage_files_exist() -> None:
+ root = Path("fixtures/pip/valid_replay_linkage")
+ assert (root / "lineage_bundle.json").is_file()
+ assert (root / "morph_replay_report.json").is_file()
+ lineage = json.loads((root / "lineage_bundle.json").read_text(encoding="utf-8"))
+ report = json.loads((root / "morph_replay_report.json").read_text(encoding="utf-8"))
+ validate_instance("pip.LineageBundle.v1", lineage)
+ validate_instance("pip.MorphReplayReport.v1", report)
+ node_digests = {n["artifact_digest"] for n in lineage["nodes"]}
+ assert report["replay_identity_digest"] in node_digests
+ for digest in report["output_artifact_digests"]:
+ assert digest in node_digests
+ for digest in report["input_artifact_digests"]:
+ assert digest in node_digests
+ for transform in lineage["transformations"]:
+ validate_instance("pip.TransformationRecord.v1", transform)
+
+
+def test_tampered_digest_fails_linkage_and_schema_membership() -> None:
+ root = Path("fixtures/pip/valid_replay_linkage")
+ report = json.loads((root / "morph_replay_report.json").read_text(encoding="utf-8"))
+ lineage = json.loads((root / "lineage_bundle.json").read_text(encoding="utf-8"))
+ report["output_artifact_digests"] = ["sha256:" + "0" * 64]
+ node_digests = {n["artifact_digest"] for n in lineage["nodes"]}
+ assert report["output_artifact_digests"][0] not in node_digests
+ # Still schema-valid as Morph report, but linkage fails (replay-check semantics).
+ validate_instance("pip.MorphReplayReport.v1", report)
+
+
+def test_invalid_morph_status_rejected() -> None:
+ with pytest.raises((PipEvidenceError, SchemaValidationError, Exception)):
+ emit_morph_replay_report(
+ replay_id="r",
+ branch_id="b",
+ replay_identity_digest="sha256:" + "d" * 64,
+ input_artifact_digests=["sha256:" + "a" * 64],
+ output_artifact_digests=["sha256:" + "d" * 64],
+ status="not-a-status", # type: ignore[arg-type]
+ )
diff --git a/tests/conformance/test_schema_instances.py b/tests/conformance/test_schema_instances.py
new file mode 100644
index 0000000..dff244e
--- /dev/null
+++ b/tests/conformance/test_schema_instances.py
@@ -0,0 +1,79 @@
+"""Always-on vendored JSON Schema instance validation."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+import pytest
+
+from runner.evidence.pcs import EvidenceError, emit_runtime_receipt
+from runner.evidence.validate import (
+ SchemaValidationError,
+ validate_instance,
+ validate_json_file,
+)
+
+
+def test_runtime_receipt_validates_against_vendored_schema() -> None:
+ receipt = emit_runtime_receipt(
+ receipt_id="r1",
+ run_id="run-1",
+ started_at="2026-07-24T00:00:00Z",
+ ended_at="2026-07-24T00:00:01Z",
+ run_outcome="passed",
+ final_reason_code="PASSED",
+ source_commit="b3018d790e3e7c622b28641c0230a61bb3b78955",
+ input_hashes={"a": "sha256:" + "a" * 64},
+ output_hashes={"b": "sha256:" + "b" * 64},
+ )
+ validate_instance("RuntimeReceipt.v0", receipt)
+
+
+def test_replay_validated_rejected_as_receipt_status() -> None:
+ with pytest.raises(EvidenceError, match="ReplayValidated"):
+ emit_runtime_receipt(
+ receipt_id="r1",
+ run_id="run-1",
+ started_at="2026-07-24T00:00:00Z",
+ ended_at="2026-07-24T00:00:01Z",
+ run_outcome="passed",
+ final_reason_code="PASSED",
+ source_commit="b3018d790e3e7c622b28641c0230a61bb3b78955",
+ input_hashes={},
+ output_hashes={},
+ status="ReplayValidated",
+ )
+
+
+def test_tampered_receipt_fails_schema() -> None:
+ receipt = emit_runtime_receipt(
+ receipt_id="r1",
+ run_id="run-1",
+ started_at="2026-07-24T00:00:00Z",
+ ended_at="2026-07-24T00:00:01Z",
+ run_outcome="passed",
+ final_reason_code="PASSED",
+ source_commit="b3018d790e3e7c622b28641c0230a61bb3b78955",
+ input_hashes={"a": "sha256:" + "a" * 64},
+ output_hashes={"b": "sha256:" + "b" * 64},
+ )
+ receipt["status"] = "NotARealStatus"
+ with pytest.raises(SchemaValidationError):
+ validate_instance("RuntimeReceipt.v0", receipt)
+
+
+def test_pip_linkage_fixtures_schema_valid() -> None:
+ root = Path("fixtures/pip/valid_replay_linkage")
+ validate_json_file("pip.LineageBundle.v1", root / "lineage_bundle.json")
+ validate_json_file("pip.MorphReplayReport.v1", root / "morph_replay_report.json")
+
+
+def test_mrr_manifest_and_profile_schema_valid() -> None:
+ validate_json_file(
+ "mrr.BranchReplayManifest.v1", Path("fixtures/branch/manifest.json")
+ )
+ profile = json.loads(
+ Path("fixtures/profile/valid_profile.json").read_text(encoding="utf-8")
+ )
+ validate_instance("mrr.ExecutionProfile.v1", profile)
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..9c5b18d
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,3 @@
+"""Shared pytest configuration."""
+
+from __future__ import annotations
diff --git a/tests/hermetic/__init__.py b/tests/hermetic/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/hermetic/test_branch_scheduler.py b/tests/hermetic/test_branch_scheduler.py
new file mode 100644
index 0000000..17ada83
--- /dev/null
+++ b/tests/hermetic/test_branch_scheduler.py
@@ -0,0 +1,232 @@
+"""Branch-N scheduler acceptance tests."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+import pytest
+
+from runner.branch import run_branch_job
+from runner.branch.manifest import load_manifest
+from runner.hermeticity import HermeticityError
+from runner.profile import load_profile
+from runner.providers.local_fake import LocalFakeProvider
+
+
+def _provider_with_fixture_snapshot(tmp_path: Path) -> LocalFakeProvider:
+ snap = json.loads(
+ Path("fixtures/branch/snapshot_ref.json").read_text(encoding="utf-8")
+ )
+ payload = Path("fixtures/branch/snapshot.payload").read_bytes()
+ provider = LocalFakeProvider(tmp_path)
+ provider.register_snapshot(
+ snap["snapshot_id"], payload, digest=snap["snapshot_digest"]
+ )
+ return provider
+
+
+def test_sixteen_branches_isolated(tmp_path: Path) -> None:
+ provider = _provider_with_fixture_snapshot(tmp_path / "prov")
+ out = tmp_path / "branches"
+ summary = run_branch_job(
+ provider=provider,
+ profile_path=Path("fixtures/profile/valid_profile.json"),
+ manifest_path=Path("fixtures/branch/manifest.json"),
+ interventions_dir=Path("fixtures/branch/interventions"),
+ parallel=16,
+ out_dir=out,
+ )
+ assert len(summary["branches"]) == 16
+ roots: set[str] = set()
+ markers: list[str] = []
+ for branch_id, result in summary["branches"].items():
+ assert result["status"] == "complete", branch_id
+ status = json.loads(
+ (out / branch_id / "status.json").read_text(encoding="utf-8")
+ )
+ assert status["status"] == "complete"
+ assert (out / branch_id / "runtime_receipt.json").is_file()
+ assert (out / branch_id / "execution_profile.json").is_file()
+ receipt = json.loads(
+ (out / branch_id / "runtime_receipt.json").read_text(encoding="utf-8")
+ )
+ assert receipt["status"] == "RuntimeObserved"
+ from runner.evidence.validate import validate_instance
+
+ validate_instance("RuntimeReceipt.v0", receipt)
+ validate_instance(
+ "pip.MorphReplayReport.v1",
+ json.loads(
+ (out / branch_id / "morph_replay_report.json").read_text(
+ encoding="utf-8"
+ )
+ ),
+ )
+ # Isolation: each branch writes its own status content keyed by id
+ marker = (out / branch_id / "status.json").read_text(encoding="utf-8")
+ assert branch_id in marker
+ markers.append(marker)
+ roots.add(str((out / branch_id).resolve()))
+ assert len(roots) == 16
+ assert len(set(markers)) == 16
+
+
+def test_profile_digest_drift_detected(tmp_path: Path) -> None:
+ provider = _provider_with_fixture_snapshot(tmp_path / "prov")
+ with pytest.raises(HermeticityError, match="digest drift"):
+ run_branch_job(
+ provider=provider,
+ profile_path=Path("fixtures/profile/valid_profile.json"),
+ manifest_path=Path("fixtures/branch/manifest_digest_drift.json"),
+ interventions_dir=Path("fixtures/branch/interventions"),
+ parallel=2,
+ out_dir=tmp_path / "out",
+ )
+
+
+def test_timeout_and_retry_from_clean(tmp_path: Path) -> None:
+ provider = _provider_with_fixture_snapshot(tmp_path / "prov")
+ profile = load_profile("fixtures/profile/valid_profile.json")
+ manifest = load_manifest("fixtures/branch/manifest.json")
+ # Shrink to one timeout branch
+ branch = manifest.branches[0].model_copy(
+ update={"action_sequence": ["__timeout__"], "branch_id": "timeout-branch"}
+ )
+ manifest = manifest.model_copy(
+ update={
+ "branches": [branch],
+ "retry_policy": manifest.retry_policy.model_copy(
+ update={"max_retries": 1, "retry_on": ["TIMEOUT"]}
+ ),
+ }
+ )
+ # Force tiny timeout via profile budgets
+ profile = profile.model_copy(
+ update={
+ "budgets": profile.budgets.model_copy(update={"wall_time_ms": 1}),
+ "profile_digest": None,
+ }
+ )
+ from runner.profile.schema import profile_digest
+
+ digest = profile_digest(profile)
+ profile = profile.model_copy(update={"profile_digest": digest})
+ manifest = manifest.model_copy(update={"execution_profile_digest": digest})
+
+ from runner.branch.scheduler import BranchScheduler, SchedulerConfig
+
+ # Write intervention file
+ iv = tmp_path / "iv"
+ iv.mkdir()
+ (iv / "timeout-branch.json").write_text("{}", encoding="utf-8")
+ branch = manifest.branches[0].model_copy(
+ update={
+ "intervention": manifest.branches[0].intervention.model_copy(
+ update={"artifact_paths": ["timeout-branch.json"]}
+ )
+ }
+ )
+ manifest = manifest.model_copy(update={"branches": [branch]})
+
+ scheduler = BranchScheduler(
+ provider,
+ profile,
+ manifest,
+ SchedulerConfig(parallel=1, out_dir=tmp_path / "out", interventions_dir=iv),
+ )
+ summary = scheduler.run()
+ result = summary["branches"]["timeout-branch"]
+ assert result["terminal"] == "TIMEOUT"
+ assert result["status"] == "partial"
+ assert result["attempts"] >= 1
+
+
+def test_partial_failure_independence(tmp_path: Path) -> None:
+ provider = _provider_with_fixture_snapshot(tmp_path / "prov")
+ profile = load_profile("fixtures/profile/valid_profile.json")
+ manifest = load_manifest("fixtures/branch/manifest.json")
+ good = manifest.branches[0]
+ bad = manifest.branches[1].model_copy(
+ update={"branch_id": "fail-branch", "action_sequence": ["__fail__"]}
+ )
+ # Fix intervention paths for fail branch file
+ iv = tmp_path / "iv"
+ iv.mkdir()
+ (iv / good.intervention.artifact_paths[0]).write_text("{}", encoding="utf-8")
+ (iv / "fail-branch.json").write_text("{}", encoding="utf-8")
+ bad = bad.model_copy(
+ update={
+ "intervention": bad.intervention.model_copy(
+ update={"artifact_paths": ["fail-branch.json"]}
+ )
+ }
+ )
+ manifest = manifest.model_copy(update={"branches": [good, bad]})
+ from runner.branch.scheduler import BranchScheduler, SchedulerConfig
+
+ summary = BranchScheduler(
+ provider,
+ profile,
+ manifest,
+ SchedulerConfig(parallel=2, out_dir=tmp_path / "out", interventions_dir=iv),
+ ).run()
+ assert summary["branches"][good.branch_id]["status"] == "complete"
+ assert summary["branches"]["fail-branch"]["status"] == "partial"
+
+
+def test_elevated_claim_class_without_pf_trace_fails(tmp_path: Path) -> None:
+ provider = _provider_with_fixture_snapshot(tmp_path / "prov")
+ profile = load_profile("fixtures/profile/valid_profile.json")
+ manifest = load_manifest("fixtures/branch/manifest.json")
+ branch = manifest.branches[0].model_copy(
+ update={"claim_class": "RuntimeChecked", "branch_id": "checked-branch"}
+ )
+ iv = tmp_path / "iv"
+ iv.mkdir()
+ (iv / "checked-branch.json").write_text("{}", encoding="utf-8")
+ branch = branch.model_copy(
+ update={
+ "intervention": branch.intervention.model_copy(
+ update={"artifact_paths": ["checked-branch.json"]}
+ )
+ }
+ )
+ manifest = manifest.model_copy(update={"branches": [branch]})
+ from runner.branch.scheduler import BranchScheduler, SchedulerConfig
+
+ summary = BranchScheduler(
+ provider,
+ profile,
+ manifest,
+ SchedulerConfig(parallel=1, out_dir=tmp_path / "out", interventions_dir=iv),
+ ).run()
+ result = summary["branches"]["checked-branch"]
+ assert result["status"] == "partial"
+ err = result.get("error", "").lower()
+ assert any(
+ needle in err
+ for needle in ("pf-core", "pf_trace", "claim_class", "pcs cli", "requires")
+ )
+
+
+def test_cleanup_after_success_and_failure(tmp_path: Path) -> None:
+ provider = _provider_with_fixture_snapshot(tmp_path / "prov")
+ out = tmp_path / "branches"
+ run_branch_job(
+ provider=provider,
+ profile_path=Path("fixtures/profile/valid_profile.json"),
+ manifest_path=Path("fixtures/branch/manifest.json"),
+ interventions_dir=Path("fixtures/branch/interventions"),
+ parallel=4,
+ out_dir=out,
+ )
+ # Provider branch workdirs cleaned
+ assert list((tmp_path / "prov" / "branches").glob("*")) == []
+ for branch_dir in out.iterdir():
+ if branch_dir.is_dir() and branch_dir.name.startswith("branch-"):
+ receipt = json.loads(
+ (branch_dir / "teardown_receipt.json").read_text(encoding="utf-8")
+ )
+ assert receipt["cleaned"] is True
+ assert receipt["secrets_scrubbed"] is True
diff --git a/tests/hermetic/test_modes.py b/tests/hermetic/test_modes.py
new file mode 100644
index 0000000..da8f562
--- /dev/null
+++ b/tests/hermetic/test_modes.py
@@ -0,0 +1,133 @@
+"""Hermeticity mode enforcement tests."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+from runner.hermeticity import (
+ EnforcementStatus,
+ HermeticityMode,
+ HermeticityPolicy,
+)
+from runner.providers.base import ExecRequest
+from runner.providers.local_fake import LocalFakeProvider
+from runner.providers.morph import MorphCloudProvider
+
+
+@pytest.mark.parametrize(
+ "mode,env,should_fail",
+ [
+ (HermeticityMode.NO_NETWORK, {"HTTP_URL": "https://evil.example"}, True),
+ (
+ HermeticityMode.ALLOWLISTED_NETWORK,
+ {"HTTP_URL": "https://allowed.example"},
+ False,
+ ),
+ (
+ HermeticityMode.ALLOWLISTED_NETWORK,
+ {"HTTP_URL": "https://denied.example"},
+ True,
+ ),
+ (
+ HermeticityMode.RECORDED_RESPONSE,
+ {"CASSETTE_DIGEST": "sha256:" + "c" * 64},
+ False,
+ ),
+ (
+ HermeticityMode.RECORDED_RESPONSE,
+ {"CASSETTE_DIGEST": "sha256:" + "d" * 64},
+ True,
+ ),
+ (
+ HermeticityMode.SYNTHETIC_DEPENDENCY,
+ {"SYNTHETIC_DEP_DIGEST": "sha256:" + "e" * 64},
+ False,
+ ),
+ (
+ HermeticityMode.SYNTHETIC_DEPENDENCY,
+ {"SYNTHETIC_DEP_DIGEST": "sha256:" + "f" * 64},
+ True,
+ ),
+ ],
+)
+def test_fake_mode_enforce_and_deny(
+ tmp_path: Path,
+ mode: HermeticityMode,
+ env: dict[str, str],
+ should_fail: bool,
+) -> None:
+ policy = HermeticityPolicy(
+ mode=mode,
+ allowlist_hosts=("allowed.example",),
+ cassette_digest="sha256:" + "c" * 64,
+ synthetic_dependency_digests=("sha256:" + "e" * 64,),
+ partner_local_paths=("/approved",),
+ )
+ provider = LocalFakeProvider(tmp_path, hermeticity=policy)
+ assert provider.supports(mode)
+ snap = provider.register_snapshot("s", b"x")
+ handle = provider.clone(snap, branch_id="b")
+ result = provider.execute(
+ handle, ExecRequest(command=["replay"], timeout_ms=1000, env=env)
+ )
+ if should_fail:
+ assert result.status.value == "FAILED"
+ assert (
+ "denied" in result.stderr.lower()
+ or "mismatch" in result.stderr.lower()
+ or "undeclared" in result.stderr.lower()
+ )
+ else:
+ assert result.status.value == "PASSED"
+
+
+def test_partner_local_path_boundary(tmp_path: Path) -> None:
+ policy = HermeticityPolicy(
+ mode=HermeticityMode.PARTNER_LOCAL,
+ partner_local_paths=(str(tmp_path / "approved"),),
+ )
+ approved = tmp_path / "approved"
+ approved.mkdir()
+ good = approved / "iv.json"
+ good.write_text("{}", encoding="utf-8")
+ bad = tmp_path / "outside.json"
+ bad.write_text("{}", encoding="utf-8")
+ provider = LocalFakeProvider(tmp_path / "prov", hermeticity=policy)
+ snap = provider.register_snapshot("s", b"x")
+ handle = provider.clone(snap, branch_id="b")
+ ok = provider.execute(
+ handle,
+ ExecRequest(
+ command=["replay"],
+ timeout_ms=1000,
+ intervention_paths=[str(good)],
+ ),
+ )
+ assert ok.status.value == "PASSED"
+ denied = provider.execute(
+ handle,
+ ExecRequest(
+ command=["replay"],
+ timeout_ms=1000,
+ intervention_paths=[str(bad)],
+ ),
+ )
+ assert denied.status.value == "FAILED"
+
+
+def test_morph_unsupported_evidence_status() -> None:
+ provider = MorphCloudProvider(api_key="x", client=object())
+ assert not provider.supports(HermeticityMode.PARTNER_LOCAL)
+ # Preflight pattern used by scheduler
+ from runner.hermeticity.modes import HermeticityEvidence
+
+ evidence = HermeticityEvidence(
+ mode=HermeticityMode.PARTNER_LOCAL,
+ status=EnforcementStatus.UNSUPPORTED,
+ policy_digest="sha256:" + "a" * 64,
+ probe_result={"supports": False},
+ message="unsupported",
+ )
+ assert evidence.status == EnforcementStatus.UNSUPPORTED
diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/integration/test_offline_gates.py b/tests/integration/test_offline_gates.py
new file mode 100644
index 0000000..71e467a
--- /dev/null
+++ b/tests/integration/test_offline_gates.py
@@ -0,0 +1,31 @@
+"""Integration: offline acceptance gates must hard-fail without Morph."""
+
+from __future__ import annotations
+
+import subprocess
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[2]
+
+
+def test_vendored_instance_script_passes() -> None:
+ completed = subprocess.run(
+ [sys.executable, str(ROOT / "scripts" / "validate_vendored_instances.py")],
+ check=False,
+ cwd=str(ROOT),
+ capture_output=True,
+ text=True,
+ )
+ assert completed.returncode == 0, completed.stdout + completed.stderr
+
+
+def test_schema_mirror_script_passes() -> None:
+ completed = subprocess.run(
+ [sys.executable, str(ROOT / "scripts" / "check_schema_mirrors.py")],
+ check=False,
+ cwd=str(ROOT),
+ capture_output=True,
+ text=True,
+ )
+ assert completed.returncode == 0, completed.stdout + completed.stderr
diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py
new file mode 100644
index 0000000..46e0b2c
--- /dev/null
+++ b/tests/unit/__init__.py
@@ -0,0 +1 @@
+"""Unit test package."""
diff --git a/tests/unit/test_accounting.py b/tests/unit/test_accounting.py
new file mode 100644
index 0000000..79e9cf5
--- /dev/null
+++ b/tests/unit/test_accounting.py
@@ -0,0 +1,60 @@
+"""Resource accounting tests."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+from runner.accounting import (
+ BudgetExceededError,
+ assert_unavailable_not_fabricated,
+ enforce_budgets,
+ sample_to_report,
+)
+from runner.profile import load_profile
+from runner.providers.base import ExecRequest, MetricValue, ResourceSample
+from runner.providers.local_fake import LocalFakeProvider
+
+
+def test_unavailable_stays_unavailable(tmp_path: Path) -> None:
+ provider = LocalFakeProvider(tmp_path)
+ snap = provider.register_snapshot("s", b"x")
+ handle = provider.clone(snap, branch_id="b")
+ provider.execute(handle, ExecRequest(command=["replay"], timeout_ms=1000))
+ sample = provider.measure(handle)
+ assert_unavailable_not_fabricated(sample)
+ report = sample_to_report("b", sample)
+ assert report["metrics"]["accelerator_alloc"]["availability"] == "unavailable"
+ assert report["metrics"]["accelerator_alloc"]["value"] is None
+
+
+def test_budget_enforcement_fail_closed() -> None:
+ profile = load_profile("fixtures/profile/valid_profile.json")
+ sample = ResourceSample(
+ metrics={
+ "wall_time_ms": MetricValue(
+ value=profile.budgets.wall_time_ms + 1,
+ unit="ms",
+ availability="observed",
+ source="provider",
+ )
+ }
+ )
+ with pytest.raises(BudgetExceededError):
+ enforce_budgets(sample, profile.budgets)
+
+
+def test_fabricated_unavailable_rejected() -> None:
+ sample = ResourceSample(
+ metrics={
+ "accelerator_alloc": MetricValue(
+ value=1,
+ unit="count",
+ availability="unavailable",
+ source="provider",
+ )
+ }
+ )
+ with pytest.raises(ValueError):
+ assert_unavailable_not_fabricated(sample)
diff --git a/tests/unit/test_diff.py b/tests/unit/test_diff.py
new file mode 100644
index 0000000..092bb94
--- /dev/null
+++ b/tests/unit/test_diff.py
@@ -0,0 +1,49 @@
+"""Differential report tests."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+from runner.diff import build_differential_report, compare_values, diff_branch_dirs
+
+
+def test_absent_not_equal() -> None:
+ assert compare_values(None, None)["result"] == "absent_both"
+ assert compare_values(None, 1)["result"] == "absent_left"
+ assert compare_values(1, None)["result"] == "absent_right"
+ assert compare_values(1, 1)["result"] == "equal"
+ assert compare_values(1, 2)["result"] == "different"
+
+
+def test_diff_reproducible(tmp_path: Path) -> None:
+ branches = tmp_path / "branches"
+ for name, terminal in (("a", {"x": 1}), ("b", {"x": 2})):
+ d = branches / name
+ d.mkdir(parents=True)
+ (d / "status.json").write_text(
+ json.dumps({"status": "complete"}), encoding="utf-8"
+ )
+ (d / "terminal_state.json").write_text(json.dumps(terminal), encoding="utf-8")
+ (d / "branch_report.json").write_text("{}", encoding="utf-8")
+ out1 = tmp_path / "d1"
+ out2 = tmp_path / "d2"
+ r1 = diff_branch_dirs(branches, [("a", "b")], out1)[0]
+ r2 = diff_branch_dirs(branches, [("a", "b")], out2)[0]
+ assert r1["report_digest"] == r2["report_digest"]
+ assert r1["comparisons"]["terminal_state"]["result"] == "different"
+ assert r1["comparisons"]["reward"]["result"] == "absent_both"
+ blob = json.dumps(r1["comparisons"]).lower()
+ assert "preferred_branch" not in blob
+ assert "remediation_rank" not in blob
+ assert "No preferred branch." in r1["non_claims"]
+
+
+def test_build_report_digest_stable() -> None:
+ left = {"terminal_state": {"a": 1}, "reward": None}
+ right = {"terminal_state": {"a": 1}, "reward": None}
+ r1 = build_differential_report("l", "r", left, right)
+ r2 = build_differential_report("l", "r", left, right)
+ assert r1["report_digest"] == r2["report_digest"]
+ assert r1["comparisons"]["terminal_state"]["result"] == "equal"
+ assert r1["comparisons"]["reward"]["result"] == "absent_both"
diff --git a/tests/unit/test_import_smoke.py b/tests/unit/test_import_smoke.py
new file mode 100644
index 0000000..8d9e081
--- /dev/null
+++ b/tests/unit/test_import_smoke.py
@@ -0,0 +1,46 @@
+"""Import smoke tests for MRR-ITE-00 quality scaffolding."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+
+def test_package_imports() -> None:
+ import runner
+ from runner import hashing
+
+ assert runner.__version__ == "0.1.0"
+ assert hashing.CANONICALIZATION_VERSION == "v1"
+
+
+def test_baseline_doc_fields_present() -> None:
+ text = Path("docs/baseline/MRR-ITE-00.md").read_text(encoding="utf-8")
+ for field in (
+ "Base commit",
+ "Python",
+ "PCS",
+ "PIP",
+ "OVK",
+ "Non-claims",
+ "Baseline commands",
+ ):
+ assert field in text
+
+
+def test_architecture_adr_checklist() -> None:
+ text = Path("docs/adr/MRR-ITE-00-architecture.md").read_text(encoding="utf-8")
+ for needle in (
+ "Provider boundary",
+ "Claim classes",
+ "Explicit non-claims",
+ "HTTP callback",
+ "LocalFakeProvider",
+ "RuntimeObserved",
+ ):
+ assert needle in text
+
+
+def test_license_apache() -> None:
+ text = Path("LICENSE").read_text(encoding="utf-8")
+ assert "Apache License" in text
+ assert "Version 2.0" in text
diff --git a/tests/unit/test_pcs_evidence.py b/tests/unit/test_pcs_evidence.py
new file mode 100644
index 0000000..ff52f14
--- /dev/null
+++ b/tests/unit/test_pcs_evidence.py
@@ -0,0 +1,80 @@
+"""PCS RuntimeReceipt emission tests."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+import pytest
+
+from runner.evidence.pcs import EvidenceError, emit_runtime_receipt
+
+
+def test_runtime_receipt_observational() -> None:
+ receipt = emit_runtime_receipt(
+ receipt_id="r1",
+ run_id="run-1",
+ started_at="2026-07-24T00:00:00Z",
+ ended_at="2026-07-24T00:00:01Z",
+ run_outcome="passed",
+ final_reason_code="PASSED",
+ source_commit="b3018d790e3e7c622b28641c0230a61bb3b78955",
+ input_hashes={"a": "sha256:" + "a" * 64},
+ output_hashes={"b": "sha256:" + "b" * 64},
+ )
+ assert receipt["status"] == "RuntimeObserved"
+ assert receipt["schema_version"] == "v0"
+ assert receipt["signature_or_digest"].startswith("sha256:")
+
+
+def test_disallowed_status_without_check() -> None:
+ with pytest.raises(EvidenceError):
+ emit_runtime_receipt(
+ receipt_id="r1",
+ run_id="run-1",
+ started_at="2026-07-24T00:00:00Z",
+ ended_at="2026-07-24T00:00:01Z",
+ run_outcome="passed",
+ final_reason_code="PASSED",
+ source_commit="b3018d790e3e7c622b28641c0230a61bb3b78955",
+ input_hashes={},
+ output_hashes={},
+ status="CertificateChecked",
+ )
+
+
+def test_receipt_validates_full_vendored_schema() -> None:
+ from runner.evidence.validate import validate_instance
+
+ receipt = emit_runtime_receipt(
+ receipt_id="r1",
+ run_id="run-1",
+ started_at="2026-07-24T00:00:00Z",
+ ended_at="2026-07-24T00:00:01Z",
+ run_outcome="passed",
+ final_reason_code="PASSED",
+ source_commit="b3018d790e3e7c622b28641c0230a61bb3b78955",
+ input_hashes={"a": "sha256:" + "a" * 64},
+ output_hashes={"b": "sha256:" + "b" * 64},
+ )
+ validate_instance("RuntimeReceipt.v0", receipt)
+
+
+def test_receipt_schema_shape_against_mirror() -> None:
+ schema = json.loads(
+ Path("schemas/pcs/RuntimeReceipt.v0.schema.json").read_text(encoding="utf-8")
+ )
+ assert schema["title"] == "RuntimeReceipt.v0"
+ required = set(schema["required"])
+ receipt = emit_runtime_receipt(
+ receipt_id="r1",
+ run_id="run-1",
+ started_at="2026-07-24T00:00:00Z",
+ ended_at="2026-07-24T00:00:01Z",
+ run_outcome="passed",
+ final_reason_code="PASSED",
+ source_commit="b3018d790e3e7c622b28641c0230a61bb3b78955",
+ input_hashes={"a": "sha256:" + "a" * 64},
+ output_hashes={"b": "sha256:" + "b" * 64},
+ )
+ assert required.issubset(receipt.keys())
diff --git a/tests/unit/test_profile.py b/tests/unit/test_profile.py
new file mode 100644
index 0000000..e94705b
--- /dev/null
+++ b/tests/unit/test_profile.py
@@ -0,0 +1,55 @@
+"""ExecutionProfile validation and digest tests."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+import pytest
+
+from runner.profile import (
+ ProfileValidationError,
+ load_profile,
+ profile_digest,
+ validate_profile,
+)
+
+FIX = Path("fixtures/profile")
+
+
+def test_valid_profile_loads() -> None:
+ profile = load_profile(FIX / "valid_profile.json")
+ assert profile.profile_digest is not None
+ assert profile.profile_digest.startswith("sha256:")
+
+
+def test_digest_drift_on_mutation() -> None:
+ profile = load_profile(FIX / "valid_profile.json")
+ base = profile_digest(profile)
+ mutated = profile.model_copy(
+ update={"tool_versions": {**profile.tool_versions, "replay-runner": "0.1.1"}}
+ )
+ assert profile_digest(mutated) != base
+
+
+def test_unknown_schema_version_fails() -> None:
+ raw = json.loads((FIX / "invalid_schema_version.json").read_text(encoding="utf-8"))
+ with pytest.raises(ProfileValidationError):
+ validate_profile(raw)
+
+
+def test_missing_material_pin_fails() -> None:
+ raw = json.loads((FIX / "invalid_missing_pin.json").read_text(encoding="utf-8"))
+ with pytest.raises(ProfileValidationError):
+ validate_profile(raw)
+
+
+def test_secret_values_rejected() -> None:
+ raw = json.loads((FIX / "invalid_secret_value.json").read_text(encoding="utf-8"))
+ with pytest.raises(ProfileValidationError):
+ validate_profile(raw)
+
+
+def test_secret_ref_only_allowed() -> None:
+ profile = load_profile(FIX / "valid_profile.json")
+ assert profile.secret_refs[0].secret_ref == "vault/morph/api"
diff --git a/tests/unit/test_providers.py b/tests/unit/test_providers.py
new file mode 100644
index 0000000..c9c0455
--- /dev/null
+++ b/tests/unit/test_providers.py
@@ -0,0 +1,97 @@
+"""LocalFakeProvider and Morph capability contract tests."""
+
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+
+from runner.hermeticity import HermeticityError, HermeticityMode, HermeticityPolicy
+from runner.providers.base import ExecRequest, ProviderError, SnapshotRef
+from runner.providers.local_fake import LocalFakeProvider
+from runner.providers.morph import MorphCloudProvider
+
+
+def test_fake_clone_isolation(tmp_path: Path) -> None:
+ provider = LocalFakeProvider(tmp_path)
+ snap = provider.register_snapshot("s1", b"base-bytes")
+ b1 = provider.clone(snap, branch_id="b1")
+ b2 = provider.clone(snap, branch_id="b2")
+ p1 = Path(b1.root_path) / "fs" / "marker"
+ p1.write_text("one", encoding="utf-8")
+ assert not (Path(b2.root_path) / "fs" / "marker").exists()
+ assert (Path(b1.root_path) / "root.bin").read_bytes() == b"base-bytes"
+ assert (Path(b2.root_path) / "root.bin").read_bytes() == b"base-bytes"
+
+
+def test_fake_supports_sixteen_concurrent(tmp_path: Path) -> None:
+ provider = LocalFakeProvider(tmp_path)
+ snap = provider.register_snapshot("s1", b"base")
+ handles = [provider.clone(snap, branch_id=f"b{i}") for i in range(16)]
+ assert len(handles) == 16
+ for handle in handles:
+ result = provider.execute(
+ handle, ExecRequest(command=["replay"], timeout_ms=1000)
+ )
+ assert result.status.value == "PASSED"
+ provider.teardown(handle)
+
+
+def test_fake_measure_unavailable_accelerator(tmp_path: Path) -> None:
+ provider = LocalFakeProvider(tmp_path)
+ snap = provider.register_snapshot("s1", b"base")
+ handle = provider.clone(snap, branch_id="b1")
+ provider.execute(handle, ExecRequest(command=["replay"], timeout_ms=1000))
+ sample = provider.measure(handle)
+ assert sample.metrics["accelerator_alloc"].availability == "unavailable"
+ assert sample.metrics["accelerator_alloc"].value is None
+ assert sample.metrics["wall_time_ms"].availability == "observed"
+
+
+def test_morph_missing_capability_fail_closed() -> None:
+ provider = MorphCloudProvider(api_key="test", client=object())
+ assert provider.supports(HermeticityMode.NO_NETWORK)
+ assert not provider.supports(HermeticityMode.ALLOWLISTED_NETWORK)
+ provider.hermeticity = HermeticityPolicy(mode=HermeticityMode.ALLOWLISTED_NETWORK)
+ # execute path raises before cloud call when mode unsupported
+ from runner.providers.base import BranchHandle
+
+ with pytest.raises(HermeticityError):
+ provider.execute(
+ BranchHandle(
+ branch_id="x",
+ snapshot_id="s",
+ snapshot_digest="sha256:" + "a" * 64,
+ root_path="morph://x",
+ provider_ref="x",
+ ),
+ ExecRequest(command=["true"], timeout_ms=1000),
+ )
+
+
+def test_morph_metric_contract_documents_unavailable() -> None:
+ assert "cpu_time_ms" in MorphCloudProvider.UNSUPPORTED_METRICS
+ provider = MorphCloudProvider(api_key="test", client=object())
+ sample = provider.measure(
+ __import__("runner.providers.base", fromlist=["BranchHandle"]).BranchHandle(
+ branch_id="x",
+ snapshot_id="s",
+ snapshot_digest="sha256:" + "a" * 64,
+ root_path="morph://x",
+ provider_ref="x",
+ )
+ )
+ assert sample.metrics["cpu_time_ms"].availability == "unavailable"
+
+
+def test_snapshot_digest_enforced(tmp_path: Path) -> None:
+ provider = LocalFakeProvider(tmp_path)
+ snap = provider.register_snapshot("s1", b"base")
+ with pytest.raises(ProviderError):
+ provider.lookup_snapshot(
+ SnapshotRef(snapshot_id="s1", snapshot_digest="sha256:" + "0" * 64)
+ )
+ got = provider.lookup_snapshot(
+ SnapshotRef(snapshot_id="s1", snapshot_digest=snap.snapshot_digest)
+ )
+ assert got.snapshot_digest == snap.snapshot_digest