mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-03 19:53:43 +00:00
add verifier bootstrap and attestation lane
This commit is contained in:
parent
be5bd182d0
commit
2282bb43c7
16 changed files with 594 additions and 28 deletions
|
|
@ -12,3 +12,5 @@ Guidance for future Codex runs in this repository:
|
||||||
- Do not hide proof failures behind warnings.
|
- Do not hide proof failures behind warnings.
|
||||||
- Keep claim cards machine-readable and reports explicit about proven claims, preconditions, exclusions, trusted base, and residual risk.
|
- Keep claim cards machine-readable and reports explicit about proven claims, preconditions, exclusions, trusted base, and residual risk.
|
||||||
- Consequence-producing commands must be policy gated. Do not build wallet or trading-agent artifacts from `R3` arithmetic evidence.
|
- Consequence-producing commands must be policy gated. Do not build wallet or trading-agent artifacts from `R3` arithmetic evidence.
|
||||||
|
- Distinguish verifier capability failures from proof failures. Missing `Mathlib`, missing `Aeneas`, or a missing pinned env script means local replay is unavailable; do not treat it as a clean proof.
|
||||||
|
- Third-party proof-checking attestations are allowed only as an explicit trust transformation. They must identify the provider, subject repo/commit, theorem names, observed axioms, and signature status. Untrusted attestations must not authorize builds.
|
||||||
|
|
|
||||||
42
README.md
42
README.md
|
|
@ -33,6 +33,7 @@ PyYAML is optional. Without it, `pacta` can still read the included simple YAML
|
||||||
```bash
|
```bash
|
||||||
python -m pacta --help
|
python -m pacta --help
|
||||||
pacta scan --config examples/repos.yaml
|
pacta scan --config examples/repos.yaml
|
||||||
|
pacta doctor --config examples/repos.yaml --repo-name dalek-ed25519-verified
|
||||||
pacta claims --config examples/repos.yaml --repo-name dalek-ed25519-verified --offline-fixture --out claims.yaml
|
pacta claims --config examples/repos.yaml --repo-name dalek-ed25519-verified --offline-fixture --out claims.yaml
|
||||||
pacta audit --repo ./repos/dalek-ed25519-verified
|
pacta audit --repo ./repos/dalek-ed25519-verified
|
||||||
pacta lean-check --repo ./repos/dalek-ed25519-verified
|
pacta lean-check --repo ./repos/dalek-ed25519-verified
|
||||||
|
|
@ -41,6 +42,7 @@ pacta score --claims claims.yaml
|
||||||
pacta agent --config examples/repos.yaml --repo-name dalek-ed25519-verified --offline-fixture --action build-library
|
pacta agent --config examples/repos.yaml --repo-name dalek-ed25519-verified --offline-fixture --action build-library
|
||||||
pacta agent --config examples/repos.yaml --repo-name dalek-ed25519-verified --clone --run-axioms --action build-library --artifact-dir artifacts-live
|
pacta agent --config examples/repos.yaml --repo-name dalek-ed25519-verified --clone --run-axioms --action build-library --artifact-dir artifacts-live
|
||||||
pacta agent --claims claims.yaml --action build-wallet-demo
|
pacta agent --claims claims.yaml --action build-wallet-demo
|
||||||
|
pacta agent --config examples/repos.yaml --repo-name dalek-ed25519-verified --attestation examples/dalek-ed25519.attestation.yaml --trust-attestation-provider example-proof-checker.invalid --action build-library
|
||||||
```
|
```
|
||||||
|
|
||||||
## Consequence Engine
|
## Consequence Engine
|
||||||
|
|
@ -54,6 +56,46 @@ This is intentional. Arithmetic proof evidence can authorize a constrained lower
|
||||||
|
|
||||||
In live mode, `--clone --run-axioms` downloads the configured repository, replays the local Lean checks, runs the axiom audit, writes `claims.yaml` and `report.md`, and only builds the capsule if the resulting score satisfies the policy threshold. Failed replay is a hard consequence: no artifact is built.
|
In live mode, `--clone --run-axioms` downloads the configured repository, replays the local Lean checks, runs the axiom audit, writes `claims.yaml` and `report.md`, and only builds the capsule if the resulting score satisfies the policy threshold. Failed replay is a hard consequence: no artifact is built.
|
||||||
|
|
||||||
|
## Verifier Bootstrap
|
||||||
|
|
||||||
|
Some verified repositories rely on a pinned Aeneas Lean project, usually exposed by an environment script such as `~/aeneas-toolchain/env.sh`. `pacta` can use that environment without running extraction:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pacta doctor --config examples/repos.yaml --repo-name dalek-ed25519-verified
|
||||||
|
pacta agent --config examples/repos.yaml --repo-name dalek-ed25519-verified --clone --run-axioms --action build-library
|
||||||
|
```
|
||||||
|
|
||||||
|
The configured defaults are:
|
||||||
|
|
||||||
|
- `env_script: ~/aeneas-toolchain/env.sh`
|
||||||
|
- `lean_project_dir: $AENEAS_HOME/backends/lean`
|
||||||
|
|
||||||
|
If those are missing, the result is `R0` for local replay because this machine lacks verifier capability. That is different from saying the theorem is false. It means the agent cannot trust the repository from local machine-checked evidence yet.
|
||||||
|
|
||||||
|
## Third-Party Attestation
|
||||||
|
|
||||||
|
For agents that should not build the full Lean/Aeneas environment locally, `pacta` also supports an attestation lane. A specialized proof-checking service can replay the proofs in its own controlled environment and publish a certificate describing:
|
||||||
|
|
||||||
|
- repository URL and commit,
|
||||||
|
- theorem/certificate names,
|
||||||
|
- observed axioms,
|
||||||
|
- Lean/toolchain environment,
|
||||||
|
- service identity and signature metadata.
|
||||||
|
|
||||||
|
The agent can consume that certificate only when the provider is explicitly trusted:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pacta agent --config examples/repos.yaml \
|
||||||
|
--repo-name dalek-ed25519-verified \
|
||||||
|
--attestation examples/dalek-ed25519.attestation.yaml \
|
||||||
|
--trust-attestation-provider example-proof-checker.invalid \
|
||||||
|
--action build-library
|
||||||
|
```
|
||||||
|
|
||||||
|
This changes the trusted base. The agent is no longer trusting local Lean replay; it is trusting the proof-checking service, its environment, signing key custody, and log retention. Without an explicitly trusted provider, attestation evidence scores `R0`.
|
||||||
|
|
||||||
|
The included `examples/dalek-ed25519.attestation.yaml` is a schema/demo fixture. A production service should publish verifiable signatures, transparency-log entries, and reproducible logs; the prototype records signature metadata but does not yet implement cryptographic signature verification.
|
||||||
|
|
||||||
## Truth Boundary
|
## Truth Boundary
|
||||||
|
|
||||||
The Ed25519 repositories should not be marketed as fully verified wallets or fully verified Ed25519 end-to-end. The strongest current claim is lower-layer and theorem-bound:
|
The Ed25519 repositories should not be marketed as fully verified wallets or fully verified Ed25519 end-to-end. The strongest current claim is lower-layer and theorem-bound:
|
||||||
|
|
|
||||||
40
examples/dalek-ed25519.attestation.yaml
Normal file
40
examples/dalek-ed25519.attestation.yaml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
schema_version: 1
|
||||||
|
provider: example-proof-checker.invalid
|
||||||
|
issued_at: "2026-07-03T00:00:00Z"
|
||||||
|
subject:
|
||||||
|
component: dalek-ed25519-verified
|
||||||
|
repo_url: https://github.com/saymrwulf/dalek-ed25519-verified.git
|
||||||
|
repo_commit: example-attested-commit
|
||||||
|
verification_dir: verification
|
||||||
|
kind: ed25519
|
||||||
|
verified_backend: serial/u64
|
||||||
|
environment:
|
||||||
|
lean_version: Lean v4.30.0-rc2
|
||||||
|
lake_version: pinned provider environment
|
||||||
|
certificates:
|
||||||
|
- name: CurveFieldProofs.fieldImplementation
|
||||||
|
status: proven
|
||||||
|
axiom_status: clean
|
||||||
|
observed_axioms:
|
||||||
|
- propext
|
||||||
|
- Classical.choice
|
||||||
|
- Quot.sound
|
||||||
|
expected_axioms:
|
||||||
|
- propext
|
||||||
|
- Classical.choice
|
||||||
|
- Quot.sound
|
||||||
|
- name: CurveFieldProofs.edwardsImplementation
|
||||||
|
status: proven
|
||||||
|
axiom_status: clean
|
||||||
|
observed_axioms:
|
||||||
|
- propext
|
||||||
|
- Classical.choice
|
||||||
|
- Quot.sound
|
||||||
|
expected_axioms:
|
||||||
|
- propext
|
||||||
|
- Classical.choice
|
||||||
|
- Quot.sound
|
||||||
|
signature:
|
||||||
|
status: not_implemented
|
||||||
|
identity: example schema only; replace with Sigstore/minisign/service signature
|
||||||
|
log_url: https://example.invalid/proof-checks/dalek-ed25519
|
||||||
|
|
@ -4,6 +4,8 @@ repos:
|
||||||
kind: ed25519
|
kind: ed25519
|
||||||
verification_dir: verification
|
verification_dir: verification
|
||||||
verified_backend: serial/u64
|
verified_backend: serial/u64
|
||||||
|
env_script: ~/aeneas-toolchain/env.sh
|
||||||
|
lean_project_dir: $AENEAS_HOME/backends/lean
|
||||||
certificates:
|
certificates:
|
||||||
- CurveFieldProofs.fieldImplementation
|
- CurveFieldProofs.fieldImplementation
|
||||||
- CurveFieldProofs.edwardsImplementation
|
- CurveFieldProofs.edwardsImplementation
|
||||||
|
|
@ -25,6 +27,8 @@ repos:
|
||||||
kind: ed25519
|
kind: ed25519
|
||||||
verification_dir: verification
|
verification_dir: verification
|
||||||
verified_backend: serial/u64
|
verified_backend: serial/u64
|
||||||
|
env_script: ~/aeneas-toolchain/env.sh
|
||||||
|
lean_project_dir: $AENEAS_HOME/backends/lean
|
||||||
certificates:
|
certificates:
|
||||||
- CurveFieldProofs.fieldImplementation
|
- CurveFieldProofs.fieldImplementation
|
||||||
- CurveFieldProofs.edwardsImplementation
|
- CurveFieldProofs.edwardsImplementation
|
||||||
|
|
@ -43,6 +47,8 @@ repos:
|
||||||
kind: ed25519
|
kind: ed25519
|
||||||
verification_dir: verification
|
verification_dir: verification
|
||||||
verified_backend: serial/u64
|
verified_backend: serial/u64
|
||||||
|
env_script: ~/aeneas-toolchain/env.sh
|
||||||
|
lean_project_dir: $AENEAS_HOME/backends/lean
|
||||||
backend_warning: pure Rust path only; do not treat zkVM accelerator/syscall path as verified
|
backend_warning: pure Rust path only; do not treat zkVM accelerator/syscall path as verified
|
||||||
certificates:
|
certificates:
|
||||||
- CurveFieldProofs.fieldImplementation
|
- CurveFieldProofs.fieldImplementation
|
||||||
|
|
@ -53,6 +59,8 @@ repos:
|
||||||
kind: ed25519
|
kind: ed25519
|
||||||
verification_dir: verification
|
verification_dir: verification
|
||||||
verified_backend: serial/u64
|
verified_backend: serial/u64
|
||||||
|
env_script: ~/aeneas-toolchain/env.sh
|
||||||
|
lean_project_dir: $AENEAS_HOME/backends/lean
|
||||||
backend_warning: pure Rust path only; do not treat Engine25519/hardware accelerator as verified
|
backend_warning: pure Rust path only; do not treat Engine25519/hardware accelerator as verified
|
||||||
certificates:
|
certificates:
|
||||||
- CurveFieldProofs.fieldImplementation
|
- CurveFieldProofs.fieldImplementation
|
||||||
|
|
@ -62,6 +70,8 @@ repos:
|
||||||
url: https://github.com/saymrwulf/pasta-pallas-verified.git
|
url: https://github.com/saymrwulf/pasta-pallas-verified.git
|
||||||
kind: pasta_pallas
|
kind: pasta_pallas
|
||||||
verification_dir: verification
|
verification_dir: verification
|
||||||
|
env_script: ~/aeneas-toolchain/env.sh
|
||||||
|
lean_project_dir: $AENEAS_HOME/backends/lean
|
||||||
known_status: foundation only unless aggregate field certificate is present
|
known_status: foundation only unless aggregate field certificate is present
|
||||||
known_exclusions:
|
known_exclusions:
|
||||||
- full fieldImplementation certificate unless present and axiom-clean
|
- full fieldImplementation certificate unless present and axiom-clean
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,9 @@ def _lib_rs(card: dict[str, Any]) -> str:
|
||||||
repo_url = _rust_string(str(card.get("repo_url") or ""))
|
repo_url = _rust_string(str(card.get("repo_url") or ""))
|
||||||
risk = card.get("risk") or {}
|
risk = card.get("risk") or {}
|
||||||
risk_level = _rust_string(str(risk.get("level") or "R0"))
|
risk_level = _rust_string(str(risk.get("level") or "R0"))
|
||||||
|
evidence = card.get("evidence") or {}
|
||||||
|
evidence_mode = _rust_string(str(evidence.get("evidence_mode") or "local_or_fixture"))
|
||||||
|
attestation_provider = _rust_string(str(evidence.get("attestation_provider") or ""))
|
||||||
kind = _rust_string(str(card.get("kind") or "unknown"))
|
kind = _rust_string(str(card.get("kind") or "unknown"))
|
||||||
backend = _rust_string(str(card.get("verified_backend") or ""))
|
backend = _rust_string(str(card.get("verified_backend") or ""))
|
||||||
constraints = [_rust_string(str(item)) for item in risk.get("deployment_constraints") or []]
|
constraints = [_rust_string(str(item)) for item in risk.get("deployment_constraints") or []]
|
||||||
|
|
@ -174,6 +177,8 @@ pub const REPO_URL: &str = "{repo_url}";
|
||||||
pub const KIND: &str = "{kind}";
|
pub const KIND: &str = "{kind}";
|
||||||
pub const VERIFIED_BACKEND: &str = "{backend}";
|
pub const VERIFIED_BACKEND: &str = "{backend}";
|
||||||
pub const RISK_LEVEL: &str = "{risk_level}";
|
pub const RISK_LEVEL: &str = "{risk_level}";
|
||||||
|
pub const EVIDENCE_MODE: &str = "{evidence_mode}";
|
||||||
|
pub const ATTESTATION_PROVIDER: &str = "{attestation_provider}";
|
||||||
pub const CLAIM_CARD_JSON: &str = include_str!("../claims.json");
|
pub const CLAIM_CARD_JSON: &str = include_str!("../claims.json");
|
||||||
|
|
||||||
pub const DEPLOYMENT_CONSTRAINTS: &[&str] = &[
|
pub const DEPLOYMENT_CONSTRAINTS: &[&str] = &[
|
||||||
|
|
|
||||||
100
src/pacta/attestation.py
Normal file
100
src/pacta/attestation.py
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .config import RepoConfig
|
||||||
|
from .yamlio import load_data
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class AttestationResult:
|
||||||
|
accepted: bool
|
||||||
|
provider: str | None
|
||||||
|
diagnostics: list[str] = field(default_factory=list)
|
||||||
|
certificates: list[dict[str, Any]] = field(default_factory=list)
|
||||||
|
evidence: dict[str, Any] = field(default_factory=dict)
|
||||||
|
trusted_base: list[str] = field(default_factory=list)
|
||||||
|
repo_commit: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def load_attestation(path: str | Path) -> dict[str, Any]:
|
||||||
|
raw = load_data(path)
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raise ValueError(f"Attestation must be a mapping: {path}")
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def validate_attestation(
|
||||||
|
raw: dict[str, Any],
|
||||||
|
repo: RepoConfig,
|
||||||
|
path: str | Path | None = None,
|
||||||
|
trusted_provider: str | None = None,
|
||||||
|
) -> AttestationResult:
|
||||||
|
provider = raw.get("provider")
|
||||||
|
subject = raw.get("subject") or {}
|
||||||
|
diagnostics: list[str] = []
|
||||||
|
if not provider:
|
||||||
|
diagnostics.append("Attestation is missing provider.")
|
||||||
|
if trusted_provider is None:
|
||||||
|
diagnostics.append("No trusted attestation provider was explicitly configured.")
|
||||||
|
elif provider != trusted_provider:
|
||||||
|
diagnostics.append(f"Attestation provider '{provider}' does not match trusted provider '{trusted_provider}'.")
|
||||||
|
if subject.get("component") and subject.get("component") != repo.name:
|
||||||
|
diagnostics.append(f"Attestation subject component '{subject.get('component')}' does not match repo '{repo.name}'.")
|
||||||
|
if repo.url and subject.get("repo_url") and subject.get("repo_url") != repo.url:
|
||||||
|
diagnostics.append("Attestation subject repo_url does not match config.")
|
||||||
|
if subject.get("verification_dir") and subject.get("verification_dir") != repo.verification_dir:
|
||||||
|
diagnostics.append("Attestation subject verification_dir does not match config.")
|
||||||
|
|
||||||
|
certs = raw.get("certificates") or []
|
||||||
|
if not isinstance(certs, list) or not certs:
|
||||||
|
diagnostics.append("Attestation contains no certificate results.")
|
||||||
|
certs = []
|
||||||
|
expected_names = set(repo.certificates)
|
||||||
|
observed_names = {str(cert.get("name")) for cert in certs if isinstance(cert, dict)}
|
||||||
|
missing = sorted(expected_names - observed_names)
|
||||||
|
if missing:
|
||||||
|
diagnostics.append("Attestation is missing configured certificate(s): " + ", ".join(missing))
|
||||||
|
|
||||||
|
signature = raw.get("signature") or {}
|
||||||
|
environment = raw.get("environment") or {}
|
||||||
|
signature_status = signature.get("status", "not_checked")
|
||||||
|
if signature_status not in {"verified", "not_implemented"}:
|
||||||
|
diagnostics.append(f"Attestation signature status is not acceptable for this prototype: {signature_status}")
|
||||||
|
|
||||||
|
accepted = not diagnostics
|
||||||
|
evidence = {
|
||||||
|
"evidence_mode": "third_party_attestation",
|
||||||
|
"attestation_provider": provider,
|
||||||
|
"attestation_path": str(path) if path else None,
|
||||||
|
"attestation_signature_status": signature_status,
|
||||||
|
"attestation_log_url": raw.get("log_url") or signature.get("log_url"),
|
||||||
|
"attestation_issued_at": raw.get("issued_at"),
|
||||||
|
"lean_version": environment.get("lean_version"),
|
||||||
|
"lake_version": environment.get("lake_version"),
|
||||||
|
}
|
||||||
|
trusted_base = []
|
||||||
|
if accepted:
|
||||||
|
trusted_base.append(f"Third-party proof-checking attestation provider: {provider}.")
|
||||||
|
trusted_base.append("Provider environment, replay implementation, signing key custody, and log retention.")
|
||||||
|
return AttestationResult(
|
||||||
|
accepted=accepted,
|
||||||
|
provider=str(provider) if provider else None,
|
||||||
|
diagnostics=diagnostics,
|
||||||
|
certificates=[_normalize_certificate(cert, repo.expected_axioms) for cert in certs if isinstance(cert, dict)],
|
||||||
|
evidence=evidence,
|
||||||
|
trusted_base=trusted_base,
|
||||||
|
repo_commit=subject.get("repo_commit"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_certificate(cert: dict[str, Any], expected_axioms: list[str]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"name": str(cert.get("name") or ""),
|
||||||
|
"status": str(cert.get("status") or "unknown"),
|
||||||
|
"axiom_status": str(cert.get("axiom_status") or "not_checked"),
|
||||||
|
"observed_axioms": list(cert.get("observed_axioms") or []),
|
||||||
|
"expected_axioms": list(cert.get("expected_axioms") or expected_axioms),
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from .attestation import AttestationResult
|
||||||
from .config import RepoConfig
|
from .config import RepoConfig
|
||||||
from .lean import AxiomAuditResult, CertificateAxiomResult, LeanCheckResult, detect_tools
|
from .lean import AxiomAuditResult, CertificateAxiomResult, LeanCheckResult, detect_tools
|
||||||
from .manifest import VerificationLayout
|
from .manifest import VerificationLayout
|
||||||
|
|
@ -36,18 +37,19 @@ def build_claim_card(
|
||||||
layout: VerificationLayout | None = None,
|
layout: VerificationLayout | None = None,
|
||||||
lean_check: LeanCheckResult | None = None,
|
lean_check: LeanCheckResult | None = None,
|
||||||
axiom_audit: AxiomAuditResult | None = None,
|
axiom_audit: AxiomAuditResult | None = None,
|
||||||
|
attestation: AttestationResult | None = None,
|
||||||
offline_fixture: bool = False,
|
offline_fixture: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
path = Path(local_path)
|
path = Path(local_path)
|
||||||
profile = get_profile(repo.kind, repo)
|
profile = get_profile(repo.kind, repo)
|
||||||
tools = detect_tools()
|
tools = detect_tools()
|
||||||
certs = _certificate_claims(repo, axiom_audit, offline_fixture)
|
certs = _certificate_claims(repo, axiom_audit, offline_fixture, attestation)
|
||||||
scanned_files = layout.relative_files() if layout else []
|
scanned_files = layout.relative_files() if layout else []
|
||||||
card: dict[str, Any] = {
|
card: dict[str, Any] = {
|
||||||
"component": repo.name,
|
"component": repo.name,
|
||||||
"repo_url": repo.url,
|
"repo_url": repo.url,
|
||||||
"local_path": str(path),
|
"local_path": str(path),
|
||||||
"repo_commit": None if offline_fixture else git_commit(path),
|
"repo_commit": attestation.repo_commit if attestation and attestation.repo_commit else (None if offline_fixture else git_commit(path)),
|
||||||
"verification_dir": repo.verification_dir,
|
"verification_dir": repo.verification_dir,
|
||||||
"kind": repo.kind,
|
"kind": repo.kind,
|
||||||
"verified_backend": repo.verified_backend,
|
"verified_backend": repo.verified_backend,
|
||||||
|
|
@ -55,13 +57,15 @@ def build_claim_card(
|
||||||
"guarantees": profile.guarantees,
|
"guarantees": profile.guarantees,
|
||||||
"preconditions": profile.preconditions,
|
"preconditions": profile.preconditions,
|
||||||
"exclusions": profile.exclusions,
|
"exclusions": profile.exclusions,
|
||||||
"trusted_base": profile.trusted_base,
|
"trusted_base": [*profile.trusted_base, *(attestation.trusted_base if attestation and attestation.accepted else [])],
|
||||||
"evidence": {
|
"evidence": {
|
||||||
"lean_version": tools.lean_version,
|
"lean_version": tools.lean_version,
|
||||||
"lake_version": tools.lake_version,
|
"lake_version": tools.lake_version,
|
||||||
"check_log_path": lean_check.log_path if lean_check else None,
|
"check_log_path": lean_check.log_path if lean_check else None,
|
||||||
"axiom_log_path": axiom_audit.log_path if axiom_audit else None,
|
"axiom_log_path": axiom_audit.log_path if axiom_audit else None,
|
||||||
|
"replay_blockers": _replay_blockers(lean_check, axiom_audit, attestation),
|
||||||
"scanned_files": scanned_files,
|
"scanned_files": scanned_files,
|
||||||
|
**(attestation.evidence if attestation else {"evidence_mode": "local_or_fixture"}),
|
||||||
},
|
},
|
||||||
"risk": {
|
"risk": {
|
||||||
"level": "R0",
|
"level": "R0",
|
||||||
|
|
@ -86,9 +90,22 @@ def _certificate_claims(
|
||||||
repo: RepoConfig,
|
repo: RepoConfig,
|
||||||
axiom_audit: AxiomAuditResult | None,
|
axiom_audit: AxiomAuditResult | None,
|
||||||
offline_fixture: bool,
|
offline_fixture: bool,
|
||||||
|
attestation: AttestationResult | None,
|
||||||
) -> list[CertificateClaim]:
|
) -> list[CertificateClaim]:
|
||||||
profile = get_profile(repo.kind, repo)
|
profile = get_profile(repo.kind, repo)
|
||||||
names = repo.certificates or profile.default_certificates
|
names = repo.certificates or profile.default_certificates
|
||||||
|
if attestation and attestation.accepted:
|
||||||
|
by_name = {cert["name"]: cert for cert in attestation.certificates}
|
||||||
|
return [
|
||||||
|
CertificateClaim(
|
||||||
|
name=name,
|
||||||
|
status=str(by_name.get(name, {}).get("status", "missing")),
|
||||||
|
axiom_status=str(by_name.get(name, {}).get("axiom_status", "not_checked")),
|
||||||
|
observed_axioms=list(by_name.get(name, {}).get("observed_axioms") or []),
|
||||||
|
expected_axioms=list(by_name.get(name, {}).get("expected_axioms") or profile.expected_axioms),
|
||||||
|
)
|
||||||
|
for name in names
|
||||||
|
]
|
||||||
if axiom_audit:
|
if axiom_audit:
|
||||||
by_name = {cert.name: cert for cert in axiom_audit.certificates}
|
by_name = {cert.name: cert for cert in axiom_audit.certificates}
|
||||||
return [_from_axiom_result(name, by_name.get(name), profile.expected_axioms) for name in names]
|
return [_from_axiom_result(name, by_name.get(name), profile.expected_axioms) for name in names]
|
||||||
|
|
@ -132,3 +149,28 @@ def _has_axiom_clean_certificate(card: dict[str, Any]) -> bool:
|
||||||
cert.get("status") == "proven" and cert.get("axiom_status") == "clean"
|
cert.get("status") == "proven" and cert.get("axiom_status") == "clean"
|
||||||
for cert in card.get("certificates") or []
|
for cert in card.get("certificates") or []
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _replay_blockers(
|
||||||
|
lean_check: LeanCheckResult | None,
|
||||||
|
axiom_audit: AxiomAuditResult | None,
|
||||||
|
attestation: AttestationResult | None,
|
||||||
|
) -> list[str]:
|
||||||
|
blockers: list[str] = []
|
||||||
|
if attestation and not attestation.accepted:
|
||||||
|
blockers.extend(attestation.diagnostics)
|
||||||
|
if lean_check:
|
||||||
|
blockers.extend(lean_check.diagnostics)
|
||||||
|
if lean_check.missing_tool:
|
||||||
|
blockers.append(f"Missing verifier capability: {lean_check.missing_tool}")
|
||||||
|
if axiom_audit:
|
||||||
|
blockers.extend(axiom_audit.diagnostics)
|
||||||
|
if axiom_audit.missing_tool:
|
||||||
|
blockers.append(f"Missing verifier capability: {axiom_audit.missing_tool}")
|
||||||
|
seen: set[str] = set()
|
||||||
|
out: list[str] = []
|
||||||
|
for blocker in blockers:
|
||||||
|
if blocker and blocker not in seen:
|
||||||
|
seen.add(blocker)
|
||||||
|
out.append(blocker)
|
||||||
|
return out
|
||||||
|
|
|
||||||
123
src/pacta/cli.py
123
src/pacta/cli.py
|
|
@ -6,10 +6,19 @@ from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .agent import component_artifact_dir, run_agent_action, write_decision
|
from .agent import component_artifact_dir, run_agent_action, write_decision
|
||||||
|
from .attestation import load_attestation, validate_attestation
|
||||||
from .audit import scan_hygiene
|
from .audit import scan_hygiene
|
||||||
from .claims import build_claim_card
|
from .claims import build_claim_card
|
||||||
from .config import RepoConfig, load_config
|
from .config import RepoConfig, load_config
|
||||||
from .lean import LeanCheckResult, lean_check_files, run_axiom_audit
|
from .lean import (
|
||||||
|
LeanCheckResult,
|
||||||
|
build_lean_env,
|
||||||
|
detect_tools,
|
||||||
|
env_script_available,
|
||||||
|
lean_check_files,
|
||||||
|
resolve_lean_project_dir,
|
||||||
|
run_axiom_audit,
|
||||||
|
)
|
||||||
from .manifest import discover_layout
|
from .manifest import discover_layout
|
||||||
from .profiles import get_profile
|
from .profiles import get_profile
|
||||||
from .repo import clone_or_fetch, status_for
|
from .repo import clone_or_fetch, status_for
|
||||||
|
|
@ -39,6 +48,13 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
scan.add_argument("--fetch", action="store_true", help="Fetch existing repositories when cloning/scanning.")
|
scan.add_argument("--fetch", action="store_true", help="Fetch existing repositories when cloning/scanning.")
|
||||||
scan.set_defaults(func=cmd_scan)
|
scan.set_defaults(func=cmd_scan)
|
||||||
|
|
||||||
|
doctor = sub.add_parser("doctor", help="Diagnose local verifier capabilities for a configured repository.")
|
||||||
|
doctor.add_argument("--config", required=True)
|
||||||
|
doctor.add_argument("--repo-name", required=True)
|
||||||
|
doctor.add_argument("--env-script")
|
||||||
|
doctor.add_argument("--lean-project-dir")
|
||||||
|
doctor.set_defaults(func=cmd_doctor)
|
||||||
|
|
||||||
audit = sub.add_parser("audit", help="Run static proof hygiene checks.")
|
audit = sub.add_parser("audit", help="Run static proof hygiene checks.")
|
||||||
audit.add_argument("--repo", required=True)
|
audit.add_argument("--repo", required=True)
|
||||||
audit.add_argument("--verification-dir", default="verification")
|
audit.add_argument("--verification-dir", default="verification")
|
||||||
|
|
@ -51,6 +67,8 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
lean_check.add_argument("--verification-dir", default="verification")
|
lean_check.add_argument("--verification-dir", default="verification")
|
||||||
lean_check.add_argument("--timeout", type=int, default=120)
|
lean_check.add_argument("--timeout", type=int, default=120)
|
||||||
lean_check.add_argument("--log-dir", default=".pacta")
|
lean_check.add_argument("--log-dir", default=".pacta")
|
||||||
|
lean_check.add_argument("--env-script")
|
||||||
|
lean_check.add_argument("--lean-project-dir")
|
||||||
lean_check.set_defaults(func=cmd_lean_check)
|
lean_check.set_defaults(func=cmd_lean_check)
|
||||||
|
|
||||||
check = sub.add_parser("check", help="Alias for lean-check.")
|
check = sub.add_parser("check", help="Alias for lean-check.")
|
||||||
|
|
@ -58,6 +76,8 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
check.add_argument("--verification-dir", default="verification")
|
check.add_argument("--verification-dir", default="verification")
|
||||||
check.add_argument("--timeout", type=int, default=120)
|
check.add_argument("--timeout", type=int, default=120)
|
||||||
check.add_argument("--log-dir", default=".pacta")
|
check.add_argument("--log-dir", default=".pacta")
|
||||||
|
check.add_argument("--env-script")
|
||||||
|
check.add_argument("--lean-project-dir")
|
||||||
check.set_defaults(func=cmd_lean_check)
|
check.set_defaults(func=cmd_lean_check)
|
||||||
|
|
||||||
axiom = sub.add_parser("axioms", help="Run configured Lean axiom audit.")
|
axiom = sub.add_parser("axioms", help="Run configured Lean axiom audit.")
|
||||||
|
|
@ -66,6 +86,8 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
axiom.add_argument("--repo-name", required=True)
|
axiom.add_argument("--repo-name", required=True)
|
||||||
axiom.add_argument("--timeout", type=int, default=120)
|
axiom.add_argument("--timeout", type=int, default=120)
|
||||||
axiom.add_argument("--log-dir", default=".pacta")
|
axiom.add_argument("--log-dir", default=".pacta")
|
||||||
|
axiom.add_argument("--env-script")
|
||||||
|
axiom.add_argument("--lean-project-dir")
|
||||||
axiom.set_defaults(func=cmd_axioms)
|
axiom.set_defaults(func=cmd_axioms)
|
||||||
|
|
||||||
claims = sub.add_parser("claims", help="Generate a machine-readable claim card.")
|
claims = sub.add_parser("claims", help="Generate a machine-readable claim card.")
|
||||||
|
|
@ -78,6 +100,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
claims.add_argument("--run-axioms", action="store_true")
|
claims.add_argument("--run-axioms", action="store_true")
|
||||||
claims.add_argument("--timeout", type=int, default=120)
|
claims.add_argument("--timeout", type=int, default=120)
|
||||||
claims.add_argument("--log-dir", default=".pacta")
|
claims.add_argument("--log-dir", default=".pacta")
|
||||||
|
claims.add_argument("--env-script")
|
||||||
|
claims.add_argument("--lean-project-dir")
|
||||||
|
claims.add_argument("--attestation")
|
||||||
|
claims.add_argument("--trust-attestation-provider")
|
||||||
claims.set_defaults(func=cmd_claims)
|
claims.set_defaults(func=cmd_claims)
|
||||||
|
|
||||||
report = sub.add_parser("report", help="Generate a human-readable Markdown risk report.")
|
report = sub.add_parser("report", help="Generate a human-readable Markdown risk report.")
|
||||||
|
|
@ -109,6 +135,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
agent.add_argument("--dry-run", action="store_true")
|
agent.add_argument("--dry-run", action="store_true")
|
||||||
agent.add_argument("--timeout", type=int, default=120)
|
agent.add_argument("--timeout", type=int, default=120)
|
||||||
agent.add_argument("--log-dir", default=".pacta")
|
agent.add_argument("--log-dir", default=".pacta")
|
||||||
|
agent.add_argument("--env-script")
|
||||||
|
agent.add_argument("--lean-project-dir")
|
||||||
|
agent.add_argument("--attestation")
|
||||||
|
agent.add_argument("--trust-attestation-provider")
|
||||||
agent.set_defaults(func=cmd_agent)
|
agent.set_defaults(func=cmd_agent)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
@ -126,6 +156,37 @@ def cmd_scan(args: argparse.Namespace) -> int:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_doctor(args: argparse.Namespace) -> int:
|
||||||
|
config = load_config(args.config)
|
||||||
|
repo = config.repo_named(args.repo_name)
|
||||||
|
env_script = args.env_script or repo.env_script
|
||||||
|
lean_project_dir = args.lean_project_dir or repo.lean_project_dir
|
||||||
|
ok_env, env_error = env_script_available(env_script)
|
||||||
|
env = build_lean_env(repo.verification_dir, env_script=env_script) if ok_env else {}
|
||||||
|
tools = detect_tools(env if env else None)
|
||||||
|
project_dir = resolve_lean_project_dir(lean_project_dir, env if env else None)
|
||||||
|
print(f"repo: {repo.name}")
|
||||||
|
print(f"env_script: {env_script or 'not configured'}")
|
||||||
|
print(f"env_script_status: {'ok' if ok_env else 'missing'}")
|
||||||
|
if env_error:
|
||||||
|
print(f"env_script_error: {env_error}")
|
||||||
|
print(f"lean_project_dir: {lean_project_dir or 'not configured'}")
|
||||||
|
print(f"lean_project_dir_status: {'ok' if project_dir else 'missing'}")
|
||||||
|
if project_dir:
|
||||||
|
print(f"lean_project_dir_resolved: {project_dir}")
|
||||||
|
print(f"lean: {tools.lean or 'missing'}")
|
||||||
|
print(f"lake: {tools.lake or 'missing'}")
|
||||||
|
print(f"lean_version: {tools.lean_version or 'unknown'}")
|
||||||
|
print(f"lake_version: {tools.lake_version or 'unknown'}")
|
||||||
|
if not ok_env or not project_dir:
|
||||||
|
print("remediation: install or point to the pinned verifier environment, for example --env-script ~/aeneas-toolchain/env.sh --lean-project-dir '$AENEAS_HOME/backends/lean'")
|
||||||
|
return 1
|
||||||
|
if not tools.lean and not tools.lake:
|
||||||
|
print("remediation: ensure lean/lake are on PATH after sourcing the verifier environment.")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def cmd_audit(args: argparse.Namespace) -> int:
|
def cmd_audit(args: argparse.Namespace) -> int:
|
||||||
repo_config = _repo_from_optional_config(args.config, args.repo_name, args.verification_dir)
|
repo_config = _repo_from_optional_config(args.config, args.repo_name, args.verification_dir)
|
||||||
layout = discover_layout(args.repo, repo_config.verification_dir)
|
layout = discover_layout(args.repo, repo_config.verification_dir)
|
||||||
|
|
@ -144,7 +205,14 @@ def cmd_lean_check(args: argparse.Namespace) -> int:
|
||||||
layout = discover_layout(args.repo, args.verification_dir)
|
layout = discover_layout(args.repo, args.verification_dir)
|
||||||
for warning in layout.warnings:
|
for warning in layout.warnings:
|
||||||
print(f"warning: {warning}")
|
print(f"warning: {warning}")
|
||||||
result = lean_check_files(layout.compile_order, layout.verification_dir, timeout=args.timeout, log_dir=args.log_dir)
|
result = lean_check_files(
|
||||||
|
layout.compile_order,
|
||||||
|
layout.verification_dir,
|
||||||
|
timeout=args.timeout,
|
||||||
|
log_dir=args.log_dir,
|
||||||
|
env_script=args.env_script,
|
||||||
|
lean_project_dir=args.lean_project_dir,
|
||||||
|
)
|
||||||
if not result.attempted:
|
if not result.attempted:
|
||||||
print("; ".join(result.diagnostics))
|
print("; ".join(result.diagnostics))
|
||||||
return 2
|
return 2
|
||||||
|
|
@ -159,7 +227,16 @@ def cmd_axioms(args: argparse.Namespace) -> int:
|
||||||
repo = config.repo_named(args.repo_name)
|
repo = config.repo_named(args.repo_name)
|
||||||
profile = get_profile(repo.kind, repo)
|
profile = get_profile(repo.kind, repo)
|
||||||
layout = discover_layout(args.repo, repo.verification_dir)
|
layout = discover_layout(args.repo, repo.verification_dir)
|
||||||
check_result = lean_check_files(layout.compile_order, layout.verification_dir, timeout=args.timeout, log_dir=args.log_dir)
|
env_script = args.env_script or repo.env_script
|
||||||
|
lean_project_dir = args.lean_project_dir or repo.lean_project_dir
|
||||||
|
check_result = lean_check_files(
|
||||||
|
layout.compile_order,
|
||||||
|
layout.verification_dir,
|
||||||
|
timeout=args.timeout,
|
||||||
|
log_dir=args.log_dir,
|
||||||
|
env_script=env_script,
|
||||||
|
lean_project_dir=lean_project_dir,
|
||||||
|
)
|
||||||
if check_result.log_path:
|
if check_result.log_path:
|
||||||
print(f"check log: {check_result.log_path}")
|
print(f"check log: {check_result.log_path}")
|
||||||
if not check_result.attempted:
|
if not check_result.attempted:
|
||||||
|
|
@ -174,6 +251,8 @@ def cmd_axioms(args: argparse.Namespace) -> int:
|
||||||
profile.expected_axioms,
|
profile.expected_axioms,
|
||||||
timeout=args.timeout,
|
timeout=args.timeout,
|
||||||
log_dir=args.log_dir,
|
log_dir=args.log_dir,
|
||||||
|
env_script=env_script,
|
||||||
|
lean_project_dir=lean_project_dir,
|
||||||
)
|
)
|
||||||
for cert in result.certificates:
|
for cert in result.certificates:
|
||||||
print(f"{cert.name}: {cert.status}, axioms={cert.axiom_status}, observed={cert.observed_axioms}")
|
print(f"{cert.name}: {cert.status}, axioms={cert.axiom_status}, observed={cert.observed_axioms}")
|
||||||
|
|
@ -197,7 +276,16 @@ def cmd_claims(args: argparse.Namespace) -> int:
|
||||||
if layout is None:
|
if layout is None:
|
||||||
raise ValueError("--run-axioms requires a local repository")
|
raise ValueError("--run-axioms requires a local repository")
|
||||||
profile = get_profile(repo.kind, repo)
|
profile = get_profile(repo.kind, repo)
|
||||||
check_result = lean_check_files(layout.compile_order, layout.verification_dir, timeout=args.timeout, log_dir=args.log_dir)
|
env_script = args.env_script or repo.env_script
|
||||||
|
lean_project_dir = args.lean_project_dir or repo.lean_project_dir
|
||||||
|
check_result = lean_check_files(
|
||||||
|
layout.compile_order,
|
||||||
|
layout.verification_dir,
|
||||||
|
timeout=args.timeout,
|
||||||
|
log_dir=args.log_dir,
|
||||||
|
env_script=env_script,
|
||||||
|
lean_project_dir=lean_project_dir,
|
||||||
|
)
|
||||||
axiom_result = run_axiom_audit(
|
axiom_result = run_axiom_audit(
|
||||||
local_path / repo.verification_dir,
|
local_path / repo.verification_dir,
|
||||||
profile.axiom_imports,
|
profile.axiom_imports,
|
||||||
|
|
@ -205,13 +293,17 @@ def cmd_claims(args: argparse.Namespace) -> int:
|
||||||
profile.expected_axioms,
|
profile.expected_axioms,
|
||||||
timeout=args.timeout,
|
timeout=args.timeout,
|
||||||
log_dir=args.log_dir,
|
log_dir=args.log_dir,
|
||||||
|
env_script=env_script,
|
||||||
|
lean_project_dir=lean_project_dir,
|
||||||
)
|
)
|
||||||
|
attestation = _attestation_for_args(args, repo)
|
||||||
card = build_claim_card(
|
card = build_claim_card(
|
||||||
repo,
|
repo,
|
||||||
local_path,
|
local_path,
|
||||||
layout=layout,
|
layout=layout,
|
||||||
lean_check=check_result,
|
lean_check=check_result,
|
||||||
axiom_audit=axiom_result,
|
axiom_audit=axiom_result,
|
||||||
|
attestation=attestation,
|
||||||
offline_fixture=args.offline_fixture,
|
offline_fixture=args.offline_fixture,
|
||||||
)
|
)
|
||||||
if args.out:
|
if args.out:
|
||||||
|
|
@ -301,7 +393,16 @@ def _card_for_agent(args: argparse.Namespace) -> dict[str, Any]:
|
||||||
if layout is None:
|
if layout is None:
|
||||||
raise ValueError("--run-axioms requires a local repository")
|
raise ValueError("--run-axioms requires a local repository")
|
||||||
profile = get_profile(repo.kind, repo)
|
profile = get_profile(repo.kind, repo)
|
||||||
check_result = lean_check_files(layout.compile_order, layout.verification_dir, timeout=args.timeout, log_dir=args.log_dir)
|
env_script = args.env_script or repo.env_script
|
||||||
|
lean_project_dir = args.lean_project_dir or repo.lean_project_dir
|
||||||
|
check_result = lean_check_files(
|
||||||
|
layout.compile_order,
|
||||||
|
layout.verification_dir,
|
||||||
|
timeout=args.timeout,
|
||||||
|
log_dir=args.log_dir,
|
||||||
|
env_script=env_script,
|
||||||
|
lean_project_dir=lean_project_dir,
|
||||||
|
)
|
||||||
axiom_result = run_axiom_audit(
|
axiom_result = run_axiom_audit(
|
||||||
local_path / repo.verification_dir,
|
local_path / repo.verification_dir,
|
||||||
profile.axiom_imports,
|
profile.axiom_imports,
|
||||||
|
|
@ -309,13 +410,17 @@ def _card_for_agent(args: argparse.Namespace) -> dict[str, Any]:
|
||||||
profile.expected_axioms,
|
profile.expected_axioms,
|
||||||
timeout=args.timeout,
|
timeout=args.timeout,
|
||||||
log_dir=args.log_dir,
|
log_dir=args.log_dir,
|
||||||
|
env_script=env_script,
|
||||||
|
lean_project_dir=lean_project_dir,
|
||||||
)
|
)
|
||||||
|
attestation = _attestation_for_args(args, repo)
|
||||||
return build_claim_card(
|
return build_claim_card(
|
||||||
repo,
|
repo,
|
||||||
local_path,
|
local_path,
|
||||||
layout=layout,
|
layout=layout,
|
||||||
lean_check=check_result,
|
lean_check=check_result,
|
||||||
axiom_audit=axiom_result,
|
axiom_audit=axiom_result,
|
||||||
|
attestation=attestation,
|
||||||
offline_fixture=args.offline_fixture,
|
offline_fixture=args.offline_fixture,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -333,3 +438,11 @@ def print_yaml(data: dict[str, Any]) -> None:
|
||||||
from .yamlio import dumps
|
from .yamlio import dumps
|
||||||
|
|
||||||
print(dumps(data), end="")
|
print(dumps(data), end="")
|
||||||
|
|
||||||
|
|
||||||
|
def _attestation_for_args(args: argparse.Namespace, repo: RepoConfig):
|
||||||
|
attestation_path = getattr(args, "attestation", None)
|
||||||
|
if not attestation_path:
|
||||||
|
return None
|
||||||
|
raw = load_attestation(attestation_path)
|
||||||
|
return validate_attestation(raw, repo, path=attestation_path, trusted_provider=getattr(args, "trust_attestation_provider", None))
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ class RepoConfig:
|
||||||
expected_axioms: list[str] = field(default_factory=lambda: STANDARD_LEAN_AXIOMS.copy())
|
expected_axioms: list[str] = field(default_factory=lambda: STANDARD_LEAN_AXIOMS.copy())
|
||||||
known_exclusions: list[str] = field(default_factory=list)
|
known_exclusions: list[str] = field(default_factory=list)
|
||||||
axiom_imports: list[str] = field(default_factory=list)
|
axiom_imports: list[str] = field(default_factory=list)
|
||||||
|
env_script: str | None = None
|
||||||
|
lean_project_dir: str | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, raw: dict[str, Any]) -> "RepoConfig":
|
def from_dict(cls, raw: dict[str, Any]) -> "RepoConfig":
|
||||||
|
|
@ -40,6 +42,8 @@ class RepoConfig:
|
||||||
expected_axioms=list(raw.get("expected_axioms") or STANDARD_LEAN_AXIOMS),
|
expected_axioms=list(raw.get("expected_axioms") or STANDARD_LEAN_AXIOMS),
|
||||||
known_exclusions=list(raw.get("known_exclusions") or []),
|
known_exclusions=list(raw.get("known_exclusions") or []),
|
||||||
axiom_imports=list(raw.get("axiom_imports") or []),
|
axiom_imports=list(raw.get("axiom_imports") or []),
|
||||||
|
env_script=raw.get("env_script"),
|
||||||
|
lean_project_dir=raw.get("lean_project_dir"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
@ -50,20 +51,25 @@ class AxiomAuditResult:
|
||||||
diagnostics: list[str] = field(default_factory=list)
|
diagnostics: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
def detect_tools() -> LeanTools:
|
def detect_tools(env: dict[str, str] | None = None) -> LeanTools:
|
||||||
lean = shutil.which("lean")
|
path = env.get("PATH") if env else None
|
||||||
lake = shutil.which("lake")
|
lean = shutil.which("lean", path=path)
|
||||||
|
lake = shutil.which("lake", path=path)
|
||||||
return LeanTools(
|
return LeanTools(
|
||||||
lean=lean,
|
lean=lean,
|
||||||
lake=lake,
|
lake=lake,
|
||||||
lean_version=_version([lean, "--version"]) if lean else None,
|
lean_version=_version([lean, "--version"], env=env) if lean else None,
|
||||||
lake_version=_version([lake, "--version"]) if lake else None,
|
lake_version=_version([lake, "--version"], env=env) if lake else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_lean_env(verification_dir: str | Path, base_env: dict[str, str] | None = None) -> dict[str, str]:
|
def build_lean_env(
|
||||||
|
verification_dir: str | Path,
|
||||||
|
base_env: dict[str, str] | None = None,
|
||||||
|
env_script: str | Path | None = None,
|
||||||
|
) -> dict[str, str]:
|
||||||
verification = Path(verification_dir).resolve()
|
verification = Path(verification_dir).resolve()
|
||||||
env = dict(base_env or os.environ)
|
env = _base_env(base_env, env_script)
|
||||||
candidates = [verification / "gen", verification]
|
candidates = [verification / "gen", verification]
|
||||||
existing = [str(path) for path in candidates if path.exists()]
|
existing = [str(path) for path in candidates if path.exists()]
|
||||||
old = env.get("LEAN_PATH")
|
old = env.get("LEAN_PATH")
|
||||||
|
|
@ -74,6 +80,19 @@ def build_lean_env(verification_dir: str | Path, base_env: dict[str, str] | None
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def env_script_available(env_script: str | Path | None) -> tuple[bool, str | None]:
|
||||||
|
if not env_script:
|
||||||
|
return True, None
|
||||||
|
path = Path(str(env_script)).expanduser()
|
||||||
|
if not path.exists():
|
||||||
|
return False, f"Verifier environment script does not exist: {path}"
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_lean_project_dir(path: str | Path | None, env: dict[str, str] | None = None) -> Path | None:
|
||||||
|
return _resolve_project_dir(path, env or os.environ)
|
||||||
|
|
||||||
|
|
||||||
def build_lean_invocation(
|
def build_lean_invocation(
|
||||||
file_path: str | Path,
|
file_path: str | Path,
|
||||||
tools: LeanTools,
|
tools: LeanTools,
|
||||||
|
|
@ -96,6 +115,8 @@ def lean_check_files(
|
||||||
verification_dir: str | Path,
|
verification_dir: str | Path,
|
||||||
timeout: int = 120,
|
timeout: int = 120,
|
||||||
log_dir: str | Path | None = None,
|
log_dir: str | Path | None = None,
|
||||||
|
env_script: str | Path | None = None,
|
||||||
|
lean_project_dir: str | Path | None = None,
|
||||||
) -> LeanCheckResult:
|
) -> LeanCheckResult:
|
||||||
if not files:
|
if not files:
|
||||||
return LeanCheckResult(
|
return LeanCheckResult(
|
||||||
|
|
@ -104,7 +125,19 @@ def lean_check_files(
|
||||||
missing_tool=None,
|
missing_tool=None,
|
||||||
diagnostics=[f"No Lean files discovered under {verification_dir}."],
|
diagnostics=[f"No Lean files discovered under {verification_dir}."],
|
||||||
)
|
)
|
||||||
tools = detect_tools()
|
ok_env, env_error = env_script_available(env_script)
|
||||||
|
if not ok_env:
|
||||||
|
return LeanCheckResult(
|
||||||
|
attempted=False,
|
||||||
|
ok=False,
|
||||||
|
missing_tool="env_script",
|
||||||
|
diagnostics=[
|
||||||
|
env_error or "Verifier environment script is not available.",
|
||||||
|
"Install or point --env-script at the pinned Lean/Aeneas environment; no extraction will be run.",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
env = build_lean_env(verification_dir, env_script=env_script)
|
||||||
|
tools = detect_tools(env)
|
||||||
if not tools.lean and not tools.lake:
|
if not tools.lean and not tools.lake:
|
||||||
return LeanCheckResult(
|
return LeanCheckResult(
|
||||||
attempted=False,
|
attempted=False,
|
||||||
|
|
@ -112,18 +145,23 @@ def lean_check_files(
|
||||||
missing_tool="lean",
|
missing_tool="lean",
|
||||||
diagnostics=["Neither lean nor lake was found on PATH."],
|
diagnostics=["Neither lean nor lake was found on PATH."],
|
||||||
)
|
)
|
||||||
verification = Path(verification_dir)
|
verification = Path(verification_dir).resolve()
|
||||||
use_lake_env = tools.lake is not None and ((verification / "lakefile.lean").exists() or (verification / "lakefile.toml").exists())
|
project_dir = _resolve_project_dir(lean_project_dir, env)
|
||||||
|
use_lake_env = tools.lake is not None and (
|
||||||
|
project_dir is not None or (verification / "lakefile.lean").exists() or (verification / "lakefile.toml").exists()
|
||||||
|
)
|
||||||
|
cwd = project_dir or verification
|
||||||
logs = _log_file(log_dir, "lean-check.log")
|
logs = _log_file(log_dir, "lean-check.log")
|
||||||
checked: list[str] = []
|
checked: list[str] = []
|
||||||
failed: list[str] = []
|
failed: list[str] = []
|
||||||
diagnostics: list[str] = []
|
diagnostics: list[str] = []
|
||||||
env = build_lean_env(verification)
|
|
||||||
with logs.open("w", encoding="utf-8") as log:
|
with logs.open("w", encoding="utf-8") as log:
|
||||||
log.write(f"lean: {tools.lean}\n")
|
log.write(f"lean: {tools.lean}\n")
|
||||||
log.write(f"lake: {tools.lake}\n")
|
log.write(f"lake: {tools.lake}\n")
|
||||||
log.write(f"lean_version: {tools.lean_version}\n")
|
log.write(f"lean_version: {tools.lean_version}\n")
|
||||||
log.write(f"lake_version: {tools.lake_version}\n\n")
|
log.write(f"lake_version: {tools.lake_version}\n\n")
|
||||||
|
log.write(f"env_script: {env_script or ''}\n")
|
||||||
|
log.write(f"lean_project_dir: {project_dir or ''}\n\n")
|
||||||
for path in files:
|
for path in files:
|
||||||
cmd = build_lean_invocation(path, tools, use_lake_env=use_lake_env, output_path=path.with_suffix(".olean"))
|
cmd = build_lean_invocation(path, tools, use_lake_env=use_lake_env, output_path=path.with_suffix(".olean"))
|
||||||
log.write("$ " + " ".join(cmd) + "\n")
|
log.write("$ " + " ".join(cmd) + "\n")
|
||||||
|
|
@ -134,7 +172,7 @@ def lean_check_files(
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
cwd=str(verification),
|
cwd=str(cwd),
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
|
|
@ -149,6 +187,7 @@ def lean_check_files(
|
||||||
log.write(f"\nexit_code: {completed.returncode}\n\n")
|
log.write(f"\nexit_code: {completed.returncode}\n\n")
|
||||||
if completed.returncode != 0:
|
if completed.returncode != 0:
|
||||||
failed.append(str(path))
|
failed.append(str(path))
|
||||||
|
diagnostics.extend(_dependency_diagnostics(completed.stdout + completed.stderr))
|
||||||
return LeanCheckResult(
|
return LeanCheckResult(
|
||||||
attempted=True,
|
attempted=True,
|
||||||
ok=not failed,
|
ok=not failed,
|
||||||
|
|
@ -167,21 +206,34 @@ def run_axiom_audit(
|
||||||
expected_axioms: list[str] | None = None,
|
expected_axioms: list[str] | None = None,
|
||||||
timeout: int = 120,
|
timeout: int = 120,
|
||||||
log_dir: str | Path | None = None,
|
log_dir: str | Path | None = None,
|
||||||
|
env_script: str | Path | None = None,
|
||||||
|
lean_project_dir: str | Path | None = None,
|
||||||
) -> AxiomAuditResult:
|
) -> AxiomAuditResult:
|
||||||
tools = detect_tools()
|
|
||||||
expected = expected_axioms or STANDARD_LEAN_AXIOMS
|
expected = expected_axioms or STANDARD_LEAN_AXIOMS
|
||||||
|
ok_env, env_error = env_script_available(env_script)
|
||||||
|
if not ok_env:
|
||||||
|
cert_results = [
|
||||||
|
CertificateAxiomResult(cert, "unknown", "not_checked", [], expected, [env_error or "Verifier environment unavailable."])
|
||||||
|
for cert in certificates
|
||||||
|
]
|
||||||
|
return AxiomAuditResult(False, False, "env_script", cert_results, None, [env_error or "Verifier environment unavailable."])
|
||||||
|
env = build_lean_env(verification_dir, env_script=env_script)
|
||||||
|
tools = detect_tools(env)
|
||||||
if not tools.lean and not tools.lake:
|
if not tools.lean and not tools.lake:
|
||||||
cert_results = [
|
cert_results = [
|
||||||
CertificateAxiomResult(cert, "unknown", "not_checked", [], expected, ["Neither lean nor lake was found on PATH."])
|
CertificateAxiomResult(cert, "unknown", "not_checked", [], expected, ["Neither lean nor lake was found on PATH."])
|
||||||
for cert in certificates
|
for cert in certificates
|
||||||
]
|
]
|
||||||
return AxiomAuditResult(False, False, "lean", cert_results, None, ["Neither lean nor lake was found on PATH."])
|
return AxiomAuditResult(False, False, "lean", cert_results, None, ["Neither lean nor lake was found on PATH."])
|
||||||
verification = Path(verification_dir)
|
verification = Path(verification_dir).resolve()
|
||||||
use_lake_env = tools.lake is not None and ((verification / "lakefile.lean").exists() or (verification / "lakefile.toml").exists())
|
project_dir = _resolve_project_dir(lean_project_dir, env)
|
||||||
|
use_lake_env = tools.lake is not None and (
|
||||||
|
project_dir is not None or (verification / "lakefile.lean").exists() or (verification / "lakefile.toml").exists()
|
||||||
|
)
|
||||||
|
cwd = project_dir or verification
|
||||||
logs = _log_file(log_dir, "axiom-audit.log")
|
logs = _log_file(log_dir, "axiom-audit.log")
|
||||||
imports_text = "\n".join(f"import {module}" for module in imports)
|
imports_text = "\n".join(f"import {module}" for module in imports)
|
||||||
prints_text = "\n".join(f"#print axioms {cert}" for cert in certificates)
|
prints_text = "\n".join(f"#print axioms {cert}" for cert in certificates)
|
||||||
env = build_lean_env(verification)
|
|
||||||
with tempfile.TemporaryDirectory(prefix="pacta-axioms-") as tmp:
|
with tempfile.TemporaryDirectory(prefix="pacta-axioms-") as tmp:
|
||||||
audit_file = Path(tmp) / "AxiomAudit.lean"
|
audit_file = Path(tmp) / "AxiomAudit.lean"
|
||||||
audit_file.write_text(f"{imports_text}\n\n{prints_text}\n", encoding="utf-8")
|
audit_file.write_text(f"{imports_text}\n\n{prints_text}\n", encoding="utf-8")
|
||||||
|
|
@ -193,7 +245,7 @@ def run_axiom_audit(
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
cwd=str(verification),
|
cwd=str(cwd),
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
output = completed.stdout + completed.stderr
|
output = completed.stdout + completed.stderr
|
||||||
|
|
@ -219,7 +271,7 @@ def run_axiom_audit(
|
||||||
missing_tool=None,
|
missing_tool=None,
|
||||||
certificates=cert_results,
|
certificates=cert_results,
|
||||||
log_path=str(logs),
|
log_path=str(logs),
|
||||||
diagnostics=[] if return_code == 0 else [f"Lean axiom audit exited with {return_code}."],
|
diagnostics=[] if return_code == 0 else [f"Lean axiom audit exited with {return_code}.", *_dependency_diagnostics(output)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -249,7 +301,7 @@ def _mentions_no_axioms(text: str) -> bool:
|
||||||
return "no axioms" in lowered or "does not depend on any axioms" in lowered
|
return "no axioms" in lowered or "does not depend on any axioms" in lowered
|
||||||
|
|
||||||
|
|
||||||
def _version(cmd: list[str | None]) -> str | None:
|
def _version(cmd: list[str | None], env: dict[str, str] | None = None) -> str | None:
|
||||||
if not cmd[0]:
|
if not cmd[0]:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
|
|
@ -259,6 +311,7 @@ def _version(cmd: list[str | None]) -> str | None:
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=10,
|
timeout=10,
|
||||||
|
env=env,
|
||||||
)
|
)
|
||||||
except (OSError, subprocess.TimeoutExpired):
|
except (OSError, subprocess.TimeoutExpired):
|
||||||
return None
|
return None
|
||||||
|
|
@ -271,3 +324,52 @@ def _log_file(log_dir: str | Path | None, name: str) -> Path:
|
||||||
directory = Path(log_dir) if log_dir else Path(".pacta")
|
directory = Path(log_dir) if log_dir else Path(".pacta")
|
||||||
directory.mkdir(parents=True, exist_ok=True)
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
return directory / name
|
return directory / name
|
||||||
|
|
||||||
|
|
||||||
|
def _base_env(base_env: dict[str, str] | None, env_script: str | Path | None) -> dict[str, str]:
|
||||||
|
env = dict(base_env or os.environ)
|
||||||
|
if not env_script:
|
||||||
|
return env
|
||||||
|
path = Path(str(env_script)).expanduser()
|
||||||
|
if not path.exists():
|
||||||
|
return env
|
||||||
|
command = f"source {shlex.quote(str(path))}; env"
|
||||||
|
try:
|
||||||
|
completed = subprocess.run(
|
||||||
|
["/bin/zsh", "-lc", command],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=20,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.TimeoutExpired):
|
||||||
|
return env
|
||||||
|
if completed.returncode != 0:
|
||||||
|
return env
|
||||||
|
for line in completed.stdout.splitlines():
|
||||||
|
if "=" in line:
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
env[key] = value
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_project_dir(path: str | Path | None, env: dict[str, str]) -> Path | None:
|
||||||
|
if not path:
|
||||||
|
return None
|
||||||
|
expanded = os.path.expandvars(str(path))
|
||||||
|
for key, value in env.items():
|
||||||
|
expanded = expanded.replace(f"${key}", value).replace(f"${{{key}}}", value)
|
||||||
|
resolved = Path(expanded).expanduser()
|
||||||
|
return resolved if resolved.exists() else None
|
||||||
|
|
||||||
|
|
||||||
|
def _dependency_diagnostics(output: str) -> list[str]:
|
||||||
|
diagnostics: list[str] = []
|
||||||
|
modules = sorted(set(re.findall(r"unknown module prefix '([^']+)'", output)))
|
||||||
|
for module in modules:
|
||||||
|
diagnostics.append(f"Missing Lean dependency/module prefix: {module}")
|
||||||
|
missing_objects = sorted(set(re.findall(r"object file '([^']+)' of module ([A-Za-z0-9_'.]+) does not exist", output)))
|
||||||
|
for _, module in missing_objects[:8]:
|
||||||
|
diagnostics.append(f"Missing compiled Lean object for module: {module}")
|
||||||
|
return diagnostics
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,11 @@ def render_markdown(card: dict[str, Any]) -> str:
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
"## macOS replay status",
|
"## Replay and attestation status",
|
||||||
"",
|
"",
|
||||||
|
f"- Evidence mode: {evidence.get('evidence_mode') or 'local_or_fixture'}",
|
||||||
|
f"- Attestation provider: {evidence.get('attestation_provider') or 'not used'}",
|
||||||
|
f"- Attestation signature: {evidence.get('attestation_signature_status') or 'not used'}",
|
||||||
f"- Lean: {evidence.get('lean_version') or 'not detected'}",
|
f"- Lean: {evidence.get('lean_version') or 'not detected'}",
|
||||||
f"- Lake: {evidence.get('lake_version') or 'not detected'}",
|
f"- Lake: {evidence.get('lake_version') or 'not detected'}",
|
||||||
f"- Check log: {evidence.get('check_log_path') or 'not recorded'}",
|
f"- Check log: {evidence.get('check_log_path') or 'not recorded'}",
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,29 @@ def score_claim_card(card: dict[str, Any]) -> RiskAssessment:
|
||||||
kind = card.get("kind", "unknown")
|
kind = card.get("kind", "unknown")
|
||||||
certificates = card.get("certificates") or []
|
certificates = card.get("certificates") or []
|
||||||
exclusions = [str(item).lower() for item in card.get("exclusions") or []]
|
exclusions = [str(item).lower() for item in card.get("exclusions") or []]
|
||||||
|
replay_blockers = list((card.get("evidence") or {}).get("replay_blockers") or [])
|
||||||
constraints = list(card.get("risk", {}).get("deployment_constraints") or [])
|
constraints = list(card.get("risk", {}).get("deployment_constraints") or [])
|
||||||
|
evidence = card.get("evidence") or {}
|
||||||
|
attested = evidence.get("evidence_mode") == "third_party_attestation"
|
||||||
blockers: list[str] = []
|
blockers: list[str] = []
|
||||||
|
|
||||||
|
if replay_blockers:
|
||||||
|
blockers.extend(replay_blockers)
|
||||||
|
if any(_is_attestation_blocker(blocker) for blocker in replay_blockers):
|
||||||
|
return RiskAssessment(
|
||||||
|
"R0",
|
||||||
|
"Third-party attestation evidence was supplied but not accepted, so no usable verification evidence is available.",
|
||||||
|
blockers,
|
||||||
|
constraints,
|
||||||
|
)
|
||||||
|
if any(_is_verifier_capability_blocker(blocker) for blocker in replay_blockers):
|
||||||
|
return RiskAssessment(
|
||||||
|
"R0",
|
||||||
|
"Local verifier capability is unavailable, so no usable machine-checked replay evidence was produced on this machine.",
|
||||||
|
blockers,
|
||||||
|
constraints,
|
||||||
|
)
|
||||||
|
|
||||||
clean_proven = [
|
clean_proven = [
|
||||||
cert
|
cert
|
||||||
for cert in certificates
|
for cert in certificates
|
||||||
|
|
@ -75,6 +95,14 @@ def score_claim_card(card: dict[str, Any]) -> RiskAssessment:
|
||||||
full_eddsa_excluded = any("eddsa" in item or "signature" in item for item in exclusions)
|
full_eddsa_excluded = any("eddsa" in item or "signature" in item for item in exclusions)
|
||||||
if not full_eddsa_excluded:
|
if not full_eddsa_excluded:
|
||||||
blockers.append("Full EdDSA boundary is not explicitly excluded in the claim card.")
|
blockers.append("Full EdDSA boundary is not explicitly excluded in the claim card.")
|
||||||
|
if attested:
|
||||||
|
provider = evidence.get("attestation_provider") or "unknown provider"
|
||||||
|
return RiskAssessment(
|
||||||
|
"R3",
|
||||||
|
f"Trusted third-party provider {provider} attests that configured field and Edwards certificates are proven and axiom-clean for a specific lower-layer backend boundary.",
|
||||||
|
blockers,
|
||||||
|
constraints,
|
||||||
|
)
|
||||||
return RiskAssessment(
|
return RiskAssessment(
|
||||||
"R3",
|
"R3",
|
||||||
"Configured field and Edwards implementation certificates are proven and axiom-clean for a specific lower-layer backend boundary.",
|
"Configured field and Edwards implementation certificates are proven and axiom-clean for a specific lower-layer backend boundary.",
|
||||||
|
|
@ -110,3 +138,23 @@ def score_claim_card(card: dict[str, Any]) -> RiskAssessment:
|
||||||
blockers,
|
blockers,
|
||||||
constraints,
|
constraints,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_verifier_capability_blocker(blocker: str) -> bool:
|
||||||
|
lowered = blocker.lower()
|
||||||
|
return any(
|
||||||
|
token in lowered
|
||||||
|
for token in (
|
||||||
|
"env_script",
|
||||||
|
"environment script",
|
||||||
|
"neither lean nor lake",
|
||||||
|
"missing lean dependency",
|
||||||
|
"unknown module prefix",
|
||||||
|
"missing verifier capability",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_attestation_blocker(blocker: str) -> bool:
|
||||||
|
lowered = blocker.lower()
|
||||||
|
return "attestation" in lowered or "trusted provider" in lowered
|
||||||
|
|
|
||||||
32
tests/test_attestation.py
Normal file
32
tests/test_attestation.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
from pacta.attestation import load_attestation, validate_attestation
|
||||||
|
from pacta.claims import build_claim_card
|
||||||
|
from pacta.config import RepoConfig
|
||||||
|
|
||||||
|
|
||||||
|
def _repo():
|
||||||
|
return RepoConfig(
|
||||||
|
name="dalek-ed25519-verified",
|
||||||
|
url="https://github.com/saymrwulf/dalek-ed25519-verified.git",
|
||||||
|
kind="ed25519",
|
||||||
|
verified_backend="serial/u64",
|
||||||
|
certificates=["CurveFieldProofs.fieldImplementation", "CurveFieldProofs.edwardsImplementation"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_trusted_attestation_can_drive_r3_claim(tmp_path):
|
||||||
|
raw = load_attestation("examples/dalek-ed25519.attestation.yaml")
|
||||||
|
result = validate_attestation(raw, _repo(), path="examples/dalek-ed25519.attestation.yaml", trusted_provider="example-proof-checker.invalid")
|
||||||
|
card = build_claim_card(_repo(), tmp_path, attestation=result)
|
||||||
|
assert result.accepted
|
||||||
|
assert card["risk"]["level"] == "R3"
|
||||||
|
assert "Trusted third-party provider" in card["risk"]["rationale"]
|
||||||
|
assert card["evidence"]["evidence_mode"] == "third_party_attestation"
|
||||||
|
assert any("Third-party proof-checking" in item for item in card["trusted_base"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_untrusted_attestation_scores_r0(tmp_path):
|
||||||
|
raw = load_attestation("examples/dalek-ed25519.attestation.yaml")
|
||||||
|
result = validate_attestation(raw, _repo(), path="examples/dalek-ed25519.attestation.yaml")
|
||||||
|
card = build_claim_card(_repo(), tmp_path, attestation=result)
|
||||||
|
assert not result.accepted
|
||||||
|
assert card["risk"]["level"] == "R0"
|
||||||
9
tests/test_doctor.py
Normal file
9
tests/test_doctor.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from pacta.cli import main
|
||||||
|
|
||||||
|
|
||||||
|
def test_doctor_reports_missing_env_script(capsys):
|
||||||
|
code = main(["doctor", "--config", "examples/repos.yaml", "--repo-name", "dalek-ed25519-verified", "--env-script", "/tmp/pacta-missing-env.sh"])
|
||||||
|
output = capsys.readouterr().out
|
||||||
|
assert code == 1
|
||||||
|
assert "env_script_status: missing" in output
|
||||||
|
assert "remediation:" in output
|
||||||
|
|
@ -22,4 +22,5 @@ def test_repo_config_merges_backend_warning():
|
||||||
def test_load_examples_config():
|
def test_load_examples_config():
|
||||||
config = load_config("examples/repos.yaml")
|
config = load_config("examples/repos.yaml")
|
||||||
assert config.repo_named("dalek-ed25519-verified").kind == "ed25519"
|
assert config.repo_named("dalek-ed25519-verified").kind == "ed25519"
|
||||||
|
assert config.repo_named("dalek-ed25519-verified").env_script == "~/aeneas-toolchain/env.sh"
|
||||||
assert config.repo_named("pasta-pallas-verified").kind == "pasta_pallas"
|
assert config.repo_named("pasta-pallas-verified").kind == "pasta_pallas"
|
||||||
|
|
|
||||||
|
|
@ -44,3 +44,16 @@ def test_risk_ordering():
|
||||||
assert risk_at_least("R3", "R3")
|
assert risk_at_least("R3", "R3")
|
||||||
assert not risk_at_least("R2", "R3")
|
assert not risk_at_least("R2", "R3")
|
||||||
assert not risk_at_least("RX", "R3")
|
assert not risk_at_least("RX", "R3")
|
||||||
|
|
||||||
|
|
||||||
|
def test_verifier_capability_blocker_scores_r0():
|
||||||
|
card = {
|
||||||
|
"kind": "ed25519",
|
||||||
|
"certificates": [
|
||||||
|
{"name": "CurveFieldProofs.fieldImplementation", "status": "unknown", "axiom_status": "not_checked"},
|
||||||
|
],
|
||||||
|
"evidence": {"replay_blockers": ["Missing Lean dependency/module prefix: Aeneas"]},
|
||||||
|
}
|
||||||
|
result = score_claim_card(card)
|
||||||
|
assert result.level == "R0"
|
||||||
|
assert "verifier capability" in result.rationale
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue