diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9cb9613..4d2beea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,9 +17,15 @@ jobs: run: | cd python pip install -e ".[dev]" + 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 pytest -q tests/test_protocol_conformance.py pcs schema check + pcs pf-core audit-claims + pcs pf-core audit-boundary + pcs pf-core audit-lean-catalog + pcs pf-core audit-lean-no-sorry pcs examples check pcs validate-release-chain ../examples/labtrust-release/ pcs validate-release-chain ../examples/labtrust-release/ --json > /dev/null @@ -69,13 +75,50 @@ jobs: pytest -q tests/test_multidomain_workflows.py ruff check pcs_core tests ruff format --check pcs_core tests + - name: PF-Core fixture validation + run: | + cd python + 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) + 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 - 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: LabTrust release fixtures run: | cd python pcs validate-release-chain ../examples/labtrust-release/ + lean: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install elan + run: 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 + - name: Build Lean libraries and PF-Core lean-check + run: | + cd lean + elan default leanprover/lean4:v4.14.0 + lake build PCS + lake build PFCore + cd ../python + pip install -e . + pcs pf-core lean-check --trace ../examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json + pcs pf-core validate-contracts \ + ../examples/pf-core-valid/contract_checked/trace.json \ + --contracts-dir ../examples/pf-core-valid/contract_checked + rust: runs-on: ubuntu-latest steps: @@ -106,17 +149,6 @@ jobs: npm run test:hash-vectors -w @pcs/core npm run lint - lean: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install Lean - run: | - curl -sSfL https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh | sh -s -- -y --default-toolchain stable - echo "$HOME/.elan/bin" >> "$GITHUB_PATH" - - name: Build Lean trust-boundary skeleton - run: cd lean && lake build - validate-cli-contract: runs-on: ubuntu-latest needs: python diff --git a/.gitignore b/.gitignore index 2805ac6..6963242 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ typescript/packages/core/dist/ lake-packages/ lean/lakefile.olean.trace lean/build/ +lean/PFCore/Generated/*.lean +!lean/PFCore/Generated/.gitkeep .DS_Store *.swp diff --git a/docs/pf-core-trace-mapping.md b/docs/pf-core-trace-mapping.md new file mode 100644 index 0000000..b26661e --- /dev/null +++ b/docs/pf-core-trace-mapping.md @@ -0,0 +1,83 @@ +# PF-Core trace ↔ PCS trace_certificate mapping + +Phase 7 documents how PCS release artifacts relate to PF-Core traces and certificates. PCS and PF-Core use **different certificate schemas**; this document records field correspondence and shared hash rules. It does not unify schemas. + +**Reference implementation:** [provability-fabric-core](https://github.com/SentinelOps-CI/provability-fabric-core) tag `pf-core-v0.6.0`, adapter `adapters/pcs/normalize_release.py`. + +**PF-Core hash and certificate semantics:** [certificate-semantics.md](https://github.com/SentinelOps-CI/provability-fabric-core/blob/pf-core-v0.6.0/pf-core/docs/certificate-semantics.md) (hash chain section). + +--- + +## PCS `trace_certificate` (v0) vs PF-Core artifacts + +| PCS `trace_certificate` field | PF-Core equivalent | Notes | +|------------------------------|-------------------|-------| +| `certificate_id` | `certificate.certificate_id` | PCS uses `cert-*` naming; PF-Core generates `cert-{uuid}` | +| `schema_version` | `certificate.schema_version` | PCS `v0` vs PF-Core `pf-core.certificate.v0` | +| `trace_hash` | `trace.trace_hash`, `certificate.trace_hash` | PCS uses `sha256:` prefix; PF-Core stores hex64 — normalize at boundary | +| `spec_hash` | `certificate.contract_hash` | PCS spec hash binds to PF-Core contract hash | +| `property_id` | (none) | PCS property identifier; map to `policy_ref` or contract id when bridging | +| `checker` | `certificate.checker` | PCS `certifyedge` vs PF-Core `lean4` | +| `checker_version` | `certificate.checker_version` | Version strings differ by design | +| `status` | `certificate.safe` | `CertificateChecked` + `safe: true` documented equivalence | +| `counterexample_ref` | (none) | PCS-only; out of PF-Core scope | +| `created_at` | (none) | Organizational metadata | +| `producer` / `producer_version` | `certificate.created_by` | Optional PF-Core field | +| `source_repo` / `source_commit` | `certificate.proof_ref` | Different semantics; cross-reference only | +| `signature_or_digest` | (none) | PCS bundle integrity; post-incident-proofs layer | + +--- + +## Event / trace hash rules (shared) + +Both repos agree on: + +1. **Genesis** `previous_event_hash`: 64 ASCII `0` characters. +2. **Canonical JSON:** sorted keys, minimal separators (`separators=(",", ":")`). +3. **`event_hash`:** `sha256(payload)` as lowercase hex; accept `sha256:` prefix at validation. +4. **`trace_hash`:** `sha256(canonical_json(trace \ {trace_hash}))`. + +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. + +--- + +## LabTrust replay path + +Pinned LabTrust release fixtures (e.g. `examples/labtrust/trace_certificate.valid.json`) feed PF-Core traces via the untrusted adapter: + +``` +labtrust-release/trace_certificate.valid.json + → normalize_release.normalize_labtrust_release() + → pf-core/examples/valid/pcs_replay_trace.json +``` + +### Release directory fields + +| LabTrust / PCS artifact | Role in PF-Core trace | +|-------------------------|----------------------| +| `trace_certificate.valid.json` | Source `trace_hash` / `spec_hash` for mapping docs; adapter reads when present | +| Release observation (when emitted) | Compiles to `pf-core.event.v1` via `compile-observation` | +| PCS `trace_hash` (with `sha256:` prefix) | Normalizes to hex64 for `trace.trace_hash` binding | +| PCS `spec_hash` | Maps to `certificate.contract_hash` when emitting PF-Core certificate | + +The reference adapter builds a single-event trace with `lab.release` effect, principal `lab-operator-1`, and genesis hash chain. See `pcs_replay_trace.json` in provability-fabric-core. + +### Verification command + +```bash +PYTHONPATH=pf-core/validator python -m pf_core.cli core check-trace \ + --schemas pf-core/schemas \ + --file pf-core/examples/valid/pcs_replay_trace.json +``` + +--- + +## Assurance boundary + +| Layer | Claim | +|-------|-------| +| PF-Core `safe: true` | T1 (Lean-proved) + T4 (runtime deciders) | +| PCS `CertificateChecked` | PCS checker semantics | +| This mapping doc | Organizational cross-reference only | + +PCS does not expand PF-Core TCB. Policy alignment remains vector-tested in provability-fabric-core. diff --git a/docs/pf-core/assumptions.md b/docs/pf-core/assumptions.md new file mode 100644 index 0000000..3b197c3 --- /dev/null +++ b/docs/pf-core/assumptions.md @@ -0,0 +1,50 @@ +# PF-Core assumptions + +PF-Core proofs and certificates are conditional on explicit assumptions. This document lists assumptions baked into the current implementation (Stages 1–7) and assumptions deferred to later research. + +## 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. + +## Producer assumptions + +- Runtime producers (`AgentRuntime`, LabTrust-Gym, adapters) emit faithful observations unless contradicted by hash-chain validation. +- Adapters that compile PCS artifacts into PF-Core traces are **untrusted**; their output must pass schema and semantic validation. + +## Lean bridge (Stages 3–4) + +- Default `pcs pf-core lean-check` runs Python deciders aligned with `lean/PFCore/` predicates, then generates a concrete proof file under `lean/PFCore/Generated/` and checks it with `lake env lean`. +- `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. + +## Role and capability alignment (permanent boundary) + +- Lean `HasCapability` inspects `principal.capabilities` only; **roles are not expanded in the kernel**. +- 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. + +## Registry assumptions + +- Registry semantic checks marked `allowed_to_skip: true` are not treated as proved. +- Deferred checks must surface as `AssumptionDeclared` or `OutOfScope` claim classes, never `LeanKernelChecked` or unjustified `ProofChecked`. +- PF-Core certificates with skipped deferrable checks require non-empty `assumption_refs` pointing at `AssumptionSet.v0` ids or documented paths under `docs/pf-core/`. + +## Out of scope (not assumed by PF-Core) + +- Model correctness or alignment. +- Clinical or production safety of real-world systems. +- Natural-language policy interpretation. +- Global non-interference across tenants before event-level trace safety is proved. + +## Assumption artifacts + +Domain assumptions must be recorded in `AssumptionSet.v0` (PCS) or referenced from PF-Core certificates. Simulation scope disclaimers from the LabTrust profile remain mandatory for QC-release demonstrations. Prefer citing `assumption_set_id` values (for example `as-labtrust-qc-v0.1`) over documentation paths alone when issuing `AssumptionDeclared` certificates. + +## Contract and invariant simplification + +See [contract-semantics.md](contract-semantics.md) for the mapping between `PFCoreContract.v0` JSON fields and runtime/Lean predicates. diff --git a/docs/pf-core/bridge-artifact.md b/docs/pf-core/bridge-artifact.md new file mode 100644 index 0000000..d296a47 --- /dev/null +++ b/docs/pf-core/bridge-artifact.md @@ -0,0 +1,70 @@ +# PCS TraceCertificate to PFCoreCertificate bridge artifact + +This document specifies the organizational bridge between PCS release-layer +`TraceCertificate.v0` artifacts and PF-Core `PFCoreCertificate.v0` claim objects. +It is a cross-reference spec for demos and adapters; it does not unify schemas. + +## Purpose + +PCS science bundles attach `TraceCertificate.v0` objects checked by external +tools (e.g. CertifyEdge). PF-Core certificates carry explicit `claim_class` +values that state what assurance was obtained. The bridge maps PCS checker output +into PF-Core certificate fields without expanding the PF-Core TCB. + +## Field mapping + +| PCS `TraceCertificate.v0` | PF-Core `PFCoreCertificate.v0` | Bridge rule | +|---------------------------|-------------------------------|-------------| +| `certificate_id` | `certificate_id` | Prefix optional; preserve id for cross-reference | +| `trace_hash` | `trace_hash` | Normalize `sha256:` prefix at boundary | +| `spec_hash` | `contract_hash` | Direct bind | +| `checker` | `checker` | Copy verbatim | +| `checker_version` | `checker_version` | Copy verbatim | +| `status: CertificateChecked` | `claim_class: CertificateChecked` | Required when external checker attests | +| `source_repo` | `source_repo` | Copy verbatim | +| `source_commit` | `source_commit` | Copy verbatim | +| `property_id` | (none) | Record in trace event `contract_refs` when compiling | +| `counterexample_ref` | (none) | PCS-only; PF-Core `OutOfScope` if replay needed | +| `signature_or_digest` | (none) | PCS bundle integrity; recompute PF-Core digest | + +## Claim class selection + +| Bridge operation | Required `claim_class` | +|------------------|------------------------| +| External checker attestation only | `CertificateChecked` | +| PF-Core runtime compiler + hash chain | `RuntimeChecked` | +| Deterministic hash replay | `ReplayValidated` | +| Lean kernel concrete proof | `LeanKernelChecked` | +| Deferred registry checks documented | `AssumptionDeclared` | + +Do not emit `LeanKernelChecked` from the bridge when only PCS +`CertificateChecked` status is available. + +## Bridge workflow (demo) + +``` +examples/labtrust/trace_certificate.valid.json + → pf_core_labtrust_adapter.normalize_labtrust_release() + → examples/pf-core-valid/labtrust_replay/trace.json + → pcs pf-core replay-trace trace.json + → pcs pf-core attach-certificate-check --trace trace.json ... + → PFCoreCertificate.v0 (claim_class: CertificateChecked) +``` + +## Assurance boundary + +| Layer | What the bridge claims | +|-------|------------------------| +| PCS `CertificateChecked` | External checker semantics for the declared property | +| PF-Core `CertificateChecked` | Same attestation recorded in PF-Core schema | +| PF-Core `ReplayValidated` | Hash chain reproducibility only | +| PF-Core `LeanKernelChecked` | Not implied by this bridge | + +Adapters that perform the bridge are **untrusted**. All bridged artifacts must +pass PCS schema and PF-Core semantic validation before release. + +## Related documents + +- [claim-boundary.md](claim-boundary.md) — claim class definitions +- [pf-core-trace-mapping.md](../pf-core-trace-mapping.md) — trace field mapping +- [assumptions.md](assumptions.md) — deferred registry obligations diff --git a/docs/pf-core/claim-boundary.md b/docs/pf-core/claim-boundary.md new file mode 100644 index 0000000..9a74ed5 --- /dev/null +++ b/docs/pf-core/claim-boundary.md @@ -0,0 +1,108 @@ +# PF-Core claim boundary + +PF-Core separates **lifecycle status** (PCS `status` field) from **claim class** (what assurance was obtained). A certificate may carry `status: CertificateChecked` while `claim_class: RuntimeChecked` if only runtime predicates were evaluated. + +## Allowed claim classes + +| Claim class | Meaning | +|-------------|---------| +| `SchemaValidated` | JSON Schema and PF-Core semantic checks passed | +| `RuntimeChecked` | Runtime observation compiled and hash chain validated | +| `CertificateChecked` | External checker attestation (e.g. CertifyEdge) | +| `LeanKernelChecked` | Concrete trace obligation proved in the Lean kernel (`traceSafeD tr = true`) | +| `ReplayValidated` | Trace replayed and hashes reproduced | +| `AssumptionDeclared` | Claim rests on documented assumptions only (required when registry checks are deferred) | +| `OutOfScope` | Explicitly outside PF-Core trusted kernel | + +Do **not** use PCS `ProofChecked` alone as a PF-Core formal claim. + +## Forbidden public phrases + +The claim-boundary linter (`pcs pf-core audit-claims`) fails on these phrases in `docs/` and `examples/`: + +| Forbidden phrase | Use instead | +|------------------|-------------| +| verified agent | trace-level safety preservation under stated assumptions | +| guarantees AI safety | contracted action safety under stated assumptions | +| model is safe | schema-validated runtime observation | +| agent is safe | Lean-kernel-checked trace theorem (only when `claim_class` is `LeanKernelChecked`) | +| 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) + +PF-Core `pcs pf-core lean-check` emits a registered `LeanCheckResult.v0` object alongside optional `PFCoreCertificate.v0` output. + +### Current behavior (Stage 4) + +- `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. +- `status: Rejected` means one or more checks failed. +- `claim_class` is mandatory: `LeanKernelChecked` only when `lean_proof_checked` is true; otherwise `RuntimeChecked` on success or `OutOfScope` on failure. +- `obligations[]` records concrete checks performed (deciders and, when run, `ConcreteTraceSafe`). +- `theorems_checked` lists catalog theorem symbols the check family is aligned with. +- `assumption_refs` points at documented assumptions (`docs/pf-core/assumptions.md`, `docs/pf-core/trusted-boundary.md`). +- `lean_build_status` records whether `lake build PFCore` ran and its outcome. +- `lean_environment_hash` (optional) fingerprints the pinned Lean toolchain and lake manifest. +- `disclaimer` states the assurance boundary for the selected pipeline mode. + +Do **not** treat PCS lifecycle `ProofChecked` as a PF-Core formal claim. PF-Core lean-check does not emit unqualified `ProofChecked`. + +### PFCoreCertificate.v0 output + +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. +- `--skip-build` or `--skip-lean-proof` yields `RuntimeChecked` only (no `proof_term_ref`). + +### Mapping guidance for PF-Core certificates + +| Actual check performed | Required `claim_class` | +|------------------------|------------------------| +| Schema validation only | `SchemaValidated` | +| Runtime compiler + hash chain | `RuntimeChecked` | +| External checker | `CertificateChecked` | +| Lean theorem + concrete proof term | `LeanKernelChecked` | +| Documented assumption only | `AssumptionDeclared` | +| Deterministic trace replay | `ReplayValidated` | +| Outside kernel | `OutOfScope` | + +## ReplayValidated (`pcs pf-core replay-trace`) + +Stage 5 adds deterministic trace replay: + +- Loads `PFCoreTrace.v0` (or recompiles from `ToolUseTrace.v0` / `PFCoreRuntimeObservation.v0` when `--source` is provided). +- Recomputes event hashes and `trace_hash` using the same rules as the runtime compiler. +- Emits `PFCoreCertificate.v0` or `LeanCheckResult.v0` with `claim_class: ReplayValidated` only when stored and recomputed hashes match. +- On mismatch, emits `claim_class: OutOfScope` or `Rejected` with detailed diff entries in `issues[]`. + +Replay validates hash-chain integrity and compiler determinism. It does not imply `LeanKernelChecked`. + +## AssumptionDeclared enforcement + +Registry semantic checks marked `allowed_to_skip: true` (for example `lean_kernel_proof` and `lean_library_build` on `PFCoreCertificate.v0`) are not treated as proved when skipped. + +When such checks are deferred: + +- Certificates must **not** claim `LeanKernelChecked` or PCS `ProofChecked`. +- Certificates must include non-empty `assumption_refs` pointing to `AssumptionSet.v0` ids (for example `as-labtrust-qc-v0.1`) or documented paths under `docs/pf-core/`. +- Prefer `claim_class: AssumptionDeclared` when assurance rests on documented assumptions only. + +`enforce_assumption_declared()` in `registry_data.py` implements these rules; `validate.py` applies them to `PFCoreCertificate.v0`. + +## CertificateChecked vs LeanKernelChecked + +| Aspect | `CertificateChecked` | `LeanKernelChecked` | +|--------|---------------------|---------------------| +| Checker | External (e.g. CertifyEdge) | PF-Core Lean kernel (`traceSafeD`) | +| Proof artifact | External attestation ref | Generated `proof_term_ref` | +| Lean build | Not required | Required (`lake build PFCore`) | +| Typical source | PCS `TraceCertificate.v0` bridge | `pcs pf-core lean-check` full pipeline | + +`pcs pf-core attach-certificate-check` wraps an external checker attestation into `PFCoreCertificate.v0` with `claim_class: CertificateChecked`. It does not run Lean proof or imply kernel-checked assurance. + +See [bridge-artifact.md](bridge-artifact.md) for PCS `TraceCertificate.v0` mapping. + +## Release-envelope vs agent safety + +PCS Lean theorems in `lean/PCS/Theorems.lean` belong to the **release-envelope consistency theorem family**. PF-Core Lean theorems in `lean/PFCore/` model trace-level action safety. Documentation and certificates must not conflate the two families. diff --git a/docs/pf-core/contract-semantics.md b/docs/pf-core/contract-semantics.md new file mode 100644 index 0000000..e8fc133 --- /dev/null +++ b/docs/pf-core/contract-semantics.md @@ -0,0 +1,67 @@ +# PF-Core contract semantics + +This document maps `PFCoreContract.v0` JSON fields to runtime checker predicates, Lean `ContractDecide` deciders, and generated proof obligations. + +## Lean structures + +| Module | Role | +|--------|------| +| `lean/PFCore/Contract.lean` | Prop-level `Contract`, `SatisfiesContract`, sequential composition | +| `lean/PFCore/ContractDecide.lean` | Decidable JSON contract specs + soundness theorems | + +### ContractDecide specs + +| Lean structure | JSON source | +|----------------|-------------| +| `ContractPreSpec` | `pre` object | +| `ContractPostSpec` | `post` object | +| `ContractInvariantSpec` | `invariant` object | + +## 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_*` | + +Per-event discharge: `satisfiesContractSpecD`. Trace-level: `traceSatisfiesContractSpecsD`. + +## Lean codegen pipeline + +When `contract_refs` appear on events and contract JSON is found alongside the trace: + +1. `pcs pf-core validate-contracts` (runtime) — required before `lean-check` succeeds. +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. + +## Invariant preservation (Lean) + +The canonical trace-safety invariant is `TraceSafe`. Lean proves conservative preservation: + +```lean +theorem trace_safe_invariant_preserved_cons (tr : Trace) (ev : Event) : + TraceSafe tr → EventSafe ev → TraceSafe (Trace.cons tr ev) +``` + +Arbitrary user-defined `Contract.invariant` functions are not automatically preserved under `cons` without additional structure. + +## When to use which checker + +| Goal | Command | +|------|---------| +| JSON contract pre/post on events | `pcs pf-core validate-contracts` | +| Hash chain + EventSafe deciders | `pcs pf-core validate-trace` | +| Tenant isolation (conservative) | `pcs pf-core validate-trace --tenant-isolation` | +| Kernel proof of trace/event + contract deciders | `pcs pf-core lean-check --trace …` | +| External temporal/property checker | `pcs pf-core certifyedge-check --trace … --property …` | + +See `docs/pf-core/non-interference.md` for tenant-scoped non-interference claims. diff --git a/docs/pf-core/current-gap-audit.md b/docs/pf-core/current-gap-audit.md new file mode 100644 index 0000000..d1978c7 --- /dev/null +++ b/docs/pf-core/current-gap-audit.md @@ -0,0 +1,86 @@ +# PF-Core gap audit + +Summary of gaps between the PF-Core vision and the current `pcs-core` repository. + +## Protocol and schemas + +| Gap | Status | Notes | +|-----|--------|-------| +| PF-Core artifact JSON schemas | Done | Stage 2 | +| `LeanCheckResult.v0` JSON schema | Done | Stage 4 | +| PFCoreCertificate proof artifacts | Done | Stage 4 | +| Replay certificate fields | Done | Stage 5 | +| ToolUseTrace optional `handoffs` | Done | Stage 7 | + +## Lean kernel + +| Gap | Status | Notes | +|-----|--------|-------| +| Release-envelope theorems | Present | `lean/PCS/Theorems.lean` | +| Agent safety predicates (`EventSafe`, `TraceSafe`) | Done | Stage 3 `lean/PFCore/` | +| Concrete Lean proof terms per trace obligation | Done | Stage 4 codegen + `lake env lean` | +| Handoff non-expansion (`HandoffSafe`) | Done | `lean/PFCore/Handoff.lean` + runtime | + +## Validation and claims + +| Gap | Status | Notes | +|-----|--------|-------| +| `pcs pf-core lean-check` CLI | Done | Stage 3–4 | +| `pcs pf-core replay-trace` | Done | Stage 5 | +| `pcs pf-core validate-contracts` | Done | Stage 7 | +| 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` | + +## Remaining research (deferred) + +1. **Full provability-fabric-core live adapter CI** — hash parity covered natively; full adapter orchestration remains cross-repo. +2. **Full agent runtime, MCP, NL policy, model safety** — out of scope. + +## 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 | + +### Honest limitations (Phase F) + +- 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`. diff --git a/docs/pf-core/mission.md b/docs/pf-core/mission.md new file mode 100644 index 0000000..1c75e2b --- /dev/null +++ b/docs/pf-core/mission.md @@ -0,0 +1,56 @@ +# PF-Core mission + +PF-Core is the minimal trusted action-trace kernel inside PCS. PCS defines evidence containers and release-chain artifacts; PF-Core defines the formal semantics of agentic actions, contracted traces, and trace-level safety preservation. + +## Relationship to PCS + +PF-Core is a **sub-protocol** inside PCS, not a replacement for it. + +| Layer | Responsibility | +|-------|----------------| +| PCS | Artifact envelopes, release chains, hash canonicalization, cross-repo conformance | +| PF-Core | Principals, capabilities, actions, events, traces, contracts, trace-level safety | +| Runtime adapters | Untrusted producers that compile observations into PF-Core artifacts | + +PCS v0.1 release-chain artifacts (`RuntimeReceipt.v0`, `TraceCertificate.v0`, `ScienceClaimBundle.v0`, and related types) remain authoritative for the LabTrust demonstration workflow. PF-Core adds a parallel trusted path for agentic action traces. + +## Current Lean scope + +Two theorem families coexist in `lean/` and must not be conflated in documentation or claim language. + +### PCS release-envelope consistency (`lean/PCS/`) + +The existing Lean package under `lean/PCS/` proves **release-envelope consistency** properties (trace hash alignment, verification admission, signed bundle coherence). Describe this family as: + +> Release-envelope consistency theorem family + +It covers PCS bundle and certificate coherence, not per-event agent authorization. + +### PF-Core trace-safety (`lean/PFCore/`) + +The PF-Core namespace (`lean/PFCore/`, `lake build PFCore`) defines agentic primitives and trace-safety predicates: + +- `EventSafe`, `TraceSafe`, and decidable counterparts `eventSafeD`, `traceSafeD` +- `HandoffSafe` and non-expanding delegation +- Contract structures and trace-safety invariant preservation (conservative) +- Concrete per-trace proofs in `lean/PFCore/Generated/` checked by `pcs pf-core lean-check` + +Describe this family as: + +> PF-Core trace-safety theorem family + +Do not describe PCS release-envelope theorems as agent safety theorems. + +## Explicit artifact typing + +No PF-Core trusted artifact may be inferred heuristically. Every trusted PF-Core artifact must declare: + +```json +"artifact_type": "PFCoreTrace.v0" +``` + +(or the matching type for the artifact class). The PCS validator accepts explicit `artifact_type` only when the JSON Schema also requires the same constant. + +## Status vs claim class + +PCS `status` fields describe workflow lifecycle state. PF-Core **claim class** describes what kind of assurance was actually obtained. These must not be conflated. See [claim-boundary.md](claim-boundary.md). diff --git a/docs/pf-core/non-interference.md b/docs/pf-core/non-interference.md new file mode 100644 index 0000000..2d2515c --- /dev/null +++ b/docs/pf-core/non-interference.md @@ -0,0 +1,49 @@ +# PF-Core conservative non-interference + +This document states what PF-Core **proves** about tenant isolation versus what remains **open research**. + +## Scope (conservative subset) + +PF-Core does **not** claim global non-interference across tenants, covert channels, or arbitrary compositional invariants. The Lean module `lean/PFCore/NonInterference.lean` formalizes a **tenant-scoped trace property** aligned with runtime checks. + +### Definitions + +| Lean | Meaning | +|------|---------| +| `SameTenant p r` | Alias for `SameTenantResource` — resource tenant equals principal tenant | +| `EventTenantScoped tenant ev` | Principal tenant is `tenant` and all read/write resources match | +| `TraceTenantScoped tenant tr` | Every event in `tr` is `EventTenantScoped tenant` | + +Boolean deciders `eventTenantScopedD` and `traceTenantScopedD` mirror these predicates (soundness theorems included). + +### Proved theorems (no `sorry`) + +| Theorem | Statement | +|---------|-----------| +| `cons_preserves_tenant_scope` | `TraceTenantScoped` preserved under `Trace.cons` when the new event is scoped | +| `eventSafe_allow_implies_tenant_scoped` | Allowed `EventSafe` events are scoped to the principal's tenant | +| `traceSafe_allowed_event_tenant_scoped` | Allowed events in a `TraceSafe` trace are tenant-scoped | +| `traceSafe_implies_tenant_scoped_for_allowed` | Same link, named for documentation | + +**Important:** `TraceSafe` does **not** imply `TraceTenantScoped` for denied events. A denied cross-tenant read is `EventSafe` but fails `EventTenantScoped`. Runtime `validate_tenant_isolation` flags such events regardless of decision. + +## Runtime alignment + +`pcs_core.pf_core_runtime.validate_tenant_isolation(trace)` returns errors when any event's principal tenant mismatches a read/write resource tenant. + +Enable via: + +```bash +pcs pf-core validate-trace --tenant-isolation examples/pf-core-valid/file_read_allowed/trace.json +``` + +Invalid fixture: `examples/pf-core-invalid/cross_tenant_leak/`. + +## Open (not claimed) + +1. Full cross-tenant non-interference (no information flow between tenants). +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. + +See also `docs/pf-core/contract-semantics.md` and `docs/pf-core/claim-boundary.md`. diff --git a/docs/pf-core/presentation/claim-boundary-slide.md b/docs/pf-core/presentation/claim-boundary-slide.md new file mode 100644 index 0000000..64b9670 --- /dev/null +++ b/docs/pf-core/presentation/claim-boundary-slide.md @@ -0,0 +1,23 @@ +# PF-Core claim class reference (external audience) + +One-page summary of what each assurance label means and what it does **not** imply. + +| Claim class | What we checked | What we did **not** check | +|-------------|-----------------|---------------------------| +| `SchemaValidated` | JSON Schema + PF-Core semantic fields | Runtime behavior, policies, Lean proofs | +| `RuntimeChecked` | Compiler + hash chain + Python deciders (capability, tenant, resource scope) | Lean kernel proof, external checker | +| `CertificateChecked` | External checker attestation recorded in PF-Core schema | Lean kernel, full platform safety | +| `LeanKernelChecked` | Concrete Lean proof of `traceSafeD` for this trace | Global non-interference, model safety, MCP/NL policy | +| `ReplayValidated` | Deterministic hash replay reproduced stored digests | Semantic re-execution of external tools | +| `AssumptionDeclared` | Documented assumptions for deferred registry checks | Execution of skipped Lean/build gates | +| `OutOfScope` | Explicitly outside PF-Core kernel | Any formal guarantee | + +## Public language + +Use trace-level safety preservation under stated assumptions. Avoid marketing phrases that imply full agent or platform verification unless the matching claim class and documentation are present. + +## PCS vs PF-Core + +- PCS `TraceCertificate.v0` / `CertificateChecked` status describes science-bundle lifecycle. +- PF-Core `claim_class` describes the PF-Core assurance obtained. +- `pcs lean-check` (PCS path) is **not** Lean-backed per trace; use `pcs pf-core lean-check --trace …` for PF-Core checking. diff --git a/docs/pf-core/presentation/demo-script.md b/docs/pf-core/presentation/demo-script.md new file mode 100644 index 0000000..fced322 --- /dev/null +++ b/docs/pf-core/presentation/demo-script.md @@ -0,0 +1,159 @@ +# PF-Core demo script + +Scripted walkthrough: compile tool-use trace, validate, lean-check, replay-trace. Audience: technical reviewers. + +## Prerequisites + +- Repository root: `pcs-core` +- Python package installed: `pip install -e python` +- Optional: Lean 4 + `lake` for full `LeanKernelChecked` path + +## 1. Compile ToolUseTrace to PFCoreTrace + +```bash +pcs pf-core compile-trace examples/pf-core-valid/tool_use_trace_compiled/tool_use_trace.json +``` + +Expected: JSON `PFCoreTrace.v0` with `claim_class: RuntimeChecked`, denied network event preserved. + +## 2. Validate hash chain + +```bash +pcs pf-core validate-trace examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json +``` + +Expected: `OK PFCoreTrace hash chain …` + +## 3. Validate contracts (Stage 7) + +```bash +pcs pf-core validate-contracts \ + examples/pf-core-valid/contract_checked/trace.json \ + --contracts-dir examples/pf-core-valid/contract_checked +``` + +Expected: `OK PF-Core contract satisfaction …` + +## 4. Lean-check (RuntimeChecked vs LeanKernelChecked) + +Runtime-only (no Lean build): + +```bash +pcs pf-core lean-check \ + --trace examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json \ + --skip-build --skip-lean-proof +``` + +Expected: certificate / result with `claim_class: RuntimeChecked`. + +Full kernel path (when Lean toolchain available): + +```bash +pcs pf-core lean-check \ + --trace examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json +``` + +Expected on success: `claim_class: LeanKernelChecked`, `lean_proof_checked: true`. + +## 5. Replay-trace + +```bash +pcs pf-core replay-trace \ + examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json \ + --source examples/pf-core-valid/tool_use_trace_compiled/tool_use_trace.json +``` + +Expected: `claim_class: ReplayValidated`, `replay_match: true`. + +## 6. LabTrust bridge (Stage 6) + +```bash +pcs validate examples/pf-core-valid/labtrust_replay/trace.json +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) + +```bash +pcs lean-check +``` + +Expected: exit code 2, stderr explains PCS path is not Lean-backed per trace; directs to `pcs pf-core lean-check`. + +## 8. LabTrust end-to-end bridge (Phase E) + +Automated script: + +```bash +bash scripts/pf-core-bridge-demo.sh +``` + +Manual steps: + +```bash +pcs validate examples/labtrust/trace_certificate.valid.json +python -c " +from pathlib import Path +import json +from pcs_core.pf_core_labtrust_adapter import normalize_labtrust_release +root = Path('.') +tc = json.loads((root/'examples/labtrust/trace_certificate.valid.json').read_text()) +scb = json.loads((root/'examples/labtrust/science_claim_bundle.certified.valid.json').read_text()) +print(json.dumps(normalize_labtrust_release(tc, scb['runtime_receipts'][0]), indent=2)) +" +pcs pf-core replay-trace examples/pf-core-valid/labtrust_replay/trace.json +pcs pf-core attach-certificate-check \ + --trace examples/pf-core-valid/labtrust_replay/trace.json \ + --checker certifyedge \ + --checker-version 0.1.0 \ + --attestation-ref examples/labtrust/trace_certificate.valid.json \ + --out /tmp/PFCoreCertificate.v0.json +``` + +Expected: bridged `PFCoreCertificate.v0` with `claim_class: CertificateChecked` (not `LeanKernelChecked`). + +CertifyEdge check (mock for CI): + +```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 +``` + +Expected: `claim_class: CertificateChecked`, never `LeanKernelChecked`. + +## 9. Tenant isolation (Phase F1) + +```bash +pcs pf-core validate-trace --tenant-isolation \ + examples/pf-core-valid/file_read_allowed/trace.json +``` + +Expected: OK. Invalid fixture `examples/pf-core-invalid/cross_tenant_leak/` fails examples check. + +## 10. Contract discharge in Lean (Phase F2) + +```bash +pcs pf-core lean-check \ + --trace examples/pf-core-valid/contract_checked/trace.json +``` + +Expected (with Lean toolchain): generated proof includes `concrete_trace_satisfies_contract_*` theorems. + +AssumptionSet-backed deferred assurance fixture: + +```bash +pcs validate examples/pf-core-valid/assumption_declared/assumption_set.json +pcs validate examples/pf-core-valid/assumption_declared/certificate.json +``` + +Expected: `claim_class: AssumptionDeclared` with `assumption_refs` citing `as-pfcore-demo-v0.1`. + +## Talking points + +- **RuntimeChecked**: Python deciders aligned with Lean predicates; no kernel proof. +- **LeanKernelChecked**: same deciders plus concrete `traceSafeD` proof in Lean. +- **ReplayValidated**: hash-chain integrity only; does not upgrade to LeanKernelChecked. diff --git a/docs/pf-core/presentation/theorem-sheet.md b/docs/pf-core/presentation/theorem-sheet.md new file mode 100644 index 0000000..0294326 --- /dev/null +++ b/docs/pf-core/presentation/theorem-sheet.md @@ -0,0 +1,113 @@ +# PF-Core theorem sheet + +Exact statements from `lean/PFCore/` trusted modules. + +## Trace safety (`lean/PFCore/Theorems.lean`) + +### `allowed_event_has_allowed_action` + +From `EventSafe`, an allowed decision implies the action was allowed. + +```lean +theorem allowed_event_has_allowed_action (ev : Event) (h : EventSafe ev) (hallow : ev.decision = Decision.allow) : + ActionAllowed ev.principal ev.action +``` + +### `event_in_safe_trace_is_safe` + +Every event in a safe trace is itself safe. + +```lean +theorem event_in_safe_trace_is_safe (tr : Trace) (ev : Event) + (hTrace : TraceSafe tr) (hIn : EventIn ev tr) : EventSafe ev +``` + +### `every_allowed_event_in_safe_trace_is_allowed` + +Any allowed event inside a safe trace corresponds to an allowed action. + +```lean +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 +``` + +## Conservative non-interference (`lean/PFCore/NonInterference.lean`) + +### `cons_preserves_tenant_scope` + +Tenant scope preserved under `Trace.cons`. + +```lean +theorem cons_preserves_tenant_scope (tenant : String) (tr : Trace) (ev : Event) : + TraceTenantScoped tenant tr → EventTenantScoped tenant ev → + TraceTenantScoped tenant (Trace.cons tr ev) +``` + +### `traceSafe_allowed_event_tenant_scoped` + +Allowed events in a safe trace are tenant-scoped (does **not** claim global non-interference). + +```lean +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 +``` + +## Contract deciders (`lean/PFCore/ContractDecide.lean`) + +### `contractPreD_sound` + +```lean +theorem contractPreD_sound (spec : ContractPreSpec) (p : Principal) (a : Action) : + contractPreD spec p a = true ↔ ContractPreHolds spec p a +``` + +### `satisfiesContractSpecD_sound` + +```lean +theorem satisfiesContractSpecD_sound (pre : ContractPreSpec) (post : ContractPostSpec) (ev : Event) : + satisfiesContractSpecD pre post ev = true ↔ SatisfiesContractSpec pre post ev +``` + +### `traceSatisfiesContractSpecsD_sound` + +```lean +theorem traceSatisfiesContractSpecsD_sound (pre : ContractPreSpec) (post : ContractPostSpec) + (inv : ContractInvariantSpec) (tr : Trace) : + traceSatisfiesContractSpecsD pre post inv tr = true ↔ + TraceSatisfiesContractSpecs pre post inv tr +``` + +## Handoff safety (`lean/PFCore/Handoff.lean`) + +### `handoffSafeD_sound` + +Boolean decider for non-expanding delegation. + +```lean +theorem handoffSafeD_sound (h : Handoff) : + handoffSafeD h = true ↔ HandoffSafe h +``` + +### `handoff_does_not_expand_authority` + +Safe handoff never grants a capability absent from the source principal. + +```lean +theorem handoff_does_not_expand_authority (h : Handoff) (cap : String) : + HandoffSafe h → cap ∈ h.delegatedCapabilities → HasCapability h.fromPrincipal cap +``` + +## Decider alignment (runtime) + +Python deciders in `lean_check.py` mirror: + +- `eventSafeD` — deny is always safe; allow requires capability + tenant + resource scope +- `traceSafeD` — all events safe +- `handoffSafeD` — delegated capabilities subset of source (via `validate_handoff_authority`) +- `validate_tenant_isolation` — conservative mirror of `EventTenantScoped` / `TraceTenantScoped` + +`LeanKernelChecked` is emitted only when a generated concrete proof evaluates `traceSafeD tr = true` in the Lean kernel (plus contract deciders when `contract_refs` are discharged). + +`CertificateChecked` is emitted only via CertifyEdge or `attach-certificate-check`; it never upgrades to `LeanKernelChecked`. diff --git a/docs/pf-core/release-checklist.md b/docs/pf-core/release-checklist.md new file mode 100644 index 0000000..8059b9e --- /dev/null +++ b/docs/pf-core/release-checklist.md @@ -0,0 +1,58 @@ +# PF-Core release checklist + +Pre-release verification for PF-Core in `pcs-core`. Run from repository root unless noted. + +## CI gates (what they prove) + +| Job / step | Proves | +|------------|--------| +| `pytest tests/test_pf_core_*.py` | Python runtime, contracts, codegen, CertifyEdge mock, fixtures | +| `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 | +| `pcs pf-core audit-lean-no-sorry` | No `sorry` / `axiom` in `lean/PFCore/` | +| `pcs examples check` | Valid/invalid PF-Core fixtures including replay and isolation | +| 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) | + +## Local full demo + +```bash +pip install -e python +bash scripts/pf-core-bridge-demo.sh +pcs pf-core lean-check --trace examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json +pcs pf-core validate-contracts \ + examples/pf-core-valid/contract_checked/trace.json \ + --contracts-dir examples/pf-core-valid/contract_checked +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 +``` + +Optional Lean build (requires Lean 4 + `lake`): + +```bash +cd lean && lake build PFCore +``` + +On Windows without native `lake`, use WSL for Lean steps. + +## Claim boundaries for external release + +| Claim class | May state | Must not state | +|-------------|-----------|----------------| +| `RuntimeChecked` | Python deciders aligned with Lean predicates | Lean kernel proof, CertifyEdge attestation | +| `LeanKernelChecked` | Concrete `traceSafeD` (+ contract deciders when refs present) proved in Lean | Global non-interference, full JSON contract discharge for role/policy/evidence fields | +| `ReplayValidated` | Hash-chain integrity | Upgrades to Lean or CertifyEdge | +| `CertificateChecked` | External checker attestation (CertifyEdge mock or live) | `LeanKernelChecked`, global non-interference | + +Reference: `docs/pf-core/claim-boundary.md`, `docs/pf-core/non-interference.md`, `docs/pf-core/contract-semantics.md`. + +## Phase F deliverables (this release) + +- F1: `NonInterference.lean` + `validate_tenant_isolation` + `cross_tenant_leak/` fixture +- F2: `ContractDecide.lean` + Lean codegen contract discharge for mapped JSON fields +- F3: `pf_core_certifyedge.py` + `pcs pf-core certifyedge-check` + mock CI path diff --git a/docs/pf-core/threat-model.md b/docs/pf-core/threat-model.md new file mode 100644 index 0000000..2a577e0 --- /dev/null +++ b/docs/pf-core/threat-model.md @@ -0,0 +1,58 @@ +# PF-Core threat model + +## Assets + +- PF-Core trace integrity (event hash chain, trace hash) +- Claim class accuracy (no upgrade from `RuntimeChecked` to `LeanKernelChecked` without proof) +- Lean trusted computing base (TCB) source files +- Registry authority (artifact type definitions, release-mode requirements) + +## Adversary capabilities + +| Threat | Description | Mitigation | +|--------|-------------|------------| +| Overclaiming in docs | Marketing language implies full AI safety | `pcs pf-core audit-claims` | +| Heuristic type confusion | Malformed JSON detected as wrong artifact type | Explicit `artifact_type` + schema `const` | +| Catalog inflation | Python catalog lists nonexistent Lean theorems | `pcs pf-core audit-lean-catalog` | +| Status / claim conflation | `ProofChecked` read as Lean kernel proof | Documented in claim-boundary; PCS `lean-check` disclaimer | +| Omitted denied events | Unsafe runs hide blocked attempts | Stage 2 compiler preserves denied events | +| Authority expansion via handoff | Delegate more capabilities than source holds | Stage 3 `HandoffSafe` theorem; Stage 7 compile gate | +| Registry deferral as proof | Skipped checks treated as verified | `AssumptionDeclared` / `OutOfScope`; `enforce_assumption_declared` | +| Hash collision | Forged digest matching valid trace | SHA-256 assumption; no claim beyond digest equality | +| Untrusted adapter tampering | Adapter rewrites release artifacts | Adapters untrusted; schema + hash validation | +| Fake Lean proof | Certificate claims kernel proof without `decide` success | Stage 4 codegen + `lake env lean`; `LeanKernelChecked` gated on proof | +| Replay forgery | Altered trace passes without hash-chain check | Stage 5 `replay-trace` (`ReplayValidated`) | +| Contract bypass | Events violate declared contracts | Stage 7 runtime checker + `validate-contracts` | + +## Trust boundaries + +``` +Untrusted runtime / adapters / PCS external checkers + | + v (schema-valid PF-Core artifacts, explicit artifact_type) +PF-Core validation + hash chain + contract checker (Python) + | + +--> ReplayValidated (Stage 5 hash-chain replay) + | + +--> RuntimeChecked (Python deciders aligned with Lean predicates) + | + v (codegen + lake env lean on generated proof) +PF-Core trace-safety Lean kernel (lean/PFCore/) + | + v LeanKernelChecked (concrete traceSafeD proof only) + +Parallel PCS release-envelope path (NOT per-trace agent safety): +Untrusted producers --> PCS schema validation --> lake build PCS + | + v Release-envelope consistency theorems (lean/PCS/Theorems.lean) +``` + +PCS `pcs lean-check` (without `--trace`) evaluates release obligations in Python only; it does **not** produce per-trace `LeanKernelChecked` claims. Use `pcs pf-core lean-check --trace` for trace-level kernel proofs. + +## Residual risk + +- PF-Core contract JSON predicates are richer than the Lean `Contract` structure; contract satisfaction in Lean is intentionally simplified (see [contract-semantics.md](contract-semantics.md)). +- Role names are not interpreted in the Lean kernel; runtime expands roles to capabilities, and lean-check requires explicit capability lists on traces (permanent assumption). +- Global non-interference and full compositional invariant research remain deferred (Phase F). +- Model correctness, NL policy, MCP semantics, and live CertifyEdge attestation are out of scope. +- Generated proofs cover `traceSafeD`, per-event `eventSafeD`, and optional `handoffSafeD`; they do not yet discharge arbitrary JSON contract invariants in Lean. diff --git a/docs/pf-core/trusted-boundary.md b/docs/pf-core/trusted-boundary.md new file mode 100644 index 0000000..a8b3584 --- /dev/null +++ b/docs/pf-core/trusted-boundary.md @@ -0,0 +1,78 @@ +# PF-Core trusted boundary + +This document lists what PCS/PF-Core treats as trusted, untrusted, or assumed when interpreting PF-Core artifacts. + +## Trusted (in-scope for PF-Core kernel claims) + +| Component | Location | Trust basis | +|-----------|----------|-------------| +| PF-Core JSON schemas | `schemas/PFCore*.v0.schema.json` | Draft 2020-12 validation, `additionalProperties: false` | +| PF-Core artifact registry entries | `python/pcs_core/registry_data.py` | Protocol authority in pcs-core | +| Explicit `artifact_type` detection | `python/pcs_core/validate.py` | Schema-locked discriminator | +| Release-envelope Lean theorems | `lean/PCS/Theorems.lean` | Lean 4 proof check (`lake build PCS`) | +| PF-Core trace-safety Lean kernel | `lean/PFCore/` | Lean 4 proof check (`lake build PFCore`) | +| PF-Core claim-boundary linter | `python/pcs_core/pf_core_claims.py` | CI-enforced documentation scan | +| Lean theorem catalog (PCS trusted set) | `python/pcs_core/lean_catalog.py` | Audited against `lean/PCS/Theorems.lean` | +| Lean theorem catalog (PF-Core trusted set) | `python/pcs_core/lean_catalog.py` | Audited against `lean/PFCore/` | +| PF-Core lean-check deciders | `python/pcs_core/lean_check.py` | Aligned with PF-Core Lean predicates; uses explicit `principal.capabilities` only | +| PF-Core concrete trace Lean proofs | `lean/PFCore/Generated/` (generated) | `lake env lean` on generated `concrete_trace_safe` theorem | +| Tool-use / witness hash alignment theorems | `lean/PCS/ToolUse.lean`, `lean/PCS/ComputationWitness.lean` | Promoted to trusted PCS catalog (Stage 4) | +| 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 | + +## Untrusted (must not produce LeanKernelChecked claims) + +| Component | Reason | +|-----------|--------| +| Heuristic artifact type detection | Order-dependent field inference | +| Python obligation evaluators without Lean term generation | Predicate checks only unless concrete Lean proof succeeds | +| Runtime producers (`AgentRuntime`, adapters) | External code; evidence not proof | +| Deferred registry semantic checks | Explicitly skipped obligations | +| Documentation and examples | Organizational; scanned but not formally verified | +| LLM or network-dependent compilation | Forbidden in PF-Core runtime compiler | +| `pcs pf-core lean-check --skip-build` or `--skip-lean-proof` | Emits `RuntimeChecked`, not `LeanKernelChecked` | + +## Assumed (explicit, not proved by PF-Core) + +See [assumptions.md](assumptions.md). Assumptions must appear in `AssumptionSet.v0` or PF-Core certificate assumption refs before any external claim. + +## Allowlisted Lean axioms + +No PF-Core trusted Lean file may contain `sorry`, `admit`, `axiom`, or `unsafe` unless listed here. + +| File | Exception | Rationale | +|------|-----------|-----------| +| (none) | — | Stage 3: no exceptions | + +## Trusted file list (Stage 3) + +### Documentation and Python + +- `docs/pf-core/*.md` +- `python/pcs_core/registry_data.py` (PF-Core entries) +- `python/pcs_core/pf_core_claims.py` +- `python/pcs_core/lean_catalog.py` +- `python/pcs_core/lean_check.py` +- `python/pcs_core/pf_core_runtime.py` + +### PCS release-envelope Lean + +- `lean/PCS/ReleaseChain.lean` +- `lean/PCS/Theorems.lean` + +### PF-Core trace-safety Lean (`lake build PFCore`) + +- `lean/PFCore/Basic.lean` +- `lean/PFCore/Principal.lean` +- `lean/PFCore/Capability.lean` +- `lean/PFCore/Resource.lean` +- `lean/PFCore/Action.lean` +- `lean/PFCore/Event.lean` +- `lean/PFCore/Trace.lean` +- `lean/PFCore/Handoff.lean` +- `lean/PFCore/Contract.lean` +- `lean/PFCore/Certificate.lean` +- `lean/PFCore/Soundness.lean` +- `lean/PFCore/Theorems.lean` +- `lean/PFCore.lean` (root module for `lake build PFCore`) +- `lean/PCS.lean` (root module for `lake build PCS`) diff --git a/examples/pf-core-invalid/claim_class_overclaim/manifest.json b/examples/pf-core-invalid/claim_class_overclaim/manifest.json new file mode 100644 index 0000000..fca6f2b --- /dev/null +++ b/examples/pf-core-invalid/claim_class_overclaim/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "ClaimClassOverclaim", + "must_fail_at": "validate_pfcore_trace_hash_chain" +} diff --git a/examples/pf-core-invalid/claim_class_overclaim/trace.json b/examples/pf-core-invalid/claim_class_overclaim/trace.json new file mode 100644 index 0000000..48df0a2 --- /dev/null +++ b/examples/pf-core-invalid/claim_class_overclaim/trace.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": "LeanKernelChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:bc26bbf4e65c1722cf2dd56723238ff13b72526ee6450d0bb9e4e54a4c3a4d30" +} diff --git a/examples/pf-core-invalid/contract_violation/contracts/contract.json b/examples/pf-core-invalid/contract_violation/contracts/contract.json new file mode 100644 index 0000000..22e6c6c --- /dev/null +++ b/examples/pf-core-invalid/contract_violation/contracts/contract.json @@ -0,0 +1,19 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreContract.v0", + "contract_id": "contract-file-read-v0", + "name": "File read within tenant", + "pre": { + "require_capability": "cap:file-read", + "require_effect": "file.read", + "require_tenant_match": true + }, + "post": { + "require_decision": "allow", + "require_event_safe": true + }, + "invariant": { + "require_trace_safe": true + }, + "signature_or_digest": "sha256:ab672cb206542354c10afdbc0e739fe018acee2d4d7db40a7486bcf3071846b3" +} diff --git a/examples/pf-core-invalid/contract_violation/manifest.json b/examples/pf-core-invalid/contract_violation/manifest.json new file mode 100644 index 0000000..5103375 --- /dev/null +++ b/examples/pf-core-invalid/contract_violation/manifest.json @@ -0,0 +1 @@ +{"expected_error": "ContractDecisionMismatch", "must_fail_at": "validate_trace_contracts"} diff --git a/examples/pf-core-invalid/contract_violation/trace.json b/examples/pf-core-invalid/contract_violation/trace.json new file mode 100644 index 0000000..43de7db --- /dev/null +++ b/examples/pf-core-invalid/contract_violation/trace.json @@ -0,0 +1,72 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-contract-checked-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": "deny", + "decision_reason": "contract test deny", + "contract_refs": [ + "contract-file-read-v0" + ], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:63c92b6ea80de7f58e25081c07ce5355a1bd2a9980d5bba1f0ac91fd4ce6c08a", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:63c92b6ea80de7f58e25081c07ce5355a1bd2a9980d5bba1f0ac91fd4ce6c08a" + } + ], + "trace_hash": "sha256:5664759460f693bcbf99197fb0256d1bfcf12041f056741924d38ab19290c1d1", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:ab672cb206542354c10afdbc0e739fe018acee2d4d7db40a7486bcf3071846b3", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:5664759460f693bcbf99197fb0256d1bfcf12041f056741924d38ab19290c1d1" +} diff --git a/examples/pf-core-invalid/cross_tenant_leak/manifest.json b/examples/pf-core-invalid/cross_tenant_leak/manifest.json new file mode 100644 index 0000000..836f7f8 --- /dev/null +++ b/examples/pf-core-invalid/cross_tenant_leak/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "TenantIsolation", + "must_fail_at": "validate_tenant_isolation" +} diff --git a/examples/pf-core-invalid/cross_tenant_leak/trace.json b/examples/pf-core-invalid/cross_tenant_leak/trace.json new file mode 100644 index 0000000..4f09c68 --- /dev/null +++ b/examples/pf-core-invalid/cross_tenant_leak/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_event/manifest.json b/examples/pf-core-invalid/dropped_denied_event/manifest.json new file mode 100644 index 0000000..9987359 --- /dev/null +++ b/examples/pf-core-invalid/dropped_denied_event/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "DroppedDeniedEvent", + "must_fail_at": "validate_denied_events_preserved" +} diff --git a/examples/pf-core-invalid/dropped_denied_event/pfcore_trace.json b/examples/pf-core-invalid/dropped_denied_event/pfcore_trace.json new file mode 100644 index 0000000..40a271d --- /dev/null +++ b/examples/pf-core-invalid/dropped_denied_event/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/examples/pf-core-invalid/dropped_denied_event/tool_use_trace.json b/examples/pf-core-invalid/dropped_denied_event/tool_use_trace.json new file mode 100644 index 0000000..d2310ec --- /dev/null +++ b/examples/pf-core-invalid/dropped_denied_event/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/examples/pf-core-invalid/event_hash_mismatch/manifest.json b/examples/pf-core-invalid/event_hash_mismatch/manifest.json new file mode 100644 index 0000000..f31a7e6 --- /dev/null +++ b/examples/pf-core-invalid/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/event_hash_mismatch/trace.json b/examples/pf-core-invalid/event_hash_mismatch/trace.json new file mode 100644 index 0000000..d5b0151 --- /dev/null +++ b/examples/pf-core-invalid/event_hash_mismatch/trace.json @@ -0,0 +1,67 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-bad-event", + "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:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:e6ae0e0c4c702dd1f83a6adb29a97e7d89b9741537b3ebd95bb476f754ea4960" + } + ], + "trace_hash": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "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:57275347628e434d1bb98616195e85f4823e31bf8f8854e3b9163f7d7b86a734" +} diff --git a/examples/pf-core-invalid/handoff_authority_expansion/handoff.json b/examples/pf-core-invalid/handoff_authority_expansion/handoff.json new file mode 100644 index 0000000..ade3dd8 --- /dev/null +++ b/examples/pf-core-invalid/handoff_authority_expansion/handoff.json @@ -0,0 +1,33 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreHandoff.v0", + "handoff_id": "handoff-bad-1", + "from_principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "file_reader" + ], + "capabilities": [] + }, + "to_principal": { + "principal_id": "agent-2", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "handoff_delegate" + ], + "capabilities": [] + }, + "delegated_capabilities": [ + { + "capability_id": "cap:network", + "effect_kind": "network.egress", + "resource_pattern": "*" + } + ], + "reason": "over-delegation", + "evidence_refs": [], + "signature_or_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/examples/pf-core-invalid/handoff_authority_expansion/manifest.json b/examples/pf-core-invalid/handoff_authority_expansion/manifest.json new file mode 100644 index 0000000..a48c9f4 --- /dev/null +++ b/examples/pf-core-invalid/handoff_authority_expansion/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "HandoffAuthorityExpansion", + "must_fail_at": "validate_handoff_authority" +} diff --git a/examples/pf-core-invalid/handoff_compile_expansion/manifest.json b/examples/pf-core-invalid/handoff_compile_expansion/manifest.json new file mode 100644 index 0000000..19d36ee --- /dev/null +++ b/examples/pf-core-invalid/handoff_compile_expansion/manifest.json @@ -0,0 +1 @@ +{"expected_error": "HandoffAuthorityExpansion", "must_fail_at": "compile_tool_use_trace_to_pfcore_trace"} diff --git a/examples/pf-core-invalid/handoff_compile_expansion/tool_use_trace.json b/examples/pf-core-invalid/handoff_compile_expansion/tool_use_trace.json new file mode 100644 index 0000000..abc3dcd --- /dev/null +++ b/examples/pf-core-invalid/handoff_compile_expansion/tool_use_trace.json @@ -0,0 +1,79 @@ +{ + "schema_version": "v0", + "trace_id": "trace-handoff-bad-compile", + "workflow_id": "agent_tool_use.safety_v0", + "agent_id": "agent-safety-conformance-001", + "policy_id": "policy-no-secret-exfiltration-v0", + "policy_hash": "sha256:76d4443f09c6fb0d6cc7bebc5c80eae53bd008e4a212ab61d0d1844ac773b5cd", + "started_at": "2026-05-18T00:00:00Z", + "completed_at": "2026-05-18T00:00:05Z", + "tool_calls": [ + { + "event_id": "evt-001", + "timestamp": "2026-05-18T00:00:01Z", + "tool_name": "filesystem.read", + "tool_category": "filesystem", + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "authorization_status": "authorized", + "policy_refs": [ + "policy-no-secret-exfiltration-v0" + ], + "resource_uri": "/data/report.txt", + "tenant": "tenant-a" + }, + { + "event_id": "evt-002", + "timestamp": "2026-05-18T00:00:02Z", + "tool_name": "network.request", + "tool_category": "network", + "input_hash": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "output_hash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "authorization_status": "rejected", + "policy_refs": [ + "policy-no-secret-exfiltration-v0" + ], + "resource_uri": "https://example.com", + "tenant": "tenant-a" + } + ], + "trace_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "a111111111111111111111111111111111111111", + "signature_or_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "handoffs": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreHandoff.v0", + "handoff_id": "handoff-bad-1", + "from_principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "file_reader" + ], + "capabilities": [] + }, + "to_principal": { + "principal_id": "agent-2", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "handoff_delegate" + ], + "capabilities": [] + }, + "delegated_capabilities": [ + { + "capability_id": "cap:network", + "effect_kind": "network.egress", + "resource_pattern": "*" + } + ], + "reason": "over-delegation", + "evidence_refs": [], + "signature_or_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } + ] +} diff --git a/examples/pf-core-invalid/missing_principal/manifest.json b/examples/pf-core-invalid/missing_principal/manifest.json new file mode 100644 index 0000000..18220c8 --- /dev/null +++ b/examples/pf-core-invalid/missing_principal/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "MissingPrincipal", + "must_fail_at": "runtime_to_pfcore_event" +} diff --git a/examples/pf-core-invalid/missing_principal/observation.json b/examples/pf-core-invalid/missing_principal/observation.json new file mode 100644 index 0000000..bdcdb6d --- /dev/null +++ b/examples/pf-core-invalid/missing_principal/observation.json @@ -0,0 +1,50 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreRuntimeObservation.v0", + "observation_id": "obs-1", + "trace_id": "trace-1", + "event_id": "ev-1", + "observed_at": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [], + "capabilities": [] + }, + "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", + "policy_ref": "policy/default.v0", + "evidence_refs": [], + "runtime_ref": "agent-runtime", + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "payload_hash": "sha256:b3e51555d4b30ba60c10a8076457d438b63f9c7fcd6ae7a864fa14d98aa7229f", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:b3e51555d4b30ba60c10a8076457d438b63f9c7fcd6ae7a864fa14d98aa7229f" +} diff --git a/examples/pf-core-invalid/resource_scope_violation/manifest.json b/examples/pf-core-invalid/resource_scope_violation/manifest.json new file mode 100644 index 0000000..0689f82 --- /dev/null +++ b/examples/pf-core-invalid/resource_scope_violation/manifest.json @@ -0,0 +1 @@ +{"expected_error": "ResourceScopeViolation", "must_fail_at": "validate_pfcore_trace_hash_chain"} diff --git a/examples/pf-core-invalid/resource_scope_violation/trace.json b/examples/pf-core-invalid/resource_scope_violation/trace.json new file mode 100644 index 0000000..0af1629 --- /dev/null +++ b/examples/pf-core-invalid/resource_scope_violation/trace.json @@ -0,0 +1,67 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-resource-scope-violation", + "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": "/etc/passwd", + "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:19d2cd927b540fddb14e986a3f4352687c136a715294b134e6598bd3bfa72cbc", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:19d2cd927b540fddb14e986a3f4352687c136a715294b134e6598bd3bfa72cbc" + } + ], + "trace_hash": "sha256:30f4f6967806094c106ceb267d421bcf0d5351f97dfd7267c1336a1ef2ec6c4a", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:ab672cb206542354c10afdbc0e739fe018acee2d4d7db40a7486bcf3071846b3", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:30f4f6967806094c106ceb267d421bcf0d5351f97dfd7267c1336a1ef2ec6c4a" +} diff --git a/examples/pf-core-invalid/trace_hash_mismatch/manifest.json b/examples/pf-core-invalid/trace_hash_mismatch/manifest.json new file mode 100644 index 0000000..fbffb16 --- /dev/null +++ b/examples/pf-core-invalid/trace_hash_mismatch/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "TraceHashMismatch", + "must_fail_at": "validate_pfcore_trace_hash_chain" +} diff --git a/examples/pf-core-invalid/trace_hash_mismatch/trace.json b/examples/pf-core-invalid/trace_hash_mismatch/trace.json new file mode 100644 index 0000000..9c7500c --- /dev/null +++ b/examples/pf-core-invalid/trace_hash_mismatch/trace.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:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "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/examples/pf-core-invalid/unknown_capability/manifest.json b/examples/pf-core-invalid/unknown_capability/manifest.json new file mode 100644 index 0000000..0863bae --- /dev/null +++ b/examples/pf-core-invalid/unknown_capability/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "UnknownCapability", + "must_fail_at": "runtime_to_pfcore_event" +} diff --git a/examples/pf-core-invalid/unknown_capability/observation.json b/examples/pf-core-invalid/unknown_capability/observation.json new file mode 100644 index 0000000..cf2c6ef --- /dev/null +++ b/examples/pf-core-invalid/unknown_capability/observation.json @@ -0,0 +1,54 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreRuntimeObservation.v0", + "observation_id": "obs-1", + "trace_id": "trace-1", + "event_id": "ev-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-bad-cap", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:unknown", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "file.read" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/x", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "policy_ref": "policy/default.v0", + "evidence_refs": [], + "runtime_ref": "agent-runtime", + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "payload_hash": "sha256:f37ea476d8395017e5a5bbafb4e48101cfff180e623aa50c2a85547bd6196ff6", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:f37ea476d8395017e5a5bbafb4e48101cfff180e623aa50c2a85547bd6196ff6" +} diff --git a/examples/pf-core-invalid/unknown_effect/manifest.json b/examples/pf-core-invalid/unknown_effect/manifest.json new file mode 100644 index 0000000..e4567a9 --- /dev/null +++ b/examples/pf-core-invalid/unknown_effect/manifest.json @@ -0,0 +1,4 @@ +{ + "expected_error": "UnknownEffect", + "must_fail_at": "runtime_to_pfcore_event" +} diff --git a/examples/pf-core-invalid/unknown_effect/observation.json b/examples/pf-core-invalid/unknown_effect/observation.json new file mode 100644 index 0000000..1343dde --- /dev/null +++ b/examples/pf-core-invalid/unknown_effect/observation.json @@ -0,0 +1,54 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreRuntimeObservation.v0", + "observation_id": "obs-1", + "trace_id": "trace-1", + "event_id": "ev-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-bad-effect", + "tool_name": "custom.tool", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*" + }, + "effects": [ + { + "effect_kind": "custom.unknown" + } + ], + "reads": [ + { + "resource_id": "res-1", + "uri": "/data/x", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + }, + "decision": "allow", + "decision_reason": "authorized", + "policy_ref": "policy/default.v0", + "evidence_refs": [], + "runtime_ref": "agent-runtime", + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "payload_hash": "sha256:d1f2bf7c32639a79e7db14428c6891cae8efbdbac78799b4db3750fdac7c1be6", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:d1f2bf7c32639a79e7db14428c6891cae8efbdbac78799b4db3750fdac7c1be6" +} diff --git a/examples/pf-core-valid/assumption_declared/assumption_set.json b/examples/pf-core-valid/assumption_declared/assumption_set.json new file mode 100644 index 0000000..d77e621 --- /dev/null +++ b/examples/pf-core-valid/assumption_declared/assumption_set.json @@ -0,0 +1,21 @@ +{ + "assumption_set_id": "as-pfcore-demo-v0.1", + "schema_version": "v0", + "created_at": "2026-06-27T12:00:00Z", + "producer": "pcs-core", + "producer_version": "0.1.0", + "source_repo": "https://github.com/example/pcs-core", + "source_commit": "cccccccccccccccccccccccccccccccccccccccc", + "assumptions": [ + { + "assumption_id": "asm-pfcore-lean-deferred", + "text": "Lean kernel proof and library build checks were skipped; assurance is AssumptionDeclared only.", + "kind": "policy", + "status": "HumanReviewed", + "source_span_refs": [] + } + ], + "human_review_status": "approved", + "status": "HumanReviewed", + "signature_or_digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222" +} diff --git a/examples/pf-core-valid/assumption_declared/certificate.json b/examples/pf-core-valid/assumption_declared/certificate.json new file mode 100644 index 0000000..50d75b5 --- /dev/null +++ b/examples/pf-core-valid/assumption_declared/certificate.json @@ -0,0 +1,26 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreCertificate.v0", + "certificate_id": "pfcore-cert-assumption-declared-demo", + "trace_hash": "sha256:716cbed45d37ebe49deffd517021e74f7f8751f6baf6e00c2fdc6c3022b626f3", + "contract_hash": "sha256:76d4443f09c6fb0d6cc7bebc5c80eae53bd008e4a212ab61d0d1844ac773b5cd", + "policy_hash": "sha256:76d4443f09c6fb0d6cc7bebc5c80eae53bd008e4a212ab61d0d1844ac773b5cd", + "claim_class": "AssumptionDeclared", + "checker": "pcs-core", + "checker_version": "0.1.0", + "assumption_refs": [ + "as-pfcore-demo-v0.1", + "docs/pf-core/assumptions.md" + ], + "lean_proof_checked": false, + "lean_build_status": { + "ok": false, + "target": "PFCore", + "detail": "skipped for AssumptionDeclared demo fixture" + }, + "disclaimer": "AssumptionDeclared: deferred lean_kernel_proof and lean_library_build registry checks.", + "event_count": 2, + "source_repo": "https://github.com/example/pcs-core", + "source_commit": "a111111111111111111111111111111111111111", + "signature_or_digest": "sha256:c67acd6c4dcad6280c3ca868d5d4339036de4e4582ec150d7b2451369bafed6b" +} diff --git a/examples/pf-core-valid/assumption_declared/manifest.json b/examples/pf-core-valid/assumption_declared/manifest.json new file mode 100644 index 0000000..0790cc4 --- /dev/null +++ b/examples/pf-core-valid/assumption_declared/manifest.json @@ -0,0 +1,9 @@ +{ + "case_id": "assumption_declared", + "description": "PFCoreCertificate with AssumptionSet.v0 assumption ref when lean checks deferred", + "expected_valid": true, + "artifacts": [ + "assumption_set.json", + "certificate.json" + ] +} diff --git a/examples/pf-core-valid/contract_checked/contract.json b/examples/pf-core-valid/contract_checked/contract.json new file mode 100644 index 0000000..22e6c6c --- /dev/null +++ b/examples/pf-core-valid/contract_checked/contract.json @@ -0,0 +1,19 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreContract.v0", + "contract_id": "contract-file-read-v0", + "name": "File read within tenant", + "pre": { + "require_capability": "cap:file-read", + "require_effect": "file.read", + "require_tenant_match": true + }, + "post": { + "require_decision": "allow", + "require_event_safe": true + }, + "invariant": { + "require_trace_safe": true + }, + "signature_or_digest": "sha256:ab672cb206542354c10afdbc0e739fe018acee2d4d7db40a7486bcf3071846b3" +} diff --git a/examples/pf-core-valid/contract_checked/manifest.json b/examples/pf-core-valid/contract_checked/manifest.json new file mode 100644 index 0000000..dc5b7b2 --- /dev/null +++ b/examples/pf-core-valid/contract_checked/manifest.json @@ -0,0 +1 @@ +{"description": "Trace satisfies PFCoreContract.v0 predicates"} diff --git a/examples/pf-core-valid/contract_checked/trace.json b/examples/pf-core-valid/contract_checked/trace.json new file mode 100644 index 0000000..4c2c436 --- /dev/null +++ b/examples/pf-core-valid/contract_checked/trace.json @@ -0,0 +1,72 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-contract-checked-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:363ecde1488a23d2a0d128ac46a31d0dce8681e21f6b3ab78ba7a8152324ec26", + "policy_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "contract_hash": "sha256:ab672cb206542354c10afdbc0e739fe018acee2d4d7db40a7486bcf3071846b3", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:363ecde1488a23d2a0d128ac46a31d0dce8681e21f6b3ab78ba7a8152324ec26" +} diff --git a/examples/pf-core-valid/email_send_authorized/event.json b/examples/pf-core-valid/email_send_authorized/event.json new file mode 100644 index 0000000..c4dbbb4 --- /dev/null +++ b/examples/pf-core-valid/email_send_authorized/event.json @@ -0,0 +1,52 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-email-1", + "trace_id": "trace-email-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:email-send" + ] + }, + "action": { + "action_id": "act-email-1", + "tool_name": "email.send", + "capability": { + "capability_id": "cap:email-send", + "effect_kind": "email.send", + "resource_pattern": "mailto:*" + }, + "effects": [ + { + "effect_kind": "email.send" + } + ], + "reads": [ + { + "resource_id": "res-mail", + "uri": "mailto:user@example.com", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "output_hash": "sha256:2222222222222222222222222222222222222222222222222222222222222222" + }, + "decision": "allow", + "decision_reason": "authorized", + "contract_refs": [], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:a63f5b46b5b06f50824e242c370dabad9ecdcd2e60c6c9f4244725301e52925a", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:a63f5b46b5b06f50824e242c370dabad9ecdcd2e60c6c9f4244725301e52925a" +} diff --git a/examples/pf-core-valid/email_send_authorized/observation.json b/examples/pf-core-valid/email_send_authorized/observation.json new file mode 100644 index 0000000..2e30564 --- /dev/null +++ b/examples/pf-core-valid/email_send_authorized/observation.json @@ -0,0 +1,54 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreRuntimeObservation.v0", + "observation_id": "obs-email-1", + "trace_id": "trace-email-1", + "event_id": "ev-email-1", + "observed_at": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:email-send" + ] + }, + "action": { + "action_id": "act-email-1", + "tool_name": "email.send", + "capability": { + "capability_id": "cap:email-send", + "effect_kind": "email.send", + "resource_pattern": "mailto:*" + }, + "effects": [ + { + "effect_kind": "email.send" + } + ], + "reads": [ + { + "resource_id": "res-mail", + "uri": "mailto:user@example.com", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "output_hash": "sha256:2222222222222222222222222222222222222222222222222222222222222222" + }, + "decision": "allow", + "decision_reason": "authorized", + "policy_ref": "policy/default.v0", + "evidence_refs": [], + "runtime_ref": "agent-runtime", + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "payload_hash": "sha256:4fc498fe4c9eb01e3b869b1b3421a858f49d4637b2cd8e5f280fcc6225011691", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:4fc498fe4c9eb01e3b869b1b3421a858f49d4637b2cd8e5f280fcc6225011691" +} diff --git a/examples/pf-core-valid/empty_trace/trace.json b/examples/pf-core-valid/empty_trace/trace.json new file mode 100644 index 0000000..c45ee68 --- /dev/null +++ b/examples/pf-core-valid/empty_trace/trace.json @@ -0,0 +1,14 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "trace-empty-1", + "workflow_id": "agent_tool_use.safety_v0", + "events": [], + "trace_hash": "sha256:0a93526149a087ca63b8b5cfc48f2a8c7ffdc4425a13f89d02ba846d431914cd", + "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:0a93526149a087ca63b8b5cfc48f2a8c7ffdc4425a13f89d02ba846d431914cd" +} diff --git a/examples/pf-core-valid/file_read_allowed/event.json b/examples/pf-core-valid/file_read_allowed/event.json new file mode 100644 index 0000000..81215f1 --- /dev/null +++ b/examples/pf-core-valid/file_read_allowed/event.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/examples/pf-core-valid/file_read_allowed/manifest.json b/examples/pf-core-valid/file_read_allowed/manifest.json new file mode 100644 index 0000000..59e86cc --- /dev/null +++ b/examples/pf-core-valid/file_read_allowed/manifest.json @@ -0,0 +1,3 @@ +{ + "description": "Allowed file read within tenant" +} diff --git a/examples/pf-core-valid/file_read_allowed/observation.json b/examples/pf-core-valid/file_read_allowed/observation.json new file mode 100644 index 0000000..5a9c5c5 --- /dev/null +++ b/examples/pf-core-valid/file_read_allowed/observation.json @@ -0,0 +1,54 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreRuntimeObservation.v0", + "observation_id": "obs-file-read-1", + "trace_id": "trace-file-read-1", + "event_id": "ev-file-read-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": "allow", + "decision_reason": "authorized", + "policy_ref": "policy/default.v0", + "evidence_refs": [], + "runtime_ref": "agent-runtime", + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "payload_hash": "sha256:4e1b379c29109b66d62d5f8c2872dfd1f975550063a0637007efb9dff7ce5927", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:4e1b379c29109b66d62d5f8c2872dfd1f975550063a0637007efb9dff7ce5927" +} diff --git a/examples/pf-core-valid/file_read_allowed/trace.json b/examples/pf-core-valid/file_read_allowed/trace.json new file mode 100644 index 0000000..32a18d1 --- /dev/null +++ b/examples/pf-core-valid/file_read_allowed/trace.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/examples/pf-core-valid/file_read_denied_cross_tenant/event.json b/examples/pf-core-valid/file_read_denied_cross_tenant/event.json new file mode 100644 index 0000000..28004ad --- /dev/null +++ b/examples/pf-core-valid/file_read_denied_cross_tenant/event.json @@ -0,0 +1,52 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-denied-tenant-1", + "trace_id": "trace-denied-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" + ] + }, + "action": { + "action_id": "act-denied-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-x", + "uri": "/data/secret.txt", + "tenant": "tenant-b" + } + ], + "writes": [], + "input_hash": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "output_hash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + }, + "decision": "deny", + "decision_reason": "authorized", + "contract_refs": [], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:c335b1a5495af902ef0e056e6a1eff60715bc505160f5439acb97a5ddd837ce1", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:c335b1a5495af902ef0e056e6a1eff60715bc505160f5439acb97a5ddd837ce1" +} 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 new file mode 100644 index 0000000..f606091 --- /dev/null +++ b/examples/pf-core-valid/file_read_denied_cross_tenant/observation.json @@ -0,0 +1,54 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreRuntimeObservation.v0", + "observation_id": "obs-denied-tenant-1", + "trace_id": "trace-denied-tenant-1", + "event_id": "ev-denied-tenant-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-denied-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-x", + "uri": "/data/secret.txt", + "tenant": "tenant-b" + } + ], + "writes": [], + "input_hash": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "output_hash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + }, + "decision": "allow", + "decision_reason": "authorized", + "policy_ref": "policy/default.v0", + "evidence_refs": [], + "runtime_ref": "agent-runtime", + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "payload_hash": "sha256:544cc9079e1ef0111aea88af31fd5a0e933dcdcd50fb6d1601832da6139cdffd", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:544cc9079e1ef0111aea88af31fd5a0e933dcdcd50fb6d1601832da6139cdffd" +} diff --git a/examples/pf-core-valid/handoff_subset_authority/handoff.json b/examples/pf-core-valid/handoff_subset_authority/handoff.json new file mode 100644 index 0000000..2fcacda --- /dev/null +++ b/examples/pf-core-valid/handoff_subset_authority/handoff.json @@ -0,0 +1,37 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreHandoff.v0", + "handoff_id": "handoff-subset-1", + "from_principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [ + "cap:handoff" + ] + }, + "to_principal": { + "principal_id": "agent-2", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "handoff_delegate" + ], + "capabilities": [] + }, + "delegated_capabilities": [ + { + "capability_id": "cap:handoff", + "effect_kind": "handoff.delegate", + "resource_pattern": "agent:*" + } + ], + "reason": "delegate handoff authority to agent-2", + "evidence_refs": [ + "evidence/handoff.v0" + ], + "signature_or_digest": "sha256:01d19eed375587e2d9e05b412e5c347ddad004105681100a21bc5e6f10d5b726" +} diff --git a/examples/pf-core-valid/labtrust_replay/README.md b/examples/pf-core-valid/labtrust_replay/README.md new file mode 100644 index 0000000..0dd8f77 --- /dev/null +++ b/examples/pf-core-valid/labtrust_replay/README.md @@ -0,0 +1,27 @@ +# LabTrust replay example + +This fixture demonstrates the untrusted adapter path from LabTrust PCS release +artifacts to `PFCoreTrace.v0`, as documented in +[docs/pf-core-trace-mapping.md](../../docs/pf-core-trace-mapping.md). + +## Source artifacts + +- `examples/labtrust/trace_certificate.valid.json` — PCS `TraceCertificate.v0` +- `examples/labtrust/science_claim_bundle.certified.valid.json` — runtime receipt + +## Adapter + +```python +from pcs_core.pf_core_labtrust_adapter import normalize_labtrust_release +``` + +The adapter builds a single-event trace with `lab.release` effect and principal +`lab-operator-1`. PCS `spec_hash` maps to PF-Core `contract_hash`; the PCS +`trace_hash` binding is recorded in `manifest.json` (organizational cross-reference). + +## Verification + +```bash +pcs pf-core validate-trace examples/pf-core-valid/labtrust_replay/trace.json +pcs pf-core replay-trace examples/pf-core-valid/labtrust_replay/trace.json +``` diff --git a/examples/pf-core-valid/labtrust_replay/manifest.json b/examples/pf-core-valid/labtrust_replay/manifest.json new file mode 100644 index 0000000..68432da --- /dev/null +++ b/examples/pf-core-valid/labtrust_replay/manifest.json @@ -0,0 +1,18 @@ +{ + "description": "LabTrust PCS release adapted to PFCoreTrace.v0 per docs/pf-core-trace-mapping.md", + "adapter": "python/pcs_core/pf_core_labtrust_adapter.py::normalize_labtrust_release", + "replay_required": true, + "trace_file": "trace.json", + "source_artifacts": { + "trace_certificate": "../../labtrust/trace_certificate.valid.json", + "runtime_receipt": "../../labtrust/science_claim_bundle.certified.valid.json#runtime_receipts[0]" + }, + "pcs_binding": { + "trace_hash": "sha256:5555555555555555555555555555555555555555555555555555555555555555", + "spec_hash": "sha256:6666666666666666666666666666666666666666666666666666666666666666", + "maps_to": { + "certificate.contract_hash": "spec_hash", + "receipt.policy_hash": "policy_hash" + } + } +} diff --git a/examples/pf-core-valid/labtrust_replay/trace.json b/examples/pf-core-valid/labtrust_replay/trace.json new file mode 100644 index 0000000..a58ffcd --- /dev/null +++ b/examples/pf-core-valid/labtrust_replay/trace.json @@ -0,0 +1,71 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": "runs-qc-release", + "workflow_id": "labtrust.qc_release.v0", + "events": [ + { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "evt-lab-release-001", + "trace_id": "runs-qc-release", + "sequence": 0, + "timestamp": "2026-05-16T11:58:00Z", + "principal": { + "principal_id": "lab-operator-1", + "principal_kind": "human", + "tenant": "labtrust-qc", + "roles": [ + "lab_operator" + ], + "capabilities": [ + "cap:lab-release" + ] + }, + "action": { + "action_id": "act-lab-release-001", + "tool_name": "lab.release", + "capability": { + "capability_id": "cap:lab-release", + "effect_kind": "lab.release", + "resource_pattern": "lab:*" + }, + "effects": [ + { + "effect_kind": "lab.release" + } + ], + "reads": [ + { + "resource_id": "res-lab-release-001", + "uri": "lab:qc-release/run-001", + "tenant": "labtrust-qc" + } + ], + "writes": [], + "input_hash": "sha256:3333333333333333333333333333333333333333333333333333333333333333", + "output_hash": "sha256:5555555555555555555555555555555555555555555555555555555555555555" + }, + "decision": "allow", + "decision_reason": "CertificateChecked", + "contract_refs": [ + "qc_release.temporal.safety" + ], + "evidence_refs": [ + "cert-trace-qc-release-v0.1" + ], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:2b93b9f8b982b6780d83c1862f979f4b265f7cf8a02c663b5f52a77bf257d379", + "source_repo": "https://github.com/fraware/CertifyEdge", + "source_commit": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "signature_or_digest": "sha256:2b93b9f8b982b6780d83c1862f979f4b265f7cf8a02c663b5f52a77bf257d379" + } + ], + "trace_hash": "sha256:b7466cd65c822b9f14d7e8590d128a4aafd21114b1a43e302cdf0abbaf33ae43", + "policy_hash": "sha256:4444444444444444444444444444444444444444444444444444444444444444", + "contract_hash": "sha256:6666666666666666666666666666666666666666666666666666666666666666", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/fraware/CertifyEdge", + "source_commit": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "signature_or_digest": "sha256:b7466cd65c822b9f14d7e8590d128a4aafd21114b1a43e302cdf0abbaf33ae43" +} diff --git a/examples/pf-core-valid/network_denied/event.json b/examples/pf-core-valid/network_denied/event.json new file mode 100644 index 0000000..24845c4 --- /dev/null +++ b/examples/pf-core-valid/network_denied/event.json @@ -0,0 +1,50 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": "ev-network-deny-1", + "trace_id": "trace-network-deny-1", + "sequence": 0, + "timestamp": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [] + }, + "action": { + "action_id": "act-network-1", + "tool_name": "network.request", + "capability": { + "capability_id": "cap:network", + "effect_kind": "network.egress", + "resource_pattern": "*" + }, + "effects": [ + { + "effect_kind": "network.egress" + } + ], + "reads": [ + { + "resource_id": "res-net", + "uri": "https://example.com", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "output_hash": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "decision": "deny", + "decision_reason": "network egress denied", + "contract_refs": [], + "evidence_refs": [], + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "event_hash": "sha256:18819bcbb4eed4cc80c4fe91ce8898770e612ee1092f93506229137c80b7f08f", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:18819bcbb4eed4cc80c4fe91ce8898770e612ee1092f93506229137c80b7f08f" +} diff --git a/examples/pf-core-valid/network_denied/observation.json b/examples/pf-core-valid/network_denied/observation.json new file mode 100644 index 0000000..39c1341 --- /dev/null +++ b/examples/pf-core-valid/network_denied/observation.json @@ -0,0 +1,52 @@ +{ + "schema_version": "v0", + "artifact_type": "PFCoreRuntimeObservation.v0", + "observation_id": "obs-network-deny-1", + "trace_id": "trace-network-deny-1", + "event_id": "ev-network-deny-1", + "observed_at": "2026-06-18T00:00:00Z", + "principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [ + "agent" + ], + "capabilities": [] + }, + "action": { + "action_id": "act-network-1", + "tool_name": "network.request", + "capability": { + "capability_id": "cap:network", + "effect_kind": "network.egress", + "resource_pattern": "*" + }, + "effects": [ + { + "effect_kind": "network.egress" + } + ], + "reads": [ + { + "resource_id": "res-net", + "uri": "https://example.com", + "tenant": "tenant-a" + } + ], + "writes": [], + "input_hash": "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "output_hash": "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "decision": "deny", + "decision_reason": "network egress denied", + "policy_ref": "policy/default.v0", + "evidence_refs": [], + "runtime_ref": "agent-runtime", + "previous_event_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "payload_hash": "sha256:2b485a3185e6d101a6a4496b860c77706246c667ba903e9ed4c11d85285ae1b9", + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:2b485a3185e6d101a6a4496b860c77706246c667ba903e9ed4c11d85285ae1b9" +} diff --git a/examples/pf-core-valid/tool_use_trace_compiled/generated_proof.example.lean b/examples/pf-core-valid/tool_use_trace_compiled/generated_proof.example.lean new file mode 100644 index 0000000..77c0fb7 --- /dev/null +++ b/examples/pf-core-valid/tool_use_trace_compiled/generated_proof.example.lean @@ -0,0 +1,78 @@ +import PFCore.TraceCheck + +/-! +# Example generated concrete trace proof (fixture) + +This file mirrors output from `pcs pf-core lean-check` for +`examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json`. +Live runs write sibling artifacts under `lean/PFCore/Generated/`. +-/ + +namespace PFCore.Generated.Trace_716cbed45d37ebe4 + +def evt_001Principal : Principal := + { + id := "agent-safety-conformance-001", + tenant := "tenant-a", + roles := ["agent"], + capabilities := ["cap:file-read", "cap:email-send", "cap:handoff", "cap:mcp-invoke"] + } + +def evt_001Action : Action := + { + id := "act-evt-001", + toolName := "filesystem.read", + capability := "cap:file-read", + effects := [Effect.read], + reads := [{ + uri := "/data/report.txt", + tenant := "tenant-a", + labels := [] + }], + writes := [] + } + +def evt_001 : Event := + { + id := "evt-001", + principal := evt_001Principal, + action := evt_001Action, + decision := Decision.allow + } + +def evt_002Principal : Principal := + { + id := "agent-safety-conformance-001", + tenant := "tenant-a", + roles := ["agent"], + capabilities := ["cap:file-read", "cap:email-send", "cap:handoff", "cap:mcp-invoke"] + } + +def evt_002Action : Action := + { + id := "act-evt-002", + toolName := "network.request", + capability := "cap:network", + effects := [Effect.network], + reads := [{ + uri := "https://example.com", + tenant := "tenant-a", + labels := [] + }], + writes := [] + } + +def evt_002 : Event := + { + id := "evt-002", + principal := evt_002Principal, + action := evt_002Action, + decision := Decision.deny + } + +def trace_agent_safety_001 : Trace := Trace.cons (Trace.cons (Trace.empty) evt_001) evt_002 + +theorem concrete_trace_safe : traceSafeD trace_agent_safety_001 = true := by + decide + +end PFCore.Generated.Trace_716cbed45d37ebe4 diff --git a/examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json b/examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json new file mode 100644 index 0000000..bf45bee --- /dev/null +++ b/examples/pf-core-valid/tool_use_trace_compiled/pfcore_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": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "a111111111111111111111111111111111111111", + "signature_or_digest": "sha256:716cbed45d37ebe49deffd517021e74f7f8751f6baf6e00c2fdc6c3022b626f3" +} diff --git a/examples/pf-core-valid/tool_use_trace_compiled/tool_use_trace.json b/examples/pf-core-valid/tool_use_trace_compiled/tool_use_trace.json new file mode 100644 index 0000000..b241e39 --- /dev/null +++ b/examples/pf-core-valid/tool_use_trace_compiled/tool_use_trace.json @@ -0,0 +1,44 @@ +{ + "schema_version": "v0", + "trace_id": "trace-agent-safety-001", + "workflow_id": "agent_tool_use.safety_v0", + "agent_id": "agent-safety-conformance-001", + "policy_id": "policy-no-secret-exfiltration-v0", + "policy_hash": "sha256:76d4443f09c6fb0d6cc7bebc5c80eae53bd008e4a212ab61d0d1844ac773b5cd", + "started_at": "2026-05-18T00:00:00Z", + "completed_at": "2026-05-18T00:00:05Z", + "tool_calls": [ + { + "event_id": "evt-001", + "timestamp": "2026-05-18T00:00:01Z", + "tool_name": "filesystem.read", + "tool_category": "filesystem", + "input_hash": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "output_hash": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "authorization_status": "authorized", + "policy_refs": [ + "policy-no-secret-exfiltration-v0" + ], + "resource_uri": "/data/report.txt", + "tenant": "tenant-a" + }, + { + "event_id": "evt-002", + "timestamp": "2026-05-18T00:00:02Z", + "tool_name": "network.request", + "tool_category": "network", + "input_hash": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "output_hash": "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "authorization_status": "rejected", + "policy_refs": [ + "policy-no-secret-exfiltration-v0" + ], + "resource_uri": "https://example.com", + "tenant": "tenant-a" + } + ], + "trace_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "a111111111111111111111111111111111111111", + "signature_or_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/examples/tool-use-release/release_manifest.v0.json b/examples/tool-use-release/release_manifest.v0.json index fe677a5..937d628 100644 --- a/examples/tool-use-release/release_manifest.v0.json +++ b/examples/tool-use-release/release_manifest.v0.json @@ -20,7 +20,7 @@ "sha256": "sha256:c1ecda744f3d3a7e55724d5555f8d97121fb8e46f6b6d83d871bab0b11985b15" }, "canonical_claim_id": "claim-qc-release-v0.1", - "limitations_notice": "This artifact is a proof-carrying tool-use simulation result. It is not a guarantee that a real deployed agent is safe.", + "limitations_notice": "This artifact is a proof-carrying tool-use simulation result. It does not guarantee trace-level safety preservation under stated assumptions for a real deployed runtime.", "producer_repos": { "pcs_core": { "repo": "https://github.com/SentinelOps-CI/pcs-core", @@ -102,7 +102,7 @@ } }, "release_status": "Validated", - "signature_or_digest": "sha256:a36994d8089a3335fca0b67e50be61a46fae8d53b4e68bc1fe1adda43de87ef4", + "signature_or_digest": "sha256:53488fc4bdd078761083ae982f1b8e5a1326564fad1e3d0d40ccddebdc5ee225", "proof_obligation": { "path": "proof_obligation.v0.json", "sha256": "sha256:850557b7f2e5c97c8c8de3ef8ff54ffad0372068705eeea5709dc545d57c1e3e" diff --git a/examples/tool-use-release/workflow_profile.v0.json b/examples/tool-use-release/workflow_profile.v0.json index 2eb2fb7..0fc3e33 100644 --- a/examples/tool-use-release/workflow_profile.v0.json +++ b/examples/tool-use-release/workflow_profile.v0.json @@ -50,6 +50,6 @@ "unapproved_network_call", "unknown_authorization_status" ], - "limitations_notice": "This artifact is a proof-carrying tool-use simulation result. It is not a guarantee that a real deployed agent is safe.", - "signature_or_digest": "sha256:39ea95403f2f7065eaa7cda0084f8f68865bdb9054882e10f016e6d255bc0a49" + "limitations_notice": "This artifact is a proof-carrying tool-use simulation result. It does not guarantee trace-level safety preservation under stated assumptions for a real deployed runtime.", + "signature_or_digest": "sha256:f08e4c928dff1be1d610cbd2513b4c5ac5a05f718b5803603c082f136fec23d0" } diff --git a/examples/workflow_profiles/agent_tool_use_safety.valid.json b/examples/workflow_profiles/agent_tool_use_safety.valid.json index 2eb2fb7..0fc3e33 100644 --- a/examples/workflow_profiles/agent_tool_use_safety.valid.json +++ b/examples/workflow_profiles/agent_tool_use_safety.valid.json @@ -50,6 +50,6 @@ "unapproved_network_call", "unknown_authorization_status" ], - "limitations_notice": "This artifact is a proof-carrying tool-use simulation result. It is not a guarantee that a real deployed agent is safe.", - "signature_or_digest": "sha256:39ea95403f2f7065eaa7cda0084f8f68865bdb9054882e10f016e6d255bc0a49" + "limitations_notice": "This artifact is a proof-carrying tool-use simulation result. It does not guarantee trace-level safety preservation under stated assumptions for a real deployed runtime.", + "signature_or_digest": "sha256:f08e4c928dff1be1d610cbd2513b4c5ac5a05f718b5803603c082f136fec23d0" } diff --git a/lean/PCS.lean b/lean/PCS.lean index f369a58..3183a38 100644 --- a/lean/PCS.lean +++ b/lean/PCS.lean @@ -5,7 +5,12 @@ import PCS.Artifact import PCS.Registry import PCS.Certificate import PCS.Bundle -import PCS.ReleaseChain +import PCS.Claim import PCS.ComputationWitness -import PCS.ToolUse +import PCS.EvidenceBundle +import PCS.Examples +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 8d8181d..6c1d42d 100644 --- a/lean/PCS/Artifact.lean +++ b/lean/PCS/Artifact.lean @@ -12,4 +12,42 @@ structure ArtifactRef where hash : Hash deriving DecidableEq, Repr +structure SourcePosition where + line : Nat + column : Nat + deriving Repr + +structure SourceSpanV0 where + sourceSpanId : String + schemaVersion : String := "v0" + sourceType : String + sourceUri : String + start : SourcePosition + endPos : SourcePosition + hash : String + description : String + deriving Repr + +structure AssumptionV0 where + assumptionId : String + text : String + kind : String + status : String + sourceSpanRefs : List String + deriving Repr + +structure AssumptionSetV0 where + assumptionSetId : String + schemaVersion : String := "v0" + createdAt : String + producer : String + producerVersion : String + sourceRepo : String + sourceCommit : String + assumptions : List AssumptionV0 + humanReviewStatus : String + status : String + signatureOrDigest : String + deriving Repr + end PCS diff --git a/lean/PCS/ComputationWitness.lean b/lean/PCS/ComputationWitness.lean index 03893b9..3c21ffd 100644 --- a/lean/PCS/ComputationWitness.lean +++ b/lean/PCS/ComputationWitness.lean @@ -17,10 +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 := + 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 feac856..bc7646c 100644 --- a/lean/PCS/ReleaseChain.lean +++ b/lean/PCS/ReleaseChain.lean @@ -9,11 +9,11 @@ import PCS.Hash 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 3d46545..1a6b888 100644 --- a/lean/PCS/Theorems.lean +++ b/lean/PCS/Theorems.lean @@ -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,7 +89,7 @@ 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 rw [hFailed] at hAdmits diff --git a/lean/PCS/ToolUse.lean b/lean/PCS/ToolUse.lean index f70f0dd..3b69bf1 100644 --- a/lean/PCS/ToolUse.lean +++ b/lean/PCS/ToolUse.lean @@ -20,6 +20,14 @@ 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 ? + toolTraceHashMatchesCertificate trace cert := by + simp [toolTraceHashMatchesCertificateD, toolTraceHashMatchesCertificate, decide_eq_true_iff] end PCS diff --git a/lean/PFCore.lean b/lean/PFCore.lean new file mode 100644 index 0000000..e7abdd2 --- /dev/null +++ b/lean/PFCore.lean @@ -0,0 +1,15 @@ +import PFCore.Basic +import PFCore.Principal +import PFCore.Capability +import PFCore.Resource +import PFCore.Action +import PFCore.Event +import PFCore.Trace +import PFCore.Handoff +import PFCore.Contract +import PFCore.Certificate +import PFCore.Soundness +import PFCore.Theorems +import PFCore.NonInterference +import PFCore.ContractDecide +import PFCore.TraceCheck diff --git a/lean/PFCore/Action.lean b/lean/PFCore/Action.lean new file mode 100644 index 0000000..df07ec4 --- /dev/null +++ b/lean/PFCore/Action.lean @@ -0,0 +1,48 @@ +import PFCore.Capability +import PFCore.Resource + +/-! +# PF-Core actions and allowance predicates +-/ + +namespace PFCore + +/-- Effect kinds for tool actions (closed enum plus custom labels). -/ +inductive Effect where + | read | write | network | externalMessage | codeExecution | stateChange + | custom : String → Effect +deriving Repr, DecidableEq + +/-- Tool invocation with capability requirement and resource footprint. -/ +structure Action where + id : String + toolName : String + capability : String + effects : List Effect + reads : List Resource + writes : List Resource +deriving Repr, DecidableEq + +/-- All read/write resources belong to principal `p`'s tenant. -/ +def ActionWithinTenant (p : Principal) (a : Action) : Prop := + allResourcesSameTenant p a.reads ∧ allResourcesSameTenant p a.writes + +def actionWithinTenantD (p : Principal) (a : Action) : Bool := + resourcesSameTenantD p a.reads && resourcesSameTenantD p a.writes + +theorem actionWithinTenantD_sound (p : Principal) (a : Action) : + actionWithinTenantD p a = true ↔ ActionWithinTenant p a := by + simp [actionWithinTenantD, ActionWithinTenant, resourcesSameTenantD_sound, and_left_comm] + +/-- Action is allowed when capability is held and resources stay in-tenant. -/ +def ActionAllowed (p : Principal) (a : Action) : Prop := + HasCapability p a.capability ∧ ActionWithinTenant p a + +def actionAllowedD (p : Principal) (a : Action) : Bool := + hasCapabilityD p a.capability && actionWithinTenantD p a + +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] + +end PFCore diff --git a/lean/PFCore/Basic.lean b/lean/PFCore/Basic.lean new file mode 100644 index 0000000..c0349d5 --- /dev/null +++ b/lean/PFCore/Basic.lean @@ -0,0 +1,14 @@ +/-! +# PF-Core trust kernel — basic definitions + +Root namespace for the PF-Core action-trace kernel. This package models +capability-bounded action safety on traces; it does not encode PCS +release-envelope or scientific-domain claims. +-/ + +namespace PFCore + +/-- PF-Core kernel version string (protocol metadata only). -/ +def pfCoreKernelVersion : String := "0.1.0" + +end PFCore diff --git a/lean/PFCore/Capability.lean b/lean/PFCore/Capability.lean new file mode 100644 index 0000000..df062b0 --- /dev/null +++ b/lean/PFCore/Capability.lean @@ -0,0 +1,22 @@ +import PFCore.Principal + +/-! +# PF-Core capability membership +-/ + +namespace PFCore + +/-- Principal `p` holds capability name `cap` when `cap` appears in `p.capabilities`. -/ +def HasCapability (p : Principal) (cap : String) : Prop := + cap ∈ p.capabilities + +/-- Boolean decider for `HasCapability`. -/ +def hasCapabilityD (p : Principal) (cap : String) : Bool := + decide (cap ∈ p.capabilities) + +/-- `hasCapabilityD` reflects `HasCapability` (soundness). -/ +theorem hasCapabilityD_sound (p : Principal) (cap : String) : + hasCapabilityD p cap = true ↔ HasCapability p cap := by + simp [hasCapabilityD, HasCapability, decide_eq_true_iff] + +end PFCore diff --git a/lean/PFCore/Certificate.lean b/lean/PFCore/Certificate.lean new file mode 100644 index 0000000..44096cb --- /dev/null +++ b/lean/PFCore/Certificate.lean @@ -0,0 +1,21 @@ +import PFCore.Trace + +/-! +# PF-Core certificate envelope (trace safety attestation metadata) +-/ + +namespace PFCore + +/-- PF-Core certificate metadata linking a trace hash to a safety claim class. -/ +structure Certificate where + certificateId : String + traceHash : String + claimClass : String + eventCount : Nat +deriving Repr, DecidableEq + +/-- Certificate attests trace safety when paired with a safe trace of matching length. -/ +def CertificateAttestsTraceSafe (cert : Certificate) (tr : Trace) : Prop := + cert.claimClass = "LeanKernelChecked" → TraceSafe tr + +end PFCore diff --git a/lean/PFCore/Contract.lean b/lean/PFCore/Contract.lean new file mode 100644 index 0000000..58dceb8 --- /dev/null +++ b/lean/PFCore/Contract.lean @@ -0,0 +1,94 @@ +import PFCore.Trace + +/-! +# PF-Core contracts on events and traces +-/ + +namespace PFCore + +/-- Named contract with pre/post conditions and trace invariant. -/ +structure Contract where + name : String + pre : Principal → Action → Prop + post : Principal → Action → Event → Prop + invariant : Trace → Prop + +/-- Canonical trace-safety invariant used by JSON `invariant.require_trace_safe`. -/ +def traceSafeInvariant : Trace → Prop := TraceSafe + +/-- Contract whose invariant is trace safety (matches runtime `require_trace_safe`). -/ +def traceSafeContract : Contract := + { name := "trace-safe" + pre := fun _ _ => True + post := fun _ _ _ => True + invariant := traceSafeInvariant } + +/-- Trace safety is preserved across `Trace.cons` when the new event is safe. -/ +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). -/ +theorem invariant_preserved_cons (tr : Trace) (ev : Event) : + traceSafeContract.invariant tr → EventSafe ev → + traceSafeContract.invariant (Trace.cons tr ev) := + trace_safe_invariant_preserved_cons tr ev + +/-- Single event satisfies contract pre and post (when allowed). -/ +def SatisfiesContract (c : Contract) (ev : Event) : Prop := + c.pre ev.principal ev.action ∧ + (ev.decision = Decision.deny ∨ c.post ev.principal ev.action ev) + +def TraceSatisfiesContract (c : Contract) : Trace → Prop + | Trace.empty => True + | Trace.cons tr ev => + TraceSatisfiesContract c tr ∧ + SatisfiesContract c ev ∧ + c.invariant (Trace.cons tr ev) + +/-- Sequential composition of two contracts (both must hold). -/ +def Contract.seq (c1 c2 : Contract) : Contract := + { name := c1.name ++ ";" ++ c2.name + pre := fun p a => c1.pre p a ∧ c2.pre p a + 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. -/ +theorem seq_contract_satisfaction_left (c1 c2 : Contract) (ev : Event) : + SatisfiesContract (Contract.seq c1 c2) ev ↔ + SatisfiesContract c1 ev ∧ SatisfiesContract c2 ev := by + simp only [SatisfiesContract, Contract.seq] + constructor + · rintro ⟨⟨hp1, hp2⟩, hpost⟩ + constructor + · exact ⟨hp1, Or.elim hpost Or.inl (fun h => Or.inr h.1)⟩ + · exact ⟨hp2, Or.elim hpost Or.inl (fun h => Or.inr h.2)⟩ + · rintro ⟨⟨hp1, hpost1⟩, ⟨hp2, hpost2⟩⟩ + refine ⟨⟨hp1, hp2⟩, ?_⟩ + rcases hpost1 with (deny1 | post1) <;> rcases hpost2 with (deny2 | post2) + · exact Or.inl deny1 + · exact Or.inl deny1 + · exact Or.inl deny2 + · exact Or.inr ⟨post1, post2⟩ + +theorem seq_contract_satisfaction_right (c1 c2 : Contract) (tr : Trace) : + TraceSatisfiesContract (Contract.seq c1 c2) tr ↔ + TraceSatisfiesContract c1 tr ∧ TraceSatisfiesContract c2 tr := by + induction tr with + | empty => simp [TraceSatisfiesContract, Contract.seq] + | cons tr ev ih => + constructor + · intro h + unfold TraceSatisfiesContract Contract.seq at h + rcases h with ⟨hTr, hEv, hInv1, hInv2⟩ + rcases ih.mp hTr with ⟨hTr1, hTr2⟩ + rcases (seq_contract_satisfaction_left c1 c2 ev).mp hEv with ⟨hEv1, hEv2⟩ + exact ⟨⟨hTr1, hEv1, hInv1⟩, ⟨hTr2, hEv2, hInv2⟩⟩ + · intro h + rcases h with ⟨⟨hTr1, hEv1, hInv1⟩, ⟨hTr2, hEv2, hInv2⟩⟩ + unfold TraceSatisfiesContract Contract.seq + refine ⟨ih.mpr ⟨hTr1, hTr2⟩, ?_, hInv1, hInv2⟩ + exact (seq_contract_satisfaction_left c1 c2 ev).mpr ⟨hEv1, hEv2⟩ + +end PFCore diff --git a/lean/PFCore/ContractDecide.lean b/lean/PFCore/ContractDecide.lean new file mode 100644 index 0000000..7904d88 --- /dev/null +++ b/lean/PFCore/ContractDecide.lean @@ -0,0 +1,143 @@ +import PFCore.Contract + +/-! +# PF-Core JSON contract deciders + +Decidable mirrors of `PFCoreContract.v0` pre/post/invariant fields used by +generated concrete proof obligations. Soundness theorems link deciders to +conservative Prop-level contract satisfaction. +-/ + +namespace PFCore + +/-- JSON contract preconditions (subset discharged in Lean). -/ +structure ContractPreSpec where + requireCapability : Option String := none + requireEffect : Option Effect := none + requireTenantMatch : Bool := false +deriving Repr + +/-- JSON contract postconditions (subset discharged in Lean). -/ +structure ContractPostSpec where + requireDecision : Option Decision := none + requireEventSafe : Bool := false +deriving Repr + +/-- JSON contract trace invariant (subset discharged in Lean). -/ +structure ContractInvariantSpec where + requireTraceSafe : Bool := false +deriving Repr + +def actionHasEffect (a : Action) (e : Effect) : Prop := e ∈ a.effects + +def actionHasEffectD (a : Action) (e : Effect) : Bool := decide (e ∈ a.effects) + +theorem actionHasEffectD_sound (a : Action) (e : Effect) : + actionHasEffectD a e = true ↔ actionHasEffect a e := by + simp [actionHasEffectD, actionHasEffect, decide_eq_true_iff] + +def contractPreD (spec : ContractPreSpec) (p : Principal) (a : Action) : Bool := + let capOk := match spec.requireCapability with + | none => true + | some cap => hasCapabilityD p cap + let effectOk := match spec.requireEffect with + | none => true + | some eff => actionHasEffectD a eff + let tenantOk := if spec.requireTenantMatch then actionWithinTenantD p a else true + capOk && effectOk && tenantOk + +def ContractPreHolds (spec : ContractPreSpec) (p : Principal) (a : Action) : Prop := + (match spec.requireCapability with + | none => True + | some cap => HasCapability p cap) ∧ + (match spec.requireEffect with + | none => True + | some eff => actionHasEffect a eff) ∧ + (if spec.requireTenantMatch then ActionWithinTenant p a else True) + +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⟩ + unfold contractPreD ContractPreHolds + cases cap <;> cases eff <;> cases tenant <;> + simp [hasCapabilityD_sound, actionHasEffectD_sound, actionWithinTenantD_sound, + decide_eq_true_iff, and_left_comm, and_assoc] + +def contractPostD (spec : ContractPostSpec) (ev : Event) : Bool := + let decisionOk := match spec.requireDecision with + | none => true + | some d => decide (ev.decision = d) + let safeOk := if spec.requireEventSafe then eventSafeD ev else true + decisionOk && safeOk + +def ContractPostHolds (spec : ContractPostSpec) (ev : Event) : Prop := + (match spec.requireDecision with + | none => True + | some d => ev.decision = d) ∧ + (if spec.requireEventSafe then EventSafe ev else True) + +theorem contractPostD_sound (spec : ContractPostSpec) (ev : Event) : + contractPostD spec ev = true ↔ ContractPostHolds spec ev := by + rcases spec with ⟨reqDec, reqSafe⟩ + cases ev with + | mk id p a d => + cases d <;> cases reqDec <;> cases reqSafe <;> + simp [contractPostD, ContractPostHolds, EventSafe, eventSafeD, eventSafeD_sound, + actionAllowedD_sound, decide_eq_true_iff, Bool.and_eq_true, if_true, if_false] + +def contractInvariantD (spec : ContractInvariantSpec) (tr : Trace) : Bool := + if spec.requireTraceSafe then traceSafeD tr else true + +def ContractInvariantHolds (spec : ContractInvariantSpec) (tr : Trace) : Prop := + if spec.requireTraceSafe then TraceSafe tr else True + +theorem contractInvariantD_sound (spec : ContractInvariantSpec) (tr : Trace) : + contractInvariantD spec tr = true ↔ ContractInvariantHolds spec tr := by + rcases spec with ⟨reqSafe⟩ + cases reqSafe <;> simp [contractInvariantD, ContractInvariantHolds, traceSafeD_sound] + +def satisfiesContractSpecD (pre : ContractPreSpec) (post : ContractPostSpec) (ev : Event) : Bool := + contractPreD pre ev.principal ev.action && contractPostD post ev + +def SatisfiesContractSpec (pre : ContractPreSpec) (post : ContractPostSpec) (ev : Event) : Prop := + ContractPreHolds pre ev.principal ev.action ∧ ContractPostHolds post ev + +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, + and_left_comm] + +def traceSatisfiesContractSpecsD (pre : ContractPreSpec) (post : ContractPostSpec) + (inv : ContractInvariantSpec) : Trace → Bool + | Trace.empty => true + | Trace.cons tr ev => + traceSatisfiesContractSpecsD pre post inv tr && + satisfiesContractSpecD pre post ev && + contractInvariantD inv (Trace.cons tr ev) + +def TraceSatisfiesContractSpecs (pre : ContractPreSpec) (post : ContractPostSpec) + (inv : ContractInvariantSpec) : Trace → Prop + | Trace.empty => True + | Trace.cons tr ev => + TraceSatisfiesContractSpecs pre post inv tr ∧ + SatisfiesContractSpec pre post ev ∧ + ContractInvariantHolds inv (Trace.cons tr ev) + +theorem traceSatisfiesContractSpecsD_sound (pre : ContractPreSpec) (post : ContractPostSpec) + (inv : ContractInvariantSpec) (tr : Trace) : + traceSatisfiesContractSpecsD pre post inv tr = true ↔ + TraceSatisfiesContractSpecs pre post inv tr := by + induction tr with + | empty => + cases inv with + | mk reqSafe => + cases reqSafe <;> simp [traceSatisfiesContractSpecsD, TraceSatisfiesContractSpecs, + contractInvariantD_sound] + | cons tr' ev ih => + cases inv with + | mk reqSafe => + cases reqSafe <;> + simp [traceSatisfiesContractSpecsD, TraceSatisfiesContractSpecs, ih, + satisfiesContractSpecD_sound, contractInvariantD_sound, and_assoc, and_left_comm] + +end PFCore diff --git a/lean/PFCore/Event.lean b/lean/PFCore/Event.lean new file mode 100644 index 0000000..80cb2c7 --- /dev/null +++ b/lean/PFCore/Event.lean @@ -0,0 +1,38 @@ +import PFCore.Action + +/-! +# PF-Core events and event-level safety +-/ + +namespace PFCore + +/-- Authorization outcome recorded on an event. -/ +inductive Decision where + | allow + | deny +deriving Repr, DecidableEq + +/-- One principal action with an explicit authorization decision. -/ +structure Event where + id : String + principal : Principal + action : Action + decision : Decision +deriving Repr, DecidableEq + +/-- Allowed events must correspond to allowed actions under PF-Core rules. -/ +def EventSafe (ev : Event) : Prop := + ev.decision = Decision.allow → ActionAllowed ev.principal ev.action + +def eventSafeD (ev : Event) : Bool := + match ev.decision with + | Decision.deny => true + | Decision.allow => actionAllowedD ev.principal ev.action + +theorem eventSafeD_sound (ev : Event) : + eventSafeD ev = true ↔ EventSafe ev := by + cases ev with + | mk _ p a d => + cases d <;> simp [EventSafe, eventSafeD, actionAllowedD_sound] + +end PFCore diff --git a/lean/PFCore/Generated/.gitkeep b/lean/PFCore/Generated/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lean/PFCore/Handoff.lean b/lean/PFCore/Handoff.lean new file mode 100644 index 0000000..e5531c7 --- /dev/null +++ b/lean/PFCore/Handoff.lean @@ -0,0 +1,45 @@ +import PFCore.Capability + +/-! +# PF-Core capability handoff (non-expanding delegation) +-/ + +namespace PFCore + +/-- Delegation of capabilities from one principal to another. -/ +structure Handoff where + fromPrincipal : Principal + toPrincipal : Principal + delegatedCapabilities : List String + +/-- Every capability in `xs` is also listed in `ys`. -/ +def CapabilitySubset (xs ys : List String) : Prop := + ∀ x, x ∈ xs → x ∈ ys + +def capabilitySubsetD (xs ys : List String) : Bool := + xs.all fun x => decide (x ∈ ys) + +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] + +/-- Handoff is safe when delegation is a subset and tenants match. -/ +def HandoffSafe (h : Handoff) : Prop := + CapabilitySubset h.delegatedCapabilities h.fromPrincipal.capabilities ∧ + h.fromPrincipal.tenant = h.toPrincipal.tenant + +def handoffSafeD (h : Handoff) : Bool := + capabilitySubsetD h.delegatedCapabilities h.fromPrincipal.capabilities && + decide (h.fromPrincipal.tenant = h.toPrincipal.tenant) + +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. -/ +theorem handoff_does_not_expand_authority (h : Handoff) (cap : String) : + HandoffSafe h → cap ∈ h.delegatedCapabilities → HasCapability h.fromPrincipal cap := by + intro hsafe hmem + exact hsafe.left cap hmem + +end PFCore diff --git a/lean/PFCore/NonInterference.lean b/lean/PFCore/NonInterference.lean new file mode 100644 index 0000000..93ef999 --- /dev/null +++ b/lean/PFCore/NonInterference.lean @@ -0,0 +1,76 @@ +import PFCore.Theorems + +/-! +# PF-Core conservative tenant-scoped non-interference + +This module proves a **conservative subset** of tenant isolation: principals and +resource reads/writes stay within a declared tenant for scoped traces and for +allowed events in safe traces. Full cross-tenant non-interference (e.g. covert +channels, handoff across tenants, or deny-event side effects) is not claimed. +-/ + +namespace PFCore + +/-- Alias for tenant alignment between principal and resource. -/ +abbrev SameTenant (p : Principal) (r : Resource) : Prop := SameTenantResource p r + +/-- Event `ev` is scoped to `tenant` when the principal and all resources match. -/ +def EventTenantScoped (tenant : String) (ev : Event) : Prop := + ev.principal.tenant = tenant ∧ ActionWithinTenant ev.principal ev.action + +def eventTenantScopedD (tenant : String) (ev : Event) : Bool := + decide (ev.principal.tenant = tenant) && actionWithinTenantD ev.principal ev.action + +theorem eventTenantScopedD_sound (tenant : String) (ev : Event) : + eventTenantScopedD tenant ev = true ↔ EventTenantScoped tenant ev := by + cases ev with + | mk _ p a d => + cases d <;> + simp [eventTenantScopedD, EventTenantScoped, actionWithinTenantD_sound, decide_eq_true_iff] + +/-- Every event in trace `tr` stays within `tenant` (principal + resources). -/ +def TraceTenantScoped (tenant : String) : Trace → Prop + | Trace.empty => True + | Trace.cons tr ev => TraceTenantScoped tenant tr ∧ EventTenantScoped tenant ev + +def traceTenantScopedD (tenant : String) (tr : Trace) : Bool := + match tr with + | Trace.empty => true + | Trace.cons tr' ev => traceTenantScopedD tenant tr' && eventTenantScopedD tenant ev + +theorem traceTenantScopedD_sound (tenant : String) (tr : Trace) : + traceTenantScopedD tenant tr = true ↔ TraceTenantScoped tenant tr := by + induction tr with + | empty => simp [traceTenantScopedD, TraceTenantScoped] + | 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. -/ +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. -/ +theorem eventSafe_allow_implies_tenant_scoped (ev : Event) (h : EventSafe ev) + (hallow : ev.decision = Decision.allow) : + EventTenantScoped ev.principal.tenant ev := by + have hallowed := allowed_event_has_allowed_action ev h hallow + rcases hallowed with ⟨_, hwithin⟩ + exact ⟨rfl, hwithin⟩ + +/-- Allowed events inside a safe trace are tenant-scoped (conservative non-interference link). -/ +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. -/ +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 := + traceSafe_allowed_event_tenant_scoped tr ev hTrace hIn hallow + +end PFCore diff --git a/lean/PFCore/Principal.lean b/lean/PFCore/Principal.lean new file mode 100644 index 0000000..bb29954 --- /dev/null +++ b/lean/PFCore/Principal.lean @@ -0,0 +1,17 @@ +import PFCore.Basic + +/-! +# PF-Core principals +-/ + +namespace PFCore + +/-- Agent or service identity with tenant scope and granted capabilities. -/ +structure Principal where + id : String + tenant : String + roles : List String + capabilities : List String +deriving Repr, DecidableEq + +end PFCore diff --git a/lean/PFCore/Resource.lean b/lean/PFCore/Resource.lean new file mode 100644 index 0000000..cd600f6 --- /dev/null +++ b/lean/PFCore/Resource.lean @@ -0,0 +1,39 @@ +import PFCore.Principal + +/-! +# PF-Core resources and tenant alignment +-/ + +namespace PFCore + +/-- Addressable resource scoped to a tenant. -/ +structure Resource where + uri : String + tenant : String + labels : List String +deriving Repr, DecidableEq + +/-- Resource `r` is visible to principal `p` when tenants match. -/ +def SameTenantResource (p : Principal) (r : Resource) : Prop := + p.tenant = r.tenant + +/-- Boolean decider for `SameTenantResource`. -/ +def sameTenantResourceD (p : Principal) (r : Resource) : Bool := + p.tenant == r.tenant + +theorem sameTenantResourceD_sound (p : Principal) (r : Resource) : + sameTenantResourceD p r = true ↔ SameTenantResource p r := by + simp [sameTenantResourceD, SameTenantResource, BEq.beq] + +/-- Every resource in `rs` shares principal `p`'s tenant. -/ +def allResourcesSameTenant (p : Principal) (rs : List Resource) : Prop := + ∀ r ∈ rs, SameTenantResource p r + +def resourcesSameTenantD (p : Principal) (rs : List Resource) : Bool := + rs.all fun r => sameTenantResourceD p r + +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] + +end PFCore diff --git a/lean/PFCore/Soundness.lean b/lean/PFCore/Soundness.lean new file mode 100644 index 0000000..97412b3 --- /dev/null +++ b/lean/PFCore/Soundness.lean @@ -0,0 +1,15 @@ +import PFCore.Action +import PFCore.Event +import PFCore.Handoff +import PFCore.Trace + +/-! +# PF-Core decider soundness aggregation module + +Imports all decider modules so `lake build PFCore` checks the full +soundness theorem family in one target. +-/ + +namespace PFCore + +end PFCore diff --git a/lean/PFCore/Theorems.lean b/lean/PFCore/Theorems.lean new file mode 100644 index 0000000..8517d75 --- /dev/null +++ b/lean/PFCore/Theorems.lean @@ -0,0 +1,36 @@ +import PFCore.Soundness + +/-! +# PF-Core trace safety theorems (trusted catalog) +-/ + +namespace PFCore + +/-- From event safety, an allowed decision implies the action was allowed. -/ +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. -/ +theorem event_in_safe_trace_is_safe (tr : Trace) (ev : Event) + (hTrace : TraceSafe tr) (hIn : EventIn ev tr) : EventSafe ev := by + induction tr with + | empty => simp [EventIn] at hIn + | cons tr' head ih => + rcases hTrace with ⟨hTrSafe, hHeadSafe⟩ + simp [EventIn] at hIn + cases hIn with + | inl heq => + subst heq + exact hHeadSafe + | inr hIn' => + exact ih hTrSafe hIn' + +/-- Any allowed event inside a safe trace corresponds to an allowed action. -/ +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 + have hEv := event_in_safe_trace_is_safe tr ev hTrace hIn + exact allowed_event_has_allowed_action ev hEv hallow + +end PFCore diff --git a/lean/PFCore/Trace.lean b/lean/PFCore/Trace.lean new file mode 100644 index 0000000..d43df51 --- /dev/null +++ b/lean/PFCore/Trace.lean @@ -0,0 +1,53 @@ +import PFCore.Event + +/-! +# PF-Core traces and trace-level safety +-/ + +namespace PFCore + +/-- Ordered action trace (oldest event nearest `empty`). -/ +inductive Trace where + | empty + | cons : Trace → Event → Trace +deriving Repr + +def TraceSafe : Trace → Prop + | Trace.empty => True + | Trace.cons tr ev => TraceSafe tr ∧ EventSafe ev + +def traceSafeD : Trace → Bool + | Trace.empty => true + | Trace.cons tr ev => traceSafeD tr && eventSafeD ev + +/-- Event membership in a trace (structural equality on `Event`). -/ +def EventIn (ev : Event) : Trace → Prop + | Trace.empty => False + | Trace.cons tr e => ev = e ∨ EventIn ev tr + +def eventInD (ev : Event) (tr : Trace) : Bool := + match tr with + | Trace.empty => false + | Trace.cons tr' e => decide (ev == e) || eventInD ev tr' + +theorem traceSafe_empty : TraceSafe Trace.empty := trivial + +theorem traceSafe_cons (tr : Trace) (ev : Event) : + TraceSafe (Trace.cons tr ev) ↔ TraceSafe tr ∧ EventSafe ev := by + rfl + +theorem traceSafeD_sound (tr : Trace) : + traceSafeD tr = true ↔ TraceSafe tr := by + induction tr with + | empty => simp [traceSafeD, TraceSafe] + | cons tr ev ih => + simp [traceSafeD, TraceSafe, eventSafeD_sound, ih, and_left_comm] + +theorem eventInD_sound (ev : Event) (tr : Trace) : + eventInD ev tr = true ↔ EventIn ev tr := by + induction tr with + | empty => simp [eventInD, EventIn] + | cons tr' e ih => + simp [eventInD, EventIn, ih, beq_iff_eq, decide_eq_true_iff] + +end PFCore diff --git a/lean/PFCore/TraceCheck.lean b/lean/PFCore/TraceCheck.lean new file mode 100644 index 0000000..08d42f5 --- /dev/null +++ b/lean/PFCore/TraceCheck.lean @@ -0,0 +1,29 @@ +import PFCore.ContractDecide +import PFCore.Handoff +import PFCore.Trace + +/-! +# PF-Core concrete trace checking helpers + +Supports generated proof obligations that reduce `traceSafeD` on concrete traces +to kernel decidable evaluation (`decide`). +-/ + +namespace PFCore + +/-- Alias for generated proof scripts referencing trace safety decider. -/ +abbrev traceSafeCheck (tr : Trace) : Bool := traceSafeD tr + +theorem traceSafeCheck_eq (tr : Trace) : traceSafeCheck tr = traceSafeD tr := rfl + +/-- Alias for generated per-event proof scripts. -/ +abbrev eventSafeCheck (ev : Event) : Bool := eventSafeD ev + +theorem eventSafeCheck_eq (ev : Event) : eventSafeCheck ev = eventSafeD ev := rfl + +/-- Alias for generated handoff proof scripts. -/ +abbrev handoffSafeCheck (h : Handoff) : Bool := handoffSafeD h + +theorem handoffSafeCheck_eq (h : Handoff) : handoffSafeCheck h = handoffSafeD h := rfl + +end PFCore diff --git a/lean/lakefile.lean b/lean/lakefile.lean index bd2ba90..d55189a 100644 --- a/lean/lakefile.lean +++ b/lean/lakefile.lean @@ -6,3 +6,6 @@ package «pcs-core» where lean_lib PCS where roots := #[`PCS] + +lean_lib PFCore where + roots := #[`PFCore] diff --git a/python/pcs_core/cli.py b/python/pcs_core/cli.py index 2454d78..f60f9d1 100644 --- a/python/pcs_core/cli.py +++ b/python/pcs_core/cli.py @@ -224,6 +224,218 @@ def cmd_examples_check() -> int: print(f" - {err}", file=sys.stderr) return 1 +def cmd_pf_core_audit_claims() -> int: + from pcs_core.pf_core_claims import audit_claims + + violations = audit_claims() + if violations: + for item in violations: + print( + f"FAIL {item.path}:{item.line}: forbidden phrase {item.phrase!r}; " + f"use {item.replacement!r}", + file=sys.stderr, + ) + return 1 + print("OK pf-core claim boundary (no forbidden phrases)") + return 0 + + +def cmd_pf_core_audit_boundary() -> int: + from pcs_core.pf_core_claims import audit_boundary + + issues = audit_boundary() + if issues: + for item in issues: + print(f"FAIL {item.code}: {item.message}", file=sys.stderr) + return 1 + print("OK pf-core trusted boundary docs and registry") + return 0 + + +def cmd_pf_core_validate_trace( + path: Path, + contracts_dir: Path | None = None, + *, + tenant_isolation: bool = False, +) -> int: + from pcs_core.pf_core_contract import load_contracts_from_dir, validate_trace_contracts + from pcs_core.pf_core_runtime import validate_pfcore_trace_hash_chain, validate_tenant_isolation + + data = _load_json(path) + errors = validate_pfcore_trace_hash_chain(data) + if tenant_isolation: + errors.extend(validate_tenant_isolation(data)) + if contracts_dir is not None: + contracts = load_contracts_from_dir(contracts_dir) + for issue in validate_trace_contracts(data, contracts): + errors.append(f"{issue.code}: {issue.message}" + (f" (at {issue.path})" if issue.path else "")) + if errors: + for err in errors: + print(f"FAIL {err}", file=sys.stderr) + return 1 + print(f"OK PFCoreTrace hash chain {path}") + return 0 + + +def cmd_pf_core_validate_contracts(trace: Path, contracts_dir: Path) -> int: + from pcs_core.pf_core_contract import load_contracts_from_dir, validate_trace_contracts + + data = _load_json(trace) + contracts = load_contracts_from_dir(contracts_dir) + issues = validate_trace_contracts(data, contracts) + if issues: + for issue in issues: + location = f" (at {issue.path})" if issue.path else "" + print(f"FAIL {issue.code}: {issue.message}{location}", file=sys.stderr) + return 1 + print(f"OK PF-Core contract satisfaction {trace}") + return 0 + + +def cmd_pf_core_compile_trace(path: Path) -> int: + from pcs_core.pf_core_runtime import compile_tool_use_trace_to_pfcore_trace + + data = _load_json(path) + try: + compiled = compile_tool_use_trace_to_pfcore_trace(data) + except Exception as exc: + print(f"FAIL {path}: {exc}", file=sys.stderr) + return 1 + print(json.dumps(compiled, indent=2)) + return 0 + + +def cmd_pf_core_audit_lean_catalog() -> int: + from pcs_core.pf_core_claims import audit_lean_catalog + + errors = audit_lean_catalog() + if errors: + for err in errors: + print(f"FAIL {err}", file=sys.stderr) + return 1 + print("OK pf-core lean catalog matches Lean sources") + return 0 + + +def cmd_pf_core_replay_trace( + trace: Path, + source: Path | None, + out: Path | None, + result_out: Path | None, +) -> int: + from pcs_core.pf_core_replay import print_replay_disclaimer, run_replay_trace + + print_replay_disclaimer() + code, result = run_replay_trace( + trace, + source_path=source, + out_path=out, + result_out_path=result_out, + ) + if code == 0: + dest = out or trace.with_name("PFCoreCertificate.v0.json") + print(f"OK PF-Core replay-trace {trace} -> {dest}") + else: + print(f"FAIL PF-Core replay-trace {trace}", file=sys.stderr) + for issue in result.get("issues", []): + print(f" - {issue.get('code')}: {issue.get('message')}", file=sys.stderr) + return code + + +def cmd_pf_core_attach_certificate_check( + trace: Path, + checker: str, + checker_version: str, + attestation_ref: str | None, + out: Path, +) -> int: + from pcs_core.pf_core_certificate import attach_external_certificate_check + from pcs_core.validate import validate_file + + data = _load_json(trace) + cert = attach_external_certificate_check( + data, + checker=checker, + checker_version=checker_version, + attestation_ref=attestation_ref, + ) + out.write_text(json.dumps(cert, indent=2), encoding="utf-8") + validate_file(out) + print(f"OK PF-Core attach-certificate-check {trace} -> {out}") + return 0 + + +def cmd_pf_core_certifyedge_check( + trace: Path, + property_id: str, + out: Path, + *, + checker_version: str = "0.1.0", + attestation_ref: str | None = None, +) -> int: + from pcs_core.pf_core_certifyedge import run_certifyedge_check, write_certifyedge_certificate + + try: + if out: + write_certifyedge_certificate( + trace, + property_id, + out, + checker_version=checker_version, + attestation_ref=attestation_ref, + ) + else: + result = run_certifyedge_check( + trace, + property_id, + checker_version=checker_version, + attestation_ref=attestation_ref, + ) + if not result.ok: + print(f"FAIL {result.message}", file=sys.stderr) + return 1 + print(json.dumps(result.certificate, indent=2)) + except RuntimeError as exc: + print(f"FAIL {exc}", file=sys.stderr) + return 1 + if out: + print(f"OK PF-Core certifyedge-check {trace} -> {out}") + return 0 + + +def cmd_pf_core_lean_check( + trace: Path, + out: Path | None, + result_out: Path | None, + skip_build: bool, + skip_lean_proof: bool, +) -> int: + from pcs_core.lean_check import run_pfcore_lean_check + + code, _result = run_pfcore_lean_check( + trace, + out_path=out, + result_out_path=result_out, + skip_build=skip_build, + skip_lean_proof=skip_lean_proof, + ) + if code == 0: + dest = out or trace.with_name("PFCoreCertificate.v0.json") + print(f"OK PF-Core lean-check {trace} -> {dest}") + return code + + +def cmd_pf_core_audit_lean_no_sorry() -> int: + from pcs_core.lean_check import audit_pfcore_lean_no_sorry + + errors = audit_pfcore_lean_no_sorry() + if errors: + for err in errors: + print(f"FAIL {err}", file=sys.stderr) + return 1 + print("OK pf-core lean no-sorry audit (lean/PFCore/)") + return 0 + def cmd_validate_release_manifest(path: Path) -> int: drift = validate_release_manifest(path) @@ -366,6 +578,115 @@ def main(argv: list[str] | None = None) -> int: hash_write = hash_sub.add_parser("write", help="Regenerate frozen hash vectors") hash_write.add_argument("--force", action="store_true") + pf_core_parser = sub.add_parser("pf-core", help="PF-Core trust boundary commands") + pf_core_sub = pf_core_parser.add_subparsers(dest="pf_core_cmd", required=True) + pf_core_sub.add_parser("audit-claims", help="Scan docs/examples for forbidden claim phrases") + pf_core_sub.add_parser("audit-boundary", help="Verify PF-Core docs and registry entries") + pf_core_sub.add_parser( + "audit-lean-catalog", + help="Verify trusted Lean catalog symbols exist in lean/**/*.lean", + ) + pf_core_validate = pf_core_sub.add_parser( + "validate-trace", + help="Validate PFCoreTrace.v0 hash chain", + ) + pf_core_validate.add_argument("path", type=Path) + pf_core_validate.add_argument( + "--contracts-dir", + type=Path, + default=None, + help="Optional directory of PFCoreContract.v0 JSON files", + ) + pf_core_validate.add_argument( + "--tenant-isolation", + action="store_true", + help="Also require conservative tenant isolation on all events", + ) + pf_core_contracts = pf_core_sub.add_parser( + "validate-contracts", + help="Validate PFCoreTrace events against PFCoreContract.v0 predicates", + ) + pf_core_contracts.add_argument("trace", type=Path) + pf_core_contracts.add_argument( + "--contracts-dir", + type=Path, + required=True, + help="Directory containing PFCoreContract.v0 JSON files", + ) + pf_core_compile = pf_core_sub.add_parser( + "compile-trace", + help="Compile ToolUseTrace.v0 to PFCoreTrace.v0", + ) + pf_core_compile.add_argument("path", type=Path) + pf_core_lean = pf_core_sub.add_parser( + "lean-check", + help="Check PFCoreTrace against deciders and PFCore Lean build", + ) + pf_core_lean.add_argument("--trace", type=Path, required=True) + pf_core_lean.add_argument("--out", type=Path, default=None) + pf_core_lean.add_argument( + "--skip-build", + action="store_true", + help="Skip lake build and concrete Lean proof (claim_class will be RuntimeChecked)", + ) + pf_core_lean.add_argument( + "--skip-lean-proof", + action="store_true", + help="Skip Lean codegen/proof; deciders only (claim_class will be RuntimeChecked)", + ) + pf_core_lean.add_argument( + "--result-out", + type=Path, + default=None, + help="Write LeanCheckResult.v0 JSON (default: alongside --out certificate)", + ) + pf_core_sub.add_parser( + "audit-lean-no-sorry", + help="Scan lean/PFCore/ for sorry/admit/axiom/unsafe", + ) + pf_core_replay = pf_core_sub.add_parser( + "replay-trace", + help="Replay PFCoreTrace.v0 hash chain (ReplayValidated)", + ) + pf_core_replay.add_argument("path", type=Path, help="PFCoreTrace.v0 JSON") + pf_core_replay.add_argument( + "--source", + type=Path, + default=None, + help="Optional ToolUseTrace.v0 or PFCoreRuntimeObservation.v0 source", + ) + pf_core_replay.add_argument("--out", type=Path, default=None) + pf_core_replay.add_argument( + "--result-out", + type=Path, + default=None, + help="Write LeanCheckResult.v0 JSON", + ) + pf_core_attach = pf_core_sub.add_parser( + "attach-certificate-check", + help="Wrap external checker attestation as CertificateChecked", + ) + pf_core_attach.add_argument("--trace", type=Path, required=True) + pf_core_attach.add_argument("--checker", type=str, required=True) + pf_core_attach.add_argument("--checker-version", type=str, required=True) + pf_core_attach.add_argument("--attestation-ref", type=str, default=None) + pf_core_attach.add_argument("--out", type=Path, required=True) + pf_core_certifyedge = pf_core_sub.add_parser( + "certifyedge-check", + help="Run CertifyEdge (or mock) and emit CertificateChecked PFCoreCertificate", + ) + pf_core_certifyedge.add_argument("--trace", type=Path, required=True) + pf_core_certifyedge.add_argument( + "--property", + type=str, + required=True, + help="CertifyEdge property id (e.g. qc_release.temporal.safety)", + ) + pf_core_certifyedge.add_argument("--out", type=Path, required=True) + pf_core_certifyedge.add_argument("--checker-version", type=str, default="0.1.0") + pf_core_certifyedge.add_argument("--attestation-ref", type=str, default=None) + + shared_hash_parser = sub.add_parser("shared-hash-vectors", help="Cross-language hash vectors") shared_hash_sub = shared_hash_parser.add_subparsers(dest="shared_hash_cmd", required=True) shared_hash_sub.add_parser("verify", help="Verify test_vectors/hash parity") @@ -515,6 +836,51 @@ def main(argv: list[str] | None = None) -> int: write_vectors(force=args.force) print("Wrote hash vectors") return 0 + + if args.command == "pf-core" and args.pf_core_cmd == "audit-claims": + return cmd_pf_core_audit_claims() + if args.command == "pf-core" and args.pf_core_cmd == "audit-boundary": + return cmd_pf_core_audit_boundary() + if args.command == "pf-core" and args.pf_core_cmd == "audit-lean-catalog": + return cmd_pf_core_audit_lean_catalog() + if args.command == "pf-core" and args.pf_core_cmd == "validate-trace": + return cmd_pf_core_validate_trace( + args.path, + args.contracts_dir, + tenant_isolation=args.tenant_isolation, + ) + if args.command == "pf-core" and args.pf_core_cmd == "validate-contracts": + return cmd_pf_core_validate_contracts(args.trace, args.contracts_dir) + if args.command == "pf-core" and args.pf_core_cmd == "compile-trace": + return cmd_pf_core_compile_trace(args.path) + if args.command == "pf-core" and args.pf_core_cmd == "lean-check": + return cmd_pf_core_lean_check( + args.trace, + args.out, + args.result_out, + args.skip_build, + args.skip_lean_proof, + ) + if args.command == "pf-core" and args.pf_core_cmd == "audit-lean-no-sorry": + return cmd_pf_core_audit_lean_no_sorry() + if args.command == "pf-core" and args.pf_core_cmd == "replay-trace": + return cmd_pf_core_replay_trace(args.path, args.source, args.out, args.result_out) + if args.command == "pf-core" and args.pf_core_cmd == "attach-certificate-check": + return cmd_pf_core_attach_certificate_check( + args.trace, + args.checker, + args.checker_version, + args.attestation_ref, + args.out, + ) + if args.command == "pf-core" and args.pf_core_cmd == "certifyedge-check": + return cmd_pf_core_certifyedge_check( + args.trace, + args.property, + args.out, + checker_version=args.checker_version, + attestation_ref=args.attestation_ref, + ) if args.command == "shared-hash-vectors" and args.shared_hash_cmd == "verify": return cmd_shared_hash_vectors_verify() if args.command == "shared-hash-vectors" and args.shared_hash_cmd == "write": diff --git a/python/pcs_core/lean_catalog.py b/python/pcs_core/lean_catalog.py index b64120f..607d1fe 100644 --- a/python/pcs_core/lean_catalog.py +++ b/python/pcs_core/lean_catalog.py @@ -1,10 +1,13 @@ -"""Fixed PCS Lean obligation kinds and theorem names (no heavy imports).""" +"""Fixed PCS and PF-Core Lean obligation kinds and theorem names.""" from __future__ import annotations -OBLIGATION_KIND_THEOREM: dict[str, str] = { +# PCS release-envelope family (lean/PCS/Theorems.lean). +PCS_OBLIGATION_KIND_THEOREM: dict[str, str] = { "CertificateMatchesRuntime": "admissible_release_has_matching_trace_hash", - "VerificationAdmitsBundle": ("admissible_release_has_verified_input_hash_equal_to_bundle_hash"), + "VerificationAdmitsBundle": ( + "admissible_release_has_verified_input_hash_equal_to_bundle_hash" + ), "SignedBundleAdmissible": ( "admissible_release_has_signed_input_hash_equal_to_verified_input_hash" ), @@ -12,5 +15,69 @@ "ComputationWitnessHashAlignment": "witness_result_hashes_admissible", } +PCS_UNTRUSTED_OBLIGATION_KIND_THEOREM: dict[str, str] = {} + +# PF-Core trace-safety family (lean/PFCore/Theorems.lean + Soundness.lean). +PF_CORE_OBLIGATION_KIND_THEOREM: dict[str, str] = { + "HasCapabilityDeciderSound": "hasCapabilityD_sound", + "ActionWithinTenantDeciderSound": "actionWithinTenantD_sound", + "ActionAllowedDeciderSound": "actionAllowedD_sound", + "EventSafeDeciderSound": "eventSafeD_sound", + "TraceSafeEmpty": "traceSafe_empty", + "TraceSafeCons": "traceSafe_cons", + "TraceSafeDeciderSound": "traceSafeD_sound", + "AllowedEventHasAllowedAction": "allowed_event_has_allowed_action", + "EveryAllowedEventInSafeTraceIsAllowed": "every_allowed_event_in_safe_trace_is_allowed", + "HandoffDoesNotExpandAuthority": "handoff_does_not_expand_authority", + "SeqContractSatisfactionLeft": "seq_contract_satisfaction_left", + "SeqContractSatisfactionRight": "seq_contract_satisfaction_right", + "ConsPreservesTenantScope": "cons_preserves_tenant_scope", + "TraceSafeAllowedEventTenantScoped": "traceSafe_allowed_event_tenant_scoped", + "ContractPreDeciderSound": "contractPreD_sound", + "ContractPostDeciderSound": "contractPostD_sound", + "SatisfiesContractSpecDeciderSound": "satisfiesContractSpecD_sound", + "TraceSatisfiesContractSpecsDeciderSound": "traceSatisfiesContractSpecsD_sound", +} + +PF_CORE_SOUNDNESS_THEOREMS = frozenset( + { + "hasCapabilityD_sound", + "actionWithinTenantD_sound", + "actionAllowedD_sound", + "eventSafeD_sound", + "traceSafe_empty", + "traceSafe_cons", + "traceSafeD_sound", + "handoffSafeD_sound", + "handoff_does_not_expand_authority", + "capabilitySubsetD_sound", + "sameTenantResourceD_sound", + "resourcesSameTenantD_sound", + "eventInD_sound", + "eventTenantScopedD_sound", + "traceTenantScopedD_sound", + "actionHasEffectD_sound", + "contractPreD_sound", + "contractPostD_sound", + "contractInvariantD_sound", + "satisfiesContractSpecD_sound", + "traceSatisfiesContractSpecsD_sound", + } +) + +PF_CORE_THEOREM_CATALOG = frozenset(PF_CORE_OBLIGATION_KIND_THEOREM.values()) | PF_CORE_SOUNDNESS_THEOREMS + +# Backward-compatible PCS aliases (Stage 1). +OBLIGATION_KIND_THEOREM = PCS_OBLIGATION_KIND_THEOREM +UNTRUSTED_OBLIGATION_KIND_THEOREM = PCS_UNTRUSTED_OBLIGATION_KIND_THEOREM KNOWN_OBLIGATION_KINDS = frozenset(OBLIGATION_KIND_THEOREM.keys()) +UNTRUSTED_OBLIGATION_KINDS = frozenset(UNTRUSTED_OBLIGATION_KIND_THEOREM.keys()) LEAN_THEOREM_CATALOG = frozenset(OBLIGATION_KIND_THEOREM.values()) +UNTRUSTED_LEAN_THEOREM_CATALOG = frozenset(UNTRUSTED_OBLIGATION_KIND_THEOREM.values()) + +LEAN_THEOREM_FAMILY = "Release-envelope consistency theorem family" +PF_CORE_LEAN_THEOREM_FAMILY = "PF-Core trace-safety theorem family" + +PF_CORE_TRUSTED_LEAN_DIR = "lean/PFCore" + +PF_CORE_FORBIDDEN_LEAN_TOKENS: tuple[str, ...] = ("sorry", "admit", "axiom", "unsafe") diff --git a/python/pcs_core/lean_check.py b/python/pcs_core/lean_check.py new file mode 100644 index 0000000..64e2869 --- /dev/null +++ b/python/pcs_core/lean_check.py @@ -0,0 +1,599 @@ +"""PF-Core Lean trace checking and no-sorry audit.""" + +from __future__ import annotations + +import json +import platform +import re +import shutil +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +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.paths import repo_root +from pcs_core.pf_core_lean_codegen import ( + compute_lean_environment_hash, + generate_proof_obligation_file, + proof_term_ref_from_path, + validate_contracts_before_codegen, +) +from pcs_core.pf_core_runtime import ( + compute_trace_hash, + expand_principal_capabilities, + principal_capabilities_explicit, + validate_pfcore_trace_hash_chain, + validate_resource_scope, +) +from pcs_core.validate import validate_schema + +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." +) + +LEAN_CHECK_DISCLAIMER = ( + "PF-Core lean-check validates trace events against Python deciders aligned with " + "the PF-Core Lean kernel predicates. When the full pipeline runs (default), it " + "generates a concrete Lean proof obligation file and requires `traceSafeD` to " + "evaluate to true via the Lean kernel (`decide`). " + "`LeanKernelChecked` is emitted only when that concrete proof succeeds. " + "Use `--skip-build` or `--skip-lean-proof` for runtime-only assurance " + "(`RuntimeChecked`)." +) + +PF_CORE_ASSUMPTION_REFS = [ + "docs/pf-core/assumptions.md", + "docs/pf-core/trusted-boundary.md", +] + +_FORBIDDEN_TOKEN_RE = re.compile( + r"\b(" + "|".join(re.escape(token) for token in PF_CORE_FORBIDDEN_LEAN_TOKENS) + r")\b" +) + + +@dataclass(frozen=True) +class PFCoreLeanCheckIssue: + code: str + message: str + path: str | None = None + + +def print_lean_check_disclaimer(*, stream=None) -> None: + stream = stream or sys.stderr + print(LEAN_CHECK_DISCLAIMER, file=stream) + + +def lean_dir() -> Path: + return repo_root() / "lean" + + +def pfcore_lean_dir() -> Path: + return lean_dir() / "PFCore" + + +def pfcore_generated_dir() -> Path: + return pfcore_lean_dir() / "Generated" + + +def pfcore_theorems_checked() -> list[str]: + return sorted(PF_CORE_THEOREM_CATALOG) + + +def lean_build_status(*, ok: bool, detail: str, target: str = "PFCore") -> dict[str, Any]: + return {"ok": ok, "target": target, "detail": detail} + + +def _windows_to_wsl_path(path: Path) -> str: + resolved = path.resolve() + drive = resolved.drive.rstrip(":").lower() + tail = resolved.as_posix().split(":", 1)[-1] + return f"/mnt/{drive}{tail}" + + +def _lake_invocation(args: list[str], *, cwd: Path) -> tuple[list[str], Path]: + if shutil.which("lake"): + return ["lake", *args], cwd + if platform.system() == "Windows" and shutil.which("wsl"): + wsl_cwd = _windows_to_wsl_path(cwd) + cmd = " ".join(["lake", *args]) + return ["wsl", "bash", "-lc", f"cd {wsl_cwd} && {cmd}"], cwd + return ["lake", *args], cwd + + +def _run_lake(args: list[str], *, cwd: Path | None = None) -> subprocess.CompletedProcess[str]: + directory = cwd or lean_dir() + command, _ = _lake_invocation(args, cwd=directory) + return subprocess.run( + command, + cwd=directory, + capture_output=True, + text=True, + check=False, + ) + + +def run_lean_library_build(*, target: str = "PFCore", skip_build: bool = False) -> tuple[bool, str]: + """Run `lake build ` in lean/ if present.""" + if skip_build: + return True, "skipped" + directory = lean_dir() + if not (directory / "lakefile.lean").is_file(): + return False, f"Lean project not found at {directory}" + if not shutil.which("lake") and not ( + platform.system() == "Windows" and shutil.which("wsl") + ): + return False, "lake executable not found (install Lean 4 toolchain or WSL)" + proc = _run_lake(["build", target], cwd=directory) + if proc.returncode != 0: + detail = (proc.stderr or proc.stdout or "").strip() + return False, detail or f"lake build {target} failed" + return True, "ok" + + +def run_lean_concrete_proof( + proof_path: Path, + *, + skip_build: bool = False, +) -> tuple[bool, str]: + """Compile a generated proof file with `lake env lean`.""" + if skip_build: + return False, "skipped" + directory = lean_dir() + if not proof_path.is_file(): + return False, f"generated proof file missing: {proof_path}" + build_ok, build_detail = run_lean_library_build(target="PFCore", skip_build=False) + if not build_ok: + return False, build_detail + try: + rel = proof_path.resolve().relative_to(directory.resolve()) + except ValueError: + return False, f"proof file must live under {directory}: {proof_path}" + proc = _run_lake(["env", "lean", rel.as_posix()], cwd=directory) + if proc.returncode != 0: + detail = (proc.stderr or proc.stdout or "").strip() + return False, detail or "lake env lean failed on generated proof" + return True, "ok" + + +def _allowed_capability_ids(principal: Mapping[str, Any]) -> set[str]: + return {str(cap) for cap in principal.get("capabilities", [])} + + +def _same_tenant(principal: Mapping[str, Any], action: Mapping[str, Any]) -> bool: + tenant = str(principal.get("tenant") or "") + for key in ("reads", "writes"): + resources = action.get(key) + if not isinstance(resources, list): + return False + for resource in resources: + if isinstance(resource, dict) and str(resource.get("tenant") or "") != tenant: + return False + return True + + +def has_capability_d(principal: Mapping[str, Any], capability: str) -> bool: + return capability in _allowed_capability_ids(principal) + + +def action_within_tenant_d(principal: Mapping[str, Any], action: Mapping[str, Any]) -> bool: + return _same_tenant(principal, action) + + +def action_allowed_d(principal: Mapping[str, Any], action: Mapping[str, Any]) -> bool: + capability = action.get("capability") + if not isinstance(capability, dict): + return False + cap_id = str(capability.get("capability_id") or "") + if not (has_capability_d(principal, cap_id) and action_within_tenant_d(principal, action)): + return False + try: + validate_resource_scope(action) + except Exception: + return False + return True + + +def event_safe_d(event: Mapping[str, Any]) -> bool: + decision = str(event.get("decision") or "") + if decision == "deny": + return True + if decision != "allow": + return False + principal = event.get("principal") + action = event.get("action") + if not isinstance(principal, dict) or not isinstance(action, dict): + return False + return action_allowed_d(principal, action) + + +def trace_safe_d(events: list[Mapping[str, Any]]) -> bool: + return all(event_safe_d(event) for event in events) + + +def build_decider_obligations(events: list[Mapping[str, Any]]) -> list[dict[str, Any]]: + obligations: list[dict[str, Any]] = [ + { + "kind": "TraceSafeDeciderSound", + "theorem": "traceSafeD_sound", + "passed": trace_safe_d(events), + } + ] + for index, event in enumerate(events): + obligations.append( + { + "kind": "EventSafeDeciderSound", + "theorem": "eventSafeD_sound", + "passed": event_safe_d(event), + "proof_ref": f"events[{index}]", + } + ) + return obligations + + +def audit_pfcore_lean_no_sorry(*, allowlist_path: Path | None = None) -> list[str]: + errors: list[str] = [] + pfcore_dir = pfcore_lean_dir() + if not pfcore_dir.is_dir(): + return ["PF-Core Lean directory missing: lean/PFCore/"] + + allowlist: set[tuple[str, str]] = set() + if allowlist_path is None: + allowlist_path = repo_root() / "docs" / "pf-core" / "trusted-boundary.md" + if allowlist_path.is_file(): + for line in allowlist_path.read_text(encoding="utf-8").splitlines(): + if not line.startswith("| `lean/PFCore/"): + continue + cells = [cell.strip() for cell in line.split("|")] + if len(cells) < 4: + continue + rel = cells[1].strip("`") + exception = cells[2] + if exception and exception != "—": + allowlist.add((rel, exception)) + + for path in sorted(pfcore_dir.rglob("*.lean")): + rel = f"lean/PFCore/{path.relative_to(pfcore_dir).as_posix()}" + try: + content = path.read_text(encoding="utf-8") + except OSError as exc: + errors.append(f"{rel}: read failed: {exc}") + continue + for match in _FORBIDDEN_TOKEN_RE.finditer(content): + token = match.group(1) + if (rel, token) in allowlist: + continue + line_no = content.count("\n", 0, match.start()) + 1 + errors.append(f"{rel}:{line_no}: forbidden token {token!r}") + return errors + + +def _trace_events(trace: Mapping[str, Any]) -> list[dict[str, Any]]: + events = trace.get("events") + if not isinstance(events, list): + return [] + return [event for event in events if isinstance(event, dict)] + + +def check_pfcore_trace_lean_semantics(trace: Mapping[str, Any]) -> list[PFCoreLeanCheckIssue]: + issues: list[PFCoreLeanCheckIssue] = [] + schema_errors = validate_schema(dict(trace), "PFCoreTrace.v0") + if schema_errors: + for err in schema_errors: + issues.append(PFCoreLeanCheckIssue("SchemaInvalid", err)) + return issues + + hash_errors = validate_pfcore_trace_hash_chain(dict(trace)) + for err in hash_errors: + issues.append(PFCoreLeanCheckIssue("HashChainInvalid", err)) + + events = _trace_events(trace) + for index, event in enumerate(events): + principal = event.get("principal") + if isinstance(principal, dict) and not principal_capabilities_explicit(principal): + expanded = expand_principal_capabilities(principal) + issues.append( + PFCoreLeanCheckIssue( + "PrincipalCapabilityMismatch", + "principal.capabilities must explicitly list all role-expanded " + f"capabilities for lean-check (expected {expanded!r})", + f"events[{index}].principal", + ) + ) + if not event_safe_d(event): + event_id = str(event.get("event_id") or index) + issues.append( + PFCoreLeanCheckIssue( + "EventUnsafe", + f"event {event_id!r} violates PF-Core EventSafe decider", + f"events[{index}]", + ) + ) + + if events and not trace_safe_d(events): + issues.append(PFCoreLeanCheckIssue("TraceUnsafe", "trace fails TraceSafe decider")) + + return issues + + +def build_pfcore_certificate( + trace: Mapping[str, Any], + *, + checker: str = "pcs-core", + checker_version: str = "0.1.0", + lean_build_ok: bool, + lean_proof_ok: bool, + skip_build: bool, + skip_lean_proof: bool, + build_detail: str, + proof_detail: str, + obligations: list[dict[str, Any]], + proof_term_ref: str | None = None, + lean_environment_hash: str | None = None, +) -> dict[str, Any]: + events = _trace_events(trace) + trace_hash = str(trace.get("trace_hash") or compute_trace_hash(dict(trace))) + policy_hash = str(trace.get("policy_hash") or "sha256:" + "0" * 64) + contract_hash = str(trace.get("contract_hash") or "sha256:" + "0" * 64) + + lean_proof_checked = lean_proof_ok and not skip_build and not skip_lean_proof + if lean_proof_checked: + claim_class = "LeanKernelChecked" + proof_ref = proof_term_ref + else: + claim_class = "RuntimeChecked" + proof_ref = None + + cert: dict[str, Any] = { + "schema_version": "v0", + "artifact_type": "PFCoreCertificate.v0", + "certificate_id": f"pfcore-cert-{trace.get('trace_id', 'unknown')}", + "trace_hash": trace_hash, + "contract_hash": contract_hash, + "policy_hash": policy_hash, + "claim_class": claim_class, + "checker": checker, + "checker_version": checker_version, + "assumption_refs": list(PF_CORE_ASSUMPTION_REFS), + "theorems_checked": pfcore_theorems_checked(), + "obligations": obligations, + "lean_build_status": lean_build_status( + ok=lean_build_ok and not skip_build, + detail=build_detail, + ), + "lean_proof_checked": lean_proof_checked, + "disclaimer": LEAN_CHECK_DISCLAIMER, + "event_count": len(events), + "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 proof_ref: + cert["proof_ref"] = proof_ref + cert["proof_term_ref"] = proof_ref + cert["signature_or_digest"] = canonical_hash(cert) + return cert + + +def build_lean_check_result( + *, + trace_path: Path, + issues: list[PFCoreLeanCheckIssue], + no_sorry_errors: list[str], + build_ok: bool, + build_detail: str, + proof_ok: bool, + proof_detail: str, + skip_build: bool, + skip_lean_proof: bool, + obligations: list[dict[str, Any]], + lean_environment_hash: str | None = None, + certificate: dict[str, Any] | None = None, +) -> dict[str, Any]: + claim_class = "OutOfScope" + status = "Rejected" + if certificate is not None: + claim_class = str(certificate["claim_class"]) + if certificate.get("lean_proof_checked"): + status = "LeanProofChecked" + else: + status = "DecidersPassed" + + result: dict[str, Any] = { + "artifact_type": "LeanCheckResult.v0", + "schema_version": "v0", + "status": status, + "claim_class": claim_class, + "trace_path": str(trace_path), + "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(), + "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}, + "disclaimer": LEAN_CHECK_DISCLAIMER, + "certificate": certificate, + "signature_or_digest": "sha256:" + "0" * 64, + } + if lean_environment_hash: + result["lean_environment_hash"] = lean_environment_hash + result["signature_or_digest"] = canonical_hash(result) + return result + + +def run_pfcore_lean_check( + trace_path: Path, + *, + out_path: Path | None = None, + result_out_path: Path | None = None, + skip_build: bool = False, + skip_lean_proof: bool = False, +) -> tuple[int, dict[str, Any]]: + """Validate trace semantics, optionally prove concrete trace safety in Lean.""" + print_lean_check_disclaimer() + data = json.loads(trace_path.read_text(encoding="utf-8")) + events = _trace_events(data) + obligations = build_decider_obligations(events) + lean_environment_hash = compute_lean_environment_hash() + + issues = check_pfcore_trace_lean_semantics(data) + contract_errors = validate_contracts_before_codegen(data, trace_path=trace_path) + for err in contract_errors: + issues.append(PFCoreLeanCheckIssue("ContractViolation", err)) + no_sorry_errors = audit_pfcore_lean_no_sorry() + build_ok, build_detail = run_lean_library_build(target="PFCore", skip_build=skip_build) + + proof_ok = False + proof_detail = "skipped" if skip_lean_proof or skip_build else "not-run" + proof_term_ref: str | None = None + + if not issues and not no_sorry_errors and not skip_lean_proof: + try: + proof_path = generate_proof_obligation_file( + data, + pfcore_generated_dir(), + trace_path=trace_path, + ) + proof_term_ref = proof_term_ref_from_path(proof_path) + obligations.append( + { + "kind": "ConcreteTraceSafe", + "theorem": "concrete_trace_safe", + "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 + except OSError as exc: + issues.append(PFCoreLeanCheckIssue("LeanCodegenFailed", str(exc))) + except ValueError as exc: + issues.append(PFCoreLeanCheckIssue("LeanCodegenFailed", str(exc))) + + def _emit(code: int, result: dict[str, Any]) -> tuple[int, dict[str, Any]]: + if result_out_path: + result_out_path.write_text(json.dumps(result, indent=2), encoding="utf-8") + elif out_path: + sibling = out_path.with_name("LeanCheckResult.v0.json") + sibling.write_text(json.dumps(result, indent=2), encoding="utf-8") + return code, result + + if issues or no_sorry_errors: + result = build_lean_check_result( + trace_path=trace_path, + issues=issues, + no_sorry_errors=no_sorry_errors, + build_ok=build_ok, + build_detail=build_detail, + proof_ok=proof_ok, + proof_detail=proof_detail, + skip_build=skip_build, + skip_lean_proof=skip_lean_proof, + obligations=obligations, + lean_environment_hash=lean_environment_hash, + ) + return _emit(1, result) + + if not build_ok and not skip_build: + issues.append(PFCoreLeanCheckIssue("LeanBuildFailed", build_detail)) + result = build_lean_check_result( + trace_path=trace_path, + issues=issues, + no_sorry_errors=no_sorry_errors, + build_ok=build_ok, + build_detail=build_detail, + proof_ok=proof_ok, + proof_detail=proof_detail, + skip_build=skip_build, + skip_lean_proof=skip_lean_proof, + obligations=obligations, + lean_environment_hash=lean_environment_hash, + ) + return _emit(1, result) + + if not skip_lean_proof and not skip_build and not proof_ok: + issues.append(PFCoreLeanCheckIssue("LeanProofFailed", proof_detail)) + result = build_lean_check_result( + trace_path=trace_path, + issues=issues, + no_sorry_errors=no_sorry_errors, + build_ok=build_ok, + build_detail=build_detail, + proof_ok=proof_ok, + proof_detail=proof_detail, + skip_build=skip_build, + skip_lean_proof=skip_lean_proof, + obligations=obligations, + lean_environment_hash=lean_environment_hash, + ) + return _emit(1, result) + + cert = build_pfcore_certificate( + data, + lean_build_ok=build_ok, + lean_proof_ok=proof_ok, + skip_build=skip_build, + skip_lean_proof=skip_lean_proof, + build_detail=build_detail, + proof_detail=proof_detail, + obligations=obligations, + proof_term_ref=proof_term_ref, + lean_environment_hash=lean_environment_hash, + ) + cert_errors = validate_schema(cert, "PFCoreCertificate.v0") + if cert_errors: + for err in cert_errors: + issues.append(PFCoreLeanCheckIssue("CertificateInvalid", err)) + result = build_lean_check_result( + trace_path=trace_path, + issues=issues, + no_sorry_errors=no_sorry_errors, + build_ok=build_ok, + build_detail=build_detail, + proof_ok=proof_ok, + proof_detail=proof_detail, + skip_build=skip_build, + skip_lean_proof=skip_lean_proof, + obligations=obligations, + lean_environment_hash=lean_environment_hash, + ) + return _emit(1, result) + + result = build_lean_check_result( + trace_path=trace_path, + issues=[], + no_sorry_errors=[], + build_ok=build_ok, + build_detail=build_detail, + proof_ok=proof_ok, + proof_detail=proof_detail, + skip_build=skip_build, + skip_lean_proof=skip_lean_proof, + obligations=obligations, + lean_environment_hash=lean_environment_hash, + certificate=cert, + ) + if out_path: + out_path.write_text(json.dumps(cert, indent=2), encoding="utf-8") + return _emit(0, result) + + +def cmd_lean_check_disclaimer_only() -> int: + print(PCS_LEAN_CHECK_DISCLAIMER, file=sys.stderr) + print_lean_check_disclaimer() + print( + "Note: use `pcs pf-core lean-check --trace ` " + "for PF-Core trace checking.", + file=sys.stderr, + ) + return 2 diff --git a/python/pcs_core/pf_core_certificate.py b/python/pcs_core/pf_core_certificate.py new file mode 100644 index 0000000..574070f --- /dev/null +++ b/python/pcs_core/pf_core_certificate.py @@ -0,0 +1,60 @@ +"""PF-Core certificate construction helpers.""" + +from __future__ import annotations + +from typing import Any, Mapping + +from pcs_core.hash import canonical_hash +from pcs_core.lean_check import PF_CORE_ASSUMPTION_REFS +from pcs_core.pf_core_runtime import GENESIS_HASH + +CERTIFICATE_CHECK_DISCLAIMER = ( + "CertificateChecked attests that an external checker (e.g. CertifyEdge) evaluated " + "the trace against a declared property. This claim class does not imply " + "LeanKernelChecked or ReplayValidated assurance." +) + + +def attach_external_certificate_check( + trace: Mapping[str, Any], + *, + checker: str, + checker_version: str, + external_status: str = "CertificateChecked", + attestation_ref: str | None = None, + assumption_refs: list[str] | None = None, +) -> dict[str, Any]: + """Wrap an external checker attestation into PFCoreCertificate.v0.""" + events = trace.get("events") + event_count = len(events) if isinstance(events, list) else 0 + refs = list(assumption_refs or PF_CORE_ASSUMPTION_REFS) + if attestation_ref: + refs.append(attestation_ref) + + cert: dict[str, Any] = { + "schema_version": "v0", + "artifact_type": "PFCoreCertificate.v0", + "certificate_id": f"pfcore-ext-{trace.get('trace_id', 'unknown')}", + "trace_hash": str(trace.get("trace_hash") or GENESIS_HASH), + "contract_hash": str(trace.get("contract_hash") or GENESIS_HASH), + "policy_hash": str(trace.get("policy_hash") or GENESIS_HASH), + "claim_class": "CertificateChecked", + "checker": checker, + "checker_version": checker_version, + "assumption_refs": refs, + "obligations": [ + { + "kind": "ExternalCheckerAttestation", + "theorem": "external_checker_attestation", + "passed": external_status == "CertificateChecked", + "proof_ref": attestation_ref, + } + ], + "disclaimer": CERTIFICATE_CHECK_DISCLAIMER, + "event_count": event_count, + "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": GENESIS_HASH, + } + cert["signature_or_digest"] = canonical_hash(cert) + return cert diff --git a/python/pcs_core/pf_core_certifyedge.py b/python/pcs_core/pf_core_certifyedge.py new file mode 100644 index 0000000..d3843ab --- /dev/null +++ b/python/pcs_core/pf_core_certifyedge.py @@ -0,0 +1,208 @@ +"""CertifyEdge external checker integration for PF-Core traces.""" + +from __future__ import annotations + +import json +import os +import shutil +import subprocess +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from pcs_core.pf_core_certificate import attach_external_certificate_check +from pcs_core.validate import validate_schema + +CERTIFYEDGE_INSTALL_DOC = ( + "Install CertifyEdge from https://github.com/fraware/CertifyEdge and ensure " + "the `certifyedge` CLI is on PATH, or set PCS_CERTIFYEDGE_MOCK=1 for CI/demo." +) + + +@dataclass(frozen=True) +class CertificateCheckResult: + ok: bool + checker: str + checker_version: str + property_id: str + external_status: str + message: str + attestation_ref: str | None = None + mock: bool = False + certificate: dict[str, Any] | None = None + + +def certifyedge_mock_enabled() -> bool: + return os.environ.get("PCS_CERTIFYEDGE_MOCK", "").strip() in {"1", "true", "yes"} + + +def _find_certifyedge_cli() -> str | None: + return shutil.which("certifyedge") + + +def _load_trace(trace_path: Path) -> dict[str, Any]: + data = json.loads(trace_path.read_text(encoding="utf-8")) + if not isinstance(data, dict): + raise ValueError(f"{trace_path}: trace root must be a JSON object") + return data + + +def run_certifyedge_check( + trace_path: Path | str, + property_spec: str, + *, + checker_version: str = "0.1.0", + attestation_ref: str | None = None, +) -> CertificateCheckResult: + """Run CertifyEdge (or mock) against a PFCoreTrace and return check metadata.""" + path = Path(trace_path) + trace = _load_trace(path) + property_id = property_spec.strip() + if not property_id: + raise ValueError("property_spec (property id) is required") + + if certifyedge_mock_enabled(): + mock_ref = attestation_ref or f"mock://certifyedge/{property_id}" + cert = attach_external_certificate_check( + trace, + checker="certifyedge", + checker_version=checker_version, + external_status="CertificateChecked", + attestation_ref=mock_ref, + ) + cert["obligations"] = [ + { + "kind": "ExternalCheckerAttestation", + "theorem": "external_checker_attestation", + "passed": True, + "proof_ref": mock_ref, + }, + { + "kind": "CertifyEdgePropertyCheck", + "theorem": "certifyedge_property_check", + "passed": True, + "proof_ref": property_id, + }, + ] + return CertificateCheckResult( + ok=True, + checker="certifyedge", + checker_version=checker_version, + property_id=property_id, + external_status="CertificateChecked", + message="CertifyEdge mock attestation (PCS_CERTIFYEDGE_MOCK=1)", + attestation_ref=mock_ref, + mock=True, + certificate=cert, + ) + + cli = _find_certifyedge_cli() + if cli is None: + return CertificateCheckResult( + ok=False, + checker="certifyedge", + checker_version=checker_version, + property_id=property_id, + external_status="Rejected", + message=f"CertifyEdge CLI not found. {CERTIFYEDGE_INSTALL_DOC}", + ) + + cmd = [ + cli, + "check-trace", + "--trace", + str(path), + "--property", + property_id, + ] + try: + proc = subprocess.run(cmd, capture_output=True, text=True, check=False) + except OSError as exc: + return CertificateCheckResult( + ok=False, + checker="certifyedge", + checker_version=checker_version, + property_id=property_id, + external_status="Rejected", + message=f"CertifyEdge invocation failed: {exc}. {CERTIFYEDGE_INSTALL_DOC}", + ) + + if proc.returncode != 0: + detail = (proc.stderr or proc.stdout or "").strip() + return CertificateCheckResult( + ok=False, + checker="certifyedge", + checker_version=checker_version, + property_id=property_id, + external_status="Rejected", + message=detail or "CertifyEdge check-trace failed", + ) + + resolved_ref = attestation_ref or detail_attestation_ref(proc.stdout) + cert = attach_external_certificate_check( + trace, + checker="certifyedge", + checker_version=checker_version, + external_status="CertificateChecked", + attestation_ref=resolved_ref, + ) + cert["obligations"] = [ + { + "kind": "ExternalCheckerAttestation", + "theorem": "external_checker_attestation", + "passed": True, + "proof_ref": resolved_ref, + }, + { + "kind": "CertifyEdgePropertyCheck", + "theorem": "certifyedge_property_check", + "passed": True, + "proof_ref": property_id, + }, + ] + return CertificateCheckResult( + ok=True, + checker="certifyedge", + checker_version=checker_version, + property_id=property_id, + external_status="CertificateChecked", + message="CertifyEdge check-trace succeeded", + attestation_ref=resolved_ref, + mock=False, + certificate=cert, + ) + + +def detail_attestation_ref(stdout: str) -> str | None: + """Best-effort parse of CertifyEdge stdout for an attestation path.""" + for line in stdout.splitlines(): + stripped = line.strip() + if stripped.startswith("attestation:"): + return stripped.split(":", 1)[1].strip() + return None + + +def write_certifyedge_certificate( + trace_path: Path | str, + property_spec: str, + out_path: Path, + *, + checker_version: str = "0.1.0", + attestation_ref: str | None = None, +) -> CertificateCheckResult: + result = run_certifyedge_check( + trace_path, + property_spec, + checker_version=checker_version, + attestation_ref=attestation_ref, + ) + if not result.ok or result.certificate is None: + raise RuntimeError(result.message) + cert = result.certificate + if cert.get("claim_class") != "CertificateChecked": + raise RuntimeError("CertifyEdge path must emit CertificateChecked only") + errors = validate_schema(cert, "PFCoreCertificate.v0") + if errors: + raise RuntimeError(f"invalid PFCoreCertificate.v0: {'; '.join(errors)}") + out_path.write_text(json.dumps(cert, indent=2) + "\n", encoding="utf-8") + return result diff --git a/python/pcs_core/pf_core_claims.py b/python/pcs_core/pf_core_claims.py new file mode 100644 index 0000000..f3afcf9 --- /dev/null +++ b/python/pcs_core/pf_core_claims.py @@ -0,0 +1,213 @@ +"""PF-Core claim-boundary linter and Lean catalog audit.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from pathlib import Path + +from pcs_core.lean_catalog import ( + LEAN_THEOREM_CATALOG, + PF_CORE_THEOREM_CATALOG, + PF_CORE_TRUSTED_LEAN_DIR, +) +from pcs_core.paths import examples_dir, repo_root +from pcs_core.registry_data import PF_CORE_CLAIM_CLASSES, pf_core_artifact_types + +FORBIDDEN_PHRASES: tuple[tuple[str, str], ...] = ( + ("verified agent", "trace-level safety preservation under stated assumptions"), + ("guarantees ai safety", "contracted action safety under stated assumptions"), + ("model is safe", "schema-validated runtime observation"), + ( + "agent is safe", + "Lean-kernel-checked trace theorem (only when claim_class is LeanKernelChecked)", + ), + ("fully verified runtime", "runtime-checked trace with explicit claim class"), + ( + "formally verified platform", + "release-envelope consistency theorem family (for PCS Lean scope)", + ), +) + +_SCAN_SUFFIXES = {".md", ".json", ".txt", ".rst"} +_CLAIM_BOUNDARY_DOC = Path("docs") / "pf-core" / "claim-boundary.md" +_THEOREM_RE = re.compile(r"^\s*theorem\s+([A-Za-z0-9_]+)", re.MULTILINE) + + +@dataclass(frozen=True) +class ClaimViolation: + path: str + phrase: str + replacement: str + line: int + + +@dataclass(frozen=True) +class BoundaryIssue: + code: str + message: str + + +def _scan_roots() -> list[Path]: + roots = [repo_root() / "docs", examples_dir()] + return [path for path in roots if path.is_dir()] + + +def _iter_scan_files(root: Path) -> list[Path]: + files: list[Path] = [] + for path in root.rglob("*"): + if path.is_file() and path.suffix.lower() in _SCAN_SUFFIXES: + files.append(path) + return sorted(files) + + +def _relative_repo_path(path: Path) -> str: + root = repo_root() + try: + return str(path.relative_to(root)) + except ValueError: + return str(path) + + +def audit_claims() -> list[ClaimViolation]: + violations: list[ClaimViolation] = [] + for root in _scan_roots(): + for path in _iter_scan_files(root): + rel = Path(_relative_repo_path(path)) + if rel.as_posix() == _CLAIM_BOUNDARY_DOC.as_posix(): + continue + try: + text = path.read_text(encoding="utf-8") + except OSError: + continue + lower = text.lower() + for phrase, replacement in FORBIDDEN_PHRASES: + start = 0 + while True: + index = lower.find(phrase, start) + if index < 0: + break + line = text.count("\n", 0, index) + 1 + violations.append( + ClaimViolation( + path=_relative_repo_path(path), + phrase=phrase, + replacement=replacement, + line=line, + ) + ) + start = index + len(phrase) + return violations + + +def audit_boundary() -> list[BoundaryIssue]: + issues: list[BoundaryIssue] = [] + + claim_boundary = repo_root() / "docs" / "pf-core" / "claim-boundary.md" + if not claim_boundary.is_file(): + issues.append(BoundaryIssue("missing_claim_boundary_doc", str(claim_boundary))) + else: + text = claim_boundary.read_text(encoding="utf-8") + for claim_class in sorted(PF_CORE_CLAIM_CLASSES): + if claim_class not in text: + issues.append( + BoundaryIssue( + "claim_class_undocumented", + f"claim_class {claim_class!r} missing from claim-boundary.md", + ) + ) + + registry_types = pf_core_artifact_types() + expected = { + "PFCorePrincipal.v0", + "PFCoreCapability.v0", + "PFCoreResource.v0", + "PFCoreAction.v0", + "PFCoreEvent.v0", + "PFCoreTrace.v0", + "PFCoreContract.v0", + "PFCoreHandoff.v0", + "PFCoreCertificate.v0", + "PFCoreRuntimeObservation.v0", + } + missing_registry = expected - registry_types + for artifact_type in sorted(missing_registry): + issues.append( + BoundaryIssue( + "missing_registry_entry", + f"registry_data.py missing entry for {artifact_type}", + ) + ) + + mission = repo_root() / "docs" / "pf-core" / "mission.md" + if mission.is_file(): + mission_text = mission.read_text(encoding="utf-8") + required = ( + "PF-Core is the minimal trusted action-trace kernel inside PCS. " + "PCS defines evidence containers and release-chain artifacts; PF-Core " + "defines the formal semantics of agentic actions, contracted traces, " + "and trace-level safety preservation." + ) + if required not in mission_text: + issues.append( + BoundaryIssue("missing_mission_sentence", "mission.md missing required sentence") + ) + else: + issues.append(BoundaryIssue("missing_mission_doc", str(mission))) + + return issues + + +def _lean_sources() -> list[Path]: + lean_dir = repo_root() / "lean" + if not lean_dir.is_dir(): + return [] + return sorted(lean_dir.rglob("*.lean")) + + +def _collect_lean_theorem_names(*, pfcore_only: bool = False) -> set[str]: + names: set[str] = set() + lean_dir = repo_root() / "lean" + if not lean_dir.is_dir(): + return names + sources = sorted(lean_dir.rglob("*.lean")) + if pfcore_only: + pfcore = lean_dir / "PFCore" + sources = sorted(pfcore.glob("*.lean")) if pfcore.is_dir() else [] + for path in sources: + try: + text = path.read_text(encoding="utf-8") + except OSError: + continue + names.update(_THEOREM_RE.findall(text)) + return names + + +def audit_lean_catalog() -> list[str]: + """Return error messages for trusted catalog theorems missing from Lean sources.""" + errors: list[str] = [] + lean_theorems = _collect_lean_theorem_names() + if not lean_theorems and _lean_sources(): + errors.append("Could not parse any theorem names from lean/**/*.lean") + + for theorem in sorted(LEAN_THEOREM_CATALOG): + if theorem not in lean_theorems: + errors.append( + f"Lean theorem {theorem!r} listed in LEAN_THEOREM_CATALOG " + f"but absent from lean/**/*.lean" + ) + + pfcore_theorems = _collect_lean_theorem_names(pfcore_only=True) + pfcore_dir = repo_root() / "lean" / "PFCore" + if not pfcore_dir.is_dir(): + errors.append(f"PF-Core Lean directory missing: {PF_CORE_TRUSTED_LEAN_DIR}/") + elif not pfcore_theorems: + errors.append(f"Could not parse any theorem names from {PF_CORE_TRUSTED_LEAN_DIR}/") + + for theorem in sorted(PF_CORE_THEOREM_CATALOG): + if theorem not in pfcore_theorems: + errors.append( + f"Lean theorem {theorem!r} listed in PF_CORE_THEOREM_CATALOG " + f"but absent from {PF_CORE_TRUSTED_LEAN_DIR}/" + ) + return errors diff --git a/python/pcs_core/pf_core_contract.py b/python/pcs_core/pf_core_contract.py new file mode 100644 index 0000000..d7b2d96 --- /dev/null +++ b/python/pcs_core/pf_core_contract.py @@ -0,0 +1,231 @@ +"""PF-Core contract satisfaction runtime checker.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Mapping + +from pcs_core.pf_core_runtime import expand_principal_capabilities +from pcs_core.validate import validate_schema + + +@dataclass(frozen=True) +class ContractIssue: + code: str + message: str + path: str | None = None + + +def load_contract(path: Path | str) -> dict[str, Any]: + data = json.loads(Path(path).read_text(encoding="utf-8")) + if not isinstance(data, dict): + raise ValueError(f"{path}: contract root must be a JSON object") + errors = validate_schema(data, "PFCoreContract.v0") + if errors: + raise ValueError(f"{path}: invalid PFCoreContract.v0: {'; '.join(errors)}") + return data + + +def load_contracts(paths: list[Path]) -> dict[str, dict[str, Any]]: + contracts: dict[str, dict[str, Any]] = {} + for path in paths: + contract = load_contract(path) + contract_id = str(contract["contract_id"]) + contracts[contract_id] = contract + return contracts + + +def load_contracts_from_dir(directory: Path) -> dict[str, dict[str, Any]]: + contracts: dict[str, dict[str, Any]] = {} + for path in sorted(directory.glob("*.json")): + data = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(data, dict): + continue + if data.get("artifact_type") != "PFCoreContract.v0": + continue + errors = validate_schema(data, "PFCoreContract.v0") + if errors: + raise ValueError(f"{path}: invalid PFCoreContract.v0: {'; '.join(errors)}") + contract_id = str(data["contract_id"]) + contracts[contract_id] = data + return contracts + + +def _principal_has_capability(principal: Mapping[str, Any], capability_id: str) -> bool: + return capability_id in expand_principal_capabilities(dict(principal)) + + +def _action_has_effect(action: Mapping[str, Any], effect_kind: str) -> bool: + effects = action.get("effects") + if not isinstance(effects, list): + return False + return any( + isinstance(effect, dict) and str(effect.get("effect_kind") or "") == effect_kind + for effect in effects + ) + + +def _tenant_matches(principal: Mapping[str, Any], action: Mapping[str, Any]) -> bool: + tenant = str(principal.get("tenant") or "") + for key in ("reads", "writes"): + resources = action.get(key) + if not isinstance(resources, list): + continue + for resource in resources: + if isinstance(resource, dict) and str(resource.get("tenant") or "") != tenant: + return False + return True + + +def validate_event_against_contract( + event: Mapping[str, Any], + contract: Mapping[str, Any], + *, + path: str, +) -> list[ContractIssue]: + issues: list[ContractIssue] = [] + pre = contract.get("pre") + post = contract.get("post") + principal = event.get("principal") + action = event.get("action") + if not isinstance(principal, dict) or not isinstance(action, dict): + issues.append(ContractIssue("ContractEventInvalid", "event missing principal or action", path)) + 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, + ) + ) + required_cap = pre.get("require_capability") + if isinstance(required_cap, str) and required_cap: + if not _principal_has_capability(principal, required_cap): + issues.append( + ContractIssue( + "ContractCapabilityRequired", + f"contract {contract.get('contract_id')!r} requires capability {required_cap!r}", + f"{path}.principal", + ) + ) + required_effect = pre.get("require_effect") + if isinstance(required_effect, str) and required_effect: + if not _action_has_effect(action, required_effect): + issues.append( + ContractIssue( + "ContractEffectRequired", + f"contract {contract.get('contract_id')!r} requires effect {required_effect!r}", + f"{path}.action.effects", + ) + ) + required_role = pre.get("require_role") + if isinstance(required_role, str) and required_role: + roles = principal.get("roles") + if not isinstance(roles, list) or required_role not in [str(role) for role in roles]: + issues.append( + ContractIssue( + "ContractRoleRequired", + f"contract {contract.get('contract_id')!r} requires role {required_role!r}", + f"{path}.principal.roles", + ) + ) + required_policy = pre.get("require_policy_ref") + if isinstance(required_policy, str) and required_policy: + refs = event.get("contract_refs") + if not isinstance(refs, list) or required_policy not in [str(ref) for ref in refs]: + issues.append( + ContractIssue( + "ContractPolicyRefRequired", + f"contract {contract.get('contract_id')!r} requires policy ref {required_policy!r}", + f"{path}.contract_refs", + ) + ) + required_evidence = pre.get("require_evidence_ref") + if isinstance(required_evidence, str) and required_evidence: + evidence = event.get("evidence_refs") + if not isinstance(evidence, list) or required_evidence not in [ + str(ref) for ref in evidence + ]: + issues.append( + ContractIssue( + "ContractEvidenceRefRequired", + f"contract {contract.get('contract_id')!r} requires evidence ref {required_evidence!r}", + f"{path}.evidence_refs", + ) + ) + + if isinstance(post, dict): + required_decision = post.get("require_decision") + if isinstance(required_decision, str) and required_decision: + decision = str(event.get("decision") or "") + if decision != required_decision: + issues.append( + ContractIssue( + "ContractDecisionMismatch", + f"contract {contract.get('contract_id')!r} requires decision {required_decision!r}, " + f"got {decision!r}", + f"{path}.decision", + ) + ) + if post.get("require_event_safe") is True: + decision = str(event.get("decision") or "") + if decision == "allow": + cap = action.get("capability") + cap_id = str(cap.get("capability_id") or "") if isinstance(cap, dict) else "" + if not cap_id or not _principal_has_capability(principal, cap_id): + issues.append( + ContractIssue( + "ContractEventUnsafe", + f"allowed event violates contract {contract.get('contract_id')!r} event safety", + path, + ) + ) + elif not _tenant_matches(principal, action): + issues.append( + ContractIssue( + "ContractEventUnsafe", + f"allowed event violates contract {contract.get('contract_id')!r} tenant safety", + path, + ) + ) + + return issues + + +def validate_trace_contracts( + trace: Mapping[str, Any], + contracts: Mapping[str, Mapping[str, Any]], +) -> list[ContractIssue]: + issues: list[ContractIssue] = [] + events = trace.get("events") + if not isinstance(events, list): + return [ContractIssue("TraceInvalid", "events must be an array", "events")] + + for index, event in enumerate(events): + if not isinstance(event, dict): + continue + base = f"events[{index}]" + refs = event.get("contract_refs") + if not isinstance(refs, list) or not refs: + continue + for ref_index, ref in enumerate(refs): + contract_id = str(ref) + contract = contracts.get(contract_id) + if contract is None: + issues.append( + ContractIssue( + "ContractRefMissing", + f"unknown contract reference {contract_id!r}", + f"{base}.contract_refs[{ref_index}]", + ) + ) + continue + issues.extend( + validate_event_against_contract(event, contract, path=base) + ) + return issues diff --git a/python/pcs_core/pf_core_hash_vector_parity.py b/python/pcs_core/pf_core_hash_vector_parity.py new file mode 100644 index 0000000..6991408 --- /dev/null +++ b/python/pcs_core/pf_core_hash_vector_parity.py @@ -0,0 +1,116 @@ +"""Compare PCS hash vectors with provability-fabric-core adapter fixtures (native parity).""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import tempfile +from pathlib import Path + +from pcs_core.paths import repo_root + +DEFAULT_PF_CORE_TAG = "pf-core-v0.6.0" +DEFAULT_PF_CORE_REPO = "https://github.com/SentinelOps-CI/provability-fabric-core.git" +UPSTREAM_REL = Path("adapters/pcs/tests/fixtures/hash_vectors") + + +def hash_vectors_dir(local: Path | None = None) -> Path: + return local or (repo_root() / "python" / "tests" / "hash_vectors") + + +def _normalize_text(text: str) -> str: + return text.replace("\r\n", "\n").replace("\r", "\n") + + +def _clone_upstream( + *, + pf_core_tag: str, + pf_core_repo: str, + work_dir: Path, +) -> Path: + dest = work_dir / "provability-fabric-core" + if dest.is_dir(): + shutil.rmtree(dest) + proc = subprocess.run( + [ + "git", + "clone", + "--depth", + "1", + "--branch", + pf_core_tag, + pf_core_repo, + str(dest), + ], + capture_output=True, + text=True, + check=False, + ) + if proc.returncode != 0: + detail = (proc.stderr or proc.stdout or "").strip() + raise RuntimeError(f"git clone {pf_core_tag} failed: {detail}") + upstream = dest / UPSTREAM_REL + if not upstream.is_dir(): + raise RuntimeError(f"missing upstream hash vectors at {upstream}") + return upstream + + +def compare_hash_vector_trees(local: Path, upstream: Path) -> list[str]: + """Return drift messages; empty when every upstream vector matches locally.""" + errors: list[str] = [] + upstream_files = sorted( + path + for path in upstream.rglob("*") + if path.is_file() and path.name != ".gitkeep" + ) + for upstream_file in upstream_files: + rel = upstream_file.relative_to(upstream) + local_file = local / rel + if not local_file.is_file(): + errors.append(f"missing local vector: {rel.as_posix()}") + continue + local_text = _normalize_text(local_file.read_text(encoding="utf-8")) + upstream_text = _normalize_text(upstream_file.read_text(encoding="utf-8")) + if local_text != upstream_text: + errors.append( + f"hash vector drift: {rel.as_posix()} " + f"(expected match with upstream provability-fabric-core fixtures)" + ) + return errors + + +def verify_pf_core_hash_vectors( + local: Path | None = None, + *, + pf_core_tag: str | None = None, + pf_core_repo: str | None = None, + upstream_dir: Path | None = None, + work_dir: Path | None = None, +) -> list[str]: + """ + Verify local PCS hash vectors match provability-fabric-core adapter fixtures. + + When ``upstream_dir`` is omitted, clones ``pf_core_tag`` into a temporary + directory (or ``work_dir`` when provided). + """ + local_root = hash_vectors_dir(local) + 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) + + cleanup: Path | None = None + try: + if upstream_dir is not None: + upstream_root = upstream_dir + else: + base = work_dir or Path(tempfile.mkdtemp(prefix="pf-core-hash-vectors-")) + cleanup = None if work_dir else base + upstream_root = _clone_upstream( + pf_core_tag=tag, + pf_core_repo=repo, + work_dir=base, + ) + return compare_hash_vector_trees(local_root, upstream_root) + finally: + if cleanup is not None and cleanup.is_dir(): + shutil.rmtree(cleanup, ignore_errors=True) diff --git a/python/pcs_core/pf_core_labtrust_adapter.py b/python/pcs_core/pf_core_labtrust_adapter.py new file mode 100644 index 0000000..9711eb1 --- /dev/null +++ b/python/pcs_core/pf_core_labtrust_adapter.py @@ -0,0 +1,110 @@ +"""Minimal LabTrust release → PFCoreTrace adapter (untrusted, schema-validated).""" + +from __future__ import annotations + +from typing import Any, Mapping + +from pcs_core.pf_core_runtime import ( + GENESIS_HASH, + compute_trace_hash, + expand_principal_capabilities, + _finalize_event, + _validate_action, + _validate_principal, +) + +LABTRUST_PRINCIPAL = { + "principal_id": "lab-operator-1", + "principal_kind": "human", + "tenant": "labtrust-qc", + "roles": ["lab_operator"], + "capabilities": [], +} + + +def normalize_labtrust_release( + trace_certificate: Mapping[str, Any], + runtime_receipt: Mapping[str, Any] | None = None, +) -> dict[str, Any]: + """ + Build a single-event PFCoreTrace.v0 from LabTrust PCS release artifacts. + + Maps PCS trace_certificate trace_hash/spec_hash to PF-Core trace/certificate + binding fields per docs/pf-core-trace-mapping.md. + """ + receipt = runtime_receipt or {} + trace_id = str(receipt.get("run_id") or "labtrust-qc-release-v0.1").replace("/", "-") + timestamp = str( + receipt.get("started_at") + or trace_certificate.get("created_at") + or "2026-05-16T11:58:00Z" + ) + source_repo = str(trace_certificate.get("source_repo") or receipt.get("source_repo") or "") + source_commit = str(trace_certificate.get("source_commit") or receipt.get("source_commit") or "") + + principal = _validate_principal(dict(LABTRUST_PRINCIPAL)) + principal["capabilities"] = expand_principal_capabilities(principal) + + action = _validate_action( + { + "action_id": "act-lab-release-001", + "tool_name": "lab.release", + "capability": { + "capability_id": "cap:lab-release", + "effect_kind": "lab.release", + "resource_pattern": "lab:*", + }, + "effects": [{"effect_kind": "lab.release"}], + "reads": [ + { + "resource_id": "res-lab-release-001", + "uri": "lab:qc-release/run-001", + "tenant": principal["tenant"], + } + ], + "writes": [], + "input_hash": str(receipt.get("events_hash") or trace_certificate.get("trace_hash") or GENESIS_HASH), + "output_hash": str( + receipt.get("output_hashes", {}).get("trace.json") + if isinstance(receipt.get("output_hashes"), dict) + else trace_certificate.get("trace_hash") or GENESIS_HASH + ), + } + ) + + event = _finalize_event( + trace_id=trace_id, + event_id="evt-lab-release-001", + sequence=0, + timestamp=timestamp, + principal=principal, + action=action, + decision="allow", + decision_reason="CertificateChecked", + contract_refs=[str(trace_certificate.get("property_id") or "qc_release.temporal.safety")], + evidence_refs=[str(trace_certificate.get("certificate_id") or "")], + previous_event_hash=GENESIS_HASH, + source_repo=source_repo, + source_commit=source_commit, + ) + + policy_hash = str(receipt.get("policy_hash") or GENESIS_HASH) + contract_hash = str(trace_certificate.get("spec_hash") or GENESIS_HASH) + + trace: dict[str, Any] = { + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": trace_id, + "workflow_id": "labtrust.qc_release.v0", + "events": [event], + "trace_hash": GENESIS_HASH, + "policy_hash": policy_hash, + "contract_hash": contract_hash, + "claim_class": "RuntimeChecked", + "source_repo": source_repo, + "source_commit": source_commit, + "signature_or_digest": GENESIS_HASH, + } + trace["trace_hash"] = compute_trace_hash(trace) + trace["signature_or_digest"] = trace["trace_hash"] + return trace diff --git a/python/pcs_core/pf_core_lean_codegen.py b/python/pcs_core/pf_core_lean_codegen.py new file mode 100644 index 0000000..46c50ef --- /dev/null +++ b/python/pcs_core/pf_core_lean_codegen.py @@ -0,0 +1,519 @@ +"""Generate concrete Lean terms and proof obligations from PFCoreTrace.v0.""" + +from __future__ import annotations + +import hashlib +import json +import re +from pathlib import Path +from typing import Any, Mapping + +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 + +EFFECT_KIND_TO_LEAN: dict[str, str] = { + "file.read": "Effect.read", + "file.write": "Effect.write", + "network.egress": "Effect.network", + "email.send": "Effect.externalMessage", + "handoff.delegate": "Effect.stateChange", + "mcp.invoke": "Effect.codeExecution", + "lab.release": 'Effect.custom "lab.release"', +} + +_LEAN_IDENT_RE = re.compile(r"[^a-zA-Z0-9_]") + + +def lean_string_literal(value: str) -> str: + return json.dumps(value, ensure_ascii=False) + + +def lean_ident(prefix: str, raw: str) -> str: + slug = _LEAN_IDENT_RE.sub("_", raw).strip("_") + if not slug or slug[0].isdigit(): + slug = f"{prefix}_{slug or 'x'}" + return slug + + +def effect_kind_to_lean(effect_kind: str) -> str: + mapped = EFFECT_KIND_TO_LEAN.get(effect_kind) + if mapped is None: + return f'Effect.custom {lean_string_literal(effect_kind)}' + return mapped + + +def principal_to_lean(principal: Mapping[str, Any], *, name: str) -> str: + roles = [lean_string_literal(str(role)) for role in principal.get("roles", [])] + capabilities = [ + lean_string_literal(str(cap)) for cap in principal.get("capabilities", []) + ] + roles_expr = "[]" if not roles else f"[{', '.join(roles)}]" + caps_expr = "[]" if not capabilities else f"[{', '.join(capabilities)}]" + return ( + f"def {name} : Principal :=\n" + " {\n" + f" id := {lean_string_literal(str(principal.get('principal_id') or ''))},\n" + f" tenant := {lean_string_literal(str(principal.get('tenant') or ''))},\n" + f" roles := {roles_expr},\n" + f" capabilities := {caps_expr}\n" + " }" + ) + + +def resource_to_lean(resource: Mapping[str, Any]) -> str: + return ( + "{\n" + f" uri := {lean_string_literal(str(resource.get('uri') or ''))},\n" + f" tenant := {lean_string_literal(str(resource.get('tenant') or ''))},\n" + " labels := []\n" + " }" + ) + + +def action_to_lean(action: Mapping[str, Any], *, name: str) -> str: + capability = action.get("capability") + cap_id = "" + if isinstance(capability, dict): + cap_id = str(capability.get("capability_id") or "") + effects = action.get("effects") + effect_exprs: list[str] = [] + if isinstance(effects, list): + for effect in effects: + if isinstance(effect, dict): + effect_exprs.append(effect_kind_to_lean(str(effect.get("effect_kind") or ""))) + if not effect_exprs: + effect_exprs = ["Effect.read"] + reads = action.get("reads") + writes = action.get("writes") + read_exprs = [ + resource_to_lean(item) + for item in reads + if isinstance(item, dict) + ] if isinstance(reads, list) else [] + write_exprs = [ + resource_to_lean(item) + for item in writes + if isinstance(item, dict) + ] if isinstance(writes, list) else [] + reads_expr = "[]" if not read_exprs else f"[{', '.join(read_exprs)}]" + writes_expr = "[]" if not write_exprs else f"[{', '.join(write_exprs)}]" + return ( + f"def {name} : Action :=\n" + " {\n" + f" id := {lean_string_literal(str(action.get('action_id') or ''))},\n" + f" toolName := {lean_string_literal(str(action.get('tool_name') or ''))},\n" + f" capability := {lean_string_literal(cap_id)},\n" + f" effects := [{', '.join(effect_exprs)}],\n" + f" reads := {reads_expr},\n" + f" writes := {writes_expr}\n" + " }" + ) + + +def decision_to_lean(decision: str) -> str: + if decision == "deny": + return "Decision.deny" + return "Decision.allow" + + +def event_to_lean(event: Mapping[str, Any], *, name: str) -> tuple[str, str, str]: + principal = event.get("principal") + action = event.get("action") + if not isinstance(principal, dict) or not isinstance(action, dict): + raise ValueError("event requires principal and action objects") + principal_name = f"{name}Principal" + action_name = f"{name}Action" + principal_def = principal_to_lean(principal, name=principal_name) + action_def = action_to_lean(action, name=action_name) + event_def = ( + f"def {name} : Event :=\n" + " {\n" + f" id := {lean_string_literal(str(event.get('event_id') or ''))},\n" + f" principal := {principal_name},\n" + f" action := {action_name},\n" + f" decision := {decision_to_lean(str(event.get('decision') or ''))}\n" + " }" + ) + return principal_def, action_def, event_def + + +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 collect_contracts_for_trace( + trace: Mapping[str, Any], + *, + trace_path: Path | None = None, + contracts_dir: Path | None = None, +) -> dict[str, dict[str, Any]]: + """Load PFCoreContract.v0 objects referenced by trace events.""" + if contracts_dir is not None and contracts_dir.is_dir(): + return load_contracts_from_dir(contracts_dir) + + if trace_path is None: + return {} + + case_dir = trace_path.parent + contracts = load_contracts_from_dir(case_dir) + nested = case_dir / "contracts" + if nested.is_dir(): + contracts.update(load_contracts_from_dir(nested)) + return contracts + + +def contract_pre_to_lean(contract: Mapping[str, Any], *, name: str) -> str: + pre = contract.get("pre") + cap_expr = "none" + effect_expr = "none" + tenant_expr = "false" + if isinstance(pre, dict): + cap = pre.get("require_capability") + if isinstance(cap, str) and cap: + cap_expr = f"some {lean_string_literal(cap)}" + effect = pre.get("require_effect") + if isinstance(effect, str) and effect: + effect_expr = f"some {effect_kind_to_lean(effect)}" + if pre.get("require_tenant_match") is True: + tenant_expr = "true" + return ( + f"def {name} : ContractPreSpec :=\n" + " {\n" + f" requireCapability := {cap_expr},\n" + f" requireEffect := {effect_expr},\n" + f" requireTenantMatch := {tenant_expr}\n" + " }" + ) + + +def contract_post_to_lean(contract: Mapping[str, Any], *, name: str) -> str: + post = contract.get("post") + decision_expr = "none" + safe_expr = "false" + if isinstance(post, dict): + decision = post.get("require_decision") + if isinstance(decision, str) and decision: + decision_expr = f"some {decision_to_lean(decision)}" + if post.get("require_event_safe") is True: + safe_expr = "true" + return ( + f"def {name} : ContractPostSpec :=\n" + " {\n" + f" requireDecision := {decision_expr},\n" + f" requireEventSafe := {safe_expr}\n" + " }" + ) + + +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: + safe_expr = "true" + return ( + f"def {name} : ContractInvariantSpec :=\n" + " {\n" + f" requireTraceSafe := {safe_expr}\n" + " }" + ) + + +def contract_specs_to_lean(contract: Mapping[str, Any], *, base_name: str) -> str: + return "\n\n".join( + [ + contract_pre_to_lean(contract, name=f"{base_name}Pre"), + contract_post_to_lean(contract, name=f"{base_name}Post"), + contract_invariant_to_lean(contract, name=f"{base_name}Inv"), + ] + ) + + +def generate_contract_proof_obligations( + trace: Mapping[str, Any], + contracts: Mapping[str, Mapping[str, Any]], +) -> tuple[list[str], list[str]]: + """Return (contract Lean defs, contract proof theorems) for referenced contracts.""" + defs: list[str] = [] + theorems: list[str] = [] + seen_contracts: set[str] = set() + + trace_id = str(trace.get("trace_id") or "trace") + trace_var = lean_ident("trace", trace_id) + + for index, event in enumerate(trace_events(trace)): + refs = event.get("contract_refs") + if not isinstance(refs, list): + continue + event_name = lean_ident("ev", str(event.get("event_id") or index)) + for ref in refs: + contract_id = str(ref) + contract = contracts.get(contract_id) + if contract is None: + continue + base_name = lean_ident("contract", contract_id) + if contract_id not in seen_contracts: + seen_contracts.add(contract_id) + defs.append(contract_specs_to_lean(contract, base_name=base_name)) + theorems.append( + f"theorem concrete_trace_satisfies_{base_name} : " + f"traceSatisfiesContractSpecsD {base_name}Pre {base_name}Post " + f"{base_name}Inv {trace_var} = true := by\n" + " decide" + ) + theorems.append( + f"theorem concrete_satisfies_{base_name}_{event_name} : " + 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" + ) + + return defs, theorems + + +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 collect_handoffs_near_trace( + trace: Mapping[str, Any], + *, + trace_path: Path | None = None, +) -> list[dict[str, Any]]: + """Collect PFCoreHandoff.v0 objects from sibling fixture files or ToolUseTrace handoffs.""" + handoffs: list[dict[str, Any]] = [] + seen: set[str] = set() + + def add_handoff(item: Mapping[str, Any]) -> None: + handoff_id = str(item.get("handoff_id") or "") + key = handoff_id or canonical_hash(dict(item)) + if key in seen: + return + seen.add(key) + handoffs.append(dict(item)) + + if trace_path is not None: + case_dir = trace_path.parent + for path in sorted(case_dir.glob("*.json")): + if path.name == trace_path.name: + continue + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + continue + if isinstance(data, dict) and data.get("artifact_type") == "PFCoreHandoff.v0": + add_handoff(data) + tool_use_path = case_dir / "tool_use_trace.json" + if tool_use_path.is_file(): + try: + tool_use = json.loads(tool_use_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + tool_use = None + if isinstance(tool_use, dict): + raw = tool_use.get("handoffs") + if isinstance(raw, list): + for item in raw: + if isinstance(item, dict) and item.get("artifact_type") == "PFCoreHandoff.v0": + add_handoff(item) + return handoffs + + +def handoff_to_lean(handoff: Mapping[str, Any], *, name: str) -> str: + from_principal = handoff.get("from_principal") + to_principal = handoff.get("to_principal") + if not isinstance(from_principal, dict) or not isinstance(to_principal, dict): + raise ValueError("handoff requires from_principal and to_principal objects") + from_name = f"{name}From" + to_name = f"{name}To" + from_def = principal_to_lean(from_principal, name=from_name) + to_def = principal_to_lean(to_principal, name=to_name) + delegated = handoff.get("delegated_capabilities") + cap_ids: list[str] = [] + if isinstance(delegated, list): + for item in delegated: + if isinstance(item, dict): + cap_id = str(item.get("capability_id") or "") + if cap_id: + cap_ids.append(cap_id) + caps_expr = "[]" if not cap_ids else f"[{', '.join(lean_string_literal(c) for c in cap_ids)}]" + handoff_def = ( + f"def {name} : Handoff :=\n" + " {\n" + f" fromPrincipal := {from_name},\n" + f" toPrincipal := {to_name},\n" + f" delegatedCapabilities := {caps_expr}\n" + " }" + ) + return "\n\n".join([from_def, to_def, handoff_def]) + + +def trace_to_lean(trace: Mapping[str, Any]) -> str: + """Generate Lean source defining concrete Principal/Action/Event/Trace values.""" + events = trace_events(trace) + defs: list[str] = [] + event_names: list[str] = [] + for index, event in enumerate(events): + event_id = str(event.get("event_id") or f"event_{index}") + base_name = lean_ident("ev", event_id) + principal_def, action_def, event_def = event_to_lean(event, name=base_name) + defs.extend([principal_def, action_def, event_def]) + event_names.append(base_name) + + trace_expr = "Trace.empty" + for event_name in event_names: + trace_expr = f"Trace.cons ({trace_expr}) {event_name}" + + trace_id = str(trace.get("trace_id") or "trace") + trace_name = lean_ident("trace", trace_id) + body = "\n\n".join(defs) + if body: + body += "\n\n" + body += f"def {trace_name} : Trace := {trace_expr}" + return body + + +def generated_module_name(trace: Mapping[str, Any]) -> str: + trace_hash = str(trace.get("trace_hash") or canonical_hash(dict(trace))) + digest = trace_hash.removeprefix("sha256:") + return f"Trace_{digest[:16]}" + + +def generate_proof_obligation_file( + trace: Mapping[str, Any], + out_dir: Path, + *, + trace_path: Path | None = None, +) -> Path: + """Write a `.lean` file proving concrete trace/event (and optional handoff) safety.""" + module = generated_module_name(trace) + out_dir.mkdir(parents=True, exist_ok=True) + out_path = out_dir / f"{module}.lean" + + events = trace_events(trace) + trace_body = trace_to_lean(trace) + trace_id = str(trace.get("trace_id") or "trace") + trace_var = lean_ident("trace", trace_id) + + event_theorems = "\n".join( + f"theorem concrete_event_safe_{lean_ident('ev', str(event.get('event_id') or index))} : " + f"eventSafeD {lean_ident('ev', str(event.get('event_id') or index))} = true := by\n" + " decide" + for index, event in enumerate(events) + ) + event_theorem_block = f"{event_theorems}\n\n" if event_theorems else "" + + handoff_defs: list[str] = [] + handoff_theorems: list[str] = [] + for index, handoff in enumerate(collect_handoffs_near_trace(trace, trace_path=trace_path)): + handoff_id = str(handoff.get("handoff_id") or f"handoff_{index}") + handoff_name = lean_ident("handoff", handoff_id) + handoff_defs.append(handoff_to_lean(handoff, name=handoff_name)) + handoff_theorems.append( + f"theorem concrete_handoff_safe_{handoff_name} : " + f"handoffSafeD {handoff_name} = true := by\n" + " decide" + ) + handoff_block = "" + if handoff_defs: + handoff_block = "\n\n".join(handoff_defs) + "\n\n" + handoff_block += "\n\n".join(handoff_theorems) + "\n\n" + + contracts = collect_contracts_for_trace(trace, trace_path=trace_path) + contract_defs, contract_theorems = generate_contract_proof_obligations(trace, contracts) + contract_def_block = "" + contract_theorem_block = "" + contract_note = "" + if contract_defs: + contract_def_block = "\n\n".join(contract_defs) + "\n\n" + contract_theorem_block = "\n\n".join(contract_theorems) + "\n\n" + contract_note = ( + "-- Contract refs discharged via ContractPreSpec/PostSpec deciders " + "(subset of PFCoreContract.v0; see docs/pf-core/contract-semantics.md).\n" + ) + elif trace_has_contract_refs(trace): + contract_note = ( + "-- Contract refs present but contract JSON not found alongside trace; " + "run `pcs pf-core validate-contracts` before lean-check.\n" + ) + + source = f"""import PFCore.TraceCheck + +/-! +# Generated concrete trace proof for `{trace_id}` + +Auto-generated by pcs-core pf-core lean-check. Do not edit by hand. +{contract_note.strip()} +-/ + +namespace PFCore.Generated.{module} + +{trace_body} + +{contract_def_block}{handoff_block}theorem concrete_trace_safe : traceSafeD {trace_var} = true := by + decide + +{event_theorem_block}{contract_theorem_block}end PFCore.Generated.{module} +""" + out_path.write_text(source, encoding="utf-8") + return out_path + + +def validate_contracts_before_codegen( + trace: Mapping[str, Any], + *, + trace_path: Path | None = None, + contracts_dir: Path | None = None, +) -> list[str]: + """Return contract validation errors (empty when satisfied or no contract JSON).""" + if not trace_has_contract_refs(trace): + return [] + contracts = collect_contracts_for_trace( + trace, trace_path=trace_path, contracts_dir=contracts_dir + ) + if not contracts: + return [] + issues = validate_trace_contracts(trace, contracts) + return [ + f"{issue.code}: {issue.message}" + (f" (at {issue.path})" if issue.path else "") + for issue in issues + ] + + +def compute_lean_environment_hash() -> str: + """Hash pinned Lean toolchain + lake manifest for reproducibility metadata.""" + lean_root = repo_root() / "lean" + parts: list[str] = [] + toolchain = repo_root() / "lean-toolchain" + if toolchain.is_file(): + parts.append(toolchain.read_text(encoding="utf-8")) + for rel in ("lakefile.lean", "lake-manifest.json"): + path = lean_root / rel + if path.is_file(): + parts.append(path.read_text(encoding="utf-8")) + digest = hashlib.sha256("\n---\n".join(parts).encode("utf-8")).hexdigest() + return f"sha256:{digest}" + + +def proof_term_ref_from_path(path: Path) -> str: + root = repo_root() + try: + return str(path.relative_to(root)).replace("\\", "/") + except ValueError: + return str(path).replace("\\", "/") diff --git a/python/pcs_core/pf_core_replay.py b/python/pcs_core/pf_core_replay.py new file mode 100644 index 0000000..cdf84cb --- /dev/null +++ b/python/pcs_core/pf_core_replay.py @@ -0,0 +1,331 @@ +"""Deterministic PF-Core trace replay validation.""" + +from __future__ import annotations + +import json +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Mapping + +from pcs_core.hash import canonical_hash +from pcs_core.pf_core_runtime import ( + GENESIS_HASH, + compute_event_hash, + compute_trace_hash, + compile_runtime_observation_to_event, + compile_tool_use_trace_to_pfcore_trace, + normalize_hash, +) +from pcs_core.validate import detect_artifact_type, validate_schema + +REPLAY_DISCLAIMER = ( + "PF-Core replay-trace recomputes event and trace hashes deterministically and " + "compares them to the stored PFCoreTrace.v0. When --source is provided, the " + "compiler is re-run from ToolUseTrace.v0 or PFCoreRuntimeObservation.v0. " + "ReplayValidated is emitted only when hashes and compiled content match." +) + +_HASH_COMPARE_KEYS = frozenset({"trace_hash", "event_hash", "signature_or_digest"}) + + +@dataclass(frozen=True) +class ReplayDiff: + path: str + expected: Any + actual: Any + + +@dataclass +class ReplayResult: + match: bool + original_trace_hash: str + recomputed_trace_hash: str + diffs: list[ReplayDiff] = field(default_factory=list) + error: str | None = None + recomputed_trace: dict[str, Any] | None = None + + +def recompute_pfcore_trace_hashes(trace: Mapping[str, Any]) -> dict[str, Any]: + """Return a copy with event_hash and trace_hash recomputed deterministically.""" + result: dict[str, Any] = json.loads(json.dumps(trace)) + events = result.get("events") + if not isinstance(events, list): + return result + + previous = normalize_hash(GENESIS_HASH) + for event in events: + if not isinstance(event, dict): + continue + 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_hash = compute_trace_hash(result) + result["trace_hash"] = trace_hash + result["signature_or_digest"] = trace_hash + return result + + +def _compare_hash_chain( + original: Mapping[str, Any], + recomputed: Mapping[str, Any], +) -> list[ReplayDiff]: + diffs: list[ReplayDiff] = [] + orig_hash = str(original.get("trace_hash") or "") + new_hash = str(recomputed.get("trace_hash") or "") + if orig_hash != new_hash: + diffs.append(ReplayDiff("trace_hash", orig_hash, new_hash)) + + orig_events = original.get("events") + new_events = recomputed.get("events") + if not isinstance(orig_events, list) or not isinstance(new_events, list): + return diffs + if len(orig_events) != len(new_events): + diffs.append(ReplayDiff("events.length", len(orig_events), len(new_events))) + return diffs + + for index, (orig_event, new_event) in enumerate(zip(orig_events, new_events, strict=False)): + if not isinstance(orig_event, dict) or not isinstance(new_event, dict): + continue + for key in ("event_hash", "previous_event_hash", "signature_or_digest"): + if orig_event.get(key) != new_event.get(key): + diffs.append( + ReplayDiff( + f"events[{index}].{key}", + orig_event.get(key), + new_event.get(key), + ) + ) + return diffs + + +def _compare_traces( + expected: Mapping[str, Any], + actual: Mapping[str, Any], +) -> list[ReplayDiff]: + diffs = _compare_hash_chain(expected, actual) + for key in ("trace_id", "workflow_id", "policy_hash", "contract_hash", "claim_class"): + if expected.get(key) != actual.get(key): + diffs.append(ReplayDiff(key, expected.get(key), actual.get(key))) + + expected_events = expected.get("events") + actual_events = actual.get("events") + if not isinstance(expected_events, list) or not isinstance(actual_events, list): + return diffs + for index, (exp_event, act_event) in enumerate( + zip(expected_events, actual_events, strict=False) + ): + if not isinstance(exp_event, dict) or not isinstance(act_event, dict): + continue + for key in sorted(set(exp_event) | set(act_event)): + if key in _HASH_COMPARE_KEYS: + continue + if exp_event.get(key) != act_event.get(key): + diffs.append( + ReplayDiff( + f"events[{index}].{key}", + exp_event.get(key), + act_event.get(key), + ) + ) + return diffs + + +def compile_observation_to_pfcore_trace(observation: dict) -> dict: + """Compile a single PFCoreRuntimeObservation.v0 into a one-event PFCoreTrace.v0.""" + event = compile_runtime_observation_to_event(observation) + trace: dict[str, Any] = { + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": str(observation["trace_id"]), + "workflow_id": str(observation.get("runtime_ref") or "observation.single"), + "events": [event], + "trace_hash": GENESIS_HASH, + "policy_hash": GENESIS_HASH, + "contract_hash": GENESIS_HASH, + "claim_class": "RuntimeChecked", + "source_repo": str(observation["source_repo"]), + "source_commit": str(observation["source_commit"]), + "signature_or_digest": GENESIS_HASH, + } + trace["trace_hash"] = compute_trace_hash(trace) + trace["signature_or_digest"] = trace["trace_hash"] + return trace + + +def compile_source_to_pfcore_trace(source: Mapping[str, Any]) -> dict: + artifact_type = detect_artifact_type(dict(source)) + if artifact_type == "ToolUseTrace.v0": + return compile_tool_use_trace_to_pfcore_trace(dict(source)) + if artifact_type == "PFCoreRuntimeObservation.v0": + return compile_observation_to_pfcore_trace(dict(source)) + raise ValueError( + f"unsupported replay source artifact type {artifact_type!r}; " + "expected ToolUseTrace.v0 or PFCoreRuntimeObservation.v0" + ) + + +def replay_trace(trace_path: Path, source_path: Path | None = None) -> ReplayResult: + """Replay a PFCoreTrace.v0, optionally recompiling from source.""" + original = json.loads(trace_path.read_text(encoding="utf-8")) + schema_errors = validate_schema(original, "PFCoreTrace.v0") + if schema_errors: + return ReplayResult( + match=False, + original_trace_hash=str(original.get("trace_hash") or ""), + recomputed_trace_hash="", + error=f"schema invalid: {'; '.join(schema_errors)}", + ) + + original_hash = str(original.get("trace_hash") or "") + + try: + if source_path is not None: + source = json.loads(source_path.read_text(encoding="utf-8")) + recomputed = compile_source_to_pfcore_trace(source) + diffs = _compare_traces(original, recomputed) + else: + recomputed = recompute_pfcore_trace_hashes(original) + diffs = _compare_hash_chain(original, recomputed) + except Exception as exc: + return ReplayResult( + match=False, + original_trace_hash=original_hash, + recomputed_trace_hash="", + error=str(exc), + ) + + recomputed_hash = str(recomputed.get("trace_hash") or "") + match = not diffs and original_hash == recomputed_hash + return ReplayResult( + match=match, + original_trace_hash=original_hash, + recomputed_trace_hash=recomputed_hash, + diffs=diffs, + recomputed_trace=recomputed, + ) + + +def build_replay_certificate(trace: Mapping[str, Any], result: ReplayResult) -> dict[str, Any]: + events = trace.get("events") + event_count = len(events) if isinstance(events, list) else 0 + cert: dict[str, Any] = { + "schema_version": "v0", + "artifact_type": "PFCoreCertificate.v0", + "certificate_id": f"pfcore-replay-{trace.get('trace_id', 'unknown')}", + "trace_hash": result.original_trace_hash, + "contract_hash": str(trace.get("contract_hash") or GENESIS_HASH), + "policy_hash": str(trace.get("policy_hash") or GENESIS_HASH), + "claim_class": "ReplayValidated" if result.match else "OutOfScope", + "checker": "pcs-core", + "checker_version": "0.1.0", + "assumption_refs": [ + "docs/pf-core/assumptions.md", + "docs/pf-core/trusted-boundary.md", + ], + "obligations": [ + { + "kind": "TraceReplay", + "theorem": "trace_hash_replay", + "passed": result.match, + } + ], + "replay_match": result.match, + "original_trace_hash": result.original_trace_hash, + "recomputed_trace_hash": result.recomputed_trace_hash, + "disclaimer": REPLAY_DISCLAIMER, + "event_count": event_count, + "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": GENESIS_HASH, + } + cert["signature_or_digest"] = canonical_hash(cert) + return cert + + +def build_replay_check_result( + trace_path: Path, + result: ReplayResult, + *, + certificate: dict[str, Any] | None = None, +) -> dict[str, Any]: + claim_class = "ReplayValidated" if result.match else "OutOfScope" + status = "ReplayValidated" if result.match else "Rejected" + issues: list[dict[str, Any]] = [] + if result.error: + issues.append({"code": "ReplayError", "message": result.error}) + for diff in result.diffs: + issues.append( + { + "code": "ReplayMismatch", + "message": f"{diff.path}: expected {diff.expected!r}, got {diff.actual!r}", + "path": diff.path, + } + ) + + payload: dict[str, Any] = { + "schema_version": "v0", + "artifact_type": "LeanCheckResult.v0", + "status": status, + "claim_class": claim_class, + "trace_path": str(trace_path), + "issues": issues, + "assumption_refs": [ + "docs/pf-core/assumptions.md", + "docs/pf-core/trusted-boundary.md", + ], + "theorems_checked": ["trace_hash_replay"], + "lean_build_status": {"ok": False, "target": "PFCore", "detail": "not-applicable"}, + "lean_proof_checked": False, + "replay_match": result.match, + "original_trace_hash": result.original_trace_hash, + "recomputed_trace_hash": result.recomputed_trace_hash, + "disclaimer": REPLAY_DISCLAIMER, + "certificate": certificate, + "signature_or_digest": GENESIS_HASH, + } + payload["signature_or_digest"] = canonical_hash(payload) + return payload + + +def run_replay_trace( + trace_path: Path, + *, + source_path: Path | None = None, + out_path: Path | None = None, + result_out_path: Path | None = None, +) -> tuple[int, dict[str, Any]]: + """Run replay validation and optionally write certificate / result artifacts.""" + original = json.loads(trace_path.read_text(encoding="utf-8")) + result = replay_trace(trace_path, source_path) + + certificate: dict[str, Any] | None = None + if result.match: + certificate = build_replay_certificate(original, result) + cert_errors = validate_schema(certificate, "PFCoreCertificate.v0") + if cert_errors: + result = ReplayResult( + match=False, + original_trace_hash=result.original_trace_hash, + recomputed_trace_hash=result.recomputed_trace_hash, + diffs=result.diffs, + error=f"certificate schema invalid: {'; '.join(cert_errors)}", + ) + certificate = None + + check_result = build_replay_check_result(trace_path, result, certificate=certificate) + + if result_out_path: + result_out_path.write_text(json.dumps(check_result, indent=2), encoding="utf-8") + if out_path and certificate is not None: + out_path.write_text(json.dumps(certificate, indent=2), encoding="utf-8") + + return (0 if result.match else 1), check_result + + +def print_replay_disclaimer(*, stream=None) -> None: + stream = stream or sys.stderr + print(REPLAY_DISCLAIMER, file=stream) diff --git a/python/pcs_core/pf_core_runtime.py b/python/pcs_core/pf_core_runtime.py new file mode 100644 index 0000000..265f089 --- /dev/null +++ b/python/pcs_core/pf_core_runtime.py @@ -0,0 +1,798 @@ +"""Deterministic PF-Core runtime observation compiler.""" + +from __future__ import annotations + +import fnmatch +from dataclasses import dataclass +from typing import Any, Mapping + +from pcs_core.hash import SIGNATURE_FIELD, canonical_hash +from pcs_core.validate import ValidationError, validate_schema + +GENESIS_HASH = "sha256:" + "0" * 64 + +EFFECT_KINDS = frozenset( + { + "file.read", + "file.write", + "network.egress", + "email.send", + "handoff.delegate", + "mcp.invoke", + "lab.release", + } +) + +CAPABILITY_CATALOG: dict[str, dict[str, str]] = { + "cap:file-read": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*", + }, + "cap:file-write": { + "capability_id": "cap:file-write", + "effect_kind": "file.write", + "resource_pattern": "/data/*", + }, + "cap:network": { + "capability_id": "cap:network", + "effect_kind": "network.egress", + "resource_pattern": "*", + }, + "cap:email-send": { + "capability_id": "cap:email-send", + "effect_kind": "email.send", + "resource_pattern": "mailto:*", + }, + "cap:handoff": { + "capability_id": "cap:handoff", + "effect_kind": "handoff.delegate", + "resource_pattern": "agent:*", + }, + "cap:mcp-invoke": { + "capability_id": "cap:mcp-invoke", + "effect_kind": "mcp.invoke", + "resource_pattern": "mcp:*", + }, + "cap:lab-release": { + "capability_id": "cap:lab-release", + "effect_kind": "lab.release", + "resource_pattern": "lab:*", + }, +} + +ROLE_CAPABILITY_MAP: dict[str, list[str]] = { + "file_reader": ["cap:file-read"], + "file_admin": ["cap:file-read", "cap:file-write"], + "network_user": ["cap:network"], + "email_user": ["cap:email-send"], + "handoff_delegate": ["cap:handoff"], + "mcp_user": ["cap:mcp-invoke"], + "lab_operator": ["cap:lab-release"], + "agent": ["cap:file-read", "cap:email-send", "cap:handoff", "cap:mcp-invoke"], +} + +TOOL_NAME_MAP: dict[tuple[str, str], tuple[str, str, str]] = { + ("filesystem.read", "filesystem"): ("cap:file-read", "file.read", "/data/*"), + ("filesystem.write", "filesystem"): ("cap:file-write", "file.write", "/data/*"), + ("network.request", "network"): ("cap:network", "network.egress", "*"), + ("email.send", "email"): ("cap:email-send", "email.send", "mailto:*"), + ("handoff.delegate", "handoff"): ("cap:handoff", "handoff.delegate", "agent:*"), + ("mcp.invoke", "mcp"): ("cap:mcp-invoke", "mcp.invoke", "mcp:*"), + ("lab.release", "lab"): ("cap:lab-release", "lab.release", "lab:*"), +} + +AUTHORIZATION_TO_DECISION = { + "authorized": "allow", + "rejected": "deny", + "unknown": "deny", + "policy_missing": "deny", +} + +RUNTIME_CHECKED_CLAIM_CLASSES = frozenset({"SchemaValidated", "RuntimeChecked", "OutOfScope"}) +LEAN_CLAIM_CLASSES = frozenset({"LeanKernelChecked"}) + + +@dataclass(frozen=True) +class PFCoreRuntimeError(Exception): + code: str + message: str + path: str | None = None + + def __str__(self) -> str: + if self.path: + return f"{self.code}: {self.message} (at {self.path})" + return f"{self.code}: {self.message}" + + +class UnknownCapability(PFCoreRuntimeError): + def __init__(self, capability: str, path: str | None = None): + super().__init__("UnknownCapability", f"unknown capability: {capability}", path) + + +class UnknownEffect(PFCoreRuntimeError): + def __init__(self, effect: str, path: str | None = None): + super().__init__("UnknownEffect", f"unknown effect: {effect}", path) + + +class MissingPrincipal(PFCoreRuntimeError): + def __init__(self, message: str = "principal required", path: str | None = None): + super().__init__("MissingPrincipal", message, path) + + +class AmbiguousMapping(PFCoreRuntimeError): + def __init__(self, message: str, path: str | None = None): + super().__init__("AmbiguousMapping", message, path) + + +class HandoffAuthorityExpansion(PFCoreRuntimeError): + def __init__(self, capability: str, path: str | None = None): + super().__init__( + "HandoffAuthorityExpansion", + f"delegated capability exceeds source authority: {capability}", + path, + ) + + +class ClaimClassOverclaim(PFCoreRuntimeError): + def __init__(self, claim_class: str, path: str | None = None): + super().__init__( + "ClaimClassOverclaim", + f"claim_class {claim_class!r} exceeds available assurance", + path, + ) + + +class DroppedDeniedEvent(PFCoreRuntimeError): + def __init__(self, event_id: str, path: str | None = None): + super().__init__( + "DroppedDeniedEvent", + f"denied event {event_id!r} missing from compiled trace", + path, + ) + + +class ResourceScopeViolation(PFCoreRuntimeError): + def __init__(self, uri: str, pattern: str, path: str | None = None): + super().__init__( + "ResourceScopeViolation", + f"resource {uri!r} outside declared pattern {pattern!r}", + path, + ) + + +def validate_denied_events_preserved( + tool_use_trace: Mapping[str, Any], + pfcore_trace: Mapping[str, Any], +) -> None: + tool_calls = tool_use_trace.get("tool_calls") + if not isinstance(tool_calls, list): + return + events = pfcore_trace.get("events") + if not isinstance(events, list): + raise DroppedDeniedEvent("", "events") + compiled_ids = {str(event.get("event_id")) for event in events if isinstance(event, dict)} + for tool_call in tool_calls: + if not isinstance(tool_call, dict): + continue + auth = str(tool_call.get("authorization_status") or "") + if AUTHORIZATION_TO_DECISION.get(auth, "deny") != "deny": + continue + event_id = str(tool_call.get("event_id") or "") + if event_id and event_id not in compiled_ids: + raise DroppedDeniedEvent(event_id, "events") + + +def _require_schema_valid(data: Mapping[str, Any], artifact_type: str) -> None: + errors = validate_schema(dict(data), artifact_type) + if errors: + raise ValidationError(f"Schema validation failed for {artifact_type}", errors=errors) + + +def normalize_hash(value: str) -> str: + if value.startswith("sha256:") and len(value) == 71: + return value + if len(value) == 64 and all(c in "0123456789abcdef" for c in value): + return f"sha256:{value}" + raise PFCoreRuntimeError("InvalidHash", f"invalid hash value: {value!r}") + + +def compute_event_hash(event: Mapping[str, Any]) -> str: + payload = {key: value for key, value in event.items() if key not in ("event_hash", SIGNATURE_FIELD)} + return canonical_hash(payload) + + +def compute_trace_hash(trace: Mapping[str, Any]) -> str: + payload = { + key: value for key, value in trace.items() if key not in ("trace_hash", SIGNATURE_FIELD) + } + return canonical_hash(payload) + + +def expand_principal_capabilities(principal: Mapping[str, Any]) -> list[str]: + """Expand roles and direct capabilities into an explicit capability id list.""" + ids: list[str] = [] + for role in principal.get("roles", []): + role = str(role) + if role in ROLE_CAPABILITY_MAP: + for cap_id in ROLE_CAPABILITY_MAP[role]: + if cap_id not in ids: + ids.append(cap_id) + elif role.startswith("cap:") and role not in ids: + ids.append(role) + for cap_id in principal.get("capabilities", []): + cap_id = str(cap_id) + if cap_id not in ids: + ids.append(cap_id) + return ids + + +def principal_capabilities_explicit(principal: Mapping[str, Any]) -> bool: + """True when principal.capabilities matches role expansion (Lean HasCapability alignment).""" + explicit = {str(cap) for cap in principal.get("capabilities", [])} + return explicit == set(expand_principal_capabilities(principal)) + + +_allowed_capability_ids = expand_principal_capabilities + + +def _validate_capability(capability: Mapping[str, Any], *, path: str) -> dict[str, str]: + cap_id = str(capability.get("capability_id") or "") + if cap_id not in CAPABILITY_CATALOG: + raise UnknownCapability(cap_id or "", path) + catalog = CAPABILITY_CATALOG[cap_id] + effect_kind = str(capability.get("effect_kind") or "") + if effect_kind not in EFFECT_KINDS: + raise UnknownEffect(effect_kind or "", f"{path}.effect_kind") + if catalog["effect_kind"] != effect_kind: + raise AmbiguousMapping( + f"capability {cap_id} maps to {catalog['effect_kind']}, not {effect_kind}", + f"{path}.effect_kind", + ) + return dict(catalog) + + +def _validate_effects(effects: list[Any], *, path: str) -> list[dict[str, str]]: + if not effects: + raise UnknownEffect("", path) + validated: list[dict[str, str]] = [] + for index, effect in enumerate(effects): + if not isinstance(effect, dict): + raise UnknownEffect("", f"{path}[{index}]") + kind = str(effect.get("effect_kind") or "") + if kind not in EFFECT_KINDS: + raise UnknownEffect(kind or "", f"{path}[{index}].effect_kind") + validated.append({"effect_kind": kind}) + return validated + + +def _validate_action(action: Mapping[str, Any], *, path: str = "action") -> dict[str, Any]: + capability = action.get("capability") + if not isinstance(capability, dict): + raise UnknownCapability("", f"{path}.capability") + cap = _validate_capability(capability, path=f"{path}.capability") + effects = action.get("effects") + if not isinstance(effects, list): + raise UnknownEffect("", f"{path}.effects") + validated_effects = _validate_effects(effects, path=f"{path}.effects") + reads = action.get("reads") + writes = action.get("writes") + if not isinstance(reads, list) or not isinstance(writes, list): + raise PFCoreRuntimeError("InvalidAction", "reads and writes must be arrays", path) + normalized = { + "action_id": str(action.get("action_id") or ""), + "tool_name": str(action.get("tool_name") or ""), + "capability": { + "capability_id": cap["capability_id"], + "effect_kind": cap["effect_kind"], + "resource_pattern": cap["resource_pattern"], + }, + "effects": validated_effects, + "reads": [dict(item) for item in reads if isinstance(item, dict)], + "writes": [dict(item) for item in writes if isinstance(item, dict)], + "input_hash": str(action.get("input_hash") or GENESIS_HASH), + "output_hash": str(action.get("output_hash") or GENESIS_HASH), + } + validate_resource_scope(normalized, path=path) + return normalized + + +def _validate_principal(principal: Any, *, path: str = "principal") -> dict[str, Any]: + if not isinstance(principal, dict): + raise MissingPrincipal(path=path) + principal_id = principal.get("principal_id") + if not isinstance(principal_id, str) or not principal_id.strip(): + raise MissingPrincipal("principal_id required", path) + normalized = { + "principal_id": principal_id, + "principal_kind": str(principal.get("principal_kind") or "agent"), + "tenant": str(principal.get("tenant") or ""), + "roles": [str(role) for role in principal.get("roles", [])], + "capabilities": [str(cap) for cap in principal.get("capabilities", [])], + } + normalized["capabilities"] = expand_principal_capabilities(normalized) + return normalized + + +def _assert_claim_class_allowed(claim_class: str, *, proof_ref: str | None = None) -> None: + proof_term_ref = proof_ref + if claim_class in LEAN_CLAIM_CLASSES and not proof_term_ref: + raise ClaimClassOverclaim(claim_class, "claim_class") + if claim_class == "CertificateChecked": + raise ClaimClassOverclaim(claim_class, "claim_class") + + +def _finalize_event( + *, + trace_id: str, + event_id: str, + sequence: int, + timestamp: str, + principal: dict[str, Any], + action: dict[str, Any], + decision: str, + decision_reason: str, + contract_refs: list[str], + evidence_refs: list[str], + previous_event_hash: str, + source_repo: str, + source_commit: str, +) -> dict[str, Any]: + if decision not in {"allow", "deny"}: + raise PFCoreRuntimeError("InvalidDecision", f"unknown decision {decision!r}", "decision") + event: dict[str, Any] = { + "schema_version": "v0", + "artifact_type": "PFCoreEvent.v0", + "event_id": event_id, + "trace_id": trace_id, + "sequence": sequence, + "timestamp": timestamp, + "principal": principal, + "action": action, + "decision": decision, + "decision_reason": decision_reason, + "contract_refs": list(contract_refs), + "evidence_refs": list(evidence_refs), + "previous_event_hash": normalize_hash(previous_event_hash), + "event_hash": GENESIS_HASH, + "source_repo": source_repo, + "source_commit": source_commit, + "signature_or_digest": GENESIS_HASH, + } + event["event_hash"] = compute_event_hash(event) + event["signature_or_digest"] = event["event_hash"] + return event + + +def resource_matches_pattern(uri: str, pattern: str) -> bool: + """Return True when ``uri`` matches capability ``resource_pattern``.""" + if pattern == "*": + return True + return fnmatch.fnmatch(uri, pattern) + + +def validate_resource_scope(action: Mapping[str, Any], *, path: str = "action") -> None: + capability = action.get("capability") + if not isinstance(capability, dict): + return + pattern = str(capability.get("resource_pattern") or "") + if not pattern: + return + for key in ("reads", "writes"): + resources = action.get(key) + if not isinstance(resources, list): + continue + for index, resource in enumerate(resources): + if not isinstance(resource, dict): + continue + uri = str(resource.get("uri") or "") + if uri and not resource_matches_pattern(uri, pattern): + raise ResourceScopeViolation(uri, pattern, f"{path}.{key}[{index}].uri") + + +def _same_tenant(principal: Mapping[str, Any], action: Mapping[str, Any]) -> bool: + tenant = str(principal.get("tenant") or "") + for key in ("reads", "writes"): + resources = action.get(key) + if not isinstance(resources, list): + continue + for resource in resources: + if isinstance(resource, dict) and str(resource.get("tenant") or "") != tenant: + return False + return True + + +def validate_tenant_isolation(trace: Mapping[str, Any]) -> list[str]: + """Return tenant isolation violations for a PFCoreTrace.v0 (empty if scoped). + + Checks that every event's principal tenant matches all read/write resource + tenants. This is a conservative runtime mirror of ``TraceTenantScoped`` / + ``EventTenantScoped`` in Lean; it does not claim full cross-tenant + non-interference. + """ + errors: list[str] = [] + events = trace.get("events") + if not isinstance(events, list): + return ["TraceInvalid: events must be an array"] + + for index, event in enumerate(events): + if not isinstance(event, dict): + continue + base = f"events[{index}]" + principal = event.get("principal") + action = event.get("action") + if not isinstance(principal, dict) or not isinstance(action, dict): + errors.append(f"TenantIsolation: {base} missing principal or action") + continue + tenant = str(principal.get("tenant") or "") + if not tenant: + errors.append(f"TenantIsolation: {base}.principal.tenant is empty") + continue + if not _same_tenant(principal, action): + errors.append( + f"TenantIsolation: cross-tenant resource access at {base} " + f"(principal tenant {tenant!r})" + ) + return errors + + +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") + + principal = _validate_principal(observation.get("principal"), path="principal") + action = _validate_action(observation.get("action", {}), path="action") + + decision = str(observation.get("decision") or "") + if decision == "allow": + cap_id = action["capability"]["capability_id"] + if cap_id not in _allowed_capability_ids(principal): + decision = "deny" + elif not _same_tenant(principal, action): + decision = "deny" + + event = _finalize_event( + trace_id=str(observation["trace_id"]), + event_id=str(observation["event_id"]), + sequence=0, + timestamp=str(observation["observed_at"]), + principal=principal, + action=action, + decision=decision, + decision_reason=str(observation.get("decision_reason") or ""), + 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"]), + source_commit=str(observation["source_commit"]), + ) + return event + + +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: + raise UnknownCapability(f"{tool_name}/{tool_category}", "tool_calls.tool_name") + return TOOL_NAME_MAP[key] + + +def _tool_call_to_action( + tool_call: Mapping[str, Any], + *, + agent_id: str, + tenant: str, + capabilities: list[str], +) -> tuple[dict[str, Any], dict[str, Any], str]: + cap_id, effect_kind, resource_pattern = _resolve_tool_mapping( + str(tool_call["tool_name"]), + str(tool_call["tool_category"]), + ) + _validate_capability( + { + "capability_id": cap_id, + "effect_kind": effect_kind, + "resource_pattern": resource_pattern, + }, + path="tool_calls.capability", + ) + resource_uri = str(tool_call.get("resource_uri") or resource_pattern.replace("*", "default")) + principal = { + "principal_id": agent_id, + "principal_kind": "agent", + "tenant": tenant, + "roles": ["agent"], + "capabilities": list(capabilities), + } + action = { + "action_id": f"act-{tool_call['event_id']}", + "tool_name": str(tool_call["tool_name"]), + "capability": { + "capability_id": cap_id, + "effect_kind": effect_kind, + "resource_pattern": resource_pattern, + }, + "effects": [{"effect_kind": effect_kind}], + "reads": [ + { + "resource_id": f"res-{tool_call['event_id']}", + "uri": resource_uri, + "tenant": tenant, + } + ], + "writes": [], + "input_hash": str(tool_call["input_hash"]), + "output_hash": str(tool_call["output_hash"]), + } + auth = str(tool_call.get("authorization_status") or "") + if auth not in AUTHORIZATION_TO_DECISION: + raise PFCoreRuntimeError( + "InvalidDecision", + f"unknown authorization_status {auth!r}", + "tool_calls.authorization_status", + ) + decision = AUTHORIZATION_TO_DECISION[auth] + if decision == "allow" and cap_id not in _allowed_capability_ids(principal): + decision = "deny" + elif decision == "allow" and not _same_tenant(principal, action): + decision = "deny" + return principal, _validate_action(action), decision + + +def _handoff_to_event( + handoff: Mapping[str, Any], + *, + trace_id: str, + sequence: int, + timestamp: str, + previous_event_hash: str, + source_repo: str, + source_commit: str, + contract_refs: list[str], +) -> dict[str, Any]: + _require_schema_valid(dict(handoff), "PFCoreHandoff.v0") + validate_handoff_authority(handoff) + from_principal = _validate_principal(handoff.get("from_principal"), path="from_principal") + delegated = handoff.get("delegated_capabilities") + if not isinstance(delegated, list) or not delegated: + raise HandoffAuthorityExpansion("", "delegated_capabilities") + first = delegated[0] if isinstance(delegated[0], dict) else {} + cap_id = str(first.get("capability_id") or "cap:handoff") + effect_kind = str(first.get("effect_kind") or "handoff.delegate") + resource_pattern = str(first.get("resource_pattern") or "agent:*") + to_principal = handoff.get("to_principal") + target_id = ( + str(to_principal.get("principal_id") or "agent-unknown") + if isinstance(to_principal, dict) + else "agent-unknown" + ) + action = _validate_action( + { + "action_id": f"act-{handoff.get('handoff_id', sequence)}", + "tool_name": "handoff.delegate", + "capability": { + "capability_id": cap_id, + "effect_kind": effect_kind, + "resource_pattern": resource_pattern, + }, + "effects": [{"effect_kind": effect_kind}], + "reads": [ + { + "resource_id": f"res-handoff-{sequence}", + "uri": f"agent:{target_id}", + "tenant": from_principal["tenant"], + } + ], + "writes": [], + "input_hash": GENESIS_HASH, + "output_hash": GENESIS_HASH, + }, + path="handoffs.action", + ) + return _finalize_event( + trace_id=trace_id, + event_id=str(handoff.get("handoff_id") or f"handoff-{sequence}"), + sequence=sequence, + timestamp=timestamp, + principal=from_principal, + action=action, + decision="allow", + decision_reason=str(handoff.get("reason") or "handoff"), + contract_refs=list(contract_refs), + evidence_refs=[str(ref) for ref in handoff.get("evidence_refs", []) if ref], + previous_event_hash=previous_event_hash, + source_repo=source_repo, + source_commit=source_commit, + ) + + +def compile_tool_use_trace_to_pfcore_trace(tool_use_trace: dict) -> dict: + """Compile a schema-valid ToolUseTrace.v0 into PFCoreTrace.v0.""" + _require_schema_valid(tool_use_trace, "ToolUseTrace.v0") + + claim_class = "RuntimeChecked" + _assert_claim_class_allowed(claim_class) + + tool_calls = tool_use_trace.get("tool_calls") + if not isinstance(tool_calls, list) or not tool_calls: + raise PFCoreRuntimeError("InvalidTrace", "tool_calls must be non-empty", "tool_calls") + + handoffs = tool_use_trace.get("handoffs") + if handoffs is not None and not isinstance(handoffs, list): + raise PFCoreRuntimeError("InvalidTrace", "handoffs must be an array", "handoffs") + + events: list[dict[str, Any]] = [] + previous_hash = GENESIS_HASH + tenant = str(tool_calls[0].get("tenant") or "default") + agent_id = str(tool_use_trace["agent_id"]) + policy_id = str(tool_use_trace["policy_id"]) + contract_refs = [policy_id] + + for index, tool_call in enumerate(tool_calls): + if not isinstance(tool_call, dict): + continue + principal, action, decision = _tool_call_to_action( + tool_call, + agent_id=agent_id, + tenant=tenant, + capabilities=list(_allowed_capability_ids({"roles": ["agent"], "capabilities": []})), + ) + event = _finalize_event( + trace_id=str(tool_use_trace["trace_id"]), + event_id=str(tool_call["event_id"]), + sequence=index, + timestamp=str(tool_call["timestamp"]), + principal=principal, + action=action, + decision=decision, + decision_reason=str(tool_call.get("authorization_status") or ""), + contract_refs=list(contract_refs), + evidence_refs=[str(ref) for ref in tool_call.get("policy_refs", []) if ref], + previous_event_hash=previous_hash, + source_repo=str(tool_use_trace["source_repo"]), + source_commit=str(tool_use_trace["source_commit"]), + ) + events.append(event) + previous_hash = event["event_hash"] + + if isinstance(handoffs, list): + for offset, handoff in enumerate(handoffs): + if not isinstance(handoff, dict): + continue + sequence = len(events) + offset + timestamp = str(handoff.get("timestamp") or tool_use_trace.get("completed_at") or "") + event = _handoff_to_event( + handoff, + trace_id=str(tool_use_trace["trace_id"]), + sequence=sequence, + timestamp=timestamp, + previous_event_hash=previous_hash, + source_repo=str(tool_use_trace["source_repo"]), + source_commit=str(tool_use_trace["source_commit"]), + contract_refs=contract_refs, + ) + events.append(event) + previous_hash = event["event_hash"] + + denied_source = [ + str(item.get("event_id")) + for item in tool_calls + if isinstance(item, dict) + and AUTHORIZATION_TO_DECISION.get(str(item.get("authorization_status")), "deny") == "deny" + ] + denied_compiled = [event["event_id"] for event in events if event["decision"] == "deny"] + for event_id in denied_source: + if event_id not in denied_compiled: + raise DroppedDeniedEvent(event_id, "events") + + trace: dict[str, Any] = { + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": str(tool_use_trace["trace_id"]), + "workflow_id": str(tool_use_trace["workflow_id"]), + "events": events, + "trace_hash": GENESIS_HASH, + "policy_hash": str(tool_use_trace["policy_hash"]), + "contract_hash": GENESIS_HASH, + "claim_class": claim_class, + "source_repo": str(tool_use_trace["source_repo"]), + "source_commit": str(tool_use_trace["source_commit"]), + "signature_or_digest": GENESIS_HASH, + } + trace["trace_hash"] = compute_trace_hash(trace) + trace["signature_or_digest"] = trace["trace_hash"] + return trace + + +def validate_handoff_authority(handoff: Mapping[str, Any]) -> None: + src = handoff.get("from_principal") + if not isinstance(src, dict): + raise MissingPrincipal(path="from_principal") + allowed = set(_allowed_capability_ids(src)) + delegated = handoff.get("delegated_capabilities") + if not isinstance(delegated, list): + raise HandoffAuthorityExpansion("", "delegated_capabilities") + for index, cap in enumerate(delegated): + if not isinstance(cap, dict): + continue + cap_id = str(cap.get("capability_id") or "") + if cap_id and cap_id not in allowed: + raise HandoffAuthorityExpansion(cap_id, f"delegated_capabilities[{index}]") + + +def validate_pfcore_trace_hash_chain(trace: dict) -> list[str]: + """Return semantic hash-chain errors for a PFCoreTrace.v0 (empty if valid).""" + errors: list[str] = [] + events = trace.get("events") + if not isinstance(events, list): + return ["TraceInvalid: events must be an array"] + + previous = normalize_hash(GENESIS_HASH) + for index, event in enumerate(events): + if not isinstance(event, dict): + errors.append(f"EventInvalid: events[{index}] must be an object") + continue + base = f"events[{index}]" + try: + prev_field = normalize_hash(str(event.get("previous_event_hash") or "")) + except PFCoreRuntimeError: + errors.append(f"EventHashMismatch: invalid previous_event_hash at {base}") + continue + if prev_field != previous: + errors.append( + "EventHashMismatch: " + f"previous_event_hash mismatch at {base} " + f"(expected {previous}, got {prev_field})" + ) + try: + actual_hash = normalize_hash(str(event.get("event_hash") or "")) + except PFCoreRuntimeError: + errors.append(f"EventHashMismatch: invalid event_hash at {base}") + continue + expected_hash = compute_event_hash(event) + if actual_hash != expected_hash: + errors.append( + "EventHashMismatch: " + f"event_hash mismatch at {base} (expected {expected_hash}, got {actual_hash})" + ) + previous = actual_hash + + trace_hash = trace.get("trace_hash") + if trace_hash is None: + return errors + if not isinstance(trace_hash, str): + errors.append("TraceHashMismatch: missing trace_hash") + else: + try: + actual_trace_hash = normalize_hash(trace_hash) + except PFCoreRuntimeError: + errors.append("TraceHashMismatch: invalid trace_hash") + else: + expected_trace_hash = compute_trace_hash(trace) + if actual_trace_hash != expected_trace_hash: + errors.append( + "TraceHashMismatch: " + f"trace_hash mismatch (expected {expected_trace_hash}, got {actual_trace_hash})" + ) + + claim_class = trace.get("claim_class") + if isinstance(claim_class, str): + try: + _assert_claim_class_allowed( + claim_class, + proof_ref=trace.get("proof_ref") or trace.get("proof_term_ref"), + ) + except ClaimClassOverclaim as exc: + errors.append(f"{exc.code}: {exc.message}") + + for index, event in enumerate(events): + if not isinstance(event, dict): + continue + action = event.get("action") + if not isinstance(action, dict): + continue + try: + validate_resource_scope(action, path=f"events[{index}].action") + except ResourceScopeViolation as exc: + errors.append(f"{exc.code}: {exc.message} (at {exc.path})") + + return errors diff --git a/python/pcs_core/registry_data.py b/python/pcs_core/registry_data.py index 2459a2f..5cd0a31 100644 --- a/python/pcs_core/registry_data.py +++ b/python/pcs_core/registry_data.py @@ -60,6 +60,126 @@ def _entry( } +_PF_CORE_PRIMITIVE_CHECKS = [ + { + "check_id": "explicit_artifact_type", + "severity": "release_blocking", + "responsible_component": PCS_CORE, + "execution_required_in_release_mode": True, + "allowed_to_skip": False, + }, + { + "check_id": "schema_valid", + "severity": "release_blocking", + "responsible_component": PCS_CORE, + "execution_required_in_release_mode": True, + "allowed_to_skip": False, + }, +] + +_PF_CORE_ARTIFACT_TYPES = frozenset( + { + "PFCorePrincipal.v0", + "PFCoreCapability.v0", + "PFCoreResource.v0", + "PFCoreAction.v0", + "PFCoreEvent.v0", + "PFCoreTrace.v0", + "PFCoreContract.v0", + "PFCoreHandoff.v0", + "PFCoreCertificate.v0", + "PFCoreRuntimeObservation.v0", + } +) + + +def _pf_core_primitive_entry( + artifact_type: str, + *, + runtime_producer: str = PCS_CORE, + allowed_runtime_producers: list[str] | None = None, +) -> dict[str, Any]: + producers = allowed_runtime_producers or [PCS_CORE, AGENT_RUNTIME] + return _entry( + artifact_type=artifact_type, + schema=f"schemas/{artifact_type}.schema.json", + schema_owner=PCS_CORE, + runtime_producer=runtime_producer, + allowed_runtime_producers=producers, + allowed_statuses=["Draft", "Validated", "Deprecated"], + required_release_fields=[ + "schema_version", + "artifact_type", + "signature_or_digest", + ], + semantic_checks=list(_PF_CORE_PRIMITIVE_CHECKS), + consumer_repos=[PCS_CORE, AGENT_RUNTIME], + release_mode_required=False, + ) + + +def _pf_core_release_entry( + artifact_type: str, + *, + id_field: str, + runtime_producer: str = PCS_CORE, + extra_release_fields: list[str] | None = None, +) -> dict[str, Any]: + required = [ + "schema_version", + "artifact_type", + id_field, + "claim_class", + "source_repo", + "source_commit", + "signature_or_digest", + ] + if extra_release_fields: + required.extend(extra_release_fields) + return _entry( + artifact_type=artifact_type, + schema=f"schemas/{artifact_type}.schema.json", + schema_owner=PCS_CORE, + runtime_producer=runtime_producer, + allowed_runtime_producers=[PCS_CORE, AGENT_RUNTIME], + allowed_statuses=[ + "Draft", + "RuntimeChecked", + "CertificateChecked", + "LeanKernelChecked", + "Rejected", + "Stale", + ], + required_release_fields=required, + semantic_checks=[ + *_PF_CORE_PRIMITIVE_CHECKS, + { + "check_id": "claim_class_matches_assurance", + "severity": "release_blocking", + "responsible_component": PCS_CORE, + "execution_required_in_release_mode": True, + "allowed_to_skip": False, + }, + { + "check_id": "lean_kernel_proof", + "severity": "release_blocking", + "responsible_component": PCS_CORE, + "execution_required_in_release_mode": False, + "allowed_to_skip": True, + }, + { + "check_id": "lean_library_build", + "severity": "release_blocking", + "responsible_component": PCS_CORE, + "execution_required_in_release_mode": False, + "allowed_to_skip": True, + }, + ], + consumer_repos=[PCS_CORE, AGENT_RUNTIME], + release_mode_required=True, + ) + + _REGISTRY_ENTRIES: dict[str, dict[str, Any]] = { "AssumptionSet.v0": _entry( artifact_type="AssumptionSet.v0", @@ -1082,6 +1202,34 @@ def _entry( consumer_repos=[PCS_CORE, LABTRUST, CERTIFYEDGE, PF, SM], release_mode_required=False, ), + + "PFCorePrincipal.v0": _pf_core_primitive_entry("PFCorePrincipal.v0"), + "PFCoreCapability.v0": _pf_core_primitive_entry("PFCoreCapability.v0"), + "PFCoreResource.v0": _pf_core_primitive_entry("PFCoreResource.v0"), + "PFCoreAction.v0": _pf_core_primitive_entry("PFCoreAction.v0"), + "PFCoreEvent.v0": _pf_core_primitive_entry( + "PFCoreEvent.v0", + runtime_producer=AGENT_RUNTIME, + ), + "PFCoreContract.v0": _pf_core_primitive_entry("PFCoreContract.v0"), + "PFCoreHandoff.v0": _pf_core_primitive_entry("PFCoreHandoff.v0"), + "PFCoreTrace.v0": _pf_core_release_entry( + "PFCoreTrace.v0", + id_field="trace_id", + extra_release_fields=["trace_hash", "events"], + ), + "PFCoreCertificate.v0": _pf_core_release_entry( + "PFCoreCertificate.v0", + id_field="certificate_id", + extra_release_fields=["trace_hash", "claim_class"], + ), + "PFCoreRuntimeObservation.v0": _pf_core_release_entry( + "PFCoreRuntimeObservation.v0", + id_field="observation_id", + runtime_producer=AGENT_RUNTIME, + extra_release_fields=["observed_at", "payload_hash"], + ), + } @@ -1100,3 +1248,136 @@ def all_registry_semantic_check_refs() -> set[str]: if isinstance(check, dict) and check.get("check_id"): refs.add(registry_semantic_check_ref(artifact_type, str(check["check_id"]))) return refs + +def pf_core_artifact_types() -> frozenset[str]: + return _PF_CORE_ARTIFACT_TYPES + + +_PROOF_OVERCLAIM_CLASSES = frozenset({"LeanKernelChecked", "ProofChecked"}) + +_ASSUMPTION_REF_PREFIXES = ( + "docs/", + "examples/", + "as-", + "AssumptionSet", +) + + + +def deferred_registry_obligations(artifact_type: str) -> list[dict[str, Any]]: + """Return registry semantic checks marked allowed_to_skip for an artifact type.""" + entry = _REGISTRY_ENTRIES.get(artifact_type) + if not entry: + return [] + checks = entry.get("semantic_checks") + if not isinstance(checks, list): + return [] + return [check for check in checks if isinstance(check, dict) and check.get("allowed_to_skip")] + + + +def infer_skipped_registry_checks( + certificate: dict[str, Any], + *, + deferred_checks: list[dict[str, Any]] | None = None, +) -> list[str]: + """Infer which deferrable registry checks were not satisfied by the certificate.""" + artifact_type = str(certificate.get("artifact_type") or "PFCoreCertificate.v0") + deferred = deferred_checks or deferred_registry_obligations(artifact_type) + skipped: list[str] = [] + for check in deferred: + check_id = str(check.get("check_id") or "") + if check_id == "lean_kernel_proof" and not certificate.get("lean_proof_checked"): + skipped.append(check_id) + elif check_id == "lean_library_build": + build = certificate.get("lean_build_status") + if not isinstance(build, dict) or not build.get("ok"): + skipped.append(check_id) + return skipped + + + +def _assumption_ref_valid(ref: str) -> bool: + text = ref.strip() + if not text: + return False + if text.endswith(".md") or text.endswith(".json"): + return True + if text.startswith(_ASSUMPTION_REF_PREFIXES): + return True + return False + + + +def enforce_assumption_declared( + certificate: dict[str, Any], + registry_context: dict[str, Any] | None = None, +) -> list[str]: + """ + Enforce AssumptionDeclared rules when deferrable registry checks were skipped. + + Certificates must not claim LeanKernelChecked or ProofChecked when deferred + obligations were not executed. Skipped checks require non-empty assumption_refs + pointing at AssumptionSet.v0 artifacts or documented assumption paths. + """ + artifact_type = str(certificate.get("artifact_type") or "PFCoreCertificate.v0") + context = registry_context or _REGISTRY_ENTRIES.get(artifact_type, {}) + explicit_skipped = context.get("skipped_checks") + if isinstance(explicit_skipped, list): + skipped = [str(item) for item in explicit_skipped] + else: + deferred = context.get("semantic_checks") + deferred_checks = ( + [check for check in deferred if isinstance(check, dict) and check.get("allowed_to_skip")] + if isinstance(deferred, list) + else deferred_registry_obligations(artifact_type) + ) + skipped = infer_skipped_registry_checks(certificate, deferred_checks=deferred_checks) + + if not skipped: + return [] + + issues: list[str] = [] + claim_class = str(certificate.get("claim_class") or "") + if claim_class in _PROOF_OVERCLAIM_CLASSES: + issues.append( + f"root: claim_class {claim_class!r} forbidden when deferred registry checks " + f"were skipped: {', '.join(skipped)}" + ) + + refs = certificate.get("assumption_refs") + if not isinstance(refs, list) or not refs: + issues.append( + "root: deferred registry checks require non-empty assumption_refs " + "(AssumptionSet.v0 id or docs/pf-core/*.md)" + ) + elif not any(_assumption_ref_valid(str(ref)) for ref in refs): + issues.append( + "root: assumption_refs must reference AssumptionSet.v0 ids or documented " + "assumption paths when registry checks are deferred" + ) + elif claim_class not in {"AssumptionDeclared", "RuntimeChecked", "CertificateChecked", "ReplayValidated", "SchemaValidated", "OutOfScope"}: + if claim_class in _PROOF_OVERCLAIM_CLASSES or claim_class == "LeanKernelChecked": + pass # already reported + elif skipped: + issues.append( + f"root: deferred registry checks require claim_class AssumptionDeclared " + f"(got {claim_class!r})" + ) + + return issues + + + +PF_CORE_CLAIM_CLASSES = frozenset( + { + "SchemaValidated", + "RuntimeChecked", + "CertificateChecked", + "LeanKernelChecked", + "ReplayValidated", + "AssumptionDeclared", + "OutOfScope", + } +) + diff --git a/python/pcs_core/validate.py b/python/pcs_core/validate.py index 38a8487..f637dad 100644 --- a/python/pcs_core/validate.py +++ b/python/pcs_core/validate.py @@ -11,29 +11,15 @@ from referencing import Registry, Resource from referencing.jsonschema import DRAFT202012 -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, - validate_pcs_bench_ingest_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.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.paths import examples_dir as default_examples_dir -from pcs_core.paths import schemas_dir from pcs_core.protocol_validate import ( validate_artifact_registry_semantics, validate_conformance_report_semantics, @@ -42,13 +28,11 @@ validate_release_manifest_fixture_refs, validate_release_manifest_semantics, ) -from pcs_core.status import ARTIFACT_STATUSES, TRACE_CERTIFICATE_STATUSES 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", @@ -93,6 +77,19 @@ "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( @@ -122,7 +119,108 @@ def __init__(self, message: str, errors: list[str] | None = None): 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) @@ -380,6 +478,7 @@ def detect_artifact_type(data: dict[str, Any]) -> str | None: return None + def _load_schema(path: Path) -> dict[str, Any]: with path.open(encoding="utf-8") as f: return json.load(f) @@ -574,6 +673,98 @@ def _validate_signed_bundle(data: dict[str, Any]) -> list[str]: 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] = [] @@ -637,9 +828,17 @@ def validate_semantics(data: dict[str, Any], artifact_type: str) -> list[str]: return errors if artifact_type == "LeanCheckResult.v0": - errors.extend(validate_lean_check_result_semantics(data)) + 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 @@ -730,9 +929,25 @@ def validate_semantics(data: dict[str, Any], artifact_type: str) -> list[str]: if status and status not in TRACE_CERTIFICATE_STATUSES: errors.append(f"TraceCertificate.v0 invalid status {status!r}") - return errors + 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: @@ -826,6 +1041,216 @@ def check_valid_examples(examples_dir: Path | None = None) -> None: 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] = { @@ -837,35 +1262,7 @@ def check_invalid_examples(examples_dir: Path | None = None) -> None: "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", - "invalid_release_manifest_placeholder_commit.json": "ReleaseManifest.v0", - "invalid_handoff_manifest_missing_input_hash.json": "HandoffManifest.v0", - "invalid_release_chain_validation_failed_status.json": "ReleaseChainValidationResult.v0", - "invalid_pcs_bench_ingest_missing_refs.json": "PcsBenchIngest.v0", - "invalid_pcs_bench_ingest_bad_ref_digest.json": "PcsBenchIngest.v0", - "invalid_pcs_bench_ingest_zero_commit.json": "PcsBenchIngest.v0", - "invalid_pcs_bench_ingest_empty_runs.json": "PcsBenchIngest.v0", - "invalid_pcs_bench_ingest_path_only.json": "PcsBenchIngest.v0", } - invalid_tool_use = examples_dir / "tool-use-release-invalid" - if invalid_tool_use.is_dir(): - from pcs_core.tool_use_validate import validate_tool_use_invalid_case - - for case_dir in sorted(p for p in invalid_tool_use.iterdir() if p.is_dir()): - harness_errors = validate_tool_use_invalid_case(case_dir) - if harness_errors: - raise ValidationError( - f"tool-use invalid case {case_dir.name}: {'; '.join(harness_errors)}", - ) - invalid_computation = examples_dir / "computation-release-invalid" - if invalid_computation.is_dir(): - from pcs_core.computation_validate import validate_computation_invalid_case - - for case_dir in sorted(p for p in invalid_computation.iterdir() if p.is_dir()): - harness_errors = validate_computation_invalid_case(case_dir) - if harness_errors: - raise ValidationError( - f"computation invalid case {case_dir.name}: {'; '.join(harness_errors)}", - ) for filename, artifact_type in invalid_cases.items(): path = examples_dir / filename data = json.loads(path.read_text(encoding="utf-8")) @@ -877,3 +1274,4 @@ def check_invalid_examples(examples_dir: Path | None = None) -> None: 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_pf_core_fixtures.py b/python/scripts/gen_pf_core_fixtures.py new file mode 100644 index 0000000..b1fafea --- /dev/null +++ b/python/scripts/gen_pf_core_fixtures.py @@ -0,0 +1,485 @@ +"""Generate PF-Core Stage 2 example fixtures (dev utility).""" + +from __future__ import annotations + +import json +from pathlib import Path + +from pcs_core.hash import canonical_hash +from pcs_core.pf_core_runtime import ( + GENESIS_HASH, + compile_runtime_observation_to_event, + compile_tool_use_trace_to_pfcore_trace, + compute_trace_hash, +) +from pcs_core.paths import repo_root + +ROOT = repo_root() / "examples" +VALID = ROOT / "pf-core-valid" +INVALID = ROOT / "pf-core-invalid" + + +def _write(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 _finalize_obs(base: dict) -> dict: + payload = {key: value for key, value in base.items() if key != "signature_or_digest"} + base["payload_hash"] = canonical_hash(payload) + base["signature_or_digest"] = base["payload_hash"] + return base + + +def _base_obs(**overrides: object) -> dict: + obs = _finalize_obs( + { + "schema_version": "v0", + "artifact_type": "PFCoreRuntimeObservation.v0", + "observation_id": "obs-1", + "trace_id": "trace-1", + "event_id": "ev-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:" + "a" * 64, + "output_hash": "sha256:" + "b" * 64, + }, + "decision": "allow", + "decision_reason": "authorized", + "policy_ref": "policy/default.v0", + "evidence_refs": [], + "runtime_ref": "agent-runtime", + "previous_event_hash": GENESIS_HASH, + "payload_hash": GENESIS_HASH, + "claim_class": "RuntimeChecked", + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": GENESIS_HASH, + } + ) + obs.update(overrides) + return _finalize_obs(obs) + + +def _trace_from_event(event: dict, *, trace_id: str, workflow_id: str) -> dict: + trace = { + "schema_version": "v0", + "artifact_type": "PFCoreTrace.v0", + "trace_id": trace_id, + "workflow_id": workflow_id, + "events": [event], + "trace_hash": GENESIS_HASH, + "policy_hash": GENESIS_HASH, + "contract_hash": GENESIS_HASH, + "claim_class": "RuntimeChecked", + "source_repo": event["source_repo"], + "source_commit": event["source_commit"], + "signature_or_digest": GENESIS_HASH, + } + trace["trace_hash"] = compute_trace_hash(trace) + trace["signature_or_digest"] = trace["trace_hash"] + return trace + + +def main() -> None: + file_read = _base_obs( + observation_id="obs-file-read-1", + trace_id="trace-file-read-1", + event_id="ev-file-read-1", + ) + file_read_event = compile_runtime_observation_to_event(file_read) + file_read_trace = _trace_from_event( + file_read_event, + trace_id="trace-file-read-1", + workflow_id="agent_tool_use.safety_v0", + ) + _write(VALID / "file_read_allowed" / "observation.json", file_read) + _write(VALID / "file_read_allowed" / "event.json", file_read_event) + _write(VALID / "file_read_allowed" / "trace.json", file_read_trace) + _write( + VALID / "file_read_allowed" / "manifest.json", + {"description": "Allowed file read within tenant"}, + ) + + denied_cross = _base_obs( + observation_id="obs-denied-tenant-1", + trace_id="trace-denied-tenant-1", + event_id="ev-denied-tenant-1", + decision="allow", + action={ + "action_id": "act-denied-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-x", + "uri": "/data/secret.txt", + "tenant": "tenant-b", + } + ], + "writes": [], + "input_hash": "sha256:" + "c" * 64, + "output_hash": "sha256:" + "d" * 64, + }, + ) + denied_event = compile_runtime_observation_to_event(denied_cross) + _write(VALID / "file_read_denied_cross_tenant" / "observation.json", denied_cross) + _write(VALID / "file_read_denied_cross_tenant" / "event.json", denied_event) + + network = _base_obs( + observation_id="obs-network-deny-1", + trace_id="trace-network-deny-1", + event_id="ev-network-deny-1", + principal={ + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": ["agent"], + "capabilities": [], + }, + action={ + "action_id": "act-network-1", + "tool_name": "network.request", + "capability": { + "capability_id": "cap:network", + "effect_kind": "network.egress", + "resource_pattern": "*", + }, + "effects": [{"effect_kind": "network.egress"}], + "reads": [{"resource_id": "res-net", "uri": "https://example.com", "tenant": "tenant-a"}], + "writes": [], + "input_hash": "sha256:" + "e" * 64, + "output_hash": "sha256:" + "f" * 64, + }, + decision="deny", + decision_reason="network egress denied", + ) + network_event = compile_runtime_observation_to_event(network) + _write(VALID / "network_denied" / "observation.json", network) + _write(VALID / "network_denied" / "event.json", network_event) + + email = _base_obs( + observation_id="obs-email-1", + trace_id="trace-email-1", + event_id="ev-email-1", + principal={ + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": ["agent"], + "capabilities": ["cap:email-send"], + }, + action={ + "action_id": "act-email-1", + "tool_name": "email.send", + "capability": { + "capability_id": "cap:email-send", + "effect_kind": "email.send", + "resource_pattern": "mailto:*", + }, + "effects": [{"effect_kind": "email.send"}], + "reads": [{"resource_id": "res-mail", "uri": "mailto:user@example.com", "tenant": "tenant-a"}], + "writes": [], + "input_hash": "sha256:" + "1" * 64, + "output_hash": "sha256:" + "2" * 64, + }, + ) + email_event = compile_runtime_observation_to_event(email) + _write(VALID / "email_send_authorized" / "observation.json", email) + _write(VALID / "email_send_authorized" / "event.json", email_event) + + handoff = { + "schema_version": "v0", + "artifact_type": "PFCoreHandoff.v0", + "handoff_id": "handoff-subset-1", + "from_principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": ["agent"], + "capabilities": ["cap:handoff"], + }, + "to_principal": { + "principal_id": "agent-2", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": ["handoff_delegate"], + "capabilities": [], + }, + "delegated_capabilities": [ + { + "capability_id": "cap:handoff", + "effect_kind": "handoff.delegate", + "resource_pattern": "agent:*", + } + ], + "reason": "delegate handoff authority to agent-2", + "evidence_refs": ["evidence/handoff.v0"], + "signature_or_digest": GENESIS_HASH, + } + payload = {k: v for k, v in handoff.items() if k != "signature_or_digest"} + handoff["signature_or_digest"] = canonical_hash(payload) + _write(VALID / "handoff_subset_authority" / "handoff.json", handoff) + + tool_trace = { + "schema_version": "v0", + "trace_id": "trace-agent-safety-001", + "workflow_id": "agent_tool_use.safety_v0", + "agent_id": "agent-safety-conformance-001", + "policy_id": "policy-no-secret-exfiltration-v0", + "policy_hash": "sha256:76d4443f09c6fb0d6cc7bebc5c80eae53bd008e4a212ab61d0d1844ac773b5cd", + "started_at": "2026-05-18T00:00:00Z", + "completed_at": "2026-05-18T00:00:05Z", + "tool_calls": [ + { + "event_id": "evt-001", + "timestamp": "2026-05-18T00:00:01Z", + "tool_name": "filesystem.read", + "tool_category": "filesystem", + "input_hash": "sha256:" + "a" * 64, + "output_hash": "sha256:" + "b" * 64, + "authorization_status": "authorized", + "policy_refs": ["policy-no-secret-exfiltration-v0"], + "resource_uri": "/data/report.txt", + "tenant": "tenant-a", + }, + { + "event_id": "evt-002", + "timestamp": "2026-05-18T00:00:02Z", + "tool_name": "network.request", + "tool_category": "network", + "input_hash": "sha256:" + "c" * 64, + "output_hash": "sha256:" + "d" * 64, + "authorization_status": "rejected", + "policy_refs": ["policy-no-secret-exfiltration-v0"], + "resource_uri": "https://example.com", + "tenant": "tenant-a", + }, + ], + "trace_hash": GENESIS_HASH, + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "a111111111111111111111111111111111111111", + "signature_or_digest": GENESIS_HASH, + } + compiled = compile_tool_use_trace_to_pfcore_trace(tool_trace) + _write(VALID / "tool_use_trace_compiled" / "tool_use_trace.json", tool_trace) + _write(VALID / "tool_use_trace_compiled" / "pfcore_trace.json", compiled) + + _write( + INVALID / "unknown_capability" / "manifest.json", + { + "expected_error": "UnknownCapability", + "must_fail_at": "runtime_to_pfcore_event", + }, + ) + bad_cap = _base_obs( + action={ + "action_id": "act-bad-cap", + "tool_name": "filesystem.read", + "capability": { + "capability_id": "cap:unknown", + "effect_kind": "file.read", + "resource_pattern": "/data/*", + }, + "effects": [{"effect_kind": "file.read"}], + "reads": [{"resource_id": "res-1", "uri": "/data/x", "tenant": "tenant-a"}], + "writes": [], + "input_hash": "sha256:" + "a" * 64, + "output_hash": "sha256:" + "b" * 64, + } + ) + _write(INVALID / "unknown_capability" / "observation.json", bad_cap) + + _write( + INVALID / "unknown_effect" / "manifest.json", + { + "expected_error": "UnknownEffect", + "must_fail_at": "runtime_to_pfcore_event", + }, + ) + bad_effect = _base_obs( + action={ + "action_id": "act-bad-effect", + "tool_name": "custom.tool", + "capability": { + "capability_id": "cap:file-read", + "effect_kind": "file.read", + "resource_pattern": "/data/*", + }, + "effects": [{"effect_kind": "custom.unknown"}], + "reads": [{"resource_id": "res-1", "uri": "/data/x", "tenant": "tenant-a"}], + "writes": [], + "input_hash": "sha256:" + "a" * 64, + "output_hash": "sha256:" + "b" * 64, + } + ) + _write(INVALID / "unknown_effect" / "observation.json", bad_effect) + + _write( + INVALID / "missing_principal" / "manifest.json", + { + "expected_error": "MissingPrincipal", + "must_fail_at": "runtime_to_pfcore_event", + }, + ) + missing_principal = _base_obs( + principal={ + "principal_id": "", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": [], + "capabilities": [], + } + ) + _write(INVALID / "missing_principal" / "observation.json", missing_principal) + + tampered_trace = dict(file_read_trace) + tampered_trace["trace_hash"] = "sha256:" + "f" * 64 + _write( + INVALID / "trace_hash_mismatch" / "manifest.json", + { + "expected_error": "TraceHashMismatch", + "must_fail_at": "validate_pfcore_trace_hash_chain", + }, + ) + _write(INVALID / "trace_hash_mismatch" / "trace.json", tampered_trace) + + tampered_event = dict(file_read_event) + tampered_event["event_hash"] = "sha256:" + "e" * 64 + bad_event_trace = _trace_from_event( + tampered_event, + trace_id="trace-bad-event", + workflow_id="agent_tool_use.safety_v0", + ) + bad_event_trace["trace_hash"] = "sha256:" + "f" * 64 + _write( + INVALID / "event_hash_mismatch" / "manifest.json", + { + "expected_error": "EventHashMismatch", + "must_fail_at": "validate_pfcore_trace_hash_chain", + }, + ) + _write(INVALID / "event_hash_mismatch" / "trace.json", bad_event_trace) + + _write( + INVALID / "dropped_denied_event" / "manifest.json", + { + "expected_error": "DroppedDeniedEvent", + "must_fail_at": "validate_denied_events_preserved", + }, + ) + dropped_tool = { + "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": GENESIS_HASH, + "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:" + "a" * 64, + "output_hash": "sha256:" + "b" * 64, + "authorization_status": "rejected", + "policy_refs": ["policy/default.v0"], + "tenant": "tenant-a", + } + ], + "trace_hash": GENESIS_HASH, + "source_repo": "https://github.com/example/agent-runtime", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": GENESIS_HASH, + } + dropped_compiled = compile_tool_use_trace_to_pfcore_trace(dropped_tool) + dropped_compiled["events"] = [ + event for event in dropped_compiled["events"] if event["decision"] != "deny" + ] + _write(INVALID / "dropped_denied_event" / "tool_use_trace.json", dropped_tool) + _write(INVALID / "dropped_denied_event" / "pfcore_trace.json", dropped_compiled) + + _write( + INVALID / "handoff_authority_expansion" / "manifest.json", + { + "expected_error": "HandoffAuthorityExpansion", + "must_fail_at": "validate_handoff_authority", + }, + ) + bad_handoff = { + "schema_version": "v0", + "artifact_type": "PFCoreHandoff.v0", + "handoff_id": "handoff-bad-1", + "from_principal": { + "principal_id": "agent-1", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": ["file_reader"], + "capabilities": [], + }, + "to_principal": { + "principal_id": "agent-2", + "principal_kind": "agent", + "tenant": "tenant-a", + "roles": ["handoff_delegate"], + "capabilities": [], + }, + "delegated_capabilities": [ + { + "capability_id": "cap:network", + "effect_kind": "network.egress", + "resource_pattern": "*", + } + ], + "reason": "over-delegation", + "evidence_refs": [], + "signature_or_digest": GENESIS_HASH, + } + _write(INVALID / "handoff_authority_expansion" / "handoff.json", bad_handoff) + + overclaim_trace = dict(file_read_trace) + overclaim_trace["claim_class"] = "LeanKernelChecked" + _write( + INVALID / "claim_class_overclaim" / "manifest.json", + { + "expected_error": "ClaimClassOverclaim", + "must_fail_at": "validate_pfcore_trace_hash_chain", + }, + ) + _write(INVALID / "claim_class_overclaim" / "trace.json", overclaim_trace) + + +if __name__ == "__main__": + main() + print(f"Wrote fixtures under {ROOT}") diff --git a/python/tests/test_pf_core_bridge_demo.py b/python/tests/test_pf_core_bridge_demo.py new file mode 100644 index 0000000..b138f0c --- /dev/null +++ b/python/tests/test_pf_core_bridge_demo.py @@ -0,0 +1,75 @@ +"""End-to-end LabTrust bridge demo tests (Phase E).""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +from pcs_core.pf_core_certificate import attach_external_certificate_check +from pcs_core.pf_core_labtrust_adapter import normalize_labtrust_release +from pcs_core.pf_core_replay import replay_trace +from pcs_core.validate import validate_artifact, validate_file + +REPO = Path(__file__).resolve().parents[2] +LABTRUST_TC = REPO / "examples" / "labtrust" / "trace_certificate.valid.json" +LABTRUST_SCB = REPO / "examples" / "labtrust" / "science_claim_bundle.certified.valid.json" +LABTRUST_TRACE = REPO / "examples" / "pf-core-valid" / "labtrust_replay" / "trace.json" +BRIDGE_SCRIPT = REPO / "scripts" / "pf-core-bridge-demo.sh" + + +def _load(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def test_trace_certificate_validates() -> None: + assert validate_file(LABTRUST_TC) == "TraceCertificate.v0" + + +def test_labtrust_adapter_produces_pfcore_trace() -> None: + tc = _load(LABTRUST_TC) + scb = _load(LABTRUST_SCB) + receipt = scb["runtime_receipts"][0] + trace = normalize_labtrust_release(tc, receipt) + assert trace["artifact_type"] == "PFCoreTrace.v0" + assert trace["events"][0]["contract_refs"] + + +def test_labtrust_replay_fixture_matches() -> None: + result = replay_trace(LABTRUST_TRACE) + assert result.match is True + + +def test_attach_certificate_check_bridge() -> None: + trace = _load(LABTRUST_TRACE) + cert = attach_external_certificate_check( + trace, + checker="certifyedge", + checker_version="0.1.0", + attestation_ref=str(LABTRUST_TC.relative_to(REPO)).replace("\\", "/"), + assumption_refs=["as-labtrust-qc-v0.1"], + ) + assert cert["claim_class"] == "CertificateChecked" + assert cert["checker"] == "certifyedge" + validate_artifact(cert, "PFCoreCertificate.v0") + + +def test_bridge_demo_script_runs() -> None: + if sys.platform == "win32": + pytest = __import__("pytest") + pytest.skip("bridge shell script requires bash (use WSL or CI)") + result = subprocess.run( + ["bash", str(BRIDGE_SCRIPT)], + cwd=REPO, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_assumption_declared_fixture_uses_assumption_set_id() -> None: + cert_path = REPO / "examples/pf-core-valid/assumption_declared/certificate.json" + cert = _load(cert_path) + assert "as-pfcore-demo-v0.1" in cert["assumption_refs"] + validate_file(cert_path) diff --git a/python/tests/test_pf_core_cross_language.py b/python/tests/test_pf_core_cross_language.py new file mode 100644 index 0000000..d711025 --- /dev/null +++ b/python/tests/test_pf_core_cross_language.py @@ -0,0 +1,99 @@ +"""Cross-language PF-Core schema registration and artifact_type detection parity.""" + +from __future__ import annotations + +import json +import re +import subprocess +import sys +from pathlib import Path + +import pytest + +from pcs_core.validate import ARTIFACT_SCHEMAS, detect_artifact_type, validate_schema + +REPO = Path(__file__).resolve().parents[2] + +PF_CORE_TYPES = sorted( + key + for key in ARTIFACT_SCHEMAS + if key.startswith("PFCore") or key in {"ToolUseTrace.v0", "LeanCheckResult.v0", "PCSBridgeCertificate.v0"} +) + +TS_SCHEMAS = REPO / "typescript" / "packages" / "core" / "src" / "schema.ts" +RUST_SCHEMAS = REPO / "rust" / "crates" / "pcs-core" / "src" / "validation.rs" + + +def _load_json(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def _extract_quoted_types(path: Path, marker: str) -> set[str]: + text = path.read_text(encoding="utf-8") + block = text.split(marker, 1)[-1] + return set(re.findall(r'"((?:PFCore|ToolUseTrace|LeanCheckResult|PCSBridgeCertificate)[^"]+)"', block)) + + +def test_python_pf_core_schemas_registered() -> None: + for artifact_type in PF_CORE_TYPES: + assert artifact_type in ARTIFACT_SCHEMAS + + +def test_typescript_registers_same_pf_core_schemas() -> None: + ts_types = _extract_quoted_types(TS_SCHEMAS, "const ARTIFACT_SCHEMAS") + assert set(PF_CORE_TYPES).issubset(ts_types) + + +def test_rust_registers_same_pf_core_schemas() -> None: + rust_types = _extract_quoted_types(RUST_SCHEMAS, "const ARTIFACT_SCHEMAS") + assert set(PF_CORE_TYPES).issubset(rust_types) + + +@pytest.mark.parametrize( + "relative", + [ + "examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json", + "examples/pf-core-valid/contract_checked/trace.json", + "examples/pf-core-valid/handoff_subset_authority/handoff.json", + "examples/pf-core-valid/assumption_declared/certificate.json", + ], +) +def test_python_explicit_artifact_type_detection(relative: str) -> None: + path = REPO / relative + data = _load_json(path) + detected = detect_artifact_type(data) + assert detected == data["artifact_type"] + + +def test_pf_core_trace_not_detected_as_trace_certificate() -> None: + trace = _load_json(REPO / "examples/pf-core-valid/tool_use_trace_compiled/pfcore_trace.json") + assert detect_artifact_type(trace) == "PFCoreTrace.v0" + assert detect_artifact_type(trace) != "TraceCertificate.v0" + + +@pytest.mark.parametrize("artifact_type", PF_CORE_TYPES) +def test_pf_core_schema_files_exist(artifact_type: str) -> None: + schema_path = REPO / "schemas" / ARTIFACT_SCHEMAS[artifact_type] + assert schema_path.is_file(), f"missing schema for {artifact_type}" + + +def test_rust_pf_core_detection_tests_pass() -> None: + result = subprocess.run( + ["cargo", "test", "pf_core_explicit_artifact_types", "--", "--nocapture"], + cwd=REPO / "rust", + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_typescript_pf_core_detection_tests_pass() -> None: + if sys.platform == "win32": + pytest.skip("typescript workspace test runner uses shell globs unavailable on Windows") + result = subprocess.run( + ["npm", "test"], + cwd=REPO / "typescript", + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + result.stderr diff --git a/python/tests/test_pf_core_hash_vector_parity.py b/python/tests/test_pf_core_hash_vector_parity.py new file mode 100644 index 0000000..9c77443 --- /dev/null +++ b/python/tests/test_pf_core_hash_vector_parity.py @@ -0,0 +1,33 @@ +"""Parity gate: PCS hash vectors must match provability-fabric-core adapter fixtures.""" + +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + +from pcs_core.pf_core_hash_vector_parity import verify_pf_core_hash_vectors + +ROOT = Path(__file__).resolve().parents[2] +LOCAL_VECTORS = Path(__file__).resolve().parent / "hash_vectors" +PF_CORE_TAG = os.environ.get("PF_CORE_TAG", "pf-core-v0.6.0") +UPSTREAM_FIXTURES = os.environ.get("PF_CORE_UPSTREAM_VECTORS") + + +def test_hash_vectors_match_pf_core_adapter_native() -> None: + """Frozen vectors match pf-core tag via native parity checker (CI-friendly).""" + upstream = Path(UPSTREAM_FIXTURES) if UPSTREAM_FIXTURES else None + drift = verify_pf_core_hash_vectors( + LOCAL_VECTORS, + pf_core_tag=PF_CORE_TAG, + upstream_dir=upstream, + ) + assert drift == [], "\n".join(drift) + + +def test_trace_certificate_vector_has_sha256_prefix_digest() -> None: + digest = (LOCAL_VECTORS / "TraceCertificate.v0" / "digest.txt").read_text( + encoding="utf-8" + ).strip() + assert digest.startswith("sha256:"), "PCS digest must retain sha256: prefix form" diff --git a/python/tests/test_pf_core_phase_d.py b/python/tests/test_pf_core_phase_d.py new file mode 100644 index 0000000..5cf98d3 --- /dev/null +++ b/python/tests/test_pf_core_phase_d.py @@ -0,0 +1,93 @@ +"""Tests for PF-Core Phase D: invariant theorem and richer Lean codegen.""" + +from __future__ import annotations + +import json +import shutil +from pathlib import Path + +import pytest + +from pcs_core.pf_core_lean_codegen import ( + generate_proof_obligation_file, + trace_has_contract_refs, + trace_to_lean, +) +from pcs_core.validate import validate_file + +REPO = Path(__file__).resolve().parents[2] +VALID_TRACE = REPO / "examples" / "pf-core-valid" / "tool_use_trace_compiled" / "pfcore_trace.json" +CONTRACT_TRACE = REPO / "examples" / "pf-core-valid" / "contract_checked" / "trace.json" +HANDOFF_FIXTURE = REPO / "examples" / "pf-core-valid" / "handoff_subset_authority" / "handoff.json" +ASSUMPTION_CERT = REPO / "examples" / "pf-core-valid" / "assumption_declared" / "certificate.json" +LAKE_AVAILABLE = shutil.which("lake") is not None or shutil.which("wsl") is not None + + +def _load(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def test_contract_lean_has_invariant_preserved_cons_theorem() -> None: + text = (REPO / "lean" / "PFCore" / "Contract.lean").read_text(encoding="utf-8") + assert "theorem invariant_preserved_cons" in text + assert "sorry" not in text + + +def test_generated_proof_includes_per_event_theorems(tmp_path: Path) -> None: + trace = _load(VALID_TRACE) + proof_path = generate_proof_obligation_file(trace, tmp_path, trace_path=VALID_TRACE) + text = proof_path.read_text(encoding="utf-8") + assert "theorem concrete_event_safe_" in text + assert "eventSafeD evt_001 = true := by" in text or "eventSafeD ev_" in text + assert "#check eventSafeD" not in text + + +def test_generated_proof_documents_contract_refs_when_missing(tmp_path: Path) -> None: + trace = _load(CONTRACT_TRACE) + assert trace_has_contract_refs(trace) + # No contract JSON alongside trace -> documents gap + proof_path = generate_proof_obligation_file(trace, tmp_path) + text = proof_path.read_text(encoding="utf-8") + assert "validate-contracts" in text + + +def test_generated_proof_discharges_contracts_with_json(tmp_path: Path) -> None: + trace = _load(CONTRACT_TRACE) + proof_path = generate_proof_obligation_file(trace, tmp_path, trace_path=CONTRACT_TRACE) + text = proof_path.read_text(encoding="utf-8") + assert "concrete_trace_satisfies_contract_" in text + + +def test_generated_proof_includes_handoff_when_present(tmp_path: Path) -> None: + handoff = _load(HANDOFF_FIXTURE) + trace = _load(VALID_TRACE) + trace_file = tmp_path / "pfcore_trace.json" + trace_file.write_text(json.dumps(trace), encoding="utf-8") + (tmp_path / "handoff.json").write_text(json.dumps(handoff), encoding="utf-8") + proof_path = generate_proof_obligation_file(trace, tmp_path / "out", trace_path=trace_file) + text = proof_path.read_text(encoding="utf-8") + assert "handoffSafeD" in text + assert "theorem concrete_handoff_safe_" in text + + +@pytest.mark.skipif(not LAKE_AVAILABLE, reason="lake or WSL not available") +def test_generated_proof_compiles_with_lake() -> None: + from pcs_core.lean_check import pfcore_generated_dir, run_lean_concrete_proof + + trace = _load(VALID_TRACE) + proof_path = generate_proof_obligation_file(trace, pfcore_generated_dir(), trace_path=VALID_TRACE) + ok, detail = run_lean_concrete_proof(proof_path, skip_build=False) + assert ok, detail + + +def test_assumption_declared_fixture_validates() -> None: + validate_file(ASSUMPTION_CERT) + validate_file(REPO / "examples/pf-core-valid/assumption_declared/assumption_set.json") + + +def test_assumption_set_id_ref_enforced_for_deferred_certificate() -> None: + cert = _load(ASSUMPTION_CERT) + refs = cert.get("assumption_refs") + assert isinstance(refs, list) + assert any(str(ref).startswith("as-") for ref in refs) + assert cert["claim_class"] == "AssumptionDeclared" diff --git a/python/tests/test_pf_core_phase_f.py b/python/tests/test_pf_core_phase_f.py new file mode 100644 index 0000000..b9d68ca --- /dev/null +++ b/python/tests/test_pf_core_phase_f.py @@ -0,0 +1,137 @@ +"""Tests for PF-Core Phase F: non-interference, contract discharge, CertifyEdge.""" + +from __future__ import annotations + +import json +import shutil +from pathlib import Path + +import pytest + +from pcs_core.lean_check import run_pfcore_lean_check +from pcs_core.pf_core_certifyedge import ( + certifyedge_mock_enabled, + run_certifyedge_check, + write_certifyedge_certificate, +) +from pcs_core.pf_core_lean_codegen import ( + generate_proof_obligation_file, + trace_has_contract_refs, + validate_contracts_before_codegen, +) +from pcs_core.pf_core_runtime import validate_tenant_isolation +from pcs_core.validate import validate_artifact, validate_file + +REPO = Path(__file__).resolve().parents[2] +CONTRACT_TRACE = REPO / "examples" / "pf-core-valid" / "contract_checked" / "trace.json" +CONTRACT_VIOLATION = REPO / "examples" / "pf-core-invalid" / "contract_violation" / "trace.json" +CROSS_TENANT = REPO / "examples" / "pf-core-invalid" / "cross_tenant_leak" / "trace.json" +FILE_READ_ALLOWED = REPO / "examples" / "pf-core-valid" / "file_read_allowed" / "trace.json" +LABTRUST_TRACE = REPO / "examples" / "pf-core-valid" / "labtrust_replay" / "trace.json" +LAKE_AVAILABLE = shutil.which("lake") is not None or shutil.which("wsl") is not None + + +def _load(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def test_non_interference_lean_module_has_theorems() -> None: + text = (REPO / "lean" / "PFCore" / "NonInterference.lean").read_text(encoding="utf-8") + assert "theorem cons_preserves_tenant_scope" in text + assert "theorem traceSafe_allowed_event_tenant_scoped" in text + assert "sorry" not in text + + +def test_contract_decide_lean_module_has_soundness() -> None: + text = (REPO / "lean" / "PFCore" / "ContractDecide.lean").read_text(encoding="utf-8") + assert "theorem contractPreD_sound" in text + assert "theorem traceSatisfiesContractSpecsD_sound" in text + assert "sorry" not in text + + +def test_validate_tenant_isolation_ok_on_allowed_fixture() -> None: + trace = _load(FILE_READ_ALLOWED) + assert validate_tenant_isolation(trace) == [] + + +def test_validate_tenant_isolation_fails_cross_tenant_leak() -> None: + trace = _load(CROSS_TENANT) + errors = validate_tenant_isolation(trace) + assert any("TenantIsolation" in err for err in errors) + + +def test_generated_proof_includes_contract_obligations(tmp_path: Path) -> None: + trace = _load(CONTRACT_TRACE) + assert trace_has_contract_refs(trace) + assert validate_contracts_before_codegen(trace, trace_path=CONTRACT_TRACE) == [] + proof_path = generate_proof_obligation_file(trace, tmp_path, trace_path=CONTRACT_TRACE) + text = proof_path.read_text(encoding="utf-8") + assert "ContractPreSpec" in text + assert "concrete_trace_satisfies_contract_" in text + assert "concrete_satisfies_contract_" in text + assert "validate-contracts only" not in text + + +def test_contract_violation_fails_before_codegen() -> None: + trace = _load(CONTRACT_VIOLATION) + errors = validate_contracts_before_codegen( + trace, + trace_path=CONTRACT_VIOLATION, + ) + assert any("ContractDecisionMismatch" in err for err in errors) + + +def test_lean_check_rejects_contract_violation(tmp_path: Path) -> None: + code, result = run_pfcore_lean_check( + CONTRACT_VIOLATION, + result_out_path=tmp_path / "result.json", + skip_build=True, + skip_lean_proof=True, + ) + assert code != 0 + issues = result.get("issues", []) + assert any(issue.get("code") == "ContractViolation" for issue in issues) + + +def test_certifyedge_mock_mode(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("PCS_CERTIFYEDGE_MOCK", "1") + assert certifyedge_mock_enabled() + result = run_certifyedge_check(LABTRUST_TRACE, "qc_release.temporal.safety") + assert result.ok + assert result.mock + assert result.certificate is not None + assert result.certificate["claim_class"] == "CertificateChecked" + validate_artifact(result.certificate, "PFCoreCertificate.v0") + + +def test_certifyedge_mock_writes_certificate(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("PCS_CERTIFYEDGE_MOCK", "1") + out = tmp_path / "cert.json" + write_certifyedge_certificate(LABTRUST_TRACE, "qc_release.temporal.safety", out) + cert = _load(out) + assert cert["claim_class"] == "CertificateChecked" + validate_file(out) + + +def test_certifyedge_without_cli_or_mock_fails(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("PCS_CERTIFYEDGE_MOCK", raising=False) + result = run_certifyedge_check(LABTRUST_TRACE, "qc_release.temporal.safety") + if shutil.which("certifyedge") is None: + assert not result.ok + assert "CertifyEdge" in result.message + + +@pytest.mark.skipif(not LAKE_AVAILABLE, reason="lake or WSL not available") +def test_contract_checked_generated_proof_compiles() -> None: + from pcs_core.lean_check import pfcore_generated_dir, run_lean_concrete_proof + + trace = _load(CONTRACT_TRACE) + proof_path = generate_proof_obligation_file( + trace, pfcore_generated_dir(), trace_path=CONTRACT_TRACE + ) + ok, detail = run_lean_concrete_proof(proof_path, skip_build=False) + assert ok, detail + + +def test_cross_tenant_invalid_fixture_in_examples_check() -> None: + validate_file(CROSS_TENANT) diff --git a/python/tests/test_pf_core_stage1.py b/python/tests/test_pf_core_stage1.py new file mode 100644 index 0000000..fb934d2 --- /dev/null +++ b/python/tests/test_pf_core_stage1.py @@ -0,0 +1,84 @@ +"""Tests for PF-Core Stage 1 trust boundary.""" + +from __future__ import annotations + +import pytest + +from pcs_core.pf_core_claims import audit_boundary, audit_claims, audit_lean_catalog +from pcs_core.registry_data import pf_core_artifact_types, registry_entries +from pcs_core.validate import detect_artifact_type + + +def test_pf_core_registry_entries() -> None: + entries = registry_entries() + assert len(pf_core_artifact_types()) == 10 + assert "AssumptionSet.v0" in entries + assert len(entries) > len(pf_core_artifact_types()) + trace = entries["PFCoreTrace.v0"] + assert trace["schema_owner"] == "pcs-core" + assert trace["release_mode_required"] is True + principal = entries["PFCorePrincipal.v0"] + assert principal["release_mode_required"] is False + + +def test_pf_core_artifact_types_set() -> None: + types = pf_core_artifact_types() + assert "PFCoreTrace.v0" in types + assert "PFCoreCertificate.v0" in types + assert "PFCoreRuntimeObservation.v0" in types + + +def test_explicit_artifact_type_requires_schema_const() -> None: + data = {"artifact_type": "ClaimArtifact.v0", "artifact_id": "x"} + assert detect_artifact_type(data) == "ClaimArtifact.v0" + + +def test_explicit_artifact_type_rejected_without_schema_const() -> None: + data = {"artifact_type": "PFCoreTrace.v0", "trace_id": "t-1"} + assert detect_artifact_type(data) == "PFCoreTrace.v0" + + +def test_audit_claims_passes_repo_docs() -> None: + assert audit_claims() == [] + + +def test_audit_boundary_passes() -> None: + assert audit_boundary() == [] + + +def test_audit_lean_catalog_passes() -> None: + errors = audit_lean_catalog() + assert errors == [], f"lean catalog audit failed: {errors}" + + +def test_forbidden_phrase_detected(tmp_path, monkeypatch: pytest.MonkeyPatch) -> None: + from pcs_core import pf_core_claims + + docs = tmp_path / "docs" + docs.mkdir() + (docs / "bad.md").write_text("This agent is safe forever.\n", encoding="utf-8") + + monkeypatch.setattr(pf_core_claims, "_scan_roots", lambda: [docs]) + violations = audit_claims() + assert len(violations) == 1 + assert violations[0].phrase == "agent is safe" + + +def test_trusted_catalog_includes_tool_use_and_witness_theorems() -> None: + from pcs_core.lean_catalog import ( + LEAN_THEOREM_CATALOG, + UNTRUSTED_LEAN_THEOREM_CATALOG, + ) + + assert "tool_trace_hash_matches_certificate" in LEAN_THEOREM_CATALOG + assert "witness_result_hashes_admissible" in LEAN_THEOREM_CATALOG + assert "tool_trace_hash_matches_certificate" not in UNTRUSTED_LEAN_THEOREM_CATALOG + + +def test_lean_check_disclaimer_constant() -> None: + from pcs_core.lean_check import LEAN_CHECK_DISCLAIMER, PCS_LEAN_CHECK_DISCLAIMER + + assert "LeanKernelChecked" in LEAN_CHECK_DISCLAIMER + assert "concrete Lean proof" in LEAN_CHECK_DISCLAIMER + assert "not Lean-backed" in PCS_LEAN_CHECK_DISCLAIMER + 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 new file mode 100644 index 0000000..62ae572 --- /dev/null +++ b/python/tests/test_pf_core_stage2.py @@ -0,0 +1,216 @@ +"""Tests for PF-Core Stage 2 schemas, compiler, and fixtures.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from pcs_core.pf_core_claims import audit_boundary +from pcs_core.pf_core_runtime import ( + ClaimClassOverclaim, + 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, +) +from pcs_core.registry_data import pf_core_artifact_types, registry_entries +from pcs_core.validate import ( + ARTIFACT_SCHEMAS, + ValidationError, + check_all_schemas, + detect_artifact_type, + load_pf_core_fixture_manifest, + validate_artifact, + validate_file, +) + +REPO = Path(__file__).resolve().parents[2] +VALID = REPO / "examples" / "pf-core-valid" +INVALID = REPO / "examples" / "pf-core-invalid" + +PF_CORE_SCHEMA_TYPES = [ + "PFCorePrincipal.v0", + "PFCoreCapability.v0", + "PFCoreResource.v0", + "PFCoreAction.v0", + "PFCoreEffect.v0", + "PFCoreDecision.v0", + "PFCoreEvent.v0", + "PFCoreTrace.v0", + "PFCoreContract.v0", + "PFCoreHandoff.v0", + "PFCoreRuntimeObservation.v0", + "PFCoreCertificate.v0", +] + + +def _load(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +@pytest.mark.parametrize("artifact_type", PF_CORE_SCHEMA_TYPES) +def test_pf_core_schema_registered(artifact_type: str) -> None: + assert artifact_type in ARTIFACT_SCHEMAS + + +def test_all_schemas_compile() -> None: + check_all_schemas() + + +@pytest.mark.parametrize("case_dir", sorted(VALID.iterdir()) if VALID.is_dir() else []) +def test_valid_pf_core_fixtures(case_dir: Path) -> None: + for path in sorted(case_dir.glob("*.json")): + if path.name == "manifest.json": + 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_tool_use_trace_compiles_to_pfcore_trace() -> None: + tool_use_trace = _load(VALID / "tool_use_trace_compiled" / "tool_use_trace.json") + expected = _load(VALID / "tool_use_trace_compiled" / "pfcore_trace.json") + compiled = compile_tool_use_trace_to_pfcore_trace(tool_use_trace) + assert compiled["artifact_type"] == "PFCoreTrace.v0" + assert len(compiled["events"]) == len(tool_use_trace["tool_calls"]) + assert compiled["trace_hash"] == expected["trace_hash"] + denied = [event for event in compiled["events"] if event["decision"] == "deny"] + assert len(denied) == 1 + assert denied[0]["event_id"] == "evt-002" + + +def test_denied_events_preserved_in_compilation() -> None: + tool_use_trace = _load(VALID / "tool_use_trace_compiled" / "tool_use_trace.json") + compiled = compile_tool_use_trace_to_pfcore_trace(tool_use_trace) + validate_denied_events_preserved(tool_use_trace, compiled) + + +def test_trace_hash_chain_validation_passes_for_valid_trace() -> None: + trace = _load(VALID / "file_read_allowed" / "trace.json") + assert validate_pfcore_trace_hash_chain(trace) == [] + + +def test_explicit_artifact_type_detection_pfcore() -> None: + data = {"artifact_type": "PFCoreTrace.v0", "trace_id": "t-1"} + assert detect_artifact_type(data) == "PFCoreTrace.v0" + + +def test_registry_audit_includes_pf_core_entries() -> None: + assert audit_boundary() == [] + entries = registry_entries() + for artifact_type in pf_core_artifact_types(): + assert artifact_type in entries + assert artifact_type in ARTIFACT_SCHEMAS + + +def test_compile_runtime_observation_produces_event() -> None: + observation = _load(VALID / "file_read_allowed" / "observation.json") + event = compile_runtime_observation_to_event(observation) + assert event["artifact_type"] == "PFCoreEvent.v0" + assert event["decision"] == "allow" + assert validate_pfcore_trace_hash_chain({"events": [event]}) == [] + + +def test_cross_tenant_allow_becomes_deny() -> None: + observation = _load(VALID / "file_read_denied_cross_tenant" / "observation.json") + event = compile_runtime_observation_to_event(observation) + assert event["decision"] == "deny" + + +def test_claim_class_overclaim_in_semantic_validation() -> None: + trace = _load(INVALID / "claim_class_overclaim" / "trace.json") + with pytest.raises(ValidationError) as exc: + validate_artifact(trace, "PFCoreTrace.v0") + assert any("ClaimClassOverclaim" in err for err in exc.value.errors) + + +def test_handoff_subset_authority_valid() -> None: + handoff = _load(VALID / "handoff_subset_authority" / "handoff.json") + validate_handoff_authority(handoff) + + +def test_network_denied_event_decision() -> None: + event = _load(VALID / "network_denied" / "event.json") + assert event["decision"] == "deny" + + +def test_claim_class_overclaim_raises() -> None: + with pytest.raises(ClaimClassOverclaim): + from pcs_core.pf_core_runtime import _assert_claim_class_allowed + + _assert_claim_class_allowed("LeanKernelChecked") diff --git a/python/tests/test_pf_core_stage3.py b/python/tests/test_pf_core_stage3.py new file mode 100644 index 0000000..5c4bec4 --- /dev/null +++ b/python/tests/test_pf_core_stage3.py @@ -0,0 +1,350 @@ +"""Tests for PF-Core Stage 3 Lean kernel and lean-check integration.""" + + + +from __future__ import annotations + + + +import json + +from pathlib import Path + + + +import pytest + + + +from pcs_core.lean_catalog import PF_CORE_THEOREM_CATALOG + +from pcs_core.lean_check import ( + + LEAN_CHECK_DISCLAIMER, + + PF_CORE_ASSUMPTION_REFS, + + audit_pfcore_lean_no_sorry, + + check_pfcore_trace_lean_semantics, + + event_safe_d, + + run_pfcore_lean_check, + + trace_safe_d, + +) + +from pcs_core.pf_core_claims import ( + + audit_boundary, + + audit_claims, + + audit_lean_catalog, + +) + +from pcs_core.pf_core_runtime import ( + + compile_tool_use_trace_to_pfcore_trace, + + expand_principal_capabilities, + + principal_capabilities_explicit, + +) + +from pcs_core.validate import validate_file + + + +REPO = Path(__file__).resolve().parents[2] + +VALID_TRACE = REPO / "examples" / "pf-core-valid" / "tool_use_trace_compiled" / "pfcore_trace.json" + +TOOL_USE_TRACE = REPO / "examples" / "pf-core-valid" / "tool_use_trace_compiled" / "tool_use_trace.json" + +PF_CORE_LEAN = REPO / "lean" / "PFCore" + + + + + +def _load(path: Path) -> dict: + + return json.loads(path.read_text(encoding="utf-8")) + + + + + +def test_pfcore_lean_directory_exists() -> None: + + assert PF_CORE_LEAN.is_dir() + + expected = { + + "Basic.lean", + + "Principal.lean", + + "Capability.lean", + + "Resource.lean", + + "Action.lean", + + "Event.lean", + + "Trace.lean", + + "Contract.lean", + + "Handoff.lean", + + "Certificate.lean", + + "Soundness.lean", + + "Theorems.lean", + + "TraceCheck.lean", + + } + + assert expected <= {path.name for path in PF_CORE_LEAN.glob("*.lean")} + + + + + +def test_pfcore_lean_catalog_matches_sources() -> None: + + errors = audit_lean_catalog() + + assert errors == [], f"PF-Core lean catalog audit failed: {errors}" + + + + + +def test_pfcore_catalog_includes_trace_safety_theorems() -> None: + + required = { + + "traceSafeD_sound", + + "allowed_event_has_allowed_action", + + "every_allowed_event_in_safe_trace_is_allowed", + + "handoff_does_not_expand_authority", + + } + + assert required <= PF_CORE_THEOREM_CATALOG + + + + + +def test_no_sorry_audit_passes() -> None: + + assert audit_pfcore_lean_no_sorry() == [] + + + + + +def test_valid_trace_passes_event_and_trace_deciders() -> None: + + trace = _load(VALID_TRACE) + + events = trace["events"] + + assert all(event_safe_d(event) for event in events) + + assert trace_safe_d(events) + + assert check_pfcore_trace_lean_semantics(trace) == [] + + + + + +def test_unsafe_allow_event_fails_decider() -> None: + + trace = _load(VALID_TRACE) + + event = dict(trace["events"][0]) + + event["decision"] = "allow" + + event["principal"] = dict(event["principal"]) + + event["principal"]["capabilities"] = [] + + event["principal"]["roles"] = [] + + assert not event_safe_d(event) + + + + + +def test_role_expansion_produces_explicit_capabilities_in_compiled_trace() -> None: + + tool_use_trace = _load(TOOL_USE_TRACE) + + compiled = compile_tool_use_trace_to_pfcore_trace(tool_use_trace) + + expected_caps = expand_principal_capabilities({"roles": ["agent"], "capabilities": []}) + + for event in compiled["events"]: + + principal = event["principal"] + + assert principal["capabilities"] == expected_caps + + assert principal_capabilities_explicit(principal) + + + + + +def test_roles_without_explicit_capabilities_fail_lean_semantics() -> None: + + trace = _load(VALID_TRACE) + + event = dict(trace["events"][0]) + + principal = dict(event["principal"]) + + principal["capabilities"] = [] + + event["principal"] = principal + + mutated = dict(trace) + + mutated["events"] = [event] + + issues = check_pfcore_trace_lean_semantics(mutated) + + assert any(issue.code == "PrincipalCapabilityMismatch" for issue in issues) + + + + + +@pytest.mark.parametrize("skip_build", [True]) + +def test_pfcore_lean_check_emits_runtime_checked_when_build_skipped( + + tmp_path: Path, skip_build: bool + +) -> None: + + out = tmp_path / "PFCoreCertificate.v0.json" + + code, result = run_pfcore_lean_check(VALID_TRACE, out_path=out, skip_build=skip_build) + + assert code == 0, result + + assert result["status"] == "DecidersPassed" + + assert result["claim_class"] == "RuntimeChecked" + + assert result["disclaimer"] == LEAN_CHECK_DISCLAIMER + + assert result["theorems_checked"] == sorted(PF_CORE_THEOREM_CATALOG) + + assert result["assumption_refs"] == PF_CORE_ASSUMPTION_REFS + + cert = json.loads(out.read_text(encoding="utf-8")) + + assert cert["artifact_type"] == "PFCoreCertificate.v0" + + assert cert["claim_class"] == "RuntimeChecked" + + assert cert["theorems_checked"] == sorted(PF_CORE_THEOREM_CATALOG) + + assert cert["disclaimer"] == LEAN_CHECK_DISCLAIMER + + assert "proof_ref" not in cert + + + + + +def test_pfcore_lean_check_never_emits_unqualified_proof_checked(tmp_path: Path) -> None: + + _, result = run_pfcore_lean_check(VALID_TRACE, out_path=tmp_path / "cert.json", skip_build=True) + + assert result["status"] != "ProofChecked" + + assert result["claim_class"] in {"RuntimeChecked", "LeanKernelChecked"} + + + + + +def test_pf_core_full_pipeline_on_valid_tool_use_example(tmp_path: Path) -> None: + + assert audit_claims() == [] + + assert audit_boundary() == [] + + assert audit_lean_catalog() == [] + + assert audit_pfcore_lean_no_sorry() == [] + + + + tool_use_trace = _load(TOOL_USE_TRACE) + + compiled = compile_tool_use_trace_to_pfcore_trace(tool_use_trace) + + validate_file(VALID_TRACE) + + + + compiled_path = tmp_path / "compiled_trace.json" + + compiled_path.write_text(json.dumps(compiled, indent=2), encoding="utf-8") + + validate_file(compiled_path) + + + + out = tmp_path / "PFCoreCertificate.v0.json" + + code, result = run_pfcore_lean_check(compiled_path, out_path=out, skip_build=True) + + assert code == 0, result + + assert result["claim_class"] == "RuntimeChecked" + + validate_file(out) + + + + + +def test_lakefile_declares_pfcore_target() -> None: + + lakefile = (REPO / "lean" / "lakefile.lean").read_text(encoding="utf-8") + + assert "lean_lib PFCore" in lakefile + + assert "PFCore" in lakefile + + + + + +def test_pcs_root_module_exists() -> None: + + assert (REPO / "lean" / "PCS.lean").is_file() + diff --git a/python/tests/test_pf_core_stage4.py b/python/tests/test_pf_core_stage4.py new file mode 100644 index 0000000..d4b69ca --- /dev/null +++ b/python/tests/test_pf_core_stage4.py @@ -0,0 +1,200 @@ +"""Tests for PF-Core Stage 4 Lean bridge (codegen + concrete proof).""" + +from __future__ import annotations + +import json +import shutil +from pathlib import Path + +import pytest + +from pcs_core.lean_check import ( + LEAN_CHECK_DISCLAIMER, + check_pfcore_trace_lean_semantics, + run_pfcore_lean_check, + run_lean_concrete_proof, + run_lean_library_build, +) +from pcs_core.pf_core_lean_codegen import ( + generate_proof_obligation_file, + trace_to_lean, +) +from pcs_core.validate import validate_file, validate_schema + +REPO = Path(__file__).resolve().parents[2] +VALID_TRACE = REPO / "examples" / "pf-core-valid" / "tool_use_trace_compiled" / "pfcore_trace.json" +EMPTY_TRACE = REPO / "examples" / "pf-core-valid" / "empty_trace" / "trace.json" +LAKE_AVAILABLE = shutil.which("lake") is not None or ( + shutil.which("wsl") is not None +) + + +def _load(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def test_lean_check_result_schema_registered() -> None: + sample = { + "schema_version": "v0", + "artifact_type": "LeanCheckResult.v0", + "status": "Rejected", + "claim_class": "OutOfScope", + "assumption_refs": ["docs/pf-core/assumptions.md"], + "theorems_checked": ["traceSafeD_sound"], + "lean_build_status": {"ok": False, "target": "PFCore", "detail": "skipped"}, + "disclaimer": LEAN_CHECK_DISCLAIMER, + "signature_or_digest": "sha256:" + "0" * 64, + } + assert validate_schema(sample, "LeanCheckResult.v0") == [] + + +def test_trace_to_lean_generates_kernel_constructs() -> None: + trace = _load(VALID_TRACE) + source = trace_to_lean(trace) + assert "def trace_trace_agent_safety_001 : Trace" in source or "Trace.cons" in source + assert "Decision.allow" in source or "Decision.deny" in source + assert "Principal :=" in source + assert "Action :=" in source + + +def test_generate_proof_obligation_file_writes_theorem(tmp_path: Path) -> None: + trace = _load(VALID_TRACE) + proof_path = generate_proof_obligation_file(trace, tmp_path, trace_path=VALID_TRACE) + text = proof_path.read_text(encoding="utf-8") + assert "theorem concrete_trace_safe" in text + assert "theorem concrete_event_safe_" in text + assert "decide" in text + assert "import PFCore.TraceCheck" in text + + +def test_empty_trace_codegen(tmp_path: Path) -> None: + trace = _load(EMPTY_TRACE) + source = trace_to_lean(trace) + assert "Trace.empty" in source + proof_path = generate_proof_obligation_file(trace, tmp_path) + assert "theorem concrete_trace_safe" in proof_path.read_text(encoding="utf-8") + + +def test_invalid_trace_fails_before_lean_proof() -> None: + trace = _load(VALID_TRACE) + event = dict(trace["events"][0]) + event["decision"] = "allow" + event["principal"] = dict(event["principal"]) + event["principal"]["capabilities"] = [] + event["principal"]["roles"] = [] + mutated = dict(trace) + mutated["events"] = [event] + issues = check_pfcore_trace_lean_semantics(mutated) + assert any(issue.code == "EventUnsafe" for issue in issues) + + +@pytest.mark.parametrize("skip_build", [True]) +def test_skip_build_emits_runtime_checked(tmp_path: Path, skip_build: bool) -> None: + out = tmp_path / "PFCoreCertificate.v0.json" + result_out = tmp_path / "LeanCheckResult.v0.json" + code, result = run_pfcore_lean_check( + VALID_TRACE, + out_path=out, + result_out_path=result_out, + skip_build=skip_build, + ) + assert code == 0, result + assert result["status"] == "DecidersPassed" + assert result["claim_class"] == "RuntimeChecked" + assert result["lean_proof_checked"] is False + cert = json.loads(out.read_text(encoding="utf-8")) + assert cert["claim_class"] == "RuntimeChecked" + assert "proof_term_ref" not in cert + validate_file(result_out) + validate_file(out) + + +@pytest.mark.parametrize("skip_lean_proof", [True]) +def test_skip_lean_proof_emits_runtime_checked(tmp_path: Path, skip_lean_proof: bool) -> None: + code, result = run_pfcore_lean_check( + VALID_TRACE, + out_path=tmp_path / "cert.json", + skip_build=True, + skip_lean_proof=skip_lean_proof, + ) + assert code == 0, result + assert result["claim_class"] == "RuntimeChecked" + assert result["status"] == "DecidersPassed" + assert not any(item.get("kind") == "ConcreteTraceSafe" for item in result.get("obligations", [])) + + +@pytest.mark.skipif(not LAKE_AVAILABLE, reason="lake or WSL not available") +def test_concrete_lean_proof_passes_for_valid_trace() -> None: + from pcs_core.lean_check import pfcore_generated_dir + + trace = _load(VALID_TRACE) + proof_path = generate_proof_obligation_file(trace, pfcore_generated_dir()) + ok, detail = run_lean_concrete_proof(proof_path, skip_build=False) + assert ok, detail + + +@pytest.mark.skipif(not LAKE_AVAILABLE, reason="lake or WSL not available") +def test_full_pipeline_emits_lean_kernel_checked(tmp_path: Path) -> None: + out = tmp_path / "PFCoreCertificate.v0.json" + result_out = tmp_path / "LeanCheckResult.v0.json" + code, result = run_pfcore_lean_check( + VALID_TRACE, + out_path=out, + result_out_path=result_out, + ) + assert code == 0, result + assert result["status"] == "LeanProofChecked" + assert result["claim_class"] == "LeanKernelChecked" + assert result["lean_proof_checked"] is True + assert any( + item.get("kind") == "ConcreteTraceSafe" and item.get("passed") is True + for item in result["obligations"] + ) + cert = json.loads(out.read_text(encoding="utf-8")) + assert cert["claim_class"] == "LeanKernelChecked" + assert cert["lean_proof_checked"] is True + assert cert.get("proof_term_ref", "").startswith("lean/PFCore/Generated/") + assert cert.get("lean_environment_hash", "").startswith("sha256:") + validate_file(result_out) + validate_file(out) + + +def test_lean_kernel_checked_not_emitted_when_proof_skipped(tmp_path: Path) -> None: + _, result = run_pfcore_lean_check( + VALID_TRACE, + out_path=tmp_path / "cert.json", + skip_build=True, + ) + assert result["claim_class"] != "LeanKernelChecked" + assert result["status"] == "DecidersPassed" + + +def test_pfcore_certificate_schema_accepts_obligations() -> None: + cert = { + "schema_version": "v0", + "artifact_type": "PFCoreCertificate.v0", + "certificate_id": "pfcore-cert-test", + "trace_hash": "sha256:" + "0" * 64, + "contract_hash": "sha256:" + "0" * 64, + "policy_hash": "sha256:" + "0" * 64, + "claim_class": "RuntimeChecked", + "checker": "pcs-core", + "checker_version": "0.1.0", + "assumption_refs": ["docs/pf-core/assumptions.md"], + "obligations": [ + {"kind": "TraceSafeDeciderSound", "theorem": "traceSafeD_sound", "passed": True} + ], + "lean_proof_checked": False, + "event_count": 0, + "source_repo": "https://github.com/example/pcs-core", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:" + "0" * 64, + } + assert validate_schema(cert, "PFCoreCertificate.v0") == [] + + +def test_lake_build_pfcore_succeeds() -> None: + if not LAKE_AVAILABLE: + pytest.skip("lake or WSL not available") + ok, detail = run_lean_library_build(target="PFCore") + assert ok, detail diff --git a/python/tests/test_pf_core_stage5.py b/python/tests/test_pf_core_stage5.py new file mode 100644 index 0000000..78b1fd2 --- /dev/null +++ b/python/tests/test_pf_core_stage5.py @@ -0,0 +1,184 @@ +"""Tests for PF-Core Stage 5 claim-class completeness.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from pcs_core.pf_core_certificate import attach_external_certificate_check +from pcs_core.pf_core_labtrust_adapter import normalize_labtrust_release +from pcs_core.pf_core_replay import replay_trace, run_replay_trace +from pcs_core.registry_data import ( + deferred_registry_obligations, + enforce_assumption_declared, + registry_entries, +) +from pcs_core.validate import validate_file, validate_semantics + +REPO = Path(__file__).resolve().parents[2] +VALID_TRACE = REPO / "examples" / "pf-core-valid" / "tool_use_trace_compiled" / "pfcore_trace.json" +TOOL_USE = REPO / "examples" / "pf-core-valid" / "tool_use_trace_compiled" / "tool_use_trace.json" +LABTRUST_TRACE = REPO / "examples" / "pf-core-valid" / "labtrust_replay" / "trace.json" +LABTRUST_TC = REPO / "examples" / "labtrust" / "trace_certificate.valid.json" +LABTRUST_SCB = REPO / "examples" / "labtrust" / "science_claim_bundle.certified.valid.json" + + +def _load(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def test_valid_fixture_replay_passes() -> None: + result = replay_trace(VALID_TRACE) + assert result.match is True + assert result.original_trace_hash == result.recomputed_trace_hash + assert result.diffs == [] + + +def test_tampered_hash_fails() -> None: + trace = _load(VALID_TRACE) + trace["trace_hash"] = "sha256:" + "f" * 64 + tampered = VALID_TRACE.parent / "tampered_trace.json" + tampered.write_text(json.dumps(trace, indent=2), encoding="utf-8") + try: + result = replay_trace(tampered) + assert result.match is False + assert result.original_trace_hash != result.recomputed_trace_hash + assert any(diff.path == "trace_hash" for diff in result.diffs) + finally: + tampered.unlink(missing_ok=True) + + +def test_tool_use_trace_compiled_replays_from_source() -> None: + result = replay_trace(VALID_TRACE, TOOL_USE) + assert result.match is True + assert result.diffs == [] + + +def test_run_replay_trace_emits_certificate(tmp_path: Path) -> None: + out = tmp_path / "PFCoreCertificate.v0.json" + result_out = tmp_path / "LeanCheckResult.v0.json" + code, result = run_replay_trace( + VALID_TRACE, + out_path=out, + result_out_path=result_out, + ) + assert code == 0 + assert result["claim_class"] == "ReplayValidated" + assert result["replay_match"] is True + cert = json.loads(out.read_text(encoding="utf-8")) + assert cert["claim_class"] == "ReplayValidated" + assert cert["replay_match"] is True + validate_file(out) + validate_file(result_out) + + +def test_deferred_registry_obligations_present() -> None: + deferred = deferred_registry_obligations("PFCoreCertificate.v0") + check_ids = {item["check_id"] for item in deferred} + assert "lean_kernel_proof" in check_ids + assert "lean_library_build" in check_ids + + +def test_certificate_with_deferred_check_and_no_assumption_refs_fails() -> None: + cert = { + "schema_version": "v0", + "artifact_type": "PFCoreCertificate.v0", + "certificate_id": "pfcore-cert-test", + "trace_hash": "sha256:" + "0" * 64, + "contract_hash": "sha256:" + "0" * 64, + "policy_hash": "sha256:" + "0" * 64, + "claim_class": "AssumptionDeclared", + "checker": "pcs-core", + "checker_version": "0.1.0", + "assumption_refs": [], + "event_count": 0, + "source_repo": "https://github.com/example/pcs-core", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:" + "0" * 64, + } + issues = enforce_assumption_declared(cert, registry_entries()["PFCoreCertificate.v0"]) + assert issues + semantic = validate_semantics(cert, "PFCoreCertificate.v0") + assert any("assumption_refs" in err for err in semantic) + + +def test_certificate_with_deferred_check_and_refs_passes_as_assumption_declared() -> None: + cert = { + "schema_version": "v0", + "artifact_type": "PFCoreCertificate.v0", + "certificate_id": "pfcore-cert-test", + "trace_hash": "sha256:" + "0" * 64, + "contract_hash": "sha256:" + "0" * 64, + "policy_hash": "sha256:" + "0" * 64, + "claim_class": "AssumptionDeclared", + "checker": "pcs-core", + "checker_version": "0.1.0", + "assumption_refs": ["docs/pf-core/assumptions.md", "as-labtrust-qc-v0.1"], + "event_count": 0, + "source_repo": "https://github.com/example/pcs-core", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:" + "0" * 64, + } + issues = enforce_assumption_declared(cert, registry_entries()["PFCoreCertificate.v0"]) + assert issues == [] + + +def test_lean_kernel_checked_forbidden_when_deferred_skipped() -> None: + cert = { + "schema_version": "v0", + "artifact_type": "PFCoreCertificate.v0", + "certificate_id": "pfcore-cert-test", + "trace_hash": "sha256:" + "0" * 64, + "contract_hash": "sha256:" + "0" * 64, + "policy_hash": "sha256:" + "0" * 64, + "claim_class": "LeanKernelChecked", + "checker": "pcs-core", + "checker_version": "0.1.0", + "proof_term_ref": "lean/PFCore/Generated/proof.lean", + "proof_ref": "lean/PFCore/Generated/proof.lean", + "lean_proof_checked": True, + "lean_build_status": {"ok": False, "target": "PFCore", "detail": "skipped"}, + "assumption_refs": ["docs/pf-core/assumptions.md"], + "event_count": 1, + "source_repo": "https://github.com/example/pcs-core", + "source_commit": "abc1234567890abc1234567890abc1234567890", + "signature_or_digest": "sha256:" + "0" * 64, + } + issues = enforce_assumption_declared(cert, registry_entries()["PFCoreCertificate.v0"]) + assert any("LeanKernelChecked" in issue for issue in issues) + + +def test_attach_certificate_check_emits_certificate_checked(tmp_path: Path) -> None: + trace = _load(VALID_TRACE) + cert = attach_external_certificate_check( + trace, + checker="certifyedge", + checker_version="0.1.0", + attestation_ref="examples/labtrust/trace_certificate.valid.json", + ) + out = tmp_path / "cert.json" + out.write_text(json.dumps(cert, indent=2), encoding="utf-8") + assert cert["claim_class"] == "CertificateChecked" + validate_file(out) + + +def test_labtrust_adapter_trace_validates() -> None: + validate_file(LABTRUST_TRACE) + + +def test_labtrust_adapter_matches_fixture() -> None: + tc = _load(LABTRUST_TC) + scb = _load(LABTRUST_SCB) + receipt = scb["runtime_receipts"][0] + adapted = normalize_labtrust_release(tc, receipt) + expected = _load(LABTRUST_TRACE) + assert adapted["trace_hash"] == expected["trace_hash"] + assert adapted["contract_hash"] == expected["contract_hash"] + assert adapted["policy_hash"] == expected["policy_hash"] + + +def test_labtrust_replay_passes() -> None: + result = replay_trace(LABTRUST_TRACE) + assert result.match is True diff --git a/python/tests/test_pf_core_stage7.py b/python/tests/test_pf_core_stage7.py new file mode 100644 index 0000000..f25de58 --- /dev/null +++ b/python/tests/test_pf_core_stage7.py @@ -0,0 +1,65 @@ +"""Tests for PF-Core Stage 7 semantic depth (contracts, handoff, resource scope).""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from pcs_core.pf_core_contract import load_contract, validate_trace_contracts +from pcs_core.pf_core_replay import replay_trace +from pcs_core.pf_core_runtime import ( + HandoffAuthorityExpansion, + compile_tool_use_trace_to_pfcore_trace, + validate_pfcore_trace_hash_chain, +) +from pcs_core.validate import check_pf_core_valid_fixtures, validate_file + +REPO = Path(__file__).resolve().parents[2] +CONTRACT_VALID = REPO / "examples" / "pf-core-valid" / "contract_checked" +CONTRACT_INVALID = REPO / "examples" / "pf-core-invalid" / "contract_violation" +RESOURCE_INVALID = REPO / "examples" / "pf-core-invalid" / "resource_scope_violation" +HANDOFF_COMPILE_INVALID = REPO / "examples" / "pf-core-invalid" / "handoff_compile_expansion" +LABTRUST_REPLAY = REPO / "examples" / "pf-core-valid" / "labtrust_replay" / "trace.json" + + +def _load(path: Path) -> dict: + return json.loads(path.read_text(encoding="utf-8")) + + +def test_contract_checked_fixture_validates() -> None: + validate_file(CONTRACT_VALID / "contract.json") + validate_file(CONTRACT_VALID / "trace.json") + + +def test_contract_satisfaction_passes_for_valid_fixture() -> None: + trace = _load(CONTRACT_VALID / "trace.json") + contract = load_contract(CONTRACT_VALID / "contract.json") + issues = validate_trace_contracts(trace, {contract["contract_id"]: contract}) + assert issues == [] + + +def test_contract_violation_detected() -> None: + trace = _load(CONTRACT_INVALID / "trace.json") + contract = load_contract(CONTRACT_INVALID / "contracts" / "contract.json") + issues = validate_trace_contracts(trace, {contract["contract_id"]: contract}) + assert any(issue.code == "ContractDecisionMismatch" for issue in issues) + + +def test_resource_scope_violation_detected() -> None: + trace = _load(RESOURCE_INVALID / "trace.json") + errors = validate_pfcore_trace_hash_chain(trace) + assert any("ResourceScopeViolation" in err for err in errors) + + +def test_handoff_compile_expansion_rejected() -> None: + tool_use = _load(HANDOFF_COMPILE_INVALID / "tool_use_trace.json") + with pytest.raises(HandoffAuthorityExpansion): + compile_tool_use_trace_to_pfcore_trace(tool_use) + + +def test_labtrust_replay_fixture_in_examples_check() -> None: + check_pf_core_valid_fixtures() + result = replay_trace(LABTRUST_REPLAY) + assert result.match is True diff --git a/rust/crates/pcs-core/src/validation.rs b/rust/crates/pcs-core/src/validation.rs index fa2f800..4cb8124 100644 --- a/rust/crates/pcs-core/src/validation.rs +++ b/rust/crates/pcs-core/src/validation.rs @@ -32,6 +32,21 @@ const ARTIFACT_SCHEMAS: &[(&str, &str)] = &[ "SignedScienceClaimBundle.v0", "SignedScienceClaimBundle.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"), ("ReleaseManifest.v0", "ReleaseManifest.v0.schema.json"), ("HandoffManifest.v0", "HandoffManifest.v0.schema.json"), ( @@ -109,8 +124,39 @@ const PROTOCOL_ARTIFACT_TYPES: &[&str] = &[ "MigrationReport.v0", ]; +const EXPLICIT_ARTIFACT_TYPES: &[&str] = &[ + "PFCorePrincipal.v0", + "PFCoreCapability.v0", + "PFCoreResource.v0", + "PFCoreAction.v0", + "PFCoreEffect.v0", + "PFCoreDecision.v0", + "PFCoreEvent.v0", + "PFCoreTrace.v0", + "PFCoreContract.v0", + "PFCoreHandoff.v0", + "PFCoreRuntimeObservation.v0", + "PFCoreCertificate.v0", + "LeanCheckResult.v0", + "ToolUseTrace.v0", + "PCSBridgeCertificate.v0", + "ClaimArtifact.v0", +]; + +fn explicit_artifact_type(value: &str) -> Option<&'static str> { + EXPLICIT_ARTIFACT_TYPES + .iter() + .copied() + .find(|artifact_type| *artifact_type == value) +} + pub fn detect_artifact_type(value: &Value) -> Option<&'static str> { let obj = value.as_object()?; + if let Some(explicit) = obj.get("artifact_type").and_then(|v| v.as_str()) { + if let Some(artifact_type) = explicit_artifact_type(explicit) { + return Some(artifact_type); + } + } if obj.get("schema_version") == Some(&Value::String("v0".into())) && obj.get("registry_id").and_then(|v| v.as_str()).is_some() && obj.get("metrics").map(|v| v.is_object()).unwrap_or(false) @@ -620,6 +666,32 @@ mod tests { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../test_vectors/hash") } + #[test] + fn pf_core_explicit_artifact_types_detect() { + let repo = examples_dir(); + let cases = [ + ( + "pf-core-valid/tool_use_trace_compiled/pfcore_trace.json", + "PFCoreTrace.v0", + ), + ( + "pf-core-valid/assumption_declared/certificate.json", + "PFCoreCertificate.v0", + ), + ]; + for (rel, expected) in cases { + let path = repo.join(rel); + let value: Value = + serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap(); + assert_eq!( + detect_artifact_type(&value), + Some(expected), + "{rel}" + ); + } + } + + #[test] #[test] fn valid_examples_pass_jsonschema_and_semantics() { for entry in WalkDir::new(examples_dir()) diff --git a/schemas/LeanCheckResult.v0.schema.json b/schemas/LeanCheckResult.v0.schema.json index 258124d..c8c15a5 100644 --- a/schemas/LeanCheckResult.v0.schema.json +++ b/schemas/LeanCheckResult.v0.schema.json @@ -2,55 +2,266 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://pcs.sentinelops.ci/schemas/LeanCheckResult.v0.schema.json", "title": "LeanCheckResult.v0", - "type": "object", - "required": [ - "schema_version", - "check_id", - "proof_obligation_id", - "lean_module", - "lean_theorem", - "status", - "checked_at", - "lean_version", - "source_repo", - "source_commit", - "failure_reason", - "signature_or_digest" - ], - "additionalProperties": false, - "properties": { - "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, - "check_id": { "type": "string", "minLength": 1 }, - "proof_obligation_id": { "type": "string", "minLength": 1 }, - "lean_module": { "type": "string", "minLength": 1 }, - "lean_theorem": { "type": "string", "minLength": 1 }, - "status": { "$ref": "#/$defs/lean_check_status" }, - "checked_at": { "$ref": "common.defs.json#/$defs/iso8601_datetime" }, - "lean_version": { "type": "string", "minLength": 1 }, - "source_repo": { "type": "string", "format": "uri" }, - "source_commit": { "$ref": "common.defs.json#/$defs/git_commit" }, - "failure_reason": { "type": "string" }, - "obligation_results": { - "type": "array", - "items": { "$ref": "#/$defs/obligation_check_result" } + "oneOf": [ + { + "$ref": "#/$defs/pcs_proof_obligation_result" }, - "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } - }, + { + "$ref": "#/$defs/pf_core_lean_check_result" + } + ], "$defs": { "lean_check_status": { "type": "string", - "enum": ["ProofChecked", "Rejected", "Stale"] + "enum": [ + "ProofChecked", + "Rejected", + "Stale" + ] }, "obligation_check_result": { "type": "object", - "required": ["obligation_id", "kind", "status", "lean_theorem"], + "required": [ + "obligation_id", + "kind", + "status", + "lean_theorem" + ], + "additionalProperties": false, + "properties": { + "obligation_id": { + "type": "string", + "minLength": 1 + }, + "kind": { + "type": "string", + "minLength": 1 + }, + "status": { + "type": "string", + "enum": [ + "passed", + "failed" + ] + }, + "lean_theorem": { + "type": "string", + "minLength": 1 + }, + "failure_reason": { + "type": "string" + } + } + }, + "pcs_proof_obligation_result": { + "type": "object", + "required": [ + "schema_version", + "check_id", + "proof_obligation_id", + "lean_module", + "lean_theorem", + "status", + "checked_at", + "lean_version", + "source_repo", + "source_commit", + "failure_reason", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { + "$ref": "common.defs.json#/$defs/schema_version" + }, + "check_id": { + "type": "string", + "minLength": 1 + }, + "proof_obligation_id": { + "type": "string", + "minLength": 1 + }, + "lean_module": { + "type": "string", + "minLength": 1 + }, + "lean_theorem": { + "type": "string", + "minLength": 1 + }, + "status": { + "$ref": "#/$defs/lean_check_status" + }, + "checked_at": { + "$ref": "common.defs.json#/$defs/iso8601_datetime" + }, + "lean_version": { + "type": "string", + "minLength": 1 + }, + "source_repo": { + "type": "string", + "format": "uri" + }, + "source_commit": { + "$ref": "common.defs.json#/$defs/git_commit" + }, + "failure_reason": { + "type": "string" + }, + "obligation_results": { + "type": "array", + "items": { + "$ref": "#/$defs/obligation_check_result" + } + }, + "signature_or_digest": { + "$ref": "common.defs.json#/$defs/hex_digest" + } + } + }, + "pf_core_lean_check_result": { + "type": "object", + "required": [ + "schema_version", + "artifact_type", + "status", + "claim_class", + "assumption_refs", + "theorems_checked", + "lean_build_status", + "disclaimer", + "signature_or_digest" + ], "additionalProperties": false, "properties": { - "obligation_id": { "type": "string", "minLength": 1 }, - "kind": { "type": "string", "minLength": 1 }, - "status": { "type": "string", "enum": ["passed", "failed"] }, - "lean_theorem": { "type": "string", "minLength": 1 }, - "failure_reason": { "type": "string" } + "schema_version": { + "$ref": "common.defs.json#/$defs/schema_version" + }, + "artifact_type": { + "const": "LeanCheckResult.v0" + }, + "status": { + "type": "string", + "enum": [ + "DecidersPassed", + "LeanProofChecked", + "ReplayValidated", + "Rejected", + "Stale" + ] + }, + "claim_class": { + "$ref": "pf_core.defs.json#/$defs/claim_class" + }, + "trace_path": { + "type": "string", + "minLength": 1 + }, + "issues": { + "type": "array", + "items": { + "type": "object", + "required": [ + "code", + "message" + ], + "additionalProperties": false, + "properties": { + "code": { + "type": "string", + "minLength": 1 + }, + "message": { + "type": "string", + "minLength": 1 + }, + "path": { + "type": "string" + } + } + } + }, + "obligations": { + "type": "array", + "items": { + "$ref": "pf_core.defs.json#/$defs/lean_obligation" + } + }, + "assumption_refs": { + "$ref": "common.defs.json#/$defs/ref_list" + }, + "theorems_checked": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "lean_build_status": { + "type": "object", + "required": [ + "ok", + "target" + ], + "additionalProperties": false, + "properties": { + "ok": { + "type": "boolean" + }, + "target": { + "type": "string", + "minLength": 1 + }, + "detail": { + "type": "string" + } + } + }, + "lean_environment_hash": { + "$ref": "common.defs.json#/$defs/hex_digest" + }, + "lean_proof_checked": { + "type": "boolean" + }, + "replay_match": { + "type": "boolean" + }, + "original_trace_hash": { + "$ref": "common.defs.json#/$defs/hex_digest" + }, + "recomputed_trace_hash": { + "$ref": "common.defs.json#/$defs/hex_digest" + }, + "no_sorry_audit": { + "type": "object", + "required": [ + "ok", + "errors" + ], + "additionalProperties": false, + "properties": { + "ok": { + "type": "boolean" + }, + "errors": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "disclaimer": { + "type": "string" + }, + "certificate": { + "$ref": "PFCoreCertificate.v0.schema.json" + }, + "signature_or_digest": { + "$ref": "common.defs.json#/$defs/hex_digest" + } } } } diff --git a/schemas/PCSBridgeCertificate.v0.schema.json b/schemas/PCSBridgeCertificate.v0.schema.json new file mode 100644 index 0000000..8e62062 --- /dev/null +++ b/schemas/PCSBridgeCertificate.v0.schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/PCSBridgeCertificate.v0.schema.json", + "title": "PCSBridgeCertificate.v0", + "type": "object", + "required": [ + "schema_version", + "artifact_type", + "bridge_id", + "pcs_certificate_id", + "pfcore_trace_hash", + "claim_class", + "source_repo", + "source_commit", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "artifact_type": { "const": "PCSBridgeCertificate.v0" }, + "bridge_id": { "type": "string", "minLength": 1 }, + "pcs_certificate_id": { "type": "string", "minLength": 1 }, + "pfcore_trace_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "pfcore_certificate_id": { "type": "string", "minLength": 1 }, + "adapter": { "type": "string", "minLength": 1 }, + "adapter_version": { "type": "string", "minLength": 1 }, + "claim_class": { "$ref": "pf_core.defs.json#/$defs/claim_class" }, + "assumption_refs": { "$ref": "common.defs.json#/$defs/ref_list" }, + "source_repo": { "type": "string", "format": "uri" }, + "source_commit": { "type": "string", "minLength": 7 }, + "local_dev": { "type": "boolean" }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/PFCoreAction.v0.schema.json b/schemas/PFCoreAction.v0.schema.json new file mode 100644 index 0000000..27c3eae --- /dev/null +++ b/schemas/PFCoreAction.v0.schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/PFCoreAction.v0.schema.json", + "title": "PFCoreAction.v0", + "type": "object", + "required": [ + "schema_version", + "artifact_type", + "action_id", + "tool_name", + "capability", + "effects", + "reads", + "writes", + "input_hash", + "output_hash", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "artifact_type": { "const": "PFCoreAction.v0" }, + "action_id": { "type": "string", "minLength": 1 }, + "tool_name": { "type": "string", "minLength": 1 }, + "capability": { "$ref": "pf_core.defs.json#/$defs/embedded_capability" }, + "effects": { + "type": "array", + "minItems": 1, + "items": { "$ref": "pf_core.defs.json#/$defs/embedded_effect" } + }, + "reads": { + "type": "array", + "items": { "$ref": "pf_core.defs.json#/$defs/embedded_resource" } + }, + "writes": { + "type": "array", + "items": { "$ref": "pf_core.defs.json#/$defs/embedded_resource" } + }, + "input_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "output_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/PFCoreCapability.v0.schema.json b/schemas/PFCoreCapability.v0.schema.json new file mode 100644 index 0000000..42de724 --- /dev/null +++ b/schemas/PFCoreCapability.v0.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/PFCoreCapability.v0.schema.json", + "title": "PFCoreCapability.v0", + "type": "object", + "required": [ + "schema_version", + "artifact_type", + "capability_id", + "effect_kind", + "resource_pattern", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "artifact_type": { "const": "PFCoreCapability.v0" }, + "capability_id": { "type": "string", "minLength": 1 }, + "effect_kind": { "$ref": "pf_core.defs.json#/$defs/effect_kind" }, + "resource_pattern": { "type": "string", "minLength": 1 }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/PFCoreCertificate.v0.schema.json b/schemas/PFCoreCertificate.v0.schema.json new file mode 100644 index 0000000..cd1b0a4 --- /dev/null +++ b/schemas/PFCoreCertificate.v0.schema.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/PFCoreCertificate.v0.schema.json", + "title": "PFCoreCertificate.v0", + "type": "object", + "required": [ + "schema_version", + "artifact_type", + "certificate_id", + "trace_hash", + "contract_hash", + "policy_hash", + "claim_class", + "checker", + "checker_version", + "assumption_refs", + "event_count", + "source_repo", + "source_commit", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "artifact_type": { "const": "PFCoreCertificate.v0" }, + "certificate_id": { "type": "string", "minLength": 1 }, + "trace_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "contract_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "policy_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "claim_class": { "$ref": "pf_core.defs.json#/$defs/claim_class" }, + "checker": { "type": "string", "minLength": 1 }, + "checker_version": { "type": "string", "minLength": 1 }, + "proof_ref": { "type": "string" }, + "proof_term_ref": { "type": "string", "minLength": 1 }, + "lean_proof_checked": { "type": "boolean" }, + "lean_environment_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "obligations": { + "type": "array", + "items": { "$ref": "pf_core.defs.json#/$defs/lean_obligation" } + }, + "assumption_refs": { "$ref": "common.defs.json#/$defs/ref_list" }, + "theorems_checked": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "lean_build_status": { + "type": "object", + "required": ["ok", "target"], + "additionalProperties": false, + "properties": { + "ok": { "type": "boolean" }, + "target": { "type": "string", "minLength": 1 }, + "detail": { "type": "string" } + } + }, + "disclaimer": { "type": "string" }, + "event_count": { "type": "integer", "minimum": 0 }, + "replay_match": { "type": "boolean" }, + "original_trace_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "recomputed_trace_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "source_repo": { "type": "string", "format": "uri" }, + "source_commit": { "type": "string", "minLength": 7 }, + "local_dev": { "type": "boolean" }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/PFCoreContract.v0.schema.json b/schemas/PFCoreContract.v0.schema.json new file mode 100644 index 0000000..50ba5e3 --- /dev/null +++ b/schemas/PFCoreContract.v0.schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/PFCoreContract.v0.schema.json", + "title": "PFCoreContract.v0", + "type": "object", + "required": [ + "schema_version", + "artifact_type", + "contract_id", + "name", + "pre", + "post", + "invariant", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "artifact_type": { "const": "PFCoreContract.v0" }, + "contract_id": { "type": "string", "minLength": 1 }, + "name": { "type": "string", "minLength": 1 }, + "pre": { "$ref": "#/$defs/contract_pre" }, + "post": { "$ref": "#/$defs/contract_post" }, + "invariant": { "$ref": "#/$defs/contract_invariant" }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + }, + "$defs": { + "contract_pre": { + "type": "object", + "additionalProperties": false, + "properties": { + "require_capability": { "type": "string", "minLength": 1 }, + "require_effect": { "$ref": "pf_core.defs.json#/$defs/effect_kind" }, + "require_role": { "type": "string", "minLength": 1 }, + "require_tenant_match": { "type": "boolean" }, + "require_policy_ref": { "type": "string", "minLength": 1 }, + "require_evidence_ref": { "type": "string", "minLength": 1 } + } + }, + "contract_post": { + "type": "object", + "additionalProperties": false, + "properties": { + "require_decision": { "$ref": "pf_core.defs.json#/$defs/decision" }, + "require_event_safe": { "type": "boolean" } + } + }, + "contract_invariant": { + "type": "object", + "additionalProperties": false, + "properties": { + "require_trace_safe": { "type": "boolean" } + } + } + } +} diff --git a/schemas/PFCoreDecision.v0.schema.json b/schemas/PFCoreDecision.v0.schema.json new file mode 100644 index 0000000..45a813b --- /dev/null +++ b/schemas/PFCoreDecision.v0.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/PFCoreDecision.v0.schema.json", + "title": "PFCoreDecision.v0", + "type": "object", + "required": [ + "schema_version", + "artifact_type", + "decision", + "decision_reason", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "artifact_type": { "const": "PFCoreDecision.v0" }, + "decision": { "$ref": "pf_core.defs.json#/$defs/decision" }, + "decision_reason": { "type": "string" }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/PFCoreEffect.v0.schema.json b/schemas/PFCoreEffect.v0.schema.json new file mode 100644 index 0000000..e9b2f78 --- /dev/null +++ b/schemas/PFCoreEffect.v0.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/PFCoreEffect.v0.schema.json", + "title": "PFCoreEffect.v0", + "type": "object", + "required": [ + "schema_version", + "artifact_type", + "effect_kind", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "artifact_type": { "const": "PFCoreEffect.v0" }, + "effect_kind": { "$ref": "pf_core.defs.json#/$defs/effect_kind" }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/PFCoreEvent.v0.schema.json b/schemas/PFCoreEvent.v0.schema.json new file mode 100644 index 0000000..e40b857 --- /dev/null +++ b/schemas/PFCoreEvent.v0.schema.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/PFCoreEvent.v0.schema.json", + "title": "PFCoreEvent.v0", + "$ref": "pf_core.defs.json#/$defs/embedded_event" +} diff --git a/schemas/PFCoreHandoff.v0.schema.json b/schemas/PFCoreHandoff.v0.schema.json new file mode 100644 index 0000000..8c7723a --- /dev/null +++ b/schemas/PFCoreHandoff.v0.schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/PFCoreHandoff.v0.schema.json", + "title": "PFCoreHandoff.v0", + "type": "object", + "required": [ + "schema_version", + "artifact_type", + "handoff_id", + "from_principal", + "to_principal", + "delegated_capabilities", + "reason", + "evidence_refs", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "artifact_type": { "const": "PFCoreHandoff.v0" }, + "handoff_id": { "type": "string", "minLength": 1 }, + "from_principal": { "$ref": "pf_core.defs.json#/$defs/embedded_principal" }, + "to_principal": { "$ref": "pf_core.defs.json#/$defs/embedded_principal" }, + "delegated_capabilities": { + "type": "array", + "minItems": 1, + "items": { "$ref": "pf_core.defs.json#/$defs/embedded_capability" } + }, + "reason": { "type": "string", "minLength": 1 }, + "evidence_refs": { "$ref": "common.defs.json#/$defs/ref_list" }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/PFCorePrincipal.v0.schema.json b/schemas/PFCorePrincipal.v0.schema.json new file mode 100644 index 0000000..50f3fd1 --- /dev/null +++ b/schemas/PFCorePrincipal.v0.schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/PFCorePrincipal.v0.schema.json", + "title": "PFCorePrincipal.v0", + "type": "object", + "required": [ + "schema_version", + "artifact_type", + "principal_id", + "principal_kind", + "tenant", + "roles", + "capabilities", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "artifact_type": { "const": "PFCorePrincipal.v0" }, + "principal_id": { "type": "string", "minLength": 1 }, + "principal_kind": { "$ref": "pf_core.defs.json#/$defs/principal_kind" }, + "tenant": { "type": "string", "minLength": 1 }, + "roles": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "capabilities": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/PFCoreResource.v0.schema.json b/schemas/PFCoreResource.v0.schema.json new file mode 100644 index 0000000..4d89477 --- /dev/null +++ b/schemas/PFCoreResource.v0.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/PFCoreResource.v0.schema.json", + "title": "PFCoreResource.v0", + "type": "object", + "required": [ + "schema_version", + "artifact_type", + "resource_id", + "uri", + "tenant", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "artifact_type": { "const": "PFCoreResource.v0" }, + "resource_id": { "type": "string", "minLength": 1 }, + "uri": { "type": "string", "minLength": 1 }, + "tenant": { "type": "string", "minLength": 1 }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/PFCoreRuntimeObservation.v0.schema.json b/schemas/PFCoreRuntimeObservation.v0.schema.json new file mode 100644 index 0000000..4282e55 --- /dev/null +++ b/schemas/PFCoreRuntimeObservation.v0.schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/PFCoreRuntimeObservation.v0.schema.json", + "title": "PFCoreRuntimeObservation.v0", + "type": "object", + "required": [ + "schema_version", + "artifact_type", + "observation_id", + "trace_id", + "event_id", + "observed_at", + "principal", + "action", + "decision", + "decision_reason", + "policy_ref", + "evidence_refs", + "runtime_ref", + "previous_event_hash", + "payload_hash", + "claim_class", + "source_repo", + "source_commit", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "artifact_type": { "const": "PFCoreRuntimeObservation.v0" }, + "observation_id": { "type": "string", "minLength": 1 }, + "trace_id": { "type": "string", "minLength": 1 }, + "event_id": { "type": "string", "minLength": 1 }, + "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" }, + "decision": { "$ref": "pf_core.defs.json#/$defs/decision" }, + "decision_reason": { "type": "string" }, + "policy_ref": { "type": "string" }, + "evidence_refs": { "$ref": "common.defs.json#/$defs/ref_list" }, + "runtime_ref": { "type": "string", "minLength": 1 }, + "previous_event_hash": { "$ref": "pf_core.defs.json#/$defs/chain_hash" }, + "payload_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "claim_class": { "$ref": "pf_core.defs.json#/$defs/claim_class" }, + "source_repo": { "type": "string", "format": "uri" }, + "source_commit": { "type": "string", "minLength": 7 }, + "local_dev": { "type": "boolean" }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/PFCoreTrace.v0.schema.json b/schemas/PFCoreTrace.v0.schema.json new file mode 100644 index 0000000..eee1171 --- /dev/null +++ b/schemas/PFCoreTrace.v0.schema.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/PFCoreTrace.v0.schema.json", + "title": "PFCoreTrace.v0", + "type": "object", + "required": [ + "schema_version", + "artifact_type", + "trace_id", + "workflow_id", + "events", + "trace_hash", + "policy_hash", + "contract_hash", + "claim_class", + "source_repo", + "source_commit", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "artifact_type": { "const": "PFCoreTrace.v0" }, + "trace_id": { "type": "string", "minLength": 1 }, + "workflow_id": { "type": "string", "minLength": 1 }, + "events": { + "type": "array", + "items": { "$ref": "pf_core.defs.json#/$defs/embedded_event" } + }, + "trace_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "policy_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "contract_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "claim_class": { "$ref": "pf_core.defs.json#/$defs/claim_class" }, + "source_repo": { "type": "string", "format": "uri" }, + "source_commit": { "type": "string", "minLength": 7 }, + "local_dev": { "type": "boolean" }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } +} diff --git a/schemas/ToolUseTrace.v0.schema.json b/schemas/ToolUseTrace.v0.schema.json index f667b74..760dc2f 100644 --- a/schemas/ToolUseTrace.v0.schema.json +++ b/schemas/ToolUseTrace.v0.schema.json @@ -33,9 +33,14 @@ "minItems": 1, "items": { "$ref": "#/$defs/tool_call" } }, + "handoffs": { + "type": "array", + "items": { "$ref": "PFCoreHandoff.v0.schema.json" } + }, "trace_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, "source_repo": { "type": "string", "format": "uri" }, "source_commit": { "$ref": "common.defs.json#/$defs/git_commit" }, + "local_dev": { "type": "boolean" }, "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } }, "$defs": { @@ -67,7 +72,9 @@ "policy_refs": { "type": "array", "items": { "type": "string", "minLength": 1 } - } + }, + "resource_uri": { "type": "string", "minLength": 1 }, + "tenant": { "type": "string", "minLength": 1 } } } } diff --git a/schemas/pf_core.defs.json b/schemas/pf_core.defs.json new file mode 100644 index 0000000..0ac9835 --- /dev/null +++ b/schemas/pf_core.defs.json @@ -0,0 +1,209 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://pcs.sentinelops.ci/schemas/pf_core.defs.json", + "title": "PF-Core Common Definitions", + "$defs": { + "artifact_envelope": { + "type": "object", + "required": ["schema_version", "artifact_type", "signature_or_digest"], + "properties": { + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } + }, + "claim_class": { + "type": "string", + "enum": [ + "RuntimeChecked", + "LeanKernelChecked", + "SchemaValidated", + "CertificateChecked", + "ReplayValidated", + "AssumptionDeclared", + "OutOfScope" + ] + }, + "decision": { + "type": "string", + "enum": ["allow", "deny"] + }, + "effect_kind": { + "type": "string", + "enum": [ + "file.read", + "file.write", + "network.egress", + "email.send", + "handoff.delegate", + "mcp.invoke", + "lab.release" + ] + }, + "principal_kind": { + "type": "string", + "enum": ["agent", "human", "service", "runtime"] + }, + "genesis_hash": { + "type": "string", + "const": "sha256:0000000000000000000000000000000000000000000000000000000000000000" + }, + "chain_hash": { + "$ref": "common.defs.json#/$defs/hex_digest" + }, + "embedded_capability": { + "type": "object", + "required": ["capability_id", "effect_kind", "resource_pattern"], + "additionalProperties": false, + "properties": { + "capability_id": { "type": "string", "minLength": 1 }, + "effect_kind": { "$ref": "#/$defs/effect_kind" }, + "resource_pattern": { "type": "string", "minLength": 1 } + } + }, + "embedded_effect": { + "type": "object", + "required": ["effect_kind"], + "additionalProperties": false, + "properties": { + "effect_kind": { "type": "string", "minLength": 1 } + } + }, + "embedded_resource": { + "type": "object", + "required": ["resource_id", "uri", "tenant"], + "additionalProperties": false, + "properties": { + "resource_id": { "type": "string", "minLength": 1 }, + "uri": { "type": "string", "minLength": 1 }, + "tenant": { "type": "string", "minLength": 1 } + } + }, + "embedded_principal": { + "type": "object", + "required": ["principal_id", "principal_kind", "tenant", "roles", "capabilities"], + "additionalProperties": false, + "properties": { + "principal_id": { "type": "string" }, + "principal_kind": { "$ref": "#/$defs/principal_kind" }, + "tenant": { "type": "string", "minLength": 1 }, + "roles": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "capabilities": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + } + } + }, + "embedded_action": { + "type": "object", + "required": [ + "action_id", + "tool_name", + "capability", + "effects", + "reads", + "writes", + "input_hash", + "output_hash" + ], + "additionalProperties": false, + "properties": { + "action_id": { "type": "string", "minLength": 1 }, + "tool_name": { "type": "string", "minLength": 1 }, + "capability": { "$ref": "#/$defs/embedded_capability" }, + "effects": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/embedded_effect" } + }, + "reads": { + "type": "array", + "items": { "$ref": "#/$defs/embedded_resource" } + }, + "writes": { + "type": "array", + "items": { "$ref": "#/$defs/embedded_resource" } + }, + "input_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "output_hash": { "$ref": "common.defs.json#/$defs/hex_digest" } + } + }, + "embedded_handoff": { + "type": "object", + "required": [ + "from_principal", + "to_principal", + "delegated_capabilities", + "reason", + "evidence_refs" + ], + "additionalProperties": false, + "properties": { + "from_principal": { "$ref": "#/$defs/embedded_principal" }, + "to_principal": { "$ref": "#/$defs/embedded_principal" }, + "delegated_capabilities": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/embedded_capability" } + }, + "reason": { "type": "string", "minLength": 1 }, + "evidence_refs": { "$ref": "common.defs.json#/$defs/ref_list" } + } + }, + "lean_obligation": { + "type": "object", + "required": ["kind", "theorem", "passed"], + "additionalProperties": false, + "properties": { + "kind": { "type": "string", "minLength": 1 }, + "theorem": { "type": "string", "minLength": 1 }, + "passed": { "type": "boolean" }, + "proof_ref": { "type": "string", "minLength": 1 } + } + }, + "embedded_event": { + "type": "object", + "required": [ + "schema_version", + "artifact_type", + "event_id", + "trace_id", + "sequence", + "timestamp", + "principal", + "action", + "decision", + "decision_reason", + "contract_refs", + "evidence_refs", + "previous_event_hash", + "event_hash", + "source_repo", + "source_commit", + "signature_or_digest" + ], + "additionalProperties": false, + "properties": { + "schema_version": { "$ref": "common.defs.json#/$defs/schema_version" }, + "artifact_type": { "const": "PFCoreEvent.v0" }, + "event_id": { "type": "string", "minLength": 1 }, + "trace_id": { "type": "string", "minLength": 1 }, + "sequence": { "type": "integer", "minimum": 0 }, + "timestamp": { "$ref": "common.defs.json#/$defs/iso8601_datetime" }, + "principal": { "$ref": "#/$defs/embedded_principal" }, + "action": { "$ref": "#/$defs/embedded_action" }, + "decision": { "$ref": "#/$defs/decision" }, + "decision_reason": { "type": "string" }, + "contract_refs": { "$ref": "common.defs.json#/$defs/ref_list" }, + "evidence_refs": { "$ref": "common.defs.json#/$defs/ref_list" }, + "previous_event_hash": { "$ref": "#/$defs/chain_hash" }, + "event_hash": { "$ref": "common.defs.json#/$defs/hex_digest" }, + "source_repo": { "type": "string", "format": "uri" }, + "source_commit": { "type": "string", "minLength": 7 }, + "signature_or_digest": { "$ref": "common.defs.json#/$defs/hex_digest" } + } + } + } +} diff --git a/scripts/pf-core-bridge-demo.sh b/scripts/pf-core-bridge-demo.sh new file mode 100644 index 0000000..3c309e8 --- /dev/null +++ b/scripts/pf-core-bridge-demo.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +echo "== PCS TraceCertificate validation ==" +cd python +pip install -e . >/dev/null +pcs validate ../examples/labtrust/trace_certificate.valid.json + +echo "== LabTrust adapter: TraceCertificate -> PFCoreTrace ==" +python - <<'PY' +import json +from pathlib import Path +from pcs_core.pf_core_labtrust_adapter import normalize_labtrust_release + +root = Path("..") +tc = json.loads((root / "examples/labtrust/trace_certificate.valid.json").read_text(encoding="utf-8")) +scb = json.loads((root / "examples/labtrust/science_claim_bundle.certified.valid.json").read_text(encoding="utf-8")) +receipt = scb["runtime_receipts"][0] +trace = normalize_labtrust_release(tc, receipt) +out = root / "examples/pf-core-valid/labtrust_replay/trace.generated.json" +out.write_text(json.dumps(trace, indent=2) + "\n", encoding="utf-8") +print(f"Wrote {out}") +PY + +echo "== PF-Core replay ==" +pcs pf-core replay-trace ../examples/pf-core-valid/labtrust_replay/trace.json + +echo "== External checker attestation -> PFCoreCertificate ==" +python - <<'PY' +import json +from pathlib import Path +from pcs_core.pf_core_certificate import attach_external_certificate_check + +root = Path("..") +trace = json.loads((root / "examples/pf-core-valid/labtrust_replay/trace.json").read_text(encoding="utf-8")) +cert = attach_external_certificate_check( + trace, + checker="certifyedge", + checker_version="0.1.0", + attestation_ref="examples/labtrust/trace_certificate.valid.json", + assumption_refs=["as-labtrust-qc-v0.1"], +) +out = root / "examples/pf-core-valid/labtrust_replay/PFCoreCertificate.v0.generated.json" +out.write_text(json.dumps(cert, indent=2) + "\n", encoding="utf-8") +print(f"Wrote {out} claim_class={cert['claim_class']}") +PY + +pcs validate ../examples/pf-core-valid/labtrust_replay/PFCoreCertificate.v0.generated.json + +echo "== CertifyEdge mock check -> PFCoreCertificate ==" +PCS_CERTIFYEDGE_MOCK=1 pcs pf-core certifyedge-check \ + --trace ../examples/pf-core-valid/labtrust_replay/trace.json \ + --property qc_release.temporal.safety \ + --out ../examples/pf-core-valid/labtrust_replay/PFCoreCertificate.certifyedge.generated.json + +pcs validate ../examples/pf-core-valid/labtrust_replay/PFCoreCertificate.certifyedge.generated.json + +echo "OK pf-core bridge demo" diff --git a/scripts/verify-pf-core-hash-vectors.sh b/scripts/verify-pf-core-hash-vectors.sh new file mode 100644 index 0000000..38cf278 --- /dev/null +++ b/scripts/verify-pf-core-hash-vectors.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Compare PCS hash vectors with provability-fabric-core adapter fixtures (Phase 7 PR-2). +set -euo pipefail + +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-hash-vectors-$$" +LOCAL="${1:-$(cd "$(dirname "$0")/.." && pwd)/python/tests/hash_vectors}" +UPSTREAM="${WORK}/provability-fabric-core/adapters/pcs/tests/fixtures/hash_vectors" + +cleanup() { rm -rf "$WORK"; } +trap cleanup EXIT + +git clone --depth 1 --branch "$PF_CORE_TAG" "$PF_CORE_REPO" "$WORK/provability-fabric-core" + +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/test_vectors/hash/workflow_profile.vector.json b/test_vectors/hash/workflow_profile.vector.json index e87379b..212c0c3 100644 --- a/test_vectors/hash/workflow_profile.vector.json +++ b/test_vectors/hash/workflow_profile.vector.json @@ -1,6 +1,6 @@ { "artifact_type": "WorkflowProfile.v0", "input_file": "examples/workflow_profiles/agent_tool_use_safety.valid.json", - "expected_digest": "sha256:39ea95403f2f7065eaa7cda0084f8f68865bdb9054882e10f016e6d255bc0a49", - "canonical_json": "{\"certificate_artifacts\":[\"ToolUseCertificate.v0\"],\"description\":\"Proof-carrying tool-use safety workflow for agent traces.\",\"domain\":\"agent_tool_use\",\"failure_modes\":[\"unauthorized_tool_call\",\"missing_policy_hash\",\"tool_output_hash_mismatch\",\"unapproved_network_call\",\"unknown_authorization_status\"],\"handoff_sequence\":[\"runtime_to_certificate\",\"certificate_to_bundle\",\"bundle_to_verifier\",\"signed_bundle_to_memory\"],\"limitations_notice\":\"This artifact is a proof-carrying tool-use simulation result. It is not a guarantee that a real deployed agent is safe.\",\"required_admission_profile\":\"agent_tool_use_safety\",\"required_registry_entries\":[\"ToolUseTrace.v0\",\"ToolUseCertificate.v0\",\"RuntimeReceipt.v0\",\"ScienceClaimBundle.v0\",\"VerificationResult.v0\",\"SignedScienceClaimBundle.v0\",\"ReleaseManifest.v0\",\"HandoffManifest.v0\",\"ReleaseChainValidationResult.v0\",\"WorkflowProfile.v0\"],\"runtime_artifacts\":[\"ToolUseTrace.v0\",\"RuntimeReceipt.v0\"],\"schema_version\":\"v0\",\"status_policy\":{\"allowed_terminal_statuses\":[\"Rejected\",\"Stale\"],\"description\":\"Tool traces require authorized calls before CertificateChecked export.\",\"forbidden_transitions\":[{\"from_status\":\"Rejected\",\"to_status\":\"ProofChecked\"}],\"policy_id\":\"pcs-v0.1-tool-use-lifecycle\"},\"workflow_id\":\"agent_tool_use.safety_v0\"}" + "expected_digest": "sha256:f08e4c928dff1be1d610cbd2513b4c5ac5a05f718b5803603c082f136fec23d0", + "canonical_json": "{\"certificate_artifacts\":[\"ToolUseCertificate.v0\"],\"description\":\"Proof-carrying tool-use safety workflow for agent traces.\",\"domain\":\"agent_tool_use\",\"failure_modes\":[\"unauthorized_tool_call\",\"missing_policy_hash\",\"tool_output_hash_mismatch\",\"unapproved_network_call\",\"unknown_authorization_status\"],\"handoff_sequence\":[\"runtime_to_certificate\",\"certificate_to_bundle\",\"bundle_to_verifier\",\"signed_bundle_to_memory\"],\"limitations_notice\":\"This artifact is a proof-carrying tool-use simulation result. It does not guarantee trace-level safety preservation under stated assumptions for a real deployed runtime.\",\"required_admission_profile\":\"agent_tool_use_safety\",\"required_registry_entries\":[\"ToolUseTrace.v0\",\"ToolUseCertificate.v0\",\"RuntimeReceipt.v0\",\"ScienceClaimBundle.v0\",\"VerificationResult.v0\",\"SignedScienceClaimBundle.v0\",\"ReleaseManifest.v0\",\"HandoffManifest.v0\",\"ReleaseChainValidationResult.v0\",\"WorkflowProfile.v0\"],\"runtime_artifacts\":[\"ToolUseTrace.v0\",\"RuntimeReceipt.v0\"],\"schema_version\":\"v0\",\"status_policy\":{\"allowed_terminal_statuses\":[\"Rejected\",\"Stale\"],\"description\":\"Tool traces require authorized calls before CertificateChecked export.\",\"forbidden_transitions\":[{\"from_status\":\"Rejected\",\"to_status\":\"ProofChecked\"}],\"policy_id\":\"pcs-v0.1-tool-use-lifecycle\"},\"workflow_id\":\"agent_tool_use.safety_v0\"}" } diff --git a/typescript/packages/core/src/schema.ts b/typescript/packages/core/src/schema.ts index 0de128b..1a9deed 100644 --- a/typescript/packages/core/src/schema.ts +++ b/typescript/packages/core/src/schema.ts @@ -68,8 +68,22 @@ const ARTIFACT_SCHEMAS: Record = { "ProfileCoverageReport.v0": "ProfileCoverageReport.v0.schema.json", "BenchmarkMetricRegistry.v0": "BenchmarkMetricRegistry.v0.schema.json", "ConformanceReport.v0": "ConformanceReport.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", }; + type Ajv = InstanceType; let ajvInstance: Ajv | null = null; diff --git a/typescript/packages/core/src/tests/examples.test.ts b/typescript/packages/core/src/tests/examples.test.ts index 4f9c7f1..b6c58aa 100644 --- a/typescript/packages/core/src/tests/examples.test.ts +++ b/typescript/packages/core/src/tests/examples.test.ts @@ -54,6 +54,19 @@ test("benchmark ingest examples include artifact refs", () => { } }); +test("PF-Core explicit artifact_type detection", () => { + const cases: Array<[string, ArtifactType]> = [ + ["pf-core-valid/tool_use_trace_compiled/pfcore_trace.json", "PFCoreTrace.v0"], + ["pf-core-valid/assumption_declared/certificate.json", "PFCoreCertificate.v0"], + ]; + for (const [rel, expected] of cases) { + const data = JSON.parse( + readFileSync(join(examplesDir, rel), "utf8"), + ) as Record; + assert.equal(detectArtifactType(data), expected, rel); + } +}); + test("valid examples pass schema and semantic validation", () => { for (const path of validExampleFiles()) { const data = JSON.parse(readFileSync(path, "utf8")) as Record; diff --git a/typescript/packages/core/src/validate.ts b/typescript/packages/core/src/validate.ts index f61ae79..d80334f 100644 --- a/typescript/packages/core/src/validate.ts +++ b/typescript/packages/core/src/validate.ts @@ -38,6 +38,19 @@ export type ArtifactType = | "ScienceClaimBundle.v0" | "VerificationResult.v0" | "SignedScienceClaimBundle.v0" + | "PFCorePrincipal.v0" + | "PFCoreCapability.v0" + | "PFCoreResource.v0" + | "PFCoreAction.v0" + | "PFCoreEffect.v0" + | "PFCoreDecision.v0" + | "PFCoreEvent.v0" + | "PFCoreTrace.v0" + | "PFCoreContract.v0" + | "PFCoreHandoff.v0" + | "PFCoreRuntimeObservation.v0" + | "PFCoreCertificate.v0" + | "PCSBridgeCertificate.v0" | "ReleaseManifest.v0" | "HandoffManifest.v0" | "ReleaseChainValidationResult.v0" @@ -82,7 +95,30 @@ const PROTOCOL_ARTIFACT_TYPES = new Set([ "MigrationReport.v0", ] as ArtifactType[]); +const EXPLICIT_ARTIFACT_TYPES = new Set([ + "PFCorePrincipal.v0", + "PFCoreCapability.v0", + "PFCoreResource.v0", + "PFCoreAction.v0", + "PFCoreEffect.v0", + "PFCoreDecision.v0", + "PFCoreEvent.v0", + "PFCoreTrace.v0", + "PFCoreContract.v0", + "PFCoreHandoff.v0", + "PFCoreRuntimeObservation.v0", + "PFCoreCertificate.v0", + "LeanCheckResult.v0", + "ToolUseTrace.v0", + "PCSBridgeCertificate.v0", + "ClaimArtifact.v0", +]); + export function detectArtifactType(data: Record): ArtifactType | null { + const explicit = data.artifact_type; + if (typeof explicit === "string" && EXPLICIT_ARTIFACT_TYPES.has(explicit as ArtifactType)) { + return explicit as ArtifactType; + } if ( data.schema_version === "v0" && typeof data.registry_id === "string" &&