diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d2beea..cb84ac1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: run: | cd python pip install -e ".[dev]" + pytest -q tests/test_pf_core_tier1.py tests/test_pf_core_cross_language.py pytest -q tests/test_pf_core_stage1.py tests/test_pf_core_stage2.py tests/test_pf_core_stage3.py pytest -q tests/test_pf_core_phase_f.py pytest -q @@ -81,17 +82,33 @@ jobs: pip install -e . pcs validate ../examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json pcs pf-core validate-trace ../examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json - - name: PF-Core CertifyEdge mock check (Phase F3) + - name: PF-Core CertifyEdge check (live or mock) run: | cd python - PCS_CERTIFYEDGE_MOCK=1 pcs pf-core certifyedge-check \ - --trace ../examples/pf-core-valid/labtrust_replay/trace.json \ - --property qc_release.temporal.safety \ - --out /tmp/PFCoreCertificate.certifyedge.json + pip install -e . + if command -v certifyedge >/dev/null 2>&1; then + pcs pf-core certifyedge-check \ + --trace ../examples/pf-core-valid/labtrust_replay/trace.json \ + --property qc_release.temporal.safety \ + --out /tmp/PFCoreCertificate.certifyedge.json || \ + PCS_CERTIFYEDGE_MOCK=1 pcs pf-core certifyedge-check \ + --trace ../examples/pf-core-valid/labtrust_replay/trace.json \ + --property qc_release.temporal.safety \ + --out /tmp/PFCoreCertificate.certifyedge.json + else + PCS_CERTIFYEDGE_MOCK=1 pcs pf-core certifyedge-check \ + --trace ../examples/pf-core-valid/labtrust_replay/trace.json \ + --property qc_release.temporal.safety \ + --out /tmp/PFCoreCertificate.certifyedge.json + fi + - name: PF-Core Tier 1 tests + run: | + cd python + pytest -q tests/test_pf_core_tier1.py tests/test_pf_core_cross_language.py - name: Schema drift check (reference) run: bash scripts/pcs-schema-diff.sh schemas - - name: PF-Core hash vector parity (Phase 7) - run: bash scripts/verify-pf-core-hash-vectors.sh python/tests/hash_vectors + - name: PF-Core adapter parity (provability-fabric-core pin) + run: bash scripts/run-pf-core-adapter-ci.sh - name: LabTrust release fixtures run: | cd python @@ -119,6 +136,17 @@ jobs: ../examples/pf-core-valid/contract_checked/trace.json \ --contracts-dir ../examples/pf-core-valid/contract_checked + pf-core-adapter: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: PF-Core provability-fabric-core adapter parity + run: bash scripts/run-pf-core-adapter-ci.sh + rust: runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f108e7c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +## PF-Core v0.1 production kernel (Tier 1–3) + +### Added + +- `semantics_layer` on `PFCoreContract.v0` declaring per-field discharge layers (`lean`, `runtime`, `out_of_scope`). +- `contract_semantics_checked` on `PFCoreCertificate.v0` derived from contract semantics layers and actual checks. +- Shared negative hash vectors under `python/tests/hash_vectors/pf_core/invalid/` with Rust and Python parity tests. +- `pcs pcs-envelope check` alias for PCS release-envelope consistency; `pcs lean-check` retained with deprecation notice. +- `scripts/run-pf-core-adapter-ci.sh` for provability-fabric-core hash-vector pin verification. +- Tier 1 tests in `python/tests/test_pf_core_tier1.py`. +- Documentation: `generated-proofs.md`, `windows-lean.md`, updated gap audit and contract semantics. + +### Changed + +- PCS release path framed as envelope-only (`ProofChecked`); not conflated with PF-Core `LeanKernelChecked`. +- Cross-language PF-Core parity extended to contract validation and denied-event preservation (Rust `pf_core.rs`, TypeScript `pfCore.ts`). +- CertifyEdge CI tries live CLI when present, falls back to `PCS_CERTIFYEDGE_MOCK=1`. + +### Documented (deferred) + +- Full global non-interference remains open research (`non-interference.md`). +- Role-to-capability split is a permanent trusted-boundary assumption (`assumptions.md`). +- PKI signing infrastructure out of scope for v0.1. diff --git a/docs/pf-core-trace-mapping.md b/docs/pf-core-trace-mapping.md index b26661e..2e0a47b 100644 --- a/docs/pf-core-trace-mapping.md +++ b/docs/pf-core-trace-mapping.md @@ -39,6 +39,21 @@ Both repos agree on: PCS hash vectors under `python/tests/hash_vectors/` must match provability-fabric-core `adapters/pcs/tests/fixtures/hash_vectors/` at the pinned PF-Core tag. +PF-Core-specific vectors live under `python/tests/hash_vectors/pf_core/` (`PFCoreEvent.v0`, `PFCoreTrace.v0`, `PFCoreContract.v0`) and are verified in Python, Rust, and TypeScript parity tests. + +--- + +## Runtime observation ordering + +`PFCoreRuntimeObservation.v0` carries a required non-negative `sequence` field. Deterministic trace compilation orders observations by `(sequence, source_index)` before hash chaining: + +1. Sort observations by `sequence` ascending; equal sequences preserve input order. +2. Compile each observation to `PFCoreEvent.v0` using its `sequence` value. +3. Recompute `previous_event_hash` / `event_hash` sequentially from genesis. +4. Reject traces that drop denied observations (`DroppedDeniedEvent`). + +Single-observation compile paths (`compile_runtime_observation_to_event`) preserve the observation `sequence` and `policy_ref` as `contract_refs`. + --- ## LabTrust replay path diff --git a/docs/pf-core/assumptions.md b/docs/pf-core/assumptions.md index 3b197c3..9b70cdd 100644 --- a/docs/pf-core/assumptions.md +++ b/docs/pf-core/assumptions.md @@ -5,7 +5,7 @@ PF-Core proofs and certificates are conditional on explicit assumptions. This do ## Cryptographic assumptions - SHA-256 collision resistance for canonical artifact hashes and event hash chains. -- `signature_or_digest` fields bind artifact bytes; full PKI signing infrastructure is out of scope for v0.1. +- `signature_or_digest` fields bind artifact bytes; **PKI signing, HSM integration, and X.509 certificate chains are out of scope for PF-Core v0.1** (documented assumption only). ## Producer assumptions @@ -18,7 +18,7 @@ PF-Core proofs and certificates are conditional on explicit assumptions. This do - `LeanKernelChecked` is emitted **only** when that concrete proof succeeds (`traceSafeD … = true` via `decide`). `--skip-build` or `--skip-lean-proof` yields `RuntimeChecked` only. - `lake build PFCore` means the PF-Core library compiles; individual traces still require generated proof files for kernel claims. - Theorem names in `PF_CORE_THEOREM_CATALOG` exist as Lean symbols (enforced by `pcs pf-core audit-lean-catalog`). -- PCS `pcs lean-check` (without `--trace`) is **not** Lean-backed per trace. It prints the assurance boundary and exits; it must not be described as kernel-verified trace safety. +- PCS `pcs pcs-envelope check` (alias `pcs lean-check`) validates release-envelope consistency only; it must not be described as PF-Core kernel-verified trace safety. ## Role and capability alignment (permanent boundary) @@ -26,7 +26,7 @@ PF-Core proofs and certificates are conditional on explicit assumptions. This do - The PF-Core runtime compiler expands known roles into explicit capability ids on compiled principals. - Traces submitted to `pcs pf-core lean-check` must list those expanded capabilities explicitly; lean-check rejects principals where `capabilities` does not match role expansion. - Runtime authorization may still consult roles during compilation; lean-check and Lean proofs rely on the explicit capability list only. -- This role-to-capability split is a **permanent trusted-boundary assumption**, not a temporary Stage 1 gap. +- This role-to-capability split is a **permanent trusted-boundary assumption** unless a future Lean RoleMap stage expands kernel capability resolution. ## Registry assumptions diff --git a/docs/pf-core/claim-boundary.md b/docs/pf-core/claim-boundary.md index 9a74ed5..fb1b74d 100644 --- a/docs/pf-core/claim-boundary.md +++ b/docs/pf-core/claim-boundary.md @@ -29,11 +29,20 @@ The claim-boundary linter (`pcs pf-core audit-claims`) fails on these phrases in | fully verified runtime | runtime-checked trace with explicit claim class | | formally verified platform | release-envelope consistency theorem family (for PCS Lean scope) | -## LeanCheckResult.v0 (PCS bridge artifact) +## PCS release-envelope consistency (`pcs pcs-envelope check`) -PF-Core `pcs pf-core lean-check` emits a registered `LeanCheckResult.v0` object alongside optional `PFCoreCertificate.v0` output. +PCS release-envelope checks validate `ProofObligation.v0` against the PCS theorem catalog in `lean/PCS/Theorems.lean`. + +- Preferred command: `pcs pcs-envelope check --obligations … --out …` +- Legacy alias: `pcs lean-check` (prints deprecation notice; same behavior) +- Emits `LeanCheckResult.v0` with lifecycle status `ProofChecked` when obligations pass +- Does **not** prove PF-Core trace safety; does not emit `LeanKernelChecked` + +Use `pcs pf-core lean-check --trace …` for PF-Core kernel assurance (`LeanKernelChecked` when concrete proof succeeds). -### Current behavior (Stage 4) +## PF-Core lean-check (`pcs pf-core lean-check`) + +PF-Core `pcs pf-core lean-check` emits a registered `LeanCheckResult.v0` object alongside optional `PFCoreCertificate.v0` output. - `status: LeanProofChecked` means Python deciders passed, the no-sorry audit passed, `lake build PFCore` succeeded, and a generated concrete proof file checked `traceSafeD tr = true` via the Lean kernel. - `status: DecidersPassed` means deciders (and no-sorry audit) passed but Lean proof was skipped (`--skip-build` or `--skip-lean-proof`) or only runtime assurance was requested. @@ -52,7 +61,7 @@ Do **not** treat PCS lifecycle `ProofChecked` as a PF-Core formal claim. PF-Core Successful lean-check writes a certificate with matching `claim_class`, `assumption_refs`, `theorems_checked`, `obligations`, `lean_build_status`, `lean_proof_checked`, and `disclaimer`. -- `LeanKernelChecked` requires `proof_term_ref`, `proof_ref`, `lean_proof_checked: true`, and successful concrete Lean proof. +- `LeanKernelChecked` requires `proof_term_ref`, `proof_ref`, `lean_proof_checked: true`, successful concrete Lean proof, and **contract grounding** (non-empty event `contract_refs` or `default_contract_ref: "trace-safe"` aligned with `PFCore.traceSafeContract`). - `--skip-build` or `--skip-lean-proof` yields `RuntimeChecked` only (no `proof_term_ref`). ### Mapping guidance for PF-Core certificates diff --git a/docs/pf-core/contract-semantics.md b/docs/pf-core/contract-semantics.md index e8fc133..3cf1b64 100644 --- a/docs/pf-core/contract-semantics.md +++ b/docs/pf-core/contract-semantics.md @@ -17,19 +17,58 @@ This document maps `PFCoreContract.v0` JSON fields to runtime checker predicates | `ContractPostSpec` | `post` object | | `ContractInvariantSpec` | `invariant` object | +## semantics_layer (PFCoreContract.v0) + +Each contract may declare which fields are discharged in Lean, checked only at runtime, or explicitly out of scope: + +```json +"semantics_layer": { + "require_capability": "lean", + "require_role": "runtime", + "require_decision": "lean", + "require_trace_safe": "lean" +} +``` + +Field names are bare (unique across pre/post/invariant). When `semantics_layer` is omitted, defaults are derived from the mapping table below. + +| Layer | Meaning | +|-------|---------| +| `lean` | Discharged by generated Lean `ContractDecide` proofs when lean-check runs | +| `runtime` | Validated by `pcs pf-core validate-contracts` only | +| `out_of_scope` | Documented gap; must not appear on active required fields | + +Validation rejects orphan semantics entries, active fields marked `out_of_scope`, and inconsistent explicit layers. + +## semantics_layer (PFCoreContract.v0) + +Contracts may declare per-field discharge layers at the source: + +```json +"semantics_layer": { + "require_capability": "lean", + "require_role": "runtime", + "require_policy_ref": "runtime" +} +``` + +Allowed values: `lean`, `runtime`, `out_of_scope`. + +When `semantics_layer` is omitted, defaults are derived from the mapping table below. Explicit entries must match the canonical layer for active fields (validated by `pcs validate`). + ## Formal mapping table (JSON ↔ Lean ↔ runtime) -| JSON field | Runtime (`pf_core_contract.py`) | Lean decider | Generated proof | -|------------|-----------------------------------|--------------|-----------------| -| `pre.require_capability` | `_principal_has_capability` | `contractPreD` / `hasCapabilityD` | `concrete_contract_pre_*` | -| `pre.require_effect` | `_action_has_effect` | `contractPreD` / `actionHasEffectD` | `concrete_contract_pre_*` | -| `pre.require_tenant_match` | `_tenant_matches` | `contractPreD` / `actionWithinTenantD` | `concrete_contract_pre_*` | -| `pre.require_role` | role list membership | **Not mapped** | — | -| `pre.require_policy_ref` | `contract_refs` contains ref | **Not mapped** | — | -| `pre.require_evidence_ref` | `evidence_refs` contains ref | **Not mapped** | — | -| `post.require_decision` | decision equality | `contractPostD` | `concrete_contract_post_*` | -| `post.require_event_safe` | capability + tenant on allow | `contractPostD` / `eventSafeD` | `concrete_contract_post_*` | -| `invariant.require_trace_safe` | (trace-level) | `contractInvariantD` / `traceSafeD` | `concrete_trace_satisfies_*` | +| JSON field | Default layer | Runtime (`pf_core_contract.py`) | Lean decider | Generated proof | +|------------|---------------|-----------------------------------|--------------|-----------------| +| `pre.require_capability` | `lean` | `_principal_has_capability` | `contractPreD` / `hasCapabilityD` | `concrete_contract_pre_*` | +| `pre.require_effect` | `lean` | `_action_has_effect` | `contractPreD` / `actionHasEffectD` | `concrete_contract_pre_*` | +| `pre.require_tenant_match` | `lean` | `_tenant_matches` | `contractPreD` / `actionWithinTenantD` | `concrete_contract_pre_*` | +| `pre.require_role` | `runtime` | role list membership | **Not mapped** | — | +| `pre.require_policy_ref` | `runtime` | `contract_refs` contains ref | **Not mapped** | — | +| `pre.require_evidence_ref` | `runtime` | `evidence_refs` contains ref | **Not mapped** | — | +| `post.require_decision` | `lean` | decision equality | `contractPostD` | `concrete_contract_post_*` | +| `post.require_event_safe` | `lean` | capability + tenant on allow | `contractPostD` / `eventSafeD` | `concrete_contract_post_*` | +| `invariant.require_trace_safe` | `lean` | (trace-level) | `contractInvariantD` / `traceSafeD` | `concrete_trace_satisfies_*` | Per-event discharge: `satisfiesContractSpecD`. Trace-level: `traceSatisfiesContractSpecsD`. @@ -41,7 +80,23 @@ When `contract_refs` appear on events and contract JSON is found alongside the t 2. Generated `.lean` file includes `ContractPreSpec` / `PostSpec` / `Inv` defs and `decide` proofs. 3. `lake env lean` on the generated module discharges concrete obligations. -Fields marked **Not mapped** remain runtime-only; codegen documents the gap in the header when refs exist but contracts are missing. +Fields marked **Not mapped** remain runtime-only unless `semantics_layer` overrides a mapped field to `runtime`. Codegen skips Lean obligations for non-`lean` fields. + +## contract_semantics_checked (PFCoreCertificate.v0) + +When `pcs pf-core lean-check` emits a certificate, it includes: + +```json +"contract_semantics_checked": { + "lean": ["contract-id.pre.require_capability", "..."], + "runtime": ["contract-id.pre.require_role", "..."] +} +``` + +- `lean`: contract fields with `semantics_layer` `lean` (discharged by generated Lean proofs when lean-check succeeds). +- `runtime`: fields with layer `runtime` (validated by `pcs pf-core validate-contracts`). + +Out-of-scope fields are omitted from both lists. ## Invariant preservation (Lean) diff --git a/docs/pf-core/current-gap-audit.md b/docs/pf-core/current-gap-audit.md index d1978c7..c202fc7 100644 --- a/docs/pf-core/current-gap-audit.md +++ b/docs/pf-core/current-gap-audit.md @@ -2,6 +2,38 @@ Summary of gaps between the PF-Core vision and the current `pcs-core` repository. +## Tier 1 — production trusted kernel (complete) + +**Status:** Tier 1 production kernel: complete (uncommitted) + +| Item | Status | Notes | +|------|--------|-------| +| `semantics_layer` on `PFCoreContract.v0` | Done | Flat field map: `lean` / `runtime` / `out_of_scope`; validator defaults | +| `contract_semantics_checked` on certificates | Done | Derived from semantics layers + checks | +| Cross-language semantic parity | Done | Rust `pf_core.rs`, TS `pfCore.ts`, shared invalid vectors | +| `pcs pcs-envelope check` | Done | Alias; `pcs lean-check` deprecated with notice | +| PCS envelope-only framing (choice B) | Done | No `LeanKernelChecked` on PCS path; docs + tests | +| CertifyEdge live-then-mock CI | Done | `pf_core_certifyedge.py`; CI mock fallback | +| `scripts/run-pf-core-adapter-ci.sh` | Done | Pinned provability-fabric-core hash parity | +| Tier 1 tests | Done | `test_pf_core_tier1.py` | + +## Tier 2 — documented deferrals (complete) + +| Item | Status | Notes | +|------|--------|-------| +| Full global non-interference | Deferred | `non-interference.md` | +| Lean RoleMap / role encoding | Deferred | Permanent assumption in `assumptions.md` | +| Full Lean role/policy/evidence contract encoding | Deferred | Runtime-only fields in semantics_layer | + +## Tier 3 — operational (complete) + +| Item | Status | Notes | +|------|--------|-------| +| Gap audit (this file) | Done | | +| `generated-proofs.md` | Done | Regeneration policy for `lean/PFCore/Generated/` | +| `CHANGELOG.md` PF-Core section | Done | | +| `windows-lean.md` | Done | elan / WSL guide | + ## Protocol and schemas | Gap | Status | Notes | @@ -31,56 +63,26 @@ Summary of gaps between the PF-Core vision and the current `pcs-core` repository | Contract satisfaction runtime checker | Done | `pf_core_contract.py` | | Resource scope enforcement | Done | Stage 7 deciders + trace validation | | Handoff preservation in trace compiler | Done | Stage 7 optional `handoffs` | -| PCS `lean-check` honest disclaimer | Done | Stage 7 release polish | - -## Stage 6 (complete) - -| Item | Status | Notes | -|------|--------|-------| -| LabTrust replay example | Done | `examples/pf-core-valid/labtrust_replay/` | -| LabTrust adapter | Done | `pf_core_labtrust_adapter.py` | -| Examples check + replay gate | Done | `replay_required` in manifest | -| Hash vector parity | Done | Native checker `pf_core_hash_vector_parity.py` | -| Bridge artifact spec | Done | `docs/pf-core/bridge-artifact.md` | - -## Stage 7 (complete subset) - -1. `python/pcs_core/pf_core_contract.py` — contract load + trace validation. -2. Handoff events compiled from optional `ToolUseTrace.handoffs` with `HandoffAuthorityExpansion` gate. -3. Resource scope checks in runtime, trace validation, and lean deciders. -4. Fixtures: `contract_checked/`, `contract_violation/`, `resource_scope_violation/`, `handoff_compile_expansion/`. -5. Tests in `python/tests/test_pf_core_stage7.py`. - -## Release polish (Phases A–E) - -| Item | Status | Notes | -|------|--------|-------| -| Phase A — documentation accuracy | Done | threat-model, assumptions, mission, gap audit | -| Phase B — CI pf-core lean-check | Done | lean job runs full lean-check on fixture | -| Phase C — TS/Rust PF-Core schemas | Done | explicit `artifact_type` detection | -| Phase D — invariant theorem + richer codegen | Done | `Contract.lean`, per-event proofs, contract-semantics doc | -| Phase E — bridge demo + AssumptionSet fixtures | Done | `assumption_declared/`, bridge script, tests | -| Phase 6 partial — registry deferral consistency | Done | ProofChecked requires assumption refs when checks deferred | -| Presentation bundle (`docs/pf-core/presentation/`) | Done | | -| Registry merged with main PCS entries + PF-Core entries | Done | | -| Cross-language parity tests | Done | `test_pf_core_cross_language.py` | +| PCS release-envelope path clarity | Done | `pcs pcs-envelope check`; lean-check deprecated | ## Remaining research (deferred) -1. **Full provability-fabric-core live adapter CI** — hash parity covered natively; full adapter orchestration remains cross-repo. +1. **Full provability-fabric-core live adapter orchestration** — hash parity covered natively via adapter CI script; full cross-repo orchestration remains optional. 2. **Full agent runtime, MCP, NL policy, model safety** — out of scope. +3. **Global non-interference** — see `non-interference.md`. ## Phase F (research-grade extensions) | Item | Status | Notes | |------|--------|-------| -| F1 — Conservative tenant non-interference | Done | `NonInterference.lean`, `validate_tenant_isolation`, `cross_tenant_leak/` | -| F2 — JSON contract discharge in Lean codegen | Done | `ContractDecide.lean`, generated contract proofs, `contract-semantics.md` | -| F3 — CertifyEdge hook + mock CI | Done | `pf_core_certifyedge.py`, `certifyedge-check` CLI | -| Release checklist + theorem sheet | Done | `release-checklist.md`, updated presentation bundle | +| F1 — Conservative tenant non-interference | Done | `NonInterference.lean`, `validate_tenant_isolation` | +| F2 — JSON contract discharge in Lean codegen | Done | `ContractDecide.lean`, `contract-semantics.md` | +| F3 — CertifyEdge hook + mock CI | Done | `pf_core_certifyedge.py` | +| Release checklist + theorem sheet | Done | `release-checklist.md` | -### Honest limitations (Phase F) +### Honest limitations (Phase F + Tier 2) - Tenant theorems cover **allowed events in safe traces**, not global cross-tenant non-interference. -- Lean contract discharge maps capability, effect, tenant, decision, event_safe, trace_safe only; role/policy/evidence refs remain runtime-only. -- CertifyEdge live CLI depends on external install; CI uses `PCS_CERTIFYEDGE_MOCK=1`. +- Lean contract discharge maps capability, effect, tenant, decision, event_safe, trace_safe only; role/policy/evidence refs remain runtime-only (`semantics_layer`). +- CertifyEdge live CLI depends on external install; CI uses mock when absent. +- PKI is documented out of scope for v0.1 only. diff --git a/docs/pf-core/generated-proofs.md b/docs/pf-core/generated-proofs.md new file mode 100644 index 0000000..e77f67d --- /dev/null +++ b/docs/pf-core/generated-proofs.md @@ -0,0 +1,51 @@ +# Generated PF-Core Lean proofs + +Concrete trace proof files under `lean/PFCore/Generated/` are **generated artifacts**, not hand-maintained source. + +## What gets generated + +When `pcs pf-core lean-check --trace ` runs the full pipeline (default), it writes a module such as: + +``` +lean/PFCore/Generated/Trace_.lean +``` + +Each file contains: + +- Concrete `Principal`, `Action`, `Event`, and `Trace` definitions for the input trace +- Optional `ContractPreSpec` / `PostSpec` / `Inv` defs when event `contract_refs` bind contracts +- `decide`-based theorems (`concrete_trace_safe`, per-event `eventSafeD`, contract discharge) + +The certificate records `proof_term_ref` pointing at the generated module path. + +## Regeneration + +From a clean checkout with Lean 4 (`lake`) available: + +```bash +cd lean +lake build PFCore +cd ../python +pcs pf-core lean-check --trace ../examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json +``` + +For contract-bound traces: + +```bash +pcs pf-core validate-contracts \ + ../examples/pf-core-valid/contract_checked/trace.json \ + --contracts-dir ../examples/pf-core-valid/contract_checked +pcs pf-core lean-check --trace ../examples/pf-core-valid/contract_checked/trace.json +``` + +## Git policy + +Generated modules may appear locally after lean-check but are not required in release tags unless a fixture explicitly pins an example (see `examples/pf-core-valid/tool_use_trace_compiled/generated_proof.example.lean`). + +Do not edit generated files by hand; re-run lean-check after trace or contract changes. + +## Semantics layer alignment + +Lean codegen emits contract deciders only for fields marked `lean` in the source contract's `semantics_layer` (or canonical defaults). Runtime-only fields (`require_role`, policy/evidence refs) are validated by `pcs pf-core validate-contracts` and listed under `contract_semantics_checked.runtime` on certificates. + +See [contract-semantics.md](contract-semantics.md). diff --git a/docs/pf-core/non-interference.md b/docs/pf-core/non-interference.md index 2d2515c..2af6ed9 100644 --- a/docs/pf-core/non-interference.md +++ b/docs/pf-core/non-interference.md @@ -39,9 +39,13 @@ pcs pf-core validate-trace --tenant-isolation examples/pf-core-valid/file_read_a Invalid fixture: `examples/pf-core-invalid/cross_tenant_leak/`. -## Open (not claimed) +## RoleMap permanent assumption -1. Full cross-tenant non-interference (no information flow between tenants). +Lean `HasCapability` inspects `principal.capabilities` only; **roles are not expanded in the kernel**. The runtime compiler applies `ROLE_CAPABILITY_MAP` in `pf_core_runtime.py` when compiling observations. Unless a future Lean `RoleMap` module is added and promoted to the trusted catalog, role-to-capability expansion remains a **permanent trusted-boundary assumption** documented here and in [assumptions.md](assumptions.md). + +## Open (not claimed — full global NI deferred) + +1. **Full global cross-tenant non-interference** (information-flow between tenants under arbitrary schedulers and adversaries). Current theorems cover tenant-scoped allowed events in safe traces only; full global NI is deferred to later research. 2. Non-interference under handoff across tenants (handoffs require matching tenants in `HandoffSafe`). 3. Deny-event side channels or resource existence leaks. 4. Compositional preservation of arbitrary user-defined contract invariants beyond the discharged JSON subset. diff --git a/docs/pf-core/presentation/demo-script.md b/docs/pf-core/presentation/demo-script.md index fced322..f46192d 100644 --- a/docs/pf-core/presentation/demo-script.md +++ b/docs/pf-core/presentation/demo-script.md @@ -74,15 +74,34 @@ pcs pf-core replay-trace examples/pf-core-valid/labtrust_replay/trace.json Expected: schema validation OK; replay match (adapter output per `docs/pf-core-trace-mapping.md`). -## 7. PCS lean-check disclaimer (not per-trace Lean) +## 7. PCS release-envelope check (not per-trace PF-Core Lean) ```bash -pcs lean-check +pcs pcs-envelope check --obligations examples/proof_obligation.valid.json --out /tmp/lean_check_result.json --skip-lean-build ``` -Expected: exit code 2, stderr explains PCS path is not Lean-backed per trace; directs to `pcs pf-core lean-check`. +Legacy alias (prints deprecation notice): -## 8. LabTrust end-to-end bridge (Phase E) +```bash +pcs lean-check --obligations examples/proof_obligation.valid.json --out /tmp/lean_check_result.json --skip-lean-build +``` + +Expected: `LeanCheckResult.v0` with PCS `ProofChecked` or rejection; stderr explains this is release-envelope consistency only and directs to `pcs pf-core lean-check` for PF-Core traces. Output must not mention `LeanKernelChecked`. + +## 8. CertifyEdge (live or mock) + +Install [CertifyEdge](https://github.com/fraware/CertifyEdge) and ensure `certifyedge` is on PATH, or use mock mode: + +```bash +PCS_CERTIFYEDGE_MOCK=1 pcs pf-core certifyedge-check \ + --trace examples/pf-core-valid/labtrust_replay/trace.json \ + --property qc_release.temporal.safety \ + --out /tmp/PFCoreCertificate.certifyedge.json +``` + +CI tries live CLI when available and falls back to mock without failing the pipeline. + +## 9. LabTrust end-to-end bridge (Phase E) Automated script: diff --git a/docs/pf-core/production-kernel-checklist.md b/docs/pf-core/production-kernel-checklist.md new file mode 100644 index 0000000..36692d6 --- /dev/null +++ b/docs/pf-core/production-kernel-checklist.md @@ -0,0 +1,49 @@ +# PF-Core production kernel — external auditor sign-off + +One-page verification checklist for Tier 1 PF-Core production kernel readiness in `pcs-core`. Run from repository root unless noted. + +## Scope + +This checklist covers the **production trusted kernel** only: contract semantics, cross-language validation parity, PCS release-envelope separation (choice B), and integration hooks. It does **not** claim full global non-interference or Lean RoleMap discharge (Tier 2 deferrals). + +## Evidence commands + +| Check | Command | Expected | +|-------|---------|----------| +| Tier 1 semantics + envelope | `cd python && pytest -q tests/test_pf_core_tier1.py` | All pass | +| Cross-language parity | `cd python && pytest -q tests/test_pf_core_cross_language.py` | All pass (TS may skip if `node` absent) | +| Full PF-Core suite | `cd python && pytest -q tests/ -k test_pf_core` | All pass | +| Rust PF-Core vectors | `cd rust && cargo test pf_core` | All pass | +| TypeScript hash vectors | `cd typescript/packages/core && npx tsc && node --test dist/tests/examples.test.js` | All pass | +| PCS envelope path | `pcs pcs-envelope check --obligations examples/proof_obligation.valid.json --out /tmp/envelope.json --skip-lean-build` | No `LeanKernelChecked` in output | +| Lean kernel (optional) | `cd lean && lake build PFCore && pcs pf-core lean-check --trace examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json` | `LeanKernelChecked` when full pipeline succeeds | +| Adapter pin parity | `bash scripts/run-pf-core-adapter-ci.sh` | Vectors match `provability-fabric-core` pin | + +## Artifact and policy checks + +| Item | Location | Auditor confirms | +|------|----------|------------------| +| `semantics_layer` schema | `schemas/PFCoreContract.v0.schema.json` | Field map with `lean` / `runtime` / `out_of_scope` | +| Semantics validator | `python/pcs_core/pf_core_contract_semantics.py` | Wired into `validate-contracts` and lean codegen | +| Trusted boundary | `docs/pf-core/trusted-boundary.md` | PCS path is envelope-only; PF-Core kernel on `pf-core lean-check` | +| Claim boundaries | `docs/pf-core/claim-boundary.md` | No silent upgrade between claim classes | +| Deferred research | `docs/pf-core/non-interference.md`, `assumptions.md` | RoleMap permanent assumption documented | +| Frozen hash vectors | `python/tests/hash_vectors/pf_core/` | Python, Rust, TypeScript agree on canonical form | + +## Claim class boundaries (must not overclaim) + +| Path | May emit | Must not emit | +|------|----------|---------------| +| `pcs pcs-envelope check` | `ProofChecked` / `Rejected` on `LeanCheckResult.v0` | `LeanKernelChecked` | +| `pcs pf-core lean-check` (full) | `LeanKernelChecked` when concrete proof succeeds | Global non-interference, full role/policy Lean discharge | +| `pcs pf-core lean-check --skip-build` | `RuntimeChecked` | `LeanKernelChecked` | +| `pcs pf-core certifyedge-check` | `CertificateChecked` | `LeanKernelChecked` | + +## Sign-off + +| Role | Name | Date | Result | +|------|------|------|--------| +| Engineering | | | Tier 1 complete / gaps noted | +| Security / assurance | | | Boundaries accepted / exceptions listed | + +Reference: `docs/pf-core/current-gap-audit.md`, `docs/pf-core/release-checklist.md`, `CHANGELOG.md`. diff --git a/docs/pf-core/release-checklist.md b/docs/pf-core/release-checklist.md index 8059b9e..1931017 100644 --- a/docs/pf-core/release-checklist.md +++ b/docs/pf-core/release-checklist.md @@ -6,7 +6,8 @@ Pre-release verification for PF-Core in `pcs-core`. Run from repository root unl | Job / step | Proves | |------------|--------| -| `pytest tests/test_pf_core_*.py` | Python runtime, contracts, codegen, CertifyEdge mock, fixtures | +| `pytest tests/test_pf_core_tier1.py` | semantics_layer, PCS envelope alias, negative vectors | +| `pytest tests/test_pf_core_cross_language.py` | Python/Rust/TS parity on shared vectors | | `pcs pf-core audit-claims` | No forbidden overclaim phrases in docs/examples | | `pcs pf-core audit-boundary` | Trusted-boundary docs and registry consistency | | `pcs pf-core audit-lean-catalog` | Catalog symbols exist in Lean sources | @@ -15,7 +16,8 @@ Pre-release verification for PF-Core in `pcs-core`. Run from repository root unl | Lean job: `lake build PFCore` | Kernel compiles; decider soundness theorems check | | Lean job: `pcs pf-core lean-check` | Concrete trace proof + `LeanKernelChecked` path on fixture | | Lean job: `validate-contracts` | Contract runtime checker on `contract_checked/` | -| `PCS_CERTIFYEDGE_MOCK=1 pcs pf-core certifyedge-check` | External checker hook (mock attestation) | +| `PCS_CERTIFYEDGE_MOCK=1 pcs pf-core certifyedge-check` | External checker hook (mock attestation; live CLI when installed) | +| `bash scripts/run-pf-core-adapter-ci.sh` | provability-fabric-core hash vector parity (optional CI job) | ## Local full demo diff --git a/docs/pf-core/trusted-boundary.md b/docs/pf-core/trusted-boundary.md index a8b3584..c4a79d6 100644 --- a/docs/pf-core/trusted-boundary.md +++ b/docs/pf-core/trusted-boundary.md @@ -20,6 +20,12 @@ This document lists what PCS/PF-Core treats as trusted, untrusted, or assumed wh | Role → capability expansion | `python/pcs_core/pf_core_runtime.py` | Compiler expands roles; lean-check requires explicit capabilities on traces | | PF-Core no-sorry audit | `python/pcs_core/lean_check.py` | Scans `lean/PFCore/` for forbidden tokens | +## PCS release-envelope path (Choice B — permanent) + +PCS `pcs pcs-envelope check` (and deprecated `pcs lean-check`) evaluate **release-envelope consistency** only (`ProofObligation.v0` against `lean/PCS/Theorems.lean`). This path is **envelope-only** and does not generate per-trace Lean proof terms or emit PF-Core `LeanKernelChecked` claims. Full PCS Lean term generation for arbitrary traces remains out of scope unless a future PCS-Lean stage is added. + +PF-Core trace kernel assurance requires `pcs pf-core lean-check --trace`. + ## Untrusted (must not produce LeanKernelChecked claims) | Component | Reason | @@ -36,7 +42,23 @@ This document lists what PCS/PF-Core treats as trusted, untrusted, or assumed wh See [assumptions.md](assumptions.md). Assumptions must appear in `AssumptionSet.v0` or PF-Core certificate assumption refs before any external claim. -## Allowlisted Lean axioms +## PCS release-envelope path (permanent envelope-only framing) + +PCS `pcs pcs-envelope check` (and deprecated `pcs lean-check`) validates `ProofObligation.v0` release-envelope consistency against the PCS theorem catalog. It emits `ProofChecked` on `LeanCheckResult.v0` when obligations pass; this is **not** PF-Core `LeanKernelChecked` trace safety. + +There is no silent upgrade from envelope checks to per-trace Lean kernel proofs. PF-Core kernel assurance requires `pcs pf-core lean-check --trace …` with concrete generated proof terms (see [generated-proofs.md](generated-proofs.md)). + +## PCS release-envelope path (permanent envelope-only scope) + +PCS `pcs pcs-envelope check` (formerly `pcs lean-check`) validates **release-envelope consistency** only: + +- Proof obligations against `lean/PCS/Theorems.lean` +- Emits `ProofChecked` on `LeanCheckResult.v0`; never `LeanKernelChecked` +- No per-trace PF-Core Lean term generation unless a future **Stage PCS-Lean** is added + +PF-Core kernel assurance remains exclusively on `pcs pf-core lean-check --trace …`. + +See `docs/pf-core/generated-proofs.md` for gitignored `lean/PFCore/Generated/` regeneration. No PF-Core trusted Lean file may contain `sorry`, `admit`, `axiom`, or `unsafe` unless listed here. diff --git a/docs/pf-core/windows-lean.md b/docs/pf-core/windows-lean.md new file mode 100644 index 0000000..e6739ce --- /dev/null +++ b/docs/pf-core/windows-lean.md @@ -0,0 +1,51 @@ +# PF-Core Lean on Windows + +PF-Core lean-check requires Lean 4 (`lake`) and optionally WSL when native tooling is unavailable. + +## Recommended paths + +| Approach | When to use | +|----------|-------------| +| WSL2 + elan | Primary recommendation on Windows; matches Linux CI | +| Native elan | When `lake` is on PATH and builds succeed locally | + +## WSL2 setup + +1. Install [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install) with an Ubuntu distribution. +2. Install elan inside WSL: + +```bash +curl -sSfL https://github.com/leanprover/elan/releases/download/v4.0.0/elan-x86_64-unknown-linux-gnu.tar.gz | tar xz +./elan-init -y --default-toolchain none +elan default leanprover/lean4:v4.14.0 +``` + +3. Clone or access the pcs-core repo from the WSL filesystem (e.g. `/mnt/c/Users/.../pcs-core`) or a Linux home checkout for best I/O performance. +4. Build and lean-check: + +```bash +cd lean +lake build PCS +lake build PFCore +cd ../python +pip install -e ".[dev]" +pcs pf-core lean-check --trace ../examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json +``` + +`pcs pf-core lean-check` detects missing native `lake` on Windows and retries via `wsl bash -lc 'cd … && lake …'` when WSL is installed. + +## Native elan (optional) + +Install elan for Windows from the [Lean releases](https://github.com/leanprover/elan/releases) page, pin `leanprover/lean4:v4.14.0`, and ensure `lake` is on PATH before running lean-check from PowerShell. + +## PCS release-envelope checks + +PCS `pcs pcs-envelope check` (and deprecated `pcs lean-check`) evaluate ProofObligation.v0 in Python; they do not require PF-Core `lake build PFCore` unless you also run PF-Core lean-check. + +## Troubleshooting + +- **`lake executable not found`**: Install elan or enable WSL fallback. +- **Path with spaces**: Prefer WSL paths or quote PowerShell arguments. +- **Generated proof stale**: Delete `lean/PFCore/Generated/Trace_*.lean` and re-run lean-check. + +See also [generated-proofs.md](generated-proofs.md) and [trusted-boundary.md](trusted-boundary.md). diff --git a/examples/pf-core-invalid/contract_capability_missing/contracts/contract.json b/examples/pf-core-invalid/contract_capability_missing/contracts/contract.json new file mode 100644 index 0000000..8941ad0 --- /dev/null +++ b/examples/pf-core-invalid/contract_capability_missing/contracts/contract.json @@ -0,0 +1,15 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreContract.v0", + "contract_id": "contract-file-read-v0", + "name": "Strict file read contract", + "pre": { + "require_capability": "cap:file-read" + }, + "post": {}, + "invariant": {}, + "semantics_layer": { + "require_capability": "lean" + }, + "signature_or_digest": "sha256:e8885483b7bad56b9854dae186f2dc9c9261895fd27329897306426cd116e085" +} diff --git a/examples/pf-core-invalid/contract_capability_missing/manifest.json b/examples/pf-core-invalid/contract_capability_missing/manifest.json new file mode 100644 index 0000000..444db16 --- /dev/null +++ b/examples/pf-core-invalid/contract_capability_missing/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "ContractCapabilityRequired", + "must_fail_at": "validate_trace_contracts" +} diff --git a/examples/pf-core-invalid/contract_capability_missing/trace.json b/examples/pf-core-invalid/contract_capability_missing/trace.json new file mode 100644 index 0000000..7668df2 --- /dev/null +++ b/examples/pf-core-invalid/contract_capability_missing/trace.json @@ -0,0 +1,69 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-file-read-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-file-read-1", + "trace_id": "trace-file-read-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "email_user" + ], + "capabilities": [ + "cap:email-send" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [ + "contract-file-read-v0" + ], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:8418535c830ec9fbdfad8f352525918aa28439c812cc18f1dd04186803b423da", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:8418535c830ec9fbdfad8f352525918aa28439c812cc18f1dd04186803b423da" + } + ], + "trace_hash": "sha256:05e6749bb3ae276234a2baba73508477d0a92e13e360ec748f9c517b5cf2b2a9", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:05e6749bb3ae276234a2baba73508477d0a92e13e360ec748f9c517b5cf2b2a9" +} diff --git a/examples/pf-core-invalid/contract_effect_missing/contracts/contract.json b/examples/pf-core-invalid/contract_effect_missing/contracts/contract.json new file mode 100644 index 0000000..7118bbd --- /dev/null +++ b/examples/pf-core-invalid/contract_effect_missing/contracts/contract.json @@ -0,0 +1,15 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreContract.v0", + "contract_id": "contract-file-read-v0", + "name": "Strict file read contract", + "pre": { + "require_effect": "file.read" + }, + "post": {}, + "invariant": {}, + "semantics_layer": { + "require_effect": "lean" + }, + "signature_or_digest": "sha256:787dd85b3b75a6480cb3165ebf520fcc8518d172baa90d8e55f04f8588802558" +} diff --git a/examples/pf-core-invalid/contract_effect_missing/manifest.json b/examples/pf-core-invalid/contract_effect_missing/manifest.json new file mode 100644 index 0000000..8193e24 --- /dev/null +++ b/examples/pf-core-invalid/contract_effect_missing/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "ContractEffectRequired", + "must_fail_at": "validate_trace_contracts" +} diff --git a/examples/pf-core-invalid/contract_effect_missing/trace.json b/examples/pf-core-invalid/contract_effect_missing/trace.json new file mode 100644 index 0000000..44655a4 --- /dev/null +++ b/examples/pf-core-invalid/contract_effect_missing/trace.json @@ -0,0 +1,72 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-file-read-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-file-read-1", + "trace_id": "trace-file-read-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read", + "cap:email-send", + "cap:handoff", + "cap:mcp-invoke" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-write", + "effect_kind": "file.write", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.write" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [ + "contract-file-read-v0" + ], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:5c9734481c83ac23f0c7a219b70a6bfb410a43d4ad6981a93c1fb08977132ae6", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:5c9734481c83ac23f0c7a219b70a6bfb410a43d4ad6981a93c1fb08977132ae6" + } + ], + "trace_hash": "sha256:febb2d7495daa50f0d1fac6d33863accd7146cc0be2704ab6d1cd0375c6c856a", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:febb2d7495daa50f0d1fac6d33863accd7146cc0be2704ab6d1cd0375c6c856a" +} diff --git a/examples/pf-core-invalid/contract_evidence_ref_missing/contracts/contract.json b/examples/pf-core-invalid/contract_evidence_ref_missing/contracts/contract.json new file mode 100644 index 0000000..e8f2222 --- /dev/null +++ b/examples/pf-core-invalid/contract_evidence_ref_missing/contracts/contract.json @@ -0,0 +1,15 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreContract.v0", + "contract_id": "contract-file-read-v0", + "name": "Strict file read contract", + "pre": { + "require_evidence_ref": "evidence/run-1" + }, + "post": {}, + "invariant": {}, + "semantics_layer": { + "require_evidence_ref": "runtime" + }, + "signature_or_digest": "sha256:6e6152407e68ec76f851e8167da1f912f70499b16b4abb1e018acac50f295cb5" +} diff --git a/examples/pf-core-invalid/contract_evidence_ref_missing/manifest.json b/examples/pf-core-invalid/contract_evidence_ref_missing/manifest.json new file mode 100644 index 0000000..88741a5 --- /dev/null +++ b/examples/pf-core-invalid/contract_evidence_ref_missing/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "ContractEvidenceRefRequired", + "must_fail_at": "validate_trace_contracts" +} diff --git a/examples/pf-core-invalid/contract_evidence_ref_missing/trace.json b/examples/pf-core-invalid/contract_evidence_ref_missing/trace.json new file mode 100644 index 0000000..5f9c72a --- /dev/null +++ b/examples/pf-core-invalid/contract_evidence_ref_missing/trace.json @@ -0,0 +1,72 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-file-read-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-file-read-1", + "trace_id": "trace-file-read-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read", + "cap:email-send", + "cap:handoff", + "cap:mcp-invoke" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [ + "contract-file-read-v0" + ], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:845ecd9de433a71ccd232200812506ed7b5884bc59e4326bf6be84444186a2ba", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:845ecd9de433a71ccd232200812506ed7b5884bc59e4326bf6be84444186a2ba" + } + ], + "trace_hash": "sha256:5c3b5a8c85ddc09d508106240d24568c8f3a5f19373dbf5124eb19e0ade0737b", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:5c3b5a8c85ddc09d508106240d24568c8f3a5f19373dbf5124eb19e0ade0737b" +} diff --git a/examples/pf-core-invalid/contract_policy_ref_missing/contracts/contract.json b/examples/pf-core-invalid/contract_policy_ref_missing/contracts/contract.json new file mode 100644 index 0000000..daac7c9 --- /dev/null +++ b/examples/pf-core-invalid/contract_policy_ref_missing/contracts/contract.json @@ -0,0 +1,15 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreContract.v0", + "contract_id": "contract-file-read-v0", + "name": "Strict file read contract", + "pre": { + "require_policy_ref": "policy/default.v0" + }, + "post": {}, + "invariant": {}, + "semantics_layer": { + "require_policy_ref": "runtime" + }, + "signature_or_digest": "sha256:42d70b98f7ca8b5144d24f5af1222c8c41f4a097b8bdecb24bb5c6a13efec7ff" +} diff --git a/examples/pf-core-invalid/contract_policy_ref_missing/manifest.json b/examples/pf-core-invalid/contract_policy_ref_missing/manifest.json new file mode 100644 index 0000000..09d0e2e --- /dev/null +++ b/examples/pf-core-invalid/contract_policy_ref_missing/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "ContractPolicyRefRequired", + "must_fail_at": "validate_trace_contracts" +} diff --git a/examples/pf-core-invalid/contract_policy_ref_missing/trace.json b/examples/pf-core-invalid/contract_policy_ref_missing/trace.json new file mode 100644 index 0000000..5f9c72a --- /dev/null +++ b/examples/pf-core-invalid/contract_policy_ref_missing/trace.json @@ -0,0 +1,72 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-file-read-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-file-read-1", + "trace_id": "trace-file-read-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read", + "cap:email-send", + "cap:handoff", + "cap:mcp-invoke" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [ + "contract-file-read-v0" + ], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:845ecd9de433a71ccd232200812506ed7b5884bc59e4326bf6be84444186a2ba", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:845ecd9de433a71ccd232200812506ed7b5884bc59e4326bf6be84444186a2ba" + } + ], + "trace_hash": "sha256:5c3b5a8c85ddc09d508106240d24568c8f3a5f19373dbf5124eb19e0ade0737b", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:5c3b5a8c85ddc09d508106240d24568c8f3a5f19373dbf5124eb19e0ade0737b" +} diff --git a/examples/pf-core-invalid/contract_ref_missing/manifest.json b/examples/pf-core-invalid/contract_ref_missing/manifest.json new file mode 100644 index 0000000..8d56555 --- /dev/null +++ b/examples/pf-core-invalid/contract_ref_missing/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "ContractRefMissing", + "must_fail_at": "validate_trace_contracts" +} diff --git a/examples/pf-core-invalid/contract_ref_missing/trace.json b/examples/pf-core-invalid/contract_ref_missing/trace.json new file mode 100644 index 0000000..3dc2c3d --- /dev/null +++ b/examples/pf-core-invalid/contract_ref_missing/trace.json @@ -0,0 +1,72 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-file-read-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-file-read-1", + "trace_id": "trace-file-read-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read", + "cap:email-send", + "cap:handoff", + "cap:mcp-invoke" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [ + "contract-missing-v0" + ], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:4ff982a5dcbbe1a572ad805d7a568d4d5bf99fd1623a8503b7e7d469163591b4", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:4ff982a5dcbbe1a572ad805d7a568d4d5bf99fd1623a8503b7e7d469163591b4" + } + ], + "trace_hash": "sha256:040568ee09edba624a5fa4597be3aafd65bf7e4717b3e32f030e0505b98cbe45", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:040568ee09edba624a5fa4597be3aafd65bf7e4717b3e32f030e0505b98cbe45" +} diff --git a/examples/pf-core-invalid/contract_violation/contracts/contract.json b/examples/pf-core-invalid/contract_violation/contracts/contract.json index 22e6c6c..739255f 100644 --- a/examples/pf-core-invalid/contract_violation/contracts/contract.json +++ b/examples/pf-core-invalid/contract_violation/contracts/contract.json @@ -15,5 +15,13 @@ "invariant": { "require_trace_safe": true }, - "signature_or_digest": "sha256:ab672cb206542354c10afdbc0e739fe018acee2d4d7db40a7486bcf3071846b3" + "semantics_layer": { + "require_capability": "lean", + "require_effect": "lean", + "require_tenant_match": "lean", + "require_decision": "lean", + "require_event_safe": "lean", + "require_trace_safe": "lean" + }, + "signature_or_digest": "sha256:ade8675a56d86d5a3d2cb576fde97b633e3153f01992005f784322e6c480ca2f" } diff --git a/examples/pf-core-invalid/cross_tenant_allowed_event/manifest.json b/examples/pf-core-invalid/cross_tenant_allowed_event/manifest.json new file mode 100644 index 0000000..836f7f8 --- /dev/null +++ b/examples/pf-core-invalid/cross_tenant_allowed_event/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "TenantIsolation", + "must_fail_at": "validate_tenant_isolation" +} diff --git a/examples/pf-core-invalid/cross_tenant_allowed_event/trace.json b/examples/pf-core-invalid/cross_tenant_allowed_event/trace.json new file mode 100644 index 0000000..4f09c68 --- /dev/null +++ b/examples/pf-core-invalid/cross_tenant_allowed_event/trace.json @@ -0,0 +1,70 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-cross-tenant-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-cross-tenant-1", + "trace_id": "trace-cross-tenant-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read", + "cap:email-send", + "cap:handoff", + "cap:mcp-invoke" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/secret.txt", + "tenant": "tenant-b" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:3f95e0f3e30e374f429ae57b2691b64a10e7f1ebd85639157e5f5ffffde068de", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:3f95e0f3e30e374f429ae57b2691b64a10e7f1ebd85639157e5f5ffffde068de" + } + ], + "trace_hash": "sha256:2289214038152161f810c4f1a7d30994b45aba10cb94cd5cd83856c390844ac6", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:2289214038152161f810c4f1a7d30994b45aba10cb94cd5cd83856c390844ac6" +} diff --git a/examples/pf-core-invalid/dropped_denied_observation/manifest.json b/examples/pf-core-invalid/dropped_denied_observation/manifest.json new file mode 100644 index 0000000..cdf85cb --- /dev/null +++ b/examples/pf-core-invalid/dropped_denied_observation/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "DroppedDeniedEvent", + "must_fail_at": "validate_denied_observations_preserved" +} diff --git a/examples/pf-core-invalid/dropped_denied_observation/observation_0.json b/examples/pf-core-invalid/dropped_denied_observation/observation_0.json new file mode 100644 index 0000000..9a69240 --- /dev/null +++ b/examples/pf-core-invalid/dropped_denied_observation/observation_0.json @@ -0,0 +1,55 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreRuntimeObservation.v0", + "observation_id": "obs-ev-allow", + "trace_id": "trace-obs-batch-1", + "event_id": "ev-allow", + "sequence": 0, + "observed_at": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "allow", + "policy_ref": "policy/default.v0", + "evidence_refs": [], + "runtime_ref": "agent-runtime", + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "payload_hash": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" +} diff --git a/examples/pf-core-invalid/dropped_denied_observation/observation_1.json b/examples/pf-core-invalid/dropped_denied_observation/observation_1.json new file mode 100644 index 0000000..3070bb8 --- /dev/null +++ b/examples/pf-core-invalid/dropped_denied_observation/observation_1.json @@ -0,0 +1,55 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreRuntimeObservation.v0", + "observation_id": "obs-ev-deny", + "trace_id": "trace-obs-batch-1", + "event_id": "ev-deny", + "sequence": 1, + "observed_at": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "deny", + "decision_reason": "deny", + "policy_ref": "policy/default.v0", + "evidence_refs": [], + "runtime_ref": "agent-runtime", + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "payload_hash": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" +} diff --git a/examples/pf-core-invalid/dropped_denied_observation/pfcore_trace.json b/examples/pf-core-invalid/dropped_denied_observation/pfcore_trace.json new file mode 100644 index 0000000..23b4313 --- /dev/null +++ b/examples/pf-core-invalid/dropped_denied_observation/pfcore_trace.json @@ -0,0 +1,70 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-obs-batch-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-allow", + "trace_id": "trace-file-read-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read", + "cap:email-send", + "cap:handoff", + "cap:mcp-invoke" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:15869784310322365c61691f44cca98b32557df99538af003859687c957ad7b0", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:15869784310322365c61691f44cca98b32557df99538af003859687c957ad7b0" + } + ], + "trace_hash": "sha256:87517cb7c7e0c93a83c6f11bfbce85608971d295bc2323b6caa0cc63964d6d64", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:87517cb7c7e0c93a83c6f11bfbce85608971d295bc2323b6caa0cc63964d6d64" +} diff --git a/examples/pf-core-invalid/lean_kernel_checked_with_skipped_build/certificate.json b/examples/pf-core-invalid/lean_kernel_checked_with_skipped_build/certificate.json new file mode 100644 index 0000000..42e7fb0 --- /dev/null +++ b/examples/pf-core-invalid/lean_kernel_checked_with_skipped_build/certificate.json @@ -0,0 +1,39 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreCertificate.v0", + "certificate_id": "pfcore-cert-skipped-build", + "trace_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "LeanKernelChecked", + "checker": "pcs-core", + "checker_version": "0.1.0", + "assumption_refs": [ + "docs/pf-core/assumptions.md" + ], + "theorems_checked": [ + "traceSafeD" + ], + "obligations": [], + "lean_build_status": { + "ok": false, + "target": "PFCore", + "detail": "skipped" + }, + "lean_proof_checked": true, + "lean_environment_hash": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "proof_ref": "lean/PFCore/Generated/example.lean", + "disclaimer": "test fixture", + "event_count": 1, + "contract_semantics_checked": { + "lean": [ + "trace-safe.invariant.require_trace_safe" + ], + "runtime": [] + }, + "default_contract_ref": "trace-safe", + "source_repo": "https://github.com/example/pcs-core", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:7ba0d9f39c69f6daeaaa5ab2e60b7299393f034b3b0b2db80e36291d86546c58", + "proof_term_ref": "lean/PFCore/Generated/example.lean" +} diff --git a/examples/pf-core-invalid/lean_kernel_checked_with_skipped_build/manifest.json b/examples/pf-core-invalid/lean_kernel_checked_with_skipped_build/manifest.json new file mode 100644 index 0000000..87540d9 --- /dev/null +++ b/examples/pf-core-invalid/lean_kernel_checked_with_skipped_build/manifest.json @@ -0,0 +1,6 @@ +{ + "expected_error": "lean_build_status.ok=true", + "must_fail_at": "validate_semantics", + "artifact_file": "certificate.json", + "artifact_type": "PFCoreCertificate.v0" +} diff --git a/examples/pf-core-invalid/lean_kernel_checked_without_contract/manifest.json b/examples/pf-core-invalid/lean_kernel_checked_without_contract/manifest.json new file mode 100644 index 0000000..e70dac4 --- /dev/null +++ b/examples/pf-core-invalid/lean_kernel_checked_without_contract/manifest.json @@ -0,0 +1,6 @@ +{ + "expected_error": "ContractBindingMissing", + "must_fail_at": "validate_semantics", + "artifact_file": "trace.json", + "artifact_type": "PFCoreTrace.v0" +} diff --git a/examples/pf-core-invalid/lean_kernel_checked_without_contract/trace.json b/examples/pf-core-invalid/lean_kernel_checked_without_contract/trace.json new file mode 100644 index 0000000..2c11d47 --- /dev/null +++ b/examples/pf-core-invalid/lean_kernel_checked_without_contract/trace.json @@ -0,0 +1,70 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-file-read-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-file-read-1", + "trace_id": "trace-file-read-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read", + "cap:email-send", + "cap:handoff", + "cap:mcp-invoke" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:4f54951a4b008bdb24f2bb88438cff876fadd84259ad6d83e8211980303a214b", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:4f54951a4b008bdb24f2bb88438cff876fadd84259ad6d83e8211980303a214b" + } + ], + "trace_hash": "sha256:f0ae031e71e7d480967bffa9c1f61736a88c574f4fc08f2a7bda8d4be22e10dc", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "LeanKernelChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:f0ae031e71e7d480967bffa9c1f61736a88c574f4fc08f2a7bda8d4be22e10dc" +} diff --git a/examples/pf-core-invalid/lean_kernel_checked_without_proof_ref/manifest.json b/examples/pf-core-invalid/lean_kernel_checked_without_proof_ref/manifest.json new file mode 100644 index 0000000..7208249 --- /dev/null +++ b/examples/pf-core-invalid/lean_kernel_checked_without_proof_ref/manifest.json @@ -0,0 +1,6 @@ +{ + "expected_error": "ClaimClassOverclaim", + "must_fail_at": "validate_semantics", + "artifact_file": "trace.json", + "artifact_type": "PFCoreTrace.v0" +} diff --git a/examples/pf-core-invalid/lean_kernel_checked_without_proof_ref/trace.json b/examples/pf-core-invalid/lean_kernel_checked_without_proof_ref/trace.json new file mode 100644 index 0000000..fc27bb4 --- /dev/null +++ b/examples/pf-core-invalid/lean_kernel_checked_without_proof_ref/trace.json @@ -0,0 +1,72 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-file-read-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-file-read-1", + "trace_id": "trace-file-read-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read", + "cap:email-send", + "cap:handoff", + "cap:mcp-invoke" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [ + "contract-file-read-v0" + ], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:845ecd9de433a71ccd232200812506ed7b5884bc59e4326bf6be84444186a2ba", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:845ecd9de433a71ccd232200812506ed7b5884bc59e4326bf6be84444186a2ba" + } + ], + "trace_hash": "sha256:1f7ab4ba5c8230a6e0a10ec3a87cce804bcfcb1a0cb9d09a2b7cbd6337c56543", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "LeanKernelChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:1f7ab4ba5c8230a6e0a10ec3a87cce804bcfcb1a0cb9d09a2b7cbd6337c56543" +} diff --git a/examples/pf-core-invalid/lean_kernel_checked_without_proof_term_ref/certificate.json b/examples/pf-core-invalid/lean_kernel_checked_without_proof_term_ref/certificate.json new file mode 100644 index 0000000..621effd --- /dev/null +++ b/examples/pf-core-invalid/lean_kernel_checked_without_proof_term_ref/certificate.json @@ -0,0 +1,38 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreCertificate.v0", + "certificate_id": "pfcore-cert-missing-proof-term", + "trace_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "LeanKernelChecked", + "checker": "pcs-core", + "checker_version": "0.1.0", + "assumption_refs": [ + "docs/pf-core/assumptions.md" + ], + "theorems_checked": [ + "traceSafeD" + ], + "obligations": [], + "lean_build_status": { + "ok": true, + "target": "PFCore", + "detail": "ok" + }, + "lean_proof_checked": true, + "lean_environment_hash": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "proof_ref": "lean/PFCore/Generated/example.lean", + "disclaimer": "test fixture", + "event_count": 1, + "contract_semantics_checked": { + "lean": [ + "trace-safe.invariant.require_trace_safe" + ], + "runtime": [] + }, + "default_contract_ref": "trace-safe", + "source_repo": "https://github.com/example/pcs-core", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:0b7dddae67cc091a82383db93faf26d620e41b28d37e68ec7081a15e6cce91c5" +} diff --git a/examples/pf-core-invalid/lean_kernel_checked_without_proof_term_ref/manifest.json b/examples/pf-core-invalid/lean_kernel_checked_without_proof_term_ref/manifest.json new file mode 100644 index 0000000..990d6df --- /dev/null +++ b/examples/pf-core-invalid/lean_kernel_checked_without_proof_term_ref/manifest.json @@ -0,0 +1,6 @@ +{ + "expected_error": "proof_term_ref", + "must_fail_at": "validate_semantics", + "artifact_file": "certificate.json", + "artifact_type": "PFCoreCertificate.v0" +} diff --git a/examples/pf-core-invalid/missing_principal/observation.json b/examples/pf-core-invalid/missing_principal/observation.json index bdcdb6d..6765f83 100644 --- a/examples/pf-core-invalid/missing_principal/observation.json +++ b/examples/pf-core-invalid/missing_principal/observation.json @@ -46,5 +46,6 @@ "claim_class": "RuntimeChecked", "source_repo": "https://github.com/example/agent-runtime", "source_commit": "abc1234567890abc1234567890abc1234567890", - "signature_or_digest": "sha256:b3e51555d4b30ba60c10a8076457d438b63f9c7fcd6ae7a864fa14d98aa7229f" + "signature_or_digest": "sha256:b3e51555d4b30ba60c10a8076457d438b63f9c7fcd6ae7a864fa14d98aa7229f", + "sequence": 0 } diff --git a/examples/pf-core-invalid/previous_event_hash_mismatch/manifest.json b/examples/pf-core-invalid/previous_event_hash_mismatch/manifest.json new file mode 100644 index 0000000..f31a7e6 --- /dev/null +++ b/examples/pf-core-invalid/previous_event_hash_mismatch/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "EventHashMismatch", + "must_fail_at": "validate_pfcore_trace_hash_chain" +} diff --git a/examples/pf-core-invalid/previous_event_hash_mismatch/trace.json b/examples/pf-core-invalid/previous_event_hash_mismatch/trace.json new file mode 100644 index 0000000..98c385d --- /dev/null +++ b/examples/pf-core-invalid/previous_event_hash_mismatch/trace.json @@ -0,0 +1,70 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-file-read-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-file-read-1", + "trace_id": "trace-file-read-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read", + "cap:email-send", + "cap:handoff", + "cap:mcp-invoke" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [], + "evidence_refs": [], + "previous_event_hash": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "event_hash": "sha256:4f54951a4b008bdb24f2bb88438cff876fadd84259ad6d83e8211980303a214b", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:4f54951a4b008bdb24f2bb88438cff876fadd84259ad6d83e8211980303a214b" + } + ], + "trace_hash": "sha256:47a6b6dde12dd26795643afa0130a379e59e6f8426d9270943e0828e5e5729f2", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:47a6b6dde12dd26795643afa0130a379e59e6f8426d9270943e0828e5e5729f2" +} diff --git a/examples/pf-core-invalid/unknown_capability/observation.json b/examples/pf-core-invalid/unknown_capability/observation.json index cf2c6ef..5579760 100644 --- a/examples/pf-core-invalid/unknown_capability/observation.json +++ b/examples/pf-core-invalid/unknown_capability/observation.json @@ -50,5 +50,6 @@ "claim_class": "RuntimeChecked", "source_repo": "https://github.com/example/agent-runtime", "source_commit": "abc1234567890abc1234567890abc1234567890", - "signature_or_digest": "sha256:f37ea476d8395017e5a5bbafb4e48101cfff180e623aa50c2a85547bd6196ff6" + "signature_or_digest": "sha256:f37ea476d8395017e5a5bbafb4e48101cfff180e623aa50c2a85547bd6196ff6", + "sequence": 0 } diff --git a/examples/pf-core-invalid/unknown_effect/observation.json b/examples/pf-core-invalid/unknown_effect/observation.json index 1343dde..a97e693 100644 --- a/examples/pf-core-invalid/unknown_effect/observation.json +++ b/examples/pf-core-invalid/unknown_effect/observation.json @@ -50,5 +50,6 @@ "claim_class": "RuntimeChecked", "source_repo": "https://github.com/example/agent-runtime", "source_commit": "abc1234567890abc1234567890abc1234567890", - "signature_or_digest": "sha256:d1f2bf7c32639a79e7db14428c6891cae8efbdbac78799b4db3750fdac7c1be6" + "signature_or_digest": "sha256:d1f2bf7c32639a79e7db14428c6891cae8efbdbac78799b4db3750fdac7c1be6", + "sequence": 0 } diff --git a/examples/pf-core-valid/contract_checked/contract.json b/examples/pf-core-valid/contract_checked/contract.json index 22e6c6c..739255f 100644 --- a/examples/pf-core-valid/contract_checked/contract.json +++ b/examples/pf-core-valid/contract_checked/contract.json @@ -15,5 +15,13 @@ "invariant": { "require_trace_safe": true }, - "signature_or_digest": "sha256:ab672cb206542354c10afdbc0e739fe018acee2d4d7db40a7486bcf3071846b3" + "semantics_layer": { + "require_capability": "lean", + "require_effect": "lean", + "require_tenant_match": "lean", + "require_decision": "lean", + "require_event_safe": "lean", + "require_trace_safe": "lean" + }, + "signature_or_digest": "sha256:ade8675a56d86d5a3d2cb576fde97b633e3153f01992005f784322e6c480ca2f" } diff --git a/examples/pf-core-valid/contract_checked/trace.json b/examples/pf-core-valid/contract_checked/trace.json index 4c2c436..1063324 100644 --- a/examples/pf-core-valid/contract_checked/trace.json +++ b/examples/pf-core-valid/contract_checked/trace.json @@ -56,17 +56,17 @@ ], "evidence_refs": [], "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", - "event_hash": "sha256:845ecd9de433a71ccd232200812506ed7b5884bc59e4326bf6be84444186a2ba", "source_repo": "https://github.com/example/agent-runtime", "source_commit": "abc1234567890abc1234567890abc1234567890", + "event_hash": "sha256:845ecd9de433a71ccd232200812506ed7b5884bc59e4326bf6be84444186a2ba", "signature_or_digest": "sha256:845ecd9de433a71ccd232200812506ed7b5884bc59e4326bf6be84444186a2ba" } ], - "trace_hash": "sha256:363ecde1488a23d2a0d128ac46a31d0dce8681e21f6b3ab78ba7a8152324ec26", "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", - "contract_hash": "sha256:ab672cb206542354c10afdbc0e739fe018acee2d4d7db40a7486bcf3071846b3", + "contract_hash": "sha256:ade8675a56d86d5a3d2cb576fde97b633e3153f01992005f784322e6c480ca2f", "claim_class": "RuntimeChecked", "source_repo": "https://github.com/example/agent-runtime", "source_commit": "abc1234567890abc1234567890abc1234567890", - "signature_or_digest": "sha256:363ecde1488a23d2a0d128ac46a31d0dce8681e21f6b3ab78ba7a8152324ec26" + "trace_hash": "sha256:fddb2a2f9ac45d4b7c7a4e8831080b8d9cfe9d3b9b2574871e45ee6616c8318f", + "signature_or_digest": "sha256:fddb2a2f9ac45d4b7c7a4e8831080b8d9cfe9d3b9b2574871e45ee6616c8318f" } diff --git a/examples/pf-core-valid/email_send_authorized/observation.json b/examples/pf-core-valid/email_send_authorized/observation.json index 2e30564..e5256f8 100644 --- a/examples/pf-core-valid/email_send_authorized/observation.json +++ b/examples/pf-core-valid/email_send_authorized/observation.json @@ -50,5 +50,6 @@ "claim_class": "RuntimeChecked", "source_repo": "https://github.com/example/agent-runtime", "source_commit": "abc1234567890abc1234567890abc1234567890", - "signature_or_digest": "sha256:4fc498fe4c9eb01e3b869b1b3421a858f49d4637b2cd8e5f280fcc6225011691" + "signature_or_digest": "sha256:4fc498fe4c9eb01e3b869b1b3421a858f49d4637b2cd8e5f280fcc6225011691", + "sequence": 0 } diff --git a/examples/pf-core-valid/file_read_allowed/observation.json b/examples/pf-core-valid/file_read_allowed/observation.json index 5a9c5c5..bf2cf20 100644 --- a/examples/pf-core-valid/file_read_allowed/observation.json +++ b/examples/pf-core-valid/file_read_allowed/observation.json @@ -50,5 +50,6 @@ "claim_class": "RuntimeChecked", "source_repo": "https://github.com/example/agent-runtime", "source_commit": "abc1234567890abc1234567890abc1234567890", - "signature_or_digest": "sha256:4e1b379c29109b66d62d5f8c2872dfd1f975550063a0637007efb9dff7ce5927" + "signature_or_digest": "sha256:4e1b379c29109b66d62d5f8c2872dfd1f975550063a0637007efb9dff7ce5927", + "sequence": 0 } diff --git a/examples/pf-core-valid/file_read_denied_cross_tenant/observation.json b/examples/pf-core-valid/file_read_denied_cross_tenant/observation.json index f606091..78c2373 100644 --- a/examples/pf-core-valid/file_read_denied_cross_tenant/observation.json +++ b/examples/pf-core-valid/file_read_denied_cross_tenant/observation.json @@ -50,5 +50,6 @@ "claim_class": "RuntimeChecked", "source_repo": "https://github.com/example/agent-runtime", "source_commit": "abc1234567890abc1234567890abc1234567890", - "signature_or_digest": "sha256:544cc9079e1ef0111aea88af31fd5a0e933dcdcd50fb6d1601832da6139cdffd" + "signature_or_digest": "sha256:544cc9079e1ef0111aea88af31fd5a0e933dcdcd50fb6d1601832da6139cdffd", + "sequence": 0 } diff --git a/examples/pf-core-valid/network_denied/observation.json b/examples/pf-core-valid/network_denied/observation.json index 39c1341..49d48ae 100644 --- a/examples/pf-core-valid/network_denied/observation.json +++ b/examples/pf-core-valid/network_denied/observation.json @@ -48,5 +48,6 @@ "claim_class": "RuntimeChecked", "source_repo": "https://github.com/example/agent-runtime", "source_commit": "abc1234567890abc1234567890abc1234567890", - "signature_or_digest": "sha256:2b485a3185e6d101a6a4496b860c77706246c667ba903e9ed4c11d85285ae1b9" + "signature_or_digest": "sha256:2b485a3185e6d101a6a4496b860c77706246c667ba903e9ed4c11d85285ae1b9", + "sequence": 0 } diff --git a/lean/PCS.lean b/lean/PCS.lean index 3183a38..ac50f36 100644 --- a/lean/PCS.lean +++ b/lean/PCS.lean @@ -5,12 +5,7 @@ import PCS.Artifact import PCS.Registry import PCS.Certificate import PCS.Bundle -import PCS.Claim import PCS.ComputationWitness -import PCS.EvidenceBundle -import PCS.Examples +import PCS.ToolUse import PCS.ReleaseChain -import PCS.RuntimeReceipt import PCS.Theorems -import PCS.ToolUse -import PCS.TraceCertificate diff --git a/lean/PCS/Artifact.lean b/lean/PCS/Artifact.lean index 6c1d42d..24256c9 100644 --- a/lean/PCS/Artifact.lean +++ b/lean/PCS/Artifact.lean @@ -1,9 +1,9 @@ +import PCS.Hash + /-! # PCS artifact references -/ -import PCS.Hash - namespace PCS structure ArtifactRef where diff --git a/lean/PCS/Bundle.lean b/lean/PCS/Bundle.lean index 6f0af94..1d13809 100644 --- a/lean/PCS/Bundle.lean +++ b/lean/PCS/Bundle.lean @@ -1,10 +1,10 @@ +import PCS.Hash +import PCS.Status + /-! # PCS bundle verification (Provability Fabric export) -/ -import PCS.Hash -import PCS.Status - namespace PCS structure VerificationResult where diff --git a/lean/PCS/Certificate.lean b/lean/PCS/Certificate.lean index 53defb8..74f15bd 100644 --- a/lean/PCS/Certificate.lean +++ b/lean/PCS/Certificate.lean @@ -1,10 +1,10 @@ +import PCS.Hash +import PCS.Status + /-! # PCS certificate and runtime receipt (trust envelope) -/ -import PCS.Hash -import PCS.Status - namespace PCS structure Certificate where diff --git a/lean/PCS/ComputationWitness.lean b/lean/PCS/ComputationWitness.lean index 3c21ffd..8f60b4e 100644 --- a/lean/PCS/ComputationWitness.lean +++ b/lean/PCS/ComputationWitness.lean @@ -1,9 +1,9 @@ +import PCS.Hash + /-! # ComputationWitness trust-boundary (structural hash alignment only) -/ -import PCS.Hash - namespace PCS structure ComputationWitness where @@ -17,17 +17,17 @@ structure ComputationWitness where /-- Witness result hashes must be drawn from the declared result artifact digest set. -/ def witnessResultHashesAdmissible (witness : ComputationWitness) (artifactHashes : List Hash) : Prop := - ? h ? witness.resultHashes, h ? artifactHashes + ∀ h ∈ witness.resultHashes, h ∈ artifactHashes theorem witness_result_hashes_admissible (witness : ComputationWitness) (artifactHashes : List Hash) (h : witnessResultHashesAdmissible witness artifactHashes) - (resultHash : Hash) (hmem : resultHash ? witness.resultHashes) : - resultHash ? artifactHashes := + (resultHash : Hash) (hmem : resultHash ∈ witness.resultHashes) : + resultHash ∈ artifactHashes := h resultHash hmem /-- ProofChecked computation releases require a CertificateChecked witness status string. -/ def proofCheckedRequiresCertificateCheckedWitness (releaseStatus witnessStatus : String) : Prop := - releaseStatus ? "ProofChecked" ? witnessStatus = "CertificateChecked" + releaseStatus = "ProofChecked" → witnessStatus = "CertificateChecked" end PCS diff --git a/lean/PCS/ReleaseChain.lean b/lean/PCS/ReleaseChain.lean index bc7646c..224a258 100644 --- a/lean/PCS/ReleaseChain.lean +++ b/lean/PCS/ReleaseChain.lean @@ -1,19 +1,19 @@ -/-! -# PCS trust-envelope predicates --/ - import PCS.Bundle import PCS.Certificate import PCS.Hash +/-! +# PCS trust-envelope predicates +-/ + namespace PCS def CertificateMatchesRuntime (cert : Certificate) (receipt : RuntimeReceipt) : Prop := - cert.traceHash = receipt.traceHash Γêº cert.status = ArtifactStatus.CertificateChecked + cert.traceHash = receipt.traceHash ∧ cert.status = ArtifactStatus.CertificateChecked def VerificationAdmitsBundle (verification : VerificationResult) (bundleHash : Hash) : Prop := - verification.status = ArtifactStatus.ProofChecked Γêº - verification.verifiedInputBundleHash = bundleHash Γêº + verification.status = ArtifactStatus.ProofChecked ∧ + verification.verifiedInputBundleHash = bundleHash ∧ verification.releaseBlockingChecksPassed = true def SignedBundleAdmissible (signedInputHash : Hash) (verifiedInputHash : Hash) : Prop := @@ -25,8 +25,8 @@ def ReleaseChainAdmissible (verification : VerificationResult) (bundleHash : Hash) (signedInputHash : Hash) : Prop := - CertificateMatchesRuntime cert receipt Γêº - VerificationAdmitsBundle verification bundleHash Γêº + CertificateMatchesRuntime cert receipt ∧ + VerificationAdmitsBundle verification bundleHash ∧ SignedBundleAdmissible signedInputHash verification.verifiedInputBundleHash /-- Certificate status is CertificateChecked in any admissible release. -/ diff --git a/lean/PCS/Theorems.lean b/lean/PCS/Theorems.lean index 1a6b888..e16ad52 100644 --- a/lean/PCS/Theorems.lean +++ b/lean/PCS/Theorems.lean @@ -1,9 +1,9 @@ +import PCS.ReleaseChain + /-! # PCS trust-envelope theorems (first family) -/ -import PCS.ReleaseChain - namespace PCS theorem admissible_release_has_matching_trace_hash @@ -44,7 +44,7 @@ theorem admissible_release_has_verified_input_hash_equal_to_bundle_hash (signedInputHash : Hash) (h : ReleaseChainAdmissible cert receipt verification bundleHash signedInputHash) : verification.verifiedInputBundleHash = bundleHash := by - exact h.right.left.right + exact h.right.left.right.left theorem admissible_release_has_signed_input_hash_equal_to_verified_input_hash (cert : Certificate) @@ -63,7 +63,7 @@ theorem rejected_certificate_cannot_produce_admissible_release (bundleHash : Hash) (signedInputHash : Hash) (hCert : cert.status = ArtifactStatus.Rejected) : - ┬¼ ReleaseChainAdmissible cert receipt verification bundleHash signedInputHash := by + ¬ ReleaseChainAdmissible cert receipt verification bundleHash signedInputHash := by intro h have hChecked := certificateCheckedInAdmissibleRelease cert receipt verification bundleHash signedInputHash h rw [hCert] at hChecked @@ -76,7 +76,7 @@ theorem stale_certificate_cannot_produce_admissible_release (bundleHash : Hash) (signedInputHash : Hash) (hCert : cert.status = ArtifactStatus.Stale) : - ┬¼ ReleaseChainAdmissible cert receipt verification bundleHash signedInputHash := by + ¬ ReleaseChainAdmissible cert receipt verification bundleHash signedInputHash := by intro h have hChecked := certificateCheckedInAdmissibleRelease cert receipt verification bundleHash signedInputHash h rw [hCert] at hChecked @@ -89,9 +89,9 @@ theorem failed_release_blocking_check_prevents_admissible_release (bundleHash : Hash) (signedInputHash : Hash) (hFailed : verification.releaseBlockingChecksPassed = false) : - ┬¼ ReleaseChainAdmissible cert receipt verification bundleHash signedInputHash := by + ¬ ReleaseChainAdmissible cert receipt verification bundleHash signedInputHash := by intro h - have hAdmits := h.right.left + have hAdmits := h.right.left.right.right rw [hFailed] at hAdmits cases hAdmits diff --git a/lean/PCS/ToolUse.lean b/lean/PCS/ToolUse.lean index 3b69bf1..5e62a67 100644 --- a/lean/PCS/ToolUse.lean +++ b/lean/PCS/ToolUse.lean @@ -1,9 +1,9 @@ +import PCS.Hash + /-! # Tool-use trust-boundary (trace/certificate hash alignment only) -/ -import PCS.Hash - namespace PCS structure ToolUseCertificate where @@ -20,13 +20,13 @@ structure ToolUseTrace where deriving Repr def toolTraceHashMatchesCertificate (trace : ToolUseTrace) (cert : ToolUseCertificate) : Prop := - cert.traceHash = trace.traceHash ? cert.policyHash = trace.policyHash + cert.traceHash = trace.traceHash ∧ cert.policyHash = trace.policyHash def toolTraceHashMatchesCertificateD (trace : ToolUseTrace) (cert : ToolUseCertificate) : Bool := decide (cert.traceHash = trace.traceHash && cert.policyHash = trace.policyHash) theorem tool_trace_hash_matches_certificate (trace : ToolUseTrace) (cert : ToolUseCertificate) : - toolTraceHashMatchesCertificateD trace cert = true ? + toolTraceHashMatchesCertificateD trace cert = true ↔ toolTraceHashMatchesCertificate trace cert := by simp [toolTraceHashMatchesCertificateD, toolTraceHashMatchesCertificate, decide_eq_true_iff] diff --git a/lean/PFCore/Action.lean b/lean/PFCore/Action.lean index df07ec4..fd52960 100644 --- a/lean/PFCore/Action.lean +++ b/lean/PFCore/Action.lean @@ -30,6 +30,13 @@ def ActionWithinTenant (p : Principal) (a : Action) : Prop := def actionWithinTenantD (p : Principal) (a : Action) : Bool := resourcesSameTenantD p a.reads && resourcesSameTenantD p a.writes +/-- +**Meaning:** The tenant decider matches in-tenant resource footprint for reads and writes. + +**Trusted use:** Tenant isolation checks aligned with runtime `validate_resource_scope`. + +**Does not imply:** Cross-tenant denial correctness or network egress safety. +-/ theorem actionWithinTenantD_sound (p : Principal) (a : Action) : actionWithinTenantD p a = true ↔ ActionWithinTenant p a := by simp [actionWithinTenantD, ActionWithinTenant, resourcesSameTenantD_sound, and_left_comm] @@ -41,6 +48,13 @@ def ActionAllowed (p : Principal) (a : Action) : Prop := def actionAllowedD (p : Principal) (a : Action) : Bool := hasCapabilityD p a.capability && actionWithinTenantD p a +/-- +**Meaning:** The combined action decider reflects capability plus tenant-scoped resources. + +**Trusted use:** Core allowance predicate for `EventSafe` and generated concrete proofs. + +**Does not imply:** Effect-level policy, contract postconditions, or external checker claims. +-/ theorem actionAllowedD_sound (p : Principal) (a : Action) : actionAllowedD p a = true ↔ ActionAllowed p a := by simp [actionAllowedD, ActionAllowed, hasCapabilityD_sound, actionWithinTenantD_sound, and_left_comm] diff --git a/lean/PFCore/Capability.lean b/lean/PFCore/Capability.lean index df062b0..2c36236 100644 --- a/lean/PFCore/Capability.lean +++ b/lean/PFCore/Capability.lean @@ -14,7 +14,13 @@ def HasCapability (p : Principal) (cap : String) : Prop := def hasCapabilityD (p : Principal) (cap : String) : Bool := decide (cap ∈ p.capabilities) -/-- `hasCapabilityD` reflects `HasCapability` (soundness). -/ +/-- +**Meaning:** The boolean `hasCapabilityD` decider reflects capability list membership. + +**Trusted use:** Soundness bridge for action allowance and contract preconditions. + +**Does not imply:** Role expansion, runtime grant provenance, or delegated authority validity. +-/ theorem hasCapabilityD_sound (p : Principal) (cap : String) : hasCapabilityD p cap = true ↔ HasCapability p cap := by simp [hasCapabilityD, HasCapability, decide_eq_true_iff] diff --git a/lean/PFCore/Contract.lean b/lean/PFCore/Contract.lean index 58dceb8..6d0bfb8 100644 --- a/lean/PFCore/Contract.lean +++ b/lean/PFCore/Contract.lean @@ -23,13 +23,25 @@ def traceSafeContract : Contract := post := fun _ _ _ => True invariant := traceSafeInvariant } -/-- Trace safety is preserved across `Trace.cons` when the new event is safe. -/ +/-- +**Meaning:** Appending a safe event preserves the canonical `TraceSafe` invariant. + +**Trusted use:** Contract invariant preservation for `require_trace_safe` JSON fields. + +**Does not imply:** Arbitrary user contract invariants are preserved without extra structure. +-/ theorem trace_safe_invariant_preserved_cons (tr : Trace) (ev : Event) : TraceSafe tr → EventSafe ev → TraceSafe (Trace.cons tr ev) := by intro htr hev exact (traceSafe_cons tr ev).mpr ⟨htr, hev⟩ -/-- `traceSafeContract.invariant` is preserved under cons (conservative invariant theorem). -/ +/-- +**Meaning:** The packaged trace-safe contract invariant is preserved under `Trace.cons`. + +**Trusted use:** Mapping JSON `invariant.require_trace_safe` to Lean preservation lemmas. + +**Does not imply:** Custom contract invariants hold without additional proof obligations. +-/ theorem invariant_preserved_cons (tr : Trace) (ev : Event) : traceSafeContract.invariant tr → EventSafe ev → traceSafeContract.invariant (Trace.cons tr ev) := @@ -54,7 +66,13 @@ def Contract.seq (c1 c2 : Contract) : Contract := post := fun p a ev => c1.post p a ev ∧ c2.post p a ev invariant := fun tr => c1.invariant tr ∧ c2.invariant tr } -/-- Sequential contract satisfaction on a single event splits component-wise. -/ +/-- +**Meaning:** Sequential contract satisfaction on an event splits to component contracts. + +**Trusted use:** Compositional contract reasoning in multi-contract traces. + +**Does not imply:** Sequential composition preserves invariants without both sides holding. +-/ theorem seq_contract_satisfaction_left (c1 c2 : Contract) (ev : Event) : SatisfiesContract (Contract.seq c1 c2) ev ↔ SatisfiesContract c1 ev ∧ SatisfiesContract c2 ev := by @@ -72,6 +90,13 @@ theorem seq_contract_satisfaction_left (c1 c2 : Contract) (ev : Event) : · exact Or.inl deny2 · exact Or.inr ⟨post1, post2⟩ +/-- +**Meaning:** Sequential trace-level contract satisfaction splits to component traces. + +**Trusted use:** Trace-wide compositional contract discharge in Lean codegen. + +**Does not imply:** Either component contract alone certifies the composed system. +-/ theorem seq_contract_satisfaction_right (c1 c2 : Contract) (tr : Trace) : TraceSatisfiesContract (Contract.seq c1 c2) tr ↔ TraceSatisfiesContract c1 tr ∧ TraceSatisfiesContract c2 tr := by diff --git a/lean/PFCore/ContractDecide.lean b/lean/PFCore/ContractDecide.lean index 7904d88..0284cc3 100644 --- a/lean/PFCore/ContractDecide.lean +++ b/lean/PFCore/ContractDecide.lean @@ -32,6 +32,13 @@ def actionHasEffect (a : Action) (e : Effect) : Prop := e ∈ a.effects def actionHasEffectD (a : Action) (e : Effect) : Bool := decide (e ∈ a.effects) +/-- +**Meaning:** Effect membership decider reflects `actionHasEffect`. + +**Trusted use:** Contract preconditions requiring specific effects. + +**Does not imply:** Effect execution occurred or was authorized at runtime. +-/ theorem actionHasEffectD_sound (a : Action) (e : Effect) : actionHasEffectD a e = true ↔ actionHasEffect a e := by simp [actionHasEffectD, actionHasEffect, decide_eq_true_iff] @@ -53,8 +60,15 @@ def ContractPreHolds (spec : ContractPreSpec) (p : Principal) (a : Action) : Pro (match spec.requireEffect with | none => True | some eff => actionHasEffect a eff) ∧ - (if spec.requireTenantMatch then ActionWithinTenant p a else True) + (if spec.requireTenantMatch then ActionWithinTenant p a else True) + +/-- +**Meaning:** JSON contract pre decider reflects conservative `ContractPreHolds`. + +**Trusted use:** Generated `concrete_contract_pre_*` proof soundness. +**Does not imply:** Unmapped JSON pre fields (`require_role`, refs) hold. +-/ theorem contractPreD_sound (spec : ContractPreSpec) (p : Principal) (a : Action) : contractPreD spec p a = true ↔ ContractPreHolds spec p a := by rcases spec with ⟨cap, eff, tenant⟩ @@ -76,6 +90,13 @@ def ContractPostHolds (spec : ContractPostSpec) (ev : Event) : Prop := | some d => ev.decision = d) ∧ (if spec.requireEventSafe then EventSafe ev else True) +/-- +**Meaning:** JSON contract post decider reflects conservative `ContractPostHolds`. + +**Trusted use:** Generated `concrete_contract_post_*` proof soundness. + +**Does not imply:** Full PFCoreContract.v0 post semantics beyond mapped fields. +-/ theorem contractPostD_sound (spec : ContractPostSpec) (ev : Event) : contractPostD spec ev = true ↔ ContractPostHolds spec ev := by rcases spec with ⟨reqDec, reqSafe⟩ @@ -91,6 +112,13 @@ def contractInvariantD (spec : ContractInvariantSpec) (tr : Trace) : Bool := def ContractInvariantHolds (spec : ContractInvariantSpec) (tr : Trace) : Prop := if spec.requireTraceSafe then TraceSafe tr else True +/-- +**Meaning:** JSON contract invariant decider reflects `ContractInvariantHolds`. + +**Trusted use:** Generated `concrete_trace_satisfies_*` invariant discharge. + +**Does not imply:** Custom invariants or unmapped contract fields hold. +-/ theorem contractInvariantD_sound (spec : ContractInvariantSpec) (tr : Trace) : contractInvariantD spec tr = true ↔ ContractInvariantHolds spec tr := by rcases spec with ⟨reqSafe⟩ @@ -102,6 +130,13 @@ def satisfiesContractSpecD (pre : ContractPreSpec) (post : ContractPostSpec) (ev def SatisfiesContractSpec (pre : ContractPreSpec) (post : ContractPostSpec) (ev : Event) : Prop := ContractPreHolds pre ev.principal ev.action ∧ ContractPostHolds post ev +/-- +**Meaning:** Per-event contract spec decider reflects `SatisfiesContractSpec`. + +**Trusted use:** Generated `concrete_satisfies_*` theorems. + +**Does not imply:** Trace-wide or sequential contract composition automatically. +-/ theorem satisfiesContractSpecD_sound (pre : ContractPreSpec) (post : ContractPostSpec) (ev : Event) : satisfiesContractSpecD pre post ev = true ↔ SatisfiesContractSpec pre post ev := by simp [satisfiesContractSpecD, SatisfiesContractSpec, contractPreD_sound, contractPostD_sound, @@ -123,6 +158,13 @@ def TraceSatisfiesContractSpecs (pre : ContractPreSpec) (post : ContractPostSpec SatisfiesContractSpec pre post ev ∧ ContractInvariantHolds inv (Trace.cons tr ev) +/-- +**Meaning:** Trace-level contract spec decider reflects `TraceSatisfiesContractSpecs`. + +**Trusted use:** Generated trace-wide contract satisfaction proofs. + +**Does not imply:** Runtime-only contract fields or missing contract JSON are discharged. +-/ theorem traceSatisfiesContractSpecsD_sound (pre : ContractPreSpec) (post : ContractPostSpec) (inv : ContractInvariantSpec) (tr : Trace) : traceSatisfiesContractSpecsD pre post inv tr = true ↔ diff --git a/lean/PFCore/Event.lean b/lean/PFCore/Event.lean index 80cb2c7..dd92a8d 100644 --- a/lean/PFCore/Event.lean +++ b/lean/PFCore/Event.lean @@ -29,6 +29,13 @@ def eventSafeD (ev : Event) : Bool := | Decision.deny => true | Decision.allow => actionAllowedD ev.principal ev.action +/-- +**Meaning:** The boolean `eventSafeD` decider reflects the `EventSafe` predicate. + +**Trusted use:** Soundness link between per-event deciders and Prop-level event safety. + +**Does not imply:** Denied events were safe to deny, or external policy correctness. +-/ theorem eventSafeD_sound (ev : Event) : eventSafeD ev = true ↔ EventSafe ev := by cases ev with diff --git a/lean/PFCore/Handoff.lean b/lean/PFCore/Handoff.lean index e5531c7..2fa45f8 100644 --- a/lean/PFCore/Handoff.lean +++ b/lean/PFCore/Handoff.lean @@ -19,6 +19,13 @@ def CapabilitySubset (xs ys : List String) : Prop := def capabilitySubsetD (xs ys : List String) : Bool := xs.all fun x => decide (x ∈ ys) +/-- +**Meaning:** Subset decider reflects list membership inclusion between capability lists. + +**Trusted use:** Handoff safety checks and authority non-expansion proofs. + +**Does not imply:** Delegation was authorized at runtime or tenants were validated externally. +-/ theorem capabilitySubsetD_sound (xs ys : List String) : capabilitySubsetD xs ys = true ↔ CapabilitySubset xs ys := by simp [capabilitySubsetD, CapabilitySubset, List.all_eq_true, decide_eq_true_iff] @@ -32,11 +39,24 @@ def handoffSafeD (h : Handoff) : Bool := capabilitySubsetD h.delegatedCapabilities h.fromPrincipal.capabilities && decide (h.fromPrincipal.tenant = h.toPrincipal.tenant) +/-- +**Meaning:** Handoff decider reflects subset delegation with matching tenant strings. + +**Trusted use:** Generated handoff proofs and runtime compile-time handoff validation alignment. + +**Does not imply:** Target principal should receive capabilities or temporal policy holds. +-/ theorem handoffSafeD_sound (h : Handoff) : handoffSafeD h = true ↔ HandoffSafe h := by simp [handoffSafeD, HandoffSafe, capabilitySubsetD_sound, decide_eq_true_iff] -/-- Safe handoff never grants a capability absent from the source principal. -/ +/-- +**Meaning:** Safe handoffs never introduce capabilities absent from the source principal. + +**Trusted use:** Authority non-expansion claim in PF-Core handoff certificates. + +**Does not imply:** Delegated capabilities were exercised safely or remain tenant-scoped in runtime. +-/ theorem handoff_does_not_expand_authority (h : Handoff) (cap : String) : HandoffSafe h → cap ∈ h.delegatedCapabilities → HasCapability h.fromPrincipal cap := by intro hsafe hmem diff --git a/lean/PFCore/NonInterference.lean b/lean/PFCore/NonInterference.lean index 93ef999..3b14903 100644 --- a/lean/PFCore/NonInterference.lean +++ b/lean/PFCore/NonInterference.lean @@ -21,6 +21,13 @@ def EventTenantScoped (tenant : String) (ev : Event) : Prop := def eventTenantScopedD (tenant : String) (ev : Event) : Bool := decide (ev.principal.tenant = tenant) && actionWithinTenantD ev.principal ev.action +/-- +**Meaning:** Event tenant-scoping decider matches principal tenant plus in-tenant resources. + +**Trusted use:** Runtime `--tenant-isolation` alignment and conservative isolation claims. + +**Does not imply:** Cross-tenant covert channels are absent or deny events are scoped. +-/ theorem eventTenantScopedD_sound (tenant : String) (ev : Event) : eventTenantScopedD tenant ev = true ↔ EventTenantScoped tenant ev := by cases ev with @@ -38,6 +45,13 @@ def traceTenantScopedD (tenant : String) (tr : Trace) : Bool := | Trace.empty => true | Trace.cons tr' ev => traceTenantScopedD tenant tr' && eventTenantScopedD tenant ev +/-- +**Meaning:** Trace tenant-scoping decider reflects inductive tenant scope over events. + +**Trusted use:** Whole-trace tenant isolation checks in certificates. + +**Does not imply:** Trace hash integrity or completeness of observed runtime events. +-/ theorem traceTenantScopedD_sound (tenant : String) (tr : Trace) : traceTenantScopedD tenant tr = true ↔ TraceTenantScoped tenant tr := by induction tr with @@ -45,14 +59,26 @@ theorem traceTenantScopedD_sound (tenant : String) (tr : Trace) : | cons tr' ev ih => simp [traceTenantScopedD, TraceTenantScoped, eventTenantScopedD_sound, ih, and_left_comm] -/-- Tenant scope is preserved under `Trace.cons` when the new event is scoped. -/ +/-- +**Meaning:** Tenant scope of a trace prefix is preserved when appending a scoped event. + +**Trusted use:** Inductive tenant isolation reasoning under `Trace.cons`. + +**Does not imply:** The appended event was allowed or safe. +-/ theorem cons_preserves_tenant_scope (tenant : String) (tr : Trace) (ev : Event) : TraceTenantScoped tenant tr → EventTenantScoped tenant ev → TraceTenantScoped tenant (Trace.cons tr ev) := by intro htr hev exact ⟨htr, hev⟩ -/-- Allowed safe events are tenant-scoped to the principal's tenant. -/ +/-- +**Meaning:** Allowed safe events place principal and resources within the principal tenant. + +**Trusted use:** Linking `EventSafe` to tenant-scoped non-interference for allow decisions. + +**Does not imply:** Denied events are tenant-scoped or policy-correct. +-/ theorem eventSafe_allow_implies_tenant_scoped (ev : Event) (h : EventSafe ev) (hallow : ev.decision = Decision.allow) : EventTenantScoped ev.principal.tenant ev := by @@ -60,14 +86,26 @@ theorem eventSafe_allow_implies_tenant_scoped (ev : Event) (h : EventSafe ev) rcases hallowed with ⟨_, hwithin⟩ exact ⟨rfl, hwithin⟩ -/-- Allowed events inside a safe trace are tenant-scoped (conservative non-interference link). -/ +/-- +**Meaning:** Allowed events inside a safe trace are tenant-scoped to the principal tenant. + +**Trusted use:** Primary non-interference lemma for certificates over safe traces. + +**Does not imply:** Full information-flow non-interference or cross-principal isolation. +-/ theorem traceSafe_allowed_event_tenant_scoped (tr : Trace) (ev : Event) (hTrace : TraceSafe tr) (hIn : EventIn ev tr) (hallow : ev.decision = Decision.allow) : EventTenantScoped ev.principal.tenant ev := eventSafe_allow_implies_tenant_scoped ev (event_in_safe_trace_is_safe tr ev hTrace hIn) hallow -/-- When a trace is tenant-scoped and safe, every allowed event stays within its principal tenant. -/ +/-- +**Meaning:** Alias lemma: safe traces imply tenant scope for allowed member events. + +**Trusted use:** Documentation-friendly entry point for tenant isolation claims. + +**Does not imply:** Stronger isolation properties beyond `traceSafe_allowed_event_tenant_scoped`. +-/ theorem traceSafe_implies_tenant_scoped_for_allowed (tr : Trace) (ev : Event) (hTrace : TraceSafe tr) (hIn : EventIn ev tr) (hallow : ev.decision = Decision.allow) : EventTenantScoped ev.principal.tenant ev := diff --git a/lean/PFCore/Resource.lean b/lean/PFCore/Resource.lean index cd600f6..139a551 100644 --- a/lean/PFCore/Resource.lean +++ b/lean/PFCore/Resource.lean @@ -21,6 +21,13 @@ def SameTenantResource (p : Principal) (r : Resource) : Prop := def sameTenantResourceD (p : Principal) (r : Resource) : Bool := p.tenant == r.tenant +/-- +**Meaning:** Tenant string equality between principal and resource is decidable and sound. + +**Trusted use:** Building blocks for action tenant checks and non-interference lemmas. + +**Does not imply:** Label-based access control or URI normalization correctness. +-/ theorem sameTenantResourceD_sound (p : Principal) (r : Resource) : sameTenantResourceD p r = true ↔ SameTenantResource p r := by simp [sameTenantResourceD, SameTenantResource, BEq.beq] @@ -32,6 +39,13 @@ def allResourcesSameTenant (p : Principal) (rs : List Resource) : Prop := def resourcesSameTenantD (p : Principal) (rs : List Resource) : Bool := rs.all fun r => sameTenantResourceD p r +/-- +**Meaning:** List tenant decider reflects universal tenant alignment over resources. + +**Trusted use:** Action read/write footprint validation in allowance deciders. + +**Does not imply:** Resource existence, reachability, or side-channel isolation. +-/ theorem resourcesSameTenantD_sound (p : Principal) (rs : List Resource) : resourcesSameTenantD p rs = true ↔ allResourcesSameTenant p rs := by simp [resourcesSameTenantD, allResourcesSameTenant, sameTenantResourceD_sound, List.all_eq_true] diff --git a/lean/PFCore/Theorems.lean b/lean/PFCore/Theorems.lean index 8517d75..070303b 100644 --- a/lean/PFCore/Theorems.lean +++ b/lean/PFCore/Theorems.lean @@ -6,12 +6,27 @@ import PFCore.Soundness namespace PFCore -/-- From event safety, an allowed decision implies the action was allowed. -/ +/-- +**Meaning:** If an event is safe and its decision is `allow`, the principal's action +was allowed under PF-Core capability and tenant rules. + +**Trusted use:** Linking decider-checked event safety to action allowance on allowed +events in certificates and generated concrete proofs. + +**Does not imply:** Denied events were correct, runtime enforcement, or domain-specific +policy satisfaction beyond PF-Core structural rules. +-/ theorem allowed_event_has_allowed_action (ev : Event) (h : EventSafe ev) (hallow : ev.decision = Decision.allow) : ActionAllowed ev.principal ev.action := by exact h hallow -/-- Every event in a safe trace is itself safe. -/ +/-- +**Meaning:** Every event structurally present in a safe trace is itself safe. + +**Trusted use:** Inductive reasoning over trace membership when `TraceSafe` is already established. + +**Does not imply:** The trace is complete, replay-valid, or hash-chain consistent with runtime. +-/ theorem event_in_safe_trace_is_safe (tr : Trace) (ev : Event) (hTrace : TraceSafe tr) (hIn : EventIn ev tr) : EventSafe ev := by induction tr with @@ -26,7 +41,13 @@ theorem event_in_safe_trace_is_safe (tr : Trace) (ev : Event) | inr hIn' => exact ih hTrSafe hIn' -/-- Any allowed event inside a safe trace corresponds to an allowed action. -/ +/-- +**Meaning:** Any allowed event inside a safe trace corresponds to an allowed action. + +**Trusted use:** Prop-level discharge for generated `concrete_allowed_events_allowed` proofs. + +**Does not imply:** All events in the trace were allowed, or that external contracts hold. +-/ theorem every_allowed_event_in_safe_trace_is_allowed (tr : Trace) (ev : Event) (hTrace : TraceSafe tr) (hIn : EventIn ev tr) (hallow : ev.decision = Decision.allow) : ActionAllowed ev.principal ev.action := by diff --git a/lean/PFCore/Trace.lean b/lean/PFCore/Trace.lean index d43df51..af58598 100644 --- a/lean/PFCore/Trace.lean +++ b/lean/PFCore/Trace.lean @@ -30,12 +30,33 @@ def eventInD (ev : Event) (tr : Trace) : Bool := | Trace.empty => false | Trace.cons tr' e => decide (ev == e) || eventInD ev tr' +/-- +**Meaning:** The empty trace is trivially safe. + +**Trusted use:** Base case for trace-safety induction and empty-trace decider proofs. + +**Does not imply:** Any runtime activity occurred or was authorized. +-/ theorem traceSafe_empty : TraceSafe Trace.empty := trivial +/-- +**Meaning:** A cons trace is safe exactly when its prefix is safe and the new head event is safe. + +**Trusted use:** Structural reasoning and `traceSafe_cons` in generated prop-level proofs. + +**Does not imply:** Hash-chain integrity or sequential contract composition beyond PF-Core rules. +-/ theorem traceSafe_cons (tr : Trace) (ev : Event) : TraceSafe (Trace.cons tr ev) ↔ TraceSafe tr ∧ EventSafe ev := by rfl +/-- +**Meaning:** The boolean `traceSafeD` decider reflects the `TraceSafe` predicate. + +**Trusted use:** Lifting decider results (including `decide` proofs) to Prop-level `TraceSafe`. + +**Does not imply:** Decider completeness for artifacts outside the PF-Core JSON mapping. +-/ theorem traceSafeD_sound (tr : Trace) : traceSafeD tr = true ↔ TraceSafe tr := by induction tr with @@ -43,6 +64,13 @@ theorem traceSafeD_sound (tr : Trace) : | cons tr ev ih => simp [traceSafeD, TraceSafe, eventSafeD_sound, ih, and_left_comm] +/-- +**Meaning:** The boolean `eventInD` decider reflects structural `EventIn` membership. + +**Trusted use:** Generated proofs referencing event membership in concrete traces. + +**Does not imply:** Semantic equality of events beyond structural `Event` equality. +-/ theorem eventInD_sound (ev : Event) (tr : Trace) : eventInD ev tr = true ↔ EventIn ev tr := by induction tr with diff --git a/lean/PFCore/TraceCheck.lean b/lean/PFCore/TraceCheck.lean index 08d42f5..b436318 100644 --- a/lean/PFCore/TraceCheck.lean +++ b/lean/PFCore/TraceCheck.lean @@ -14,16 +14,37 @@ namespace PFCore /-- Alias for generated proof scripts referencing trace safety decider. -/ abbrev traceSafeCheck (tr : Trace) : Bool := traceSafeD tr +/-- +**Meaning:** `traceSafeCheck` is definitionally equal to `traceSafeD`. + +**Trusted use:** Stable alias name in generated proof scripts. + +**Does not imply:** Additional safety properties beyond `traceSafeD`. +-/ theorem traceSafeCheck_eq (tr : Trace) : traceSafeCheck tr = traceSafeD tr := rfl /-- Alias for generated per-event proof scripts. -/ abbrev eventSafeCheck (ev : Event) : Bool := eventSafeD ev +/-- +**Meaning:** `eventSafeCheck` is definitionally equal to `eventSafeD`. + +**Trusted use:** Stable alias name in generated per-event proof scripts. + +**Does not imply:** Additional safety properties beyond `eventSafeD`. +-/ theorem eventSafeCheck_eq (ev : Event) : eventSafeCheck ev = eventSafeD ev := rfl /-- Alias for generated handoff proof scripts. -/ abbrev handoffSafeCheck (h : Handoff) : Bool := handoffSafeD h +/-- +**Meaning:** `handoffSafeCheck` is definitionally equal to `handoffSafeD`. + +**Trusted use:** Stable alias name in generated handoff proof scripts. + +**Does not imply:** Additional safety properties beyond `handoffSafeD`. +-/ theorem handoffSafeCheck_eq (h : Handoff) : handoffSafeCheck h = handoffSafeD h := rfl end PFCore diff --git a/python/pcs_core/cli.py b/python/pcs_core/cli.py index f60f9d1..2e594f8 100644 --- a/python/pcs_core/cli.py +++ b/python/pcs_core/cli.py @@ -727,7 +727,7 @@ def main(argv: list[str] | None = None) -> int: p_lean_check = sub.add_parser( "lean-check", - help="Check ProofObligation.v0 against the PCS Lean trust kernel catalog", + help="Deprecated alias for `pcs pcs-envelope check` (release-envelope consistency)", ) p_lean_check.add_argument( "--obligations", @@ -747,6 +747,33 @@ def main(argv: list[str] | None = None) -> int: help="Skip lake build (for tests only)", ) + envelope_parser = sub.add_parser( + "pcs-envelope", + help="PCS release-envelope consistency checks (ProofObligation.v0)", + ) + envelope_sub = envelope_parser.add_subparsers(dest="envelope_cmd", required=True) + p_envelope_check = envelope_sub.add_parser( + "check", + help="Validate ProofObligation.v0 release-envelope consistency", + ) + p_envelope_check.add_argument( + "--obligations", + type=Path, + required=True, + help="ProofObligation.v0 JSON path", + ) + p_envelope_check.add_argument( + "--out", + type=Path, + required=True, + help="Output LeanCheckResult.v0 JSON path", + ) + p_envelope_check.add_argument( + "--skip-lean-build", + action="store_true", + help="Skip lake build (for tests only)", + ) + benchmark_parser = sub.add_parser("benchmark", help="PCS benchmark evaluation protocol") benchmark_sub = benchmark_parser.add_subparsers(dest="benchmark_cmd", required=True) benchmark_sub.add_parser("list", help="List registered benchmark suite ids") @@ -892,7 +919,19 @@ def main(argv: list[str] | None = None) -> int: if args.command == "extract-proof-obligations": return cmd_extract_proof_obligations(args.release, args.out) if args.command == "lean-check": - return cmd_lean_check(args.obligations, args.out, skip_lean_build=args.skip_lean_build) + return cmd_pcs_envelope_check( + args.obligations, + args.out, + skip_lean_build=args.skip_lean_build, + deprecated=True, + ) + if args.command == "pcs-envelope" and args.envelope_cmd == "check": + return cmd_pcs_envelope_check( + args.obligations, + args.out, + skip_lean_build=args.skip_lean_build, + deprecated=False, + ) if args.command == "benchmark" and args.benchmark_cmd == "list": return cmd_benchmark_list() if args.command == "benchmark" and args.benchmark_cmd == "validate": @@ -1074,8 +1113,19 @@ def cmd_extract_proof_obligations(release: Path, out_path: Path) -> int: return 1 -def cmd_lean_check(obligations_path: Path, out_path: Path, *, skip_lean_build: bool = False) -> int: - from pcs_core.lean_trust import run_lean_check +def cmd_pcs_envelope_check( + obligations_path: Path, + out_path: Path, + *, + skip_lean_build: bool = False, + deprecated: bool = False, +) -> int: + from pcs_core.lean_check import PCS_LEAN_CHECK_DEPRECATION + from pcs_core.lean_trust import PCS_LEAN_CHECK_DISCLAIMER, run_lean_check + + if deprecated: + print(PCS_LEAN_CHECK_DEPRECATION, file=sys.stderr) + print(PCS_LEAN_CHECK_DISCLAIMER, file=sys.stderr) try: obligations_doc = _load_json(obligations_path) @@ -1087,12 +1137,21 @@ def cmd_lean_check(obligations_path: Path, out_path: Path, *, skip_lean_build: b out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8") validate_file(out_path) - print(f"OK LeanCheckResult.v0 {out_path} status={result.get('status')}") + print(f"OK PCS release-envelope check {out_path} status={result.get('status')}") return 0 if result.get("status") == "ProofChecked" else 1 except (ValidationError, ValueError) as exc: - print(f"FAIL lean-check: {exc}", file=sys.stderr) + print(f"FAIL pcs-envelope check: {exc}", file=sys.stderr) return 1 +def cmd_lean_check(obligations_path: Path, out_path: Path, *, skip_lean_build: bool = False) -> int: + return cmd_pcs_envelope_check( + obligations_path, + out_path, + skip_lean_build=skip_lean_build, + deprecated=True, + ) + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/python/pcs_core/conformance.py b/python/pcs_core/conformance.py index bd5f2dd..acc5b24 100644 --- a/python/pcs_core/conformance.py +++ b/python/pcs_core/conformance.py @@ -539,6 +539,59 @@ def _suite_status_transition() -> tuple[list[str], list[str], int]: return errors, [], len(cases) +@_record("pf-core") +def _suite_pf_core() -> tuple[list[str], list[str], int]: + from pcs_core.lean_check import audit_pfcore_lean_no_sorry, check_pfcore_trace_lean_semantics + from pcs_core.pf_core_contract import load_contracts_from_dir, validate_trace_contracts + from pcs_core.validate import ( + check_pf_core_invalid_fixtures, + check_pf_core_valid_fixtures, + iter_pf_core_example_dirs, + load_pf_core_fixture_manifest, + validate_file, + ) + + errors: list[str] = [] + checks = 0 + try: + check_pf_core_valid_fixtures() + checks += 1 + except ValidationError as exc: + errors.append(f"pf-core valid fixtures: {exc}") + try: + check_pf_core_invalid_fixtures() + checks += 1 + except ValidationError as exc: + errors.append(f"pf-core invalid fixtures: {exc}") + for case_dir in iter_pf_core_example_dirs("valid"): + manifest = load_pf_core_fixture_manifest(case_dir) + trace_name = str(manifest.get("trace_file") or "trace.json") + trace_path = case_dir / trace_name + if not trace_path.is_file(): + errors.append(f"{case_dir.name}: missing trace file {trace_name}") + continue + checks += 1 + try: + validate_file(trace_path) + data = json.loads(trace_path.read_text(encoding="utf-8")) + for issue in check_pfcore_trace_lean_semantics(data): + errors.append(f"{trace_path.name}: {issue.code}: {issue.message}") + contracts_dir = case_dir / "contracts" + contracts = ( + load_contracts_from_dir(contracts_dir) + if contracts_dir.is_dir() + else load_contracts_from_dir(case_dir) + ) + if contracts: + for issue in validate_trace_contracts(data, contracts): + errors.append(f"{trace_path.name}: {issue.code}: {issue.message}") + except ValidationError as exc: + errors.append(f"{trace_path}: {exc}") + checks += 1 + errors.extend(audit_pfcore_lean_no_sorry()) + return errors, [], checks + + def list_suites() -> list[str]: return sorted(SUITES.keys()) diff --git a/python/pcs_core/lean_catalog.py b/python/pcs_core/lean_catalog.py index 607d1fe..59a4902 100644 --- a/python/pcs_core/lean_catalog.py +++ b/python/pcs_core/lean_catalog.py @@ -67,6 +67,17 @@ PF_CORE_THEOREM_CATALOG = frozenset(PF_CORE_OBLIGATION_KIND_THEOREM.values()) | PF_CORE_SOUNDNESS_THEOREMS +# Concrete proof obligations emitted by pf_core_lean_codegen (LeanKernelChecked only). +PF_CORE_CONCRETE_PROOF_THEOREMS = frozenset( + { + "concrete_trace_safe", + "concrete_trace_safe_prop", + "concrete_allowed_events_allowed", + } +) + +PF_CORE_LEAN_KERNEL_THEOREM_CATALOG = PF_CORE_THEOREM_CATALOG | PF_CORE_CONCRETE_PROOF_THEOREMS + # Backward-compatible PCS aliases (Stage 1). OBLIGATION_KIND_THEOREM = PCS_OBLIGATION_KIND_THEOREM UNTRUSTED_OBLIGATION_KIND_THEOREM = PCS_UNTRUSTED_OBLIGATION_KIND_THEOREM diff --git a/python/pcs_core/lean_check.py b/python/pcs_core/lean_check.py index 64e2869..b94c76b 100644 --- a/python/pcs_core/lean_check.py +++ b/python/pcs_core/lean_check.py @@ -13,14 +13,26 @@ from typing import Any, Mapping from pcs_core.hash import canonical_hash -from pcs_core.lean_catalog import PF_CORE_FORBIDDEN_LEAN_TOKENS, PF_CORE_THEOREM_CATALOG +from pcs_core.lean_catalog import ( + PF_CORE_CONCRETE_PROOF_THEOREMS, + PF_CORE_FORBIDDEN_LEAN_TOKENS, + PF_CORE_LEAN_KERNEL_THEOREM_CATALOG, + PF_CORE_THEOREM_CATALOG, +) from pcs_core.paths import repo_root +from pcs_core.pf_core_contract_semantics import build_contract_semantics_checked from pcs_core.pf_core_lean_codegen import ( + collect_contracts_for_trace, compute_lean_environment_hash, generate_proof_obligation_file, proof_term_ref_from_path, validate_contracts_before_codegen, ) +from pcs_core.pf_core_contract import ( + DEFAULT_TRACE_SAFE_CONTRACT_ID, + default_trace_safe_contract_hash, + trace_has_contract_binding, +) from pcs_core.pf_core_runtime import ( compute_trace_hash, expand_principal_capabilities, @@ -30,12 +42,16 @@ ) from pcs_core.validate import validate_schema +PCS_LEAN_CHECK_DEPRECATION = ( + "Deprecation: `pcs lean-check` is deprecated for PCS release work; use " + "`pcs pcs-envelope check` for PCS release-envelope consistency checks." +) + PCS_LEAN_CHECK_DISCLAIMER = ( - "PCS `lean-check` is not Lean-backed for arbitrary traces. It prints the PF-Core " - "assurance boundary and exits without kernel verification. Use " - "`pcs pf-core lean-check --trace ` for PF-Core trace checking " - "with Python deciders and optional concrete Lean proof (`LeanKernelChecked`). " - "Without that command, no trace receives LeanKernelChecked assurance." + "PCS release-envelope consistency check validates ProofObligation.v0 against " + "the PCS theorem catalog. A `ProofChecked` LeanCheckResult does not imply " + "PF-Core trace safety (`RuntimeChecked` / concrete Lean proof). Use " + "`pcs pf-core lean-check --trace ` for PF-Core kernel assurance." ) LEAN_CHECK_DISCLAIMER = ( @@ -82,8 +98,13 @@ def pfcore_generated_dir() -> Path: return pfcore_lean_dir() / "Generated" -def pfcore_theorems_checked() -> list[str]: - return sorted(PF_CORE_THEOREM_CATALOG) +def pfcore_theorems_checked(*, lean_kernel: bool = False) -> list[str]: + catalog = PF_CORE_LEAN_KERNEL_THEOREM_CATALOG if lean_kernel else PF_CORE_THEOREM_CATALOG + return sorted(catalog) + + +def pfcore_concrete_proof_theorems() -> list[str]: + return sorted(PF_CORE_CONCRETE_PROOF_THEOREMS) def lean_build_status(*, ok: bool, detail: str, target: str = "PFCore") -> dict[str, Any]: @@ -319,6 +340,17 @@ def check_pfcore_trace_lean_semantics(trace: Mapping[str, Any]) -> list[PFCoreLe if events and not trace_safe_d(events): issues.append(PFCoreLeanCheckIssue("TraceUnsafe", "trace fails TraceSafe decider")) + claim_class = str(trace.get("claim_class") or "") + if claim_class == "LeanKernelChecked" and not trace_has_contract_binding(trace): + issues.append( + PFCoreLeanCheckIssue( + "ContractBindingMissing", + "claim_class LeanKernelChecked requires contract_refs on events or " + "default trace-safe contract binding", + "root", + ) + ) + return issues @@ -350,6 +382,23 @@ def build_pfcore_certificate( claim_class = "RuntimeChecked" proof_ref = None + contracts = collect_contracts_for_trace(trace) + contract_semantics = build_contract_semantics_checked(trace, contracts) + + default_contract_ref: str | None = None + if lean_proof_checked: + explicit_default = str(trace.get("default_contract_ref") or "") + if explicit_default == DEFAULT_TRACE_SAFE_CONTRACT_ID: + default_contract_ref = DEFAULT_TRACE_SAFE_CONTRACT_ID + elif str(trace.get("contract_hash") or "") == default_trace_safe_contract_hash(): + default_contract_ref = DEFAULT_TRACE_SAFE_CONTRACT_ID + elif trace_has_contract_binding(trace): + for event in events: + refs = event.get("contract_refs") if isinstance(event, dict) else None + if isinstance(refs, list) and refs: + default_contract_ref = None + break + cert: dict[str, Any] = { "schema_version": "v0", "artifact_type": "PFCoreCertificate.v0", @@ -361,7 +410,7 @@ def build_pfcore_certificate( "checker": checker, "checker_version": checker_version, "assumption_refs": list(PF_CORE_ASSUMPTION_REFS), - "theorems_checked": pfcore_theorems_checked(), + "theorems_checked": pfcore_theorems_checked(lean_kernel=lean_proof_checked), "obligations": obligations, "lean_build_status": lean_build_status( ok=lean_build_ok and not skip_build, @@ -370,12 +419,15 @@ def build_pfcore_certificate( "lean_proof_checked": lean_proof_checked, "disclaimer": LEAN_CHECK_DISCLAIMER, "event_count": len(events), + "contract_semantics_checked": contract_semantics, "source_repo": str(trace.get("source_repo") or "https://github.com/example/pcs-core"), "source_commit": str(trace.get("source_commit") or "0000000"), "signature_or_digest": trace_hash, } if lean_environment_hash: cert["lean_environment_hash"] = lean_environment_hash + if default_contract_ref: + cert["default_contract_ref"] = default_contract_ref if proof_ref: cert["proof_ref"] = proof_ref cert["proof_term_ref"] = proof_ref @@ -416,7 +468,9 @@ def build_lean_check_result( "issues": [{"code": i.code, "message": i.message, "path": i.path} for i in issues], "obligations": obligations, "assumption_refs": list(PF_CORE_ASSUMPTION_REFS), - "theorems_checked": pfcore_theorems_checked(), + "theorems_checked": pfcore_theorems_checked( + lean_kernel=bool(certificate and certificate.get("lean_proof_checked")) + ), "lean_build_status": lean_build_status(ok=build_ok and not skip_build, detail=build_detail), "lean_proof_checked": bool(certificate and certificate.get("lean_proof_checked")), "no_sorry_audit": {"ok": not no_sorry_errors, "errors": no_sorry_errors}, @@ -472,9 +526,26 @@ def run_pfcore_lean_check( "proof_ref": proof_term_ref, } ) + obligations.append( + { + "kind": "ConcreteTraceSafeProp", + "theorem": "concrete_trace_safe_prop", + "passed": False, + "proof_ref": proof_term_ref, + } + ) + obligations.append( + { + "kind": "ConcreteAllowedEventsAllowed", + "theorem": "concrete_allowed_events_allowed", + "passed": False, + "proof_ref": proof_term_ref, + } + ) if not skip_build: proof_ok, proof_detail = run_lean_concrete_proof(proof_path, skip_build=False) - obligations[-1]["passed"] = proof_ok + for entry in obligations[-3:]: + entry["passed"] = proof_ok except OSError as exc: issues.append(PFCoreLeanCheckIssue("LeanCodegenFailed", str(exc))) except ValueError as exc: diff --git a/python/pcs_core/lean_trust.py b/python/pcs_core/lean_trust.py index 21ef672..2014e93 100644 --- a/python/pcs_core/lean_trust.py +++ b/python/pcs_core/lean_trust.py @@ -19,6 +19,13 @@ PCS_CORE_COMMIT_PLACEHOLDER = "d444444444444444444444444444444444444444" +PCS_LEAN_CHECK_DISCLAIMER = ( + "PCS release-envelope consistency check validates ProofObligation.v0 release-envelope " + "consistency against the PCS theorem catalog. A `ProofChecked` LeanCheckResult does " + "not imply PF-Core trace safety. Use " + "`pcs pf-core lean-check --trace ` for PF-Core kernel assurance." +) + def _file_digest(content: bytes) -> str: from pcs_core.release_fixtures import file_digest @@ -403,6 +410,9 @@ def run_lean_check( require_lean_build: bool = True, ) -> dict[str, Any]: """Evaluate obligations against the fixed PCS theorem set; emit LeanCheckResult.v0.""" + import sys + + print(PCS_LEAN_CHECK_DISCLAIMER, file=sys.stderr) build_ok, build_reason = run_lean_build() if require_lean_build else (True, "") obligation_results: list[dict[str, Any]] = [] failures: list[str] = [] @@ -439,6 +449,7 @@ def run_lean_check( body: dict[str, Any] = { "schema_version": "v0", + "artifact_type": "LeanCheckResult.v0", "check_id": check_id or f"lean-check-{proof_obligation_id}", "proof_obligation_id": proof_obligation_id, "lean_module": str(obligations_doc.get("lean_module", LEAN_MODULE)), @@ -450,6 +461,7 @@ def run_lean_check( "source_commit": source_commit or str(obligations_doc.get("source_commit", PCS_CORE_COMMIT_PLACEHOLDER)), "failure_reason": "; ".join(failures), + "disclaimer": PCS_LEAN_CHECK_DISCLAIMER, "obligation_results": obligation_results, "signature_or_digest": PLACEHOLDER_DIGEST, } diff --git a/python/pcs_core/pf_core_certifyedge.py b/python/pcs_core/pf_core_certifyedge.py index d3843ab..67f7550 100644 --- a/python/pcs_core/pf_core_certifyedge.py +++ b/python/pcs_core/pf_core_certifyedge.py @@ -36,6 +36,21 @@ def certifyedge_mock_enabled() -> bool: return os.environ.get("PCS_CERTIFYEDGE_MOCK", "").strip() in {"1", "true", "yes"} +def certifyedge_cli_available() -> bool: + return _find_certifyedge_cli() is not None + + +def certifyedge_status() -> dict[str, object]: + """Report CertifyEdge CLI availability for operators and CI.""" + cli = _find_certifyedge_cli() + return { + "available": cli is not None, + "cli_path": cli, + "mock_enabled": certifyedge_mock_enabled(), + "install_doc": CERTIFYEDGE_INSTALL_DOC, + } + + def _find_certifyedge_cli() -> str | None: return shutil.which("certifyedge") diff --git a/python/pcs_core/pf_core_contract.py b/python/pcs_core/pf_core_contract.py index d7b2d96..f9c7d97 100644 --- a/python/pcs_core/pf_core_contract.py +++ b/python/pcs_core/pf_core_contract.py @@ -4,12 +4,45 @@ import json from dataclasses import dataclass +from functools import lru_cache from pathlib import Path from typing import Any, Mapping +from pcs_core.hash import canonical_hash +from pcs_core.pf_core_contract_semantics import ( + SemanticsLayerIssue, + build_contract_semantics_checked, + default_semantics_layer_for_contract, + field_semantics_layer, + resolve_semantics_layer, + validate_semantics_layer, +) from pcs_core.pf_core_runtime import expand_principal_capabilities from pcs_core.validate import validate_schema +DEFAULT_TRACE_SAFE_CONTRACT_ID = "trace-safe" + +__all__ = [ + "DEFAULT_TRACE_SAFE_CONTRACT_ID", + "ContractIssue", + "SemanticsLayerIssue", + "build_contract_semantics_checked", + "default_semantics_layer_for_contract", + "default_trace_safe_contract", + "default_trace_safe_contract_hash", + "field_semantics_layer", + "load_contract", + "load_contracts", + "load_contracts_from_dir", + "resolve_semantics_layer", + "trace_has_contract_binding", + "validate_event_against_contract", + "validate_pfcore_contract_semantics", + "validate_semantics_layer", + "validate_trace_contract_binding", + "validate_trace_contracts", +] + @dataclass(frozen=True) class ContractIssue: @@ -18,6 +51,78 @@ class ContractIssue: path: str | None = None +def validate_pfcore_contract_semantics(contract: Mapping[str, Any]) -> list[str]: + """Semantic validation for PFCoreContract.v0 beyond JSON Schema.""" + return [ + f"{issue.path or 'root'}: {issue.code}: {issue.message}" + for issue in validate_semantics_layer(contract) + ] + + +def default_trace_safe_contract() -> dict[str, Any]: + """Canonical trace-safe contract aligned with ``PFCore.traceSafeContract`` in Lean.""" + contract: dict[str, Any] = { + "schema_version": "v0", + "artifact_type": "PFCoreContract.v0", + "contract_id": DEFAULT_TRACE_SAFE_CONTRACT_ID, + "name": "Trace-safe default", + "pre": {}, + "post": {}, + "invariant": {"require_trace_safe": True}, + "semantics_layer": {"require_trace_safe": "lean"}, + "signature_or_digest": "sha256:" + "0" * 64, + } + contract["signature_or_digest"] = canonical_hash(contract) + return contract + + +@lru_cache(maxsize=1) +def default_trace_safe_contract_hash() -> str: + return str(default_trace_safe_contract()["signature_or_digest"]) + + +def trace_has_contract_binding(trace: Mapping[str, Any]) -> bool: + """True when the trace binds explicit or default contract refs for LeanKernelChecked.""" + default_ref = str(trace.get("default_contract_ref") or "") + if default_ref == DEFAULT_TRACE_SAFE_CONTRACT_ID: + return True + contract_hash = str(trace.get("contract_hash") or "") + if contract_hash and contract_hash == default_trace_safe_contract_hash(): + return True + events = trace.get("events") + if not isinstance(events, list): + return False + for event in events: + if not isinstance(event, dict): + continue + refs = event.get("contract_refs") + if isinstance(refs, list) and refs: + return True + return False + + +def validate_trace_contract_binding(trace: Mapping[str, Any]) -> list[str]: + """Return errors when LeanKernelChecked traces lack contract grounding.""" + claim_class = str(trace.get("claim_class") or "") + if claim_class != "LeanKernelChecked": + return [] + if trace_has_contract_binding(trace): + return [] + return [ + "ContractBindingMissing: claim_class LeanKernelChecked requires contract_refs on " + f"events or default_contract_ref {DEFAULT_TRACE_SAFE_CONTRACT_ID!r}" + ] + + +def _validate_contract_layers(data: dict[str, Any], path: Path | str) -> None: + layer_issues = validate_semantics_layer(data) + if layer_issues: + raise ValueError( + f"{path}: invalid semantics_layer: " + + "; ".join(f"{issue.code}: {issue.message}" for issue in layer_issues) + ) + + def load_contract(path: Path | str) -> dict[str, Any]: data = json.loads(Path(path).read_text(encoding="utf-8")) if not isinstance(data, dict): @@ -25,6 +130,7 @@ def load_contract(path: Path | str) -> dict[str, Any]: errors = validate_schema(data, "PFCoreContract.v0") if errors: raise ValueError(f"{path}: invalid PFCoreContract.v0: {'; '.join(errors)}") + _validate_contract_layers(data, path) return data @@ -48,6 +154,7 @@ def load_contracts_from_dir(directory: Path) -> dict[str, dict[str, Any]]: errors = validate_schema(data, "PFCoreContract.v0") if errors: raise ValueError(f"{path}: invalid PFCoreContract.v0: {'; '.join(errors)}") + _validate_contract_layers(data, path) contract_id = str(data["contract_id"]) contracts[contract_id] = data return contracts @@ -95,16 +202,24 @@ def validate_event_against_contract( return issues if isinstance(pre, dict): - if pre.get("require_tenant_match") and not _tenant_matches(principal, action): - issues.append( - ContractIssue( - "ContractTenantMismatch", - f"contract {contract.get('contract_id')!r} requires tenant match", - path, + if pre.get("require_tenant_match") and field_semantics_layer( + contract, section="pre", field="require_tenant_match" + ) != "out_of_scope": + if not _tenant_matches(principal, action): + issues.append( + ContractIssue( + "ContractTenantMismatch", + f"contract {contract.get('contract_id')!r} requires tenant match", + path, + ) ) - ) required_cap = pre.get("require_capability") - if isinstance(required_cap, str) and required_cap: + if ( + isinstance(required_cap, str) + and required_cap + and field_semantics_layer(contract, section="pre", field="require_capability") + != "out_of_scope" + ): if not _principal_has_capability(principal, required_cap): issues.append( ContractIssue( @@ -114,7 +229,12 @@ def validate_event_against_contract( ) ) required_effect = pre.get("require_effect") - if isinstance(required_effect, str) and required_effect: + if ( + isinstance(required_effect, str) + and required_effect + and field_semantics_layer(contract, section="pre", field="require_effect") + != "out_of_scope" + ): if not _action_has_effect(action, required_effect): issues.append( ContractIssue( @@ -124,7 +244,12 @@ def validate_event_against_contract( ) ) required_role = pre.get("require_role") - if isinstance(required_role, str) and required_role: + if ( + isinstance(required_role, str) + and required_role + and field_semantics_layer(contract, section="pre", field="require_role") + != "out_of_scope" + ): roles = principal.get("roles") if not isinstance(roles, list) or required_role not in [str(role) for role in roles]: issues.append( @@ -135,7 +260,12 @@ def validate_event_against_contract( ) ) required_policy = pre.get("require_policy_ref") - if isinstance(required_policy, str) and required_policy: + if ( + isinstance(required_policy, str) + and required_policy + and field_semantics_layer(contract, section="pre", field="require_policy_ref") + != "out_of_scope" + ): refs = event.get("contract_refs") if not isinstance(refs, list) or required_policy not in [str(ref) for ref in refs]: issues.append( @@ -146,7 +276,12 @@ def validate_event_against_contract( ) ) required_evidence = pre.get("require_evidence_ref") - if isinstance(required_evidence, str) and required_evidence: + if ( + isinstance(required_evidence, str) + and required_evidence + and field_semantics_layer(contract, section="pre", field="require_evidence_ref") + != "out_of_scope" + ): evidence = event.get("evidence_refs") if not isinstance(evidence, list) or required_evidence not in [ str(ref) for ref in evidence @@ -161,7 +296,12 @@ def validate_event_against_contract( if isinstance(post, dict): required_decision = post.get("require_decision") - if isinstance(required_decision, str) and required_decision: + if ( + isinstance(required_decision, str) + and required_decision + and field_semantics_layer(contract, section="post", field="require_decision") + != "out_of_scope" + ): decision = str(event.get("decision") or "") if decision != required_decision: issues.append( @@ -172,8 +312,10 @@ def validate_event_against_contract( f"{path}.decision", ) ) - if post.get("require_event_safe") is True: - decision = str(event.get("decision") or "") + if post.get("require_event_safe") is True and field_semantics_layer( + contract, section="post", field="require_event_safe" + ) != "out_of_scope": + decision = str(event.get("decision") or "allow") if decision == "allow": cap = action.get("capability") cap_id = str(cap.get("capability_id") or "") if isinstance(cap, dict) else "" @@ -225,7 +367,5 @@ def validate_trace_contracts( ) ) continue - issues.extend( - validate_event_against_contract(event, contract, path=base) - ) + issues.extend(validate_event_against_contract(event, contract, path=base)) return issues diff --git a/python/pcs_core/pf_core_contract_semantics.py b/python/pcs_core/pf_core_contract_semantics.py new file mode 100644 index 0000000..458f1bd --- /dev/null +++ b/python/pcs_core/pf_core_contract_semantics.py @@ -0,0 +1,219 @@ +"""Machine-readable PFCoreContract.v0 field semantics layers (lean | runtime | out_of_scope).""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Mapping + +SEMANTICS_LAYERS = frozenset({"lean", "runtime", "out_of_scope"}) + +# Canonical mapping from docs/pf-core/contract-semantics.md (bare field names). +DEFAULT_FIELD_LAYERS: dict[str, str] = { + "require_capability": "lean", + "require_effect": "lean", + "require_tenant_match": "lean", + "require_role": "runtime", + "require_policy_ref": "runtime", + "require_evidence_ref": "runtime", + "require_decision": "lean", + "require_event_safe": "lean", + "require_trace_safe": "lean", +} + +CONTRACT_SECTION_FIELDS: dict[str, tuple[str, ...]] = { + "pre": ( + "require_capability", + "require_effect", + "require_tenant_match", + "require_role", + "require_policy_ref", + "require_evidence_ref", + ), + "post": ("require_decision", "require_event_safe"), + "invariant": ("require_trace_safe",), +} + + +@dataclass(frozen=True) +class SemanticsLayerIssue: + code: str + message: str + path: str | None = None + + +def _field_active(block: Mapping[str, Any] | None, field: str) -> bool: + if not isinstance(block, dict): + return False + value = block.get(field) + if value is None: + return False + if isinstance(value, bool): + return value + if isinstance(value, str): + return bool(value.strip()) + return True + + +def contract_fields_in_use(contract: Mapping[str, Any]) -> dict[str, str]: + """Return bare field names present with non-empty values, mapped to their section.""" + active: dict[str, str] = {} + for section, fields in CONTRACT_SECTION_FIELDS.items(): + block = contract.get(section) + for field in fields: + if _field_active(block if isinstance(block, dict) else None, field): + active[field] = section + return active + + +def default_semantics_layer_for_contract(contract: Mapping[str, Any]) -> dict[str, str]: + """Derive semantics_layer entries for fields referenced by pre/post/invariant.""" + return { + field: DEFAULT_FIELD_LAYERS.get(field, "runtime") + for field in contract_fields_in_use(contract) + } + + +def resolve_semantics_layer(contract: Mapping[str, Any]) -> dict[str, str]: + """Effective semantics_layer map (explicit entries merged with defaults for active fields).""" + explicit = contract.get("semantics_layer") + resolved = default_semantics_layer_for_contract(contract) + if isinstance(explicit, dict): + for key, value in explicit.items(): + if isinstance(key, str) and isinstance(value, str): + resolved[key] = value + return resolved + + +def validate_semantics_layer(contract: Mapping[str, Any]) -> list[SemanticsLayerIssue]: + """Validate semantics_layer consistency with contract body and canonical defaults.""" + issues: list[SemanticsLayerIssue] = [] + explicit = contract.get("semantics_layer") + active = contract_fields_in_use(contract) + + if explicit is not None and not isinstance(explicit, dict): + issues.append( + SemanticsLayerIssue( + "SemanticsLayerInvalid", + "semantics_layer must be an object when present", + "semantics_layer", + ) + ) + return issues + + if isinstance(explicit, dict): + for key, value in explicit.items(): + if not isinstance(key, str) or not isinstance(value, str): + issues.append( + SemanticsLayerIssue( + "SemanticsLayerInvalid", + "semantics_layer entries must be string keys and string values", + f"semantics_layer.{key}", + ) + ) + continue + if value not in SEMANTICS_LAYERS: + issues.append( + SemanticsLayerIssue( + "SemanticsLayerInvalid", + f"semantics_layer[{key!r}] must be one of lean, runtime, out_of_scope", + f"semantics_layer.{key}", + ) + ) + if key not in DEFAULT_FIELD_LAYERS: + issues.append( + SemanticsLayerIssue( + "SemanticsLayerUnknownField", + f"unknown semantics_layer field {key!r}", + f"semantics_layer.{key}", + ) + ) + elif key in active and value != DEFAULT_FIELD_LAYERS[key]: + issues.append( + SemanticsLayerIssue( + "SemanticsLayerMismatch", + f"semantics_layer[{key!r}]={value!r} conflicts with canonical layer " + f"{DEFAULT_FIELD_LAYERS[key]!r}", + f"semantics_layer.{key}", + ) + ) + elif key not in active: + issues.append( + SemanticsLayerIssue( + "SemanticsLayerOrphanField", + f"semantics_layer[{key!r}] set but {key} is absent from pre/post/invariant", + f"semantics_layer.{key}", + ) + ) + + for field in active: + layer = resolve_semantics_layer(contract).get(field, "runtime") + if layer == "out_of_scope": + issues.append( + SemanticsLayerIssue( + "SemanticsLayerOutOfScopeFieldSet", + f"contract sets {field} but semantics_layer marks it out_of_scope", + field, + ) + ) + + return issues + + +def field_semantics_layer(contract: Mapping[str, Any], *, section: str, field: str) -> str: + """Return discharge layer for a contract field (section is used for documentation only).""" + _ = section + return resolve_semantics_layer(contract).get(field, DEFAULT_FIELD_LAYERS.get(field, "runtime")) + + +def _trace_events(trace: Mapping[str, Any]) -> list[dict[str, Any]]: + events = trace.get("events") + if not isinstance(events, list): + return [] + typed = [event for event in events if isinstance(event, dict)] + return sorted(typed, key=lambda item: int(item.get("sequence") or 0)) + + +def _trace_has_contract_refs(trace: Mapping[str, Any]) -> bool: + for event in _trace_events(trace): + refs = event.get("contract_refs") + if isinstance(refs, list) and refs: + return True + return False + + +def build_contract_semantics_checked( + trace: Mapping[str, Any], + contracts: Mapping[str, Mapping[str, Any]], +) -> dict[str, list[str]]: + """Summarize contract fields checked in Lean vs runtime from semantics_layer.""" + lean_checks: list[str] = [] + runtime_checks: list[str] = [] + if not _trace_has_contract_refs(trace): + return {"lean": lean_checks, "runtime": runtime_checks} + + seen: set[str] = set() + for event in _trace_events(trace): + refs = event.get("contract_refs") + if not isinstance(refs, list): + continue + for ref in refs: + contract_id = str(ref) + contract = contracts.get(contract_id) + if contract is None: + runtime_checks.append(f"missing_contract:{contract_id}") + continue + for field, section in contract_fields_in_use(contract).items(): + cert_key = f"{contract_id}.{section}.{field}" + if cert_key in seen: + continue + seen.add(cert_key) + layer = field_semantics_layer(contract, section=section, field=field) + if layer == "lean": + lean_checks.append(cert_key) + elif layer == "runtime": + runtime_checks.append(cert_key) + + return { + "lean": sorted(set(lean_checks)), + "runtime": sorted(set(runtime_checks)), + } diff --git a/python/pcs_core/pf_core_hash_vector_parity.py b/python/pcs_core/pf_core_hash_vector_parity.py index 6991408..5f0774e 100644 --- a/python/pcs_core/pf_core_hash_vector_parity.py +++ b/python/pcs_core/pf_core_hash_vector_parity.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json import os import shutil import subprocess @@ -80,6 +81,32 @@ def compare_hash_vector_trees(local: Path, upstream: Path) -> list[str]: return errors +def verify_local_pf_core_hash_vectors(local: Path | None = None) -> list[str]: + """Verify PF-Core event/trace vectors under python/tests/hash_vectors/pf_core/.""" + from pcs_core.pf_core_runtime import compute_event_hash, compute_trace_hash + + errors: list[str] = [] + root = hash_vectors_dir(local) / "pf_core" + if not root.is_dir(): + return ["missing local PF-Core hash vectors at python/tests/hash_vectors/pf_core/"] + for name, computer in ( + ("PFCoreEvent.v0", compute_event_hash), + ("PFCoreTrace.v0", compute_trace_hash), + ): + vector_dir = root / name + input_path = vector_dir / "input.json" + digest_path = vector_dir / "digest.txt" + if not input_path.is_file() or not digest_path.is_file(): + errors.append(f"missing local PF-Core vector files for {name}") + continue + payload = json.loads(input_path.read_text(encoding="utf-8")) + expected = digest_path.read_text(encoding="utf-8").strip() + actual = computer(payload) + if actual != expected: + errors.append(f"PF-Core hash vector drift: {name} (expected {expected}, got {actual})") + return errors + + def verify_pf_core_hash_vectors( local: Path | None = None, *, @@ -95,6 +122,9 @@ def verify_pf_core_hash_vectors( directory (or ``work_dir`` when provided). """ local_root = hash_vectors_dir(local) + errors = verify_local_pf_core_hash_vectors(local_root) + if errors: + return errors tag = pf_core_tag or os.environ.get("PF_CORE_TAG", DEFAULT_PF_CORE_TAG) repo = pf_core_repo or os.environ.get("PF_CORE_REPO", DEFAULT_PF_CORE_REPO) diff --git a/python/pcs_core/pf_core_lean_codegen.py b/python/pcs_core/pf_core_lean_codegen.py index 46c50ef..0ea557e 100644 --- a/python/pcs_core/pf_core_lean_codegen.py +++ b/python/pcs_core/pf_core_lean_codegen.py @@ -10,7 +10,11 @@ from pcs_core.hash import canonical_hash from pcs_core.paths import repo_root -from pcs_core.pf_core_contract import load_contracts_from_dir, validate_trace_contracts +from pcs_core.pf_core_contract import ( + field_semantics_layer, + load_contracts_from_dir, + validate_trace_contracts, +) EFFECT_KIND_TO_LEAN: dict[str, str] = { "file.read": "Effect.read", @@ -174,12 +178,23 @@ def contract_pre_to_lean(contract: Mapping[str, Any], *, name: str) -> str: tenant_expr = "false" if isinstance(pre, dict): cap = pre.get("require_capability") - if isinstance(cap, str) and cap: + if ( + isinstance(cap, str) + and cap + and field_semantics_layer(contract, section="pre", field="require_capability") == "lean" + ): cap_expr = f"some {lean_string_literal(cap)}" effect = pre.get("require_effect") - if isinstance(effect, str) and effect: + if ( + isinstance(effect, str) + and effect + and field_semantics_layer(contract, section="pre", field="require_effect") == "lean" + ): effect_expr = f"some {effect_kind_to_lean(effect)}" - if pre.get("require_tenant_match") is True: + if ( + pre.get("require_tenant_match") is True + and field_semantics_layer(contract, section="pre", field="require_tenant_match") == "lean" + ): tenant_expr = "true" return ( f"def {name} : ContractPreSpec :=\n" @@ -197,9 +212,16 @@ def contract_post_to_lean(contract: Mapping[str, Any], *, name: str) -> str: safe_expr = "false" if isinstance(post, dict): decision = post.get("require_decision") - if isinstance(decision, str) and decision: + if ( + isinstance(decision, str) + and decision + and field_semantics_layer(contract, section="post", field="require_decision") == "lean" + ): decision_expr = f"some {decision_to_lean(decision)}" - if post.get("require_event_safe") is True: + if ( + post.get("require_event_safe") is True + and field_semantics_layer(contract, section="post", field="require_event_safe") == "lean" + ): safe_expr = "true" return ( f"def {name} : ContractPostSpec :=\n" @@ -213,7 +235,11 @@ def contract_post_to_lean(contract: Mapping[str, Any], *, name: str) -> str: def contract_invariant_to_lean(contract: Mapping[str, Any], *, name: str) -> str: invariant = contract.get("invariant") safe_expr = "false" - if isinstance(invariant, dict) and invariant.get("require_trace_safe") is True: + if ( + isinstance(invariant, dict) + and invariant.get("require_trace_safe") is True + and field_semantics_layer(contract, section="invariant", field="require_trace_safe") == "lean" + ): safe_expr = "true" return ( f"def {name} : ContractInvariantSpec :=\n" @@ -270,20 +296,61 @@ def generate_contract_proof_obligations( f"satisfiesContractSpecD {base_name}Pre {base_name}Post {event_name} = true := by\n" " decide" ) - theorems.append( - f"theorem concrete_contract_pre_{base_name}_{event_name} : " - f"contractPreD {base_name}Pre {event_name}Principal {event_name}Action = true := by\n" - " decide" - ) - theorems.append( - f"theorem concrete_contract_post_{base_name}_{event_name} : " - f"contractPostD {base_name}Post {event_name} = true := by\n" - " decide" - ) + if _contract_has_lean_pre_fields(contract): + theorems.append( + f"theorem concrete_contract_pre_{base_name}_{event_name} : " + f"contractPreD {base_name}Pre {event_name}Principal {event_name}Action = true := by\n" + " decide" + ) + if _contract_has_lean_post_fields(contract): + theorems.append( + f"theorem concrete_contract_post_{base_name}_{event_name} : " + f"contractPostD {base_name}Post {event_name} = true := by\n" + " decide" + ) return defs, theorems +def _contract_has_lean_pre_fields(contract: Mapping[str, Any]) -> bool: + pre = contract.get("pre") + if not isinstance(pre, dict): + return False + if ( + isinstance(pre.get("require_capability"), str) + and pre.get("require_capability") + and field_semantics_layer(contract, section="pre", field="require_capability") == "lean" + ): + return True + if ( + isinstance(pre.get("require_effect"), str) + and pre.get("require_effect") + and field_semantics_layer(contract, section="pre", field="require_effect") == "lean" + ): + return True + if ( + pre.get("require_tenant_match") is True + and field_semantics_layer(contract, section="pre", field="require_tenant_match") == "lean" + ): + return True + return False + + +def _contract_has_lean_post_fields(contract: Mapping[str, Any]) -> bool: + post = contract.get("post") + if not isinstance(post, dict): + return False + if post.get("require_decision") and field_semantics_layer( + contract, section="post", field="require_decision" + ) == "lean": + return True + if post.get("require_event_safe") is True and field_semantics_layer( + contract, section="post", field="require_event_safe" + ) == "lean": + return True + return False + + def trace_has_contract_refs(trace: Mapping[str, Any]) -> bool: for event in trace_events(trace): refs = event.get("contract_refs") @@ -453,7 +520,8 @@ def generate_proof_obligation_file( "run `pcs pf-core validate-contracts` before lean-check.\n" ) - source = f"""import PFCore.TraceCheck + source = f"""import PFCore.Theorems +import PFCore.TraceCheck /-! # Generated concrete trace proof for `{trace_id}` @@ -469,6 +537,15 @@ def generate_proof_obligation_file( {contract_def_block}{handoff_block}theorem concrete_trace_safe : traceSafeD {trace_var} = true := by decide +theorem concrete_trace_safe_prop : TraceSafe {trace_var} := + (traceSafeD_sound {trace_var}).mp concrete_trace_safe + +theorem concrete_allowed_events_allowed : + ∀ ev, EventIn ev {trace_var} → ev.decision = Decision.allow → + ActionAllowed ev.principal ev.action := + fun ev hIn hAllow => + every_allowed_event_in_safe_trace_is_allowed {trace_var} ev concrete_trace_safe_prop hIn hAllow + {event_theorem_block}{contract_theorem_block}end PFCore.Generated.{module} """ out_path.write_text(source, encoding="utf-8") diff --git a/python/pcs_core/pf_core_runtime.py b/python/pcs_core/pf_core_runtime.py index 265f089..ed53b92 100644 --- a/python/pcs_core/pf_core_runtime.py +++ b/python/pcs_core/pf_core_runtime.py @@ -436,6 +436,48 @@ def validate_tenant_isolation(trace: Mapping[str, Any]) -> list[str]: return errors +def validate_denied_observations_preserved( + observations: list[Any] | list[Mapping[str, Any]], + events: list[Mapping[str, Any]], +) -> None: + """Ensure denied runtime observations appear as deny events in a compiled trace.""" + compiled: dict[str, Mapping[str, Any]] = {} + for event in events: + if isinstance(event, dict): + compiled[str(event.get("event_id") or "")] = event + for index, observation in enumerate(observations): + if not isinstance(observation, dict): + continue + if str(observation.get("decision") or "") != "deny": + continue + event_id = str(observation.get("event_id") or "") + if not event_id: + raise DroppedDeniedEvent("", f"observations[{index}].event_id") + event = compiled.get(event_id) + if event is None: + raise DroppedDeniedEvent(event_id, "events") + if str(event.get("decision") or "") != "deny": + raise DroppedDeniedEvent(event_id, f"events[{event_id}].decision") + + +def _observation_sequence(observation: Mapping[str, Any]) -> int: + sequence = observation.get("sequence") + if isinstance(sequence, int) and sequence >= 0: + return sequence + raise PFCoreRuntimeError( + "InvalidObservation", + "sequence must be a non-negative integer", + "sequence", + ) + + +def _sort_observations(observations: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Return observations ordered by ``sequence`` with stable index tie-breaking.""" + indexed = list(enumerate(observations)) + indexed.sort(key=lambda item: (_observation_sequence(item[1]), item[0])) + return [observation for _, observation in indexed] + + def compile_runtime_observation_to_event(observation: dict) -> dict: """Compile a schema-valid runtime observation into a PFCoreEvent.v0.""" _require_schema_valid(observation, "PFCoreRuntimeObservation.v0") @@ -451,16 +493,19 @@ def compile_runtime_observation_to_event(observation: dict) -> dict: elif not _same_tenant(principal, action): decision = "deny" + policy_ref = str(observation.get("policy_ref") or "").strip() + contract_refs = [policy_ref] if policy_ref else [] + event = _finalize_event( trace_id=str(observation["trace_id"]), event_id=str(observation["event_id"]), - sequence=0, + sequence=_observation_sequence(observation), timestamp=str(observation["observed_at"]), principal=principal, action=action, decision=decision, decision_reason=str(observation.get("decision_reason") or ""), - contract_refs=[], + contract_refs=contract_refs, evidence_refs=[str(ref) for ref in observation.get("evidence_refs", []) if ref], previous_event_hash=str(observation.get("previous_event_hash") or GENESIS_HASH), source_repo=str(observation["source_repo"]), @@ -469,6 +514,63 @@ def compile_runtime_observation_to_event(observation: dict) -> dict: return event +def compile_runtime_observations_to_pfcore_trace( + observations: list[dict[str, Any]], + *, + workflow_id: str | None = None, +) -> dict[str, Any]: + """Compile ordered runtime observations into a PFCoreTrace.v0 with chained hashes.""" + if not observations: + raise PFCoreRuntimeError("InvalidTrace", "observations must be non-empty", "observations") + + for index, observation in enumerate(observations): + _require_schema_valid(observation, "PFCoreRuntimeObservation.v0") + + ordered = _sort_observations(observations) + trace_id = str(ordered[0]["trace_id"]) + for observation in ordered[1:]: + if str(observation["trace_id"]) != trace_id: + raise PFCoreRuntimeError( + "InvalidTrace", + "all observations must share trace_id", + "trace_id", + ) + + events: list[dict[str, Any]] = [] + previous_hash = GENESIS_HASH + for observation in ordered: + event = compile_runtime_observation_to_event(observation) + event = dict(event) + event["previous_event_hash"] = previous_hash + event["event_hash"] = compute_event_hash(event) + event["signature_or_digest"] = event["event_hash"] + events.append(event) + previous_hash = event["event_hash"] + + validate_denied_observations_preserved(ordered, events) + + claim_class = "RuntimeChecked" + _assert_claim_class_allowed(claim_class) + + trace: dict[str, Any] = { + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": trace_id, + "workflow_id": workflow_id or str(ordered[0].get("runtime_ref") or "observation.batch"), + "events": events, + "trace_hash": GENESIS_HASH, + "policy_hash": GENESIS_HASH, + "contract_hash": GENESIS_HASH, + "claim_class": claim_class, + "source_repo": str(ordered[0]["source_repo"]), + "source_commit": str(ordered[0]["source_commit"]), + "signature_or_digest": GENESIS_HASH, + } + trace["trace_hash"] = compute_trace_hash(trace) + trace["signature_or_digest"] = trace["trace_hash"] + return trace + + def _resolve_tool_mapping(tool_name: str, tool_category: str) -> tuple[str, str, str]: key = (tool_name, tool_category) if key not in TOOL_NAME_MAP: diff --git a/python/pcs_core/validate.py b/python/pcs_core/validate.py index f637dad..c77b9f1 100644 --- a/python/pcs_core/validate.py +++ b/python/pcs_core/validate.py @@ -1,1277 +1,6 @@ """JSON Schema and semantic validation for PCS artifacts.""" -from __future__ import annotations - -import json -import re -from pathlib import Path -from typing import Any - -from jsonschema import Draft202012Validator -from referencing import Registry, Resource -from referencing.jsonschema import DRAFT202012 - -from pcs_core.paths import examples_dir as default_examples_dir -from pcs_core.paths import repo_root, schemas_dir -from pcs_core.registry_data import PF_CORE_CLAIM_CLASSES -from pcs_core.status import ARTIFACT_STATUSES, TRACE_CERTIFICATE_STATUSES - -from pcs_core.lean_validate import ( - validate_lean_check_result_semantics, - validate_proof_obligation_semantics, -) -from pcs_core.protocol_validate import ( - validate_artifact_registry_semantics, - validate_conformance_report_semantics, - validate_handoff_manifest_semantics, - validate_release_chain_validation_result_semantics, - validate_release_manifest_fixture_refs, - validate_release_manifest_semantics, -) -from pcs_core.tool_use_validate import ( - validate_tool_use_certificate_semantics, - validate_tool_use_trace_semantics, - validate_workflow_profile_semantics, -) -ARTIFACT_SCHEMAS: dict[str, str] = { - "AssumptionSet.v0": "AssumptionSet.v0.schema.json", - "SourceSpan.v0": "SourceSpan.v0.schema.json", - "ClaimArtifact.v0": "ClaimArtifact.v0.schema.json", - "RuntimeReceipt.v0": "RuntimeReceipt.v0.schema.json", - "TraceCertificate.v0": "TraceCertificate.v0.schema.json", - "EvidenceBundle.v0": "EvidenceBundle.v0.schema.json", - "ScienceClaimBundle.v0": "ScienceClaimBundle.v0.schema.json", - "VerificationResult.v0": "VerificationResult.v0.schema.json", - "SignedScienceClaimBundle.v0": "SignedScienceClaimBundle.v0.schema.json", - "ReleaseManifest.v0": "ReleaseManifest.v0.schema.json", - "HandoffManifest.v0": "HandoffManifest.v0.schema.json", - "ReleaseChainValidationResult.v0": "ReleaseChainValidationResult.v0.schema.json", - "ArtifactRegistry.v0": "ArtifactRegistry.v0.schema.json", - "MigrationReport.v0": "MigrationReport.v0.schema.json", - "ComponentReleaseFragment.v0": "ComponentReleaseFragment.v0.schema.json", - "SemanticCheckExecution.v0": "SemanticCheckExecution.v0.schema.json", - "ConformanceReport.v0": "ConformanceReport.v0.schema.json", - "WorkflowProfile.v0": "WorkflowProfile.v0.schema.json", - "ToolUseTrace.v0": "ToolUseTrace.v0.schema.json", - "ToolUseCertificate.v0": "ToolUseCertificate.v0.schema.json", - "DatasetReceipt.v0": "DatasetReceipt.v0.schema.json", - "EnvironmentReceipt.v0": "EnvironmentReceipt.v0.schema.json", - "ComputationRunReceipt.v0": "ComputationRunReceipt.v0.schema.json", - "ResultArtifact.v0": "ResultArtifact.v0.schema.json", - "ComputationWitness.v0": "ComputationWitness.v0.schema.json", - "ProofObligation.v0": "ProofObligation.v0.schema.json", - "LeanCheckResult.v0": "LeanCheckResult.v0.schema.json", - "BenchmarkTask.v0": "BenchmarkTask.v0.schema.json", - "BenchmarkCase.v0": "BenchmarkCase.v0.schema.json", - "BenchmarkRun.v0": "BenchmarkRun.v0.schema.json", - "BenchmarkReport.v0": "BenchmarkReport.v0.schema.json", - "BenchmarkRegistry.v0": "BenchmarkRegistry.v0.schema.json", - "BenchmarkSuiteManifest.v0": "BenchmarkSuiteManifest.v0.schema.json", - "ConformanceRun.v0": "ConformanceRun.v0.schema.json", - "FailureCaseManifest.v0": "FailureCaseManifest.v0.schema.json", - "FailureLocalizationResult.v0": "FailureLocalizationResult.v0.schema.json", - "CoverageReport.v0": "CoverageReport.v0.schema.json", - "ExplainQualityReport.v0": "ExplainQualityReport.v0.schema.json", - "ProfileCoverageReport.v0": "ProfileCoverageReport.v0.schema.json", - "BenchmarkMetricRegistry.v0": "BenchmarkMetricRegistry.v0.schema.json", - "MetricSummary.v0": "MetricSummary.v0.schema.json", - "PcsBenchIngest.v0": "PcsBenchIngest.v0.schema.json", - "BenchmarkArtifactRef.v0": "BenchmarkArtifactRef.v0.schema.json", - "PFCorePrincipal.v0": "PFCorePrincipal.v0.schema.json", - "PFCoreCapability.v0": "PFCoreCapability.v0.schema.json", - "PFCoreResource.v0": "PFCoreResource.v0.schema.json", - "PFCoreAction.v0": "PFCoreAction.v0.schema.json", - "PFCoreEffect.v0": "PFCoreEffect.v0.schema.json", - "PFCoreDecision.v0": "PFCoreDecision.v0.schema.json", - "PFCoreEvent.v0": "PFCoreEvent.v0.schema.json", - "PFCoreTrace.v0": "PFCoreTrace.v0.schema.json", - "PFCoreContract.v0": "PFCoreContract.v0.schema.json", - "PFCoreHandoff.v0": "PFCoreHandoff.v0.schema.json", - "PFCoreRuntimeObservation.v0": "PFCoreRuntimeObservation.v0.schema.json", - "PFCoreCertificate.v0": "PFCoreCertificate.v0.schema.json", - "PCSBridgeCertificate.v0": "PCSBridgeCertificate.v0.schema.json", -} - -CERTIFIED_CLAIM_STATUSES = frozenset( - { - "CertificateChecked", - "ProofChecked", - "RuntimeChecked", - } -) - -IMPORT_READY_VERIFICATION_STATUSES = frozenset( - { - "ProofChecked", - "CertificateChecked", - "RuntimeChecked", - } -) - -_ZERO_COMMIT_RE = re.compile(r"^0+$") - - -class ValidationError(Exception): - """Raised when artifact validation fails.""" - - def __init__(self, message: str, errors: list[str] | None = None): - super().__init__(message) - self.errors = errors or [] - - -_PF_CORE_ARTIFACT_TYPES = frozenset( - key for key in ARTIFACT_SCHEMAS if key.startswith("PFCore") or key == "ToolUseTrace.v0" -) - -CERTIFIED_CLAIM_STATUSES = frozenset( - { - "CertificateChecked", - "ProofChecked", - "RuntimeChecked", - } -) - -IMPORT_READY_VERIFICATION_STATUSES = frozenset( - { - "ProofChecked", - "CertificateChecked", - "RuntimeChecked", - } -) - -LEAN_CHECK_RESULT_STATUSES = frozenset( - { - "DecidersPassed", - "LeanProofChecked", - "ReplayValidated", - "Rejected", - "Stale", - } -) - -_ZERO_COMMIT_RE = re.compile(r"^0+$") - - -class ValidationError(Exception): - """Raised when artifact validation fails.""" - - def __init__(self, message: str, errors: list[str] | None = None): - super().__init__(message) - self.errors = errors or [] - - -def _resolve_schema_ref(schema: dict[str, Any], ref: str) -> dict[str, Any]: - if ref.startswith("pf_core.defs.json#/$defs/"): - defs_path = schemas_dir() / "pf_core.defs.json" - defs_schema = _load_schema(defs_path) - def_name = ref.split("/")[-1] - target = defs_schema.get("$defs", {}).get(def_name) - if isinstance(target, dict): - return target - return {} - - -def _schema_requires_artifact_type(artifact_type: str) -> bool: - schema_name = ARTIFACT_SCHEMAS.get(artifact_type) - if not schema_name: - return False - schema = _load_schema(schemas_dir() / schema_name) - if "$ref" in schema: - ref = str(schema["$ref"]) - if ref.endswith("embedded_event") and artifact_type == "PFCoreEvent.v0": - return True - resolved = _resolve_schema_ref(schema, ref) - if resolved: - schema = resolved - props = schema.get("properties") - if not isinstance(props, dict): - return False - artifact_type_schema = props.get("artifact_type") - if not isinstance(artifact_type_schema, dict): - return False - return artifact_type_schema.get("const") == artifact_type - - - -def detect_artifact_type(data: dict[str, Any]) -> str | None: - explicit = data.get("artifact_type") - if isinstance(explicit, str) and explicit in ARTIFACT_SCHEMAS: - if _schema_requires_artifact_type(explicit): - return explicit - if ( - "trace_id" in data - and "tool_calls" in data - and "workflow_id" in data - and "agent_id" in data - and data.get("schema_version") == "v0" - and "artifact_type" not in data - ): - return "ToolUseTrace.v0" - if "signed_bundle_id" in data and "science_claim_bundle" in data: - return "SignedScienceClaimBundle.v0" - if "bundle_id" in data and "claim_artifact" in data: - return "ScienceClaimBundle.v0" - if "verification_id" in data: - return "VerificationResult.v0" - if "receipt_id" in data: - return "RuntimeReceipt.v0" - if "certificate_id" in data: - return "TraceCertificate.v0" - if "assumption_set_id" in data: - return "AssumptionSet.v0" - if "source_span_id" in data: - return "SourceSpan.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("registry_id"), str) - and isinstance(data.get("metrics"), dict) - and "registry_version" in data - and "suites" not in data - ): - return "BenchmarkMetricRegistry.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("registry_id"), str) - and isinstance(data.get("suites"), dict) - and "registry_version" in data - ): - return "BenchmarkRegistry.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("suite_id"), str) - and isinstance(data.get("case_ids"), list) - and isinstance(data.get("cases"), list) - and "case_count" in data - and "task_id" in data - ): - return "BenchmarkSuiteManifest.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("report_id"), str) - and isinstance(data.get("required_sections"), list) - and "quality_score" in data - ): - return "ExplainQualityReport.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("coverage_id"), str) - and isinstance(data.get("workflow_profile_id"), str) - and isinstance(data.get("artifact_types_required"), list) - ): - return "ProfileCoverageReport.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("artifact_type"), str) - and isinstance(data.get("path"), str) - and isinstance(data.get("sha256"), str) - and isinstance(data.get("role"), str) - and "producer_id" not in data - and "benchmark_runs" not in data - ): - return "BenchmarkArtifactRef.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("producer_id"), str) - and isinstance(data.get("suite_id"), str) - and isinstance(data.get("benchmark_runs"), list) - and isinstance(data.get("logs"), list) - and "workflow_id" in data - ): - return "PcsBenchIngest.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("metric_id"), str) - and "applicability" in data - and "score" in data - and "numerator" in data - and "benchmark_suite_id" not in data - ): - return "MetricSummary.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("report_id"), str) - and isinstance(data.get("benchmark_suite_id"), str) - and isinstance(data.get("summary"), dict) - ): - return "BenchmarkReport.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("run_id"), str) - and isinstance(data.get("case_id"), str) - and "duration_ms" in data - and "observed_status" in data - ): - return "BenchmarkRun.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("case_id"), str) - and isinstance(data.get("case_kind"), str) - and "input_artifacts" in data - ): - return "BenchmarkCase.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("task_id"), str) - and isinstance(data.get("metrics"), list) - and "success_criteria" in data - ): - return "BenchmarkTask.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("coverage_id"), str) - and "coverage_ratio" in data - and "numerator" in data - and ("metric" in data or "metric_id" in data) - ): - return "CoverageReport.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("result_id"), str) - and "localized_correctly" in data - ): - return "FailureLocalizationResult.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("manifest_id"), str) - and isinstance(data.get("failure_code"), str) - and "repair_hint_kind" in data - ): - return "FailureCaseManifest.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("run_id"), str) - and isinstance(data.get("suite"), str) - and "started_at" in data - and "completed_at" in data - ): - return "ConformanceRun.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("suite"), str) - and "checks_passed" in data - and "checks_failed" in data - and isinstance(data.get("failures"), list) - ): - return "ConformanceReport.v0" - if ( - "policy_id" in data - and "severity_definitions" in data - and isinstance(data.get("checks"), list) - ): - return "SemanticCheckExecution.v0" - if ( - "from_version" in data - and "to_version" in data - and "changes" in data - and "artifact_type" in data - ): - return "MigrationReport.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("check_id"), str) - and isinstance(data.get("proof_obligation_id"), str) - and "lean_theorem" in data - and "lean_version" in data - ): - return "LeanCheckResult.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("obligation_id"), str) - and isinstance(data.get("obligations"), list) - and "lean_module" in data - ): - return "ProofObligation.v0" - if "validation_id" in data and "artifacts_checked" in data: - return "ReleaseChainValidationResult.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("component"), str) - and isinstance(data.get("artifacts"), dict) - and "signature_or_digest" in data - and "source_commit" in data - ): - return "ComponentReleaseFragment.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("workflow_id"), str) - and isinstance(data.get("domain"), str) - and isinstance(data.get("handoff_sequence"), list) - and isinstance(data.get("runtime_artifacts"), list) - ): - return "WorkflowProfile.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("witness_id"), str) - and isinstance(data.get("dataset_hash"), str) - and isinstance(data.get("run_receipt_hash"), str) - ): - return "ComputationWitness.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("dataset_id"), str) - and isinstance(data.get("aggregate_hash"), str) - and isinstance(data.get("files"), list) - ): - return "DatasetReceipt.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("environment_id"), str) - and isinstance(data.get("environment_kind"), str) - ): - return "EnvironmentReceipt.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("run_id"), str) - and isinstance(data.get("command"), str) - and "dataset_receipt_ref" in data - ): - return "ComputationRunReceipt.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("result_id"), str) - and isinstance(data.get("result_kind"), str) - ): - return "ResultArtifact.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("trace_id"), str) - and isinstance(data.get("tool_calls"), list) - ): - return "ToolUseTrace.v0" - if "handoff_id" in data and "handoff_kind" in data: - return "HandoffManifest.v0" - if "registry_id" in data and "entries" in data and "registry_version" in data: - return "ArtifactRegistry.v0" - if ( - "release_id" in data - and "producer_repos" in data - and "validation_profile" in data - and "workflow_profile_id" in data - ): - return "ReleaseManifest.v0" - if "signed_bundle_id" in data and "science_claim_bundle" in data: - return "SignedScienceClaimBundle.v0" - if "bundle_id" in data and "claim_artifact" in data: - return "ScienceClaimBundle.v0" - if "verification_id" in data: - return "VerificationResult.v0" - if "receipt_id" in data: - return "RuntimeReceipt.v0" - if ( - data.get("schema_version") == "v0" - and isinstance(data.get("certificate_id"), str) - and "policy_hash" in data - and isinstance(data.get("violations"), list) - and "spec_hash" not in data - ): - return "ToolUseCertificate.v0" - if "certificate_id" in data: - return "TraceCertificate.v0" - if "assumption_set_id" in data: - return "AssumptionSet.v0" - if "source_span_id" in data: - return "SourceSpan.v0" - if data.get("artifact_type") == "ClaimArtifact.v0": - return "ClaimArtifact.v0" - if "bundle_id" in data and "claim_refs" in data: - return "EvidenceBundle.v0" - return None - - - -def _load_schema(path: Path) -> dict[str, Any]: - with path.open(encoding="utf-8") as f: - return json.load(f) - - -def build_registry() -> Registry: - schema_root = schemas_dir() - resources: list[tuple[str, Resource]] = [] - for path in sorted(schema_root.glob("*.json")): - schema = _load_schema(path) - schema_id = schema.get("$id") - if schema_id: - resources.append( - (schema_id, Resource.from_contents(schema, default_specification=DRAFT202012)) - ) - file_uri = path.as_uri() - resources.append( - (file_uri, Resource.from_contents(schema, default_specification=DRAFT202012)) - ) - resources.append( - (path.name, Resource.from_contents(schema, default_specification=DRAFT202012)) - ) - return Registry().with_resources(resources) - - -_REGISTRY: Registry | None = None - - -def get_registry() -> Registry: - global _REGISTRY - if _REGISTRY is None: - _REGISTRY = build_registry() - return _REGISTRY - - -def get_validator(artifact_type: str) -> Draft202012Validator: - schema_name = ARTIFACT_SCHEMAS.get(artifact_type) - if not schema_name: - raise ValidationError(f"Unknown artifact type: {artifact_type}") - schema_path = schemas_dir() / schema_name - schema = _load_schema(schema_path) - return Draft202012Validator(schema, registry=get_registry()) - - -def validate_schema(data: dict[str, Any], artifact_type: str) -> list[str]: - validator = get_validator(artifact_type) - errors = sorted(validator.iter_errors(data), key=lambda e: list(e.path)) - return [e.message for e in errors] - - -def _is_zero_source_commit(value: str) -> bool: - return bool(_ZERO_COMMIT_RE.match(value.strip())) - - -def _local_dev_enabled(obj: dict[str, Any], inherited: bool) -> bool: - if inherited: - return True - if obj.get("local_dev") is True: - return True - return False - - -def _check_source_commits( - obj: Any, - path: str, - errors: list[str], - *, - inherited_local_dev: bool = False, -) -> None: - if isinstance(obj, dict): - local_dev = _local_dev_enabled(obj, inherited_local_dev) - commit = obj.get("source_commit") - if isinstance(commit, str) and _is_zero_source_commit(commit) and not local_dev: - errors.append( - f"{path or 'root'}: zero source_commit not allowed without local_dev=true" - ) - for key, value in obj.items(): - child = f"{path}.{key}" if path else key - _check_source_commits(value, child, errors, inherited_local_dev=local_dev) - elif isinstance(obj, list): - for index, item in enumerate(obj): - _check_source_commits( - item, - f"{path}[{index}]", - errors, - inherited_local_dev=inherited_local_dev, - ) - - -def _validate_status_fields(obj: Any, path: str, errors: list[str]) -> None: - if isinstance(obj, dict): - if "check_id" not in obj: - status = obj.get("status") - if isinstance(status, str): - if "certificate_id" in obj: - if status not in TRACE_CERTIFICATE_STATUSES: - errors.append(f"{path}: invalid TraceCertificate status {status!r}") - elif status not in ARTIFACT_STATUSES: - errors.append(f"{path}: unknown status {status!r}") - for key, value in obj.items(): - child = f"{path}.{key}" if path else key - _validate_status_fields(value, child, errors) - elif isinstance(obj, list): - for index, item in enumerate(obj): - _validate_status_fields(item, f"{path}[{index}]", errors) - - -def _validate_science_claim_bundle(data: dict[str, Any]) -> list[str]: - errors: list[str] = [] - - assumption_set = data.get("assumption_set") - if not isinstance(assumption_set, dict): - errors.append("ScienceClaimBundle.v0 requires assumption_set") - else: - assumptions = assumption_set.get("assumptions") - if not assumptions: - errors.append("ScienceClaimBundle.v0 requires non-empty assumption_set.assumptions") - - receipts = data.get("runtime_receipts") - if not isinstance(receipts, list) or len(receipts) == 0: - errors.append("ScienceClaimBundle.v0 requires non-empty runtime_receipts") - - claim = data.get("claim_artifact") - if isinstance(claim, dict): - ref = claim.get("assumption_set_ref") - if not ref or not str(ref).strip(): - errors.append("claim_artifact requires non-empty assumption_set_ref") - elif isinstance(assumption_set, dict): - if ref != assumption_set.get("assumption_set_id"): - errors.append( - "claim_artifact.assumption_set_ref must match assumption_set.assumption_set_id" - ) - - certificates = data.get("certificates") - if not isinstance(certificates, list): - certificates = [] - - claim_status = str(claim.get("status") or "") if isinstance(claim, dict) else "" - if claim_status in CERTIFIED_CLAIM_STATUSES and len(certificates) == 0: - errors.append("certified ScienceClaimBundle requires at least one TraceCertificate") - - if isinstance(receipts, list): - for receipt in receipts: - if not isinstance(receipt, dict): - continue - r_hash = receipt.get("trace_hash") - for cert in certificates: - if not isinstance(cert, dict): - continue - c_status = str(cert.get("status") or "") - if c_status and c_status not in TRACE_CERTIFICATE_STATUSES: - errors.append( - f"TraceCertificate {cert.get('certificate_id')}: " - f"invalid status {c_status!r}" - ) - c_hash = cert.get("trace_hash") - if r_hash and c_hash and r_hash != c_hash: - errors.append( - f"trace_hash mismatch: receipt {receipt.get('receipt_id')} " - f"({r_hash}) vs certificate {cert.get('certificate_id')} ({c_hash})" - ) - - return errors - - -def _validate_verification_result(data: dict[str, Any]) -> list[str]: - errors: list[str] = [] - checks = data.get("checks") - if not isinstance(checks, list): - return errors - has_failed = any( - isinstance(check, dict) and check.get("status") == "failed" for check in checks - ) - top_status = str(data.get("status") or "") - if has_failed and top_status in IMPORT_READY_VERIFICATION_STATUSES: - errors.append( - "VerificationResult.v0 with failed checks cannot use import-ready status " - f"{top_status!r} (Scientific Memory import contract)" - ) - return errors - - -def _validate_signed_bundle(data: dict[str, Any]) -> list[str]: - errors: list[str] = [] - scb = data.get("science_claim_bundle") - if isinstance(scb, dict): - errors.extend(_validate_science_claim_bundle(scb)) - vr = data.get("verification_result") - if isinstance(vr, dict): - _validate_status_fields(vr, "verification_result", errors) - errors.extend(_validate_verification_result(vr)) - return errors - - -def _resolve_schema_ref(schema: dict[str, Any], ref: str) -> dict[str, Any]: - if ref.startswith("pf_core.defs.json#/$defs/"): - defs_path = schemas_dir() / "pf_core.defs.json" - defs_schema = _load_schema(defs_path) - def_name = ref.split("/")[-1] - target = defs_schema.get("$defs", {}).get(def_name) - if isinstance(target, dict): - return target - return {} - - -def _schema_requires_artifact_type(artifact_type: str) -> bool: - schema_name = ARTIFACT_SCHEMAS.get(artifact_type) - if not schema_name: - return False - schema = _load_schema(schemas_dir() / schema_name) - if "$ref" in schema: - ref = str(schema["$ref"]) - if ref.endswith("embedded_event") and artifact_type == "PFCoreEvent.v0": - return True - resolved = _resolve_schema_ref(schema, ref) - if resolved: - schema = resolved - props = schema.get("properties") - if not isinstance(props, dict): - return False - artifact_type_schema = props.get("artifact_type") - if not isinstance(artifact_type_schema, dict): - return False - return artifact_type_schema.get("const") == artifact_type - - -def _validate_pfcore_claim_class(data: dict[str, Any], path: str, errors: list[str]) -> None: - claim_class = data.get("claim_class") - if not isinstance(claim_class, str): - return - if claim_class not in PF_CORE_CLAIM_CLASSES: - errors.append(f"{path}: invalid claim_class {claim_class!r}") - return - if claim_class == "LeanKernelChecked" and not data.get("proof_ref"): - errors.append( - f"{path}: claim_class LeanKernelChecked requires proof_ref (ClaimClassOverclaim)" - ) - if claim_class == "LeanKernelChecked" and not data.get("proof_term_ref"): - errors.append( - f"{path}: claim_class LeanKernelChecked requires proof_term_ref (ClaimClassOverclaim)" - ) - if claim_class == "LeanKernelChecked" and data.get("lean_proof_checked") is not True: - errors.append( - f"{path}: claim_class LeanKernelChecked requires lean_proof_checked=true" - ) - - -def _validate_pfcore_trace(data: dict[str, Any]) -> list[str]: - from pcs_core.pf_core_runtime import validate_pfcore_trace_hash_chain - - errors: list[str] = [] - _validate_pfcore_claim_class(data, "root", errors) - errors.extend(validate_pfcore_trace_hash_chain(data)) - return errors - - -def _validate_pfcore_certificate(data: dict[str, Any]) -> list[str]: - from pcs_core.registry_data import enforce_assumption_declared, registry_entries - - errors: list[str] = [] - _validate_pfcore_claim_class(data, "root", errors) - if data.get("lean_proof_checked") and not data.get("proof_term_ref"): - errors.append("root: lean_proof_checked requires proof_term_ref") - errors.extend(enforce_assumption_declared(data, registry_entries().get("PFCoreCertificate.v0"))) - return errors - - -def _validate_lean_check_result(data: dict[str, Any]) -> list[str]: - errors: list[str] = [] - claim_class = data.get("claim_class") - if isinstance(claim_class, str) and claim_class not in PF_CORE_CLAIM_CLASSES: - errors.append(f"root: invalid claim_class {claim_class!r}") - status = str(data.get("status") or "") - lean_proof_checked = data.get("lean_proof_checked") is True - if status == "LeanProofChecked" and claim_class != "LeanKernelChecked": - errors.append("root: status LeanProofChecked requires claim_class LeanKernelChecked") - if status == "ReplayValidated" and claim_class != "ReplayValidated": - errors.append("root: status ReplayValidated requires claim_class ReplayValidated") - if status == "LeanProofChecked" and not lean_proof_checked: - errors.append("root: status LeanProofChecked requires lean_proof_checked=true") - if claim_class == "LeanKernelChecked" and status != "LeanProofChecked": - errors.append("root: claim_class LeanKernelChecked requires status LeanProofChecked") - cert = data.get("certificate") - if isinstance(cert, dict): - errors.extend(_validate_pfcore_certificate(cert)) - return errors -def validate_semantics(data: dict[str, Any], artifact_type: str) -> list[str]: - errors: list[str] = [] - - if artifact_type == "ArtifactRegistry.v0": - errors.extend(validate_artifact_registry_semantics(data)) - return errors - - if artifact_type == "ComponentReleaseFragment.v0": - _check_source_commits(data, "", errors) - return errors - - if artifact_type == "MigrationReport.v0": - return errors - - if artifact_type == "ReleaseManifest.v0": - errors.extend(validate_release_manifest_semantics(data)) - return errors - - if artifact_type == "HandoffManifest.v0": - errors.extend(validate_handoff_manifest_semantics(data)) - return errors - - if artifact_type == "ConformanceReport.v0": - errors.extend(validate_conformance_report_semantics(data)) - return errors - - if artifact_type == "WorkflowProfile.v0": - errors.extend(validate_workflow_profile_semantics(data)) - return errors - - if artifact_type == "ToolUseTrace.v0": - errors.extend(validate_tool_use_trace_semantics(data)) - return errors - - if artifact_type == "ToolUseCertificate.v0": - errors.extend(validate_tool_use_certificate_semantics(data)) - return errors - - if artifact_type == "DatasetReceipt.v0": - errors.extend(validate_dataset_receipt_semantics(data)) - return errors - - if artifact_type == "EnvironmentReceipt.v0": - errors.extend(validate_environment_receipt_semantics(data)) - return errors - - if artifact_type == "ComputationRunReceipt.v0": - errors.extend(validate_computation_run_receipt_semantics(data)) - return errors - - if artifact_type == "ResultArtifact.v0": - errors.extend(validate_result_artifact_semantics(data)) - return errors - - if artifact_type == "ComputationWitness.v0": - errors.extend(validate_computation_witness_semantics(data)) - return errors - - if artifact_type == "ProofObligation.v0": - errors.extend(validate_proof_obligation_semantics(data)) - return errors - - if artifact_type == "LeanCheckResult.v0": - if data.get("artifact_type") == "LeanCheckResult.v0": - errors.extend(_validate_lean_check_result(data)) - elif "check_id" in data: - errors.extend(validate_lean_check_result_semantics(data)) - else: - errors.append( - "LeanCheckResult.v0: expected PF-Core artifact_type or PCS check_id shape" - ) - return errors - - - if artifact_type == "BenchmarkMetricRegistry.v0": - errors.extend(validate_benchmark_metric_registry_semantics(data)) - return errors - - if artifact_type == "BenchmarkRegistry.v0": - errors.extend(validate_benchmark_registry_semantics(data)) - return errors - - if artifact_type == "BenchmarkSuiteManifest.v0": - errors.extend(validate_benchmark_suite_manifest_semantics(data)) - return errors - - if artifact_type == "BenchmarkTask.v0": - errors.extend(validate_benchmark_task_semantics(data)) - return errors - - if artifact_type == "BenchmarkCase.v0": - errors.extend(validate_benchmark_case_semantics(data)) - return errors - - if artifact_type == "BenchmarkRun.v0": - errors.extend(validate_benchmark_run_semantics(data)) - return errors - - if artifact_type == "BenchmarkReport.v0": - errors.extend(validate_benchmark_report_semantics(data)) - return errors - - if artifact_type == "MetricSummary.v0": - return errors - - if artifact_type == "BenchmarkArtifactRef.v0": - from pcs_core.benchmark_validate import validate_benchmark_artifact_ref_semantics - - errors.extend(validate_benchmark_artifact_ref_semantics(data)) - return errors - - if artifact_type == "PcsBenchIngest.v0": - errors.extend(validate_pcs_bench_ingest_semantics(data)) - return errors - - if artifact_type == "ConformanceRun.v0": - return errors - - if artifact_type == "FailureCaseManifest.v0": - return errors - - if artifact_type == "FailureLocalizationResult.v0": - return errors - - if artifact_type == "CoverageReport.v0": - return errors - - if artifact_type == "ExplainQualityReport.v0": - return errors - - if artifact_type == "ProfileCoverageReport.v0": - return errors - - if artifact_type == "ReleaseChainValidationResult.v0": - errors.extend(validate_release_chain_validation_result_semantics(data)) - checks = data.get("checks") - if isinstance(checks, list): - for index, check in enumerate(checks): - if isinstance(check, dict): - _validate_status_fields(check, f"checks[{index}]", errors) - return errors - - _check_source_commits(data, "", errors) - _validate_status_fields(data, "", errors) - - if artifact_type == "ClaimArtifact.v0": - ref = data.get("assumption_set_ref") - if not ref or not str(ref).strip(): - errors.append("ClaimArtifact.v0 requires non-empty assumption_set_ref") - - if artifact_type == "ScienceClaimBundle.v0": - errors.extend(_validate_science_claim_bundle(data)) - - if artifact_type == "VerificationResult.v0": - errors.extend(_validate_verification_result(data)) - - if artifact_type == "SignedScienceClaimBundle.v0": - errors.extend(_validate_signed_bundle(data)) - - if artifact_type == "TraceCertificate.v0": - status = str(data.get("status") or "") - if status and status not in TRACE_CERTIFICATE_STATUSES: - errors.append(f"TraceCertificate.v0 invalid status {status!r}") - - if artifact_type == "PFCoreTrace.v0": - errors.extend(_validate_pfcore_trace(data)) - - if artifact_type == "PFCoreCertificate.v0": - errors.extend(_validate_pfcore_certificate(data)) - - if artifact_type == "LeanCheckResult.v0": - errors.extend(_validate_lean_check_result(data)) - - if artifact_type in _PF_CORE_ARTIFACT_TYPES and artifact_type not in { - "PFCoreTrace.v0", - "PFCoreCertificate.v0", - "LeanCheckResult.v0", - "ToolUseTrace.v0", - }: - _validate_pfcore_claim_class(data, "root", errors) - - return errors - return errors -def validate_artifact(data: dict[str, Any], artifact_type: str | None = None) -> None: - artifact_type = artifact_type or detect_artifact_type(data) - if not artifact_type: - raise ValidationError("Could not detect artifact type from JSON content") - - schema_errors = validate_schema(data, artifact_type) - semantic_errors = validate_semantics(data, artifact_type) - all_errors = schema_errors + semantic_errors - if all_errors: - raise ValidationError( - f"Validation failed for {artifact_type}", - errors=all_errors, - ) - - -def validate_file(path: Path | str) -> str: - path = Path(path) - with path.open(encoding="utf-8") as f: - data = json.load(f) - if not isinstance(data, dict): - raise ValidationError("Artifact root must be a JSON object") - artifact_type = detect_artifact_type(data) - if not artifact_type: - raise ValidationError(f"Could not detect artifact type in {path}") - validate_artifact(data, artifact_type) - if artifact_type == "ReleaseManifest.v0": - ref_errors = validate_release_manifest_fixture_refs(data, path.parent) - if ref_errors: - raise ValidationError( - f"Validation failed for {artifact_type}", - errors=ref_errors, - ) - return artifact_type - - -def _is_valid_example(path: Path) -> bool: - if "tool-use-release-invalid" in path.parts or "computation-release-invalid" in path.parts: - return False - return path.suffix == ".json" and ".valid." in path.name - - -def iter_example_json_files(examples_dir: Path) -> list[Path]: - return sorted(p for p in examples_dir.rglob("*.json") if p.is_file()) - - -def check_all_schemas() -> None: - for artifact_type, schema_name in ARTIFACT_SCHEMAS.items(): - schema_path = schemas_dir() / schema_name - schema = _load_schema(schema_path) - Draft202012Validator.check_schema(schema) - get_validator(artifact_type) - - -def check_valid_examples(examples_dir: Path | None = None) -> None: - examples_dir = examples_dir or default_examples_dir() - for path in iter_example_json_files(examples_dir): - if _is_valid_example(path): - validate_file(path) - for name in ( - "release_manifest.valid.json", - "handoff_manifest.valid.json", - "release_chain_validation_result.valid.json", - "artifact_registry.valid.json", - "migration_report.valid.json", - "proof_obligation.valid.json", - "lean_check_result.valid.json", - "benchmark_registry.valid.json", - "benchmark_metric_registry.valid.json", - ): - validate_file(examples_dir / name) - - benchmarks_examples = examples_dir / "benchmarks" - if benchmarks_examples.is_dir(): - for path in sorted(benchmarks_examples.rglob("*.valid.json")): - validate_file(path) - compat = benchmarks_examples / "compatibility" - if compat.is_dir(): - for path in sorted(compat.glob("*.normalized.json")) + sorted( - compat.glob("*.pcs_bench_ingest.normalized.json"), - ): - validate_file(path) - - producer_examples = examples_dir / "benchmark" - if producer_examples.is_dir(): - for path in sorted(producer_examples.glob("*.valid.json")): - validate_file(path) - - ingest_examples = examples_dir / "benchmark_ingest" - if ingest_examples.is_dir(): - for path in sorted(ingest_examples.glob("*.pcs_bench_ingest.valid.json")): - validate_file(path) - - -def iter_pf_core_example_dirs(kind: str) -> list[Path]: - root = repo_root() / "examples" / f"pf-core-{kind}" - if not root.is_dir(): - return [] - return sorted(path for path in root.iterdir() if path.is_dir()) - - -def load_pf_core_fixture_manifest(case_dir: Path) -> dict[str, Any]: - manifest_path = case_dir / "manifest.json" - if not manifest_path.is_file(): - raise ValidationError(f"Missing manifest.json in {case_dir}") - manifest = json.loads(manifest_path.read_text(encoding="utf-8")) - if not isinstance(manifest, dict): - raise ValidationError(f"manifest.json root must be an object in {case_dir}") - return manifest - - -def validate_artifact(data: dict[str, Any], artifact_type: str | None = None) -> None: - artifact_type = artifact_type or detect_artifact_type(data) - if not artifact_type: - raise ValidationError("Could not detect artifact type from JSON content") - - schema_errors = validate_schema(data, artifact_type) - semantic_errors = validate_semantics(data, artifact_type) - all_errors = schema_errors + semantic_errors - if all_errors: - raise ValidationError( - f"Validation failed for {artifact_type}", - errors=all_errors, - ) - - -def validate_file(path: Path | str) -> str: - path = Path(path) - with path.open(encoding="utf-8") as f: - data = json.load(f) - if not isinstance(data, dict): - raise ValidationError("Artifact root must be a JSON object") - artifact_type = detect_artifact_type(data) - if not artifact_type: - raise ValidationError(f"Could not detect artifact type in {path}") - validate_artifact(data, artifact_type) - return artifact_type - - -def _is_valid_example(path: Path) -> bool: - return path.suffix == ".json" and ".valid." in path.name - - -def iter_example_json_files(examples_dir: Path) -> list[Path]: - return sorted(p for p in examples_dir.rglob("*.json") if p.is_file()) - - -def check_all_schemas() -> None: - for artifact_type, schema_name in ARTIFACT_SCHEMAS.items(): - schema_path = schemas_dir() / schema_name - schema = _load_schema(schema_path) - Draft202012Validator.check_schema(schema) - get_validator(artifact_type) - - -def check_valid_examples(examples_dir: Path | None = None) -> None: - examples_dir = examples_dir or default_examples_dir() - for path in iter_example_json_files(examples_dir): - if _is_valid_example(path): - validate_file(path) - check_pf_core_valid_fixtures() - - -def check_pf_core_valid_fixtures() -> None: - from pcs_core.pf_core_replay import replay_trace - - for case_dir in iter_pf_core_example_dirs("valid"): - manifest = None - manifest_path = case_dir / "manifest.json" - if manifest_path.is_file(): - manifest = load_pf_core_fixture_manifest(case_dir) - for path in sorted(case_dir.glob("*.json")): - if path.name == "manifest.json": - continue - validate_file(path) - if manifest and manifest.get("replay_required"): - trace_path = case_dir / str(manifest.get("trace_file") or "trace.json") - if trace_path.is_file(): - result = replay_trace(trace_path) - if not result.match: - raise ValidationError( - f"Replay failed for {case_dir}: {result.diffs!r}" - ) - - -def check_pf_core_invalid_fixtures() -> None: - from pcs_core.pf_core_contract import validate_trace_contracts - from pcs_core.pf_core_runtime import ( - DroppedDeniedEvent, - HandoffAuthorityExpansion, - MissingPrincipal, - UnknownCapability, - UnknownEffect, - compile_runtime_observation_to_event, - compile_tool_use_trace_to_pfcore_trace, - validate_denied_events_preserved, - validate_handoff_authority, - validate_pfcore_trace_hash_chain, - ) - - for case_dir in iter_pf_core_example_dirs("invalid"): - manifest = load_pf_core_fixture_manifest(case_dir) - expected_error = str(manifest["expected_error"]) - must_fail_at = str(manifest["must_fail_at"]) - - if must_fail_at == "runtime_to_pfcore_event": - observation = json.loads((case_dir / "observation.json").read_text(encoding="utf-8")) - try: - compile_runtime_observation_to_event(observation) - except (UnknownCapability, UnknownEffect, MissingPrincipal) as exc: - if exc.code != expected_error: - raise ValidationError( - f"{case_dir}: expected {expected_error!r}, got {exc.code!r}" - ) from exc - else: - raise ValidationError(f"Expected {case_dir} to fail at {must_fail_at}") - continue - - if must_fail_at == "validate_pfcore_trace_hash_chain": - trace = json.loads((case_dir / "trace.json").read_text(encoding="utf-8")) - errors = validate_pfcore_trace_hash_chain(trace) - if not any(expected_error in err for err in errors): - raise ValidationError( - f"Expected {case_dir} to fail with {expected_error!r}, got {errors!r}" - ) - continue - - if must_fail_at == "validate_denied_events_preserved": - tool_use_trace = json.loads( - (case_dir / "tool_use_trace.json").read_text(encoding="utf-8") - ) - pfcore_trace = json.loads((case_dir / "pfcore_trace.json").read_text(encoding="utf-8")) - try: - validate_denied_events_preserved(tool_use_trace, pfcore_trace) - except DroppedDeniedEvent as exc: - if exc.code != expected_error: - raise ValidationError( - f"{case_dir}: expected {expected_error!r}, got {exc.code!r}" - ) from exc - else: - raise ValidationError(f"Expected {case_dir} to fail at {must_fail_at}") - continue - - if must_fail_at == "validate_handoff_authority": - handoff = json.loads((case_dir / "handoff.json").read_text(encoding="utf-8")) - try: - validate_handoff_authority(handoff) - except HandoffAuthorityExpansion as exc: - if exc.code != expected_error: - raise ValidationError( - f"{case_dir}: expected {expected_error!r}, got {exc.code!r}" - ) from exc - else: - raise ValidationError(f"Expected {case_dir} to fail at {must_fail_at}") - continue - - if must_fail_at == "compile_tool_use_trace_to_pfcore_trace": - tool_use_trace = json.loads( - (case_dir / "tool_use_trace.json").read_text(encoding="utf-8") - ) - try: - compile_tool_use_trace_to_pfcore_trace(tool_use_trace) - except HandoffAuthorityExpansion as exc: - if exc.code != expected_error: - raise ValidationError( - f"{case_dir}: expected {expected_error!r}, got {exc.code!r}" - ) from exc - else: - raise ValidationError(f"Expected {case_dir} to fail at {must_fail_at}") - continue - - if must_fail_at == "validate_trace_contracts": - trace = json.loads((case_dir / "trace.json").read_text(encoding="utf-8")) - contracts_dir = case_dir / "contracts" - contracts = { - str(data["contract_id"]): data - for data in ( - json.loads(path.read_text(encoding="utf-8")) - for path in sorted(contracts_dir.glob("*.json")) - ) - } - issues = validate_trace_contracts(trace, contracts) - if not any(issue.code == expected_error for issue in issues): - raise ValidationError( - f"Expected {case_dir} to fail with {expected_error!r}, got " - f"{[issue.code for issue in issues]!r}" - ) - continue - - if must_fail_at == "validate_tenant_isolation": - from pcs_core.pf_core_runtime import validate_tenant_isolation - - trace = json.loads((case_dir / "trace.json").read_text(encoding="utf-8")) - errors = validate_tenant_isolation(trace) - if not any(expected_error in err for err in errors): - raise ValidationError( - f"Expected {case_dir} to fail with {expected_error!r}, got {errors!r}" - ) - continue - - raise ValidationError(f"Unknown must_fail_at {must_fail_at!r} in {case_dir}") - - - -def check_invalid_examples(examples_dir: Path | None = None) -> None: - examples_dir = examples_dir or default_examples_dir() - invalid_cases: dict[str, str | None] = { - "invalid_unknown_status.json": "RuntimeReceipt.v0", - "invalid_missing_assumption_set.json": "ScienceClaimBundle.v0", - "invalid_mismatched_trace_hash.json": "ScienceClaimBundle.v0", - "invalid_zero_source_commit.release.json": "RuntimeReceipt.v0", - "labtrust/invalid_singular_runtime_receipt_bundle.json": "ScienceClaimBundle.v0", - "labtrust/invalid_signed_schema_version_artifact_name.json": "SignedScienceClaimBundle.v0", - "labtrust/invalid_failed_verification_result.json": "VerificationResult.v0", - "labtrust/invalid_missing_trace_certificate.json": "ScienceClaimBundle.v0", - } - for filename, artifact_type in invalid_cases.items(): - path = examples_dir / filename - data = json.loads(path.read_text(encoding="utf-8")) - detected = detect_artifact_type(data) - use_type = artifact_type or detected - if not use_type: - raise ValidationError(f"Could not detect type for {filename}") - schema_errors = validate_schema(data, use_type) - semantic_errors = validate_semantics(data, use_type) - if not schema_errors and not semantic_errors: - raise ValidationError(f"Expected {filename} to fail validation") - check_pf_core_invalid_fixtures() +from pcs_core.validate_detect import * # noqa: F403 +from pcs_core.validate_pcs_core import * # noqa: F403 +from pcs_core.validate_pf_core import * # noqa: F403 +from pcs_core.validate_semantics import * # noqa: F403 diff --git a/python/pcs_core/validate_detect.py b/python/pcs_core/validate_detect.py new file mode 100644 index 0000000..e79edd8 --- /dev/null +++ b/python/pcs_core/validate_detect.py @@ -0,0 +1,448 @@ +"""Artifact type detection and JSON Schema validation.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + +from jsonschema import Draft202012Validator +from referencing import Registry, Resource +from referencing.jsonschema import DRAFT202012 + +from pcs_core.paths import repo_root, schemas_dir +ARTIFACT_SCHEMAS: dict[str, str] = { + "AssumptionSet.v0": "AssumptionSet.v0.schema.json", + "SourceSpan.v0": "SourceSpan.v0.schema.json", + "ClaimArtifact.v0": "ClaimArtifact.v0.schema.json", + "RuntimeReceipt.v0": "RuntimeReceipt.v0.schema.json", + "TraceCertificate.v0": "TraceCertificate.v0.schema.json", + "EvidenceBundle.v0": "EvidenceBundle.v0.schema.json", + "ScienceClaimBundle.v0": "ScienceClaimBundle.v0.schema.json", + "VerificationResult.v0": "VerificationResult.v0.schema.json", + "SignedScienceClaimBundle.v0": "SignedScienceClaimBundle.v0.schema.json", + "ReleaseManifest.v0": "ReleaseManifest.v0.schema.json", + "HandoffManifest.v0": "HandoffManifest.v0.schema.json", + "ReleaseChainValidationResult.v0": "ReleaseChainValidationResult.v0.schema.json", + "ArtifactRegistry.v0": "ArtifactRegistry.v0.schema.json", + "MigrationReport.v0": "MigrationReport.v0.schema.json", + "ComponentReleaseFragment.v0": "ComponentReleaseFragment.v0.schema.json", + "SemanticCheckExecution.v0": "SemanticCheckExecution.v0.schema.json", + "ConformanceReport.v0": "ConformanceReport.v0.schema.json", + "WorkflowProfile.v0": "WorkflowProfile.v0.schema.json", + "ToolUseTrace.v0": "ToolUseTrace.v0.schema.json", + "ToolUseCertificate.v0": "ToolUseCertificate.v0.schema.json", + "DatasetReceipt.v0": "DatasetReceipt.v0.schema.json", + "EnvironmentReceipt.v0": "EnvironmentReceipt.v0.schema.json", + "ComputationRunReceipt.v0": "ComputationRunReceipt.v0.schema.json", + "ResultArtifact.v0": "ResultArtifact.v0.schema.json", + "ComputationWitness.v0": "ComputationWitness.v0.schema.json", + "ProofObligation.v0": "ProofObligation.v0.schema.json", + "LeanCheckResult.v0": "LeanCheckResult.v0.schema.json", + "BenchmarkTask.v0": "BenchmarkTask.v0.schema.json", + "BenchmarkCase.v0": "BenchmarkCase.v0.schema.json", + "BenchmarkRun.v0": "BenchmarkRun.v0.schema.json", + "BenchmarkReport.v0": "BenchmarkReport.v0.schema.json", + "BenchmarkRegistry.v0": "BenchmarkRegistry.v0.schema.json", + "BenchmarkSuiteManifest.v0": "BenchmarkSuiteManifest.v0.schema.json", + "ConformanceRun.v0": "ConformanceRun.v0.schema.json", + "FailureCaseManifest.v0": "FailureCaseManifest.v0.schema.json", + "FailureLocalizationResult.v0": "FailureLocalizationResult.v0.schema.json", + "CoverageReport.v0": "CoverageReport.v0.schema.json", + "ExplainQualityReport.v0": "ExplainQualityReport.v0.schema.json", + "ProfileCoverageReport.v0": "ProfileCoverageReport.v0.schema.json", + "BenchmarkMetricRegistry.v0": "BenchmarkMetricRegistry.v0.schema.json", + "MetricSummary.v0": "MetricSummary.v0.schema.json", + "PcsBenchIngest.v0": "PcsBenchIngest.v0.schema.json", + "BenchmarkArtifactRef.v0": "BenchmarkArtifactRef.v0.schema.json", + "PFCorePrincipal.v0": "PFCorePrincipal.v0.schema.json", + "PFCoreCapability.v0": "PFCoreCapability.v0.schema.json", + "PFCoreResource.v0": "PFCoreResource.v0.schema.json", + "PFCoreAction.v0": "PFCoreAction.v0.schema.json", + "PFCoreEffect.v0": "PFCoreEffect.v0.schema.json", + "PFCoreDecision.v0": "PFCoreDecision.v0.schema.json", + "PFCoreEvent.v0": "PFCoreEvent.v0.schema.json", + "PFCoreTrace.v0": "PFCoreTrace.v0.schema.json", + "PFCoreContract.v0": "PFCoreContract.v0.schema.json", + "PFCoreHandoff.v0": "PFCoreHandoff.v0.schema.json", + "PFCoreRuntimeObservation.v0": "PFCoreRuntimeObservation.v0.schema.json", + "PFCoreCertificate.v0": "PFCoreCertificate.v0.schema.json", + "PCSBridgeCertificate.v0": "PCSBridgeCertificate.v0.schema.json", +} +class ValidationError(Exception): + """Raised when artifact validation fails.""" + + def __init__(self, message: str, errors: list[str] | None = None): + super().__init__(message) + self.errors = errors or [] +def _resolve_schema_ref(schema: dict[str, Any], ref: str) -> dict[str, Any]: + if ref.startswith("pf_core.defs.json#/$defs/"): + defs_path = schemas_dir() / "pf_core.defs.json" + defs_schema = _load_schema(defs_path) + def_name = ref.split("/")[-1] + target = defs_schema.get("$defs", {}).get(def_name) + if isinstance(target, dict): + return target + return {} + + +def _schema_requires_artifact_type(artifact_type: str) -> bool: + schema_name = ARTIFACT_SCHEMAS.get(artifact_type) + if not schema_name: + return False + schema = _load_schema(schemas_dir() / schema_name) + if "$ref" in schema: + ref = str(schema["$ref"]) + if ref.endswith("embedded_event") and artifact_type == "PFCoreEvent.v0": + return True + resolved = _resolve_schema_ref(schema, ref) + if resolved: + schema = resolved + props = schema.get("properties") + if not isinstance(props, dict): + return False + artifact_type_schema = props.get("artifact_type") + if not isinstance(artifact_type_schema, dict): + return False + return artifact_type_schema.get("const") == artifact_type + + + +def detect_artifact_type(data: dict[str, Any]) -> str | None: + explicit = data.get("artifact_type") + if isinstance(explicit, str) and explicit in ARTIFACT_SCHEMAS: + if _schema_requires_artifact_type(explicit): + return explicit + if explicit == "LeanCheckResult.v0" and data.get("schema_version") == "v0": + if "trace_path" in data or "check_id" in data: + return explicit + if ( + "trace_id" in data + and "tool_calls" in data + and "workflow_id" in data + and "agent_id" in data + and data.get("schema_version") == "v0" + and "artifact_type" not in data + ): + return "ToolUseTrace.v0" + if "signed_bundle_id" in data and "science_claim_bundle" in data: + return "SignedScienceClaimBundle.v0" + if "bundle_id" in data and "claim_artifact" in data: + return "ScienceClaimBundle.v0" + if "verification_id" in data: + return "VerificationResult.v0" + if "receipt_id" in data: + return "RuntimeReceipt.v0" + if "certificate_id" in data: + return "TraceCertificate.v0" + if "assumption_set_id" in data: + return "AssumptionSet.v0" + if "source_span_id" in data: + return "SourceSpan.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("registry_id"), str) + and isinstance(data.get("metrics"), dict) + and "registry_version" in data + and "suites" not in data + ): + return "BenchmarkMetricRegistry.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("registry_id"), str) + and isinstance(data.get("suites"), dict) + and "registry_version" in data + ): + return "BenchmarkRegistry.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("suite_id"), str) + and isinstance(data.get("case_ids"), list) + and isinstance(data.get("cases"), list) + and "case_count" in data + and "task_id" in data + ): + return "BenchmarkSuiteManifest.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("report_id"), str) + and isinstance(data.get("required_sections"), list) + and "quality_score" in data + ): + return "ExplainQualityReport.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("coverage_id"), str) + and isinstance(data.get("workflow_profile_id"), str) + and isinstance(data.get("artifact_types_required"), list) + ): + return "ProfileCoverageReport.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("artifact_type"), str) + and isinstance(data.get("path"), str) + and isinstance(data.get("sha256"), str) + and isinstance(data.get("role"), str) + and "producer_id" not in data + and "benchmark_runs" not in data + ): + return "BenchmarkArtifactRef.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("producer_id"), str) + and isinstance(data.get("suite_id"), str) + and isinstance(data.get("benchmark_runs"), list) + and isinstance(data.get("logs"), list) + and "workflow_id" in data + ): + return "PcsBenchIngest.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("metric_id"), str) + and "applicability" in data + and "score" in data + and "numerator" in data + and "benchmark_suite_id" not in data + ): + return "MetricSummary.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("report_id"), str) + and isinstance(data.get("benchmark_suite_id"), str) + and isinstance(data.get("summary"), dict) + ): + return "BenchmarkReport.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("run_id"), str) + and isinstance(data.get("case_id"), str) + and "duration_ms" in data + and "observed_status" in data + ): + return "BenchmarkRun.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("case_id"), str) + and isinstance(data.get("case_kind"), str) + and "input_artifacts" in data + ): + return "BenchmarkCase.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("task_id"), str) + and isinstance(data.get("metrics"), list) + and "success_criteria" in data + ): + return "BenchmarkTask.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("coverage_id"), str) + and "coverage_ratio" in data + and "numerator" in data + and ("metric" in data or "metric_id" in data) + ): + return "CoverageReport.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("result_id"), str) + and "localized_correctly" in data + ): + return "FailureLocalizationResult.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("manifest_id"), str) + and isinstance(data.get("failure_code"), str) + and "repair_hint_kind" in data + ): + return "FailureCaseManifest.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("run_id"), str) + and isinstance(data.get("suite"), str) + and "started_at" in data + and "completed_at" in data + ): + return "ConformanceRun.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("suite"), str) + and "checks_passed" in data + and "checks_failed" in data + and isinstance(data.get("failures"), list) + ): + return "ConformanceReport.v0" + if ( + "policy_id" in data + and "severity_definitions" in data + and isinstance(data.get("checks"), list) + ): + return "SemanticCheckExecution.v0" + if ( + "from_version" in data + and "to_version" in data + and "changes" in data + and "artifact_type" in data + ): + return "MigrationReport.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("check_id"), str) + and isinstance(data.get("proof_obligation_id"), str) + and "lean_theorem" in data + and "lean_version" in data + ): + return "LeanCheckResult.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("obligation_id"), str) + and isinstance(data.get("obligations"), list) + and "lean_module" in data + ): + return "ProofObligation.v0" + if "validation_id" in data and "artifacts_checked" in data: + return "ReleaseChainValidationResult.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("component"), str) + and isinstance(data.get("artifacts"), dict) + and "signature_or_digest" in data + and "source_commit" in data + ): + return "ComponentReleaseFragment.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("workflow_id"), str) + and isinstance(data.get("domain"), str) + and isinstance(data.get("handoff_sequence"), list) + and isinstance(data.get("runtime_artifacts"), list) + ): + return "WorkflowProfile.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("witness_id"), str) + and isinstance(data.get("dataset_hash"), str) + and isinstance(data.get("run_receipt_hash"), str) + ): + return "ComputationWitness.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("dataset_id"), str) + and isinstance(data.get("aggregate_hash"), str) + and isinstance(data.get("files"), list) + ): + return "DatasetReceipt.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("environment_id"), str) + and isinstance(data.get("environment_kind"), str) + ): + return "EnvironmentReceipt.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("run_id"), str) + and isinstance(data.get("command"), str) + and "dataset_receipt_ref" in data + ): + return "ComputationRunReceipt.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("result_id"), str) + and isinstance(data.get("result_kind"), str) + ): + return "ResultArtifact.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("trace_id"), str) + and isinstance(data.get("tool_calls"), list) + ): + return "ToolUseTrace.v0" + if "handoff_id" in data and "handoff_kind" in data: + return "HandoffManifest.v0" + if "registry_id" in data and "entries" in data and "registry_version" in data: + return "ArtifactRegistry.v0" + if ( + "release_id" in data + and "producer_repos" in data + and "validation_profile" in data + and "workflow_profile_id" in data + ): + return "ReleaseManifest.v0" + if "signed_bundle_id" in data and "science_claim_bundle" in data: + return "SignedScienceClaimBundle.v0" + if "bundle_id" in data and "claim_artifact" in data: + return "ScienceClaimBundle.v0" + if "verification_id" in data: + return "VerificationResult.v0" + if "receipt_id" in data: + return "RuntimeReceipt.v0" + if ( + data.get("schema_version") == "v0" + and isinstance(data.get("certificate_id"), str) + and "policy_hash" in data + and isinstance(data.get("violations"), list) + and "spec_hash" not in data + ): + return "ToolUseCertificate.v0" + if "certificate_id" in data: + return "TraceCertificate.v0" + if "assumption_set_id" in data: + return "AssumptionSet.v0" + if "source_span_id" in data: + return "SourceSpan.v0" + if data.get("artifact_type") == "ClaimArtifact.v0": + return "ClaimArtifact.v0" + if "bundle_id" in data and "claim_refs" in data: + return "EvidenceBundle.v0" + return None + + + +def _load_schema(path: Path) -> dict[str, Any]: + with path.open(encoding="utf-8") as f: + return json.load(f) + + +def build_registry() -> Registry: + schema_root = schemas_dir() + resources: list[tuple[str, Resource]] = [] + for path in sorted(schema_root.glob("*.json")): + schema = _load_schema(path) + schema_id = schema.get("$id") + if schema_id: + resources.append( + (schema_id, Resource.from_contents(schema, default_specification=DRAFT202012)) + ) + file_uri = path.as_uri() + resources.append( + (file_uri, Resource.from_contents(schema, default_specification=DRAFT202012)) + ) + resources.append( + (path.name, Resource.from_contents(schema, default_specification=DRAFT202012)) + ) + return Registry().with_resources(resources) + + +_REGISTRY: Registry | None = None + + +def get_registry() -> Registry: + global _REGISTRY + if _REGISTRY is None: + _REGISTRY = build_registry() + return _REGISTRY + + +def get_validator(artifact_type: str) -> Draft202012Validator: + schema_name = ARTIFACT_SCHEMAS.get(artifact_type) + if not schema_name: + raise ValidationError(f"Unknown artifact type: {artifact_type}") + schema_path = schemas_dir() / schema_name + schema = _load_schema(schema_path) + return Draft202012Validator(schema, registry=get_registry()) + + +def validate_schema(data: dict[str, Any], artifact_type: str) -> list[str]: + validator = get_validator(artifact_type) + errors = sorted(validator.iter_errors(data), key=lambda e: list(e.path)) + return [e.message for e in errors] diff --git a/python/pcs_core/validate_pcs_core.py b/python/pcs_core/validate_pcs_core.py new file mode 100644 index 0000000..1338648 --- /dev/null +++ b/python/pcs_core/validate_pcs_core.py @@ -0,0 +1,167 @@ +"""PCS core semantic validation helpers.""" + +import re +from typing import Any + +from pcs_core.status import ARTIFACT_STATUSES, TRACE_CERTIFICATE_STATUSES + +_ZERO_COMMIT_RE = re.compile(r"^0+$") + +CERTIFIED_CLAIM_STATUSES = frozenset( + { + "CertificateChecked", + "ProofChecked", + "RuntimeChecked", + } +) + +IMPORT_READY_VERIFICATION_STATUSES = frozenset( + { + "ProofChecked", + "CertificateChecked", + "RuntimeChecked", + } +) +def _is_zero_source_commit(value: str) -> bool: + return bool(_ZERO_COMMIT_RE.match(value.strip())) + + +def _local_dev_enabled(obj: dict[str, Any], inherited: bool) -> bool: + if inherited: + return True + if obj.get("local_dev") is True: + return True + return False + + +def _check_source_commits( + obj: Any, + path: str, + errors: list[str], + *, + inherited_local_dev: bool = False, +) -> None: + if isinstance(obj, dict): + local_dev = _local_dev_enabled(obj, inherited_local_dev) + commit = obj.get("source_commit") + if isinstance(commit, str) and _is_zero_source_commit(commit) and not local_dev: + errors.append( + f"{path or 'root'}: zero source_commit not allowed without local_dev=true" + ) + for key, value in obj.items(): + child = f"{path}.{key}" if path else key + _check_source_commits(value, child, errors, inherited_local_dev=local_dev) + elif isinstance(obj, list): + for index, item in enumerate(obj): + _check_source_commits( + item, + f"{path}[{index}]", + errors, + inherited_local_dev=inherited_local_dev, + ) + + +def _validate_status_fields(obj: Any, path: str, errors: list[str]) -> None: + if isinstance(obj, dict): + if "check_id" not in obj: + status = obj.get("status") + if isinstance(status, str): + if "certificate_id" in obj: + if status not in TRACE_CERTIFICATE_STATUSES: + errors.append(f"{path}: invalid TraceCertificate status {status!r}") + elif status not in ARTIFACT_STATUSES: + errors.append(f"{path}: unknown status {status!r}") + for key, value in obj.items(): + child = f"{path}.{key}" if path else key + _validate_status_fields(value, child, errors) + elif isinstance(obj, list): + for index, item in enumerate(obj): + _validate_status_fields(item, f"{path}[{index}]", errors) + + +def _validate_science_claim_bundle(data: dict[str, Any]) -> list[str]: + errors: list[str] = [] + + assumption_set = data.get("assumption_set") + if not isinstance(assumption_set, dict): + errors.append("ScienceClaimBundle.v0 requires assumption_set") + else: + assumptions = assumption_set.get("assumptions") + if not assumptions: + errors.append("ScienceClaimBundle.v0 requires non-empty assumption_set.assumptions") + + receipts = data.get("runtime_receipts") + if not isinstance(receipts, list) or len(receipts) == 0: + errors.append("ScienceClaimBundle.v0 requires non-empty runtime_receipts") + + claim = data.get("claim_artifact") + if isinstance(claim, dict): + ref = claim.get("assumption_set_ref") + if not ref or not str(ref).strip(): + errors.append("claim_artifact requires non-empty assumption_set_ref") + elif isinstance(assumption_set, dict): + if ref != assumption_set.get("assumption_set_id"): + errors.append( + "claim_artifact.assumption_set_ref must match assumption_set.assumption_set_id" + ) + + certificates = data.get("certificates") + if not isinstance(certificates, list): + certificates = [] + + claim_status = str(claim.get("status") or "") if isinstance(claim, dict) else "" + if claim_status in CERTIFIED_CLAIM_STATUSES and len(certificates) == 0: + errors.append("certified ScienceClaimBundle requires at least one TraceCertificate") + + if isinstance(receipts, list): + for receipt in receipts: + if not isinstance(receipt, dict): + continue + r_hash = receipt.get("trace_hash") + for cert in certificates: + if not isinstance(cert, dict): + continue + c_status = str(cert.get("status") or "") + if c_status and c_status not in TRACE_CERTIFICATE_STATUSES: + errors.append( + f"TraceCertificate {cert.get('certificate_id')}: " + f"invalid status {c_status!r}" + ) + c_hash = cert.get("trace_hash") + if r_hash and c_hash and r_hash != c_hash: + errors.append( + f"trace_hash mismatch: receipt {receipt.get('receipt_id')} " + f"({r_hash}) vs certificate {cert.get('certificate_id')} ({c_hash})" + ) + + return errors + + +def _validate_verification_result(data: dict[str, Any]) -> list[str]: + errors: list[str] = [] + checks = data.get("checks") + if not isinstance(checks, list): + return errors + has_failed = any( + isinstance(check, dict) and check.get("status") == "failed" for check in checks + ) + top_status = str(data.get("status") or "") + if has_failed and top_status in IMPORT_READY_VERIFICATION_STATUSES: + errors.append( + "VerificationResult.v0 with failed checks cannot use import-ready status " + f"{top_status!r} (Scientific Memory import contract)" + ) + return errors + + +def _validate_signed_bundle(data: dict[str, Any]) -> list[str]: + errors: list[str] = [] + scb = data.get("science_claim_bundle") + if isinstance(scb, dict): + errors.extend(_validate_science_claim_bundle(scb)) + vr = data.get("verification_result") + if isinstance(vr, dict): + _validate_status_fields(vr, "verification_result", errors) + errors.extend(_validate_verification_result(vr)) + return errors + diff --git a/python/pcs_core/validate_pf_core.py b/python/pcs_core/validate_pf_core.py new file mode 100644 index 0000000..b1d335c --- /dev/null +++ b/python/pcs_core/validate_pf_core.py @@ -0,0 +1,367 @@ +"""PF-Core semantic validation and fixture harness.""" + +import json +from pathlib import Path +from typing import Any + +from pcs_core.paths import repo_root +from pcs_core.registry_data import PF_CORE_CLAIM_CLASSES +from pcs_core.validate_detect import ARTIFACT_SCHEMAS, ValidationError, detect_artifact_type + +_PF_CORE_ARTIFACT_TYPES = frozenset( + key for key in ARTIFACT_SCHEMAS if key.startswith("PFCore") or key == "ToolUseTrace.v0" +) + +LEAN_CHECK_RESULT_STATUSES = frozenset( + { + "DecidersPassed", + "LeanProofChecked", + "ReplayValidated", + "Rejected", + "Stale", + } +) +def _validate_pfcore_claim_class(data: dict[str, Any], path: str, errors: list[str]) -> None: + claim_class = data.get("claim_class") + if not isinstance(claim_class, str): + return + if claim_class not in PF_CORE_CLAIM_CLASSES: + errors.append(f"{path}: invalid claim_class {claim_class!r}") + return + if claim_class == "LeanKernelChecked" and not data.get("proof_ref"): + errors.append( + f"{path}: claim_class LeanKernelChecked requires proof_ref (ClaimClassOverclaim)" + ) + if claim_class == "LeanKernelChecked" and not data.get("proof_term_ref"): + errors.append( + f"{path}: claim_class LeanKernelChecked requires proof_term_ref (ClaimClassOverclaim)" + ) + if claim_class == "LeanKernelChecked" and data.get("lean_proof_checked") is not True: + errors.append( + f"{path}: claim_class LeanKernelChecked requires lean_proof_checked=true" + ) + + +def _validate_pfcore_trace(data: dict[str, Any]) -> list[str]: + from pcs_core.pf_core_contract import validate_trace_contract_binding + from pcs_core.pf_core_runtime import validate_pfcore_trace_hash_chain + + errors: list[str] = [] + _validate_pfcore_claim_class(data, "root", errors) + errors.extend(validate_trace_contract_binding(data)) + errors.extend(validate_pfcore_trace_hash_chain(data)) + return errors + + +def _validate_pfcore_certificate(data: dict[str, Any]) -> list[str]: + from pcs_core.lean_catalog import PF_CORE_CONCRETE_PROOF_THEOREMS + from pcs_core.pf_core_contract import DEFAULT_TRACE_SAFE_CONTRACT_ID + from pcs_core.registry_data import enforce_assumption_declared, registry_entries + + errors: list[str] = [] + _validate_pfcore_claim_class(data, "root", errors) + claim_class = data.get("claim_class") + lean_proof_checked = data.get("lean_proof_checked") is True + if lean_proof_checked and not data.get("proof_term_ref"): + errors.append("root: lean_proof_checked requires proof_term_ref") + if lean_proof_checked: + build = data.get("lean_build_status") + if not isinstance(build, dict) or build.get("ok") is not True: + errors.append("root: lean_proof_checked requires lean_build_status.ok=true") + theorems = data.get("theorems_checked") + if isinstance(theorems, list): + theorem_set = {str(item) for item in theorems} + missing = PF_CORE_CONCRETE_PROOF_THEOREMS - theorem_set + if missing: + errors.append( + "root: lean_proof_checked theorems_checked missing " + f"{sorted(missing)!r}" + ) + obligations = data.get("obligations") + if isinstance(obligations, list): + required = { + "concrete_trace_safe", + "concrete_trace_safe_prop", + "concrete_allowed_events_allowed", + } + passed = { + str(item.get("theorem")) + for item in obligations + if isinstance(item, dict) and item.get("passed") is True + } + missing_obligations = required - passed + if missing_obligations: + errors.append( + "root: lean_proof_checked obligations missing passed proofs for " + f"{sorted(missing_obligations)!r}" + ) + if claim_class == "LeanKernelChecked" and not lean_proof_checked: + errors.append("root: claim_class LeanKernelChecked requires lean_proof_checked=true") + if claim_class == "LeanKernelChecked": + env_hash = data.get("lean_environment_hash") + if not isinstance(env_hash, str) or not env_hash.startswith("sha256:"): + errors.append("root: claim_class LeanKernelChecked requires lean_environment_hash") + default_ref = str(data.get("default_contract_ref") or "") + semantics = data.get("contract_semantics_checked") + has_semantics = isinstance(semantics, dict) and ( + bool(semantics.get("lean")) or bool(semantics.get("runtime")) + ) + if default_ref != DEFAULT_TRACE_SAFE_CONTRACT_ID and not has_semantics: + errors.append( + "root: claim_class LeanKernelChecked requires contract_refs or " + f"default_contract_ref {DEFAULT_TRACE_SAFE_CONTRACT_ID!r}" + ) + errors.extend(enforce_assumption_declared(data, registry_entries().get("PFCoreCertificate.v0"))) + return errors + + +def _validate_lean_check_result(data: dict[str, Any]) -> list[str]: + errors: list[str] = [] + claim_class = data.get("claim_class") + if isinstance(claim_class, str) and claim_class not in PF_CORE_CLAIM_CLASSES: + errors.append(f"root: invalid claim_class {claim_class!r}") + status = str(data.get("status") or "") + lean_proof_checked = data.get("lean_proof_checked") is True + if status == "LeanProofChecked" and claim_class != "LeanKernelChecked": + errors.append("root: status LeanProofChecked requires claim_class LeanKernelChecked") + if status == "ReplayValidated" and claim_class != "ReplayValidated": + errors.append("root: status ReplayValidated requires claim_class ReplayValidated") + if status == "LeanProofChecked" and not lean_proof_checked: + errors.append("root: status LeanProofChecked requires lean_proof_checked=true") + if claim_class == "LeanKernelChecked" and status != "LeanProofChecked": + errors.append("root: claim_class LeanKernelChecked requires status LeanProofChecked") + cert = data.get("certificate") + if isinstance(cert, dict): + errors.extend(_validate_pfcore_certificate(cert)) + return errors + + +def iter_pf_core_example_dirs(kind: str) -> list[Path]: + root = repo_root() / "examples" / f"pf-core-{kind}" + if not root.is_dir(): + return [] + return sorted(path for path in root.iterdir() if path.is_dir()) + + +def load_pf_core_fixture_manifest(case_dir: Path) -> dict[str, Any]: + manifest_path = case_dir / "manifest.json" + if not manifest_path.is_file(): + raise ValidationError(f"Missing manifest.json in {case_dir}") + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + if not isinstance(manifest, dict): + raise ValidationError(f"manifest.json root must be an object in {case_dir}") + return manifest + + +def check_pf_core_valid_fixtures() -> None: + from pcs_core.pf_core_replay import replay_trace + from pcs_core.validate_semantics import validate_file + + for case_dir in iter_pf_core_example_dirs("valid"): + manifest = None + manifest_path = case_dir / "manifest.json" + if manifest_path.is_file(): + manifest = load_pf_core_fixture_manifest(case_dir) + for path in sorted(case_dir.glob("*.json")): + if path.name == "manifest.json": + continue + if path.name == "tool_use_trace.json" and (case_dir / "pfcore_trace.json").is_file(): + continue + validate_file(path) + if manifest and manifest.get("replay_required"): + trace_path = case_dir / str(manifest.get("trace_file") or "trace.json") + if trace_path.is_file(): + result = replay_trace(trace_path) + if not result.match: + raise ValidationError( + f"Replay failed for {case_dir}: {result.diffs!r}" + ) + + +def check_pf_core_invalid_fixtures() -> None: + from pcs_core.pf_core_contract import validate_trace_contracts + from pcs_core.pf_core_runtime import ( + DroppedDeniedEvent, + HandoffAuthorityExpansion, + MissingPrincipal, + UnknownCapability, + UnknownEffect, + compile_runtime_observation_to_event, + compile_tool_use_trace_to_pfcore_trace, + validate_denied_events_preserved, + validate_handoff_authority, + validate_pfcore_trace_hash_chain, + ) + + for case_dir in iter_pf_core_example_dirs("invalid"): + manifest = load_pf_core_fixture_manifest(case_dir) + expected_error = str(manifest["expected_error"]) + must_fail_at = str(manifest["must_fail_at"]) + + if must_fail_at == "runtime_to_pfcore_event": + observation = json.loads((case_dir / "observation.json").read_text(encoding="utf-8")) + try: + compile_runtime_observation_to_event(observation) + except (UnknownCapability, UnknownEffect, MissingPrincipal) as exc: + if exc.code != expected_error: + raise ValidationError( + f"{case_dir}: expected {expected_error!r}, got {exc.code!r}" + ) from exc + else: + raise ValidationError(f"Expected {case_dir} to fail at {must_fail_at}") + continue + + if must_fail_at == "validate_pfcore_trace_hash_chain": + trace = json.loads((case_dir / "trace.json").read_text(encoding="utf-8")) + errors = validate_pfcore_trace_hash_chain(trace) + if not any(expected_error in err for err in errors): + raise ValidationError( + f"Expected {case_dir} to fail with {expected_error!r}, got {errors!r}" + ) + continue + + if must_fail_at == "validate_denied_events_preserved": + tool_use_trace = json.loads( + (case_dir / "tool_use_trace.json").read_text(encoding="utf-8") + ) + pfcore_trace = json.loads((case_dir / "pfcore_trace.json").read_text(encoding="utf-8")) + try: + validate_denied_events_preserved(tool_use_trace, pfcore_trace) + except DroppedDeniedEvent as exc: + if exc.code != expected_error: + raise ValidationError( + f"{case_dir}: expected {expected_error!r}, got {exc.code!r}" + ) from exc + else: + raise ValidationError(f"Expected {case_dir} to fail at {must_fail_at}") + continue + + if must_fail_at == "validate_denied_observations_preserved": + from pcs_core.pf_core_runtime import validate_denied_observations_preserved + + observations = [ + json.loads(path.read_text(encoding="utf-8")) + for path in sorted(case_dir.glob("observation*.json")) + ] + pfcore_trace = json.loads((case_dir / "pfcore_trace.json").read_text(encoding="utf-8")) + events = pfcore_trace.get("events") + if not isinstance(events, list): + raise ValidationError(f"{case_dir}: pfcore_trace.json missing events array") + try: + validate_denied_observations_preserved(observations, events) + except DroppedDeniedEvent as exc: + if exc.code != expected_error: + raise ValidationError( + f"{case_dir}: expected {expected_error!r}, got {exc.code!r}" + ) from exc + else: + raise ValidationError(f"Expected {case_dir} to fail at {must_fail_at}") + continue + + if must_fail_at == "compile_runtime_observations_to_pfcore_trace": + from pcs_core.pf_core_runtime import compile_runtime_observations_to_pfcore_trace + + observations = [ + json.loads(path.read_text(encoding="utf-8")) + for path in sorted(case_dir.glob("observation*.json")) + ] + try: + compile_runtime_observations_to_pfcore_trace(observations) + except DroppedDeniedEvent as exc: + if exc.code != expected_error: + raise ValidationError( + f"{case_dir}: expected {expected_error!r}, got {exc.code!r}" + ) from exc + else: + raise ValidationError(f"Expected {case_dir} to fail at {must_fail_at}") + continue + + if must_fail_at == "validate_semantics": + from pcs_core.validate_semantics import validate_semantics + + artifact_file = str(manifest.get("artifact_file") or "trace.json") + artifact_type = str(manifest.get("artifact_type") or "") + data = json.loads((case_dir / artifact_file).read_text(encoding="utf-8")) + detected = artifact_type or detect_artifact_type(data) + if not detected: + raise ValidationError(f"{case_dir}: could not detect artifact type") + semantic_errors = validate_semantics(data, detected) + if not any(expected_error in err for err in semantic_errors): + raise ValidationError( + f"Expected {case_dir} to fail with {expected_error!r}, got {semantic_errors!r}" + ) + continue + + if must_fail_at == "check_pfcore_trace_lean_semantics": + from pcs_core.lean_check import check_pfcore_trace_lean_semantics + + trace = json.loads((case_dir / "trace.json").read_text(encoding="utf-8")) + issues = check_pfcore_trace_lean_semantics(trace) + if not any(issue.code == expected_error for issue in issues): + raise ValidationError( + f"Expected {case_dir} to fail with {expected_error!r}, got " + f"{[issue.code for issue in issues]!r}" + ) + continue + + if must_fail_at == "validate_handoff_authority": + handoff = json.loads((case_dir / "handoff.json").read_text(encoding="utf-8")) + try: + validate_handoff_authority(handoff) + except HandoffAuthorityExpansion as exc: + if exc.code != expected_error: + raise ValidationError( + f"{case_dir}: expected {expected_error!r}, got {exc.code!r}" + ) from exc + else: + raise ValidationError(f"Expected {case_dir} to fail at {must_fail_at}") + continue + + if must_fail_at == "compile_tool_use_trace_to_pfcore_trace": + tool_use_trace = json.loads( + (case_dir / "tool_use_trace.json").read_text(encoding="utf-8") + ) + try: + compile_tool_use_trace_to_pfcore_trace(tool_use_trace) + except HandoffAuthorityExpansion as exc: + if exc.code != expected_error: + raise ValidationError( + f"{case_dir}: expected {expected_error!r}, got {exc.code!r}" + ) from exc + else: + raise ValidationError(f"Expected {case_dir} to fail at {must_fail_at}") + continue + + if must_fail_at == "validate_trace_contracts": + trace = json.loads((case_dir / "trace.json").read_text(encoding="utf-8")) + contracts_dir = case_dir / "contracts" + contracts = { + str(data["contract_id"]): data + for data in ( + json.loads(path.read_text(encoding="utf-8")) + for path in sorted(contracts_dir.glob("*.json")) + ) + } + issues = validate_trace_contracts(trace, contracts) + if not any(issue.code == expected_error for issue in issues): + raise ValidationError( + f"Expected {case_dir} to fail with {expected_error!r}, got " + f"{[issue.code for issue in issues]!r}" + ) + continue + + if must_fail_at == "validate_tenant_isolation": + from pcs_core.pf_core_runtime import validate_tenant_isolation + + trace = json.loads((case_dir / "trace.json").read_text(encoding="utf-8")) + errors = validate_tenant_isolation(trace) + if not any(expected_error in err for err in errors): + raise ValidationError( + f"Expected {case_dir} to fail with {expected_error!r}, got {errors!r}" + ) + continue + + raise ValidationError(f"Unknown must_fail_at {must_fail_at!r} in {case_dir}") + + + diff --git a/python/pcs_core/validate_semantics.py b/python/pcs_core/validate_semantics.py new file mode 100644 index 0000000..373b86d --- /dev/null +++ b/python/pcs_core/validate_semantics.py @@ -0,0 +1,384 @@ +"""Semantic validation orchestration and public validate API.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + +from pcs_core.paths import examples_dir as default_examples_dir +from pcs_core.paths import repo_root, schemas_dir +from pcs_core.registry_data import PF_CORE_CLAIM_CLASSES +from pcs_core.status import ARTIFACT_STATUSES, TRACE_CERTIFICATE_STATUSES + +from pcs_core.lean_validate import ( + validate_lean_check_result_semantics, + validate_proof_obligation_semantics, +) +from pcs_core.protocol_validate import ( + validate_artifact_registry_semantics, + validate_conformance_report_semantics, + validate_handoff_manifest_semantics, + validate_release_chain_validation_result_semantics, + validate_release_manifest_fixture_refs, + validate_release_manifest_semantics, +) +from pcs_core.tool_use_validate import ( + validate_tool_use_certificate_semantics, + validate_tool_use_trace_semantics, + validate_workflow_profile_semantics, +) +from pcs_core.computation_validate import ( + validate_computation_run_receipt_semantics, + validate_computation_witness_semantics, + validate_dataset_receipt_semantics, + validate_environment_receipt_semantics, + validate_result_artifact_semantics, +) +from pcs_core.benchmark_validate import ( + validate_benchmark_case_semantics, + validate_benchmark_metric_registry_semantics, + validate_benchmark_registry_semantics, + validate_benchmark_report_semantics, + validate_benchmark_run_semantics, + validate_benchmark_suite_manifest_semantics, + validate_benchmark_task_semantics, +) +from pcs_core.validate_detect import ( + ARTIFACT_SCHEMAS, + ValidationError, + detect_artifact_type, + get_validator, + validate_schema, + _load_schema, +) +from pcs_core.validate_pcs_core import ( + _check_source_commits, + _validate_science_claim_bundle, + _validate_signed_bundle, + _validate_status_fields, + _validate_verification_result, +) +from pcs_core.validate_pf_core import ( + _PF_CORE_ARTIFACT_TYPES, + _validate_lean_check_result, + _validate_pfcore_certificate, + _validate_pfcore_claim_class, + _validate_pfcore_trace, +) + +def validate_semantics(data: dict[str, Any], artifact_type: str) -> list[str]: + errors: list[str] = [] + + if artifact_type == "ArtifactRegistry.v0": + errors.extend(validate_artifact_registry_semantics(data)) + return errors + + if artifact_type == "ComponentReleaseFragment.v0": + _check_source_commits(data, "", errors) + return errors + + if artifact_type == "MigrationReport.v0": + return errors + + if artifact_type == "ReleaseManifest.v0": + errors.extend(validate_release_manifest_semantics(data)) + return errors + + if artifact_type == "HandoffManifest.v0": + errors.extend(validate_handoff_manifest_semantics(data)) + return errors + + if artifact_type == "ConformanceReport.v0": + errors.extend(validate_conformance_report_semantics(data)) + return errors + + if artifact_type == "WorkflowProfile.v0": + errors.extend(validate_workflow_profile_semantics(data)) + return errors + + if artifact_type == "ToolUseTrace.v0": + errors.extend(validate_tool_use_trace_semantics(data)) + return errors + + if artifact_type == "ToolUseCertificate.v0": + errors.extend(validate_tool_use_certificate_semantics(data)) + return errors + + if artifact_type == "DatasetReceipt.v0": + errors.extend(validate_dataset_receipt_semantics(data)) + return errors + + if artifact_type == "EnvironmentReceipt.v0": + errors.extend(validate_environment_receipt_semantics(data)) + return errors + + if artifact_type == "ComputationRunReceipt.v0": + errors.extend(validate_computation_run_receipt_semantics(data)) + return errors + + if artifact_type == "ResultArtifact.v0": + errors.extend(validate_result_artifact_semantics(data)) + return errors + + if artifact_type == "ComputationWitness.v0": + errors.extend(validate_computation_witness_semantics(data)) + return errors + + if artifact_type == "ProofObligation.v0": + errors.extend(validate_proof_obligation_semantics(data)) + return errors + + if artifact_type == "LeanCheckResult.v0": + if data.get("artifact_type") == "LeanCheckResult.v0": + errors.extend(_validate_lean_check_result(data)) + elif "check_id" in data: + errors.extend(validate_lean_check_result_semantics(data)) + else: + errors.append( + "LeanCheckResult.v0: expected PF-Core artifact_type or PCS check_id shape" + ) + return errors + + + if artifact_type == "BenchmarkMetricRegistry.v0": + errors.extend(validate_benchmark_metric_registry_semantics(data)) + return errors + + if artifact_type == "BenchmarkRegistry.v0": + errors.extend(validate_benchmark_registry_semantics(data)) + return errors + + if artifact_type == "BenchmarkSuiteManifest.v0": + errors.extend(validate_benchmark_suite_manifest_semantics(data)) + return errors + + if artifact_type == "BenchmarkTask.v0": + errors.extend(validate_benchmark_task_semantics(data)) + return errors + + if artifact_type == "BenchmarkCase.v0": + errors.extend(validate_benchmark_case_semantics(data)) + return errors + + if artifact_type == "BenchmarkRun.v0": + errors.extend(validate_benchmark_run_semantics(data)) + return errors + + if artifact_type == "BenchmarkReport.v0": + errors.extend(validate_benchmark_report_semantics(data)) + return errors + + if artifact_type == "MetricSummary.v0": + return errors + + if artifact_type == "BenchmarkArtifactRef.v0": + from pcs_core.benchmark_validate import validate_benchmark_artifact_ref_semantics + + errors.extend(validate_benchmark_artifact_ref_semantics(data)) + return errors + + if artifact_type == "PcsBenchIngest.v0": + from pcs_core.benchmark_ingest import validate_pcs_bench_ingest_semantics + + errors.extend(validate_pcs_bench_ingest_semantics(data)) + return errors + + if artifact_type == "ConformanceRun.v0": + return errors + + if artifact_type == "FailureCaseManifest.v0": + return errors + + if artifact_type == "FailureLocalizationResult.v0": + return errors + + if artifact_type == "CoverageReport.v0": + return errors + + if artifact_type == "ExplainQualityReport.v0": + return errors + + if artifact_type == "ProfileCoverageReport.v0": + return errors + + if artifact_type == "ReleaseChainValidationResult.v0": + errors.extend(validate_release_chain_validation_result_semantics(data)) + checks = data.get("checks") + if isinstance(checks, list): + for index, check in enumerate(checks): + if isinstance(check, dict): + _validate_status_fields(check, f"checks[{index}]", errors) + return errors + + _check_source_commits(data, "", errors) + _validate_status_fields(data, "", errors) + + if artifact_type == "ClaimArtifact.v0": + ref = data.get("assumption_set_ref") + if not ref or not str(ref).strip(): + errors.append("ClaimArtifact.v0 requires non-empty assumption_set_ref") + + if artifact_type == "ScienceClaimBundle.v0": + errors.extend(_validate_science_claim_bundle(data)) + + if artifact_type == "VerificationResult.v0": + errors.extend(_validate_verification_result(data)) + + if artifact_type == "SignedScienceClaimBundle.v0": + errors.extend(_validate_signed_bundle(data)) + + if artifact_type == "TraceCertificate.v0": + status = str(data.get("status") or "") + if status and status not in TRACE_CERTIFICATE_STATUSES: + errors.append(f"TraceCertificate.v0 invalid status {status!r}") + + if artifact_type == "PFCoreTrace.v0": + errors.extend(_validate_pfcore_trace(data)) + + if artifact_type == "PFCoreContract.v0": + from pcs_core.pf_core_contract import validate_pfcore_contract_semantics + + errors.extend(validate_pfcore_contract_semantics(data)) + + if artifact_type == "PFCoreCertificate.v0": + errors.extend(_validate_pfcore_certificate(data)) + + if artifact_type == "LeanCheckResult.v0": + errors.extend(_validate_lean_check_result(data)) + + if artifact_type in _PF_CORE_ARTIFACT_TYPES and artifact_type not in { + "PFCoreTrace.v0", + "PFCoreCertificate.v0", + "LeanCheckResult.v0", + "ToolUseTrace.v0", + }: + _validate_pfcore_claim_class(data, "root", errors) + + return errors + + +def validate_artifact(data: dict[str, Any], artifact_type: str | None = None) -> None: + artifact_type = artifact_type or detect_artifact_type(data) + if not artifact_type: + raise ValidationError("Could not detect artifact type from JSON content") + + schema_errors = validate_schema(data, artifact_type) + semantic_errors = validate_semantics(data, artifact_type) + all_errors = schema_errors + semantic_errors + if all_errors: + raise ValidationError( + f"Validation failed for {artifact_type}", + errors=all_errors, + ) + + +def validate_file(path: Path | str) -> str: + path = Path(path) + with path.open(encoding="utf-8") as f: + data = json.load(f) + if not isinstance(data, dict): + raise ValidationError("Artifact root must be a JSON object") + artifact_type = detect_artifact_type(data) + if not artifact_type: + raise ValidationError(f"Could not detect artifact type in {path}") + validate_artifact(data, artifact_type) + if artifact_type == "ReleaseManifest.v0": + ref_errors = validate_release_manifest_fixture_refs(data, path.parent) + if ref_errors: + raise ValidationError( + f"Validation failed for {artifact_type}", + errors=ref_errors, + ) + return artifact_type + + +def _is_valid_example(path: Path) -> bool: + if "tool-use-release-invalid" in path.parts or "computation-release-invalid" in path.parts: + return False + return path.suffix == ".json" and ".valid." in path.name + + +def iter_example_json_files(examples_dir: Path) -> list[Path]: + return sorted(p for p in examples_dir.rglob("*.json") if p.is_file()) + + +def check_all_schemas() -> None: + from jsonschema import Draft202012Validator + + from pcs_core.validate_detect import ARTIFACT_SCHEMAS, get_validator, _load_schema + from pcs_core.paths import schemas_dir + + for artifact_type, schema_name in ARTIFACT_SCHEMAS.items(): + schema_path = schemas_dir() / schema_name + schema = _load_schema(schema_path) + Draft202012Validator.check_schema(schema) + get_validator(artifact_type) + + +def check_valid_examples(examples_dir: Path | None = None) -> None: + examples_dir = examples_dir or default_examples_dir() + for path in iter_example_json_files(examples_dir): + if _is_valid_example(path): + validate_file(path) + for name in ( + "release_manifest.valid.json", + "handoff_manifest.valid.json", + "release_chain_validation_result.valid.json", + "artifact_registry.valid.json", + "migration_report.valid.json", + "proof_obligation.valid.json", + "lean_check_result.valid.json", + "benchmark_registry.valid.json", + "benchmark_metric_registry.valid.json", + ): + validate_file(examples_dir / name) + + benchmarks_examples = examples_dir / "benchmarks" + if benchmarks_examples.is_dir(): + for path in sorted(benchmarks_examples.rglob("*.valid.json")): + validate_file(path) + compat = benchmarks_examples / "compatibility" + if compat.is_dir(): + for path in sorted(compat.glob("*.normalized.json")) + sorted( + compat.glob("*.pcs_bench_ingest.normalized.json"), + ): + validate_file(path) + + producer_examples = examples_dir / "benchmark" + if producer_examples.is_dir(): + for path in sorted(producer_examples.glob("*.valid.json")): + validate_file(path) + + ingest_examples = examples_dir / "benchmark_ingest" + if ingest_examples.is_dir(): + for path in sorted(ingest_examples.glob("*.pcs_bench_ingest.valid.json")): + validate_file(path) + + check_pf_core_valid_fixtures() + +def check_invalid_examples(examples_dir: Path | None = None) -> None: + examples_dir = examples_dir or default_examples_dir() + invalid_cases: dict[str, str | None] = { + "invalid_unknown_status.json": "RuntimeReceipt.v0", + "invalid_missing_assumption_set.json": "ScienceClaimBundle.v0", + "invalid_mismatched_trace_hash.json": "ScienceClaimBundle.v0", + "invalid_zero_source_commit.release.json": "RuntimeReceipt.v0", + "labtrust/invalid_singular_runtime_receipt_bundle.json": "ScienceClaimBundle.v0", + "labtrust/invalid_signed_schema_version_artifact_name.json": "SignedScienceClaimBundle.v0", + "labtrust/invalid_failed_verification_result.json": "VerificationResult.v0", + "labtrust/invalid_missing_trace_certificate.json": "ScienceClaimBundle.v0", + } + for filename, artifact_type in invalid_cases.items(): + path = examples_dir / filename + data = json.loads(path.read_text(encoding="utf-8")) + detected = detect_artifact_type(data) + use_type = artifact_type or detected + if not use_type: + raise ValidationError(f"Could not detect type for {filename}") + schema_errors = validate_schema(data, use_type) + semantic_errors = validate_semantics(data, use_type) + if not schema_errors and not semantic_errors: + raise ValidationError(f"Expected {filename} to fail validation") + check_pf_core_invalid_fixtures() diff --git a/python/scripts/gen_deferred_pf_core_fixtures.py b/python/scripts/gen_deferred_pf_core_fixtures.py new file mode 100644 index 0000000..131c14e --- /dev/null +++ b/python/scripts/gen_deferred_pf_core_fixtures.py @@ -0,0 +1,448 @@ +"""Generate deferred PF-Core invalid fixtures and shared hash vectors.""" + +from __future__ import annotations + +import json +import shutil +from pathlib import Path + +from pcs_core.hash import canonical_hash, canonicalize_for_hash +from pcs_core.pf_core_contract import default_trace_safe_contract +from pcs_core.pf_core_runtime import ( + GENESIS_HASH, + compute_event_hash, + compute_trace_hash, +) + +ROOT = Path(__file__).resolve().parents[2] +INVALID = ROOT / "examples" / "pf-core-invalid" +VALID = ROOT / "examples" / "pf-core-valid" +HASH_ROOT = ROOT / "python" / "tests" / "hash_vectors" / "pf_core" + + +def write_json(path: Path, data: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + + +def write_manifest(case_dir: Path, *, expected_error: str, must_fail_at: str, **extra: object) -> None: + payload = {"expected_error": expected_error, "must_fail_at": must_fail_at, **extra} + write_json(case_dir / "manifest.json", payload) + + +def base_event(*, decision: str = "allow", contract_refs: list[str] | None = None) -> dict: + return { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-file-read-1", + "trace_id": "trace-file-read-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": ["agent"], + "capabilities": [ + "cap:file-read", + "cap:email-send", + "cap:handoff", + "cap:mcp-invoke", + ], + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*", + }, + "effects": [{"effect_kind": "file.read"}], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a", + } + ], + "writes": [], + "input_hash": "sha256:" + "a" * 64, + "output_hash": "sha256:" + "b" * 64, + }, + "decision": decision, + "decision_reason": "authorized" if decision == "allow" else "denied", + "contract_refs": list(contract_refs or []), + "evidence_refs": [], + "previous_event_hash": GENESIS_HASH, + "event_hash": GENESIS_HASH, + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": GENESIS_HASH, + } + + +def finalize_trace(trace: dict) -> dict: + events = trace["events"] + previous = GENESIS_HASH + for event in events: + event["previous_event_hash"] = previous + event_hash = compute_event_hash(event) + event["event_hash"] = event_hash + event["signature_or_digest"] = event_hash + previous = event_hash + trace["trace_hash"] = compute_trace_hash(trace) + trace["signature_or_digest"] = trace["trace_hash"] + return trace + + +def base_trace(**kwargs: object) -> dict: + trace = { + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-file-read-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [base_event()], + "trace_hash": GENESIS_HASH, + "policy_hash": GENESIS_HASH, + "contract_hash": GENESIS_HASH, + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": GENESIS_HASH, + } + trace.update(kwargs) + return finalize_trace(trace) + + +def strict_contract(**overrides: object) -> dict: + contract = { + "schema_version": "v0", + "artifact_type": "PFCoreContract.v0", + "contract_id": "contract-file-read-v0", + "name": "Strict file read contract", + "pre": { + "require_capability": "cap:file-read", + "require_effect": "file.read", + "require_tenant_match": True, + "require_policy_ref": "policy/default.v0", + "require_evidence_ref": "evidence/run-1", + }, + "post": { + "require_decision": "allow", + "require_event_safe": True, + }, + "invariant": {"require_trace_safe": True}, + "signature_or_digest": GENESIS_HASH, + } + contract.update(overrides) + contract["signature_or_digest"] = canonical_hash( + {k: v for k, v in contract.items() if k != "signature_or_digest"} + ) + return contract + + +def observation(*, decision: str, event_id: str, sequence: int) -> dict: + return { + "schema_version": "v0", + "artifact_type": "PFCoreRuntimeObservation.v0", + "observation_id": f"obs-{event_id}", + "trace_id": "trace-obs-batch-1", + "event_id": event_id, + "sequence": sequence, + "observed_at": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": ["agent"], + "capabilities": ["cap:file-read"], + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*", + }, + "effects": [{"effect_kind": "file.read"}], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a", + } + ], + "writes": [], + "input_hash": "sha256:" + "a" * 64, + "output_hash": "sha256:" + "b" * 64, + }, + "decision": decision, + "decision_reason": decision, + "policy_ref": "policy/default.v0", + "evidence_refs": [], + "runtime_ref": "agent-runtime", + "previous_event_hash": GENESIS_HASH, + "payload_hash": "sha256:" + "c" * 64, + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:" + "d" * 64, + } + + +def generate_invalid_fixtures() -> None: + case = INVALID / "previous_event_hash_mismatch" + trace = base_trace() + trace = finalize_trace(trace) + trace["events"][0]["previous_event_hash"] = "sha256:" + "f" * 64 + write_json(case / "trace.json", trace) + write_manifest(case, expected_error="EventHashMismatch", must_fail_at="validate_pfcore_trace_hash_chain") + + case = INVALID / "lean_kernel_checked_without_proof_ref" + trace = base_trace(claim_class="LeanKernelChecked") + trace["events"][0]["contract_refs"] = ["contract-file-read-v0"] + trace = finalize_trace(trace) + write_json(case / "trace.json", trace) + write_manifest( + case, + expected_error="ClaimClassOverclaim", + must_fail_at="validate_semantics", + artifact_file="trace.json", + artifact_type="PFCoreTrace.v0", + ) + + case = INVALID / "lean_kernel_checked_without_contract" + trace = base_trace(claim_class="LeanKernelChecked") + write_json(case / "trace.json", trace) + write_manifest( + case, + expected_error="ContractBindingMissing", + must_fail_at="validate_semantics", + artifact_file="trace.json", + artifact_type="PFCoreTrace.v0", + ) + + case = INVALID / "lean_kernel_checked_without_proof_term_ref" + cert = { + "schema_version": "v0", + "artifact_type": "PFCoreCertificate.v0", + "certificate_id": "pfcore-cert-missing-proof-term", + "trace_hash": GENESIS_HASH, + "contract_hash": GENESIS_HASH, + "policy_hash": GENESIS_HASH, + "claim_class": "LeanKernelChecked", + "checker": "pcs-core", + "checker_version": "0.1.0", + "assumption_refs": ["docs/pf-core/assumptions.md"], + "theorems_checked": ["traceSafeD"], + "obligations": [], + "lean_build_status": {"ok": True, "target": "PFCore", "detail": "ok"}, + "lean_proof_checked": True, + "lean_environment_hash": "sha256:" + "e" * 64, + "proof_ref": "lean/PFCore/Generated/example.lean", + "disclaimer": "test fixture", + "event_count": 1, + "contract_semantics_checked": {"lean": ["trace-safe.invariant.require_trace_safe"], "runtime": []}, + "default_contract_ref": "trace-safe", + "source_repo": "https://github.com/example/pcs-core", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": GENESIS_HASH, + } + cert["signature_or_digest"] = canonical_hash(cert) + write_json(case / "certificate.json", cert) + write_manifest( + case, + expected_error="proof_term_ref", + must_fail_at="validate_semantics", + artifact_file="certificate.json", + artifact_type="PFCoreCertificate.v0", + ) + + case = INVALID / "lean_kernel_checked_with_skipped_build" + cert = dict(cert) + cert["certificate_id"] = "pfcore-cert-skipped-build" + cert["proof_term_ref"] = cert["proof_ref"] + cert["lean_build_status"] = {"ok": False, "target": "PFCore", "detail": "skipped"} + cert["signature_or_digest"] = canonical_hash({k: v for k, v in cert.items() if k != "signature_or_digest"}) + write_json(case / "certificate.json", cert) + write_manifest( + case, + expected_error="lean_build_status.ok=true", + must_fail_at="validate_semantics", + artifact_file="certificate.json", + artifact_type="PFCoreCertificate.v0", + ) + + case = INVALID / "cross_tenant_allowed_event" + if not case.is_dir(): + shutil.copytree(INVALID / "cross_tenant_leak", case) + write_manifest(case, expected_error="TenantIsolation", must_fail_at="validate_tenant_isolation") + + case = INVALID / "contract_ref_missing" + trace = base_trace() + trace["events"][0]["contract_refs"] = ["contract-missing-v0"] + trace = finalize_trace(trace) + write_json(case / "trace.json", trace) + write_manifest(case, expected_error="ContractRefMissing", must_fail_at="validate_trace_contracts") + + def contract_case( + name: str, + expected_error: str, + mutate_event, + *, + contract: dict[str, object] | None = None, + ) -> None: + case_dir = INVALID / name + contract_obj = dict(contract or strict_contract()) + event = base_event(contract_refs=[str(contract_obj["contract_id"])]) + mutate_event(event) + trace = base_trace(events=[event]) + write_json(case_dir / "trace.json", trace) + write_json(case_dir / "contracts" / "contract.json", contract_obj) + write_manifest(case_dir, expected_error=expected_error, must_fail_at="validate_trace_contracts") + + contract_case( + "contract_capability_missing", + "ContractCapabilityRequired", + lambda event: ( + event["principal"].update( + { + "roles": ["email_user"], + "capabilities": ["cap:email-send"], + } + ) + ), + contract={ + **strict_contract(), + "pre": {"require_capability": "cap:file-read"}, + "post": {}, + "invariant": {}, + }, + ) + contract_case( + "contract_effect_missing", + "ContractEffectRequired", + lambda event: event["action"].update( + { + "effects": [{"effect_kind": "file.write"}], + "capability": { + "capability_id": "cap:file-write", + "effect_kind": "file.write", + "resource_pattern": "/data/*", + }, + } + ), + contract={ + **strict_contract(), + "pre": {"require_effect": "file.read"}, + "post": {}, + "invariant": {}, + }, + ) + contract_case( + "contract_policy_ref_missing", + "ContractPolicyRefRequired", + lambda event: event.update({"contract_refs": ["contract-file-read-v0"]}), + contract={ + **strict_contract(), + "pre": {"require_policy_ref": "policy/default.v0"}, + "post": {}, + "invariant": {}, + }, + ) + contract_case( + "contract_evidence_ref_missing", + "ContractEvidenceRefRequired", + lambda event: event.update({"evidence_refs": []}), + contract={ + **strict_contract(), + "pre": {"require_evidence_ref": "evidence/run-1"}, + "post": {}, + "invariant": {}, + }, + ) + + case = INVALID / "dropped_denied_observation" + write_json(case / "observation_0.json", observation(decision="allow", event_id="ev-allow", sequence=0)) + write_json( + case / "observation_1.json", + observation(decision="deny", event_id="ev-deny", sequence=1), + ) + pfcore = base_trace( + trace_id="trace-obs-batch-1", + events=[ + finalize_trace(base_trace())["events"][0], + ], + ) + pfcore["events"][0]["event_id"] = "ev-allow" + pfcore["events"][0]["decision"] = "allow" + pfcore = finalize_trace(pfcore) + write_json(case / "pfcore_trace.json", pfcore) + write_manifest( + case, + expected_error="DroppedDeniedEvent", + must_fail_at="validate_denied_observations_preserved", + ) + + +def write_hash_vector(name: str, payload: dict, *, digest: str, canonical: str) -> None: + target = HASH_ROOT / name + target.mkdir(parents=True, exist_ok=True) + write_json(target / "input.json", payload) + (target / "canonical.txt").write_text(canonical + "\n", encoding="utf-8") + (target / "digest.txt").write_text(digest + "\n", encoding="utf-8") + + +def generate_hash_vectors() -> None: + from pcs_core.hash import canonicalize_for_hash + + event_path = VALID / "file_read_allowed" / "event.json" + trace_path = VALID / "file_read_allowed" / "trace.json" + event = json.loads(event_path.read_text(encoding="utf-8")) + trace = json.loads(trace_path.read_text(encoding="utf-8")) + event_canonical = json.dumps( + canonicalize_for_hash({k: v for k, v in event.items() if k not in ("event_hash", "signature_or_digest")}), + separators=(",", ":"), + ensure_ascii=False, + ) + trace_canonical = json.dumps( + canonicalize_for_hash({k: v for k, v in trace.items() if k not in ("trace_hash", "signature_or_digest")}), + separators=(",", ":"), + ensure_ascii=False, + ) + write_hash_vector( + "PFCoreEvent.v0", + event, + digest=compute_event_hash(event), + canonical=event_canonical, + ) + write_hash_vector( + "PFCoreTrace.v0", + trace, + digest=compute_trace_hash(trace), + canonical=trace_canonical, + ) + contract = default_trace_safe_contract() + contract_canonical = json.dumps( + canonicalize_for_hash({k: v for k, v in contract.items() if k != "signature_or_digest"}), + separators=(",", ":"), + ensure_ascii=False, + ) + write_hash_vector( + "PFCoreContract.v0", + contract, + digest=canonical_hash(contract), + canonical=contract_canonical, + ) + + +def main() -> None: + generate_invalid_fixtures() + generate_hash_vectors() + print("generated deferred PF-Core fixtures and hash vectors") + + +if __name__ == "__main__": + main() diff --git a/python/scripts/split_validate.py b/python/scripts/split_validate.py new file mode 100644 index 0000000..24d2e05 --- /dev/null +++ b/python/scripts/split_validate.py @@ -0,0 +1,154 @@ +"""Split pcs_core.validate into focused modules.""" + +from __future__ import annotations + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] / "pcs_core" +SOURCE = lines_source = ROOT / "validate.py" +# Reconstruct monolithic source from modules if validate.py is already split. +if "validate_detect import" in SOURCE.read_text(encoding="utf-8"): + merged = [] + for name in ("validate_detect.py", "validate_pcs_core.py", "validate_pf_core.py", "validate_semantics.py"): + part = (ROOT / name).read_text(encoding="utf-8") + part = part.split('from __future__ import annotations\n', 1)[-1] + merged.append(part) + # Not reversible safely; use stored line backup + backup = ROOT / "_validate_monolith_backup.py" + if not backup.is_file(): + raise SystemExit("missing validate monolith backup; restore validate.py before split") + lines = backup.read_text(encoding="utf-8").splitlines(keepends=True) +else: + backup = ROOT / "_validate_monolith_backup.py" + backup.write_text(SOURCE.read_text(encoding="utf-8"), encoding="utf-8") + lines = backup.read_text(encoding="utf-8").splitlines(keepends=True) + +IMPORT_BLOCK = lines[1:35] + +COMMON_IMPORTS = '''from pcs_core.paths import examples_dir as default_examples_dir +from pcs_core.paths import repo_root, schemas_dir +from pcs_core.registry_data import PF_CORE_CLAIM_CLASSES +from pcs_core.status import ARTIFACT_STATUSES, TRACE_CERTIFICATE_STATUSES + +from pcs_core.lean_validate import ( + validate_lean_check_result_semantics, + validate_proof_obligation_semantics, +) +from pcs_core.protocol_validate import ( + validate_artifact_registry_semantics, + validate_conformance_report_semantics, + validate_handoff_manifest_semantics, + validate_release_chain_validation_result_semantics, + validate_release_manifest_fixture_refs, + validate_release_manifest_semantics, +) +from pcs_core.tool_use_validate import ( + validate_tool_use_certificate_semantics, + validate_tool_use_trace_semantics, + validate_workflow_profile_semantics, +) +from pcs_core.computation_validate import ( + validate_computation_run_receipt_semantics, + validate_computation_witness_semantics, + validate_dataset_receipt_semantics, + validate_environment_receipt_semantics, + validate_result_artifact_semantics, +) +from pcs_core.benchmark_validate import ( + validate_benchmark_case_semantics, + validate_benchmark_metric_registry_semantics, + validate_benchmark_registry_semantics, + validate_benchmark_report_semantics, + validate_benchmark_run_semantics, + validate_benchmark_suite_manifest_semantics, + validate_benchmark_task_semantics, +) +from pcs_core.benchmark_ingest import validate_pcs_bench_ingest_semantics +''' + + +def join(parts: list[str]) -> str: + return "".join(parts) + + +detect = ( + join(IMPORT_BLOCK[:2]) + + join(IMPORT_BLOCK[2:9]) + + COMMON_IMPORTS + + join(lines[35:93]) + + join(lines[113:119]) + + join(lines[138:508]) +) + +pcs = ( + join(IMPORT_BLOCK[:2]) + + "from typing import Any\n\n" + + "from pcs_core.status import ARTIFACT_STATUSES, TRACE_CERTIFICATE_STATUSES\n\n" + + join(lines[94:109]) + + join(lines[510:653]) +) + +pf = ( + join(IMPORT_BLOCK[:2]) + + "import json\nfrom pathlib import Path\nfrom typing import Any\n\n" + + "from pcs_core.validate_detect import ValidationError, detect_artifact_type\n\n" + + join(lines[121:134]) + + join(lines[654:764]) + + join(lines[1044:1270]) +) + +sem = ( + join(IMPORT_BLOCK[:2]) + + join(IMPORT_BLOCK[2:9]) + + COMMON_IMPORTS + + "from pcs_core.validate_detect import (\n" + + " ARTIFACT_SCHEMAS,\n" + + " ValidationError,\n" + + " detect_artifact_type,\n" + + " get_validator,\n" + + " validate_schema,\n" + + " _load_schema,\n" + + ")\n" + + "from pcs_core.validate_pcs_core import (\n" + + " _check_source_commits,\n" + + " _validate_science_claim_bundle,\n" + + " _validate_signed_bundle,\n" + + " _validate_status_fields,\n" + + " _validate_verification_result,\n" + + ")\n" + + "from pcs_core.validate_pf_core import (\n" + + " _PF_CORE_ARTIFACT_TYPES,\n" + + " _validate_lean_check_result,\n" + + " _validate_pfcore_certificate,\n" + + " _validate_pfcore_claim_class,\n" + + " _validate_pfcore_trace,\n" + + ")\n\n" + + join(lines[765:1043]) + + join(lines[1270:]) +) + +(ROOT / "validate_detect.py").write_text( + '"""Artifact type detection and JSON Schema validation."""\n' + detect, + encoding="utf-8", +) +(ROOT / "validate_pcs_core.py").write_text( + '"""PCS core semantic validation helpers."""\n' + pcs, + encoding="utf-8", +) +(ROOT / "validate_pf_core.py").write_text( + '"""PF-Core semantic validation and fixture harness."""\n' + pf, + encoding="utf-8", +) +(ROOT / "validate_semantics.py").write_text( + '"""Semantic validation orchestration and public validate API."""\n' + sem, + encoding="utf-8", +) +(ROOT / "validate.py").write_text( + '"""JSON Schema and semantic validation for PCS artifacts."""\n\n' + "from pcs_core.validate_detect import * # noqa: F403\n" + "from pcs_core.validate_pcs_core import * # noqa: F403\n" + "from pcs_core.validate_pf_core import * # noqa: F403\n" + "from pcs_core.validate_semantics import * # noqa: F403\n", + encoding="utf-8", +) +print("split complete") diff --git a/python/tests/hash_vectors/pf_core/PFCoreContract.v0/canonical.txt b/python/tests/hash_vectors/pf_core/PFCoreContract.v0/canonical.txt new file mode 100644 index 0000000..2c65d6c --- /dev/null +++ b/python/tests/hash_vectors/pf_core/PFCoreContract.v0/canonical.txt @@ -0,0 +1 @@ +{"artifact_type":"PFCoreContract.v0","contract_id":"trace-safe","invariant":{"require_trace_safe":true},"name":"Trace-safe default","post":{},"pre":{},"schema_version":"v0","semantics_layer":{"require_trace_safe":"lean"}} diff --git a/python/tests/hash_vectors/pf_core/PFCoreContract.v0/digest.txt b/python/tests/hash_vectors/pf_core/PFCoreContract.v0/digest.txt new file mode 100644 index 0000000..ac54ed2 --- /dev/null +++ b/python/tests/hash_vectors/pf_core/PFCoreContract.v0/digest.txt @@ -0,0 +1 @@ +sha256:d399d3289e5438986ea1af914820bd8a5b0a6b674e7253482fc5d56c12ff763c diff --git a/python/tests/hash_vectors/pf_core/PFCoreContract.v0/input.json b/python/tests/hash_vectors/pf_core/PFCoreContract.v0/input.json new file mode 100644 index 0000000..db661ac --- /dev/null +++ b/python/tests/hash_vectors/pf_core/PFCoreContract.v0/input.json @@ -0,0 +1,15 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreContract.v0", + "contract_id": "trace-safe", + "name": "Trace-safe default", + "pre": {}, + "post": {}, + "invariant": { + "require_trace_safe": true + }, + "semantics_layer": { + "require_trace_safe": "lean" + }, + "signature_or_digest": "sha256:d399d3289e5438986ea1af914820bd8a5b0a6b674e7253482fc5d56c12ff763c" +} diff --git a/python/tests/hash_vectors/pf_core/PFCoreEvent.v0/canonical.txt b/python/tests/hash_vectors/pf_core/PFCoreEvent.v0/canonical.txt new file mode 100644 index 0000000..81c0b40 --- /dev/null +++ b/python/tests/hash_vectors/pf_core/PFCoreEvent.v0/canonical.txt @@ -0,0 +1 @@ +{"action":{"action_id":"act-1","capability":{"capability_id":"cap:file-read","effect_kind":"file.read","resource_pattern":"/data/*"},"effects":[{"effect_kind":"file.read"}],"input_hash":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","output_hash":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","reads":[{"resource_id":"res-1","tenant":"tenant-a","uri":"/data/report.txt"}],"tool_name":"filesystem.read","writes":[]},"artifact_type":"PFCoreEvent.v0","contract_refs":[],"decision":"allow","decision_reason":"authorized","event_id":"ev-file-read-1","evidence_refs":[],"previous_event_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000","principal":{"capabilities":["cap:file-read"],"principal_id":"agent-1","principal_kind":"agent","roles":["agent"],"tenant":"tenant-a"},"schema_version":"v0","sequence":0,"source_commit":"abc1234567890abc1234567890abc1234567890","source_repo":"https://github.com/example/agent-runtime","timestamp":"2026-06-18T00:00:00Z","trace_id":"trace-file-read-1"} diff --git a/python/tests/hash_vectors/pf_core/PFCoreEvent.v0/digest.txt b/python/tests/hash_vectors/pf_core/PFCoreEvent.v0/digest.txt new file mode 100644 index 0000000..70653a0 --- /dev/null +++ b/python/tests/hash_vectors/pf_core/PFCoreEvent.v0/digest.txt @@ -0,0 +1 @@ +sha256:e6ae0e0c4c702dd1f83a6adb29a97e7d89b9741537b3ebd95bb476f754ea4960 diff --git a/python/tests/hash_vectors/pf_core/PFCoreEvent.v0/input.json b/python/tests/hash_vectors/pf_core/PFCoreEvent.v0/input.json new file mode 100644 index 0000000..81215f1 --- /dev/null +++ b/python/tests/hash_vectors/pf_core/PFCoreEvent.v0/input.json @@ -0,0 +1,52 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-file-read-1", + "trace_id": "trace-file-read-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:e6ae0e0c4c702dd1f83a6adb29a97e7d89b9741537b3ebd95bb476f754ea4960", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:e6ae0e0c4c702dd1f83a6adb29a97e7d89b9741537b3ebd95bb476f754ea4960" +} diff --git a/python/tests/hash_vectors/pf_core/PFCoreTrace.v0/canonical.txt b/python/tests/hash_vectors/pf_core/PFCoreTrace.v0/canonical.txt new file mode 100644 index 0000000..a87d454 --- /dev/null +++ b/python/tests/hash_vectors/pf_core/PFCoreTrace.v0/canonical.txt @@ -0,0 +1 @@ +{"artifact_type":"PFCoreTrace.v0","claim_class":"RuntimeChecked","contract_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000","events":[{"action":{"action_id":"act-1","capability":{"capability_id":"cap:file-read","effect_kind":"file.read","resource_pattern":"/data/*"},"effects":[{"effect_kind":"file.read"}],"input_hash":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","output_hash":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","reads":[{"resource_id":"res-1","tenant":"tenant-a","uri":"/data/report.txt"}],"tool_name":"filesystem.read","writes":[]},"artifact_type":"PFCoreEvent.v0","contract_refs":[],"decision":"allow","decision_reason":"authorized","event_hash":"sha256:e6ae0e0c4c702dd1f83a6adb29a97e7d89b9741537b3ebd95bb476f754ea4960","event_id":"ev-file-read-1","evidence_refs":[],"previous_event_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000","principal":{"capabilities":["cap:file-read"],"principal_id":"agent-1","principal_kind":"agent","roles":["agent"],"tenant":"tenant-a"},"schema_version":"v0","sequence":0,"signature_or_digest":"sha256:e6ae0e0c4c702dd1f83a6adb29a97e7d89b9741537b3ebd95bb476f754ea4960","source_commit":"abc1234567890abc1234567890abc1234567890","source_repo":"https://github.com/example/agent-runtime","timestamp":"2026-06-18T00:00:00Z","trace_id":"trace-file-read-1"}],"policy_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000","schema_version":"v0","source_commit":"abc1234567890abc1234567890abc1234567890","source_repo":"https://github.com/example/agent-runtime","trace_id":"trace-file-read-1","workflow_id":"agent_tool_use.safety_v0"} diff --git a/python/tests/hash_vectors/pf_core/PFCoreTrace.v0/digest.txt b/python/tests/hash_vectors/pf_core/PFCoreTrace.v0/digest.txt new file mode 100644 index 0000000..3c2fd4f --- /dev/null +++ b/python/tests/hash_vectors/pf_core/PFCoreTrace.v0/digest.txt @@ -0,0 +1 @@ +sha256:bc26bbf4e65c1722cf2dd56723238ff13b72526ee6450d0bb9e4e54a4c3a4d30 diff --git a/python/tests/hash_vectors/pf_core/PFCoreTrace.v0/input.json b/python/tests/hash_vectors/pf_core/PFCoreTrace.v0/input.json new file mode 100644 index 0000000..32a18d1 --- /dev/null +++ b/python/tests/hash_vectors/pf_core/PFCoreTrace.v0/input.json @@ -0,0 +1,67 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-file-read-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-file-read-1", + "trace_id": "trace-file-read-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:e6ae0e0c4c702dd1f83a6adb29a97e7d89b9741537b3ebd95bb476f754ea4960", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:e6ae0e0c4c702dd1f83a6adb29a97e7d89b9741537b3ebd95bb476f754ea4960" + } + ], + "trace_hash": "sha256:bc26bbf4e65c1722cf2dd56723238ff13b72526ee6450d0bb9e4e54a4c3a4d30", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:bc26bbf4e65c1722cf2dd56723238ff13b72526ee6450d0bb9e4e54a4c3a4d30" +} diff --git a/python/tests/hash_vectors/pf_core/invalid/claim_class_overclaim_trace.json b/python/tests/hash_vectors/pf_core/invalid/claim_class_overclaim_trace.json new file mode 100644 index 0000000..ec70c95 --- /dev/null +++ b/python/tests/hash_vectors/pf_core/invalid/claim_class_overclaim_trace.json @@ -0,0 +1,133 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-agent-safety-001", + "workflow_id": "agent_tool_use.safety_v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "evt-001", + "trace_id": "trace-agent-safety-001", + "sequence": 0, + "timestamp": "2026-05-18T00:00:01Z", + "principal": { + "principal_id": "agent-safety-conformance-001", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read", + "cap:email-send", + "cap:handoff", + "cap:mcp-invoke" + ] + }, + "action": { + "action_id": "act-evt-001", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-evt-001", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [ + "policy-no-secret-exfiltration-v0" + ], + "evidence_refs": [ + "policy-no-secret-exfiltration-v0" + ], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:91f2c6090442e9ddfa99dde83bd8bcc6762103e8b5e4adfd0a612cba20e68a01", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "a111111111111111111111111111111111111111", + "signature_or_digest": "sha256:91f2c6090442e9ddfa99dde83bd8bcc6762103e8b5e4adfd0a612cba20e68a01" + }, + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "evt-002", + "trace_id": "trace-agent-safety-001", + "sequence": 1, + "timestamp": "2026-05-18T00:00:02Z", + "principal": { + "principal_id": "agent-safety-conformance-001", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read", + "cap:email-send", + "cap:handoff", + "cap:mcp-invoke" + ] + }, + "action": { + "action_id": "act-evt-002", + "tool_name": "network.request", + "capability": { + "capability_id": "cap:network", + "effect_kind": "network.egress", + "resource_pattern": "*" + }, + "effects": [ + { + "effect_kind": "network.egress" + } + ], + "reads": [ + { + "resource_id": "res-evt-002", + "uri": "https://example.com", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "output_hash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + }, + "decision": "deny", + "decision_reason": "rejected", + "contract_refs": [ + "policy-no-secret-exfiltration-v0" + ], + "evidence_refs": [ + "policy-no-secret-exfiltration-v0" + ], + "previous_event_hash": "sha256:91f2c6090442e9ddfa99dde83bd8bcc6762103e8b5e4adfd0a612cba20e68a01", + "event_hash": "sha256:7994dfefd17f813d69a257195ba97cba519c4698f475b55a90933fc29a5db839", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "a111111111111111111111111111111111111111", + "signature_or_digest": "sha256:7994dfefd17f813d69a257195ba97cba519c4698f475b55a90933fc29a5db839" + } + ], + "trace_hash": "sha256:716cbed45d37ebe49deffd517021e74f7f8751f6baf6e00c2fdc6c3022b626f3", + "policy_hash": "sha256:76d4443f09c6fb0d6cc7bebc5c80eae53bd008e4a212ab61d0d1844ac773b5cd", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "LeanKernelChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "a111111111111111111111111111111111111111", + "signature_or_digest": "sha256:716cbed45d37ebe49deffd517021e74f7f8751f6baf6e00c2fdc6c3022b626f3" +} diff --git a/python/tests/hash_vectors/pf_core/invalid/contract_capability_missing/contract.json b/python/tests/hash_vectors/pf_core/invalid/contract_capability_missing/contract.json new file mode 100644 index 0000000..8941ad0 --- /dev/null +++ b/python/tests/hash_vectors/pf_core/invalid/contract_capability_missing/contract.json @@ -0,0 +1,15 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreContract.v0", + "contract_id": "contract-file-read-v0", + "name": "Strict file read contract", + "pre": { + "require_capability": "cap:file-read" + }, + "post": {}, + "invariant": {}, + "semantics_layer": { + "require_capability": "lean" + }, + "signature_or_digest": "sha256:e8885483b7bad56b9854dae186f2dc9c9261895fd27329897306426cd116e085" +} diff --git a/python/tests/hash_vectors/pf_core/invalid/contract_capability_missing/trace.json b/python/tests/hash_vectors/pf_core/invalid/contract_capability_missing/trace.json new file mode 100644 index 0000000..7668df2 --- /dev/null +++ b/python/tests/hash_vectors/pf_core/invalid/contract_capability_missing/trace.json @@ -0,0 +1,69 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-file-read-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-file-read-1", + "trace_id": "trace-file-read-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "email_user" + ], + "capabilities": [ + "cap:email-send" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [ + "contract-file-read-v0" + ], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:8418535c830ec9fbdfad8f352525918aa28439c812cc18f1dd04186803b423da", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:8418535c830ec9fbdfad8f352525918aa28439c812cc18f1dd04186803b423da" + } + ], + "trace_hash": "sha256:05e6749bb3ae276234a2baba73508477d0a92e13e360ec748f9c517b5cf2b2a9", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:05e6749bb3ae276234a2baba73508477d0a92e13e360ec748f9c517b5cf2b2a9" +} diff --git a/python/tests/hash_vectors/pf_core/invalid/denied_event_dropped/pfcore_trace.json b/python/tests/hash_vectors/pf_core/invalid/denied_event_dropped/pfcore_trace.json new file mode 100644 index 0000000..40a271d --- /dev/null +++ b/python/tests/hash_vectors/pf_core/invalid/denied_event_dropped/pfcore_trace.json @@ -0,0 +1,14 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-dropped-deny", + "workflow_id": "agent_tool_use.safety_v0", + "events": [], + "trace_hash": "sha256:835902e07614c701bf583a77eedac63bc18f6c36aaea416c6ea30eed1582df21", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:835902e07614c701bf583a77eedac63bc18f6c36aaea416c6ea30eed1582df21" +} diff --git a/python/tests/hash_vectors/pf_core/invalid/denied_event_dropped/tool_use_trace.json b/python/tests/hash_vectors/pf_core/invalid/denied_event_dropped/tool_use_trace.json new file mode 100644 index 0000000..d2310ec --- /dev/null +++ b/python/tests/hash_vectors/pf_core/invalid/denied_event_dropped/tool_use_trace.json @@ -0,0 +1,29 @@ +{ + "schema_version": "v0", + "trace_id": "trace-dropped-deny", + "workflow_id": "agent_tool_use.safety_v0", + "agent_id": "agent-1", + "policy_id": "policy/default.v0", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "started_at": "2026-06-18T00:00:00Z", + "completed_at": "2026-06-18T00:00:01Z", + "tool_calls": [ + { + "event_id": "evt-deny", + "timestamp": "2026-06-18T00:00:01Z", + "tool_name": "network.request", + "tool_category": "network", + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "authorization_status": "rejected", + "policy_refs": [ + "policy/default.v0" + ], + "tenant": "tenant-a" + } + ], + "trace_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/python/tests/hash_vectors/pf_core/invalid/trace_hash_chain_break.json b/python/tests/hash_vectors/pf_core/invalid/trace_hash_chain_break.json new file mode 100644 index 0000000..98c385d --- /dev/null +++ b/python/tests/hash_vectors/pf_core/invalid/trace_hash_chain_break.json @@ -0,0 +1,70 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-file-read-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-file-read-1", + "trace_id": "trace-file-read-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:file-read", + "cap:email-send", + "cap:handoff", + "cap:mcp-invoke" + ] + }, + "action": { + "action_id": "act-1", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/report.txt", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [], + "evidence_refs": [], + "previous_event_hash": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "event_hash": "sha256:4f54951a4b008bdb24f2bb88438cff876fadd84259ad6d83e8211980303a214b", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:4f54951a4b008bdb24f2bb88438cff876fadd84259ad6d83e8211980303a214b" + } + ], + "trace_hash": "sha256:47a6b6dde12dd26795643afa0130a379e59e6f8426d9270943e0828e5e5729f2", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:47a6b6dde12dd26795643afa0130a379e59e6f8426d9270943e0828e5e5729f2" +} diff --git a/python/tests/test_pf_core_cross_language.py b/python/tests/test_pf_core_cross_language.py index d711025..ac2c608 100644 --- a/python/tests/test_pf_core_cross_language.py +++ b/python/tests/test_pf_core_cross_language.py @@ -10,9 +10,17 @@ import pytest +from pcs_core.pf_core_contract import validate_trace_contracts +from pcs_core.pf_core_runtime import ( + compute_event_hash, + compute_trace_hash, + validate_denied_events_preserved, + validate_pfcore_trace_hash_chain, +) from pcs_core.validate import ARTIFACT_SCHEMAS, detect_artifact_type, validate_schema REPO = Path(__file__).resolve().parents[2] +INVALID_VECTORS = REPO / "python" / "tests" / "hash_vectors" / "pf_core" / "invalid" PF_CORE_TYPES = sorted( key @@ -22,6 +30,7 @@ TS_SCHEMAS = REPO / "typescript" / "packages" / "core" / "src" / "schema.ts" RUST_SCHEMAS = REPO / "rust" / "crates" / "pcs-core" / "src" / "validation.rs" +VALID_TRACE = REPO / "examples" / "pf-core-valid" / "tool_use_trace_compiled" / "pfcore_trace.json" def _load_json(path: Path) -> dict: @@ -77,9 +86,67 @@ def test_pf_core_schema_files_exist(artifact_type: str) -> None: assert schema_path.is_file(), f"missing schema for {artifact_type}" +def test_python_pf_core_trace_hash_chain_valid_fixture() -> None: + trace = _load_json(VALID_TRACE) + assert validate_pfcore_trace_hash_chain(trace) == [] + + +def test_python_pf_core_trace_hash_recompute() -> None: + trace = _load_json(VALID_TRACE) + assert compute_trace_hash(trace) == trace["trace_hash"] + for event in trace["events"]: + assert compute_event_hash(event) == event["event_hash"] + + +def test_python_claim_class_overclaim_on_trace() -> None: + trace = _load_json(VALID_TRACE) + trace = dict(trace) + trace["claim_class"] = "LeanKernelChecked" + errors = validate_pfcore_trace_hash_chain(trace) + assert any("ClaimClassOverclaim" in err for err in errors) + + +def test_python_pf_core_shared_hash_vectors() -> None: + vector_root = REPO / "python" / "tests" / "hash_vectors" / "pf_core" + for name in ("PFCoreEvent.v0", "PFCoreTrace.v0", "PFCoreContract.v0"): + payload = _load_json(vector_root / name / "input.json") + digest = (vector_root / name / "digest.txt").read_text(encoding="utf-8").strip() + if name == "PFCoreEvent.v0": + assert compute_event_hash(payload) == digest + elif name == "PFCoreTrace.v0": + assert compute_trace_hash(payload) == digest + else: + from pcs_core.hash import canonical_hash + + assert canonical_hash(payload) == digest + + +@pytest.mark.parametrize( + "relative,needle", + [ + ("invalid/trace_hash_chain_break.json", "EventHashMismatch"), + ("invalid/claim_class_overclaim_trace.json", "ClaimClassOverclaim"), + ], +) +def test_python_invalid_pf_core_vectors(relative: str, needle: str) -> None: + trace = _load_json(REPO / "python" / "tests" / "hash_vectors" / "pf_core" / relative) + errors = validate_pfcore_trace_hash_chain(trace) + assert any(needle in err for err in errors) + + +def test_python_denied_event_preserved_invalid_vector() -> None: + from pcs_core.pf_core_runtime import DroppedDeniedEvent, validate_denied_events_preserved + + root = REPO / "python" / "tests" / "hash_vectors" / "pf_core" / "invalid" / "denied_event_dropped" + tool_use = _load_json(root / "tool_use_trace.json") + pfcore = _load_json(root / "pfcore_trace.json") + with pytest.raises(DroppedDeniedEvent): + validate_denied_events_preserved(tool_use, pfcore) + + def test_rust_pf_core_detection_tests_pass() -> None: result = subprocess.run( - ["cargo", "test", "pf_core_explicit_artifact_types", "--", "--nocapture"], + ["cargo", "test", "pf_core", "--", "--nocapture"], cwd=REPO / "rust", capture_output=True, text=True, @@ -97,3 +164,34 @@ def test_typescript_pf_core_detection_tests_pass() -> None: text=True, ) assert result.returncode == 0, result.stdout + result.stderr + + +def test_shared_negative_vectors_python() -> None: + trace = _load_json(INVALID_VECTORS / "trace_hash_chain_break.json") + assert any("EventHashMismatch" in err for err in validate_pfcore_trace_hash_chain(trace)) + + overclaim = _load_json(INVALID_VECTORS / "claim_class_overclaim_trace.json") + assert any("ClaimClassOverclaim" in err for err in validate_pfcore_trace_hash_chain(overclaim)) + + contract_dir = INVALID_VECTORS / "contract_capability_missing" + contract_trace = _load_json(contract_dir / "trace.json") + contract = _load_json(contract_dir / "contract.json") + issues = validate_trace_contracts(contract_trace, {contract["contract_id"]: contract}) + assert any(issue.code == "ContractCapabilityRequired" for issue in issues) + + denied_dir = INVALID_VECTORS / "denied_event_dropped" + with pytest.raises(Exception): + validate_denied_events_preserved( + _load_json(denied_dir / "tool_use_trace.json"), + _load_json(denied_dir / "pfcore_trace.json"), + ) + + +def test_rust_negative_vector_tests_in_pf_core_suite() -> None: + result = subprocess.run( + ["cargo", "test", "pf_core_", "--", "--nocapture"], + cwd=REPO / "rust", + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/python/tests/test_pf_core_deferred.py b/python/tests/test_pf_core_deferred.py new file mode 100644 index 0000000..8f67238 --- /dev/null +++ b/python/tests/test_pf_core_deferred.py @@ -0,0 +1,149 @@ +"""Tests for deferred PF-Core execution plan items.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from pcs_core.hash import canonical_hash, canonicalize_for_hash +from pcs_core.pf_core_contract import ( + DEFAULT_TRACE_SAFE_CONTRACT_ID, + trace_has_contract_binding, + validate_trace_contract_binding, +) +from pcs_core.pf_core_runtime import ( + DroppedDeniedEvent, + compile_runtime_observation_to_event, + compile_runtime_observations_to_pfcore_trace, + compute_event_hash, + compute_trace_hash, + validate_denied_observations_preserved, +) +from pcs_core.validate import ( + ValidationError, + check_pf_core_invalid_fixtures, + load_pf_core_fixture_manifest, + validate_semantics, +) + +REPO = Path(__file__).resolve().parents[2] +VALID = REPO / "examples" / "pf-core-valid" +INVALID = REPO / "examples" / "pf-core-invalid" +HASH_VECTORS = Path(__file__).resolve().parent / "hash_vectors" / "pf_core" + + +def _load(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def test_runtime_observation_schema_requires_sequence() -> None: + observation = _load(VALID / "file_read_allowed" / "observation.json") + assert observation["sequence"] == 0 + event = compile_runtime_observation_to_event(observation) + assert event["sequence"] == 0 + + +def test_ordered_observation_compilation_uses_sequence() -> None: + allow = _load(VALID / "file_read_allowed" / "observation.json") + deny = _load(VALID / "network_denied" / "observation.json") + allow = dict(allow) + deny = dict(deny) + allow["trace_id"] = "trace-order-1" + allow["event_id"] = "ev-second" + allow["sequence"] = 1 + deny["trace_id"] = "trace-order-1" + deny["event_id"] = "ev-first" + deny["sequence"] = 0 + + trace = compile_runtime_observations_to_pfcore_trace([allow, deny]) + assert [event["event_id"] for event in trace["events"]] == ["ev-first", "ev-second"] + assert [event["sequence"] for event in trace["events"]] == [0, 1] + + +def test_denied_observation_preserved_in_batch_compile() -> None: + allow = _load(VALID / "file_read_allowed" / "observation.json") + deny = _load(VALID / "network_denied" / "observation.json") + for obs in (allow, deny): + obs["trace_id"] = "trace-deny-batch" + deny["sequence"] = 1 + allow["sequence"] = 0 + trace = compile_runtime_observations_to_pfcore_trace([allow, deny]) + validate_denied_observations_preserved([allow, deny], trace["events"]) + + +def test_dropped_denied_observation_fixture() -> None: + case = INVALID / "dropped_denied_observation" + manifest = load_pf_core_fixture_manifest(case) + observations = [_load(path) for path in sorted(case.glob("observation*.json"))] + pfcore = _load(case / "pfcore_trace.json") + with pytest.raises(DroppedDeniedEvent) as exc: + validate_denied_observations_preserved(observations, pfcore["events"]) + assert exc.value.code == manifest["expected_error"] + + +def test_lean_kernel_checked_requires_contract_binding() -> None: + trace = _load(INVALID / "lean_kernel_checked_without_contract" / "trace.json") + errors = validate_trace_contract_binding(trace) + assert any("ContractBindingMissing" in err for err in errors) + semantic = validate_semantics(trace, "PFCoreTrace.v0") + assert any("ContractBindingMissing" in err for err in semantic) + + +def test_default_trace_safe_contract_binding() -> None: + trace = _load(VALID / "contract_checked" / "trace.json") + assert trace_has_contract_binding(trace) is True + trace = dict(trace) + trace["default_contract_ref"] = DEFAULT_TRACE_SAFE_CONTRACT_ID + trace["events"] = [ + dict(event, contract_refs=[]) for event in trace["events"] if isinstance(event, dict) + ] + assert trace_has_contract_binding(trace) is True + + +@pytest.mark.parametrize( + "case_name", + [ + "previous_event_hash_mismatch", + "lean_kernel_checked_without_proof_ref", + "lean_kernel_checked_without_proof_term_ref", + "lean_kernel_checked_with_skipped_build", + "contract_ref_missing", + "contract_capability_missing", + "contract_effect_missing", + "contract_policy_ref_missing", + "contract_evidence_ref_missing", + "lean_kernel_checked_without_contract", + "dropped_denied_observation", + ], +) +def test_invalid_fixture_manifest(case_name: str) -> None: + manifest = load_pf_core_fixture_manifest(INVALID / case_name) + assert manifest["expected_error"] + assert manifest["must_fail_at"] + + +def test_invalid_fixture_harness_runs() -> None: + check_pf_core_invalid_fixtures() + + +@pytest.mark.parametrize( + "artifact", + ["PFCoreEvent.v0", "PFCoreTrace.v0", "PFCoreContract.v0"], +) +def test_pf_core_hash_vectors(artifact: str) -> None: + vector_dir = HASH_VECTORS / artifact + payload = _load(vector_dir / "input.json") + digest = (vector_dir / "digest.txt").read_text(encoding="utf-8").strip() + canonical = (vector_dir / "canonical.txt").read_text(encoding="utf-8").strip() + if artifact == "PFCoreEvent.v0": + assert compute_event_hash(payload) == digest + elif artifact == "PFCoreTrace.v0": + assert compute_trace_hash(payload) == digest + else: + assert canonical_hash(payload) == digest + stripped = canonicalize_for_hash( + {k: v for k, v in payload.items() if k not in ("event_hash", "trace_hash", "signature_or_digest")} + ) + assert json.dumps(stripped, separators=(",", ":"), ensure_ascii=False) == canonical.rstrip("\n") diff --git a/python/tests/test_pf_core_stage1.py b/python/tests/test_pf_core_stage1.py index fb934d2..c9d0434 100644 --- a/python/tests/test_pf_core_stage1.py +++ b/python/tests/test_pf_core_stage1.py @@ -80,5 +80,5 @@ def test_lean_check_disclaimer_constant() -> None: assert "LeanKernelChecked" in LEAN_CHECK_DISCLAIMER assert "concrete Lean proof" in LEAN_CHECK_DISCLAIMER - assert "not Lean-backed" in PCS_LEAN_CHECK_DISCLAIMER + assert "release-envelope" in PCS_LEAN_CHECK_DISCLAIMER.lower() assert "pf-core lean-check" in PCS_LEAN_CHECK_DISCLAIMER diff --git a/python/tests/test_pf_core_stage2.py b/python/tests/test_pf_core_stage2.py index 62ae572..fe34ef8 100644 --- a/python/tests/test_pf_core_stage2.py +++ b/python/tests/test_pf_core_stage2.py @@ -70,76 +70,18 @@ def test_valid_pf_core_fixtures(case_dir: Path) -> None: for path in sorted(case_dir.glob("*.json")): if path.name == "manifest.json": continue + if path.name == "tool_use_trace.json" and (case_dir / "pfcore_trace.json").is_file(): + continue data = _load(path) artifact_type = detect_artifact_type(data) assert artifact_type is not None, f"Could not detect type for {path}" validate_artifact(data, artifact_type) -@pytest.mark.parametrize("case_dir", sorted(INVALID.iterdir()) if INVALID.is_dir() else []) -def test_invalid_pf_core_fixtures(case_dir: Path) -> None: - manifest = load_pf_core_fixture_manifest(case_dir) - expected_error = manifest["expected_error"] - must_fail_at = manifest["must_fail_at"] - - if must_fail_at == "runtime_to_pfcore_event": - observation = _load(case_dir / "observation.json") - with pytest.raises((UnknownCapability, UnknownEffect, MissingPrincipal)) as exc: - compile_runtime_observation_to_event(observation) - assert exc.value.code == expected_error - return - - if must_fail_at == "validate_pfcore_trace_hash_chain": - trace = _load(case_dir / "trace.json") - errors = validate_pfcore_trace_hash_chain(trace) - assert any(expected_error in err for err in errors) - return - - if must_fail_at == "validate_denied_events_preserved": - tool_use_trace = _load(case_dir / "tool_use_trace.json") - pfcore_trace = _load(case_dir / "pfcore_trace.json") - with pytest.raises(DroppedDeniedEvent) as exc: - validate_denied_events_preserved(tool_use_trace, pfcore_trace) - assert exc.value.code == expected_error - return - - if must_fail_at == "validate_handoff_authority": - handoff = _load(case_dir / "handoff.json") - with pytest.raises(HandoffAuthorityExpansion) as exc: - validate_handoff_authority(handoff) - assert exc.value.code == expected_error - return - - if must_fail_at == "compile_tool_use_trace_to_pfcore_trace": - tool_use_trace = _load(case_dir / "tool_use_trace.json") - with pytest.raises(HandoffAuthorityExpansion) as exc: - compile_tool_use_trace_to_pfcore_trace(tool_use_trace) - assert exc.value.code == expected_error - return - - if must_fail_at == "validate_trace_contracts": - from pcs_core.pf_core_contract import validate_trace_contracts - - trace = _load(case_dir / "trace.json") - contracts = { - str(data["contract_id"]): data - for data in ( - _load(path) for path in sorted((case_dir / "contracts").glob("*.json")) - ) - } - issues = validate_trace_contracts(trace, contracts) - assert any(issue.code == expected_error for issue in issues) - return - - if must_fail_at == "validate_tenant_isolation": - from pcs_core.pf_core_runtime import validate_tenant_isolation - - trace = _load(case_dir / "trace.json") - errors = validate_tenant_isolation(trace) - assert any(expected_error in err for err in errors) - return - - pytest.fail(f"Unknown must_fail_at {must_fail_at!r} in {case_dir}") +def test_invalid_pf_core_fixtures_harness() -> None: + from pcs_core.validate import check_pf_core_invalid_fixtures + + check_pf_core_invalid_fixtures() def test_tool_use_trace_compiles_to_pfcore_trace() -> None: diff --git a/python/tests/test_pf_core_tier1.py b/python/tests/test_pf_core_tier1.py new file mode 100644 index 0000000..dbdfbae --- /dev/null +++ b/python/tests/test_pf_core_tier1.py @@ -0,0 +1,196 @@ +"""Tier 1 PF-Core tests: semantics_layer, PCS envelope path, cross-language vectors.""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +from pcs_core.hash import canonical_hash +from pcs_core.lean_check import PCS_LEAN_CHECK_DISCLAIMER +from pcs_core.pf_core_certifyedge import ( + CERTIFYEDGE_INSTALL_DOC, + certifyedge_cli_available, + certifyedge_mock_enabled, +) +from pcs_core.pf_core_contract import ( + load_contract, + resolve_semantics_layer, + validate_trace_contracts, +) +from pcs_core.pf_core_contract_semantics import ( + build_contract_semantics_checked, + default_semantics_layer_for_contract, + validate_semantics_layer, +) +from pcs_core.pf_core_runtime import validate_denied_events_preserved, validate_pfcore_trace_hash_chain + +REPO = Path(__file__).resolve().parents[2] +CONTRACT_VALID = REPO / "examples" / "pf-core-valid" / "contract_checked" +INVALID_VECTORS = REPO / "python" / "tests" / "hash_vectors" / "pf_core" / "invalid" +PROOF_OBLIGATION = REPO / "examples" / "proof_obligation.valid.json" + + +def _load(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def test_contract_checked_has_semantics_layer() -> None: + contract = load_contract(CONTRACT_VALID / "contract.json") + assert "semantics_layer" in contract + layers = resolve_semantics_layer(contract) + assert layers["require_capability"] == "lean" + assert layers["require_decision"] == "lean" + assert layers["require_trace_safe"] == "lean" + + +def test_default_semantics_layer_matches_contract_semantics_doc() -> None: + contract = { + "pre": { + "require_capability": "cap:file-read", + "require_role": "agent", + }, + "post": {"require_decision": "allow"}, + "invariant": {"require_trace_safe": True}, + } + layers = default_semantics_layer_for_contract(contract) + assert layers["require_capability"] == "lean" + assert layers["require_role"] == "runtime" + assert layers["require_decision"] == "lean" + assert layers["require_trace_safe"] == "lean" + + +def test_semantics_layer_orphan_field_rejected() -> None: + contract = _load(CONTRACT_VALID / "contract.json") + contract = dict(contract) + contract.pop("signature_or_digest", None) + contract["semantics_layer"] = {"require_role": "runtime"} + issues = validate_semantics_layer(contract) + assert any(issue.code == "SemanticsLayerOrphanField" for issue in issues) + + +def test_semantics_layer_out_of_scope_active_field_rejected() -> None: + contract = _load(CONTRACT_VALID / "contract.json") + contract = dict(contract) + contract.pop("signature_or_digest", None) + contract["semantics_layer"] = {"require_capability": "out_of_scope"} + issues = validate_semantics_layer(contract) + assert any(issue.code == "SemanticsLayerOutOfScopeFieldSet" for issue in issues) + + +def test_build_contract_semantics_checked_uses_semantics_layer() -> None: + trace = _load(CONTRACT_VALID / "trace.json") + contract = load_contract(CONTRACT_VALID / "contract.json") + checked = build_contract_semantics_checked(trace, {contract["contract_id"]: contract}) + assert "contract-file-read-v0.pre.require_capability" in checked["lean"] + assert checked["runtime"] == [] + + +def test_contract_semantics_checked_excludes_runtime_only_fields() -> None: + contract = _load(CONTRACT_VALID / "contract.json") + contract = dict(contract) + contract.pop("signature_or_digest", None) + contract["pre"]["require_role"] = "agent" + contract["semantics_layer"] = resolve_semantics_layer(contract) + contract["semantics_layer"]["require_role"] = "runtime" + contract["signature_or_digest"] = canonical_hash( + {k: v for k, v in contract.items() if k != "signature_or_digest"} + ) + trace = _load(CONTRACT_VALID / "trace.json") + checked = build_contract_semantics_checked(trace, {contract["contract_id"]: contract}) + assert "contract-file-read-v0.pre.require_role" in checked["runtime"] + + +def test_negative_hash_chain_vector_python() -> None: + trace = _load(INVALID_VECTORS / "trace_hash_chain_break.json") + errors = validate_pfcore_trace_hash_chain(trace) + assert any("EventHashMismatch" in err for err in errors) + + +def test_negative_claim_class_overclaim_vector_python() -> None: + trace = _load(INVALID_VECTORS / "claim_class_overclaim_trace.json") + errors = validate_pfcore_trace_hash_chain(trace) + assert any("ClaimClassOverclaim" in err for err in errors) + + +def test_negative_contract_violation_vector_python() -> None: + root = INVALID_VECTORS / "contract_capability_missing" + trace = _load(root / "trace.json") + contract = _load(root / "contract.json") + issues = validate_trace_contracts(trace, {contract["contract_id"]: contract}) + assert any(issue.code == "ContractCapabilityRequired" for issue in issues) + + +def test_negative_denied_event_dropped_vector_python() -> None: + root = INVALID_VECTORS / "denied_event_dropped" + tool_use = _load(root / "tool_use_trace.json") + pfcore = _load(root / "pfcore_trace.json") + with pytest.raises(Exception) as exc: + validate_denied_events_preserved(tool_use, pfcore) + assert "DroppedDeniedEvent" in str(exc.value) + + +def test_pcs_lean_check_disclaimer_never_mentions_lean_kernel_checked() -> None: + combined = PCS_LEAN_CHECK_DISCLAIMER.lower() + assert "leankernelchecked" not in combined.replace("_", "").replace("-", "") + + +def test_pcs_envelope_check_alias_runs_same_as_lean_check(tmp_path: Path) -> None: + if not PROOF_OBLIGATION.is_file(): + pytest.skip("proof_obligation.valid.json missing") + out = tmp_path / "lean_check_result.json" + result = subprocess.run( + [ + sys.executable, + "-m", + "pcs_core.cli", + "pcs-envelope", + "check", + "--obligations", + str(PROOF_OBLIGATION), + "--out", + str(out), + "--skip-lean-build", + ], + cwd=REPO / "python", + capture_output=True, + text=True, + ) + assert result.returncode in {0, 1} + assert out.is_file() + payload = _load(out) + assert payload.get("check_id") + assert payload.get("status") in {"ProofChecked", "Rejected", "Stale"} + assert "LeanKernelChecked" not in (result.stdout + result.stderr) + + +def test_pcs_lean_check_prints_deprecation_notice(tmp_path: Path) -> None: + if not PROOF_OBLIGATION.is_file(): + pytest.skip("proof_obligation.valid.json missing") + out = tmp_path / "lean_check_result.json" + result = subprocess.run( + [ + sys.executable, + "-m", + "pcs_core.cli", + "lean-check", + "--obligations", + str(PROOF_OBLIGATION), + "--out", + str(out), + "--skip-lean-build", + ], + cwd=REPO / "python", + capture_output=True, + text=True, + ) + assert "pcs-envelope check" in result.stderr + assert "LeanKernelChecked" not in (result.stdout + result.stderr) + + +def test_certifyedge_install_doc_present() -> None: + assert "certifyedge" in CERTIFYEDGE_INSTALL_DOC.lower() + assert certifyedge_mock_enabled() or isinstance(certifyedge_cli_available(), bool) diff --git a/rust/crates/pcs-core/src/lib.rs b/rust/crates/pcs-core/src/lib.rs index ecf38dd..162b9bb 100644 --- a/rust/crates/pcs-core/src/lib.rs +++ b/rust/crates/pcs-core/src/lib.rs @@ -1,9 +1,16 @@ pub mod hash; +pub mod pf_core; pub mod schema; pub mod status; pub mod validation; pub use hash::{canonical_hash, canonical_json_bytes, canonical_json_string}; +pub use pf_core::{ + compute_event_hash, compute_trace_hash, validate_claim_class_overclaim, + validate_denied_events_preserved, validate_event_against_contract, + validate_pfcore_certificate_semantics, validate_pfcore_trace_hash_chain, + validate_trace_contracts, GENESIS_HASH, +}; pub use validation::{ detect_artifact_type, validate_artifact, validate_semantics, ValidationError, }; diff --git a/rust/crates/pcs-core/src/pf_core.rs b/rust/crates/pcs-core/src/pf_core.rs new file mode 100644 index 0000000..eba5d41 --- /dev/null +++ b/rust/crates/pcs-core/src/pf_core.rs @@ -0,0 +1,580 @@ +use serde_json::{Map, Value}; + +use crate::hash::canonical_hash; + +pub const GENESIS_HASH: &str = "sha256:0000000000000000000000000000000000000000000000000000000000000000"; + +const LEAN_CLAIM_CLASSES: &[&str] = &["LeanKernelChecked"]; + +const AUTHORIZATION_TO_DECISION: &[(&str, &str)] = &[ + ("authorized", "allow"), + ("rejected", "deny"), + ("unknown", "deny"), + ("policy_missing", "deny"), +]; + +fn object_mut(value: &Value) -> Option<&Map> { + value.as_object() +} + +fn strip_digest_fields(obj: &mut Map, keys: &[&str]) { + for key in keys { + obj.remove(*key); + } + obj.remove("signature_or_digest"); +} + +pub fn compute_event_hash(event: &Value) -> String { + let mut obj = object_mut(event).expect("event must be object").clone(); + strip_digest_fields(&mut obj, &["event_hash"]); + canonical_hash(&Value::Object(obj)) +} + +pub fn compute_trace_hash(trace: &Value) -> String { + let mut obj = object_mut(trace).expect("trace must be object").clone(); + strip_digest_fields(&mut obj, &["trace_hash"]); + canonical_hash(&Value::Object(obj)) +} + +fn normalize_hash(value: &str) -> Result { + let trimmed = value.trim(); + if !trimmed.starts_with("sha256:") || trimmed.len() != 71 { + return Err(format!("invalid hash {value:?}")); + } + Ok(trimmed.to_string()) +} + +pub fn validate_claim_class_overclaim( + claim_class: &str, + proof_ref: Option<&Value>, + lean_proof_checked: Option<&Value>, +) -> Result<(), String> { + let has_proof = proof_ref + .and_then(|v| v.as_str()) + .is_some_and(|s| !s.is_empty()); + if LEAN_CLAIM_CLASSES.contains(&claim_class) && !has_proof { + return Err(format!( + "ClaimClassOverclaim: claim_class {claim_class:?} exceeds available assurance" + )); + } + if claim_class == "CertificateChecked" { + return Err( + "ClaimClassOverclaim: claim_class \"CertificateChecked\" exceeds available assurance" + .into(), + ); + } + if claim_class == "LeanKernelChecked" && lean_proof_checked != Some(&Value::Bool(true)) { + return Err( + "ClaimClassOverclaim: claim_class LeanKernelChecked requires lean_proof_checked=true" + .into(), + ); + } + Ok(()) +} + +pub fn validate_pfcore_trace_hash_chain(trace: &Value) -> Vec { + let mut errors = Vec::new(); + let events = match trace.get("events") { + Some(Value::Array(items)) => items, + _ => return vec!["TraceInvalid: events must be an array".into()], + }; + + let mut previous = match normalize_hash(GENESIS_HASH) { + Ok(value) => value, + Err(message) => return vec![message], + }; + + for (index, event) in events.iter().enumerate() { + let base = format!("events[{index}]"); + let Some(event_obj) = object_mut(event) else { + errors.push(format!("EventInvalid: {base} must be an object")); + continue; + }; + let prev_field = match event_obj + .get("previous_event_hash") + .and_then(|v| v.as_str()) + .map(normalize_hash) + { + Some(Ok(value)) => value, + _ => { + errors.push(format!("EventHashMismatch: invalid previous_event_hash at {base}")); + continue; + } + }; + if prev_field != previous { + errors.push(format!( + "EventHashMismatch: previous_event_hash mismatch at {base} (expected {previous}, got {prev_field})" + )); + } + let actual_hash = match event_obj + .get("event_hash") + .and_then(|v| v.as_str()) + .map(normalize_hash) + { + Some(Ok(value)) => value, + _ => { + errors.push(format!("EventHashMismatch: invalid event_hash at {base}")); + continue; + } + }; + let expected_hash = compute_event_hash(event); + if actual_hash != expected_hash { + errors.push(format!( + "EventHashMismatch: event_hash mismatch at {base} (expected {expected_hash}, got {actual_hash})" + )); + } + previous = actual_hash; + } + + if let Some(trace_hash) = trace.get("trace_hash") { + if let Some(raw) = trace_hash.as_str() { + match normalize_hash(raw) { + Ok(actual_trace_hash) => { + let expected_trace_hash = compute_trace_hash(trace); + if actual_trace_hash != expected_trace_hash { + errors.push(format!( + "TraceHashMismatch: trace_hash mismatch (expected {expected_trace_hash}, got {actual_trace_hash})" + )); + } + } + Err(_) => errors.push("TraceHashMismatch: invalid trace_hash".into()), + } + } else { + errors.push("TraceHashMismatch: missing trace_hash".into()); + } + } + + if let Some(claim_class) = trace.get("claim_class").and_then(|v| v.as_str()) { + if let Err(message) = validate_claim_class_overclaim( + claim_class, + trace.get("proof_ref").or(trace.get("proof_term_ref")), + trace.get("lean_proof_checked"), + ) { + errors.push(message); + } + } + + errors +} + +pub fn validate_pfcore_certificate_semantics(certificate: &Value) -> Vec { + let mut errors = Vec::new(); + let claim_class = certificate + .get("claim_class") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if let Err(message) = validate_claim_class_overclaim( + claim_class, + certificate + .get("proof_ref") + .or(certificate.get("proof_term_ref")), + certificate.get("lean_proof_checked"), + ) { + errors.push(message); + } + if claim_class == "LeanKernelChecked" { + if certificate.get("lean_proof_checked") != Some(&Value::Bool(true)) { + errors.push( + "root: claim_class LeanKernelChecked requires lean_proof_checked=true".into(), + ); + } + if certificate + .get("proof_term_ref") + .and_then(|v| v.as_str()) + .map(|s| s.is_empty()) + .unwrap_or(true) + { + errors.push( + "root: claim_class LeanKernelChecked requires proof_term_ref (ClaimClassOverclaim)" + .into(), + ); + } + if certificate + .get("lean_environment_hash") + .and_then(|v| v.as_str()) + .is_none_or(|s| !s.starts_with("sha256:")) + { + errors.push("root: claim_class LeanKernelChecked requires lean_environment_hash".into()); + } + let build_ok = certificate + .get("lean_build_status") + .and_then(|v| v.get("ok")) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + if !build_ok { + errors.push("root: lean_proof_checked requires lean_build_status.ok=true".into()); + } + } + errors +} + +fn default_field_layer(section: &str, field: &str) -> &'static str { + match (section, field) { + ("pre", "require_capability") => "lean", + ("pre", "require_effect") => "lean", + ("pre", "require_tenant_match") => "lean", + ("pre", "require_role") => "runtime", + ("pre", "require_policy_ref") => "runtime", + ("pre", "require_evidence_ref") => "runtime", + ("post", "require_decision") => "lean", + ("post", "require_event_safe") => "lean", + ("invariant", "require_trace_safe") => "lean", + _ => "runtime", + } +} + +fn field_layer(contract: &Value, section: &str, field: &str) -> String { + let _ = section; + contract + .get("semantics_layer") + .and_then(|v| v.get(field)) + .and_then(|v| v.as_str()) + .map(str::to_string) + .unwrap_or_else(|| default_field_layer(section, field).to_string()) +} + +fn principal_has_capability(principal: &Value, capability_id: &str) -> bool { + let Some(caps) = principal.get("capabilities").and_then(|v| v.as_array()) else { + return false; + }; + caps.iter() + .filter_map(|v| v.as_str()) + .any(|cap| cap == capability_id) +} + +fn action_has_effect(action: &Value, effect_kind: &str) -> bool { + let Some(effects) = action.get("effects").and_then(|v| v.as_array()) else { + return false; + }; + effects.iter().any(|effect| { + effect + .get("effect_kind") + .and_then(|v| v.as_str()) + .is_some_and(|kind| kind == effect_kind) + }) +} + +fn tenant_matches(principal: &Value, action: &Value) -> bool { + let tenant = principal + .get("tenant") + .and_then(|v| v.as_str()) + .unwrap_or(""); + for key in ["reads", "writes"] { + let Some(resources) = action.get(key).and_then(|v| v.as_array()) else { + continue; + }; + for resource in resources { + if resource + .get("tenant") + .and_then(|v| v.as_str()) + .is_some_and(|value| value != tenant) + { + return false; + } + } + } + true +} + +pub fn validate_event_against_contract(event: &Value, contract: &Value, path: &str) -> Vec { + let mut errors = Vec::new(); + let Some(principal) = event.get("principal") else { + return vec![format!("ContractEventInvalid: event missing principal or action at {path}")]; + }; + let Some(action) = event.get("action") else { + return vec![format!("ContractEventInvalid: event missing principal or action at {path}")]; + }; + let contract_id = contract + .get("contract_id") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + if let Some(pre) = contract.get("pre").and_then(|v| v.as_object()) { + if pre.get("require_tenant_match").and_then(|v| v.as_bool()) == Some(true) + && field_layer(contract, "pre", "require_tenant_match") != "out_of_scope" + && !tenant_matches(principal, action) + { + errors.push(format!( + "ContractTenantMismatch: contract {contract_id:?} requires tenant match at {path}" + )); + } + if let Some(required_cap) = pre.get("require_capability").and_then(|v| v.as_str()) { + if !required_cap.is_empty() + && field_layer(contract, "pre", "require_capability") != "out_of_scope" + && !principal_has_capability(principal, required_cap) + { + errors.push(format!( + "ContractCapabilityRequired: contract {contract_id:?} requires capability {required_cap:?} at {path}.principal" + )); + } + } + if let Some(required_effect) = pre.get("require_effect").and_then(|v| v.as_str()) { + if !required_effect.is_empty() + && field_layer(contract, "pre", "require_effect") != "out_of_scope" + && !action_has_effect(action, required_effect) + { + errors.push(format!( + "ContractEffectRequired: contract {contract_id:?} requires effect {required_effect:?} at {path}.action.effects" + )); + } + } + if let Some(required_role) = pre.get("require_role").and_then(|v| v.as_str()) { + if !required_role.is_empty() + && field_layer(contract, "pre", "require_role") != "out_of_scope" + { + let roles = principal.get("roles").and_then(|v| v.as_array()); + let has_role = roles.is_some_and(|items| { + items + .iter() + .filter_map(|v| v.as_str()) + .any(|role| role == required_role) + }); + if !has_role { + errors.push(format!( + "ContractRoleRequired: contract {contract_id:?} requires role {required_role:?} at {path}.principal.roles" + )); + } + } + } + if let Some(required_policy) = pre.get("require_policy_ref").and_then(|v| v.as_str()) { + if !required_policy.is_empty() + && field_layer(contract, "pre", "require_policy_ref") != "out_of_scope" + { + let refs = event.get("contract_refs").and_then(|v| v.as_array()); + let has_ref = refs.is_some_and(|items| { + items + .iter() + .filter_map(|v| v.as_str()) + .any(|value| value == required_policy) + }); + if !has_ref { + errors.push(format!( + "ContractPolicyRefRequired: contract {contract_id:?} requires policy ref {required_policy:?} at {path}.contract_refs" + )); + } + } + } + if let Some(required_evidence) = pre.get("require_evidence_ref").and_then(|v| v.as_str()) { + if !required_evidence.is_empty() + && field_layer(contract, "pre", "require_evidence_ref") != "out_of_scope" + { + let evidence = event.get("evidence_refs").and_then(|v| v.as_array()); + let has_ref = evidence.is_some_and(|items| { + items + .iter() + .filter_map(|v| v.as_str()) + .any(|value| value == required_evidence) + }); + if !has_ref { + errors.push(format!( + "ContractEvidenceRefRequired: contract {contract_id:?} requires evidence ref {required_evidence:?} at {path}.evidence_refs" + )); + } + } + } + } + + if let Some(post) = contract.get("post").and_then(|v| v.as_object()) { + if let Some(required_decision) = post.get("require_decision").and_then(|v| v.as_str()) { + if !required_decision.is_empty() + && field_layer(contract, "post", "require_decision") != "out_of_scope" + { + let decision = event.get("decision").and_then(|v| v.as_str()).unwrap_or(""); + if decision != required_decision { + errors.push(format!( + "ContractDecisionMismatch: contract {contract_id:?} requires decision {required_decision:?}, got {decision:?} at {path}.decision" + )); + } + } + } + if post.get("require_event_safe").and_then(|v| v.as_bool()) == Some(true) + && field_layer(contract, "post", "require_event_safe") != "out_of_scope" + { + let decision = event.get("decision").and_then(|v| v.as_str()).unwrap_or(""); + if decision == "allow" { + let cap_id = action + .get("capability") + .and_then(|v| v.get("capability_id")) + .and_then(|v| v.as_str()) + .unwrap_or(""); + if cap_id.is_empty() || !principal_has_capability(principal, cap_id) { + errors.push(format!( + "ContractEventUnsafe: allowed event violates contract {contract_id:?} event safety at {path}" + )); + } else if !tenant_matches(principal, action) { + errors.push(format!( + "ContractEventUnsafe: allowed event violates contract {contract_id:?} tenant safety at {path}" + )); + } + } + } + } + + errors +} + +pub fn validate_trace_contracts( + trace: &Value, + contracts: &std::collections::HashMap, +) -> Vec { + let mut errors = Vec::new(); + let Some(events) = trace.get("events").and_then(|v| v.as_array()) else { + return vec!["TraceInvalid: events must be an array".into()]; + }; + for (index, event) in events.iter().enumerate() { + let base = format!("events[{index}]"); + let Some(refs) = event.get("contract_refs").and_then(|v| v.as_array()) else { + continue; + }; + if refs.is_empty() { + continue; + } + for (ref_index, reference) in refs.iter().enumerate() { + let Some(contract_id) = reference.as_str() else { + continue; + }; + let Some(contract) = contracts.get(contract_id) else { + errors.push(format!( + "ContractRefMissing: unknown contract reference {contract_id:?} at {base}.contract_refs[{ref_index}]" + )); + continue; + }; + errors.extend(validate_event_against_contract(event, contract, &base)); + } + } + errors +} + +fn authorization_decision(status: &str) -> &'static str { + AUTHORIZATION_TO_DECISION + .iter() + .find_map(|(auth, decision)| (*auth == status).then_some(*decision)) + .unwrap_or("deny") +} + +pub fn validate_denied_events_preserved(tool_use_trace: &Value, pfcore_trace: &Value) -> Vec { + let Some(tool_calls) = tool_use_trace.get("tool_calls").and_then(|v| v.as_array()) else { + return Vec::new(); + }; + let Some(events) = pfcore_trace.get("events").and_then(|v| v.as_array()) else { + return vec!["DroppedDeniedEvent: denied event \"\" missing from compiled trace (at events)".into()]; + }; + let compiled_ids: std::collections::HashSet = events + .iter() + .filter_map(|event| { + event + .get("event_id") + .and_then(|v| v.as_str()) + .map(str::to_string) + }) + .collect(); + let mut errors = Vec::new(); + for tool_call in tool_calls { + let auth = tool_call + .get("authorization_status") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if authorization_decision(auth) != "deny" { + continue; + } + let event_id = tool_call + .get("event_id") + .and_then(|v| v.as_str()) + .unwrap_or(""); + if !event_id.is_empty() && !compiled_ids.contains(event_id) { + errors.push(format!( + "DroppedDeniedEvent: denied event {event_id:?} missing from compiled trace (at events)" + )); + } + } + errors +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + use std::fs; + use std::path::PathBuf; + + fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("..") + } + + fn load_json(path: PathBuf) -> Value { + let text = fs::read_to_string(path).expect("fixture"); + serde_json::from_str(&text).expect("json") + } + + #[test] + fn pf_core_trace_hash_chain_valid_fixture() { + let path = repo_root().join("examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json"); + let trace = load_json(path); + let errors = validate_pfcore_trace_hash_chain(&trace); + assert!(errors.is_empty(), "{errors:?}"); + } + + #[test] + fn pf_core_shared_hash_vectors() { + let root = repo_root(); + for name in ["PFCoreEvent.v0", "PFCoreTrace.v0"] { + let base = root.join(format!("python/tests/hash_vectors/pf_core/{name}")); + let input_text = fs::read_to_string(base.join("input.json")).expect("input.json"); + let expected_digest = fs::read_to_string(base.join("digest.txt")) + .expect("digest.txt") + .trim() + .to_string(); + let value: Value = serde_json::from_str(&input_text).expect("json"); + let actual = if name == "PFCoreEvent.v0" { + compute_event_hash(&value) + } else { + compute_trace_hash(&value) + }; + assert_eq!(actual, expected_digest, "{name} digest mismatch"); + } + } + + #[test] + fn pf_core_invalid_hash_chain_vector() { + let path = repo_root().join("python/tests/hash_vectors/pf_core/invalid/trace_hash_chain_break.json"); + let trace = load_json(path); + let errors = validate_pfcore_trace_hash_chain(&trace); + assert!(errors.iter().any(|err| err.contains("EventHashMismatch"))); + } + + #[test] + fn pf_core_claim_class_overclaim_vector() { + let path = repo_root().join("python/tests/hash_vectors/pf_core/invalid/claim_class_overclaim_trace.json"); + let trace = load_json(path); + let errors = validate_pfcore_trace_hash_chain(&trace); + assert!(errors.iter().any(|err| err.contains("ClaimClassOverclaim"))); + } + + #[test] + fn pf_core_contract_violation_vector() { + let root = repo_root().join("python/tests/hash_vectors/pf_core/invalid/contract_capability_missing"); + let trace = load_json(root.join("trace.json")); + let contract = load_json(root.join("contract.json")); + let contract_id = contract + .get("contract_id") + .and_then(|v| v.as_str()) + .expect("contract_id") + .to_string(); + let mut contracts = HashMap::new(); + contracts.insert(contract_id, contract); + let errors = validate_trace_contracts(&trace, &contracts); + assert!(errors.iter().any(|err| err.contains("ContractCapabilityRequired"))); + } + + #[test] + fn pf_core_denied_event_dropped_vector() { + let root = repo_root().join("python/tests/hash_vectors/pf_core/invalid/denied_event_dropped"); + let tool_use = load_json(root.join("tool_use_trace.json")); + let pfcore = load_json(root.join("pfcore_trace.json")); + let errors = validate_denied_events_preserved(&tool_use, &pfcore); + assert!(errors.iter().any(|err| err.contains("DroppedDeniedEvent"))); + } +} diff --git a/rust/crates/pcs-core/src/validation.rs b/rust/crates/pcs-core/src/validation.rs index 4cb8124..ae1de1d 100644 --- a/rust/crates/pcs-core/src/validation.rs +++ b/rust/crates/pcs-core/src/validation.rs @@ -616,6 +616,12 @@ pub fn validate_semantics(value: &Value, artifact_type: &str) -> Result<(), Vali } } } + if artifact_type == "PFCoreTrace.v0" { + errors.extend(crate::pf_core::validate_pfcore_trace_hash_chain(value)); + } + if artifact_type == "PFCoreCertificate.v0" { + errors.extend(crate::pf_core::validate_pfcore_certificate_semantics(value)); + } } if errors.is_empty() { diff --git a/schemas/PFCoreCertificate.v0.schema.json b/schemas/PFCoreCertificate.v0.schema.json index cd1b0a4..68309dc 100644 --- a/schemas/PFCoreCertificate.v0.schema.json +++ b/schemas/PFCoreCertificate.v0.schema.json @@ -54,6 +54,21 @@ } }, "disclaimer": { "type": "string" }, + "contract_semantics_checked": { + "type": "object", + "additionalProperties": false, + "properties": { + "lean": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "runtime": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + } + }, + "default_contract_ref": { "type": "string", "minLength": 1 }, "event_count": { "type": "integer", "minimum": 0 }, "replay_match": { "type": "boolean" }, "original_trace_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, diff --git a/schemas/PFCoreContract.v0.schema.json b/schemas/PFCoreContract.v0.schema.json index 50ba5e3..c9c899f 100644 --- a/schemas/PFCoreContract.v0.schema.json +++ b/schemas/PFCoreContract.v0.schema.json @@ -22,9 +22,29 @@ "pre": { "$ref": "#/$defs/contract_pre" }, "post": { "$ref": "#/$defs/contract_post" }, "invariant": { "$ref": "#/$defs/contract_invariant" }, + "semantics_layer": { "$ref": "#/$defs/semantics_layer" }, "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } }, "$defs": { + "semantics_layer_value": { + "type": "string", + "enum": ["lean", "runtime", "out_of_scope"] + }, + "semantics_layer": { + "type": "object", + "additionalProperties": false, + "properties": { + "require_capability": { "$ref": "#/$defs/semantics_layer_value" }, + "require_effect": { "$ref": "#/$defs/semantics_layer_value" }, + "require_role": { "$ref": "#/$defs/semantics_layer_value" }, + "require_tenant_match": { "$ref": "#/$defs/semantics_layer_value" }, + "require_policy_ref": { "$ref": "#/$defs/semantics_layer_value" }, + "require_evidence_ref": { "$ref": "#/$defs/semantics_layer_value" }, + "require_decision": { "$ref": "#/$defs/semantics_layer_value" }, + "require_event_safe": { "$ref": "#/$defs/semantics_layer_value" }, + "require_trace_safe": { "$ref": "#/$defs/semantics_layer_value" } + } + }, "contract_pre": { "type": "object", "additionalProperties": false, diff --git a/schemas/PFCoreRuntimeObservation.v0.schema.json b/schemas/PFCoreRuntimeObservation.v0.schema.json index 4282e55..90d2e30 100644 --- a/schemas/PFCoreRuntimeObservation.v0.schema.json +++ b/schemas/PFCoreRuntimeObservation.v0.schema.json @@ -9,6 +9,7 @@ "observation_id", "trace_id", "event_id", + "sequence", "observed_at", "principal", "action", @@ -31,6 +32,7 @@ "observation_id": { "type": "string", "minLength": 1 }, "trace_id": { "type": "string", "minLength": 1 }, "event_id": { "type": "string", "minLength": 1 }, + "sequence": { "type": "integer", "minimum": 0 }, "observed_at": { "$ref": "common.defs.json#/$defs/iso8601_datetime" }, "principal": { "$ref": "pf_core.defs.json#/$defs/embedded_principal" }, "action": { "$ref": "pf_core.defs.json#/$defs/embedded_action" }, diff --git a/scripts/run-pf-core-adapter-ci.sh b/scripts/run-pf-core-adapter-ci.sh new file mode 100644 index 0000000..4ed9198 --- /dev/null +++ b/scripts/run-pf-core-adapter-ci.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# PF-Core adapter CI: compare pcs-core hash vectors with provability-fabric-core pin. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +PF_CORE_TAG="${PF_CORE_TAG:-pf-core-v0.6.0}" +PF_CORE_REPO="${PF_CORE_REPO:-https://github.com/SentinelOps-CI/provability-fabric-core.git}" +WORK="${TMPDIR:-/tmp}/pf-core-adapter-ci-$$" +LOCAL="${ROOT}/python/tests/hash_vectors" + +cleanup() { rm -rf "$WORK"; } +trap cleanup EXIT + +echo "PF-Core adapter CI (pin ${PF_CORE_TAG})" + +git clone --depth 1 --branch "$PF_CORE_TAG" "$PF_CORE_REPO" "$WORK/provability-fabric-core" + +UPSTREAM="$WORK/provability-fabric-core/adapters/pcs/tests/fixtures/hash_vectors" +if [ ! -d "$UPSTREAM" ]; then + echo "missing upstream hash vectors at $UPSTREAM" + exit 1 +fi + +fail=0 +while IFS= read -r -d '' rel; do + local_file="$LOCAL/$rel" + upstream_file="$UPSTREAM/$rel" + if [ ! -f "$local_file" ]; then + echo "missing local vector: $rel" + fail=1 + continue + fi + if ! diff -q "$local_file" "$upstream_file" >/dev/null 2>&1; then + echo "hash vector drift: $rel (expected match with $PF_CORE_TAG)" + diff -u "$local_file" "$upstream_file" || true + fail=1 + fi +done < <(cd "$UPSTREAM" && find . -type f ! -name '.gitkeep' -print0) + +if [ "$fail" -ne 0 ]; then + exit 1 +fi + +echo "OK: PCS hash vectors match provability-fabric-core ${PF_CORE_TAG}" diff --git a/scripts/run-release-verify.sh b/scripts/run-release-verify.sh index e364f09..e5c94a3 100644 --- a/scripts/run-release-verify.sh +++ b/scripts/run-release-verify.sh @@ -45,6 +45,21 @@ step "labtrust conformance pytest" pytest -q tests/test_labtrust_conformance.py step "multidomain pytest" pytest -q tests/test_multidomain_workflows.py step "pytest" pytest -q step "pytest protocol" pytest -q tests/test_protocol_conformance.py tests/test_benchmark_ingest_contract.py tests/test_release_chain.py +step "pf-core valid fixtures" pcs pf-core validate-trace ../examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json +step "pf-core lean no-sorry audit" pcs pf-core audit-lean-no-sorry +step "pf-core pytest" pytest -q tests/test_pf_core_*.py + +if command -v wsl >/dev/null 2>&1; then + step "lake build PCS" wsl bash -lc "cd /mnt/c/Users/mateo/pcs-core/lean && lake build PCS" + step "lake build PFCore" wsl bash -lc "cd /mnt/c/Users/mateo/pcs-core/lean && lake build PFCore" +elif command -v lake >/dev/null 2>&1; then + step "lake build PCS" bash -lc "cd ../lean && lake build PCS" + step "lake build PFCore" bash -lc "cd ../lean && lake build PFCore" +else + echo "SKIP lake build (lake and wsl unavailable)" +fi + +step "pf-core conformance" pcs conformance run --suite pf-core for suite in \ labtrust-qc-release-v0 \ diff --git a/scripts/verify-pf-core-adapter.sh b/scripts/verify-pf-core-adapter.sh new file mode 100644 index 0000000..9e22e82 --- /dev/null +++ b/scripts/verify-pf-core-adapter.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Thin wrapper retained for local/CI use. Delegates to run-pf-core-adapter-ci.sh. +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec bash "${ROOT}/scripts/run-pf-core-adapter-ci.sh" "$@" diff --git a/typescript/packages/core/src/index.ts b/typescript/packages/core/src/index.ts index 4b4eb97..b3d1004 100644 --- a/typescript/packages/core/src/index.ts +++ b/typescript/packages/core/src/index.ts @@ -1,5 +1,6 @@ export * from "./benchmarkIngest.js"; export * from "./hash.js"; +export * from "./pfCore.js"; export * from "./schema.js"; export * from "./status.js"; export * from "./validate.js"; diff --git a/typescript/packages/core/src/pfCore.ts b/typescript/packages/core/src/pfCore.ts new file mode 100644 index 0000000..2c5c2a1 --- /dev/null +++ b/typescript/packages/core/src/pfCore.ts @@ -0,0 +1,463 @@ +import { canonicalHash, canonicalJsonBytes } from "./hash.js"; + +export const GENESIS_HASH = + "sha256:0000000000000000000000000000000000000000000000000000000000000000"; + +const LEAN_CLAIM_CLASSES = new Set(["LeanKernelChecked"]); + +function stripDigestFields( + data: Record, + extraKeys: string[], +): Record { + const payload = { ...data }; + for (const key of extraKeys) { + delete payload[key]; + } + delete payload.signature_or_digest; + return payload; +} + +export function canonicalEventJsonBytes(event: Record): Uint8Array { + return canonicalJsonBytes(stripDigestFields(event, ["event_hash"])); +} + +export function canonicalTraceJsonBytes(trace: Record): Uint8Array { + return canonicalJsonBytes(stripDigestFields(trace, ["trace_hash"])); +} + +export function computeEventHash(event: Record): string { + return canonicalHash(stripDigestFields(event, ["event_hash"])); +} + +export function computeTraceHash(trace: Record): string { + return canonicalHash(stripDigestFields(trace, ["trace_hash"])); +} + +function normalizeHash(value: string): string { + const trimmed = value.trim(); + if (!trimmed.startsWith("sha256:") || trimmed.length !== 71) { + throw new Error(`invalid hash ${value}`); + } + return trimmed; +} + +export function validateClaimClassOverclaim( + claimClass: string, + proofRef?: unknown, + leanProofChecked?: unknown, +): string | null { + const hasProof = + typeof proofRef === "string" && proofRef.trim().length > 0; + if (LEAN_CLAIM_CLASSES.has(claimClass) && !hasProof) { + return `ClaimClassOverclaim: claim_class ${JSON.stringify(claimClass)} exceeds available assurance`; + } + if (claimClass === "CertificateChecked") { + return 'ClaimClassOverclaim: claim_class "CertificateChecked" exceeds available assurance'; + } + if (claimClass === "LeanKernelChecked" && leanProofChecked !== true) { + return "ClaimClassOverclaim: claim_class LeanKernelChecked requires lean_proof_checked=true"; + } + return null; +} + +export function validatePfcoreTraceHashChain(trace: Record): string[] { + const errors: string[] = []; + const events = trace.events; + if (!Array.isArray(events)) { + return ["TraceInvalid: events must be an array"]; + } + + let previous = normalizeHash(GENESIS_HASH); + for (let index = 0; index < events.length; index += 1) { + const base = `events[${index}]`; + const event = events[index]; + if (!event || typeof event !== "object" || Array.isArray(event)) { + errors.push(`EventInvalid: ${base} must be an object`); + continue; + } + const eventObj = event as Record; + try { + const prevField = normalizeHash(String(eventObj.previous_event_hash ?? "")); + if (prevField !== previous) { + errors.push( + `EventHashMismatch: previous_event_hash mismatch at ${base} (expected ${previous}, got ${prevField})`, + ); + } + const actualHash = normalizeHash(String(eventObj.event_hash ?? "")); + const expectedHash = computeEventHash(eventObj); + if (actualHash !== expectedHash) { + errors.push( + `EventHashMismatch: event_hash mismatch at ${base} (expected ${expectedHash}, got ${actualHash})`, + ); + } + previous = actualHash; + } catch { + errors.push(`EventHashMismatch: invalid event hash fields at ${base}`); + } + } + + if (trace.trace_hash !== undefined) { + try { + const actualTraceHash = normalizeHash(String(trace.trace_hash)); + const expectedTraceHash = computeTraceHash(trace); + if (actualTraceHash !== expectedTraceHash) { + errors.push( + `TraceHashMismatch: trace_hash mismatch (expected ${expectedTraceHash}, got ${actualTraceHash})`, + ); + } + } catch { + errors.push("TraceHashMismatch: invalid trace_hash"); + } + } + + if (typeof trace.claim_class === "string") { + const overclaim = validateClaimClassOverclaim( + trace.claim_class, + trace.proof_ref ?? trace.proof_term_ref, + trace.lean_proof_checked, + ); + if (overclaim) { + errors.push(overclaim); + } + } + + return errors; +} + +export function validatePfcoreCertificateSemantics( + certificate: Record, +): string[] { + const errors: string[] = []; + const claimClass = String(certificate.claim_class ?? ""); + const overclaim = validateClaimClassOverclaim( + claimClass, + certificate.proof_ref ?? certificate.proof_term_ref, + certificate.lean_proof_checked, + ); + if (overclaim) { + errors.push(overclaim); + } + if (claimClass === "LeanKernelChecked") { + if (certificate.lean_proof_checked !== true) { + errors.push("root: claim_class LeanKernelChecked requires lean_proof_checked=true"); + } + if ( + typeof certificate.proof_term_ref !== "string" || + certificate.proof_term_ref.trim().length === 0 + ) { + errors.push( + "root: claim_class LeanKernelChecked requires proof_term_ref (ClaimClassOverclaim)", + ); + } + const envHash = certificate.lean_environment_hash; + if (typeof envHash !== "string" || !envHash.startsWith("sha256:")) { + errors.push("root: claim_class LeanKernelChecked requires lean_environment_hash"); + } + const build = certificate.lean_build_status; + if ( + !build || + typeof build !== "object" || + Array.isArray(build) || + (build as Record).ok !== true + ) { + errors.push("root: lean_proof_checked requires lean_build_status.ok=true"); + } + } + return errors; +} + +const AUTHORIZATION_TO_DECISION: Record = { + authorized: "allow", + rejected: "deny", + unknown: "deny", + policy_missing: "deny", +}; + +function defaultFieldLayer(section: string, field: string): string { + const key = `${section}.${field}`; + const mapping: Record = { + "pre.require_capability": "lean", + "pre.require_effect": "lean", + "pre.require_tenant_match": "lean", + "pre.require_role": "runtime", + "pre.require_policy_ref": "runtime", + "pre.require_evidence_ref": "runtime", + "post.require_decision": "lean", + "post.require_event_safe": "lean", + "invariant.require_trace_safe": "lean", + }; + return mapping[key] ?? "runtime"; +} + +function fieldLayer(contract: Record, section: string, field: string): string { + const semantics = contract.semantics_layer; + if (semantics && typeof semantics === "object" && !Array.isArray(semantics)) { + const layer = (semantics as Record)[field]; + if (typeof layer === "string") { + return layer; + } + } + return defaultFieldLayer(section, field); +} + +function principalHasCapability(principal: Record, capabilityId: string): boolean { + const caps = principal.capabilities; + if (!Array.isArray(caps)) { + return false; + } + return caps.some((cap) => String(cap) === capabilityId); +} + +function actionHasEffect(action: Record, effectKind: string): boolean { + const effects = action.effects; + if (!Array.isArray(effects)) { + return false; + } + return effects.some( + (effect) => + effect && + typeof effect === "object" && + !Array.isArray(effect) && + String((effect as Record).effect_kind ?? "") === effectKind, + ); +} + +function tenantMatches(principal: Record, action: Record): boolean { + const tenant = String(principal.tenant ?? ""); + for (const key of ["reads", "writes"]) { + const resources = action[key]; + if (!Array.isArray(resources)) { + continue; + } + for (const resource of resources) { + if ( + resource && + typeof resource === "object" && + !Array.isArray(resource) && + String((resource as Record).tenant ?? "") !== tenant + ) { + return false; + } + } + } + return true; +} + +export function validateEventAgainstContract( + event: Record, + contract: Record, + path: string, +): string[] { + const errors: string[] = []; + const principal = event.principal; + const action = event.action; + if (!principal || typeof principal !== "object" || Array.isArray(principal)) { + return [`ContractEventInvalid: event missing principal or action at ${path}`]; + } + if (!action || typeof action !== "object" || Array.isArray(action)) { + return [`ContractEventInvalid: event missing principal or action at ${path}`]; + } + const principalObj = principal as Record; + const actionObj = action as Record; + const contractId = String(contract.contract_id ?? ""); + + const pre = contract.pre; + if (pre && typeof pre === "object" && !Array.isArray(pre)) { + const preObj = pre as Record; + if ( + preObj.require_tenant_match === true && + fieldLayer(contract, "pre", "require_tenant_match") !== "out_of_scope" && + !tenantMatches(principalObj, actionObj) + ) { + errors.push( + `ContractTenantMismatch: contract ${JSON.stringify(contractId)} requires tenant match at ${path}`, + ); + } + const requiredCap = preObj.require_capability; + if ( + typeof requiredCap === "string" && + requiredCap && + fieldLayer(contract, "pre", "require_capability") !== "out_of_scope" && + !principalHasCapability(principalObj, requiredCap) + ) { + errors.push( + `ContractCapabilityRequired: contract ${JSON.stringify(contractId)} requires capability ${JSON.stringify(requiredCap)} at ${path}.principal`, + ); + } + const requiredEffect = preObj.require_effect; + if ( + typeof requiredEffect === "string" && + requiredEffect && + fieldLayer(contract, "pre", "require_effect") !== "out_of_scope" && + !actionHasEffect(actionObj, requiredEffect) + ) { + errors.push( + `ContractEffectRequired: contract ${JSON.stringify(contractId)} requires effect ${JSON.stringify(requiredEffect)} at ${path}.action.effects`, + ); + } + const requiredRole = preObj.require_role; + if ( + typeof requiredRole === "string" && + requiredRole && + fieldLayer(contract, "pre", "require_role") !== "out_of_scope" + ) { + const roles = principalObj.roles; + const hasRole = + Array.isArray(roles) && roles.some((role) => String(role) === requiredRole); + if (!hasRole) { + errors.push( + `ContractRoleRequired: contract ${JSON.stringify(contractId)} requires role ${JSON.stringify(requiredRole)} at ${path}.principal.roles`, + ); + } + } + const requiredPolicy = preObj.require_policy_ref; + if ( + typeof requiredPolicy === "string" && + requiredPolicy && + fieldLayer(contract, "pre", "require_policy_ref") !== "out_of_scope" + ) { + const refs = event.contract_refs; + const hasRef = + Array.isArray(refs) && refs.some((ref) => String(ref) === requiredPolicy); + if (!hasRef) { + errors.push( + `ContractPolicyRefRequired: contract ${JSON.stringify(contractId)} requires policy ref ${JSON.stringify(requiredPolicy)} at ${path}.contract_refs`, + ); + } + } + const requiredEvidence = preObj.require_evidence_ref; + if ( + typeof requiredEvidence === "string" && + requiredEvidence && + fieldLayer(contract, "pre", "require_evidence_ref") !== "out_of_scope" + ) { + const evidence = event.evidence_refs; + const hasRef = + Array.isArray(evidence) && evidence.some((ref) => String(ref) === requiredEvidence); + if (!hasRef) { + errors.push( + `ContractEvidenceRefRequired: contract ${JSON.stringify(contractId)} requires evidence ref ${JSON.stringify(requiredEvidence)} at ${path}.evidence_refs`, + ); + } + } + } + + const post = contract.post; + if (post && typeof post === "object" && !Array.isArray(post)) { + const postObj = post as Record; + const requiredDecision = postObj.require_decision; + if ( + typeof requiredDecision === "string" && + requiredDecision && + fieldLayer(contract, "post", "require_decision") !== "out_of_scope" + ) { + const decision = String(event.decision ?? ""); + if (decision !== requiredDecision) { + errors.push( + `ContractDecisionMismatch: contract ${JSON.stringify(contractId)} requires decision ${JSON.stringify(requiredDecision)}, got ${JSON.stringify(decision)} at ${path}.decision`, + ); + } + } + if ( + postObj.require_event_safe === true && + fieldLayer(contract, "post", "require_event_safe") !== "out_of_scope" + ) { + const decision = String(event.decision ?? ""); + if (decision === "allow") { + const capability = actionObj.capability; + const capId = + capability && + typeof capability === "object" && + !Array.isArray(capability) + ? String((capability as Record).capability_id ?? "") + : ""; + if (!capId || !principalHasCapability(principalObj, capId)) { + errors.push( + `ContractEventUnsafe: allowed event violates contract ${JSON.stringify(contractId)} event safety at ${path}`, + ); + } else if (!tenantMatches(principalObj, actionObj)) { + errors.push( + `ContractEventUnsafe: allowed event violates contract ${JSON.stringify(contractId)} tenant safety at ${path}`, + ); + } + } + } + } + + return errors; +} + +export function validateTraceContracts( + trace: Record, + contracts: Record>, +): string[] { + const errors: string[] = []; + const events = trace.events; + if (!Array.isArray(events)) { + return ["TraceInvalid: events must be an array"]; + } + for (let index = 0; index < events.length; index += 1) { + const base = `events[${index}]`; + const event = events[index]; + if (!event || typeof event !== "object" || Array.isArray(event)) { + continue; + } + const refs = (event as Record).contract_refs; + if (!Array.isArray(refs) || refs.length === 0) { + continue; + } + for (let refIndex = 0; refIndex < refs.length; refIndex += 1) { + const contractId = String(refs[refIndex] ?? ""); + const contract = contracts[contractId]; + if (!contract) { + errors.push( + `ContractRefMissing: unknown contract reference ${JSON.stringify(contractId)} at ${base}.contract_refs[${refIndex}]`, + ); + continue; + } + errors.push( + ...validateEventAgainstContract(event as Record, contract, base), + ); + } + } + return errors; +} + +export function validateDeniedEventsPreserved( + toolUseTrace: Record, + pfcoreTrace: Record, +): string[] { + const toolCalls = toolUseTrace.tool_calls; + if (!Array.isArray(toolCalls)) { + return []; + } + const events = pfcoreTrace.events; + if (!Array.isArray(events)) { + return [ + 'DroppedDeniedEvent: denied event "" missing from compiled trace (at events)', + ]; + } + const compiledIds = new Set( + events + .filter((event) => event && typeof event === "object" && !Array.isArray(event)) + .map((event) => String((event as Record).event_id ?? "")), + ); + const errors: string[] = []; + for (const toolCall of toolCalls) { + if (!toolCall || typeof toolCall !== "object" || Array.isArray(toolCall)) { + continue; + } + const auth = String((toolCall as Record).authorization_status ?? ""); + const decision = AUTHORIZATION_TO_DECISION[auth] ?? "deny"; + if (decision !== "deny") { + continue; + } + const eventId = String((toolCall as Record).event_id ?? ""); + if (eventId && !compiledIds.has(eventId)) { + errors.push( + `DroppedDeniedEvent: denied event ${JSON.stringify(eventId)} missing from compiled trace (at events)`, + ); + } + } + return errors; +} diff --git a/typescript/packages/core/src/tests/examples.test.ts b/typescript/packages/core/src/tests/examples.test.ts index b6c58aa..7b02333 100644 --- a/typescript/packages/core/src/tests/examples.test.ts +++ b/typescript/packages/core/src/tests/examples.test.ts @@ -5,13 +5,28 @@ import { fileURLToPath } from "node:url"; import test from "node:test"; import { canonicalHash, canonicalJsonBytes } from "../hash.js"; -import { detectArtifactType, validateArtifact, ValidationError } from "../validate.js"; +import { + canonicalEventJsonBytes, + canonicalTraceJsonBytes, + computeEventHash, + computeTraceHash, + validateClaimClassOverclaim, + validateDeniedEventsPreserved, + validatePfcoreTraceHashChain, + validateTraceContracts, +} from "../pfCore.js"; +import { detectArtifactType, validateArtifact, ValidationError, type ArtifactType } from "../validate.js"; const examplesDir = join(dirname(fileURLToPath(import.meta.url)), "../../../../../examples"); const vectorsDir = join( dirname(fileURLToPath(import.meta.url)), "../../../../../python/tests/hash_vectors", ); +const pfCoreVectorsDir = join( + dirname(fileURLToPath(import.meta.url)), + "../../../../../python/tests/hash_vectors/pf_core", +); +const pfCoreInvalidVectorsDir = join(pfCoreVectorsDir, "invalid"); const sharedVectorsDir = join( dirname(fileURLToPath(import.meta.url)), "../../../../../test_vectors/hash", @@ -55,7 +70,7 @@ test("benchmark ingest examples include artifact refs", () => { }); test("PF-Core explicit artifact_type detection", () => { - const cases: Array<[string, ArtifactType]> = [ + const cases: Array<[string, string]> = [ ["pf-core-valid/tool_use_trace_compiled/pfcore_trace.json", "PFCoreTrace.v0"], ["pf-core-valid/assumption_declared/certificate.json", "PFCoreCertificate.v0"], ]; @@ -187,6 +202,69 @@ test("hash vectors match frozen fixtures", () => { } }); +test("pf-core hash vectors match frozen fixtures", () => { + for (const artifact of ["PFCoreEvent.v0", "PFCoreTrace.v0"]) { + const dir = join(pfCoreVectorsDir, artifact); + const data = JSON.parse(readFileSync(join(dir, "input.json"), "utf8")) as Record< + string, + unknown + >; + const expectedCanonical = readFileSync(join(dir, "canonical.txt"), "utf8").trim(); + const expectedDigest = readFileSync(join(dir, "digest.txt"), "utf8").trim(); + const canonicalBytes = + artifact === "PFCoreEvent.v0" + ? canonicalEventJsonBytes(data) + : canonicalTraceJsonBytes(data); + assert.equal(Buffer.from(canonicalBytes).toString("utf8"), expectedCanonical); + if (artifact === "PFCoreEvent.v0") { + assert.equal(computeEventHash(data), expectedDigest); + } else { + assert.equal(computeTraceHash(data), expectedDigest); + } + } +}); + +test("pf-core negative hash vectors parity", () => { + const trace = JSON.parse( + readFileSync(join(pfCoreInvalidVectorsDir, "trace_hash_chain_break.json"), "utf8"), + ) as Record; + const hashErrors = validatePfcoreTraceHashChain(trace); + assert.ok(hashErrors.some((err) => err.includes("EventHashMismatch"))); + + const overclaimTrace = JSON.parse( + readFileSync(join(pfCoreInvalidVectorsDir, "claim_class_overclaim_trace.json"), "utf8"), + ) as Record; + const overclaimErrors = validatePfcoreTraceHashChain(overclaimTrace); + assert.ok(overclaimErrors.some((err) => err.includes("ClaimClassOverclaim"))); + + const contractDir = join(pfCoreInvalidVectorsDir, "contract_capability_missing"); + const contractTrace = JSON.parse( + readFileSync(join(contractDir, "trace.json"), "utf8"), + ) as Record; + const contract = JSON.parse( + readFileSync(join(contractDir, "contract.json"), "utf8"), + ) as Record; + const contractId = String(contract.contract_id ?? ""); + const contractErrors = validateTraceContracts(contractTrace, { [contractId]: contract }); + assert.ok(contractErrors.some((err) => err.includes("ContractCapabilityRequired"))); + + const deniedDir = join(pfCoreInvalidVectorsDir, "denied_event_dropped"); + const toolUse = JSON.parse( + readFileSync(join(deniedDir, "tool_use_trace.json"), "utf8"), + ) as Record; + const pfcore = JSON.parse( + readFileSync(join(deniedDir, "pfcore_trace.json"), "utf8"), + ) as Record; + const deniedErrors = validateDeniedEventsPreserved(toolUse, pfcore); + assert.ok(deniedErrors.some((err) => err.includes("DroppedDeniedEvent"))); + + assert.ok( + validateClaimClassOverclaim("LeanKernelChecked", undefined, undefined)?.includes( + "ClaimClassOverclaim", + ), + ); +}); + test("shared hash vectors match test_vectors/hash fixtures", () => { for (const fileName of readdirSync(sharedVectorsDir)) { if (!fileName.endsWith(".vector.json")) { diff --git a/typescript/packages/core/src/validate.ts b/typescript/packages/core/src/validate.ts index d80334f..1816d93 100644 --- a/typescript/packages/core/src/validate.ts +++ b/typescript/packages/core/src/validate.ts @@ -2,6 +2,10 @@ import { validateBenchmarkArtifactRefSemantics, validatePcsBenchIngestSemantics, } from "./benchmarkIngest.js"; +import { + validatePfcoreCertificateSemantics, + validatePfcoreTraceHashChain, +} from "./pfCore.js"; import { ARTIFACT_STATUSES, TRACE_CERTIFICATE_STATUSES } from "./status.js"; import { isZeroSourceCommit } from "./hash.js"; import { validateSchema } from "./schema.js"; @@ -533,6 +537,12 @@ export function validateArtifact( errors.push(`TraceCertificate.v0 invalid status ${status}`); } } + if (type === "PFCoreTrace.v0") { + errors.push(...validatePfcoreTraceHashChain(data)); + } + if (type === "PFCoreCertificate.v0") { + errors.push(...validatePfcoreCertificateSemantics(data)); + } if (type === "BenchmarkArtifactRef.v0") { errors.push(...validateBenchmarkArtifactRefSemantics(data)); }