From 81b2ab467b8522dadcf1cf548074800a158ecb60 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Fri, 10 Jul 2026 18:56:28 -0700 Subject: [PATCH 01/31] test(sync): reproduce missing threshold attestation adapter --- tests/test_sync_core_human_attestation.py | 169 ++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 tests/test_sync_core_human_attestation.py diff --git a/tests/test_sync_core_human_attestation.py b/tests/test_sync_core_human_attestation.py new file mode 100644 index 000000000..bdb5c7299 --- /dev/null +++ b/tests/test_sync_core_human_attestation.py @@ -0,0 +1,169 @@ +"""Contract tests for protected threshold-Ed25519 human approvals.""" + +import base64 +import json +import subprocess +from datetime import datetime, timedelta, timezone +from pathlib import Path, PurePosixPath + +import pytest +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from pdd.sync_core import ( + EvidenceOutcome, + HumanAttestationError, + HumanAttestationRequest, + HumanAttestationVerifier, + RunBinding, + UnitId, + VerificationObligation, + VerificationProfile, + load_human_attestation_policy, +) + + +NOW = datetime(2026, 7, 10, 12, 0, tzinfo=timezone.utc) +UNIT = UnitId("repository-1", PurePosixPath("prompts/widget.prompt"), "python") + + +def _git(root: Path, *args: str) -> str: + return subprocess.run( + ["git", *args], cwd=root, capture_output=True, text=True, check=True + ).stdout.strip() + + +def _key(seed: bytes) -> Ed25519PrivateKey: + return Ed25519PrivateKey.from_private_bytes(seed * 32) + + +def _public(key: Ed25519PrivateKey) -> str: + return base64.b64encode(key.public_key().public_bytes_raw()).decode("ascii") + + +def _request() -> HumanAttestationRequest: + obligation = VerificationObligation( + "human", "human-attestation", "threshold-ed25519", "policy-digest", + ("CONTRACT-SHA256:abc",), (), + ) + profile = VerificationProfile( + UNIT, (obligation,), ("CONTRACT-SHA256:abc",), "profile-digest" + ) + return HumanAttestationRequest( + profile, + obligation, + RunBinding("snapshot-digest", "base-sha", "head-sha", "artifact-digest"), + ) + + +def _approval( + key: Ed25519PrivateKey, + identity: str, + request: HumanAttestationRequest, + *, + nonce: str, + decision: str = "PASS", +) -> dict[str, object]: + payload: dict[str, object] = { + "approval_id": f"approval-{identity}", + "identity": identity, + "key_id": identity, + "decision": decision, + "repository_id": request.profile.unit_id.repository_id, + "prompt_relpath": request.profile.unit_id.prompt_relpath.as_posix(), + "language_id": request.profile.unit_id.language_id, + "requirement_ids": list(request.obligation.requirement_ids), + "obligation_id": request.obligation.obligation_id, + "snapshot_digest": request.binding.snapshot_digest, + "profile_digest": request.profile.profile_digest, + "protected_base_sha": request.binding.base_sha, + "checked_head_sha": request.binding.head_sha, + "artifact_closure_digest": request.binding.artifact_closure_digest, + "policy_version": "v1", + "policy_digest": "POLICY_DIGEST", + "nonce": nonce, + "issued_at": NOW.isoformat(), + "expires_at": (NOW + timedelta(hours=1)).isoformat(), + } + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + payload["signature"] = base64.b64encode(key.sign(encoded)).decode("ascii") + return payload + + +def _repository(tmp_path: Path) -> tuple[Path, str, Path, tuple[Ed25519PrivateKey, ...]]: + root = tmp_path / "repo" + store = tmp_path / "external-store" + root.mkdir() + store.mkdir() + _git(root, "init", "-q") + _git(root, "config", "user.email", "attestation@example.com") + _git(root, "config", "user.name", "Attestation Test") + keys = (_key(b"a"), _key(b"b"), _key(b"c")) + policy = { + "version": "v1", + "threshold": 2, + "required_role": "approver", + "signers": [ + {"identity": identity, "key_id": identity, "public_key": _public(key), "roles": ["approver"]} + for identity, key in zip(("alice", "bob", "carol"), keys) + ], + "revoked_identities": [], + "revoked_key_ids": [], + } + policy_path = root / ".pdd/human-attestation-policy.json" + policy_path.parent.mkdir() + policy_path.write_text(json.dumps(policy), encoding="utf-8") + (root / "prompts").mkdir() + (root / "prompts/widget.prompt").write_text("opaque contract", encoding="utf-8") + _git(root, "add", ".") + _git(root, "commit", "-q", "-m", "protected human attestation policy") + return root, _git(root, "rev-parse", "HEAD"), store, keys + + +def _write_approvals(store: Path, approvals: list[dict[str, object]]) -> None: + (store / "approvals.json").write_text(json.dumps({"approvals": approvals}), encoding="utf-8") + + +def test_external_threshold_approvals_pass_every_opaque_requirement(tmp_path: Path) -> None: + root, base, store, keys = _repository(tmp_path) + request = _request() + policy = load_human_attestation_policy(root, base, store) + approvals = [ + _approval(keys[0], "alice", request, nonce="alice-1"), + _approval(keys[1], "bob", request, nonce="bob-1"), + ] + for approval in approvals: + approval["policy_digest"] = policy.digest + approval["signature"] = "" + encoded = json.dumps({key: value for key, value in approval.items() if key != "signature"}, sort_keys=True, separators=(",", ":")).encode() + key = keys[("alice", "bob").index(approval["identity"])] + approval["signature"] = base64.b64encode(key.sign(encoded)).decode("ascii") + _write_approvals(store, approvals) + verifier = HumanAttestationVerifier(policy, store / "replay.json") + assert verifier.verify(request, now=NOW) is EvidenceOutcome.PASS + + +@pytest.mark.parametrize("mutator", ["candidate_policy", "single", "mixed", "rebind"]) +def test_human_attestation_fails_closed_for_untrusted_or_incomplete_approvals( + tmp_path: Path, mutator: str +) -> None: + root, base, store, keys = _repository(tmp_path) + request = _request() + policy = load_human_attestation_policy(root, base, store) + approvals = [ + _approval(keys[0], "alice", request, nonce="same"), + _approval(keys[1], "bob", request, nonce="bob-1"), + ] + if mutator == "candidate_policy": + (root / ".pdd/human-attestation-policy.json").write_text( + json.dumps({"threshold": 1, "signers": []}), encoding="utf-8" + ) + if mutator == "single": + approvals.pop() + if mutator == "mixed": + approvals[1]["decision"] = "FAIL" + if mutator == "rebind": + approvals[1]["nonce"] = "same" + _write_approvals(store, approvals) + verifier = HumanAttestationVerifier(policy, store / "replay.json") + with pytest.raises(HumanAttestationError): + verifier.verify(request, now=NOW) From e13641da870c6a14f3b5db6d0356c225b569dd92 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Fri, 10 Jul 2026 19:02:33 -0700 Subject: [PATCH 02/31] fix(sync): verify threshold human attestations --- pdd/sync_core/__init__.py | 12 + pdd/sync_core/human_attestation.py | 289 ++++++++++++++++++++++ pdd/sync_core/runner.py | 55 +++- tests/test_sync_core_human_attestation.py | 93 ++++++- 4 files changed, 440 insertions(+), 9 deletions(-) create mode 100644 pdd/sync_core/human_attestation.py diff --git a/pdd/sync_core/__init__.py b/pdd/sync_core/__init__.py index e16779e6e..116d5c0d3 100644 --- a/pdd/sync_core/__init__.py +++ b/pdd/sync_core/__init__.py @@ -29,6 +29,13 @@ initialize_repository_identity, read_repository_identity, ) +from .human_attestation import ( + HumanAttestationError, + HumanAttestationPolicy, + HumanAttestationRequest, + HumanAttestationVerifier, + load_human_attestation_policy, +) from .fingerprint_store import ( CorruptFingerprintError, FingerprintStore, @@ -164,6 +171,10 @@ "FingerprintProvenance", "FingerprintStore", "FingerprintStoreError", + "HumanAttestationError", + "HumanAttestationPolicy", + "HumanAttestationRequest", + "HumanAttestationVerifier", "FileReplayStore", "FinalizeResult", "GlobalCertificateOptions", @@ -240,6 +251,7 @@ "load_verification_profiles", "load_attestation", "load_candidate_artifact_provenance", + "load_human_attestation_policy", "load_trust_policy", "load_sync_waivers", "initialize_repository_identity", diff --git a/pdd/sync_core/human_attestation.py b/pdd/sync_core/human_attestation.py new file mode 100644 index 000000000..b9442dfc7 --- /dev/null +++ b/pdd/sync_core/human_attestation.py @@ -0,0 +1,289 @@ +"""Protected threshold-Ed25519 verification for external human approvals.""" + +# pylint: disable=too-many-instance-attributes,too-many-locals,too-many-branches,too-many-statements + +from __future__ import annotations + +import base64 +import hashlib +import json +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from pathlib import Path, PurePosixPath +from typing import Any, Mapping + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + +from .git_io import read_git_blob +from .trust import AttestationError, FileReplayStore +from .types import EvidenceOutcome, VerificationObligation, VerificationProfile + + +HUMAN_ATTESTATION_POLICY_PATH = PurePosixPath(".pdd/human-attestation-policy.json") +_APPROVALS_FILENAME = "approvals.json" + + +class HumanAttestationError(AttestationError): + """Raised when threshold human approval cannot be trusted.""" + + +@dataclass(frozen=True) +class HumanAttestationSigner: + """One protected signer identity and its permitted roles.""" + + identity: str + key_id: str + public_key: bytes + roles: frozenset[str] + not_before: datetime | None + not_after: datetime | None + + +@dataclass(frozen=True) +class HumanAttestationPolicy: + """Protected immutable quorum policy for human approval verification.""" + + version: str + digest: str + threshold: int + maximum_lifetime_seconds: int + required_role: str + signers: Mapping[str, HumanAttestationSigner] + revoked_identities: frozenset[str] + revoked_key_ids: frozenset[str] + candidate_root: Path + external_store: Path + + +@dataclass(frozen=True) +class HumanAttestationRequest: + """Exact immutable closure a human approval must cover.""" + + profile: VerificationProfile + obligation: VerificationObligation + binding: Any + + def __post_init__(self) -> None: + if ( + self.obligation.kind != "human-attestation" + or self.obligation.validator_id != "threshold-ed25519" + ): + raise ValueError("human attestation request requires threshold-ed25519") + if self.obligation not in self.profile.obligations: + raise ValueError("human attestation obligation is outside the profile") + if not self.binding.artifact_closure_digest: + raise ValueError("human attestation requires an artifact closure digest") + + +def _required_string(payload: Mapping[str, Any], name: str) -> str: + value = payload.get(name) + if not isinstance(value, str) or not value: + raise HumanAttestationError(f"human attestation field {name!r} is invalid") + return value + + +def _timestamp(value: object, name: str) -> datetime: + if not isinstance(value, str): + raise HumanAttestationError(f"human attestation field {name!r} is invalid") + try: + parsed = datetime.fromisoformat(value) + except ValueError as exc: + raise HumanAttestationError(f"human attestation field {name!r} is malformed") from exc + if parsed.tzinfo is None: + raise HumanAttestationError(f"human attestation field {name!r} lacks a timezone") + return parsed.astimezone(timezone.utc) + + +def _optional_timestamp(value: object, name: str) -> datetime | None: + return None if value is None else _timestamp(value, name) + + +def load_human_attestation_policy( + root: Path, protected_base_ref: str, external_store: Path +) -> HumanAttestationPolicy: + """Load quorum authority only from the protected base Git tree.""" + root = Path(root).resolve() + external = Path(external_store).resolve() + try: + external.relative_to(root) + except ValueError: + pass + else: + raise HumanAttestationError( + "human approval store must be outside the candidate checkout" + ) + raw = read_git_blob(root, protected_base_ref, HUMAN_ATTESTATION_POLICY_PATH) + if raw is None: + raise HumanAttestationError("protected base has no human attestation policy") + try: + payload = json.loads(raw) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise HumanAttestationError("protected human attestation policy is malformed") from exc + if not isinstance(payload, dict): + raise HumanAttestationError("protected human attestation policy must be an object") + version = _required_string(payload, "version") + threshold = payload.get("threshold") + maximum_lifetime_seconds = payload.get("maximum_lifetime_seconds") + required_role = _required_string(payload, "required_role") + rows = payload.get("signers") + if ( + not isinstance(threshold, int) + or threshold < 1 + or not isinstance(maximum_lifetime_seconds, int) + or maximum_lifetime_seconds < 1 + or not isinstance(rows, list) + ): + raise HumanAttestationError("protected human attestation threshold is invalid") + signers: dict[str, HumanAttestationSigner] = {} + key_ids: set[str] = set() + for row in rows: + if not isinstance(row, dict): + raise HumanAttestationError("protected human signer is malformed") + identity = _required_string(row, "identity") + key_id = _required_string(row, "key_id") + roles = row.get("roles") + if ( + not isinstance(roles, list) + or not roles + or not all(isinstance(role, str) and role for role in roles) + ): + raise HumanAttestationError("protected human signer roles are invalid") + try: + public_key = base64.b64decode(_required_string(row, "public_key"), validate=True) + except ValueError as exc: + raise HumanAttestationError("protected human signer key is malformed") from exc + if len(public_key) != 32 or identity in signers or key_id in key_ids: + raise HumanAttestationError("protected human signer identity or key is duplicate") + signer = HumanAttestationSigner( + identity, key_id, public_key, frozenset(roles), + _optional_timestamp(row.get("not_before"), "not_before"), + _optional_timestamp(row.get("not_after"), "not_after"), + ) + if signer.not_before and signer.not_after and signer.not_after <= signer.not_before: + raise HumanAttestationError("protected human signer validity is invalid") + signers[identity] = signer + key_ids.add(key_id) + revoked_identities = frozenset(payload.get("revoked_identities", [])) + revoked_key_ids = frozenset(payload.get("revoked_key_ids", [])) + if not all(isinstance(value, str) for value in revoked_identities | revoked_key_ids): + raise HumanAttestationError("protected human revocation list is invalid") + if threshold > len(signers) or not any( + required_role in signer.roles for signer in signers.values() + ): + raise HumanAttestationError("protected human attestation policy is unsatisfiable") + return HumanAttestationPolicy( + version, hashlib.sha256(raw).hexdigest(), threshold, + maximum_lifetime_seconds, required_role, signers, revoked_identities, + revoked_key_ids, root, external, + ) + + +def _canonical_payload(approval: Mapping[str, Any]) -> bytes: + unsigned = {key: value for key, value in approval.items() if key != "signature"} + return json.dumps(unsigned, sort_keys=True, separators=(",", ":")).encode() + + +class HumanAttestationVerifier: # pylint: disable=too-few-public-methods + """Verify external, thresholded human decisions without any signing surface.""" + + def __init__(self, policy: HumanAttestationPolicy, replay_ledger_path: Path) -> None: + self.policy = policy + replay = Path(replay_ledger_path).resolve() + try: + replay.relative_to(policy.candidate_root) + except ValueError: + pass + else: + raise HumanAttestationError( + "human attestation replay ledger must be outside the candidate checkout" + ) + self._replay = FileReplayStore(replay) + + def _load_approvals(self, store: Path) -> list[Mapping[str, Any]]: + path = Path(store).resolve() / _APPROVALS_FILENAME + if path.is_symlink() or not path.is_file(): + raise HumanAttestationError("external human approval store is missing or unsafe") + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise HumanAttestationError("external human approval store is malformed") from exc + approvals = payload.get("approvals") if isinstance(payload, dict) else None + if not isinstance(approvals, list) or not all(isinstance(item, dict) for item in approvals): + raise HumanAttestationError("external human approvals must be an object list") + return approvals + + def _verify_approval( + self, approval: Mapping[str, Any], request: HumanAttestationRequest, now: datetime + ) -> str: + identity = _required_string(approval, "identity") + signer = self.policy.signers.get(identity) + if signer is None or identity in self.policy.revoked_identities: + raise HumanAttestationError("human approval identity is not authorized") + if ( + _required_string(approval, "key_id") != signer.key_id + or signer.key_id in self.policy.revoked_key_ids + ): + raise HumanAttestationError("human approval key is revoked or rotated") + if self.policy.required_role not in signer.roles: + raise HumanAttestationError("human approval signer lacks the required role") + issued = _timestamp(approval.get("issued_at"), "issued_at") + expires = _timestamp(approval.get("expires_at"), "expires_at") + if ( + expires <= issued + or expires - issued > timedelta(seconds=self.policy.maximum_lifetime_seconds) + or issued > now + or expires <= now + ): + raise HumanAttestationError("human approval is stale or not yet valid") + if ( + signer.not_before and issued < signer.not_before + ) or ( + signer.not_after and issued >= signer.not_after + ): + raise HumanAttestationError("human approval signer is outside its validity window") + expected = { + "decision": "PASS", "repository_id": request.profile.unit_id.repository_id, + "prompt_relpath": request.profile.unit_id.prompt_relpath.as_posix(), + "language_id": request.profile.unit_id.language_id, + "requirement_ids": list(request.obligation.requirement_ids), + "obligation_id": request.obligation.obligation_id, + "snapshot_digest": request.binding.snapshot_digest, + "profile_digest": request.profile.profile_digest, + "protected_base_sha": request.binding.base_sha, + "checked_head_sha": request.binding.head_sha, + "artifact_closure_digest": request.binding.artifact_closure_digest, + "policy_version": self.policy.version, "policy_digest": self.policy.digest, + } + if any(approval.get(key) != value for key, value in expected.items()): + raise HumanAttestationError("human approval does not bind the checked closure") + try: + signature = base64.b64decode( + _required_string(approval, "signature"), validate=True + ) + Ed25519PublicKey.from_public_bytes(signer.public_key).verify( + signature, _canonical_payload(approval) + ) + except (ValueError, InvalidSignature) as exc: + raise HumanAttestationError("human approval signature is invalid") from exc + approval_id = _required_string(approval, "approval_id") + nonce = _required_string(approval, "nonce") + digest = hashlib.sha256(_canonical_payload(approval)).hexdigest() + self._replay.consume(identity, nonce, f"{approval_id}:{digest}") + return identity + + def verify( + self, request: HumanAttestationRequest, *, now: datetime | None = None + ) -> EvidenceOutcome: + """Return PASS only when distinct authorized humans reach threshold.""" + instant = now or datetime.now(timezone.utc) + approvals = self._load_approvals(self.policy.external_store) + try: + identities = { + self._verify_approval(approval, request, instant) for approval in approvals + } + except AttestationError as exc: + raise HumanAttestationError(str(exc)) from exc + if len(identities) < self.policy.threshold: + raise HumanAttestationError("human approval threshold is not met") + return EvidenceOutcome.PASS diff --git a/pdd/sync_core/runner.py b/pdd/sync_core/runner.py index 2b5fc7873..edd671317 100644 --- a/pdd/sync_core/runner.py +++ b/pdd/sync_core/runner.py @@ -29,6 +29,12 @@ ) from .isolation import SECRET_ENV_MARKERS, untrusted_child_environment from .git_io import read_git_blob, read_git_mode +from .human_attestation import ( + HumanAttestationError, + HumanAttestationRequest, + HumanAttestationVerifier, + load_human_attestation_policy, +) from .types import ( EvidenceOutcome, ObligationEvidence, @@ -116,6 +122,8 @@ class RunnerConfig: playwright_command: tuple[str, ...] | None = None playwright_toolchain_manifest: Path | None = None playwright_toolchain_identity: str | None = None + human_attestation_store: Path | None = None + human_attestation_replay_ledger: Path | None = None @dataclass(frozen=True) @@ -135,6 +143,7 @@ class RunBinding: snapshot_digest: str base_sha: str head_sha: str + artifact_closure_digest: str = "" @dataclass(frozen=True) @@ -3238,6 +3247,40 @@ def run_obligation( ) +def _run_human_attestation( + root: Path, + profile: VerificationProfile, + obligation: VerificationObligation, + binding: RunBinding, + config: RunnerConfig, +) -> RunnerExecution: + """Verify an external threshold decision without launching a child process.""" + if config.human_attestation_store is None or config.human_attestation_replay_ledger is None: + return RunnerExecution( + obligation.obligation_id, EvidenceOutcome.ERROR, + obligation.validator_config_digest, + "external human attestation store and replay ledger are required", + ) + try: + policy = load_human_attestation_policy( + root, binding.base_sha, config.human_attestation_store + ) + if obligation.validator_config_digest != policy.digest: + raise HumanAttestationError("human obligation does not bind protected policy") + outcome = HumanAttestationVerifier( + policy, config.human_attestation_replay_ledger + ).verify(HumanAttestationRequest(profile, obligation, binding)) + except (HumanAttestationError, ValueError) as exc: + return RunnerExecution( + obligation.obligation_id, EvidenceOutcome.QUARANTINED, + obligation.validator_config_digest, str(exc), + ) + return RunnerExecution( + obligation.obligation_id, outcome, obligation.validator_config_digest, + "external threshold human approval verified", + ) + + def run_profile( root: Path, profile: VerificationProfile, @@ -3257,11 +3300,13 @@ def run_profile( if identity is not None: config = replace(config, playwright_toolchain_identity=identity) executions = tuple( - run_obligation( - root, - obligation, - base_sha=binding.base_sha, - head_sha=binding.head_sha, + _run_human_attestation(root, profile, obligation, binding, config) + if ( + obligation.kind == "human-attestation" + and obligation.validator_id == "threshold-ed25519" + ) + else run_obligation( + root, obligation, base_sha=binding.base_sha, head_sha=binding.head_sha, config=config, ) for obligation in profile.obligations diff --git a/tests/test_sync_core_human_attestation.py b/tests/test_sync_core_human_attestation.py index bdb5c7299..ccf3997c1 100644 --- a/tests/test_sync_core_human_attestation.py +++ b/tests/test_sync_core_human_attestation.py @@ -10,19 +10,23 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from pdd.sync_core import ( + AttestationIssue, + AttestationSigner, EvidenceOutcome, HumanAttestationError, HumanAttestationRequest, HumanAttestationVerifier, + RunnerConfig, RunBinding, UnitId, VerificationObligation, VerificationProfile, load_human_attestation_policy, + run_profile, ) -NOW = datetime(2026, 7, 10, 12, 0, tzinfo=timezone.utc) +NOW = datetime.now(timezone.utc).replace(microsecond=0) UNIT = UnitId("repository-1", PurePosixPath("prompts/widget.prompt"), "python") @@ -101,6 +105,7 @@ def _repository(tmp_path: Path) -> tuple[Path, str, Path, tuple[Ed25519PrivateKe policy = { "version": "v1", "threshold": 2, + "maximum_lifetime_seconds": 3600, "required_role": "approver", "signers": [ {"identity": identity, "key_id": identity, "public_key": _public(key), "roles": ["approver"]} @@ -123,6 +128,18 @@ def _write_approvals(store: Path, approvals: list[dict[str, object]]) -> None: (store / "approvals.json").write_text(json.dumps({"approvals": approvals}), encoding="utf-8") +def _sign(key: Ed25519PrivateKey, approval: dict[str, object]) -> None: + approval["signature"] = base64.b64encode( + key.sign( + json.dumps( + {name: value for name, value in approval.items() if name != "signature"}, + sort_keys=True, + separators=(",", ":"), + ).encode() + ) + ).decode("ascii") + + def test_external_threshold_approvals_pass_every_opaque_requirement(tmp_path: Path) -> None: root, base, store, keys = _repository(tmp_path) request = _request() @@ -133,10 +150,8 @@ def test_external_threshold_approvals_pass_every_opaque_requirement(tmp_path: Pa ] for approval in approvals: approval["policy_digest"] = policy.digest - approval["signature"] = "" - encoded = json.dumps({key: value for key, value in approval.items() if key != "signature"}, sort_keys=True, separators=(",", ":")).encode() key = keys[("alice", "bob").index(approval["identity"])] - approval["signature"] = base64.b64encode(key.sign(encoded)).decode("ascii") + _sign(key, approval) _write_approvals(store, approvals) verifier = HumanAttestationVerifier(policy, store / "replay.json") assert verifier.verify(request, now=NOW) is EvidenceOutcome.PASS @@ -157,6 +172,7 @@ def test_human_attestation_fails_closed_for_untrusted_or_incomplete_approvals( (root / ".pdd/human-attestation-policy.json").write_text( json.dumps({"threshold": 1, "signers": []}), encoding="utf-8" ) + approvals.pop() if mutator == "single": approvals.pop() if mutator == "mixed": @@ -167,3 +183,72 @@ def test_human_attestation_fails_closed_for_untrusted_or_incomplete_approvals( verifier = HumanAttestationVerifier(policy, store / "replay.json") with pytest.raises(HumanAttestationError): verifier.verify(request, now=NOW) + + +@pytest.mark.parametrize("mutator", ["duplicate", "expired", "future", "revoked", "wrong_requirement"]) +def test_human_attestation_rejects_each_invalid_signer_or_binding( + tmp_path: Path, mutator: str +) -> None: + root, base, store, keys = _repository(tmp_path) + request = _request() + policy = load_human_attestation_policy(root, base, store) + approvals = [ + _approval(keys[0], "alice", request, nonce="alice-1"), + _approval(keys[1], "bob", request, nonce="bob-1"), + ] + for index, approval in enumerate(approvals): + approval["policy_digest"] = policy.digest + _sign(keys[index], approval) + if mutator == "duplicate": + approvals[1] = dict(approvals[0]) + approvals[1]["approval_id"] = "another-alice" + _sign(keys[0], approvals[1]) + elif mutator == "expired": + approvals[1]["expires_at"] = (NOW - timedelta(minutes=1)).isoformat() + _sign(keys[1], approvals[1]) + elif mutator == "future": + approvals[1]["issued_at"] = (NOW + timedelta(minutes=1)).isoformat() + _sign(keys[1], approvals[1]) + elif mutator == "revoked": + policy = policy.__class__( + policy.version, policy.digest, policy.threshold, + policy.maximum_lifetime_seconds, policy.required_role, policy.signers, + frozenset({"bob"}), policy.revoked_key_ids, policy.candidate_root, + policy.external_store, + ) + else: + approvals[1]["requirement_ids"] = ["CONTRACT-SHA256:other"] + _sign(keys[1], approvals[1]) + _write_approvals(store, approvals) + with pytest.raises(HumanAttestationError): + HumanAttestationVerifier(policy, store / "replay.json").verify(request, now=NOW) + + +def test_runner_normalizes_external_threshold_quorum_to_pass(tmp_path: Path) -> None: + root, base, store, keys = _repository(tmp_path) + binding = RunBinding("snapshot-digest", base, base, "artifact-digest") + request = _request() + policy = load_human_attestation_policy(root, base, store) + obligation = VerificationObligation( + "human", "human-attestation", "threshold-ed25519", policy.digest, + request.obligation.requirement_ids, (), + ) + profile = VerificationProfile(UNIT, (obligation,), request.profile.required_requirement_ids, "profile-digest") + request = HumanAttestationRequest(profile, obligation, binding) + approvals = [ + _approval(keys[0], "alice", request, nonce="alice-run"), + _approval(keys[1], "bob", request, nonce="bob-run"), + ] + for index, approval in enumerate(approvals): + approval["policy_digest"] = policy.digest + _sign(keys[index], approval) + _write_approvals(store, approvals) + _envelope, executions = run_profile( + root, profile, request.binding, + AttestationIssue(AttestationSigner("runner", b"r" * 32), "run", "run-nonce", NOW), + config=RunnerConfig( + human_attestation_store=store, + human_attestation_replay_ledger=store / "replay.json", + ), + ) + assert executions[0].outcome is EvidenceOutcome.PASS From 52b7dda9dfec5318c567ac8ffbcb5153e3f623e4 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Fri, 10 Jul 2026 19:05:34 -0700 Subject: [PATCH 03/31] test(sync): reject duplicate threshold attestation keys --- tests/test_sync_core_human_attestation.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_sync_core_human_attestation.py b/tests/test_sync_core_human_attestation.py index ccf3997c1..0fe5566aa 100644 --- a/tests/test_sync_core_human_attestation.py +++ b/tests/test_sync_core_human_attestation.py @@ -252,3 +252,16 @@ def test_runner_normalizes_external_threshold_quorum_to_pass(tmp_path: Path) -> ), ) assert executions[0].outcome is EvidenceOutcome.PASS + + +def test_policy_rejects_duplicate_public_key_under_distinct_identities(tmp_path: Path) -> None: + """One Ed25519 key must never satisfy multiple human threshold slots.""" + root, base, store, _keys = _repository(tmp_path) + policy_path = root / ".pdd/human-attestation-policy.json" + policy = json.loads(policy_path.read_text(encoding="utf-8")) + policy["signers"][1]["public_key"] = policy["signers"][0]["public_key"] + policy_path.write_text(json.dumps(policy), encoding="utf-8") + _git(root, "add", ".pdd/human-attestation-policy.json") + duplicate_key_base = _git(root, "commit", "-qm", "duplicate protected key") + with pytest.raises(HumanAttestationError, match="duplicate"): + load_human_attestation_policy(root, duplicate_key_base, store) From 91826feefd62a723b3163e18076639af65b14c7a Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Fri, 10 Jul 2026 19:06:23 -0700 Subject: [PATCH 04/31] fix(sync): reject duplicate threshold attestation keys --- pdd/sync_core/human_attestation.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pdd/sync_core/human_attestation.py b/pdd/sync_core/human_attestation.py index b9442dfc7..3e36032a1 100644 --- a/pdd/sync_core/human_attestation.py +++ b/pdd/sync_core/human_attestation.py @@ -137,6 +137,7 @@ def load_human_attestation_policy( raise HumanAttestationError("protected human attestation threshold is invalid") signers: dict[str, HumanAttestationSigner] = {} key_ids: set[str] = set() + public_keys: set[bytes] = set() for row in rows: if not isinstance(row, dict): raise HumanAttestationError("protected human signer is malformed") @@ -153,7 +154,12 @@ def load_human_attestation_policy( public_key = base64.b64decode(_required_string(row, "public_key"), validate=True) except ValueError as exc: raise HumanAttestationError("protected human signer key is malformed") from exc - if len(public_key) != 32 or identity in signers or key_id in key_ids: + if ( + len(public_key) != 32 + or identity in signers + or key_id in key_ids + or public_key in public_keys + ): raise HumanAttestationError("protected human signer identity or key is duplicate") signer = HumanAttestationSigner( identity, key_id, public_key, frozenset(roles), @@ -164,6 +170,7 @@ def load_human_attestation_policy( raise HumanAttestationError("protected human signer validity is invalid") signers[identity] = signer key_ids.add(key_id) + public_keys.add(public_key) revoked_identities = frozenset(payload.get("revoked_identities", [])) revoked_key_ids = frozenset(payload.get("revoked_key_ids", [])) if not all(isinstance(value, str) for value in revoked_identities | revoked_key_ids): From da36187be428832b87c3cd32faea705de4197361 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Fri, 10 Jul 2026 19:48:46 -0700 Subject: [PATCH 05/31] test(sync): reproduce shared approval store rejection --- tests/test_sync_core_human_attestation.py | 41 +++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/test_sync_core_human_attestation.py b/tests/test_sync_core_human_attestation.py index 0fe5566aa..cbe64ebf0 100644 --- a/tests/test_sync_core_human_attestation.py +++ b/tests/test_sync_core_human_attestation.py @@ -44,13 +44,17 @@ def _public(key: Ed25519PrivateKey) -> str: return base64.b64encode(key.public_key().public_bytes_raw()).decode("ascii") -def _request() -> HumanAttestationRequest: +def _request( + unit: UnitId = UNIT, + *, + profile_digest: str = "profile-digest", +) -> HumanAttestationRequest: obligation = VerificationObligation( "human", "human-attestation", "threshold-ed25519", "policy-digest", ("CONTRACT-SHA256:abc",), (), ) profile = VerificationProfile( - UNIT, (obligation,), ("CONTRACT-SHA256:abc",), "profile-digest" + unit, (obligation,), ("CONTRACT-SHA256:abc",), profile_digest ) return HumanAttestationRequest( profile, @@ -157,6 +161,39 @@ def test_external_threshold_approvals_pass_every_opaque_requirement(tmp_path: Pa assert verifier.verify(request, now=NOW) is EvidenceOutcome.PASS +def test_shared_store_selects_each_units_exact_quorum(tmp_path: Path) -> None: + """Repository-wide approvals must not make unrelated unit checks fail.""" + root, base, store, keys = _repository(tmp_path) + first = _request() + second = _request( + UnitId( + "repository-1", + PurePosixPath("prompts/second.prompt"), + "python", + ), + profile_digest="second-profile-digest", + ) + policy = load_human_attestation_policy(root, base, store) + approvals = [] + for request, suffix in ((first, "first"), (second, "second")): + for index, identity in enumerate(("alice", "bob")): + approval = _approval( + keys[index], + identity, + request, + nonce=f"{identity}-{suffix}", + ) + approval["approval_id"] = f"approval-{identity}-{suffix}" + approval["policy_digest"] = policy.digest + _sign(keys[index], approval) + approvals.append(approval) + _write_approvals(store, approvals) + verifier = HumanAttestationVerifier(policy, store / "replay.json") + + assert verifier.verify(first, now=NOW) is EvidenceOutcome.PASS + assert verifier.verify(second, now=NOW) is EvidenceOutcome.PASS + + @pytest.mark.parametrize("mutator", ["candidate_policy", "single", "mixed", "rebind"]) def test_human_attestation_fails_closed_for_untrusted_or_incomplete_approvals( tmp_path: Path, mutator: str From 334d795a51d3c8fd358684d19eb871bcd4014e07 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Fri, 10 Jul 2026 19:49:28 -0700 Subject: [PATCH 06/31] fix(sync): select exact human approval quorum --- pdd/sync_core/human_attestation.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/pdd/sync_core/human_attestation.py b/pdd/sync_core/human_attestation.py index 3e36032a1..d7d99347f 100644 --- a/pdd/sync_core/human_attestation.py +++ b/pdd/sync_core/human_attestation.py @@ -191,6 +191,24 @@ def _canonical_payload(approval: Mapping[str, Any]) -> bytes: return json.dumps(unsigned, sort_keys=True, separators=(",", ":")).encode() +def _targets_request( + approval: Mapping[str, Any], request: HumanAttestationRequest +) -> bool: + """Select one immutable unit/closure without trusting approval validity.""" + expected = { + "repository_id": request.profile.unit_id.repository_id, + "prompt_relpath": request.profile.unit_id.prompt_relpath.as_posix(), + "language_id": request.profile.unit_id.language_id, + "obligation_id": request.obligation.obligation_id, + "snapshot_digest": request.binding.snapshot_digest, + "profile_digest": request.profile.profile_digest, + "protected_base_sha": request.binding.base_sha, + "checked_head_sha": request.binding.head_sha, + "artifact_closure_digest": request.binding.artifact_closure_digest, + } + return all(approval.get(key) == value for key, value in expected.items()) + + class HumanAttestationVerifier: # pylint: disable=too-few-public-methods """Verify external, thresholded human decisions without any signing surface.""" @@ -284,7 +302,11 @@ def verify( ) -> EvidenceOutcome: """Return PASS only when distinct authorized humans reach threshold.""" instant = now or datetime.now(timezone.utc) - approvals = self._load_approvals(self.policy.external_store) + approvals = [ + approval + for approval in self._load_approvals(self.policy.external_store) + if _targets_request(approval, request) + ] try: identities = { self._verify_approval(approval, request, instant) for approval in approvals From ac686d07f763e26b308ce5391eae4118e7399dcb Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Fri, 10 Jul 2026 22:59:27 -0700 Subject: [PATCH 07/31] test: cover human attestation revocation gaps --- tests/test_sync_core_human_attestation.py | 96 ++++++++++++++++++++++- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/tests/test_sync_core_human_attestation.py b/tests/test_sync_core_human_attestation.py index cbe64ebf0..bdeaa82e8 100644 --- a/tests/test_sync_core_human_attestation.py +++ b/tests/test_sync_core_human_attestation.py @@ -132,6 +132,14 @@ def _write_approvals(store: Path, approvals: list[dict[str, object]]) -> None: (store / "approvals.json").write_text(json.dumps({"approvals": approvals}), encoding="utf-8") +def _commit_policy(root: Path, policy: dict[str, object], message: str) -> str: + policy_path = root / ".pdd/human-attestation-policy.json" + policy_path.write_text(json.dumps(policy), encoding="utf-8") + _git(root, "add", ".pdd/human-attestation-policy.json") + _git(root, "commit", "-q", "-m", message) + return _git(root, "rev-parse", "HEAD") + + def _sign(key: Ed25519PrivateKey, approval: dict[str, object]) -> None: approval["signature"] = base64.b64encode( key.sign( @@ -261,6 +269,88 @@ def test_human_attestation_rejects_each_invalid_signer_or_binding( HumanAttestationVerifier(policy, store / "replay.json").verify(request, now=NOW) +@pytest.mark.parametrize( + ("field", "value"), + [ + ("revoked_identities", "bob"), + ("revoked_identities", {"bob": True}), + ("revoked_key_ids", "bob"), + ("revoked_key_ids", {"bob": True}), + ], +) +def test_policy_rejects_malformed_revocation_lists( + tmp_path: Path, field: str, value: object +) -> None: + root, _base, store, _keys = _repository(tmp_path) + policy_path = root / ".pdd/human-attestation-policy.json" + policy = json.loads(policy_path.read_text(encoding="utf-8")) + policy[field] = value + base = _commit_policy(root, policy, "malformed revocation policy") + + with pytest.raises(HumanAttestationError, match="revocation"): + load_human_attestation_policy(root, base, store) + + +def test_policy_requires_threshold_active_role_qualified_signers(tmp_path: Path) -> None: + root, _base, store, _keys = _repository(tmp_path) + policy_path = root / ".pdd/human-attestation-policy.json" + policy = json.loads(policy_path.read_text(encoding="utf-8")) + policy["revoked_identities"] = ["bob", "carol"] + base = _commit_policy(root, policy, "unsatisfiable active policy") + + with pytest.raises(HumanAttestationError, match="unsatisfiable"): + load_human_attestation_policy(root, base, store) + + +@pytest.mark.parametrize( + ("revoked_identities", "revoked_key_ids"), + [(["bob"], []), ([], ["bob"])], +) +def test_revoked_identity_and_key_cannot_contribute_to_threshold( + tmp_path: Path, revoked_identities: list[str], revoked_key_ids: list[str] +) -> None: + root, _base, store, keys = _repository(tmp_path) + policy_path = root / ".pdd/human-attestation-policy.json" + policy_payload = json.loads(policy_path.read_text(encoding="utf-8")) + policy_payload["revoked_identities"] = revoked_identities + policy_payload["revoked_key_ids"] = revoked_key_ids + base = _commit_policy(root, policy_payload, "revoked protected signers") + policy = load_human_attestation_policy(root, base, store) + request = _request() + approvals = [ + _approval(keys[0], "alice", request, nonce="alice-revocation"), + _approval(keys[1], "bob", request, nonce="bob-revocation"), + ] + for index, approval in enumerate(approvals): + approval["policy_digest"] = policy.digest + _sign(keys[index], approval) + _write_approvals(store, approvals) + + with pytest.raises(HumanAttestationError): + HumanAttestationVerifier(policy, store / "replay.json").verify(request, now=NOW) + + +def test_stale_targeted_history_does_not_block_fresh_quorum(tmp_path: Path) -> None: + root, base, store, keys = _repository(tmp_path) + request = _request() + policy = load_human_attestation_policy(root, base, store) + stale = _approval(keys[2], "carol", request, nonce="carol-stale") + stale["expires_at"] = (NOW - timedelta(minutes=1)).isoformat() + approvals = [ + stale, + _approval(keys[0], "alice", request, nonce="alice-fresh"), + _approval(keys[1], "bob", request, nonce="bob-fresh"), + ] + for index, approval in zip((2, 0, 1), approvals): + approval["policy_digest"] = policy.digest + _sign(keys[index], approval) + _write_approvals(store, approvals) + + assert HumanAttestationVerifier(policy, store / "replay.json").verify( + request, now=NOW + ) is EvidenceOutcome.PASS + + def test_runner_normalizes_external_threshold_quorum_to_pass(tmp_path: Path) -> None: root, base, store, keys = _repository(tmp_path) binding = RunBinding("snapshot-digest", base, base, "artifact-digest") @@ -293,12 +383,10 @@ def test_runner_normalizes_external_threshold_quorum_to_pass(tmp_path: Path) -> def test_policy_rejects_duplicate_public_key_under_distinct_identities(tmp_path: Path) -> None: """One Ed25519 key must never satisfy multiple human threshold slots.""" - root, base, store, _keys = _repository(tmp_path) + root, _base, store, _keys = _repository(tmp_path) policy_path = root / ".pdd/human-attestation-policy.json" policy = json.loads(policy_path.read_text(encoding="utf-8")) policy["signers"][1]["public_key"] = policy["signers"][0]["public_key"] - policy_path.write_text(json.dumps(policy), encoding="utf-8") - _git(root, "add", ".pdd/human-attestation-policy.json") - duplicate_key_base = _git(root, "commit", "-qm", "duplicate protected key") + duplicate_key_base = _commit_policy(root, policy, "duplicate protected key") with pytest.raises(HumanAttestationError, match="duplicate"): load_human_attestation_policy(root, duplicate_key_base, store) From c134f5d4c2b479a8ce0d27a8bb6ffc76cc8cb9d0 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Fri, 10 Jul 2026 23:02:51 -0700 Subject: [PATCH 08/31] fix: validate human attestation revocations --- pdd/sync_core/human_attestation.py | 39 ++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/pdd/sync_core/human_attestation.py b/pdd/sync_core/human_attestation.py index d7d99347f..847314ad5 100644 --- a/pdd/sync_core/human_attestation.py +++ b/pdd/sync_core/human_attestation.py @@ -83,6 +83,17 @@ def _required_string(payload: Mapping[str, Any], name: str) -> str: return value +def _string_array(payload: Mapping[str, Any], name: str) -> frozenset[str]: + value = payload.get(name, []) + if ( + not isinstance(value, list) + or not all(isinstance(item, str) and item for item in value) + or len(value) != len(set(value)) + ): + raise HumanAttestationError("protected human revocation list is invalid") + return frozenset(value) + + def _timestamp(value: object, name: str) -> datetime: if not isinstance(value, str): raise HumanAttestationError(f"human attestation field {name!r} is invalid") @@ -171,13 +182,15 @@ def load_human_attestation_policy( signers[identity] = signer key_ids.add(key_id) public_keys.add(public_key) - revoked_identities = frozenset(payload.get("revoked_identities", [])) - revoked_key_ids = frozenset(payload.get("revoked_key_ids", [])) - if not all(isinstance(value, str) for value in revoked_identities | revoked_key_ids): - raise HumanAttestationError("protected human revocation list is invalid") - if threshold > len(signers) or not any( - required_role in signer.roles for signer in signers.values() - ): + revoked_identities = _string_array(payload, "revoked_identities") + revoked_key_ids = _string_array(payload, "revoked_key_ids") + active_signers = [ + signer for identity, signer in signers.items() + if identity not in revoked_identities + and signer.key_id not in revoked_key_ids + and required_role in signer.roles + ] + if len(active_signers) < threshold: raise HumanAttestationError("protected human attestation policy is unsatisfiable") return HumanAttestationPolicy( version, hashlib.sha256(raw).hexdigest(), threshold, @@ -307,12 +320,12 @@ def verify( for approval in self._load_approvals(self.policy.external_store) if _targets_request(approval, request) ] - try: - identities = { - self._verify_approval(approval, request, instant) for approval in approvals - } - except AttestationError as exc: - raise HumanAttestationError(str(exc)) from exc + identities = set() + for approval in approvals: + try: + identities.add(self._verify_approval(approval, request, instant)) + except AttestationError: + continue if len(identities) < self.policy.threshold: raise HumanAttestationError("human approval threshold is not met") return EvidenceOutcome.PASS From ce1cdec57a09004fe1982d759bed3f606d9f1047 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Fri, 10 Jul 2026 23:42:05 -0700 Subject: [PATCH 09/31] test: require final artifact closure attestation --- tests/test_sync_core_reporting.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_sync_core_reporting.py b/tests/test_sync_core_reporting.py index 224fe874f..faf605b30 100644 --- a/tests/test_sync_core_reporting.py +++ b/tests/test_sync_core_reporting.py @@ -567,6 +567,10 @@ def test_trusted_finalizer_commits_artifact_closure_evidence_and_fingerprint( tmp_path, ) -> None: root, commit = _repository(tmp_path) + manifest = build_unit_manifest(root, base_ref=commit, head_ref=commit) + profile = load_verification_profiles(root, manifest).profiles[0] + snapshot = build_unit_snapshot(root, manifest, manifest.managed_units[0], profile) + expected_artifact_closure = snapshot.digest() result = finalize_unit( root, PurePosixPath("prompts/widget_python.prompt"), @@ -576,6 +580,13 @@ def test_trusted_finalizer_commits_artifact_closure_evidence_and_fingerprint( replay_ledger_path=tmp_path / "external-trust/finalizer.json", ) assert result.transaction.phase.value == "COMMITTED" + attestation = json.loads( + (root / evidence_relpath(result.attestation_id)).read_text(encoding="utf-8") + ) + assert ( + attestation["binding"]["artifact_closure_digest"] + == expected_artifact_closure + ) _git(root, "add", ".pdd/meta/v2", ".pdd/evidence/v2") _git(root, "commit", "-q", "-m", "commit trusted sync evidence") finalized_commit = _git(root, "rev-parse", "HEAD") From 0fa7d610bc140b025c7d33e34630d6e97a3c5ac4 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Fri, 10 Jul 2026 23:44:10 -0700 Subject: [PATCH 10/31] fix: sign finalized artifact closure digest --- pdd/sync_core/evidence_store.py | 2 ++ pdd/sync_core/finalize.py | 3 ++- pdd/sync_core/runner.py | 1 + pdd/sync_core/trust.py | 2 ++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pdd/sync_core/evidence_store.py b/pdd/sync_core/evidence_store.py index 67bf40780..580401c68 100644 --- a/pdd/sync_core/evidence_store.py +++ b/pdd/sync_core/evidence_store.py @@ -64,6 +64,7 @@ def attestation_payload(envelope: AttestationEnvelope) -> dict[str, Any]: "tool_version": binding.tool_version, "base_sha": binding.base_sha, "checked_sha": binding.checked_sha, + "artifact_closure_digest": binding.artifact_closure_digest, }, "results": [ { @@ -133,6 +134,7 @@ def decode_attestation(payload: Mapping[str, Any]) -> AttestationEnvelope: _string(binding_data, "checked_sha"), tuple(command_data) if command_data is not None else None, binding_data.get("playwright_toolchain_manifest"), + str(binding_data.get("artifact_closure_digest") or ""), ) results = tuple( ObligationEvidence( diff --git a/pdd/sync_core/finalize.py b/pdd/sync_core/finalize.py index 8eb6b8bf8..330c189c8 100644 --- a/pdd/sync_core/finalize.py +++ b/pdd/sync_core/finalize.py @@ -200,6 +200,7 @@ def _reusable_result( envelope.binding.checked_sha, envelope.binding.playwright_command, envelope.binding.playwright_toolchain_manifest, + snapshot.digest(), ) verifier.verify_current_for_idempotency(envelope, binding, now=now) ancestry = subprocess.run( @@ -301,7 +302,7 @@ def finalize_unit( envelope, executions = run_profile( repository_root, profile, - RunBinding(snapshot.digest(), base_sha, head_sha), + RunBinding(snapshot.digest(), base_sha, head_sha, snapshot.digest()), AttestationIssue(signer, attestation_id, str(uuid.uuid4()), now), config, ) diff --git a/pdd/sync_core/runner.py b/pdd/sync_core/runner.py index edd671317..741d00353 100644 --- a/pdd/sync_core/runner.py +++ b/pdd/sync_core/runner.py @@ -3344,6 +3344,7 @@ def run_profile( config.playwright_command, str(config.playwright_toolchain_manifest.resolve()) if config.playwright_toolchain_manifest is not None else None, + binding.artifact_closure_digest, ) results = tuple( ObligationEvidence(item.obligation_id, item.outcome) for item in executions diff --git a/pdd/sync_core/trust.py b/pdd/sync_core/trust.py index 2100018c5..275ec8fb5 100644 --- a/pdd/sync_core/trust.py +++ b/pdd/sync_core/trust.py @@ -179,6 +179,7 @@ class AttestationBinding: checked_sha: str playwright_command: tuple[str, ...] | None = None playwright_toolchain_manifest: str | None = None + artifact_closure_digest: str = "" @dataclass(frozen=True) @@ -218,6 +219,7 @@ def payload(self) -> bytes: "tool_version": self.binding.tool_version, "base_sha": self.binding.base_sha, "checked_sha": self.binding.checked_sha, + "artifact_closure_digest": self.binding.artifact_closure_digest, }, "results": [ { From 4c1300f15c4fc6e6917030497b673c6f08213109 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 00:27:54 -0700 Subject: [PATCH 11/31] test(sync): cover human attestation validate CLI wiring --- tests/test_sync_core_reporting.py | 67 +++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/test_sync_core_reporting.py b/tests/test_sync_core_reporting.py index faf605b30..8bc38e51d 100644 --- a/tests/test_sync_core_reporting.py +++ b/tests/test_sync_core_reporting.py @@ -563,6 +563,73 @@ def test_validate_command_wires_protected_playwright_runner_config( ) +def test_validate_command_wires_protected_human_attestation_config( + tmp_path, monkeypatch +) -> None: + root, commit = _repository(tmp_path) + store = tmp_path / "external-human-store" + ledger = tmp_path / "external-human-replay.json" + store.mkdir() + signer = object() + monkeypatch.chdir(root) + with patch( + "pdd.commands.sync_core.attestation_signer_from_environment", + return_value=signer, + ), patch("pdd.commands.sync_core.finalize_unit") as mocked_finalize: + mocked_finalize.return_value.transaction.transaction_id = "tx-1" + mocked_finalize.return_value.attestation_id = "att-1" + mocked_finalize.return_value.fingerprint_path = PurePosixPath( + ".pdd/meta/v2/fingerprint.json" + ) + result = CliRunner().invoke( + validate_command, + [ + "--module", + "prompts/widget_python.prompt", + "--base-ref", + commit, + "--human-attestation-store", + str(store), + "--human-attestation-replay-ledger", + str(ledger), + ], + ) + + assert result.exit_code == 0, result.output + call = mocked_finalize.call_args + assert call.kwargs["signer"] is signer + assert call.kwargs["config"].human_attestation_store == store.resolve() + assert call.kwargs["config"].human_attestation_replay_ledger == ledger.resolve() + + +def test_validate_command_rejects_candidate_local_human_attestation_paths( + tmp_path, monkeypatch +) -> None: + root, commit = _repository(tmp_path) + candidate_store = root / ".pdd" / "candidate-approvals" + candidate_ledger = root / ".pdd" / "candidate-replay.json" + candidate_store.mkdir() + monkeypatch.chdir(root) + with patch("pdd.commands.sync_core.finalize_unit") as mocked_finalize: + result = CliRunner().invoke( + validate_command, + [ + "--module", + "prompts/widget_python.prompt", + "--base-ref", + commit, + "--human-attestation-store", + str(candidate_store), + "--human-attestation-replay-ledger", + str(candidate_ledger), + ], + ) + + assert result.exit_code != 0 + assert "candidate checkout" in result.output + mocked_finalize.assert_not_called() + + def test_trusted_finalizer_commits_artifact_closure_evidence_and_fingerprint( tmp_path, ) -> None: From 6610324c509d3b9061777addd29084030f0f3b54 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 00:27:54 -0700 Subject: [PATCH 12/31] fix(sync): wire protected human attestation CLI --- pdd/commands/sync_core.py | 62 ++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/pdd/commands/sync_core.py b/pdd/commands/sync_core.py index 8f473929e..6f47071f5 100644 --- a/pdd/commands/sync_core.py +++ b/pdd/commands/sync_core.py @@ -187,6 +187,19 @@ def _protected_playwright_command( return command +def _protected_external_path(value: Path | None, option: str, cwd: Path) -> Path | None: + """Resolve an external protected path and reject candidate-local storage.""" + if value is None: + return None + root = cwd.resolve() + resolved = value.expanduser().resolve() + try: + resolved.relative_to(root) + except ValueError: + return resolved + raise click.ClickException(f"{option} must be outside the candidate checkout") + + def _runner_config_from_options( options: dict[str, object], cwd: Path ) -> RunnerConfig: @@ -195,6 +208,8 @@ def _runner_config_from_options( vitest_command = options.get("vitest_command") playwright_command = options.get("playwright_command") playwright_manifest = options.get("playwright_toolchain_manifest") + human_store = options.get("human_attestation_store") + human_replay = options.get("human_attestation_replay_ledger") return RunnerConfig( jest_command=_protected_command( jest_command if isinstance(jest_command, str) else None, @@ -212,6 +227,16 @@ def _runner_config_from_options( ), playwright_toolchain_manifest=Path(playwright_manifest).expanduser().resolve() if isinstance(playwright_manifest, str) and playwright_manifest else None, + human_attestation_store=_protected_external_path( + human_store if isinstance(human_store, Path) else None, + "--human-attestation-store", + cwd, + ), + human_attestation_replay_ledger=_protected_external_path( + human_replay if isinstance(human_replay, Path) else None, + "--human-attestation-replay-ledger", + cwd, + ), ) @@ -467,6 +492,18 @@ def baseline(ctx: click.Context, module: str, reviewed_by: str, reason: str) -> type=click.Path(path_type=Path), help="Protected external Playwright toolchain manifest.", ) +@click.option( + "--human-attestation-store", + type=click.Path(path_type=Path), + envvar="PDD_SYNC_HUMAN_ATTESTATION_STORE", + help="Protected external human approval store path.", +) +@click.option( + "--human-attestation-replay-ledger", + type=click.Path(path_type=Path), + envvar="PDD_SYNC_HUMAN_ATTESTATION_REPLAY_LEDGER", + help="Protected external human approval replay ledger path.", +) @click.pass_context def validate( ctx: click.Context, @@ -477,26 +514,31 @@ def validate( vitest_command: str | None, playwright_command: str | None, playwright_toolchain_manifest: Path | None, + human_attestation_store: Path | None, + human_attestation_replay_ledger: Path | None, ) -> None: """Run protected obligations and transactionally finalize trusted evidence.""" ctx.ensure_object(dict) root = Path.cwd().resolve() + config = _runner_config_from_options( + { + "jest_command": jest_command, + "vitest_command": vitest_command, + "playwright_command": playwright_command, + "playwright_toolchain_manifest": str(playwright_toolchain_manifest) + if playwright_toolchain_manifest else None, + "human_attestation_store": human_attestation_store, + "human_attestation_replay_ledger": human_attestation_replay_ledger, + }, + root, + ) result = finalize_unit( root, PurePosixPath(module), base_ref=base_ref, head_ref=head_ref, signer=attestation_signer_from_environment(), - config=_runner_config_from_options( - { - "jest_command": jest_command, - "vitest_command": vitest_command, - "playwright_command": playwright_command, - "playwright_toolchain_manifest": str(playwright_toolchain_manifest) - if playwright_toolchain_manifest else None, - }, - root, - ), + config=config, ) click.echo( json.dumps( From 879cbcf4cb0219007e47b5a3b5d672fd395d2824 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 00:28:47 -0700 Subject: [PATCH 13/31] test(sync): specify reporting metrics encoding --- tests/test_sync_core_reporting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_sync_core_reporting.py b/tests/test_sync_core_reporting.py index 8bc38e51d..3d360e7bc 100644 --- a/tests/test_sync_core_reporting.py +++ b/tests/test_sync_core_reporting.py @@ -707,11 +707,11 @@ def test_trusted_finalizer_second_run_is_zero_write_no_op(tmp_path) -> None: metrics_path = os.environ.get("PDD_LIFECYCLE_METRICS_PATH") if metrics_path: path = Path(metrics_path) - payload = json.loads(path.read_text()) if path.exists() else {} + payload = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {} payload["post_repair_second_run_writes"] = ( len(second.transaction.changed_paths) + int(before != after) ) - path.write_text(json.dumps(payload, sort_keys=True)) + path.write_text(json.dumps(payload, sort_keys=True), encoding="utf-8") def test_trusted_finalizer_rejects_dirty_support_before_reuse(tmp_path) -> None: From f0ad93863d9ff7e9e225cb1a2bc06eec29e273e4 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 06:35:13 -0700 Subject: [PATCH 14/31] test(sync): reject unsafe threshold attestations --- tests/test_sync_core_human_attestation.py | 45 +++++++++++++++++++++++ tests/test_sync_core_reporting.py | 17 ++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/tests/test_sync_core_human_attestation.py b/tests/test_sync_core_human_attestation.py index bdeaa82e8..2076360e6 100644 --- a/tests/test_sync_core_human_attestation.py +++ b/tests/test_sync_core_human_attestation.py @@ -302,6 +302,51 @@ def test_policy_requires_threshold_active_role_qualified_signers(tmp_path: Path) load_human_attestation_policy(root, base, store) +@pytest.mark.parametrize( + ("field", "value"), + [ + ("threshold", True), + ("maximum_lifetime_seconds", True), + ("maximum_lifetime_seconds", 86_401), + ("version", "v2"), + ], +) +def test_policy_rejects_unsafe_schema_values( + tmp_path: Path, field: str, value: object +) -> None: + root, _base, store, _keys = _repository(tmp_path) + policy_path = root / ".pdd/human-attestation-policy.json" + policy = json.loads(policy_path.read_text(encoding="utf-8")) + policy[field] = value + base = _commit_policy(root, policy, f"unsafe protected {field}") + + with pytest.raises(HumanAttestationError): + load_human_attestation_policy(root, base, store) + + +def test_approval_cannot_outlive_signer_validity_window(tmp_path: Path) -> None: + root, _base, store, keys = _repository(tmp_path) + policy_path = root / ".pdd/human-attestation-policy.json" + policy_payload = json.loads(policy_path.read_text(encoding="utf-8")) + policy_payload["signers"][1]["not_after"] = (NOW - timedelta(minutes=1)).isoformat() + base = _commit_policy(root, policy_payload, "expired protected signer") + policy = load_human_attestation_policy(root, base, store) + request = _request() + approvals = [ + _approval(keys[0], "alice", request, nonce="alice-current"), + _approval(keys[1], "bob", request, nonce="bob-backdated"), + ] + approvals[1]["issued_at"] = (NOW - timedelta(minutes=2)).isoformat() + approvals[1]["expires_at"] = (NOW + timedelta(minutes=30)).isoformat() + for index, approval in enumerate(approvals): + approval["policy_digest"] = policy.digest + _sign(keys[index], approval) + _write_approvals(store, approvals) + + with pytest.raises(HumanAttestationError, match="threshold"): + HumanAttestationVerifier(policy, store / "replay.json").verify(request, now=NOW) + + @pytest.mark.parametrize( ("revoked_identities", "revoked_key_ids"), [(["bob"], []), ([], ["bob"])], diff --git a/tests/test_sync_core_reporting.py b/tests/test_sync_core_reporting.py index 3d360e7bc..855093343 100644 --- a/tests/test_sync_core_reporting.py +++ b/tests/test_sync_core_reporting.py @@ -146,7 +146,9 @@ def _repository(tmp_path: Path) -> tuple[Path, str]: return root, _git(root, "rev-parse", "HEAD") -def _finalize_trusted_baseline(root: Path, commit: str) -> None: +def _finalize_trusted_baseline( + root: Path, commit: str, *, artifact_closure_digest: str | None = None +) -> None: manifest = build_unit_manifest(root, base_ref=commit, head_ref=commit) profile = load_verification_profiles(root, manifest).profiles[0] unit = manifest.managed_units[0] @@ -159,6 +161,7 @@ def _finalize_trusted_baseline(root: Path, commit: str) -> None: TRUSTED_RUNNER_VERSION, commit, commit, + artifact_closure_digest or snapshot.digest(), ) envelope = SIGNER.issue( AttestationRequest( @@ -224,6 +227,18 @@ def test_trusted_transactional_baseline_passes_canonical_predicate(tmp_path) -> assert report["units"][0]["in_sync"] is True +def test_canonical_report_rejects_signed_mismatched_artifact_closure(tmp_path) -> None: + root, commit = _repository(tmp_path) + _finalize_trusted_baseline( + root, commit, artifact_closure_digest="different-artifact-closure" + ) + + report = build_canonical_report(root, _options(tmp_path, commit)) + + assert report["ok"] is False + assert report["counts"]["trusted_current_evidence"] == 0 + + def test_managed_waiver_is_counted_and_blocks_certificate_predicate(tmp_path) -> None: root, _commit = _repository(tmp_path) (root / ".pdd/sync-waivers.json").write_text( From 00387bd061d331f3e19a7b99f47854759a42659b Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 06:37:52 -0700 Subject: [PATCH 15/31] fix(sync): harden threshold attestation boundaries --- pdd/sync_core/human_attestation.py | 18 +++++++++++++----- pdd/sync_core/reporting.py | 3 +++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pdd/sync_core/human_attestation.py b/pdd/sync_core/human_attestation.py index 847314ad5..2933a8127 100644 --- a/pdd/sync_core/human_attestation.py +++ b/pdd/sync_core/human_attestation.py @@ -22,6 +22,8 @@ HUMAN_ATTESTATION_POLICY_PATH = PurePosixPath(".pdd/human-attestation-policy.json") _APPROVALS_FILENAME = "approvals.json" +_SUPPORTED_POLICY_VERSION = "v1" +_MAXIMUM_LIFETIME_SECONDS = 24 * 60 * 60 class HumanAttestationError(AttestationError): @@ -110,6 +112,10 @@ def _optional_timestamp(value: object, name: str) -> datetime | None: return None if value is None else _timestamp(value, name) +def _positive_integer(value: object) -> bool: + return isinstance(value, int) and not isinstance(value, bool) and value > 0 + + def load_human_attestation_policy( root: Path, protected_base_ref: str, external_store: Path ) -> HumanAttestationPolicy: @@ -134,15 +140,16 @@ def load_human_attestation_policy( if not isinstance(payload, dict): raise HumanAttestationError("protected human attestation policy must be an object") version = _required_string(payload, "version") + if version != _SUPPORTED_POLICY_VERSION: + raise HumanAttestationError("protected human attestation version is unsupported") threshold = payload.get("threshold") maximum_lifetime_seconds = payload.get("maximum_lifetime_seconds") required_role = _required_string(payload, "required_role") rows = payload.get("signers") if ( - not isinstance(threshold, int) - or threshold < 1 - or not isinstance(maximum_lifetime_seconds, int) - or maximum_lifetime_seconds < 1 + not _positive_integer(threshold) + or not _positive_integer(maximum_lifetime_seconds) + or maximum_lifetime_seconds > _MAXIMUM_LIFETIME_SECONDS or not isinstance(rows, list) ): raise HumanAttestationError("protected human attestation threshold is invalid") @@ -277,7 +284,8 @@ def _verify_approval( if ( signer.not_before and issued < signer.not_before ) or ( - signer.not_after and issued >= signer.not_after + signer.not_after + and (issued >= signer.not_after or expires > signer.not_after) ): raise HumanAttestationError("human approval signer is outside its validity window") expected = { diff --git a/pdd/sync_core/reporting.py b/pdd/sync_core/reporting.py index 1c40f108c..10fef80b4 100644 --- a/pdd/sync_core/reporting.py +++ b/pdd/sync_core/reporting.py @@ -101,6 +101,7 @@ class EvidenceExpectation: unit: ManifestUnit snapshot_digest: str + artifact_closure_digest: str profile_digest: str attestation_ref: str | None @@ -156,6 +157,7 @@ def _evidence( if ( binding.subject != expectation.unit.unit_id or binding.snapshot_digest != expectation.snapshot_digest + or binding.artifact_closure_digest != expectation.artifact_closure_digest or binding.profile_digest != expectation.profile_digest or binding.base_sha != context.manifest.base_ref ): @@ -193,6 +195,7 @@ def _unit_verdict(context: ReportContext, unit: ManifestUnit) -> SyncVerdict: EvidenceExpectation( unit, snapshot.digest(), + snapshot.digest(), profile.profile_digest, attestation_ref, ), From c62e650336c54858acddd8ee3c29886a88bcd42a Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 07:04:43 -0700 Subject: [PATCH 16/31] test(sync): bind threshold closure by field name --- tests/test_sync_core_reporting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_sync_core_reporting.py b/tests/test_sync_core_reporting.py index 855093343..49df2c92b 100644 --- a/tests/test_sync_core_reporting.py +++ b/tests/test_sync_core_reporting.py @@ -161,7 +161,7 @@ def _finalize_trusted_baseline( TRUSTED_RUNNER_VERSION, commit, commit, - artifact_closure_digest or snapshot.digest(), + artifact_closure_digest=artifact_closure_digest or snapshot.digest(), ) envelope = SIGNER.issue( AttestationRequest( From 6a6957b6be7eb41812d35735e710c5f554b6e8e9 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 11:04:30 -0700 Subject: [PATCH 17/31] fix(sync): anchor threshold replay trust root --- pdd/sync_core/descriptor_store.py | 31 ++++++++++++++++++++++-------- pdd/sync_core/human_attestation.py | 2 +- pdd/sync_core/trust.py | 9 +++++++-- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/pdd/sync_core/descriptor_store.py b/pdd/sync_core/descriptor_store.py index 97b13d2db..2930995ca 100644 --- a/pdd/sync_core/descriptor_store.py +++ b/pdd/sync_core/descriptor_store.py @@ -33,15 +33,19 @@ def _unlock(descriptor: int) -> None: fcntl.flock(descriptor, fcntl.LOCK_UN) -def _safe_directory(metadata: os.stat_result) -> bool: - """Return whether metadata describes a private checker-owned directory.""" +def _owned_directory(metadata: os.stat_result) -> bool: + """Return whether metadata describes a checker-owned directory.""" return ( stat.S_ISDIR(metadata.st_mode) and (not hasattr(os, "getuid") or metadata.st_uid == os.getuid()) - and not stat.S_IMODE(metadata.st_mode) & 0o077 ) +def _safe_directory(metadata: os.stat_result) -> bool: + """Return whether metadata describes a private checker-owned directory.""" + return _owned_directory(metadata) and not stat.S_IMODE(metadata.st_mode) & 0o077 + + def _legacy_safe_parent(path: Path) -> tuple[int, os.stat_result]: """Preserve strict filesystem-root validation for unanchored callers.""" flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0) @@ -75,6 +79,21 @@ def _legacy_safe_parent(path: Path) -> tuple[int, os.stat_result]: return descriptor, opened +def _open_trust_root(trust_root: Path, flags: int) -> int: + """Open and privatize one owner-controlled root without following links.""" + trust_root.mkdir(mode=0o700, parents=True, exist_ok=True) + descriptor = os.open(trust_root, flags) + metadata = os.fstat(descriptor) + if not _owned_directory(metadata): + os.close(descriptor) + raise DescriptorStoreError("replay ledger root is unsafe") + os.fchmod(descriptor, 0o700) + if not _safe_directory(os.fstat(descriptor)): + os.close(descriptor) + raise DescriptorStoreError("replay ledger root is unsafe") + return descriptor + + def _safe_parent( path: Path, trust_root: Path | None ) -> tuple[int, os.stat_result]: @@ -92,11 +111,7 @@ def _safe_parent( flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0) descriptor = -1 try: - trust_root.mkdir(mode=0o700, parents=True, exist_ok=True) - descriptor = os.open(trust_root, flags) - root_metadata = os.fstat(descriptor) - if not _safe_directory(root_metadata): - raise DescriptorStoreError("replay ledger root is unsafe") + descriptor = _open_trust_root(trust_root, flags) for component in relative_parent.parts: if component in {"", ".", ".."}: raise DescriptorStoreError("replay ledger parent is unsafe") diff --git a/pdd/sync_core/human_attestation.py b/pdd/sync_core/human_attestation.py index 2933a8127..3541210e7 100644 --- a/pdd/sync_core/human_attestation.py +++ b/pdd/sync_core/human_attestation.py @@ -243,7 +243,7 @@ def __init__(self, policy: HumanAttestationPolicy, replay_ledger_path: Path) -> raise HumanAttestationError( "human attestation replay ledger must be outside the candidate checkout" ) - self._replay = FileReplayStore(replay) + self._replay = FileReplayStore(replay, trust_root=replay.parent) def _load_approvals(self, store: Path) -> list[Mapping[str, Any]]: path = Path(store).resolve() / _APPROVALS_FILENAME diff --git a/pdd/sync_core/trust.py b/pdd/sync_core/trust.py index 275ec8fb5..0789fd087 100644 --- a/pdd/sync_core/trust.py +++ b/pdd/sync_core/trust.py @@ -75,8 +75,11 @@ def is_durable(self) -> bool: class FileReplayStore(ReplayStore): """Locked durable nonce ledger located outside candidate-controlled state.""" - def __init__(self, path: Path) -> None: + def __init__(self, path: Path, *, trust_root: Path | None = None) -> None: self.path = Path(path).absolute() + self.trust_root = ( + Path(trust_root).absolute() if trust_root is not None else None + ) def _ensure_parent(self) -> None: parent = self.path.parent @@ -137,7 +140,9 @@ def consume_record(records): error = None for _attempt in range(3): try: - update_json(self.path, {}, consume_record) + update_json( + self.path, {}, consume_record, trust_root=self.trust_root + ) return except DescriptorStoreError as exc: error = exc From b1c725fdf6b944558b4fbac3d6cf36d900f6395e Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 11:22:38 -0700 Subject: [PATCH 18/31] test(sync): cover canonical human config plumbing --- tests/test_sync_core_reporting.py | 25 +++++++++++++++++++++++++ tests/test_sync_orchestration.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/test_sync_core_reporting.py b/tests/test_sync_core_reporting.py index 49df2c92b..3b5c1816b 100644 --- a/tests/test_sync_core_reporting.py +++ b/tests/test_sync_core_reporting.py @@ -30,6 +30,7 @@ encode_attestation, encode_fingerprint, evidence_relpath, + finalize_legacy_paths, finalize_unit, load_verification_profiles, runner_identity_digest, @@ -617,6 +618,30 @@ def test_validate_command_wires_protected_human_attestation_config( assert call.kwargs["config"].human_attestation_replay_ledger == ledger.resolve() +def test_legacy_finalizer_wires_protected_human_attestation_environment( + tmp_path, monkeypatch +) -> None: + root, commit = _repository(tmp_path) + store = tmp_path / "external-human-store" + ledger = tmp_path / "external-human-replay.json" + store.mkdir() + prompt = root / "prompts/widget_python.prompt" + monkeypatch.setenv("PDD_SYNC_PROTECTED_BASE_SHA", commit) + monkeypatch.setenv("PDD_SYNC_HUMAN_ATTESTATION_STORE", str(store)) + monkeypatch.setenv("PDD_SYNC_HUMAN_ATTESTATION_REPLAY_LEDGER", str(ledger)) + with patch( + "pdd.sync_core.finalize.canonical_root_for_paths", return_value=root + ), patch( + "pdd.sync_core.finalize.attestation_signer_from_environment", + return_value=object(), + ), patch("pdd.sync_core.finalize.finalize_unit") as mocked_finalize: + assert finalize_legacy_paths({"prompt": prompt}) is True + + config = mocked_finalize.call_args.kwargs["config"] + assert config.human_attestation_store == store.resolve() + assert config.human_attestation_replay_ledger == ledger.resolve() + + def test_validate_command_rejects_candidate_local_human_attestation_paths( tmp_path, monkeypatch ) -> None: diff --git a/tests/test_sync_orchestration.py b/tests/test_sync_orchestration.py index 1b841fec0..069c615a7 100644 --- a/tests/test_sync_orchestration.py +++ b/tests/test_sync_orchestration.py @@ -13,7 +13,7 @@ # may carry their own @pytest.mark.timeout override. pytestmark = pytest.mark.timeout(450) -from pdd.sync_orchestration import sync_orchestration, _execute_tests_and_create_run_report, _try_auto_fix_env_var_error, _compose_sync_summary +from pdd.sync_orchestration import sync_orchestration, _execute_tests_and_create_run_report, _try_auto_fix_env_var_error, _compose_sync_summary, _save_fingerprint_atomic from pdd.sync_determine_operation import SyncDecision, get_pdd_file_paths # Test Plan: @@ -9521,3 +9521,30 @@ def test_sync_orchestration_skip_handler_for_fix(orchestration_fixture): orchestration_fixture['_save_fingerprint_atomic'].assert_any_call( "calculator", "python", "skip:fix", ANY, 0.0, "skipped" ) + + +def test_atomic_fingerprint_finalizer_wires_human_attestation_environment( + tmp_path, monkeypatch +): + root = tmp_path / "repo" + prompt = root / "prompts/widget_python.prompt" + prompt.parent.mkdir(parents=True) + prompt.write_text("Build widget\n") + store = tmp_path / "external-human-store" + ledger = tmp_path / "external-human-replay.json" + store.mkdir() + monkeypatch.setenv("PDD_SYNC_PROTECTED_BASE_SHA", "protected-base") + monkeypatch.setenv("PDD_SYNC_HUMAN_ATTESTATION_STORE", str(store)) + monkeypatch.setenv("PDD_SYNC_HUMAN_ATTESTATION_REPLAY_LEDGER", str(ledger)) + with patch("pdd.continuous_sync.canonical_sync_enabled", return_value=True), patch( + "pdd.continuous_sync.repository_root", return_value=root + ), patch( + "pdd.sync_core.attestation_signer_from_environment", return_value=object() + ), patch("pdd.sync_core.finalize_unit") as mocked_finalize: + _save_fingerprint_atomic( + "widget", "python", "generate", {"prompt": prompt}, 0.0, "test" + ) + + config = mocked_finalize.call_args.kwargs["config"] + assert config.human_attestation_store == store.resolve() + assert config.human_attestation_replay_ledger == ledger.resolve() From 6bc3ce59d456a24a48dc587ece3fd9c2210fef2c Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 11:23:13 -0700 Subject: [PATCH 19/31] fix(sync): share protected finalizer configuration --- pdd/sync_core/__init__.py | 2 ++ pdd/sync_core/finalize.py | 29 +++++++++++++++++++++++++++++ pdd/sync_orchestration.py | 2 ++ 3 files changed, 33 insertions(+) diff --git a/pdd/sync_core/__init__.py b/pdd/sync_core/__init__.py index 116d5c0d3..34a9ccabb 100644 --- a/pdd/sync_core/__init__.py +++ b/pdd/sync_core/__init__.py @@ -49,6 +49,7 @@ attestation_signer_from_environment, canonical_root_for_paths, finalize_legacy_paths, + protected_runner_config_from_environment, finalize_unit, ) from .evidence_store import ( @@ -238,6 +239,7 @@ "evidence_relpath", "finalize_unit", "finalize_legacy_paths", + "protected_runner_config_from_environment", "build_include_closure", "build_canonical_report", "build_global_certificate", diff --git a/pdd/sync_core/finalize.py b/pdd/sync_core/finalize.py index 330c189c8..18079906e 100644 --- a/pdd/sync_core/finalize.py +++ b/pdd/sync_core/finalize.py @@ -60,6 +60,34 @@ class CanonicalFinalizationError(RuntimeError): """Raised when an opted-in mutation cannot commit trusted final state.""" +def protected_runner_config_from_environment(root: Path) -> RunnerConfig: + """Load external protected-runner state shared by canonical finalizers.""" + store_raw = os.environ.get("PDD_SYNC_HUMAN_ATTESTATION_STORE") + replay_raw = os.environ.get("PDD_SYNC_HUMAN_ATTESTATION_REPLAY_LEDGER") + if bool(store_raw) != bool(replay_raw): + raise ValueError( + "human attestation store and replay ledger must be configured together" + ) + if not store_raw: + return RunnerConfig() + candidate_root = Path(root).resolve() + + def external(value: str, label: str) -> Path: + resolved = Path(value).expanduser().resolve() + try: + resolved.relative_to(candidate_root) + except ValueError: + return resolved + raise ValueError(f"{label} must be outside the candidate checkout") + + return RunnerConfig( + human_attestation_store=external(store_raw, "human attestation store"), + human_attestation_replay_ledger=external( + replay_raw, "human attestation replay ledger" + ), + ) + + def canonical_root_for_paths(paths: dict[str, Path] | None) -> Path | None: """Return the opted-in Git root for legacy path-based callers.""" # pylint: disable=import-outside-toplevel @@ -90,6 +118,7 @@ def finalize_legacy_paths(paths: dict[str, Path] | None) -> bool: base_ref=protected_base, head_ref="HEAD", signer=attestation_signer_from_environment(), + config=protected_runner_config_from_environment(root), ) except (OSError, RuntimeError, ValueError) as exc: raise CanonicalFinalizationError( diff --git a/pdd/sync_orchestration.py b/pdd/sync_orchestration.py index 5db06ecd9..24f9bc6c4 100644 --- a/pdd/sync_orchestration.py +++ b/pdd/sync_orchestration.py @@ -689,6 +689,7 @@ def _save_fingerprint_atomic(basename: str, language: str, operation: str, from .sync_core import ( attestation_signer_from_environment, finalize_unit, + protected_runner_config_from_environment, ) root = repository_root(start) @@ -708,6 +709,7 @@ def _save_fingerprint_atomic(basename: str, language: str, operation: str, base_ref=protected_base, head_ref="HEAD", signer=attestation_signer_from_environment(), + config=protected_runner_config_from_environment(root), ) return if atomic_state: From 53eba5c4bbb1bf8c78ead33a447a23dcae063b58 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 11:23:52 -0700 Subject: [PATCH 20/31] test(sync): reject compromised replay roots --- tests/test_sync_core_descriptor_store.py | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_sync_core_descriptor_store.py b/tests/test_sync_core_descriptor_store.py index 059115f2b..9f05dd7c1 100644 --- a/tests/test_sync_core_descriptor_store.py +++ b/tests/test_sync_core_descriptor_store.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import os import pytest @@ -47,3 +48,44 @@ def test_checker_owned_replay_root_rejects_symlinked_descendant(tmp_path) -> Non ) assert not (outside / "replay.json").exists() + + +def test_existing_writable_replay_root_is_rejected_without_repair(tmp_path) -> None: + replay_root = tmp_path / "checker-owned" + replay_root.mkdir(mode=0o700) + ledger = replay_root / "replay.json" + ledger.write_text('["preserved-nonce"]\n', encoding="utf-8") + ledger.chmod(0o600) + replay_root.chmod(0o777) + + with pytest.raises(DescriptorStoreError, match="root is unsafe"): + update_json( + ledger, + [], + lambda records: [*records, "new-nonce"], + trust_root=replay_root, + ) + + assert replay_root.stat().st_mode & 0o777 == 0o777 + assert json.loads(ledger.read_text(encoding="utf-8")) == ["preserved-nonce"] + + +def test_foreign_owned_replay_leaves_are_rejected(tmp_path, monkeypatch) -> None: + replay_root = tmp_path / "checker-owned" + replay_root.mkdir(mode=0o700) + ledger = replay_root / "replay.json" + ledger.write_text("[]\n", encoding="utf-8") + ledger.chmod(0o600) + original_fstat = os.fstat + + def foreign_regular(descriptor): + metadata = original_fstat(descriptor) + if not os.path.isfile(f"/dev/fd/{descriptor}"): + return metadata + fields = list(metadata) + fields[4] = metadata.st_uid + 1 + return os.stat_result(fields) + + monkeypatch.setattr("pdd.sync_core.descriptor_store.os.fstat", foreign_regular) + with pytest.raises(DescriptorStoreError, match="unsafe"): + update_json(ledger, [], lambda records: records, trust_root=replay_root) From 808ab0ee6ab02c475ac1cb6767180dd5cd9c6ef2 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 11:24:17 -0700 Subject: [PATCH 21/31] fix(sync): reject compromised replay roots --- pdd/sync_core/descriptor_store.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/pdd/sync_core/descriptor_store.py b/pdd/sync_core/descriptor_store.py index 2930995ca..bb3f3ce26 100644 --- a/pdd/sync_core/descriptor_store.py +++ b/pdd/sync_core/descriptor_store.py @@ -80,14 +80,20 @@ def _legacy_safe_parent(path: Path) -> tuple[int, os.stat_result]: def _open_trust_root(trust_root: Path, flags: int) -> int: - """Open and privatize one owner-controlled root without following links.""" - trust_root.mkdir(mode=0o700, parents=True, exist_ok=True) + """Open a new private root or reject compromised pre-existing state.""" + try: + os.lstat(trust_root) + existed = True + except FileNotFoundError: + existed = False + trust_root.mkdir(mode=0o700, parents=True, exist_ok=True) descriptor = os.open(trust_root, flags) metadata = os.fstat(descriptor) - if not _owned_directory(metadata): + if not _owned_directory(metadata) or (existed and not _safe_directory(metadata)): os.close(descriptor) raise DescriptorStoreError("replay ledger root is unsafe") - os.fchmod(descriptor, 0o700) + if not existed: + os.fchmod(descriptor, 0o700) if not _safe_directory(os.fstat(descriptor)): os.close(descriptor) raise DescriptorStoreError("replay ledger root is unsafe") @@ -162,7 +168,11 @@ def _read_json(parent_fd: int, name: str, empty: Any) -> Any: descriptor = _open_relative(parent_fd, name, os.O_RDONLY) try: metadata = os.fstat(descriptor) - if not stat.S_ISREG(metadata.st_mode) or stat.S_IMODE(metadata.st_mode) != 0o600: + if ( + not stat.S_ISREG(metadata.st_mode) + or stat.S_IMODE(metadata.st_mode) != 0o600 + or (hasattr(os, "getuid") and metadata.st_uid != os.getuid()) + ): raise DescriptorStoreError("replay ledger is unsafe") with os.fdopen(descriptor, "r", encoding="utf-8", closefd=False) as handle: return json.load(handle) @@ -209,7 +219,10 @@ def update_json( lock_fd = -1 try: lock_fd = _open_relative(parent_fd, lock_name, os.O_RDWR | os.O_CREAT, 0o600) - if not stat.S_ISREG(os.fstat(lock_fd).st_mode): + lock_metadata = os.fstat(lock_fd) + if not stat.S_ISREG(lock_metadata.st_mode) or ( + hasattr(os, "getuid") and lock_metadata.st_uid != os.getuid() + ): raise DescriptorStoreError("replay ledger lock is unsafe") os.fchmod(lock_fd, 0o600) _lock(lock_fd) From c0cba1baeb8525c46f8c3e2b7662d7903e09532e Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 11:24:50 -0700 Subject: [PATCH 22/31] test(sync): normalize replay filesystem failures --- tests/test_sync_core_trust.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_sync_core_trust.py b/tests/test_sync_core_trust.py index 817a9298d..41f0a93fd 100644 --- a/tests/test_sync_core_trust.py +++ b/tests/test_sync_core_trust.py @@ -261,3 +261,14 @@ def test_file_replay_store_rejects_symlinked_ancestor(tmp_path) -> None: store = FileReplayStore(protected / "linked" / "nested" / "replay.json") with pytest.raises(AttestationError, match="unsafe"): store.consume("trusted-ci", "nonce", "attestation") + + +def test_file_replay_store_normalizes_filesystem_failures(tmp_path, monkeypatch) -> None: + store = FileReplayStore(tmp_path / "replay.json") + + def fail_io(*_args, **_kwargs): + raise OSError("disk full") + + monkeypatch.setattr("pdd.sync_core.trust.update_json", fail_io) + with pytest.raises(AttestationError, match="replay ledger I/O failed"): + store.consume("trusted-ci", "nonce", "attestation") From 57e264659042e005b8bc63cdef132ed3a9293227 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 11:25:11 -0700 Subject: [PATCH 23/31] fix(sync): normalize replay filesystem failures --- pdd/sync_core/trust.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pdd/sync_core/trust.py b/pdd/sync_core/trust.py index 0789fd087..85019b8a1 100644 --- a/pdd/sync_core/trust.py +++ b/pdd/sync_core/trust.py @@ -144,9 +144,11 @@ def consume_record(records): self.path, {}, consume_record, trust_root=self.trust_root ) return - except DescriptorStoreError as exc: + except (DescriptorStoreError, OSError) as exc: error = exc if error is not None: + if isinstance(error, OSError): + raise AttestationError("replay ledger I/O failed") from error raise AttestationError(str(error)) from error def is_durable(self) -> bool: From b59c9218f9c97b02a994bd396cf5252c26df0e69 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 11:25:47 -0700 Subject: [PATCH 24/31] test(sync): specify legacy attestation migration --- tests/test_sync_core_evidence_store.py | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_sync_core_evidence_store.py b/tests/test_sync_core_evidence_store.py index 6a866b82e..bcfb6ddce 100644 --- a/tests/test_sync_core_evidence_store.py +++ b/tests/test_sync_core_evidence_store.py @@ -7,10 +7,14 @@ from datetime import datetime, timezone from pathlib import Path, PurePosixPath +import pytest + from pdd.sync_core import ( AttestationBinding, AttestationRequest, AttestationSigner, + AttestationTrustPolicy, + EvidenceStoreError, EvidenceOutcome, decode_attestation, encode_attestation, @@ -66,6 +70,36 @@ def test_attestation_json_round_trip_preserves_playwright_command() -> None: assert decoded.payload() == envelope.payload() +def test_pre_closure_signed_envelope_uses_explicit_legacy_payload_shape() -> None: + envelope = _envelope() + payload = json.loads(encode_attestation(envelope)) + payload.pop("payload_version", None) + payload["binding"].pop("artifact_closure_digest") + payload.pop("signature") + legacy_bytes = json.dumps( + payload, sort_keys=True, separators=(",", ":") + ).encode() + payload["signature"] = SIGNER.sign_bytes(legacy_bytes) + + decoded = decode_attestation(payload) + + assert decoded.payload_version == 1 + assert decoded.payload() == legacy_bytes + AttestationTrustPolicy({SIGNER.issuer: SIGNER.public_key_bytes()}).verify( + decoded, + decoded.binding, + now=datetime(2026, 7, 10, 12, 1, tzinfo=timezone.utc), + ) + + +def test_unknown_attestation_payload_version_is_rejected() -> None: + payload = json.loads(encode_attestation(_envelope())) + payload["payload_version"] = 99 + + with pytest.raises(EvidenceStoreError, match="payload version"): + decode_attestation(payload) + + def test_candidate_cannot_replace_protected_base_public_key(tmp_path) -> None: root = tmp_path / "repo" root.mkdir() From c40d52c02d01d3f73b9357f0bc47ccf13cdd6225 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 11:26:27 -0700 Subject: [PATCH 25/31] fix(sync): version legacy attestation migration --- pdd/sync_core/evidence_store.py | 11 +++++++++++ pdd/sync_core/trust.py | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pdd/sync_core/evidence_store.py b/pdd/sync_core/evidence_store.py index 580401c68..547d39c03 100644 --- a/pdd/sync_core/evidence_store.py +++ b/pdd/sync_core/evidence_store.py @@ -50,6 +50,7 @@ def attestation_payload(envelope: AttestationEnvelope) -> dict[str, Any]: """Convert a signed envelope to stable JSON data without altering its payload.""" binding = envelope.binding payload = { + "payload_version": envelope.payload_version, "attestation_id": envelope.attestation_id, "issuer": envelope.issuer, "binding": { @@ -86,6 +87,8 @@ def attestation_payload(envelope: AttestationEnvelope) -> dict[str, Any]: payload["binding"]["playwright_toolchain_manifest"] = ( binding.playwright_toolchain_manifest ) + if envelope.payload_version == 1: + payload["binding"].pop("artifact_closure_digest") return payload @@ -107,6 +110,13 @@ def decode_attestation(payload: Mapping[str, Any]) -> AttestationEnvelope: """Decode untrusted JSON into a typed envelope without granting trust.""" try: binding_data = payload["binding"] + version_data = payload.get("payload_version") + if version_data is None: + payload_version = 1 if "artifact_closure_digest" not in binding_data else 2 + elif type(version_data) is int and version_data in {1, 2}: + payload_version = version_data + else: + raise ValueError("attestation payload version is unsupported") subject_data = binding_data["subject"] validity_data = payload["validity"] results_data = payload["results"] @@ -155,6 +165,7 @@ def decode_attestation(payload: Mapping[str, Any]) -> AttestationEnvelope: results, validity, _string(payload, "signature"), + payload_version, ) except (KeyError, TypeError, ValueError) as exc: raise EvidenceStoreError(f"attestation envelope is malformed: {exc}") from exc diff --git a/pdd/sync_core/trust.py b/pdd/sync_core/trust.py index 85019b8a1..0cc3b8257 100644 --- a/pdd/sync_core/trust.py +++ b/pdd/sync_core/trust.py @@ -208,9 +208,12 @@ class AttestationEnvelope: results: tuple[ObligationEvidence, ...] validity: AttestationValidity signature: str = "" + payload_version: int = 2 def payload(self) -> bytes: """Return canonical signed bytes, excluding the signature itself.""" + if self.payload_version not in {1, 2}: + raise AttestationError("attestation payload version is unsupported") data = { "attestation_id": self.attestation_id, "issuer": self.issuer, @@ -226,7 +229,6 @@ def payload(self) -> bytes: "tool_version": self.binding.tool_version, "base_sha": self.binding.base_sha, "checked_sha": self.binding.checked_sha, - "artifact_closure_digest": self.binding.artifact_closure_digest, }, "results": [ { @@ -241,6 +243,11 @@ def payload(self) -> bytes: "nonce": self.validity.nonce, }, } + if self.payload_version == 2: + data["payload_version"] = 2 + data["binding"]["artifact_closure_digest"] = ( + self.binding.artifact_closure_digest + ) if self.binding.playwright_command is not None: data["binding"]["playwright_command"] = list( self.binding.playwright_command From ceabe00b0714f4b263367d83beddb5e0f6dd50af Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 11:31:22 -0700 Subject: [PATCH 26/31] fix(sync): preserve non-writable replay roots --- pdd/sync_core/descriptor_store.py | 7 ++++--- tests/test_sync_core_reporting.py | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pdd/sync_core/descriptor_store.py b/pdd/sync_core/descriptor_store.py index bb3f3ce26..22f1c315a 100644 --- a/pdd/sync_core/descriptor_store.py +++ b/pdd/sync_core/descriptor_store.py @@ -89,11 +89,12 @@ def _open_trust_root(trust_root: Path, flags: int) -> int: trust_root.mkdir(mode=0o700, parents=True, exist_ok=True) descriptor = os.open(trust_root, flags) metadata = os.fstat(descriptor) - if not _owned_directory(metadata) or (existed and not _safe_directory(metadata)): + if not _owned_directory(metadata) or ( + existed and stat.S_IMODE(metadata.st_mode) & 0o022 + ): os.close(descriptor) raise DescriptorStoreError("replay ledger root is unsafe") - if not existed: - os.fchmod(descriptor, 0o700) + os.fchmod(descriptor, 0o700) if not _safe_directory(os.fstat(descriptor)): os.close(descriptor) raise DescriptorStoreError("replay ledger root is unsafe") diff --git a/tests/test_sync_core_reporting.py b/tests/test_sync_core_reporting.py index 3b5c1816b..663080889 100644 --- a/tests/test_sync_core_reporting.py +++ b/tests/test_sync_core_reporting.py @@ -21,6 +21,7 @@ FingerprintRecord, FingerprintStore, PlannedWrite, + RunnerConfig, SemanticStatus, TransactionManager, TRUSTED_RUNNER_VERSION, @@ -338,6 +339,7 @@ def test_nested_config_cannot_bypass_repository_canonical_finalizer( base_ref=head, head_ref="HEAD", signer=signer, + config=RunnerConfig(), ) From 617593b6804550a04f2d4356cb45137c0e2bc861 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 11:36:21 -0700 Subject: [PATCH 27/31] fix(sync): reject boolean payload versions --- pdd/sync_core/evidence_store.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pdd/sync_core/evidence_store.py b/pdd/sync_core/evidence_store.py index 547d39c03..e6910d652 100644 --- a/pdd/sync_core/evidence_store.py +++ b/pdd/sync_core/evidence_store.py @@ -113,7 +113,11 @@ def decode_attestation(payload: Mapping[str, Any]) -> AttestationEnvelope: version_data = payload.get("payload_version") if version_data is None: payload_version = 1 if "artifact_closure_digest" not in binding_data else 2 - elif type(version_data) is int and version_data in {1, 2}: + elif ( + isinstance(version_data, int) + and not isinstance(version_data, bool) + and version_data in {1, 2} + ): payload_version = version_data else: raise ValueError("attestation payload version is unsupported") From 4b2a3f2a53c9494df0f1fbc3921148e7310a3265 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 12:30:17 -0700 Subject: [PATCH 28/31] test(sync): close threshold attestation review gaps --- tests/test_sync_core_evidence_store.py | 16 ++++++++ tests/test_sync_core_reporting.py | 45 ++++++++++++++++++++++- tests/test_sync_core_runner_jest.py | 18 ++++++++- tests/test_sync_core_runner_playwright.py | 24 +++++++++++- tests/test_sync_core_runner_vitest.py | 20 +++++++++- tests/test_sync_core_trust.py | 7 ++++ 6 files changed, 126 insertions(+), 4 deletions(-) diff --git a/tests/test_sync_core_evidence_store.py b/tests/test_sync_core_evidence_store.py index bcfb6ddce..01b52ef6f 100644 --- a/tests/test_sync_core_evidence_store.py +++ b/tests/test_sync_core_evidence_store.py @@ -100,6 +100,22 @@ def test_unknown_attestation_payload_version_is_rejected() -> None: decode_attestation(payload) +def test_v1_attestation_rejects_unsigned_artifact_closure() -> None: + payload = json.loads(encode_attestation(_envelope())) + payload["payload_version"] = 1 + + with pytest.raises(EvidenceStoreError, match="artifact_closure_digest"): + decode_attestation(payload) + + +def test_v2_attestation_requires_nonempty_artifact_closure() -> None: + payload = json.loads(encode_attestation(_envelope())) + payload["binding"]["artifact_closure_digest"] = "" + + with pytest.raises(EvidenceStoreError, match="artifact_closure_digest"): + decode_attestation(payload) + + def test_candidate_cannot_replace_protected_base_public_key(tmp_path) -> None: root = tmp_path / "repo" root.mkdir() diff --git a/tests/test_sync_core_reporting.py b/tests/test_sync_core_reporting.py index 663080889..4730bff33 100644 --- a/tests/test_sync_core_reporting.py +++ b/tests/test_sync_core_reporting.py @@ -4,6 +4,7 @@ import json import os import subprocess +from dataclasses import replace from datetime import datetime, timezone from pathlib import Path, PurePosixPath from unittest.mock import patch @@ -149,7 +150,11 @@ def _repository(tmp_path: Path) -> tuple[Path, str]: def _finalize_trusted_baseline( - root: Path, commit: str, *, artifact_closure_digest: str | None = None + root: Path, + commit: str, + *, + artifact_closure_digest: str | None = None, + legacy_pre_closure: bool = False, ) -> None: manifest = build_unit_manifest(root, base_ref=commit, head_ref=commit) profile = load_verification_profiles(root, manifest).profiles[0] @@ -174,6 +179,15 @@ def _finalize_trusted_baseline( NOW, ) ) + if legacy_pre_closure: + envelope = SIGNER.sign( + replace( + envelope, + binding=replace(envelope.binding, artifact_closure_digest=""), + signature="", + payload_version=1, + ) + ) provenance = FingerprintProvenance( "generated", "pdd sync widget", @@ -241,6 +255,35 @@ def test_canonical_report_rejects_signed_mismatched_artifact_closure(tmp_path) - assert report["counts"]["trusted_current_evidence"] == 0 +def test_canonical_report_migrates_fresh_signed_pre_closure_envelope(tmp_path) -> None: + root, commit = _repository(tmp_path) + _finalize_trusted_baseline(root, commit, legacy_pre_closure=True) + + report = build_canonical_report(root, _options(tmp_path, commit)) + + assert report["ok"] is True + assert report["counts"]["trusted_in_sync"] == 1 + + +def test_finalizer_reuses_fresh_signed_pre_closure_envelope(tmp_path) -> None: + root, commit = _repository(tmp_path) + _finalize_trusted_baseline(root, commit, legacy_pre_closure=True) + + with patch("pdd.sync_core.finalize.run_profile") as run_profile_mock: + result = finalize_unit( + root, + PurePosixPath("prompts/widget_python.prompt"), + base_ref=commit, + head_ref=commit, + signer=SIGNER, + replay_ledger_path=tmp_path / "external-trust/legacy-reuse.json", + ) + + assert result.transaction.no_op is True + assert result.attestation_id == "attestation-widget-1" + run_profile_mock.assert_not_called() + + def test_managed_waiver_is_counted_and_blocks_certificate_predicate(tmp_path) -> None: root, _commit = _repository(tmp_path) (root / ".pdd/sync-waivers.json").write_text( diff --git a/tests/test_sync_core_runner_jest.py b/tests/test_sync_core_runner_jest.py index 42f07403b..7254c5aec 100644 --- a/tests/test_sync_core_runner_jest.py +++ b/tests/test_sync_core_runner_jest.py @@ -18,7 +18,7 @@ VerificationProfile, run_profile, ) -from pdd.sync_core.runner import jest_validator_config_digest +from pdd.sync_core.runner import _jest_command_error, jest_validator_config_digest from pdd.sync_core.runner import _local_javascript_imports @@ -449,6 +449,22 @@ def test_pathless_jest_script_operand_is_not_resolved_from_candidate( assert "pathless" in executions[0].detail +@pytest.mark.parametrize( + "command_tail", + [("fake_jest",), ("--require=./candidate-helper", "/external/jest.js")], +) +def test_jest_prefix_rejects_pathless_entrypoints_and_code_loaders( + tmp_path: Path, command_tail: tuple[str, ...] +) -> None: + executable = tmp_path / "python" + executable.write_text("#!/bin/sh\n", encoding="utf-8") + executable.chmod(0o755) + + error = _jest_command_error(tmp_path / "candidate", (str(executable), *command_tail)) + + assert error is not None + + def test_jest_subprocess_cannot_read_secret(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root, commit = _repository(tmp_path) fake = _fake_jest(tmp_path) diff --git a/tests/test_sync_core_runner_playwright.py b/tests/test_sync_core_runner_playwright.py index 92c6731ad..7a3095145 100644 --- a/tests/test_sync_core_runner_playwright.py +++ b/tests/test_sync_core_runner_playwright.py @@ -1045,7 +1045,17 @@ def test_playwright_launch_failures_are_normalized( assert "launch" in executions[0].detail.lower() -@pytest.mark.parametrize("option", ["--require=helper", "--import=helper", "--loader=helper"]) +@pytest.mark.parametrize( + "option", + [ + "--require=helper", + "--import=helper", + "--loader=helper", + "--require=./candidate-helper", + "--import=./candidate-helper", + "--loader=./candidate-helper", + ], +) def test_playwright_command_rejects_candidate_resolving_prefix_options( tmp_path: Path, option: str ) -> None: @@ -1060,6 +1070,18 @@ def test_playwright_command_rejects_candidate_resolving_prefix_options( assert "exactly" in error or "options" in error +def test_playwright_command_rejects_extensionless_pathless_entrypoint( + tmp_path: Path, +) -> None: + executable = tmp_path.parent / "node" + executable.write_bytes(b"node") + + error = _playwright_command_error(tmp_path, (str(executable), "fake_playwright")) + + assert error is not None + assert "absolute" in error or "pathless" in error + + def test_playwright_candidate_node_modules_dependency_is_not_trusted( tmp_path: Path, ) -> None: diff --git a/tests/test_sync_core_runner_vitest.py b/tests/test_sync_core_runner_vitest.py index ec142a00b..f94785cae 100644 --- a/tests/test_sync_core_runner_vitest.py +++ b/tests/test_sync_core_runner_vitest.py @@ -18,7 +18,7 @@ VerificationProfile, run_profile, ) -from pdd.sync_core.runner import vitest_validator_config_digest +from pdd.sync_core.runner import _vitest_command_error, vitest_validator_config_digest from pdd.sync_core.runner import _local_javascript_imports @@ -463,6 +463,24 @@ def test_pathless_vitest_script_operand_is_not_resolved_from_candidate( assert "pathless" in executions[0].detail +@pytest.mark.parametrize( + "command_tail", + [("fake_vitest",), ("--loader=./candidate-helper", "/external/vitest.mjs")], +) +def test_vitest_prefix_rejects_pathless_entrypoints_and_code_loaders( + tmp_path: Path, command_tail: tuple[str, ...] +) -> None: + executable = tmp_path / "python" + executable.write_text("#!/bin/sh\n", encoding="utf-8") + executable.chmod(0o755) + + error = _vitest_command_error( + tmp_path / "candidate", (str(executable), *command_tail) + ) + + assert error is not None + + def test_vitest_subprocess_cannot_read_secret( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/tests/test_sync_core_trust.py b/tests/test_sync_core_trust.py index 41f0a93fd..126c7e603 100644 --- a/tests/test_sync_core_trust.py +++ b/tests/test_sync_core_trust.py @@ -70,6 +70,13 @@ def test_valid_attestation_produces_sealed_evidence() -> None: assert evidence.attestation_id == "attestation-1" +def test_boolean_payload_version_is_rejected_by_domain_payload() -> None: + envelope = replace(_envelope(), payload_version=True) + + with pytest.raises(AttestationError, match="payload version"): + envelope.payload() + + def test_remote_attestation_authority_signature_is_verified(monkeypatch) -> None: request = AttestationRequest( "remote-attestation", From 3d4ab0b1f1d29dc9918496c76ea048599b97fc96 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 12:58:32 -0700 Subject: [PATCH 29/31] fix(sync): close threshold attestation review gaps --- pdd/sync_core/evidence_store.py | 12 +++- pdd/sync_core/reporting.py | 17 +++++- pdd/sync_core/runner.py | 80 +++++++++++++------------- pdd/sync_core/trust.py | 26 ++++++++- tests/test_sync_core_evidence_store.py | 11 +++- tests/test_sync_core_trust.py | 1 + 6 files changed, 98 insertions(+), 49 deletions(-) diff --git a/pdd/sync_core/evidence_store.py b/pdd/sync_core/evidence_store.py index e6910d652..b383030d7 100644 --- a/pdd/sync_core/evidence_store.py +++ b/pdd/sync_core/evidence_store.py @@ -121,6 +121,16 @@ def decode_attestation(payload: Mapping[str, Any]) -> AttestationEnvelope: payload_version = version_data else: raise ValueError("attestation payload version is unsupported") + if payload_version == 1: + if "artifact_closure_digest" in binding_data: + raise ValueError( + "v1 binding must not contain artifact_closure_digest" + ) + artifact_closure_digest = "" + else: + artifact_closure_digest = _string( + binding_data, "artifact_closure_digest" + ) subject_data = binding_data["subject"] validity_data = payload["validity"] results_data = payload["results"] @@ -148,7 +158,7 @@ def decode_attestation(payload: Mapping[str, Any]) -> AttestationEnvelope: _string(binding_data, "checked_sha"), tuple(command_data) if command_data is not None else None, binding_data.get("playwright_toolchain_manifest"), - str(binding_data.get("artifact_closure_digest") or ""), + artifact_closure_digest, ) results = tuple( ObligationEvidence( diff --git a/pdd/sync_core/reporting.py b/pdd/sync_core/reporting.py index 10fef80b4..02390cae3 100644 --- a/pdd/sync_core/reporting.py +++ b/pdd/sync_core/reporting.py @@ -4,7 +4,7 @@ import os import subprocess -from dataclasses import dataclass +from dataclasses import dataclass, replace from datetime import datetime, timezone from pathlib import Path from typing import Any, Iterable @@ -134,8 +134,14 @@ def _evidence( if not expectation.attestation_ref or context.trust_policy is None: return None envelope = load_attestation(context.root, expectation.attestation_ref) + expected_binding = envelope.binding + if envelope.payload_version == 1: + expected_binding = replace( + envelope.binding, + artifact_closure_digest=expectation.artifact_closure_digest, + ) evidence = context.trust_policy.verify( - envelope, envelope.binding, now=context.now + envelope, expected_binding, now=context.now ) ancestry = subprocess.run( [ @@ -154,10 +160,15 @@ def _evidence( "attestation checked commit is not an ancestor of certified head" ) binding = envelope.binding + artifact_closure_digest = ( + binding.snapshot_digest + if envelope.payload_version == 1 + else binding.artifact_closure_digest + ) if ( binding.subject != expectation.unit.unit_id or binding.snapshot_digest != expectation.snapshot_digest - or binding.artifact_closure_digest != expectation.artifact_closure_digest + or artifact_closure_digest != expectation.artifact_closure_digest or binding.profile_digest != expectation.profile_digest or binding.base_sha != context.manifest.base_ref ): diff --git a/pdd/sync_core/runner.py b/pdd/sync_core/runner.py index 741d00353..55c716aa2 100644 --- a/pdd/sync_core/runner.py +++ b/pdd/sync_core/runner.py @@ -1635,11 +1635,6 @@ def _command_part_identity(root: Path, part: str) -> str: return _file_identity(resolved) if resolved.exists() else part -def _is_script_like_operand(value: str) -> bool: - """Return true for argv operands that are likely executable script paths.""" - return Path(value).suffix in _JAVASCRIPT_SUFFIXES + (".py",) - - def _external_node_modules_root( root: Path, command: tuple[str, ...] | None ) -> Path | None: @@ -1890,45 +1885,42 @@ def _command_uses_candidate_checkout(root: Path, command: tuple[str, ...]) -> bo def _protected_command_error(root: Path, command: tuple[str, ...]) -> str | None: - """Return a fail-closed reason for unprotected explicit validator argv.""" + """Enforce an executable plus identity-bound external entrypoint grammar.""" if not command: return "explicit validator command is empty" + if len(command) != 2: + return "explicit validator command must be exactly a launcher and entrypoint" candidate_root = root.resolve() - path_operands: list[Path] = [] - executable = Path(command[0]).expanduser() - if not executable.is_absolute(): - return "explicit validator command executable must be an absolute path" - path_operands.append(executable) - expect_module_operand = False - for part in command[1:]: - if expect_module_operand: - if "/" not in part: - return ( - "pathless validator module operand is not trusted; use an " - "absolute external path or protected module launcher" - ) - expect_module_operand = False - if part == "-m": - expect_module_operand = True - continue - if part.startswith("-"): - continue - path = Path(part).expanduser() - if not path.is_absolute() and "/" not in part: - if _is_script_like_operand(part): - return ( - "pathless validator script operand is not trusted; use an " - "absolute external path" - ) - continue - path_operands.append(path if path.is_absolute() else root / path) - for operand in path_operands: - resolved = operand.resolve() + operands = tuple(Path(part).expanduser() for part in command) + if not all(operand.is_absolute() for operand in operands): + return ( + "pathless or relative validator operands are not trusted; launcher " + "and entrypoint must be absolute external paths" + ) + for operand in operands: + try: + resolved = operand.resolve(strict=True) + except OSError: + return "validator launcher or entrypoint is missing" if _path_is_relative_to(resolved, candidate_root): return "explicit validator command inside the candidate checkout is not trusted" + if not resolved.is_file(): + return "validator launcher and entrypoint must be files" + if not os.access(operands[0], os.X_OK): + return "validator launch executable is not executable" return None +def _jest_command_error(root: Path, command: tuple[str, ...]) -> str | None: + """Enforce the protected Jest command-prefix grammar.""" + return _protected_command_error(root, command) + + +def _vitest_command_error(root: Path, command: tuple[str, ...]) -> str | None: + """Enforce the protected Vitest command-prefix grammar.""" + return _protected_command_error(root, command) + + _JEST_REPORTER = """class PddTrustedReporter { constructor() { this.tests = []; } onTestResult(test, result) { @@ -1986,7 +1978,7 @@ def _run_jest( if (root / "node_modules" / "jest" / "bin" / "jest.js").is_file(): return RunnerExecution("jest", EvidenceOutcome.ERROR, "jest-untrusted", "candidate node_modules Jest runner is not trusted"), () return RunnerExecution("jest", EvidenceOutcome.ERROR, "jest-unavailable", "no local Jest binary is available"), () - command_error = _protected_command_error(root, command_prefix) + command_error = _jest_command_error(root, command_prefix) if command_error is not None: return RunnerExecution( "jest", EvidenceOutcome.ERROR, "jest-untrusted", command_error @@ -2094,7 +2086,7 @@ def _run_vitest( if (tool_root / "node_modules" / "vitest" / "vitest.mjs").is_file(): return RunnerExecution("vitest", EvidenceOutcome.ERROR, "vitest-untrusted", "candidate node_modules Vitest runner is not trusted"), () return RunnerExecution("vitest", EvidenceOutcome.ERROR, "vitest-unavailable", "no local Vitest binary is available"), () - command_error = _protected_command_error(root, command_prefix) + command_error = _vitest_command_error(root, command_prefix) if command_error is not None: return RunnerExecution( "vitest", EvidenceOutcome.ERROR, "vitest-untrusted", command_error @@ -2143,6 +2135,12 @@ def _playwright_command_error(root: Path, command: tuple[str, ...]) -> str | Non """Enforce the exact protected Playwright launcher grammar.""" error = _protected_command_error(root, command) if error is not None: + if error in { + "validator launcher or entrypoint is missing", + "validator launcher and entrypoint must be files", + "validator launch executable is not executable", + }: + return f"Playwright launch is invalid: {error}" return error if len(command) != 2: return "Playwright command must be exactly an executable and CLI entrypoint" @@ -3149,7 +3147,7 @@ def run_obligation( + ", ".join(sorted(dirty)), ) if obligation.validator_id == "jest" and config.jest_command is not None: - command_error = _protected_command_error(root, config.jest_command) + command_error = _jest_command_error(root, config.jest_command) if command_error is not None: return RunnerExecution( obligation.obligation_id, @@ -3168,7 +3166,7 @@ def run_obligation( "candidate node_modules Vitest runner is not trusted", ) if config.vitest_command is not None: - command_error = _protected_command_error(root, config.vitest_command) + command_error = _vitest_command_error(root, config.vitest_command) if command_error is not None: return RunnerExecution( obligation.obligation_id, @@ -3344,7 +3342,7 @@ def run_profile( config.playwright_command, str(config.playwright_toolchain_manifest.resolve()) if config.playwright_toolchain_manifest is not None else None, - binding.artifact_closure_digest, + binding.artifact_closure_digest or binding.snapshot_digest, ) results = tuple( ObligationEvidence(item.obligation_id, item.outcome) for item in executions diff --git a/pdd/sync_core/trust.py b/pdd/sync_core/trust.py index 0cc3b8257..7327441a3 100644 --- a/pdd/sync_core/trust.py +++ b/pdd/sync_core/trust.py @@ -23,6 +23,10 @@ from .types import EvidenceOutcome, ObligationEvidence, UnitId +# Pre-closure v1 envelopes had an eight-day maximum lifetime when v2 shipped. +LEGACY_CLOSURE_MIGRATION_CUTOFF = datetime(2026, 7, 20, tzinfo=timezone.utc) + + class AttestationError(ValueError): """Raised when semantic evidence fails protected trust policy.""" @@ -212,8 +216,16 @@ class AttestationEnvelope: def payload(self) -> bytes: """Return canonical signed bytes, excluding the signature itself.""" - if self.payload_version not in {1, 2}: + if ( + not isinstance(self.payload_version, int) + or isinstance(self.payload_version, bool) + or self.payload_version not in {1, 2} + ): raise AttestationError("attestation payload version is unsupported") + if self.payload_version == 1 and self.binding.artifact_closure_digest: + raise AttestationError("v1 attestation cannot claim an artifact closure") + if self.payload_version == 2 and not self.binding.artifact_closure_digest: + raise AttestationError("v2 attestation requires an artifact closure") data = { "attestation_id": self.attestation_id, "issuer": self.issuer, @@ -476,7 +488,17 @@ def _verify( raise AttestationError("attestation is revoked") self._verify_signature(envelope) self._verify_freshness(envelope, now or datetime.now(timezone.utc)) - if envelope.binding != expected: + expected_binding = expected + if envelope.payload_version == 1: + expires = _parse_timestamp(envelope.validity.expires_at) + if expires > LEGACY_CLOSURE_MIGRATION_CUTOFF: + raise AttestationError("legacy attestation migration window is closed") + if expected.artifact_closure_digest != envelope.binding.snapshot_digest: + raise AttestationError( + "legacy attestation cannot establish the current artifact closure" + ) + expected_binding = replace(expected, artifact_closure_digest="") + if envelope.binding != expected_binding: raise AttestationError("attestation does not match the checked closure") identifiers = [result.obligation_id for result in envelope.results] if len(identifiers) != len(set(identifiers)): diff --git a/tests/test_sync_core_evidence_store.py b/tests/test_sync_core_evidence_store.py index 01b52ef6f..8129ce5db 100644 --- a/tests/test_sync_core_evidence_store.py +++ b/tests/test_sync_core_evidence_store.py @@ -36,7 +36,14 @@ def _git(root: Path, *args: str) -> str: def _envelope(): binding = AttestationBinding( - UNIT, "snapshot", "profile", "runner", "pdd-test", "base", "head" + UNIT, + "snapshot", + "profile", + "runner", + "pdd-test", + "base", + "head", + artifact_closure_digest="snapshot", ) return SIGNER.issue( AttestationRequest( @@ -87,7 +94,7 @@ def test_pre_closure_signed_envelope_uses_explicit_legacy_payload_shape() -> Non assert decoded.payload() == legacy_bytes AttestationTrustPolicy({SIGNER.issuer: SIGNER.public_key_bytes()}).verify( decoded, - decoded.binding, + replace(decoded.binding, artifact_closure_digest="snapshot"), now=datetime(2026, 7, 10, 12, 1, tzinfo=timezone.utc), ) diff --git a/tests/test_sync_core_trust.py b/tests/test_sync_core_trust.py index 126c7e603..f50005d3f 100644 --- a/tests/test_sync_core_trust.py +++ b/tests/test_sync_core_trust.py @@ -54,6 +54,7 @@ def _binding(**overrides): "tool_version": "pdd-test", "base_sha": "base-1", "checked_sha": "head-1", + "artifact_closure_digest": "snapshot-1", } values.update(overrides) return AttestationBinding(**values) From e0ab4f2219de1fe4c2070770712597d764801f4c Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 14:12:01 -0700 Subject: [PATCH 30/31] test(sync): reject vulnerable legacy runner evidence --- tests/test_sync_core_reporting.py | 44 +++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/tests/test_sync_core_reporting.py b/tests/test_sync_core_reporting.py index 4730bff33..33f78d743 100644 --- a/tests/test_sync_core_reporting.py +++ b/tests/test_sync_core_reporting.py @@ -4,6 +4,7 @@ import json import os import subprocess +import sys from dataclasses import replace from datetime import datetime, timezone from pathlib import Path, PurePosixPath @@ -155,6 +156,7 @@ def _finalize_trusted_baseline( *, artifact_closure_digest: str | None = None, legacy_pre_closure: bool = False, + playwright_command: tuple[str, ...] | None = None, ) -> None: manifest = build_unit_manifest(root, base_ref=commit, head_ref=commit) profile = load_verification_profiles(root, manifest).profiles[0] @@ -164,17 +166,27 @@ def _finalize_trusted_baseline( unit.unit_id, snapshot.digest(), profile.profile_digest, - runner_identity_digest(profile, root=root, ref=commit), + runner_identity_digest( + profile, + root=root, + ref=commit, + config=RunnerConfig(playwright_command=playwright_command), + ), TRUSTED_RUNNER_VERSION, commit, commit, + playwright_command, artifact_closure_digest=artifact_closure_digest or snapshot.digest(), ) envelope = SIGNER.issue( AttestationRequest( "attestation-widget-1", binding, - (ObligationEvidence("pytest", EvidenceOutcome.PASS),), + ( + ObligationEvidence( + profile.obligations[0].obligation_id, EvidenceOutcome.PASS + ), + ), "nonce-widget-1", NOW, ) @@ -265,6 +277,34 @@ def test_canonical_report_migrates_fresh_signed_pre_closure_envelope(tmp_path) - assert report["counts"]["trusted_in_sync"] == 1 +def test_canonical_report_rejects_signed_legacy_pathless_playwright_command( + tmp_path, +) -> None: + root, commit = _repository(tmp_path) + profile_path = root / ".pdd/verification-profiles.json" + profile = json.loads(profile_path.read_text(encoding="utf-8")) + profile["profiles"][0]["obligations"][0].update( + {"obligation_id": "playwright", "validator_id": "playwright"} + ) + profile_path.write_text(json.dumps(profile), encoding="utf-8") + _git(root, "add", str(profile_path.relative_to(root))) + _git(root, "commit", "-q", "-m", "use Playwright verification") + commit = _git(root, "rev-parse", "HEAD") + + _finalize_trusted_baseline( + root, + commit, + legacy_pre_closure=True, + playwright_command=(sys.executable, "fake_playwright"), + ) + + report = build_canonical_report(root, _options(tmp_path, commit)) + + assert report["ok"] is False + assert report["counts"]["trusted_current_evidence"] == 0 + assert "Playwright" in report["units"][0]["reason"] + + def test_finalizer_reuses_fresh_signed_pre_closure_envelope(tmp_path) -> None: root, commit = _repository(tmp_path) _finalize_trusted_baseline(root, commit, legacy_pre_closure=True) From 43885fe7b5e55d4cfda4a3928007c22e77ec6756 Mon Sep 17 00:00:00 2001 From: Greg Tanaka Date: Sat, 11 Jul 2026 14:30:29 -0700 Subject: [PATCH 31/31] fix(sync): version legacy runner attestation migration --- pdd/sync_core/finalize.py | 21 +++--- pdd/sync_core/reporting.py | 23 ++----- pdd/sync_core/runner.py | 49 +++++++++++++- tests/test_sync_core_reporting.py | 109 +++++++++++++++++++++++++++++- 4 files changed, 170 insertions(+), 32 deletions(-) diff --git a/pdd/sync_core/finalize.py b/pdd/sync_core/finalize.py index 18079906e..bf3837ea4 100644 --- a/pdd/sync_core/finalize.py +++ b/pdd/sync_core/finalize.py @@ -26,8 +26,8 @@ AttestationIssue, RunBinding, RunnerConfig, + attested_runner_identity_error, run_profile, - runner_identity_digest, ) from .snapshot import build_unit_snapshot from .transaction import ( @@ -213,18 +213,8 @@ def _reusable_result( snapshot.unit_id, snapshot.digest(), profile.profile_digest, - runner_identity_digest( - profile, - root=root, - ref=head_sha, - config=RunnerConfig( - playwright_command=envelope.binding.playwright_command, - playwright_toolchain_manifest=Path( - envelope.binding.playwright_toolchain_manifest - ) if envelope.binding.playwright_toolchain_manifest else None, - ), - ), - TRUSTED_RUNNER_VERSION, + envelope.binding.runner_digest, + envelope.binding.tool_version, base_sha, envelope.binding.checked_sha, envelope.binding.playwright_command, @@ -232,6 +222,11 @@ def _reusable_result( snapshot.digest(), ) verifier.verify_current_for_idempotency(envelope, binding, now=now) + runner_error = attested_runner_identity_error( + root, head_sha, profile, envelope.binding + ) + if runner_error is not None: + raise ValueError(runner_error) ancestry = subprocess.run( ["git", "merge-base", "--is-ancestor", binding.checked_sha, head_sha], cwd=root, diff --git a/pdd/sync_core/reporting.py b/pdd/sync_core/reporting.py index 02390cae3..1f9a69cb1 100644 --- a/pdd/sync_core/reporting.py +++ b/pdd/sync_core/reporting.py @@ -19,7 +19,7 @@ from .git_io import resolve_git_commit from .manifest import ManifestUnit, UnitManifest, build_unit_manifest from .snapshot import SnapshotError, build_unit_snapshot -from .runner import RunnerConfig, TRUSTED_RUNNER_VERSION, runner_identity_digest +from .runner import attested_runner_identity_error from .transaction import TransactionError, TransactionManager from .trust import AttestationError, ValidationEvidence from .types import ( @@ -174,22 +174,13 @@ def _evidence( ): return None profile = context.profiles.for_unit(expectation.unit.unit_id) - if ( - profile is None - or binding.runner_digest - != runner_identity_digest( - profile, - root=context.root, - ref=context.manifest.head_ref, - config=RunnerConfig( - playwright_command=binding.playwright_command, - playwright_toolchain_manifest=Path(binding.playwright_toolchain_manifest) - if binding.playwright_toolchain_manifest else None, - ), - ) - or binding.tool_version != TRUSTED_RUNNER_VERSION - ): + if profile is None: raise AttestationError("attestation runner identity is not protected") + runner_error = attested_runner_identity_error( + context.root, context.manifest.head_ref, profile, binding + ) + if runner_error is not None: + raise AttestationError(runner_error) return evidence diff --git a/pdd/sync_core/runner.py b/pdd/sync_core/runner.py index 55c716aa2..ba590b4b2 100644 --- a/pdd/sync_core/runner.py +++ b/pdd/sync_core/runner.py @@ -45,7 +45,8 @@ from .supervisor import run_supervised -TRUSTED_RUNNER_VERSION = "pdd-trusted-runner-v2" +PREDECESSOR_TRUSTED_RUNNER_VERSION = "pdd-trusted-runner-v2" +TRUSTED_RUNNER_VERSION = "pdd-trusted-runner-v3" PYTEST_CONFIG_PATHS = ( PurePosixPath("pytest.ini"), PurePosixPath("pyproject.toml"), @@ -1402,10 +1403,11 @@ def _managed_subprocess( def runner_identity_digest( profile: VerificationProfile, *, root: Path | None = None, ref: str = "HEAD", config: RunnerConfig = RunnerConfig(), + tool_version: str = TRUSTED_RUNNER_VERSION, ) -> str: """Bind evidence to protected adapters, configs, and exact artifact scopes.""" payload = { - "tool_version": TRUSTED_RUNNER_VERSION, + "tool_version": tool_version, "pytest_command": [ sys.executable, "-m", @@ -2157,6 +2159,49 @@ def _playwright_command_error(root: Path, command: tuple[str, ...]) -> str | Non return None +def attested_runner_identity_error( + root: Path, + ref: str, + profile: VerificationProfile, + binding: AttestationBinding, +) -> str | None: + """Validate current or safely migratable predecessor runner identity.""" + playwright_obligation = any( + obligation.validator_id == "playwright" + for obligation in profile.obligations + ) + if binding.playwright_command is not None: + if not playwright_obligation: + return "attestation contains an unexpected Playwright command" + command_error = _playwright_command_error(root, binding.playwright_command) + if command_error is not None: + return f"attested Playwright command is not trusted: {command_error}" + elif binding.playwright_toolchain_manifest is not None: + return "attestation contains a Playwright manifest without a command" + + allowed_versions = { + TRUSTED_RUNNER_VERSION, + PREDECESSOR_TRUSTED_RUNNER_VERSION, + } + if binding.tool_version not in allowed_versions: + return "attestation runner version is not protected" + expected_digest = runner_identity_digest( + profile, + root=root, + ref=ref, + config=RunnerConfig( + playwright_command=binding.playwright_command, + playwright_toolchain_manifest=Path( + binding.playwright_toolchain_manifest + ) if binding.playwright_toolchain_manifest else None, + ), + tool_version=binding.tool_version, + ) + if binding.runner_digest != expected_digest: + return "attestation runner digest is not protected" + return None + + def _playwright_candidate_toolchain(root: Path) -> bool: """Return whether candidate checkout infrastructure would affect Playwright.""" node_modules = root / "node_modules" diff --git a/tests/test_sync_core_reporting.py b/tests/test_sync_core_reporting.py index 33f78d743..becd90b83 100644 --- a/tests/test_sync_core_reporting.py +++ b/tests/test_sync_core_reporting.py @@ -38,6 +38,7 @@ load_verification_profiles, runner_identity_digest, ) +from pdd.sync_core.runner import PREDECESSOR_TRUSTED_RUNNER_VERSION from pdd.sync_core.identity import initialize_repository_identity from pdd.sync_core.types import ObligationEvidence from pdd.ci_drift_heal import main as ci_drift_heal_main @@ -157,6 +158,7 @@ def _finalize_trusted_baseline( artifact_closure_digest: str | None = None, legacy_pre_closure: bool = False, playwright_command: tuple[str, ...] | None = None, + tool_version: str = TRUSTED_RUNNER_VERSION, ) -> None: manifest = build_unit_manifest(root, base_ref=commit, head_ref=commit) profile = load_verification_profiles(root, manifest).profiles[0] @@ -171,8 +173,9 @@ def _finalize_trusted_baseline( root=root, ref=commit, config=RunnerConfig(playwright_command=playwright_command), + tool_version=tool_version, ), - TRUSTED_RUNNER_VERSION, + tool_version, commit, commit, playwright_command, @@ -296,6 +299,7 @@ def test_canonical_report_rejects_signed_legacy_pathless_playwright_command( commit, legacy_pre_closure=True, playwright_command=(sys.executable, "fake_playwright"), + tool_version=PREDECESSOR_TRUSTED_RUNNER_VERSION, ) report = build_canonical_report(root, _options(tmp_path, commit)) @@ -305,6 +309,37 @@ def test_canonical_report_rejects_signed_legacy_pathless_playwright_command( assert "Playwright" in report["units"][0]["reason"] +def test_canonical_report_migrates_safe_predecessor_playwright_command( + tmp_path, +) -> None: + root, _commit = _repository(tmp_path) + profile_path = root / ".pdd/verification-profiles.json" + profile = json.loads(profile_path.read_text(encoding="utf-8")) + profile["profiles"][0]["obligations"][0].update( + {"obligation_id": "playwright", "validator_id": "playwright"} + ) + profile_path.write_text(json.dumps(profile), encoding="utf-8") + _git(root, "add", str(profile_path.relative_to(root))) + _git(root, "commit", "-q", "-m", "use Playwright verification") + commit = _git(root, "rev-parse", "HEAD") + external_entrypoint = tmp_path / "trusted-tools" / "playwright-cli" + external_entrypoint.parent.mkdir() + external_entrypoint.write_text("// trusted entrypoint\n", encoding="utf-8") + + _finalize_trusted_baseline( + root, + commit, + legacy_pre_closure=True, + playwright_command=(sys.executable, str(external_entrypoint)), + tool_version=PREDECESSOR_TRUSTED_RUNNER_VERSION, + ) + + report = build_canonical_report(root, _options(tmp_path, commit)) + + assert report["ok"] is True + assert report["counts"]["trusted_current_evidence"] == 1 + + def test_finalizer_reuses_fresh_signed_pre_closure_envelope(tmp_path) -> None: root, commit = _repository(tmp_path) _finalize_trusted_baseline(root, commit, legacy_pre_closure=True) @@ -324,6 +359,78 @@ def test_finalizer_reuses_fresh_signed_pre_closure_envelope(tmp_path) -> None: run_profile_mock.assert_not_called() +def test_finalizer_does_not_reuse_legacy_pathless_playwright_command( + tmp_path, +) -> None: + root, _commit = _repository(tmp_path) + profile_path = root / ".pdd/verification-profiles.json" + profile = json.loads(profile_path.read_text(encoding="utf-8")) + profile["profiles"][0]["obligations"][0].update( + {"obligation_id": "playwright", "validator_id": "playwright"} + ) + profile_path.write_text(json.dumps(profile), encoding="utf-8") + _git(root, "add", str(profile_path.relative_to(root))) + _git(root, "commit", "-q", "-m", "use Playwright verification") + commit = _git(root, "rev-parse", "HEAD") + _finalize_trusted_baseline( + root, + commit, + legacy_pre_closure=True, + playwright_command=(sys.executable, "fake_playwright"), + tool_version=PREDECESSOR_TRUSTED_RUNNER_VERSION, + ) + + with patch("pdd.sync_core.finalize.run_profile") as run_profile_mock: + with pytest.raises(ValueError, match="attested Playwright command"): + finalize_unit( + root, + PurePosixPath("prompts/widget_python.prompt"), + base_ref=commit, + head_ref=commit, + signer=SIGNER, + replay_ledger_path=tmp_path / "external-trust/legacy-reuse.json", + ) + + run_profile_mock.assert_not_called() + + +def test_finalizer_reuses_safe_predecessor_playwright_command(tmp_path) -> None: + root, _commit = _repository(tmp_path) + profile_path = root / ".pdd/verification-profiles.json" + profile = json.loads(profile_path.read_text(encoding="utf-8")) + profile["profiles"][0]["obligations"][0].update( + {"obligation_id": "playwright", "validator_id": "playwright"} + ) + profile_path.write_text(json.dumps(profile), encoding="utf-8") + _git(root, "add", str(profile_path.relative_to(root))) + _git(root, "commit", "-q", "-m", "use Playwright verification") + commit = _git(root, "rev-parse", "HEAD") + external_entrypoint = tmp_path / "trusted-tools" / "playwright-cli" + external_entrypoint.parent.mkdir() + external_entrypoint.write_text("// trusted entrypoint\n", encoding="utf-8") + _finalize_trusted_baseline( + root, + commit, + legacy_pre_closure=True, + playwright_command=(sys.executable, str(external_entrypoint)), + tool_version=PREDECESSOR_TRUSTED_RUNNER_VERSION, + ) + + with patch("pdd.sync_core.finalize.run_profile") as run_profile_mock: + result = finalize_unit( + root, + PurePosixPath("prompts/widget_python.prompt"), + base_ref=commit, + head_ref=commit, + signer=SIGNER, + replay_ledger_path=tmp_path / "external-trust/legacy-reuse.json", + ) + + assert result.transaction.no_op is True + assert result.attestation_id == "attestation-widget-1" + run_profile_mock.assert_not_called() + + def test_managed_waiver_is_counted_and_blocks_certificate_predicate(tmp_path) -> None: root, _commit = _repository(tmp_path) (root / ".pdd/sync-waivers.json").write_text(