From 8a6e10d0ef5508100b9979b6caad21461f61dd1a Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 05:22:43 -0700 Subject: [PATCH 1/7] feat: require signed production e2e evidence --- CHANGELOG.md | 7 + artifacts/p1_acceptance_report.json | 27 +- contracts/schemas/certification-run.v1.json | 21 +- deploy/deploy_forge.sh | 2 +- docs/OPERATIONS.md | 8 + docs/PROJECT_CONTRACT.md | 16 + policies/mandatory-rules.v1.json | 8 + policies/mandatory-rules.v2.json | 8 + pyproject.toml | 2 +- scripts/certification_journey.py | 2 +- scripts/p1_acceptance.py | 29 +- src/echo_certification_forge/__init__.py | 2 +- src/echo_certification_forge/executor.py | 40 +- src/echo_certification_forge/intake.py | 66 +++- src/echo_certification_forge/models.py | 6 + .../production_e2e.py | 351 ++++++++++++++++++ src/echo_certification_forge/run_worker.py | 45 ++- src/echo_certification_forge/verdict.py | 45 +++ tests/conftest.py | 5 +- tests/production_e2e_support.py | 46 +++ tests/test_additional_contracts.py | 9 +- tests/test_evidence_and_verdict.py | 58 ++- tests/test_p5_executor_adapter_gate.py | 5 +- tests/test_p6_deployment_enforcement.py | 6 +- tests/test_p6_platform_integration.py | 32 +- tests/test_p7_subscriber_governance.py | 3 + tests/test_production_e2e.py | 214 +++++++++++ tests/test_runner.py | 7 +- tests/test_t4_live_run.py | 4 +- tests/test_t4p5_executor.py | 6 +- tests/test_t4p7_e2e.py | 2 + tests/test_t4p8_sandbox.py | 2 + 32 files changed, 1027 insertions(+), 57 deletions(-) create mode 100644 src/echo_certification_forge/production_e2e.py create mode 100644 tests/production_e2e_support.py create mode 100644 tests/test_production_e2e.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a8b23b..4d2f4c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.2.0] - 2026-08-30 + +### Added + +- A pinned-key-signed production E2E gate required before any `PRODUCTION_READY` verdict. +- Exact E2E bindings for source, deployment, target, environment, accounts, and clients. + All notable changes to Echo Certification Forge are documented here. Versions follow Semantic Versioning; dates use ISO 8601. diff --git a/artifacts/p1_acceptance_report.json b/artifacts/p1_acceptance_report.json index 771a206..1e54769 100644 --- a/artifacts/p1_acceptance_report.json +++ b/artifacts/p1_acceptance_report.json @@ -1,10 +1,10 @@ { - "manifest_digest": "0ca325c3e6d39b7f22cdd3d01068c8f99dcd976c4422200b5b4d4cfe452f29f7", + "manifest_digest": "9e9f963377374e827ccc9c8b07ea82cc7966e25a2392838997d3c1371f5a4e5c", "manifest_id": "certforge.release-strict.v1", "passed": true, "phase": "P1", - "platform": "Windows-11-10.0.22631-SP0", - "python": "3.13.0 (tags/v3.13.0:60403a5, Oct 7 2024, 09:38:07) [MSC v.1941 64 bit (AMD64)]", + "platform": "Windows-11-10.0.26200-SP0", + "python": "3.12.10 (tags/v3.12.10:0cc8128, Apr 8 2025, 12:21:36) [MSC v.1943 64 bit (AMD64)]", "scenarios": { "default_block": { "passed": true, @@ -17,27 +17,30 @@ "mandatory_rule_missing:environment_identity", "mandatory_rule_missing:evidence_integrity", "mandatory_rule_missing:immutable_target_identity", + "mandatory_rule_missing:production_e2e", "mandatory_rule_missing:runner_control_channel", "mandatory_rule_missing:signing_authority_separation", "mandatory_rule_missing:tenant_isolation", + "production_e2e_attestation_missing", "run_outcome_inconclusive" ], "verdict": "NOT_READY" }, + "source_only_blocks": { + "deploy_gate_allowed": false, + "evidence_merkle_root": "41a4fc282a478767dc16f6c64128c93ab2535866e620a3f5b0ae922220ffb449", + "passed": true, + "payload_sha256": "a7255f5bae53fcd49b1e049b3edd1feb6c9b03f4520b0e2abb0e44f0da7138e7", + "signing_key_id": "ed25519:e222f1c31e85f1b8a6d8cfed30fd2300", + "verdict": "NOT_READY" + }, "tamper_blocks": { "deploy_gate_allowed": false, "passed": true, "reasons": [ - "current_evidence_invalid" + "current_evidence_invalid", + "verdict_not_production_ready" ] - }, - "verified_ready": { - "deploy_gate_allowed": true, - "evidence_merkle_root": "438efda29a515669d0db0472f6527c201c55d0d80afebf2963123d11c77a7d73", - "passed": true, - "payload_sha256": "db3fe179c7c950a9ffaf90cfef6d475fc915af87f0d1b10a85071c7d8411b975", - "signing_key_id": "ed25519:4592da038060823a957506d477e49114", - "verdict": "PRODUCTION_READY" } }, "schema_version": "1.0.0" diff --git a/contracts/schemas/certification-run.v1.json b/contracts/schemas/certification-run.v1.json index 3e4f4b0..4797ffd 100644 --- a/contracts/schemas/certification-run.v1.json +++ b/contracts/schemas/certification-run.v1.json @@ -3,7 +3,7 @@ "$id": "https://cert.echoforge.com/contracts/certification-run.v1.json", "title": "CertificationRunV1", "type": "object", - "required": ["run_id", "tenant_id", "state", "run_outcome", "release_verdict", "target_identity_digest", "environment_identity_digest", "policy_version"], + "required": ["run_id", "tenant_id", "state", "run_outcome", "release_verdict", "target_identity_digest", "environment_identity_digest", "policy_version", "production_e2e"], "properties": { "run_id": {"type": "string", "minLength": 1, "maxLength": 128}, "tenant_id": {"type": "string", "minLength": 1, "maxLength": 128}, @@ -14,6 +14,25 @@ "environment_identity_digest": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, "policy_version": {"type": "string", "minLength": 1, "maxLength": 128}, "evidence_merkle_root": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"}, + "production_e2e": { + "type": "object", + "required": ["verified", "profile", "attestation_id", "source_commit", "deployment_sha", "target_identity_digest", "environment_identity_digest", "canonical_target", "tool_count", "signature_verified", "collector_key_id", "attestation_envelope_sha256"], + "properties": { + "verified": {"type": "boolean"}, + "profile": {"type": ["string", "null"]}, + "attestation_id": {"type": ["string", "null"]}, + "source_commit": {"type": ["string", "null"], "pattern": "^[0-9a-f]{40}(?:[0-9a-f]{24})?$"}, + "deployment_sha": {"type": ["string", "null"], "pattern": "^[0-9a-f]{40}(?:[0-9a-f]{24})?$"}, + "target_identity_digest": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"}, + "environment_identity_digest": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"}, + "canonical_target": {"type": ["string", "null"]}, + "tool_count": {"type": ["integer", "null"], "minimum": 0}, + "signature_verified": {"type": "boolean"}, + "collector_key_id": {"type": ["string", "null"], "pattern": "^ed25519:[0-9a-f]{32}$"}, + "attestation_envelope_sha256": {"type": ["string", "null"], "pattern": "^[0-9a-f]{64}$"} + }, + "additionalProperties": false + }, "created_at": {"type": "string", "format": "date-time"}, "updated_at": {"type": "string", "format": "date-time"} }, diff --git a/deploy/deploy_forge.sh b/deploy/deploy_forge.sh index c280297..2e649b5 100644 --- a/deploy/deploy_forge.sh +++ b/deploy/deploy_forge.sh @@ -16,7 +16,7 @@ CURRENT_LINK="${CERTFORGE_CURRENT_LINK:-/home/forge/echo-certification-forge-cur STATE_ROOT="${CERTFORGE_STATE_ROOT:-/home/forge/echo-certification-forge/var}" ADAPTER_DIR="${ECHO_CERTFORGE_PROD_ADAPTER_DIR:-$STATE_ROOT/p5}" ADAPTER_MODE="${CERTFORGE_ADAPTER_MODE:-required}" -TRUSTED_MANIFEST_SHA256="${ECHO_CERTFORGE_TRUSTED_MANIFEST_SHA256:-7dc98e0e95e6dd2c000ec069a8c46c4d1d49a4fe869ad4eae25e059d103644f4}" +TRUSTED_MANIFEST_SHA256="${ECHO_CERTFORGE_TRUSTED_MANIFEST_SHA256:-965106b00917268d556b325719f26f5096e6c3746551658ffecb9fd4a95ec342}" UNIT_PATH="/etc/systemd/system/$SERVICE.service" DISPATCH_UNIT_PATH="/etc/systemd/system/$DISPATCH_SERVICE.service" RELEASE_DROPIN="/etc/systemd/system/$SERVICE.service.d/10-release.conf" diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 66e4b6f..7e47fee 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -10,6 +10,14 @@ ## Verification sequence +Before a worker may issue `PRODUCTION_READY`, mount a collector envelope through +`--production-e2e-attestation` (or +`ECHO_CERTFORGE_PRODUCTION_E2E_ATTESTATION`) and a directory containing only the +pinned collector public keys through `--trusted-production-e2e-keys` (or +`ECHO_CERTFORGE_TRUSTED_PRODUCTION_E2E_KEYS`). The private collector key must not +be present on the worker. Absence, signature failure, expiration, or any exact +identity/E2E mismatch is a normal `NOT_READY` result, never a bypass. + 1. Confirm hosted CI succeeded for the exact source SHA. 2. Execute the declared production-shaped journey with the pinned runner image. 3. Verify evidence custody, receipt chains, signatures, expiry, and revocation state. diff --git a/docs/PROJECT_CONTRACT.md b/docs/PROJECT_CONTRACT.md index eb191dd..e452ded 100644 --- a/docs/PROJECT_CONTRACT.md +++ b/docs/PROJECT_CONTRACT.md @@ -41,3 +41,19 @@ GS343 may propose discovery, classification, and bounded harness repairs. Determ ## Completion rule Implementation, source validation, contracts, real runtime, failure and recovery, evidence package, scoped Git record, and Commander acceptance are distinct gates. A registered capability is not presumed visible, healthy, schema-complete, authorized, or runnable. + +## Production E2E verdict gate + +`PRODUCTION_READY` requires a current Ed25519-signed `certforge.production-e2e.v1` +attestation from an independently pinned collector. The attestation must bind the +exact target and environment identities, exact deployed source revision, real +critical journeys, negative controls, repeated stability probes, and external +acceptance. Source checks, local journeys, HTTP 200, unsigned status fields, and +target-authored evidence cannot satisfy this gate. Missing, expired, untrusted, +partially passing, or identity-mismatched attestations remain `NOT_READY`. + +For Echo GitHub Autonomy the target-specific profile additionally requires the +canonical MCP and OAuth surface, 30-tool schema, repeated discovery plus +invocation without registry loss, reconciled public/private inventory and +read/write/certify authority for all four exact GitHub account IDs, and matching +private-repository fingerprints from ChatGPT, Claude, Codex, and Grok. diff --git a/policies/mandatory-rules.v1.json b/policies/mandatory-rules.v1.json index ae17648..25e8723 100644 --- a/policies/mandatory-rules.v1.json +++ b/policies/mandatory-rules.v1.json @@ -83,6 +83,14 @@ "conditional_allowed": false, "minimum_evidence": 1, "description": "Deployment authorization checks the exact certified digest, policy, environment, and lifecycle." + }, + { + "id": "production_e2e", + "severity": "BLOCKER", + "mandatory": true, + "conditional_allowed": false, + "minimum_evidence": 1, + "description": "A trusted independent collector proves the exact deployed identity, real critical journeys, negative controls, stability, and external client acceptance end to end." } ] } diff --git a/policies/mandatory-rules.v2.json b/policies/mandatory-rules.v2.json index 45c4ee9..30a31dc 100644 --- a/policies/mandatory-rules.v2.json +++ b/policies/mandatory-rules.v2.json @@ -91,6 +91,14 @@ "conditional_allowed": false, "minimum_evidence": 1, "description": "Deployment authorization checks the exact certified digest, policy, environment, and lifecycle." + }, + { + "id": "production_e2e", + "severity": "BLOCKER", + "mandatory": true, + "conditional_allowed": false, + "minimum_evidence": 1, + "description": "A trusted independent collector proves the exact deployed identity, real critical journeys, negative controls, stability, and external client acceptance end to end." } ] } diff --git a/pyproject.toml b/pyproject.toml index 754c58c..3f1a10e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "echo-certification-forge" -version = "1.1.0" +version = "1.2.0" description = "Deterministic, evidence-backed release authority for EchoForge" readme = "README.md" requires-python = ">=3.12" diff --git a/scripts/certification_journey.py b/scripts/certification_journey.py index 712b631..38ca793 100644 --- a/scripts/certification_journey.py +++ b/scripts/certification_journey.py @@ -45,7 +45,7 @@ def require(condition: bool, message: str) -> None: def main() -> int: - require(__version__ == "1.1.0", "package version is not synchronized") + require(__version__ == "1.2.0", "package version is not synchronized") python_files = sorted(SRC.rglob("*.py")) require(bool(python_files), "source tree contains no Python modules") for path in python_files: diff --git a/scripts/p1_acceptance.py b/scripts/p1_acceptance.py index 77c43e4..163a603 100644 --- a/scripts/p1_acceptance.py +++ b/scripts/p1_acceptance.py @@ -70,30 +70,37 @@ def main() -> int: "reasons": list(default.reasons), } - store.register_run("cert-verified-ready", target, environment, manifest.manifest_id, manifest.digest) + # P1 is source/control-plane acceptance, not an external production E2E run. Even with + # every local rule row marked true, it must remain NOT_READY without the independently + # signed production_e2e attestation. + store.register_run("cert-source-only", target, environment, manifest.manifest_id, manifest.digest) for index, rule in enumerate(manifest.rules, start=1): artifact_id = f"proof-{index:02d}" content = json.dumps({"rule": rule.id, "passed": True}, sort_keys=True).encode() store.append_artifact( - "cert-verified-ready", target.tenant_id, artifact_id, content, + "cert-source-only", target.tenant_id, artifact_id, content, "application/json", "p1-independent-harness", RedactionStatus.COMPLETE, ) store.record_rule_result( - "cert-verified-ready", target.tenant_id, + "cert-source-only", target.tenant_id, RuleResult(rule.id, True, (artifact_id,), {"fixture": "known-good"}), ) - store.set_run_outcome("cert-verified-ready", target.tenant_id, RunOutcome.COMPLETE) - ready = engine.evaluate(store, "cert-verified-ready", target.tenant_id, manifest, signer.key_id) + store.set_run_outcome("cert-source-only", target.tenant_id, RunOutcome.COMPLETE) + ready = engine.evaluate(store, "cert-source-only", target.tenant_id, manifest, signer.key_id) envelope = signer.sign(ready) - store.save_signed_verdict("cert-verified-ready", target.tenant_id, envelope) + store.save_signed_verdict("cert-source-only", target.tenant_id, envelope) trusted = TrustedPublicKeyRegistry.empty() trusted.add_pem(signer.public_key_pem) gate = DeployGate(store, trusted).evaluate( - target.tenant_id, "cert-verified-ready", target.identity_digest, + target.tenant_id, "cert-source-only", target.identity_digest, environment.identity_digest, manifest.digest, ) - report["scenarios"]["verified_ready"] = { - "passed": ready.release_verdict.value == "PRODUCTION_READY" and gate.allowed, + report["scenarios"]["source_only_blocks"] = { + "passed": ( + ready.release_verdict.value == "NOT_READY" + and not gate.allowed + and "production_e2e_schema_invalid" in ready.reasons + ), "verdict": ready.release_verdict.value, "deploy_gate_allowed": gate.allowed, "evidence_merkle_root": ready.evidence_merkle_root, @@ -101,10 +108,10 @@ def main() -> int: "signing_key_id": envelope.key_id, } - tampered_path = workspace / "evidence" / target.tenant_id / "cert-verified-ready" / "artifacts" / "proof-01.bin" + tampered_path = workspace / "evidence" / target.tenant_id / "cert-source-only" / "artifacts" / "proof-01.bin" tampered_path.write_bytes(b"tampered") tampered_gate = DeployGate(store, trusted).evaluate( - target.tenant_id, "cert-verified-ready", target.identity_digest, + target.tenant_id, "cert-source-only", target.identity_digest, environment.identity_digest, manifest.digest, ) report["scenarios"]["tamper_blocks"] = { diff --git a/src/echo_certification_forge/__init__.py b/src/echo_certification_forge/__init__.py index c8c894b..ddc7799 100644 --- a/src/echo_certification_forge/__init__.py +++ b/src/echo_certification_forge/__init__.py @@ -3,4 +3,4 @@ from .models import ReleaseVerdict, RunOutcome, RunState __all__ = ["ReleaseVerdict", "RunOutcome", "RunState"] -__version__ = "1.1.0" +__version__ = "1.2.0" diff --git a/src/echo_certification_forge/executor.py b/src/echo_certification_forge/executor.py index 4105c7e..e909dc3 100644 --- a/src/echo_certification_forge/executor.py +++ b/src/echo_certification_forge/executor.py @@ -29,8 +29,11 @@ from .canonical import to_utc_iso, utc_now from .evidence import EvidenceStore from .hostile import scan_target_source -from .models import RedactionStatus, RunOutcome, RuleResult, RunState +from .models import EnvironmentIdentity, RedactionStatus, RuleResult, RunOutcome, RunState, TargetIdentity from .policy import RuleManifest +from .production_e2e import RULE_ID as PRODUCTION_E2E_RULE_ID +from .production_e2e import VerifiedProductionE2E +from .production_e2e import validate_production_e2e from .signing import Ed25519VerdictSigner from .supply_chain import scan_dockerfile from .verdict import DeterministicVerdictEngine @@ -193,6 +196,7 @@ def execute( control_attestations: dict[str, bool] | None = None, adapter_records: tuple[AdapterExecutionRecord, ...] | None = None, adapter_policy: AdapterAcceptancePolicy | None = None, + production_e2e_attestation: VerifiedProductionE2E | None = None, execution_guard: Callable[[], None] | None = None, completion_callback: Callable[[Any], None] | None = None, ) -> ExecutionResult: @@ -393,6 +397,32 @@ def bound(digest_key: str) -> bool: # Includes adapter records/assessment when supplied. evidence_chain_ok = self.store.verify_evidence(run_id, tenant_id).valid + production_e2e_passed = False + production_e2e_detail: dict[str, Any] = { + "validation": "production_e2e_attestation_missing" + } + try: + target_identity = TargetIdentity(**json.loads(run["target_identity_json"])) + environment_identity = EnvironmentIdentity(**json.loads(run["environment_identity_json"])) + production_e2e_payload = ( + production_e2e_attestation.payload + if isinstance(production_e2e_attestation, VerifiedProductionE2E) + else None + ) + production_e2e_passed, validation = validate_production_e2e( + production_e2e_payload, + target_identity, + environment_identity, + now=utc_now(), + ) + production_e2e_detail = ( + production_e2e_attestation.result_details() + if production_e2e_passed and production_e2e_attestation is not None + else {"validation": validation} + ) + except (TypeError, ValueError): + production_e2e_detail = {"validation": "production_e2e_identity_invalid"} + checks: dict[str, tuple[bool, dict]] = { "immutable_target_identity": ( bound("target_identity_digest"), @@ -429,6 +459,7 @@ def bound(digest_key: str) -> bool: bool(control_attestations.get("signing_authority_separation", False)), {"check": "trusted_caller_attestation"}, ), + PRODUCTION_E2E_RULE_ID: (production_e2e_passed, production_e2e_detail), } for rule in self.manifest.rules: if execution_guard is not None: @@ -444,7 +475,12 @@ def bound(digest_key: str) -> bool: self.store.record_rule_result( run_id, tenant_id, - RuleResult(rule.id, passed, (artifact_id,), {"source": "executor"}), + RuleResult( + rule.id, + passed, + (artifact_id,), + detail if rule.id == PRODUCTION_E2E_RULE_ID else {"source": "executor"}, + ), ) self._t(run_id, tenant_id, RunState.CLASSIFYING_FINDINGS, "classify") diff --git a/src/echo_certification_forge/intake.py b/src/echo_certification_forge/intake.py index 2842cf5..67cf2af 100644 --- a/src/echo_certification_forge/intake.py +++ b/src/echo_certification_forge/intake.py @@ -12,9 +12,17 @@ from pydantic import BaseModel, ConfigDict, Field -from .canonical import sha256_json +from .canonical import sha256_json, utc_now from .evidence import EvidenceStore -from .models import ReleaseVerdict, RunState, declared_target_identity_digest +from .models import ( + EnvironmentIdentity, + ReleaseVerdict, + RunState, + TargetIdentity, + declared_target_identity_digest, +) +from .production_e2e import RULE_ID as PRODUCTION_E2E_RULE_ID +from .production_e2e import validate_production_e2e from .policy import RuleManifest # Internal fine-grained RunState -> coarse public state (contracts/schemas/certification-run.v1.json). @@ -152,6 +160,59 @@ def project_run(store: EvidenceStore, row: dict[str, Any]) -> dict[str, Any]: payload = json.loads(verdict_row["payload_json"]) release_verdict = payload.get("release_verdict", release_verdict) evidence_merkle_root = payload.get("evidence_merkle_root") + production_e2e = { + "verified": False, + "profile": None, + "attestation_id": None, + "source_commit": None, + "deployment_sha": None, + "target_identity_digest": None, + "environment_identity_digest": None, + "canonical_target": None, + "tool_count": None, + "signature_verified": False, + "collector_key_id": None, + "attestation_envelope_sha256": None, + } + result = store.list_rule_results(run_id, tenant_id).get(PRODUCTION_E2E_RULE_ID) + if result is not None: + try: + target = TargetIdentity(**json.loads(row["target_identity_json"])) + environment = EnvironmentIdentity(**json.loads(row["environment_identity_json"])) + verified, _reason = validate_production_e2e( + result.details, + target, + environment, + now=utc_now(), + ) + production_e2e = { + "verified": verified, + "profile": result.details.get("profile") if verified else None, + "attestation_id": result.details.get("attestation_id") if verified else None, + "source_commit": result.details.get("source_commit") if verified else None, + "deployment_sha": result.details.get("deployment_sha") if verified else None, + "target_identity_digest": ( + result.details.get("target_identity_digest") if verified else None + ), + "environment_identity_digest": ( + result.details.get("environment_identity_digest") if verified else None + ), + "canonical_target": ( + result.details.get("canonical_target") if verified else None + ), + "tool_count": result.details.get("tool_count") if verified else None, + "signature_verified": ( + result.details.get("signature_verified") is True if verified else False + ), + "collector_key_id": ( + result.details.get("collector_key_id") if verified else None + ), + "attestation_envelope_sha256": ( + result.details.get("attestation_envelope_sha256") if verified else None + ), + } + except (TypeError, ValueError): + pass projected = { "run_id": run_id, "tenant_id": tenant_id, @@ -162,6 +223,7 @@ def project_run(store: EvidenceStore, row: dict[str, Any]) -> dict[str, Any]: "environment_identity_digest": row["environment_identity_digest"], "policy_version": row["policy_version"] or row["rule_manifest_id"], "evidence_merkle_root": evidence_merkle_root, + "production_e2e": production_e2e, "created_at": row["created_at"], "updated_at": row["updated_at"], } diff --git a/src/echo_certification_forge/models.py b/src/echo_certification_forge/models.py index 546faa7..74602d6 100644 --- a/src/echo_certification_forge/models.py +++ b/src/echo_certification_forge/models.py @@ -182,6 +182,9 @@ class VerdictDecision: rule_manifest_id: str rule_manifest_digest: str evidence_merkle_root: str + production_e2e_attestation_id: str | None + production_e2e_profile: str | None + production_e2e_envelope_sha256: str | None signing_key_id: str issued_at: datetime expires_at: datetime @@ -199,6 +202,9 @@ def to_dict(self) -> dict[str, Any]: "rule_manifest_id": self.rule_manifest_id, "rule_manifest_digest": self.rule_manifest_digest, "evidence_merkle_root": self.evidence_merkle_root, + "production_e2e_attestation_id": self.production_e2e_attestation_id, + "production_e2e_profile": self.production_e2e_profile, + "production_e2e_envelope_sha256": self.production_e2e_envelope_sha256, "signing_key_id": self.signing_key_id, "issued_at": to_utc_iso(self.issued_at), "expires_at": to_utc_iso(self.expires_at), diff --git a/src/echo_certification_forge/production_e2e.py b/src/echo_certification_forge/production_e2e.py new file mode 100644 index 0000000..b35e4eb --- /dev/null +++ b/src/echo_certification_forge/production_e2e.py @@ -0,0 +1,351 @@ +"""Fail-closed validation for signed production end-to-end attestations. + +Source checks, a successful process exit, and a reachable URL are not production +evidence. This contract is evaluated by the deterministic verdict authority +after a trusted collector has signed the attestation. Target-controlled code +cannot manufacture the attestation or select its trust key. +""" +from __future__ import annotations + +import json +import re +from collections.abc import Mapping +from dataclasses import dataclass +from datetime import UTC, datetime +from pathlib import Path +from typing import Any + +from .canonical import sha256_json +from .models import EnvironmentIdentity, SignedVerdictEnvelope, TargetIdentity +from .signing import TrustedPublicKeyRegistry + +RULE_ID = "production_e2e" +SCHEMA_VERSION = "certforge.production-e2e.v1" +GENERIC_PROFILE = "generic-production-v1" +ECHO_GITHUB_AUTONOMY_PROFILE = "echo-github-autonomy-remote-mcp-v1" +ECHO_GITHUB_AUTONOMY_REPOSITORY = "echoomegaprime/echo-github-autonomy" +ECHO_GITHUB_AUTONOMY_CANONICAL_MCP = "https://echo-ghub.grok.me/api/plugin/mcp" + +BASE_CHECKS = frozenset( + { + "runtime_or_artifact_executed", + "exact_identity_readback", + "critical_journeys_complete", + "negative_controls_pass", + "stability_verified", + "external_acceptance_verified", + } +) +ECHO_GITHUB_AUTONOMY_CHECKS = BASE_CHECKS | frozenset( + { + "canonical_mcp_health", + "oauth_discovery", + "mcp_initialize", + "tool_schema", + "repeated_tool_invocation", + "registry_persistence", + "four_account_reconciliation", + "private_public_visibility", + "read_write_certify", + "cross_client_consistency", + } +) +ECHO_GITHUB_ACCOUNTS = { + "echoomegaprime": 314902331, + "ECHO-OMEGA-PRIME": 264607697, + "Bmcbob76": 203470412, + "bobmcwilliams4": 235318155, +} +ECHO_CLIENTS = frozenset({"chatgpt", "claude", "codex", "grok"}) + +_SHA = re.compile(r"^[0-9a-f]{40}(?:[0-9a-f]{24})?$") +_SHA256 = re.compile(r"^[0-9a-f]{64}$") +_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$") +_SIGNING_KEY_ID = re.compile(r"^ed25519:[0-9a-f]{32}$") +_SECRET_VALUE = re.compile( + r"(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|" + r"-----BEGIN [A-Z ]*PRIVATE KEY-----|Bearer\s+[A-Za-z0-9._~-]{16,})" +) +_RESTRICTED_KEYS = { + "password", + "passwd", + "secret", + "token", + "private_key", + "authorization", + "cookie", + "api_key", +} +_VERIFIED_MARKER = object() + + +@dataclass(frozen=True, slots=True) +class VerifiedProductionE2E: + """An attestation whose envelope was verified by an independently pinned key registry.""" + + payload: dict[str, Any] + collector_key_id: str + envelope_sha256: str + _marker: object + + def __post_init__(self) -> None: + if self._marker is not _VERIFIED_MARKER: + raise ValueError("VerifiedProductionE2E must come from signature verification") + + def result_details(self) -> dict[str, Any]: + return { + **self.payload, + "signature_verified": True, + "collector_key_id": self.collector_key_id, + "attestation_envelope_sha256": self.envelope_sha256, + } + + +def _parse_time(value: object) -> datetime | None: + if not isinstance(value, str): + return None + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + return None + return parsed.astimezone(UTC) if parsed.tzinfo is not None else None + + +def _contains_restricted_value(value: Any, *, key_name: str = "") -> bool: + normalized = key_name.casefold().replace("-", "_") + if normalized in _RESTRICTED_KEYS: + return True + if isinstance(value, Mapping): + return any( + _contains_restricted_value(item, key_name=str(key)) for key, item in value.items() + ) + if isinstance(value, (list, tuple, set)): + return any(_contains_restricted_value(item) for item in value) + return isinstance(value, str) and bool(_SECRET_VALUE.search(value)) + + +def _source_repository(target: TargetIdentity) -> str | None: + reference = target.canonical_ref.casefold() + marker = "github.com/" + if marker not in reference: + return None + suffix = reference.split(marker, 1)[1].split("@", 1)[0] + if suffix.endswith(".git"): + suffix = suffix[:-4] + return suffix + + +def _validate_common( + payload: Mapping[str, Any], + target: TargetIdentity, + environment: EnvironmentIdentity, + *, + now: datetime, +) -> str | None: + if _contains_restricted_value(payload): + return "production_e2e_contains_restricted_value" + if payload.get("schema_version") != SCHEMA_VERSION: + return "production_e2e_schema_invalid" + if not _IDENTIFIER.fullmatch(str(payload.get("attestation_id") or "")): + return "production_e2e_attestation_id_invalid" + if payload.get("target_identity_digest") != target.identity_digest: + return "production_e2e_target_identity_mismatch" + if payload.get("environment_identity_digest") != environment.identity_digest: + return "production_e2e_environment_identity_mismatch" + if not _SIGNING_KEY_ID.fullmatch(str(payload.get("signing_key_id") or "")): + return "production_e2e_signing_key_id_invalid" + + observed_at = _parse_time(payload.get("observed_at")) + expires_at = _parse_time(payload.get("expires_at")) + current = now.astimezone(UTC) if now.tzinfo is not None else None + if current is None or observed_at is None or expires_at is None: + return "production_e2e_time_invalid" + if expires_at <= observed_at or (expires_at - observed_at).total_seconds() > 3600: + return "production_e2e_lifetime_invalid" + if observed_at > current or current >= expires_at: + return "production_e2e_attestation_not_current" + + source_commit = payload.get("source_commit") + deployment_sha = payload.get("deployment_sha") + if target.source_commit is not None: + expected = target.source_commit.casefold() + if not _SHA.fullmatch(expected): + return "production_e2e_target_source_commit_not_exact" + if source_commit != expected or deployment_sha != expected: + return "production_e2e_deployment_sha_mismatch" + elif source_commit is not None or deployment_sha is not None: + if source_commit != deployment_sha or not _SHA.fullmatch(str(source_commit)): + return "production_e2e_deployment_sha_invalid" + + required_checks = payload.get("required_checks") + checks = payload.get("checks") + if not isinstance(required_checks, list) or not isinstance(checks, Mapping): + return "production_e2e_checks_invalid" + if len(required_checks) != len(set(required_checks)): + return "production_e2e_checks_duplicated" + check_set = frozenset(required_checks) + if set(checks) != set(check_set) or any(checks.get(name) is not True for name in check_set): + return "production_e2e_checks_incomplete" + if not BASE_CHECKS <= check_set: + return "production_e2e_baseline_checks_missing" + if not isinstance(payload.get("stability_probe_count"), int) or payload["stability_probe_count"] < 3: + return "production_e2e_stability_probe_count_insufficient" + return None + + +def _validate_echo_github_autonomy(payload: Mapping[str, Any]) -> str | None: + if payload.get("canonical_target") != ECHO_GITHUB_AUTONOMY_CANONICAL_MCP: + return "production_e2e_canonical_mcp_mismatch" + if frozenset(payload.get("required_checks") or ()) != ECHO_GITHUB_AUTONOMY_CHECKS: + return "production_e2e_github_autonomy_checks_incomplete" + if payload.get("tool_count") != 30: + return "production_e2e_tool_count_mismatch" + + repositories = payload.get("sample_private_repositories") + if not isinstance(repositories, Mapping) or set(repositories) != set(ECHO_GITHUB_ACCOUNTS): + return "production_e2e_sample_repositories_incomplete" + sample_digests: dict[str, str] = {} + for login, sample in repositories.items(): + if not isinstance(sample, Mapping): + return "production_e2e_sample_repository_invalid" + if ( + not isinstance(sample.get("repository_id"), int) + or isinstance(sample["repository_id"], bool) + or sample["repository_id"] <= 0 + ): + return "production_e2e_sample_repository_id_invalid" + if not str(sample.get("node_id") or "").strip(): + return "production_e2e_sample_node_id_invalid" + if not str(sample.get("default_branch") or "").strip(): + return "production_e2e_sample_default_branch_invalid" + if not _SHA.fullmatch(str(sample.get("head_sha") or "")): + return "production_e2e_sample_head_sha_invalid" + digest = str(sample.get("fingerprint_sha256") or "") + if not _SHA256.fullmatch(digest): + return "production_e2e_sample_fingerprint_invalid" + sample_digests[login] = digest + + accounts = payload.get("accounts") + if not isinstance(accounts, Mapping) or set(accounts) != set(ECHO_GITHUB_ACCOUNTS): + return "production_e2e_accounts_incomplete" + for login, account_id in ECHO_GITHUB_ACCOUNTS.items(): + account = accounts.get(login) + if not isinstance(account, Mapping) or account.get("account_id") != account_id: + return "production_e2e_account_identity_mismatch" + counts = ( + account.get("enumerated_count"), + account.get("upstream_total_count"), + account.get("public_count"), + account.get("private_count"), + ) + if any(not isinstance(value, int) or isinstance(value, bool) or value < 0 for value in counts): + return "production_e2e_account_counts_invalid" + if counts[0] != counts[1] or counts[2] <= 0 or counts[3] <= 0: + return "production_e2e_account_reconciliation_failed" + if any(account.get(name) is not True for name in ("read", "write", "certify")): + return "production_e2e_account_authority_incomplete" + if account.get("credential_source") not in { + "github_app_installation", + "vault_user_token_fallback", + }: + return "production_e2e_credential_source_invalid" + if account.get("secret_exposed") is not False: + return "production_e2e_secret_boundary_failed" + + clients = payload.get("clients") + if not isinstance(clients, Mapping) or set(clients) != set(ECHO_CLIENTS): + return "production_e2e_clients_incomplete" + for client in clients.values(): + if not isinstance(client, Mapping) or client.get("accepted") is not True: + return "production_e2e_client_not_accepted" + fingerprints = client.get("repository_fingerprints") + if not isinstance(fingerprints, Mapping) or dict(fingerprints) != sample_digests: + return "production_e2e_cross_client_repository_mismatch" + return None + + +def validate_production_e2e( + payload: Mapping[str, Any] | None, + target: TargetIdentity, + environment: EnvironmentIdentity, + *, + now: datetime, +) -> tuple[bool, str]: + """Validate one trusted-collector payload against the exact run identities.""" + + if not isinstance(payload, Mapping): + return False, "production_e2e_attestation_missing" + error = _validate_common(payload, target, environment, now=now) + if error is not None: + return False, error + repository = _source_repository(target) + profile = payload.get("profile") + if repository == ECHO_GITHUB_AUTONOMY_REPOSITORY: + if profile != ECHO_GITHUB_AUTONOMY_PROFILE: + return False, "production_e2e_profile_mismatch" + error = _validate_echo_github_autonomy(payload) + return (error is None, error or "production_e2e_verified") + if profile != GENERIC_PROFILE: + return False, "production_e2e_profile_mismatch" + if frozenset(payload.get("required_checks") or ()) != BASE_CHECKS: + return False, "production_e2e_generic_checks_incomplete" + return True, "production_e2e_verified" + + +def validate_attestation_trust_metadata(payload: Mapping[str, Any]) -> tuple[bool, str]: + """Require the signature-verification metadata added by ``VerifiedProductionE2E``.""" + + if payload.get("signature_verified") is not True: + return False, "production_e2e_signature_not_verified" + if payload.get("collector_key_id") != payload.get("signing_key_id"): + return False, "production_e2e_collector_key_mismatch" + if not _SIGNING_KEY_ID.fullmatch(str(payload.get("collector_key_id") or "")): + return False, "production_e2e_collector_key_invalid" + if not _SHA256.fullmatch(str(payload.get("attestation_envelope_sha256") or "")): + return False, "production_e2e_envelope_sha256_invalid" + return True, "production_e2e_signature_verified" + + +def verify_signed_attestation( + envelope: SignedVerdictEnvelope, + trusted_keys: TrustedPublicKeyRegistry, +) -> VerifiedProductionE2E: + """Verify one collector envelope against an independently selected key registry.""" + + verified, reason = trusted_keys.verify(envelope) + if not verified: + raise ValueError(f"production E2E attestation is not trusted: {reason}") + if envelope.payload.get("schema_version") != SCHEMA_VERSION: + raise ValueError("production E2E attestation schema is unsupported") + if envelope.payload.get("signing_key_id") != envelope.key_id: + raise ValueError("production E2E attestation key binding is invalid") + envelope_sha256 = sha256_json( + { + "payload": envelope.payload, + "signature_b64": envelope.signature_b64, + "key_id": envelope.key_id, + } + ) + return VerifiedProductionE2E( + payload=dict(envelope.payload), + collector_key_id=envelope.key_id, + envelope_sha256=envelope_sha256, + _marker=_VERIFIED_MARKER, + ) + + +def load_signed_attestation(path: Path, trusted_key_directory: Path) -> VerifiedProductionE2E: + """Load and independently verify a collector envelope using pinned public keys.""" + + try: + document = json.loads(path.read_text(encoding="utf-8")) + envelope = SignedVerdictEnvelope( + payload=dict(document["payload"]), + signature_b64=str(document["signature_b64"]), + key_id=str(document["key_id"]), + public_key_pem=str(document.get("public_key_pem") or ""), + ) + except (OSError, ValueError, TypeError, KeyError, json.JSONDecodeError) as exc: + raise ValueError("production E2E attestation envelope is invalid") from exc + registry = TrustedPublicKeyRegistry.from_directory(trusted_key_directory) + return verify_signed_attestation(envelope, registry) diff --git a/src/echo_certification_forge/run_worker.py b/src/echo_certification_forge/run_worker.py index 3774b5d..e2fc1a6 100644 --- a/src/echo_certification_forge/run_worker.py +++ b/src/echo_certification_forge/run_worker.py @@ -17,6 +17,7 @@ import sqlite3 import tempfile import threading +from collections.abc import Callable from pathlib import Path from cryptography.hazmat.primitives import serialization @@ -38,6 +39,7 @@ from .executor import RetentionPolicy, RunExecutor, StaticEntitlement from .models import EnvironmentIdentity, RunOutcome, RunState, TargetIdentity from .policy import RuleManifest +from .production_e2e import VerifiedProductionE2E, load_signed_attestation from .runner import RunnerResponse from .sandbox import DEFAULT_IMAGE, DockerSandbox, sandboxed_journey_runner from .signing import Ed25519VerdictSigner @@ -47,7 +49,7 @@ _ADAPTER_RULE = "adapter_identity_and_quality" _PRODUCTION_MANIFEST_ID = "certforge.release-strict.v2" _PRODUCTION_MANIFEST_SHA256 = ( - "7dc98e0e95e6dd2c000ec069a8c46c4d1d49a4fe869ad4eae25e059d103644f4" + "08ba068ceb3e14bfed2690337edbb94c546e3e0a1a89b1321f7657653d8eea43" ) class _ClaimHeartbeat: def __init__(self, governance: SubscriberGovernance, claim) -> None: @@ -160,6 +162,10 @@ def run( adapter_records: tuple[AdapterExecutionRecord, ...] | None = None, adapter_policy: AdapterAcceptancePolicy | None = None, adapter_bundle_response: RunnerResponse | None = None, + production_e2e_attestation: VerifiedProductionE2E | None = None, + production_e2e_provider: ( + Callable[[TargetIdentity, EnvironmentIdentity], VerifiedProductionE2E] | None + ) = None, worker_id: str | None = None, worker_attestation_sha256: str | None = None, execution_location: str = "local", @@ -348,6 +354,8 @@ def run( else None ) environment = _worker_environment(adapter_digest, adapter_execution_profile_sha256) + if production_e2e_attestation is None and production_e2e_provider is not None: + production_e2e_attestation = production_e2e_provider(target, environment) if subscribers is not None: if existing is None or existing["state"] != RunState.QUEUED.value: if claim is not None: @@ -530,6 +538,7 @@ def complete_subscriber_execution(envelope): }, adapter_records=adapter_records, adapter_policy=adapter_policy, + production_e2e_attestation=production_e2e_attestation, execution_guard=execution_guard, completion_callback=completion_callback, ) @@ -669,6 +678,26 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--tenant", required=True) parser.add_argument("--target-json", required=True, help='e.g. {"type":"local","path":"/abs/dir"}') parser.add_argument("--journey-json", default=None, help='e.g. ["python3","hello.py"]') + parser.add_argument( + "--production-e2e-attestation", + type=Path, + default=( + Path(os.environ["ECHO_CERTFORGE_PRODUCTION_E2E_ATTESTATION"]) + if os.environ.get("ECHO_CERTFORGE_PRODUCTION_E2E_ATTESTATION") + else None + ), + help="trusted-collector signed production E2E envelope", + ) + parser.add_argument( + "--trusted-production-e2e-keys", + type=Path, + default=( + Path(os.environ["ECHO_CERTFORGE_TRUSTED_PRODUCTION_E2E_KEYS"]) + if os.environ.get("ECHO_CERTFORGE_TRUSTED_PRODUCTION_E2E_KEYS") + else None + ), + help="directory of pinned Ed25519 collector public keys", + ) parser.add_argument("--policy-version", default=None) parser.add_argument( "--db", @@ -875,6 +904,19 @@ def main(argv: list[str] | None = None) -> int: ) target_spec = json.loads(args.target_json) journey = json.loads(args.journey_json) if args.journey_json else None + production_e2e_attestation = None + if args.production_e2e_attestation is not None: + if args.trusted_production_e2e_keys is None: + print(json.dumps({"error": "trusted_production_e2e_keys_missing"})) + return 2 + try: + production_e2e_attestation = load_signed_attestation( + args.production_e2e_attestation, + args.trusted_production_e2e_keys, + ) + except ValueError as exc: + print(json.dumps({"error": "production_e2e_attestation_invalid", "detail": str(exc)})) + return 2 sandbox = None if args.sandbox: sandbox = DockerSandbox( @@ -896,6 +938,7 @@ def main(argv: list[str] | None = None) -> int: adapter_records=adapter_records, adapter_policy=adapter_policy, adapter_bundle_response=adapter_rebound_response, + production_e2e_attestation=production_e2e_attestation, worker_id=args.worker_id, worker_attestation_sha256=args.worker_attestation_sha256, execution_location=args.execution_location, diff --git a/src/echo_certification_forge/verdict.py b/src/echo_certification_forge/verdict.py index c739357..457751c 100644 --- a/src/echo_certification_forge/verdict.py +++ b/src/echo_certification_forge/verdict.py @@ -9,6 +9,9 @@ from .evidence import EvidenceStore from .models import EnvironmentIdentity, ReleaseVerdict, RunOutcome, TargetIdentity, VerdictDecision from .policy import RuleManifest +from .production_e2e import RULE_ID as PRODUCTION_E2E_RULE_ID +from .production_e2e import validate_attestation_trust_metadata +from .production_e2e import validate_production_e2e class DeterministicVerdictEngine: @@ -34,6 +37,8 @@ def evaluate( # ({tenant_id, target_type, declared_identity_digest, reference}), not the full canonical # TargetIdentity. Such a run has not been reconciled to an acquired artifact, so it cannot be # certified: fail-closed with an explicit reason instead of crashing on TargetIdentity(**...). + target: TargetIdentity | None = None + environment: EnvironmentIdentity | None = None if "declared_identity_digest" in target_data or "artifact_sha256" not in target_data: reasons.append("target_identity_not_reconciled") else: @@ -72,6 +77,35 @@ def evaluate( if invalid_references: reasons.append(f"invalid_rule_evidence:{rule.id}") + # PRODUCTION_READY is impossible without a current, signed, exact-identity E2E + # attestation. This is an engine invariant, not an optional policy convention; + # legacy/custom manifests that omit the rule fail closed as well. + if not any(rule.id == PRODUCTION_E2E_RULE_ID for rule in manifest.rules): + reasons.append("production_e2e_rule_missing_from_policy") + e2e_result = results.get(PRODUCTION_E2E_RULE_ID) + verified_e2e: dict[str, Any] | None = None + if target is None or environment is None: + reasons.append("production_e2e_identity_unavailable") + elif e2e_result is None: + reasons.append("production_e2e_attestation_missing") + else: + e2e_valid, e2e_reason = validate_production_e2e( + e2e_result.details, + target, + environment, + now=utc_now(), + ) + if not e2e_valid: + reasons.append(e2e_reason) + else: + trust_valid, trust_reason = validate_attestation_trust_metadata( + e2e_result.details + ) + if not trust_valid: + reasons.append(trust_reason) + else: + verified_e2e = e2e_result.details + if store.blocking_findings(run_id, tenant_id): reasons.append("blocking_findings_present") @@ -97,6 +131,17 @@ def evaluate( rule_manifest_id=manifest.manifest_id, rule_manifest_digest=manifest.digest, evidence_merkle_root=verification.merkle_root, + production_e2e_attestation_id=( + str(verified_e2e["attestation_id"]) if verified_e2e is not None else None + ), + production_e2e_profile=( + str(verified_e2e["profile"]) if verified_e2e is not None else None + ), + production_e2e_envelope_sha256=( + str(verified_e2e["attestation_envelope_sha256"]) + if verified_e2e is not None + else None + ), signing_key_id=signing_key_id, issued_at=issued_at, expires_at=issued_at + timedelta(seconds=manifest.verdict_ttl_seconds), diff --git a/tests/conftest.py b/tests/conftest.py index f674a9a..138dac5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,12 +26,13 @@ def store(tmp_path: Path) -> EvidenceStore: @pytest.fixture def target() -> TargetIdentity: + source_commit = "a" * 40 return TargetIdentity( tenant_id="tenant-alpha", target_type="git", - canonical_ref="https://github.com/example/project@abc123", + canonical_ref=f"https://github.com/example/project@{source_commit}", artifact_sha256=digest("artifact"), - source_commit="abc123", + source_commit=source_commit, dependency_sha256=digest("dependencies"), configuration_sha256=digest("configuration"), ) diff --git a/tests/production_e2e_support.py b/tests/production_e2e_support.py new file mode 100644 index 0000000..8c3d2c7 --- /dev/null +++ b/tests/production_e2e_support.py @@ -0,0 +1,46 @@ +"""Trusted production-E2E attestations used by tests that exercise ready paths.""" + +from __future__ import annotations + +from datetime import timedelta + +from echo_certification_forge.canonical import to_utc_iso, utc_now +from echo_certification_forge.models import EnvironmentIdentity, TargetIdentity +from echo_certification_forge.production_e2e import ( + BASE_CHECKS, + GENERIC_PROFILE, + SCHEMA_VERSION, + VerifiedProductionE2E, + verify_signed_attestation, +) +from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry + + +def trusted_generic_production_e2e( + target: TargetIdentity, + environment: EnvironmentIdentity, +) -> VerifiedProductionE2E: + """Create a fresh signed generic attestation through the real trust verifier.""" + + observed_at = utc_now() + signer = Ed25519VerdictSigner.generate() + payload = { + "schema_version": SCHEMA_VERSION, + "attestation_id": f"test-e2e-{target.identity_digest[:16]}", + "profile": GENERIC_PROFILE, + "target_identity_digest": target.identity_digest, + "environment_identity_digest": environment.identity_digest, + "source_commit": target.source_commit, + "deployment_sha": target.source_commit, + "canonical_target": target.canonical_ref, + "required_checks": sorted(BASE_CHECKS), + "checks": {name: True for name in sorted(BASE_CHECKS)}, + "stability_probe_count": 3, + "observed_at": to_utc_iso(observed_at), + "expires_at": to_utc_iso(observed_at + timedelta(minutes=30)), + "signing_key_id": signer.key_id, + } + envelope = signer.sign_payload(payload) + trusted = TrustedPublicKeyRegistry.empty() + trusted.add_pem(signer.public_key_pem) + return verify_signed_attestation(envelope, trusted) diff --git a/tests/test_additional_contracts.py b/tests/test_additional_contracts.py index 7687852..362b3f7 100644 --- a/tests/test_additional_contracts.py +++ b/tests/test_additional_contracts.py @@ -22,10 +22,12 @@ from echo_certification_forge.service import ServiceContext, create_app from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry from echo_certification_forge.verdict import DeterministicVerdictEngine +from production_e2e_support import trusted_generic_production_e2e def complete_run(store, manifest, target, environment, run_id="cert-extra"): store.register_run(run_id, target, environment, manifest.manifest_id, manifest.digest) + production_e2e = trusted_generic_production_e2e(target, environment).result_details() for index, rule in enumerate(manifest.rules, start=1): artifact_id = f"extra-{index:02d}" store.append_artifact( @@ -34,7 +36,12 @@ def complete_run(store, manifest, target, environment, run_id="cert-extra"): ) store.record_rule_result( run_id, target.tenant_id, - RuleResult(rule.id, True, (artifact_id,), {"verified": True}), + RuleResult( + rule.id, + True, + (artifact_id,), + production_e2e if rule.id == "production_e2e" else {"verified": True}, + ), ) store.set_run_outcome(run_id, target.tenant_id, RunOutcome.COMPLETE) return run_id diff --git a/tests/test_evidence_and_verdict.py b/tests/test_evidence_and_verdict.py index 716f488..2265075 100644 --- a/tests/test_evidence_and_verdict.py +++ b/tests/test_evidence_and_verdict.py @@ -3,6 +3,7 @@ import json import sqlite3 from contextlib import closing +from datetime import timedelta from pathlib import Path import pytest @@ -13,10 +14,13 @@ ReleaseVerdict, RuleResult, RunOutcome, + EnvironmentIdentity, TargetIdentity, VerdictLifecycleEvent, ) from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry +from echo_certification_forge.canonical import to_utc_iso, utc_now +from echo_certification_forge.production_e2e import BASE_CHECKS, GENERIC_PROFILE, SCHEMA_VERSION from echo_certification_forge.verdict import DeterministicVerdictEngine @@ -25,8 +29,33 @@ def register(store, manifest, target, environment, run_id="cert-001"): return run_id -def satisfy_all_rules(store, manifest, run_id, tenant_id): +def satisfy_all_rules(store, manifest, run_id, tenant_id, *, include_production_e2e=True): + run = store.get_run(run_id, tenant_id) + target = TargetIdentity(**json.loads(run["target_identity_json"])) + environment = EnvironmentIdentity(**json.loads(run["environment_identity_json"])) + observed_at = utc_now() + production_e2e = { + "schema_version": SCHEMA_VERSION, + "attestation_id": f"e2e-{run_id}", + "profile": GENERIC_PROFILE, + "target_identity_digest": target.identity_digest, + "environment_identity_digest": environment.identity_digest, + "source_commit": target.source_commit, + "deployment_sha": target.source_commit, + "canonical_target": "https://example.test/runtime", + "required_checks": sorted(BASE_CHECKS), + "checks": {name: True for name in sorted(BASE_CHECKS)}, + "stability_probe_count": 3, + "observed_at": to_utc_iso(observed_at), + "expires_at": to_utc_iso(observed_at + timedelta(minutes=30)), + "signing_key_id": "ed25519:" + "a" * 32, + "signature_verified": True, + "collector_key_id": "ed25519:" + "a" * 32, + "attestation_envelope_sha256": "f" * 64, + } for index, rule in enumerate(manifest.rules, start=1): + if rule.id == "production_e2e" and not include_production_e2e: + continue artifact_id = f"evidence-{index:02d}" store.append_artifact( run_id, @@ -40,7 +69,12 @@ def satisfy_all_rules(store, manifest, run_id, tenant_id): store.record_rule_result( run_id, tenant_id, - RuleResult(rule.id, True, (artifact_id,), {"source": "acceptance"}), + RuleResult( + rule.id, + True, + (artifact_id,), + production_e2e if rule.id == "production_e2e" else {"source": "acceptance"}, + ), ) @@ -81,6 +115,26 @@ def test_complete_run_with_verified_evidence_gets_signed_ready(store, manifest, assert gate.allowed +def test_source_and_rule_rows_without_production_e2e_never_get_ready( + store, manifest, target, environment +): + run_id = register(store, manifest, target, environment) + satisfy_all_rules( + store, + manifest, + run_id, + target.tenant_id, + include_production_e2e=False, + ) + store.set_run_outcome(run_id, target.tenant_id, RunOutcome.COMPLETE) + signer = Ed25519VerdictSigner.generate() + decision = DeterministicVerdictEngine().evaluate( + store, run_id, target.tenant_id, manifest, signer.key_id + ) + assert decision.release_verdict is ReleaseVerdict.NOT_READY + assert "production_e2e_attestation_missing" in decision.reasons + + def test_bare_database_rows_cannot_manufacture_ready(store, manifest, target, environment): run_id = register(store, manifest, target, environment) now = "2026-07-16T00:00:00Z" diff --git a/tests/test_p5_executor_adapter_gate.py b/tests/test_p5_executor_adapter_gate.py index 9f1b9fd..8607e44 100644 --- a/tests/test_p5_executor_adapter_gate.py +++ b/tests/test_p5_executor_adapter_gate.py @@ -17,6 +17,7 @@ from echo_certification_forge.models import EnvironmentIdentity from echo_certification_forge.policy import RuleManifest from echo_certification_forge.signing import Ed25519VerdictSigner +from production_e2e_support import trusted_generic_production_e2e RUN = "cert-p5-executor" @@ -89,7 +90,8 @@ def manifest_v2() -> RuleManifest: def execute(store, target, tmp_path: Path, *, records, adapter_policy, adapter_digest: str): manifest = manifest_v2() - store.register_run(RUN, target, environment(adapter_digest), manifest.manifest_id, manifest.digest) + run_environment = environment(adapter_digest) + store.register_run(RUN, target, run_environment, manifest.manifest_id, manifest.digest) executor = RunExecutor(store, manifest, Ed25519VerdictSigner.generate()) return executor.execute( RUN, @@ -103,6 +105,7 @@ def execute(store, target, tmp_path: Path, *, records, adapter_policy, adapter_d }, adapter_records=records, adapter_policy=adapter_policy, + production_e2e_attestation=trusted_generic_production_e2e(target, run_environment), ) diff --git a/tests/test_p6_deployment_enforcement.py b/tests/test_p6_deployment_enforcement.py index 5c4496a..937915b 100644 --- a/tests/test_p6_deployment_enforcement.py +++ b/tests/test_p6_deployment_enforcement.py @@ -41,6 +41,7 @@ VerdictLifecycleEvent, ) from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry +from production_e2e_support import trusted_generic_production_e2e TENANT = "tenant-alpha" OTHER_TENANT = "tenant-beta" @@ -57,7 +58,7 @@ def _target(artifact_label: str, tenant_id: str = TENANT) -> TargetIdentity: target_type="container", canonical_ref=f"registry.echo/app@{artifact_label}", artifact_sha256=_digest(artifact_label), - source_commit="abc123def456", + source_commit="abc123abc123abc123abc123abc123abc123abcd", dependency_sha256=_digest("dependencies"), configuration_sha256=_digest("configuration"), ) @@ -107,6 +108,7 @@ def _certify( entitlement=StaticEntitlement(frozenset({target.tenant_id})), journey=[sys.executable, "hello.py"], control_attestations={"runner_control_channel": True, "signing_authority_separation": True}, + production_e2e_attestation=trusted_generic_production_e2e(target, environment), ) assert result.release_verdict == "PRODUCTION_READY", result.blocking_findings @@ -1120,4 +1122,4 @@ def test_delayed_stale_binding_cannot_shadow_active_certification( _admission(target.artifact_sha256, "staging", environment, rolled_manifest, "deploy-shadow-s2"), ACTOR, ) - assert stale_decision.run_id == "cert-shadow-new" \ No newline at end of file + assert stale_decision.run_id == "cert-shadow-new" diff --git a/tests/test_p6_platform_integration.py b/tests/test_p6_platform_integration.py index e5406f3..267bd0a 100644 --- a/tests/test_p6_platform_integration.py +++ b/tests/test_p6_platform_integration.py @@ -51,12 +51,14 @@ from echo_certification_forge.service import ServiceContext, create_app from echo_certification_forge.sandbox import DockerSandbox from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry +from production_e2e_support import trusted_generic_production_e2e TENANT = "tenant-alpha" OTHER_TENANT = "tenant-beta" SECRET = "p6-webhook-secret-0123456789abcdef" DEPLOY_SECRET = "p6-deploy-credential-alpha-0001" OTHER_DEPLOY_SECRET = "p6-deploy-credential-beta-0002" +SOURCE_COMMIT = "abc123abc123abc123abc123abc123abc123abcd" def _digest(label: str) -> str: @@ -101,6 +103,7 @@ def _certify(store, manifest, signer, environment, tmp_path, run_id, target) -> entitlement=StaticEntitlement(frozenset({target.tenant_id})), journey=[sys.executable, "hello.py"], control_attestations={"runner_control_channel": True, "signing_authority_separation": True}, + production_e2e_attestation=trusted_generic_production_e2e(target, environment), ) assert result.release_verdict == "PRODUCTION_READY", result.blocking_findings @@ -112,7 +115,7 @@ def certified_target(store, manifest, signer, environment, tmp_path) -> TargetId target_type="container", canonical_ref="registry.echo/app@v1", artifact_sha256=_digest("http-app-v1"), - source_commit="abc123def456", + source_commit=SOURCE_COMMIT, dependency_sha256=_digest("dependencies"), configuration_sha256=_digest("configuration"), ) @@ -401,7 +404,7 @@ def test_http_binding_refuses_unsigned_run_and_foreign_tenant( target_type="container", canonical_ref="registry.echo/app@u1", artifact_sha256=_digest("unsigned-app"), - source_commit="abc123def456", + source_commit=SOURCE_COMMIT, ) store.register_run("cert-unsigned", target, environment, manifest.manifest_id, manifest.digest) response = _post_signed(client, "/v1/certifications/cert-unsigned/bindings") @@ -442,7 +445,7 @@ def _build_event(event_id: str = "evt-00000001") -> dict: "event_type": "build.artifact.published", "tenant_id": TENANT, "artifact_sha256": _digest("built-artifact"), - "source_commit": "abc123def456", + "source_commit": SOURCE_COMMIT, "repository": "https://github.com/echo/app", "environment_identity_digest": _digest("build-env"), "policy_version": "certforge.release-strict.v1", @@ -488,7 +491,7 @@ def test_registry_webhook_maps_container_target(client): "tenant_id": TENANT, "image_digest": f"sha256:{_digest('pushed-image')}", "image_repository": "registry.echo/app", - "source_commit": "abc123def456", + "source_commit": SOURCE_COMMIT, "environment_identity_digest": _digest("registry-env"), "policy_version": "certforge.release-strict.v1", } @@ -557,7 +560,7 @@ def _declared_event(artifact: str, environment, manifest, event_id: str = "evt-r "event_type": "build.artifact.published", "tenant_id": TENANT, "artifact_sha256": artifact, - "source_commit": "abc123def456", + "source_commit": SOURCE_COMMIT, "repository": "https://github.com/echo/app", "environment_identity_digest": environment.identity_digest, "policy_version": manifest.manifest_id, @@ -582,9 +585,9 @@ def test_webhook_declared_run_reconciles_certifies_and_deploys_end_to_end( acquired = TargetIdentity( tenant_id=TENANT, target_type="container", - canonical_ref="registry.echo/app@abc123def456", + canonical_ref=f"registry.echo/app@{SOURCE_COMMIT}", artifact_sha256=artifact, - source_commit="abc123def456", + source_commit=SOURCE_COMMIT, ) store.reconcile_declared_target(run_id, TENANT, acquired, environment) row = store.get_run(run_id, TENANT) @@ -602,6 +605,7 @@ def test_webhook_declared_run_reconciles_certifies_and_deploys_end_to_end( entitlement=StaticEntitlement(frozenset({TENANT})), journey=[sys.executable, "hello.py"], control_attestations={"runner_control_channel": True, "signing_authority_separation": True}, + production_e2e_attestation=trusted_generic_production_e2e(acquired, environment), ) assert result.release_verdict == "PRODUCTION_READY", result.blocking_findings @@ -646,9 +650,9 @@ def test_reconciliation_refuses_mismatched_artifact_and_run_stays_undeployable( tampered = TargetIdentity( tenant_id=TENANT, target_type="container", - canonical_ref="registry.echo/app@abc123def456", + canonical_ref=f"registry.echo/app@{SOURCE_COMMIT}", artifact_sha256=_digest("DIFFERENT-acquired-artifact"), - source_commit="abc123def456", + source_commit=SOURCE_COMMIT, ) with pytest.raises(ValueError, match="target_declared_artifact_mismatch"): store.reconcile_declared_target(run_id, TENANT, tampered, environment) @@ -732,9 +736,9 @@ def test_reconciliation_window_closes_at_acquisition(client, store, manifest, en acquired = TargetIdentity( tenant_id=TENANT, target_type="container", - canonical_ref="registry.echo/app@abc123def456", + canonical_ref=f"registry.echo/app@{SOURCE_COMMIT}", artifact_sha256=artifact, - source_commit="abc123def456", + source_commit=SOURCE_COMMIT, ) with pytest.raises(ValueError, match="target_reconciliation_window_closed"): store.reconcile_declared_target(run_id, TENANT, acquired, environment) @@ -772,6 +776,7 @@ def test_run_worker_executes_webhook_declared_run_end_to_end( signer=signer, entitled=frozenset({TENANT}), journey=[sys.executable, "hello.py"], + production_e2e_provider=trusted_generic_production_e2e, ) assert outcome.get("error") is None, outcome assert outcome["release_verdict"] == "PRODUCTION_READY" @@ -1837,7 +1842,7 @@ def test_registry_webhook_oci_run_certifies_and_deploys_end_to_end( "tenant_id": TENANT, "image_digest": manifest_digest, "image_repository": repo, - "source_commit": "abc123def456", + "source_commit": SOURCE_COMMIT, # the platform declares the WORKER's environment commitment for the run "environment_identity_digest": run_worker._worker_environment().identity_digest, "policy_version": manifest.manifest_id, @@ -1862,6 +1867,7 @@ def test_registry_webhook_oci_run_certifies_and_deploys_end_to_end( entitled=frozenset({TENANT}), journey=["python3", "app/hello.py"], sandbox=DockerSandbox(docker=(sys.executable, str(stub))), + production_e2e_provider=trusted_generic_production_e2e, ) finally: os.environ.pop("CERTFORGE_TEST_DOCKER_LOG", None) @@ -2025,7 +2031,7 @@ def test_registry_webhook_oci_journey_without_sandbox_fails_closed( "tenant_id": TENANT, "image_digest": manifest_digest, "image_repository": repo, - "source_commit": "abc123def456", + "source_commit": SOURCE_COMMIT, "environment_identity_digest": run_worker._worker_environment().identity_digest, "policy_version": manifest.manifest_id, } diff --git a/tests/test_p7_subscriber_governance.py b/tests/test_p7_subscriber_governance.py index ce9ba38..a112bba 100644 --- a/tests/test_p7_subscriber_governance.py +++ b/tests/test_p7_subscriber_governance.py @@ -40,6 +40,7 @@ SubscriberGovernance, SubscriberPolicy, ) +from production_e2e_support import trusted_generic_production_e2e PEPPER = b"p7-acceptance-pepper-material-32-bytes-minimum" @@ -1166,6 +1167,7 @@ def test_run_worker_enforces_live_subscriber_entitlement( entitled=frozenset({org.organization_id}), subscribers=governance, journey=[sys.executable, "journey.py"], + production_e2e_provider=trusted_generic_production_e2e, ) assert result["error"] == "subscriber_worker_claim_denied" @@ -3578,6 +3580,7 @@ def test_legal_hold_lifecycle_and_public_verification_use_hardened_subscriber_au entitled=frozenset({owner.organization_id}), subscribers=governance, journey=[sys.executable, "journey.py"], + production_e2e_provider=trusted_generic_production_e2e, ) assert result["state"] == RunState.COMPLETED.value headers = _headers(owner.organization_id, owner.bootstrap_api_key) diff --git a/tests/test_production_e2e.py b/tests/test_production_e2e.py new file mode 100644 index 0000000..81460f5 --- /dev/null +++ b/tests/test_production_e2e.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +import json +from datetime import UTC, datetime, timedelta + +import pytest + +from echo_certification_forge.models import EnvironmentIdentity, TargetIdentity +from echo_certification_forge.production_e2e import ( + ECHO_CLIENTS, + ECHO_GITHUB_ACCOUNTS, + ECHO_GITHUB_AUTONOMY_CANONICAL_MCP, + ECHO_GITHUB_AUTONOMY_CHECKS, + ECHO_GITHUB_AUTONOMY_PROFILE, + SCHEMA_VERSION, + load_signed_attestation, + validate_production_e2e, +) +from echo_certification_forge.signing import Ed25519VerdictSigner + +NOW = datetime(2026, 8, 30, 12, 0, tzinfo=UTC) +SOURCE_SHA = "5" * 40 + + +def _target() -> TargetIdentity: + return TargetIdentity( + tenant_id="echo-github-apps", + target_type="git", + canonical_ref=( + "https://github.com/echoomegaprime/echo-github-autonomy.git@" + SOURCE_SHA + ), + artifact_sha256="a" * 64, + source_commit=SOURCE_SHA, + dependency_sha256="b" * 64, + configuration_sha256="c" * 64, + ) + + +def _environment() -> EnvironmentIdentity: + return EnvironmentIdentity( + **{ + name: str(index) * 64 + for index, name in enumerate(EnvironmentIdentity.__dataclass_fields__, start=1) + } + ) + + +def _payload(*, signing_key_id: str = "ed25519:" + "d" * 32) -> dict: + target = _target() + environment = _environment() + samples = { + login: { + "repository_id": index, + "node_id": f"R_repo_{index}", + "default_branch": "main", + "head_sha": str(index) * 40, + "fingerprint_sha256": str(index) * 64, + } + for index, login in enumerate(ECHO_GITHUB_ACCOUNTS, start=1) + } + accounts = { + login: { + "account_id": account_id, + "enumerated_count": 10 + index, + "upstream_total_count": 10 + index, + "public_count": 5, + "private_count": 5 + index, + "read": True, + "write": True, + "certify": True, + "credential_source": ( + "github_app_installation" + if index == 1 + else "vault_user_token_fallback" + ), + "secret_exposed": False, + } + for index, (login, account_id) in enumerate(ECHO_GITHUB_ACCOUNTS.items(), start=1) + } + fingerprints = { + login: sample["fingerprint_sha256"] for login, sample in samples.items() + } + return { + "schema_version": SCHEMA_VERSION, + "attestation_id": "echo-github-autonomy-live-001", + "profile": ECHO_GITHUB_AUTONOMY_PROFILE, + "target_identity_digest": target.identity_digest, + "environment_identity_digest": environment.identity_digest, + "source_commit": SOURCE_SHA, + "deployment_sha": SOURCE_SHA, + "canonical_target": ECHO_GITHUB_AUTONOMY_CANONICAL_MCP, + "required_checks": sorted(ECHO_GITHUB_AUTONOMY_CHECKS), + "checks": {name: True for name in sorted(ECHO_GITHUB_AUTONOMY_CHECKS)}, + "stability_probe_count": 3, + "tool_count": 30, + "accounts": accounts, + "sample_private_repositories": samples, + "clients": { + name: {"accepted": True, "repository_fingerprints": fingerprints} + for name in sorted(ECHO_CLIENTS) + }, + "observed_at": NOW.isoformat(), + "expires_at": (NOW + timedelta(minutes=30)).isoformat(), + "signing_key_id": signing_key_id, + } + + +def test_echo_github_autonomy_requires_complete_exact_cross_client_e2e() -> None: + valid, reason = validate_production_e2e( + _payload(), _target(), _environment(), now=NOW + timedelta(minutes=1) + ) + assert valid, reason + + +@pytest.mark.parametrize( + ("mutation", "reason"), + [ + (lambda value: value.__setitem__("deployment_sha", "6" * 40), "production_e2e_deployment_sha_mismatch"), + (lambda value: value.__setitem__("tool_count", 27), "production_e2e_tool_count_mismatch"), + ( + lambda value: value["accounts"]["Bmcbob76"].__setitem__("private_count", 0), + "production_e2e_account_reconciliation_failed", + ), + ( + lambda value: value["clients"]["grok"].__setitem__("accepted", False), + "production_e2e_client_not_accepted", + ), + ( + lambda value: value["accounts"]["Bmcbob76"].__setitem__( + "credential_source", "model_config_pat" + ), + "production_e2e_credential_source_invalid", + ), + ( + lambda value: value["accounts"]["Bmcbob76"].__setitem__( + "secret_exposed", True + ), + "production_e2e_secret_boundary_failed", + ), + ( + lambda value: value["sample_private_repositories"]["Bmcbob76"].__setitem__( + "repository_id", True + ), + "production_e2e_sample_repository_id_invalid", + ), + ], +) +def test_incomplete_or_mismatched_e2e_fails_closed(mutation, reason: str) -> None: + payload = _payload() + mutation(payload) + valid, actual = validate_production_e2e( + payload, _target(), _environment(), now=NOW + timedelta(minutes=1) + ) + assert not valid + assert actual == reason + + +def test_stale_e2e_attestation_fails_closed() -> None: + valid, reason = validate_production_e2e( + _payload(), _target(), _environment(), now=NOW + timedelta(hours=2) + ) + assert not valid + assert reason == "production_e2e_attestation_not_current" + + +def test_attestation_loader_requires_a_pinned_collector_key(tmp_path) -> None: + signer = Ed25519VerdictSigner.generate() + payload = _payload(signing_key_id=signer.key_id) + envelope = signer.sign_payload(payload) + envelope_path = tmp_path / "attestation.json" + envelope_path.write_text( + json.dumps( + { + "payload": envelope.payload, + "signature_b64": envelope.signature_b64, + "key_id": envelope.key_id, + "public_key_pem": envelope.public_key_pem, + } + ), + encoding="utf-8", + ) + trusted = tmp_path / "trusted" + trusted.mkdir() + with pytest.raises(ValueError, match="untrusted_signing_key"): + load_signed_attestation(envelope_path, trusted) + (trusted / "collector.pem").write_text(signer.public_key_pem, encoding="ascii") + verified = load_signed_attestation(envelope_path, trusted) + assert verified.payload == payload + assert verified.collector_key_id == signer.key_id + assert len(verified.envelope_sha256) == 64 + + +def test_self_selected_public_key_cannot_replace_the_pinned_collector(tmp_path) -> None: + trusted_signer = Ed25519VerdictSigner.generate() + attacker = Ed25519VerdictSigner.generate() + payload = _payload(signing_key_id=attacker.key_id) + envelope = attacker.sign_payload(payload) + envelope_path = tmp_path / "attestation.json" + envelope_path.write_text( + json.dumps( + { + "payload": envelope.payload, + "signature_b64": envelope.signature_b64, + "key_id": envelope.key_id, + "public_key_pem": envelope.public_key_pem, + } + ), + encoding="utf-8", + ) + trusted = tmp_path / "trusted" + trusted.mkdir() + (trusted / "collector.pem").write_text(trusted_signer.public_key_pem, encoding="ascii") + with pytest.raises(ValueError, match="untrusted_signing_key"): + load_signed_attestation(envelope_path, trusted) diff --git a/tests/test_runner.py b/tests/test_runner.py index 7a16b63..0266497 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -634,7 +634,12 @@ def test_workspace_owner_marker_and_symlink_detection(tmp_path: Path) -> None: ) target = tmp_path / "outside.txt" target.write_text("outside") - os.symlink(target, workspace / "link") + try: + os.symlink(target, workspace / "link") + except OSError as exc: + if os.name == "nt" and getattr(exc, "winerror", None) == 1314: + pytest.skip("Windows symlink privilege is unavailable") + raise with pytest.raises(IsolationFailure, match="symlink_not_allowed"): assert_no_symlinks(workspace) diff --git a/tests/test_t4_live_run.py b/tests/test_t4_live_run.py index 3d7e6fa..02d6784 100644 --- a/tests/test_t4_live_run.py +++ b/tests/test_t4_live_run.py @@ -18,6 +18,7 @@ from echo_certification_forge.models import SignedVerdictEnvelope from echo_certification_forge.run_worker import run from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry +from production_e2e_support import trusted_generic_production_e2e def _benign(tmp_path: Path) -> Path: @@ -99,7 +100,8 @@ def _run_worker(store, manifest, tenant, source, journey): signer = Ed25519VerdictSigner.generate() result = run("cert-live", tenant, {"type": "local", "path": str(source)}, store=store, manifest=manifest, signer=signer, - entitled=frozenset({tenant}), journey=journey) + entitled=frozenset({tenant}), journey=journey, + production_e2e_provider=trusted_generic_production_e2e) return result, signer diff --git a/tests/test_t4p5_executor.py b/tests/test_t4p5_executor.py index f4cd76d..60dbfc8 100644 --- a/tests/test_t4p5_executor.py +++ b/tests/test_t4p5_executor.py @@ -11,6 +11,7 @@ from echo_certification_forge.canonical import to_utc_iso, utc_now from echo_certification_forge.executor import RunExecutor, StaticEntitlement from echo_certification_forge.signing import Ed25519VerdictSigner +from production_e2e_support import trusted_generic_production_e2e RUN = "cert-exec" @@ -140,7 +141,10 @@ def test_retention_purge_removes_expired_content_but_keeps_signed_verdict( entitlement=StaticEntitlement(frozenset({target.tenant_id})), journey=[sys.executable, "hello.py"], control_attestations={"runner_control_channel": True, - "signing_authority_separation": True}) + "signing_authority_separation": True}, + production_e2e_attestation=trusted_generic_production_e2e( + target, environment + )) assert result.release_verdict == "PRODUCTION_READY", result.blocking_findings verdict_before = store.latest_signed_verdict(RUN, target.tenant_id) diff --git a/tests/test_t4p7_e2e.py b/tests/test_t4p7_e2e.py index 025c9a8..e733c57 100644 --- a/tests/test_t4p7_e2e.py +++ b/tests/test_t4p7_e2e.py @@ -15,6 +15,7 @@ from echo_certification_forge.executor import RunExecutor, StaticEntitlement from echo_certification_forge.models import SignedVerdictEnvelope from echo_certification_forge.signing import Ed25519VerdictSigner, TrustedPublicKeyRegistry +from production_e2e_support import trusted_generic_production_e2e RUN = "cert-e2e" @@ -46,6 +47,7 @@ def test_e2e_benign_target_certified_and_deploy_gate_allows(store, manifest, tar entitlement=StaticEntitlement(frozenset({target.tenant_id})), journey=[sys.executable, "hello.py"], control_attestations={"runner_control_channel": True, "signing_authority_separation": True}, + production_e2e_attestation=trusted_generic_production_e2e(target, environment), ) # certified through the full path diff --git a/tests/test_t4p8_sandbox.py b/tests/test_t4p8_sandbox.py index b0d3d45..a1e9dc5 100644 --- a/tests/test_t4p8_sandbox.py +++ b/tests/test_t4p8_sandbox.py @@ -15,6 +15,7 @@ DEFAULT_IMAGE, DockerSandbox, SandboxError, sandboxed_journey_runner, ) from echo_certification_forge.signing import Ed25519VerdictSigner +from production_e2e_support import trusted_generic_production_e2e def test_build_command_has_all_hardening_flags(tmp_path): @@ -72,6 +73,7 @@ def fake_runner(argv, workdir): entitlement=StaticEntitlement(frozenset({target.tenant_id})), journey=["python3", "hello.py"], journey_runner=fake_runner, control_attestations={"runner_control_channel": True, "signing_authority_separation": True}, + production_e2e_attestation=trusted_generic_production_e2e(target, environment), ) assert calls and calls[0][0] == ["python3", "hello.py"] # the injected runner was used assert result.release_verdict == "PRODUCTION_READY" From 61cebbb7b8dbd84e1cb444df5e69acb6b12ebaa1 Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 05:43:38 -0700 Subject: [PATCH 2/7] Test the P6 gate with signed production E2E --- artifacts/p6_acceptance.json | 22 +++++++++--------- artifacts/p6_acceptance.summary.json | 2 +- scripts/p6_acceptance.py | 34 ++++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/artifacts/p6_acceptance.json b/artifacts/p6_acceptance.json index cfbd348..13c22df 100644 --- a/artifacts/p6_acceptance.json +++ b/artifacts/p6_acceptance.json @@ -1,8 +1,8 @@ { "phase": "P6", "title": "deployment enforcement and platform integration \u2014 live local acceptance", - "generated_at": "2026-07-26T02:18:04.411111Z", - "python": "3.13.0 (tags/v3.13.0:60403a5, Oct 7 2024, 09:38:07) [MSC v.1941 64 bit (AMD64)]", + "generated_at": "2026-08-30T12:40:31.960114Z", + "python": "3.12.10 (tags/v3.12.10:0cc8128, Apr 8 2025, 12:21:36) [MSC v.1943 64 bit (AMD64)]", "passed": true, "checks": [ { @@ -11,7 +11,7 @@ "passed": true, "detail": { "exit": 2, - "stdout": "{\"admission_id\": \"dep-3413cd5f5334d3b106b9ce81f7c7a26b\", \"allowed\": false, \"artifact_sha256\": \"cc9a65d96c25c8f96ba720e01628b734d6df4f5a1057238506ab6d504fb998fd\", \"chain_hash\": \"ab88eb7a583dc68353ca39d53cd4e37d9737ac4b6fa0e6c9b0ab8c659325a64d\", \"created_at\": \"2026-07-26T02:17:59.537952Z\", \"deployment_environment\": \"production\", \"deployment_id\": \"deploy-a1\", \"reasons\": [\"artifact_not_certified\", \"staging_acceptance_missing\"], \"rollback_candidate\": null, \"run_id\": null, \"tenant_id\": \"tenant-alpha\"}" + "stdout": "{\"admission_id\": \"dep-39149ed9c3643c355bbe8f4f09eec260\", \"allowed\": false, \"artifact_sha256\": \"cc9a65d96c25c8f96ba720e01628b734d6df4f5a1057238506ab6d504fb998fd\", \"chain_hash\": \"ff5f335a43355fdc85e2b64c11e7429ad3aa9070dca78655a74bf0d929da28b0\", \"created_at\": \"2026-08-30T12:40:27.987414Z\", \"deployment_environment\": \"production\", \"deployment_id\": \"deploy-a1\", \"reasons\": [\"artifact_not_certified\", \"staging_acceptance_missing\"], \"rollback_candidate\": null, \"run_id\": null, \"tenant_id\": \"tenant-alpha\"}" } }, { @@ -29,7 +29,7 @@ "passed": true, "detail": { "exit": 2, - "stdout": "{\"admission_id\": \"dep-f49980b48837ae58dec6fa3ffbc216e4\", \"allowed\": false, \"artifact_sha256\": \"11d7d626a19af7cf0b64c0862b3214a15fac77e3aa1400d70e6902f1a77fdb86\", \"chain_hash\": \"40c1d83374649745ee6fcd3c42c58ce5b6257e362aa6e22954f224c7284b61f5\", \"created_at\": \"2026-07-26T02:17:59.744469Z\", \"deployment_environment\": \"staging\", \"deployment_id\": \"deploy-a2\", \"reasons\": [\"artifact_not_certified\"], \"rollback_candidate\": null, \"run_id\": null, \"tenant_id\": \"tenant-alpha\"}" + "stdout": "{\"admission_id\": \"dep-de226c922ca25e4fe770043fa5cce489\", \"allowed\": false, \"artifact_sha256\": \"11d7d626a19af7cf0b64c0862b3214a15fac77e3aa1400d70e6902f1a77fdb86\", \"chain_hash\": \"1b08dbf5ba6527bbb6b743ab658e726bb487454d6ab95bd8ec1ec7889764501a\", \"created_at\": \"2026-08-30T12:40:28.129813Z\", \"deployment_environment\": \"staging\", \"deployment_id\": \"deploy-a2\", \"reasons\": [\"artifact_not_certified\"], \"rollback_candidate\": null, \"run_id\": null, \"tenant_id\": \"tenant-alpha\"}" } }, { @@ -38,7 +38,7 @@ "passed": true, "detail": { "exit": 2, - "stdout": "{\"admission_id\": \"dep-8ac38cbd2ade07ff1bb5c59d63d2582c\", \"allowed\": false, \"artifact_sha256\": \"67b7548d601f8ca9e8689fced7d40d8f407894ac7d6a2c09d2e524ee5514e7b0\", \"chain_hash\": \"5d31c6a43518753dc06259bee965f1e8fb91e4114f86c47bf61adde8a84ff4dd\", \"created_at\": \"2026-07-26T02:17:59.912909Z\", \"deployment_environment\": \"production\", \"deployment_id\": \"deploy-a4\", \"reasons\": [\"staging_acceptance_missing\"], \"rollback_candidate\": null, \"run_id\": \"cert-p6-v1\", \"tenant_id\": \"tenant-alpha\"}" + "stdout": "{\"admission_id\": \"dep-393b3f1c13aeaf883a659c67fe3a6062\", \"allowed\": false, \"artifact_sha256\": \"67b7548d601f8ca9e8689fced7d40d8f407894ac7d6a2c09d2e524ee5514e7b0\", \"chain_hash\": \"4adc5c6ca6c45d5664c517904d6b86304b4ae573fa525cef6397e6a315d30422\", \"created_at\": \"2026-08-30T12:40:28.260911Z\", \"deployment_environment\": \"production\", \"deployment_id\": \"deploy-a4\", \"reasons\": [\"staging_acceptance_missing\"], \"rollback_candidate\": null, \"run_id\": \"cert-p6-v1\", \"tenant_id\": \"tenant-alpha\"}" } }, { @@ -57,8 +57,8 @@ "passed": true, "detail": { "failure_candidate": { - "admission_id": "dep-c3f042d865f9fcd3c85e9d0a1c91484a", - "admitted_at": "2026-07-26T02:18:00.438507Z", + "admission_id": "dep-88177f711ac3ea76d71d3f5657753a8a", + "admitted_at": "2026-08-30T12:40:28.640308Z", "artifact_sha256": "67b7548d601f8ca9e8689fced7d40d8f407894ac7d6a2c09d2e524ee5514e7b0", "deployment_id": "deploy-a3-prd", "run_id": "cert-p6-v1" @@ -66,12 +66,12 @@ "rollback_target": { "artifact_sha256": "67b7548d601f8ca9e8689fced7d40d8f407894ac7d6a2c09d2e524ee5514e7b0", "run_id": "cert-p6-v1", - "admission_id": "dep-c3f042d865f9fcd3c85e9d0a1c91484a", + "admission_id": "dep-88177f711ac3ea76d71d3f5657753a8a", "deployment_id": "deploy-a3-prd", - "admitted_at": "2026-07-26T02:18:00.438507Z" + "admitted_at": "2026-08-30T12:40:28.640308Z" }, "rolled_back": { - "admission_id": "dep-d1f547a64a4ddb086ca7a521e0d1a4ce", + "admission_id": "dep-368a34d3be4daeb9870a2b93e41ebf45", "artifact_sha256": "6cf9eb2e75fbaf4037393188b98be82a05499dcfc0c45a6eeeb840bd7f775c20", "deployment_environment": "production", "detail": "restored v1", @@ -87,7 +87,7 @@ "passed": true, "detail": { "exit": 2, - "stdout": "{\"admission_id\": \"dep-4ec864d16bf5447abf4145975d62ed0a\", \"allowed\": false, \"artifact_sha256\": \"67b7548d601f8ca9e8689fced7d40d8f407894ac7d6a2c09d2e524ee5514e7b0\", \"chain_hash\": \"928bee7494d599c95422e4d3f6c8db2a7e80c2ec1ff5837056aead46be211115\", \"created_at\": \"2026-07-26T02:18:01.842335Z\", \"deployment_environment\": \"production\", \"deployment_id\": \"deploy-a5\", \"reasons\": [\"verdict_revoked\"], \"rollback_candidate\": null, \"run_id\": \"cert-p6-v1\", \"tenant_id\": \"tenant-alpha\"}" + "stdout": "{\"admission_id\": \"dep-1d051283684978484ff174d2f3cc5b7b\", \"allowed\": false, \"artifact_sha256\": \"67b7548d601f8ca9e8689fced7d40d8f407894ac7d6a2c09d2e524ee5514e7b0\", \"chain_hash\": \"8cbb0ce94045d583aaaaa7e150f5a997759f188f1de824d89f973711ef9a5754\", \"created_at\": \"2026-08-30T12:40:29.545903Z\", \"deployment_environment\": \"production\", \"deployment_id\": \"deploy-a5\", \"reasons\": [\"verdict_revoked\"], \"rollback_candidate\": null, \"run_id\": \"cert-p6-v1\", \"tenant_id\": \"tenant-alpha\"}" } }, { diff --git a/artifacts/p6_acceptance.summary.json b/artifacts/p6_acceptance.summary.json index 2c77777..ac231fd 100644 --- a/artifacts/p6_acceptance.summary.json +++ b/artifacts/p6_acceptance.summary.json @@ -3,5 +3,5 @@ "passed": true, "checks_total": 12, "checks_passed": 12, - "generated_at": "2026-07-26T02:18:04.411111Z" + "generated_at": "2026-08-30T12:40:31.960114Z" } diff --git a/scripts/p6_acceptance.py b/scripts/p6_acceptance.py index 05c4429..15490fe 100644 --- a/scripts/p6_acceptance.py +++ b/scripts/p6_acceptance.py @@ -41,7 +41,7 @@ import time import urllib.parse import urllib.request -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] @@ -64,6 +64,12 @@ VerdictLifecycleEvent, ) from echo_certification_forge.policy import RuleManifest # noqa: E402 +from echo_certification_forge.production_e2e import ( # noqa: E402 + BASE_CHECKS, + GENERIC_PROFILE, + SCHEMA_VERSION, + verify_signed_attestation, +) from echo_certification_forge.release_hooks import ( # noqa: E402 SIGNATURE_HEADER, TIMESTAMP_HEADER, @@ -187,6 +193,9 @@ def check(check_id: str, description: str, passed: bool, detail: object) -> None signer = Ed25519VerdictSigner.generate() trusted = TrustedPublicKeyRegistry.empty() trusted.add_pem(signer.public_key_pem) + e2e_signer = Ed25519VerdictSigner.generate() + e2e_trusted = TrustedPublicKeyRegistry.empty() + e2e_trusted.add_pem(e2e_signer.public_key_pem) environment = EnvironmentIdentity( runner_image_sha256=digest("runner"), @@ -205,6 +214,26 @@ def certify(run_id: str, target: TargetIdentity) -> None: workdir = tmp_path / f"src-{run_id}" workdir.mkdir() (workdir / "hello.py").write_text("print('service ok')\n", encoding="utf-8") + observed_at = utc_now() + e2e_payload = { + "schema_version": SCHEMA_VERSION, + "attestation_id": f"p6-e2e-{target.identity_digest[:16]}", + "profile": GENERIC_PROFILE, + "target_identity_digest": target.identity_digest, + "environment_identity_digest": environment.identity_digest, + "source_commit": target.source_commit, + "deployment_sha": target.source_commit, + "canonical_target": target.canonical_ref, + "required_checks": sorted(BASE_CHECKS), + "checks": {name: True for name in sorted(BASE_CHECKS)}, + "stability_probe_count": 3, + "observed_at": to_utc_iso(observed_at), + "expires_at": to_utc_iso(observed_at + timedelta(minutes=30)), + "signing_key_id": e2e_signer.key_id, + } + production_e2e = verify_signed_attestation( + e2e_signer.sign_payload(e2e_payload), e2e_trusted + ) result = RunExecutor(store, manifest, signer).execute( run_id, target.tenant_id, @@ -215,6 +244,7 @@ def certify(run_id: str, target: TargetIdentity) -> None: "runner_control_channel": True, "signing_authority_separation": True, }, + production_e2e_attestation=production_e2e, ) if result.release_verdict != "PRODUCTION_READY": raise RuntimeError(f"certification failed: {result.blocking_findings}") @@ -225,7 +255,7 @@ def make_target(label: str) -> TargetIdentity: target_type="container", canonical_ref=f"registry.echo/app@{label}", artifact_sha256=digest(label), - source_commit="abc123def456", + source_commit=digest("source-commit")[:40], dependency_sha256=digest("dependencies"), configuration_sha256=digest("configuration"), ) From 7a7a1c92b9ee4978bf27693620f4c503e524f51e Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 05:57:39 -0700 Subject: [PATCH 3/7] Bind deployment checks to the effective database --- deploy/deploy_forge.sh | 11 ++++++----- tests/test_deploy_gate.py | 11 +++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/deploy/deploy_forge.sh b/deploy/deploy_forge.sh index 2e649b5..72ee784 100644 --- a/deploy/deploy_forge.sh +++ b/deploy/deploy_forge.sh @@ -173,6 +173,7 @@ set -a set +a PROD_PEPPER="${ECHO_CERTFORGE_API_KEY_PEPPER:-}" STAGING_PEPPER="${ECHO_CERTFORGE_STAGING_API_KEY_PEPPER:-}" +PROD_DB_PATH="${ECHO_CERTFORGE_DB:-$STATE_ROOT/certforge.sqlite3}" test "${#PROD_PEPPER}" -ge 32 || { echo "!! production ECHO_CERTFORGE_API_KEY_PEPPER must be at least 32 bytes" exit 1 @@ -305,7 +306,7 @@ if sudo test -f "$DISPATCH_RELEASE_DROPIN"; then sudo cat "$DISPATCH_RELEASE_DROPIN" >"$DISPATCH_RELEASE_DROPIN_BACKUP" HAD_DISPATCH_RELEASE_DROPIN=1 fi -DB_PATH="$STATE_ROOT/certforge.sqlite3" +DB_PATH="$PROD_DB_PATH" DB_BACKUP="$STATE_ROOT/deploy-scratch/echo-certforge-db.$RELEASE_ID" HAD_DB=0 DB_SNAPSHOT_READY=0 @@ -503,7 +504,7 @@ Type=simple User=forge WorkingDirectory=$CURRENT_LINK Environment=PYTHONUNBUFFERED=1 -Environment=ECHO_CERTFORGE_DB=$STATE_ROOT/certforge.sqlite3 +Environment=ECHO_CERTFORGE_DB=$DB_PATH Environment=ECHO_CERTFORGE_EVIDENCE_ROOT=$STATE_ROOT/evidence Environment=ECHO_CERTFORGE_POLICY=$CURRENT_LINK/policies/mandatory-rules.v2.json Environment=ECHO_CERTFORGE_SUBSCRIBER_POLICY=$CURRENT_LINK/policies/subscriber-governance.v1.json @@ -546,7 +547,7 @@ Type=simple User=forge WorkingDirectory=$CURRENT_LINK Environment=PYTHONUNBUFFERED=1 -Environment=ECHO_CERTFORGE_DB=$STATE_ROOT/certforge.sqlite3 +Environment=ECHO_CERTFORGE_DB=$DB_PATH Environment=ECHO_CERTFORGE_EVIDENCE_ROOT=$STATE_ROOT/evidence Environment=ECHO_CERTFORGE_POLICY=$CURRENT_LINK/policies/mandatory-rules.v2.json Environment=ECHO_CERTFORGE_SUBSCRIBER_POLICY=$CURRENT_LINK/policies/subscriber-governance.v1.json @@ -593,7 +594,7 @@ for _ in $(seq 1 40); do } sleep 0.5 done -if [ "$ready" != 1 ] || ! ECHO_CERTFORGE_DB="$STATE_ROOT/certforge.sqlite3" \ +if [ "$ready" != 1 ] || ! ECHO_CERTFORGE_DB="$DB_PATH" \ ECHO_CERTFORGE_SUBSCRIBER_POLICY="$RELEASE_DIR/policies/subscriber-governance.v1.json" \ ECHO_CERTFORGE_API_KEY_PEPPER="$PROD_PEPPER" \ ECHO_CERTFORGE_EXPECT_PRODUCT_READY=1 \ @@ -621,7 +622,7 @@ if [ "$dispatcher_ready" != 1 ]; then exit 1 fi if [ "$ADAPTER_MODE" = required ]; then - ECHO_CERTFORGE_DB="$STATE_ROOT/certforge.sqlite3" \ + ECHO_CERTFORGE_DB="$DB_PATH" \ ECHO_CERTFORGE_SUBSCRIBER_POLICY="$RELEASE_DIR/policies/subscriber-governance.v1.json" \ ECHO_CERTFORGE_API_KEY_PEPPER="$PROD_PEPPER" \ "$RELEASE_DIR/.venv/bin/python" "$RELEASE_DIR/deploy/smoke_dispatch.py" \ diff --git a/tests/test_deploy_gate.py b/tests/test_deploy_gate.py index 8a0a1ed..67f7b9c 100644 --- a/tests/test_deploy_gate.py +++ b/tests/test_deploy_gate.py @@ -96,6 +96,17 @@ def test_deploy_gate_snapshots_and_restores_persistent_database() -> None: assert 'DB_SNAPSHOT_READY=1' in DEPLOY_SCRIPT +def test_deploy_gate_uses_the_effective_production_database_everywhere() -> None: + assert ( + 'PROD_DB_PATH="${ECHO_CERTFORGE_DB:-$STATE_ROOT/certforge.sqlite3}"' + in DEPLOY_SCRIPT + ) + assert 'DB_PATH="$PROD_DB_PATH"' in DEPLOY_SCRIPT + assert DEPLOY_SCRIPT.count("Environment=ECHO_CERTFORGE_DB=$DB_PATH") == 2 + assert DEPLOY_SCRIPT.count('ECHO_CERTFORGE_DB="$DB_PATH" \\') == 2 + assert 'ECHO_CERTFORGE_DB="$STATE_ROOT/certforge.sqlite3"' not in DEPLOY_SCRIPT + + def test_deploy_gate_restores_prior_service_lifecycle() -> None: assert ( 'PREV_ENABLED="$(systemctl is-enabled "$SERVICE.service"' From 917ffe933b2c6b36bbbaab5f621b1c5889b2fd25 Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 06:08:08 -0700 Subject: [PATCH 4/7] Inherit the live CertForge database during deploy --- deploy/deploy_forge.sh | 23 ++++++++++++++++++++++- tests/test_deploy_gate.py | 10 ++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/deploy/deploy_forge.sh b/deploy/deploy_forge.sh index 72ee784..c9c3b34 100644 --- a/deploy/deploy_forge.sh +++ b/deploy/deploy_forge.sh @@ -173,7 +173,28 @@ set -a set +a PROD_PEPPER="${ECHO_CERTFORGE_API_KEY_PEPPER:-}" STAGING_PEPPER="${ECHO_CERTFORGE_STAGING_API_KEY_PEPPER:-}" -PROD_DB_PATH="${ECHO_CERTFORGE_DB:-$STATE_ROOT/certforge.sqlite3}" +PROD_DB_PATH="${ECHO_CERTFORGE_DB:-}" +if [ -z "$PROD_DB_PATH" ]; then + CURRENT_SERVICE_PID="$(systemctl show "$SERVICE.service" --property MainPID --value 2>/dev/null || true)" + if [ "${CURRENT_SERVICE_PID:-0}" -gt 1 ] 2>/dev/null && \ + [ -r "/proc/$CURRENT_SERVICE_PID/environ" ]; then + PROD_DB_PATH="$(python3 - "$CURRENT_SERVICE_PID" <<'PY' +from pathlib import Path +import sys + +for entry in Path(f"/proc/{sys.argv[1]}/environ").read_bytes().split(b"\0"): + if entry.startswith(b"ECHO_CERTFORGE_DB="): + print(entry.split(b"=", 1)[1].decode("utf-8")) + break +PY +)" + fi +fi +PROD_DB_PATH="${PROD_DB_PATH:-$STATE_ROOT/certforge.sqlite3}" +case "$(realpath -m "$PROD_DB_PATH")" in + "$STATE_ROOT"/*|/mnt/documentation/echo/certforge/*) ;; + *) echo "!! effective production database is outside an approved state root"; exit 1 ;; +esac test "${#PROD_PEPPER}" -ge 32 || { echo "!! production ECHO_CERTFORGE_API_KEY_PEPPER must be at least 32 bytes" exit 1 diff --git a/tests/test_deploy_gate.py b/tests/test_deploy_gate.py index 67f7b9c..9f83072 100644 --- a/tests/test_deploy_gate.py +++ b/tests/test_deploy_gate.py @@ -97,10 +97,12 @@ def test_deploy_gate_snapshots_and_restores_persistent_database() -> None: def test_deploy_gate_uses_the_effective_production_database_everywhere() -> None: - assert ( - 'PROD_DB_PATH="${ECHO_CERTFORGE_DB:-$STATE_ROOT/certforge.sqlite3}"' - in DEPLOY_SCRIPT - ) + assert 'PROD_DB_PATH="${ECHO_CERTFORGE_DB:-}"' in DEPLOY_SCRIPT + assert 'systemctl show "$SERVICE.service" --property MainPID --value' in DEPLOY_SCRIPT + assert 'Path(f"/proc/{sys.argv[1]}/environ")' in DEPLOY_SCRIPT + assert 'entry.startswith(b"ECHO_CERTFORGE_DB=")' in DEPLOY_SCRIPT + assert 'PROD_DB_PATH="${PROD_DB_PATH:-$STATE_ROOT/certforge.sqlite3}"' in DEPLOY_SCRIPT + assert '/mnt/documentation/echo/certforge/*' in DEPLOY_SCRIPT assert 'DB_PATH="$PROD_DB_PATH"' in DEPLOY_SCRIPT assert DEPLOY_SCRIPT.count("Environment=ECHO_CERTFORGE_DB=$DB_PATH") == 2 assert DEPLOY_SCRIPT.count('ECHO_CERTFORGE_DB="$DB_PATH" \\') == 2 From fba53a4bd3a251d939d8a71f174a80c5f47b786e Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 06:21:24 -0700 Subject: [PATCH 5/7] Make production deploy smoke prove fail-closed E2E gate --- deploy/smoke_dispatch.py | 39 +++++++++++++----- tests/test_deploy_dispatch_smoke_contract.py | 43 ++++++++++++++++++++ 2 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 tests/test_deploy_dispatch_smoke_contract.py diff --git a/deploy/smoke_dispatch.py b/deploy/smoke_dispatch.py index b0410c1..1661749 100644 --- a/deploy/smoke_dispatch.py +++ b/deploy/smoke_dispatch.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 -"""Production subscriber-to-dispatcher certification smoke. +"""Production subscriber-to-dispatcher fail-closed certification smoke. This test runs only after the real API and dispatcher are active. It submits an exact-identity -local target through the customer HTTP contract, waits for the durable dispatcher, and verifies -the signed PRODUCTION_READY verdict and append-only evidence through the public API. +local target through the customer HTTP contract without a trusted production-E2E attestation, +waits for the durable dispatcher, and verifies that the service signs ``NOT_READY`` while keeping +the append-only evidence and public signature-verification paths healthy. A deploy smoke must not +manufacture the external evidence that the production gate is intended to require. """ from __future__ import annotations @@ -65,6 +67,22 @@ def _require(condition: bool, message: str, detail: object | None = None) -> Non print(f" [ok ] {message}") +def _require_attestation_gate_closed(terminal: dict) -> None: + """Prove an unattested run cannot be promoted by the production dispatcher.""" + + _require( + terminal.get("release_verdict") == "NOT_READY", + "unattested customer run is NOT_READY", + terminal.get("release_verdict"), + ) + production_e2e = terminal.get("production_e2e") + _require( + isinstance(production_e2e, dict) and production_e2e.get("verified") is False, + "unattested customer run has no verified production E2E", + production_e2e, + ) + + def main() -> int: base = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8309" db_path = os.environ.get("ECHO_CERTFORGE_DB") @@ -177,11 +195,7 @@ def main() -> int: time.sleep(1) _require(terminal.get("state") == "COMPLETE", "dispatcher completed the customer run", terminal) _require(terminal.get("run_outcome") == "COMPLETE", "customer run outcome is COMPLETE") - _require( - terminal.get("release_verdict") == "PRODUCTION_READY", - "customer received PRODUCTION_READY", - terminal.get("release_verdict"), - ) + _require_attestation_gate_closed(terminal) _require( terminal.get("environment_identity_digest") == environment_digest, "verdict retained the published environment identity", @@ -198,7 +212,12 @@ def main() -> int: ) _require(verdict_status == 200, "signed verdict is retrievable", verdict_status) payload = verdict.get("payload", {}) - _require(payload.get("release_verdict") == "PRODUCTION_READY", "signed verdict is PRODUCTION_READY") + _require(payload.get("release_verdict") == "NOT_READY", "signed verdict is NOT_READY") + _require( + "production_e2e_attestation_missing" in payload.get("reasons", []), + "signed verdict records the missing production E2E attestation", + payload.get("reasons"), + ) _require(payload.get("environment_identity_digest") == environment_digest, "signed verdict binds environment") verify_status, verified = _request( @@ -209,7 +228,7 @@ def main() -> int: "public verdict verification succeeds", verified, ) - print(f"PRODUCTION DISPATCH SMOKE GREEN - run_id={run_id}") + print(f"PRODUCTION DISPATCH FAIL-CLOSED SMOKE GREEN - run_id={run_id}") return 0 finally: shutil.rmtree(target_root, ignore_errors=True) diff --git a/tests/test_deploy_dispatch_smoke_contract.py b/tests/test_deploy_dispatch_smoke_contract.py new file mode 100644 index 0000000..2118fa4 --- /dev/null +++ b/tests/test_deploy_dispatch_smoke_contract.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "certforge_deploy_smoke_dispatch", ROOT / "deploy" / "smoke_dispatch.py" +) +assert SPEC is not None and SPEC.loader is not None +SMOKE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(SMOKE) + + +def test_production_dispatch_smoke_requires_fail_closed_unattested_verdict() -> None: + SMOKE._require_attestation_gate_closed( # noqa: SLF001 + { + "release_verdict": "NOT_READY", + "production_e2e": {"verified": False}, + } + ) + + +@pytest.mark.parametrize( + "terminal", + [ + { + "release_verdict": "PRODUCTION_READY", + "production_e2e": {"verified": False}, + }, + { + "release_verdict": "NOT_READY", + "production_e2e": {"verified": True}, + }, + {"release_verdict": "NOT_READY"}, + ], +) +def test_production_dispatch_smoke_rejects_open_or_ambiguous_gate(terminal: dict) -> None: + with pytest.raises(RuntimeError): + SMOKE._require_attestation_gate_closed(terminal) # noqa: SLF001 From c7982ee3de2ccd9e8b84da86b3021652d7e2ccab Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 06:27:47 -0700 Subject: [PATCH 6/7] Preserve missing production E2E failure reason --- src/echo_certification_forge/verdict.py | 9 ++++++ tests/test_evidence_and_verdict.py | 42 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/echo_certification_forge/verdict.py b/src/echo_certification_forge/verdict.py index 457751c..82c1e12 100644 --- a/src/echo_certification_forge/verdict.py +++ b/src/echo_certification_forge/verdict.py @@ -88,6 +88,15 @@ def evaluate( reasons.append("production_e2e_identity_unavailable") elif e2e_result is None: reasons.append("production_e2e_attestation_missing") + elif ( + not e2e_result.passed + and e2e_result.details.get("validation") + == "production_e2e_attestation_missing" + ): + # The executor records a fail-closed rule row even when no attestation was supplied. + # Preserve that explicit cause instead of re-validating the diagnostic placeholder as + # though it were a malformed attestation payload. + reasons.append("production_e2e_attestation_missing") else: e2e_valid, e2e_reason = validate_production_e2e( e2e_result.details, diff --git a/tests/test_evidence_and_verdict.py b/tests/test_evidence_and_verdict.py index 2265075..c616c7c 100644 --- a/tests/test_evidence_and_verdict.py +++ b/tests/test_evidence_and_verdict.py @@ -135,6 +135,48 @@ def test_source_and_rule_rows_without_production_e2e_never_get_ready( assert "production_e2e_attestation_missing" in decision.reasons +def test_failed_executor_rule_preserves_missing_attestation_reason( + store, manifest, target, environment +): + run_id = register(store, manifest, target, environment) + for index, rule in enumerate(manifest.rules, start=1): + artifact_id = f"executor-evidence-{index:02d}" + store.append_artifact( + run_id, + target.tenant_id, + artifact_id, + json.dumps({"rule": rule.id}, sort_keys=True).encode(), + "application/json", + "executor-regression", + RedactionStatus.COMPLETE, + ) + is_production_e2e = rule.id == "production_e2e" + store.record_rule_result( + run_id, + target.tenant_id, + RuleResult( + rule.id, + not is_production_e2e, + (artifact_id,), + ( + {"validation": "production_e2e_attestation_missing"} + if is_production_e2e + else {"source": "executor-regression"} + ), + ), + ) + store.set_run_outcome(run_id, target.tenant_id, RunOutcome.COMPLETE) + + signer = Ed25519VerdictSigner.generate() + decision = DeterministicVerdictEngine().evaluate( + store, run_id, target.tenant_id, manifest, signer.key_id + ) + + assert decision.release_verdict is ReleaseVerdict.NOT_READY + assert "production_e2e_attestation_missing" in decision.reasons + assert "production_e2e_schema_invalid" not in decision.reasons + + def test_bare_database_rows_cannot_manufacture_ready(store, manifest, target, environment): run_id = register(store, manifest, target, environment) now = "2026-07-16T00:00:00Z" From 3bfe40b486520be31e4bc94cb83740dbc8b224be Mon Sep 17 00:00:00 2001 From: ECHO OMEGA PRIME Date: Sun, 30 Aug 2026 06:38:58 -0700 Subject: [PATCH 7/7] Canonicalize the GitHub App publish capability --- .echo/sdk.json | 1 + contracts/certforge-sdk-capabilities.v1.json | 29 +++++++++++++++++++- scripts/certification_journey.py | 4 +-- scripts/generate_certforge_sdk_contract.py | 10 +++++++ scripts/master_acceptance.py | 6 ++-- scripts/register_certforge_caps.sql | 6 ++++ scripts/register_certforge_sdk_schemas.sql | 5 ++-- tests/test_echo_sdk_manifest.py | 2 +- tests/test_sdk_capability_contract.py | 10 ++++++- 9 files changed, 63 insertions(+), 10 deletions(-) diff --git a/.echo/sdk.json b/.echo/sdk.json index 2d1fe37..f856e69 100644 --- a/.echo/sdk.json +++ b/.echo/sdk.json @@ -46,6 +46,7 @@ "echo.certforge.projects.create", "echo.certforge.projects.list", "echo.certforge.public_verification", + "echo.certforge.publish", "echo.certforge.r5.status", "echo.certforge.r5.submit_async", "echo.certforge.run", diff --git a/contracts/certforge-sdk-capabilities.v1.json b/contracts/certforge-sdk-capabilities.v1.json index e3522ea..66e2950 100644 --- a/contracts/certforge-sdk-capabilities.v1.json +++ b/contracts/certforge-sdk-capabilities.v1.json @@ -1412,6 +1412,33 @@ "type": "object" } }, + "echo.certforge.publish": { + "input_schema": { + "additionalProperties": false, + "properties": { + "command": { + "maxLength": 64, + "minLength": 1, + "type": "string" + }, + "run_id": { + "type": "string" + } + }, + "required": [ + "command", + "run_id" + ], + "type": "object" + }, + "operation": "POST /v1/subscriber/certifications/{run_id}/publish", + "output_schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, "echo.certforge.r5.status": { "input_schema": { "additionalProperties": false, @@ -2295,7 +2322,7 @@ } } }, - "capability_count": 60, + "capability_count": 61, "policy": "Every Certification Forge SDK action has a command-bearing strict input schema and an output schema derived from its production route contract.", "schema_version": "1.0.0" } diff --git a/scripts/certification_journey.py b/scripts/certification_journey.py index 38ca793..bf48bc6 100644 --- a/scripts/certification_journey.py +++ b/scripts/certification_journey.py @@ -62,8 +62,8 @@ def main() -> int: sdk = json.loads((ROOT / ".echo" / "sdk.json").read_text(encoding="utf-8")) capabilities = sdk.get("capabilities", []) - require(len(capabilities) == 60, "SDK capability declaration must contain exactly 60 entries") - require(len(set(capabilities)) == 60, "SDK capability declaration contains duplicates") + require(len(capabilities) == 61, "SDK capability declaration must contain exactly 61 entries") + require(len(set(capabilities)) == 61, "SDK capability declaration contains duplicates") require(all(isinstance(item, str) and item.startswith("echo.") for item in capabilities), "SDK capability declaration contains an invalid identifier") diff --git a/scripts/generate_certforge_sdk_contract.py b/scripts/generate_certforge_sdk_contract.py index 1537784..b061f51 100644 --- a/scripts/generate_certforge_sdk_contract.py +++ b/scripts/generate_certforge_sdk_contract.py @@ -216,6 +216,16 @@ def build_contract() -> dict[str, Any]: "input_schema": _operation_input(operation, components), "output_schema": _operation_output(operation, components), } + # Keep the least-privileged publishing capability used by the Certification Forge GitHub + # App as a first-class compatibility alias. It targets the same fail-closed route as the + # tier-2 administrative capability, but the service still re-evaluates the exact signed + # PRODUCTION_READY verdict before publishing public verification material. + publish = capabilities["echo.certforge.admin.publish"] + capabilities["echo.certforge.publish"] = { + "operation": publish["operation"], + "input_schema": publish["input_schema"], + "output_schema": publish["output_schema"], + } capabilities.update(_manual_capabilities()) return { "schema_version": "1.0.0", diff --git a/scripts/master_acceptance.py b/scripts/master_acceptance.py index e9f4a2e..4048db6 100644 --- a/scripts/master_acceptance.py +++ b/scripts/master_acceptance.py @@ -72,10 +72,10 @@ def _p7(value: dict[str, Any]) -> None: def _sdk(value: dict[str, Any]) -> None: capabilities = value.get("capabilities") - _require(value.get("capability_count") == 60, "SDK contract must contain exactly 60 capabilities") - _require(isinstance(capabilities, dict) and len(capabilities) == 60, "SDK capability rows are incomplete") + _require(value.get("capability_count") == 61, "SDK contract must contain exactly 61 capabilities") + _require(isinstance(capabilities, dict) and len(capabilities) == 61, "SDK capability rows are incomplete") names = list(capabilities) - _require(len(set(names)) == 60, "SDK capability IDs are not unique") + _require(len(set(names)) == 61, "SDK capability IDs are not unique") _require(all(isinstance(name, str) and name.startswith("echo.cert") for name in names), "SDK capability namespace drift") _require( all( diff --git a/scripts/register_certforge_caps.sql b/scripts/register_certforge_caps.sql index 80dbd57..6969503 100644 --- a/scripts/register_certforge_caps.sql +++ b/scripts/register_certforge_caps.sql @@ -136,6 +136,12 @@ VALUES '{"type":"object","required":["run_id"],"properties":{"run_id":{"type":"string","minLength":1,"maxLength":128}},"additionalProperties":false}'::jsonb, 'certforge.admin.mutate', 2, '{"X-Tenant-ID":"org-echo-sovereign","Authorization":"vault:certforge.desktop_admin_api_key"}'::jsonb, 15, 'active', 'unknown'), + ('echo.certforge.publish', + 'Certification Forge: publish public-only verification material for one already-current, signed, production-ready run. The service re-evaluates the deployment gate; this capability cannot change evidence, policy, verdicts, or lifecycle state.', + 'http', 'http://127.0.0.1:8309/v1/subscriber/certifications/{run_id}/publish', 'POST', 'path', 'forge', + '{"type":"object","required":["run_id"],"properties":{"run_id":{"type":"string","minLength":1,"maxLength":128}},"additionalProperties":false}'::jsonb, + 'certforge.read', 1, '{"X-Tenant-ID":"org-echo-sovereign","Authorization":"vault:certforge.desktop_admin_api_key"}'::jsonb, 15, 'active', 'unknown'), + ('echo.certforge.admin.quarantine', 'Certification Forge administration: quarantine one authenticated runner or adapter, removing it from eligible capacity/execution without deleting telemetry. Tier-2 HMAC and Desktop reauthentication are required.', 'http', 'http://127.0.0.1:8309/v1/subscriber/operational-quarantines', 'POST', 'json_body', 'forge', diff --git a/scripts/register_certforge_sdk_schemas.sql b/scripts/register_certforge_sdk_schemas.sql index fbfaae4..78f876b 100644 --- a/scripts/register_certforge_sdk_schemas.sql +++ b/scripts/register_certforge_sdk_schemas.sql @@ -50,6 +50,7 @@ INSERT INTO certforge_sdk_contract(id, input_schema_json, output_schema_json) VA ('echo.certforge.projects.create', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"name":{"maxLength":160,"minLength":1,"type":"string"},"slug":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$","type":"string"},"target_reference":{"maxLength":2048,"minLength":1,"type":"string"}},"required":["command","name","slug","target_reference"],"type":"object"}'::jsonb, '{"additionalProperties":true,"type":"object"}'::jsonb), ('echo.certforge.projects.list', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"}},"required":["command"],"type":"object"}'::jsonb, '{"items":{"additionalProperties":true,"type":"object"},"type":"array"}'::jsonb), ('echo.certforge.public_verification', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"verification_id":{"type":"string"}},"required":["command","verification_id"],"type":"object"}'::jsonb, '{"additionalProperties":true,"type":"object"}'::jsonb), + ('echo.certforge.publish', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"run_id":{"type":"string"}},"required":["command","run_id"],"type":"object"}'::jsonb, '{"additionalProperties":{"type":"string"},"type":"object"}'::jsonb), ('echo.certforge.r5.status', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"run_id":{"pattern":"^[A-Za-z0-9._-]{1,64}$","type":"string"}},"required":["command","run_id"],"type":"object"}'::jsonb, '{"additionalProperties":false,"properties":{"note":{"type":"string"},"result":{"anyOf":[{"additionalProperties":false,"properties":{"completion_marker":{"type":["string","null"]},"deployment_authorized":{"type":"boolean"},"evidence_run_id":{"type":"string"},"forge_verification":{"additionalProperties":true,"type":"object"},"mode":{"enum":["preflight","full"],"type":"string"},"operator_exit_code":{"type":"integer"},"operator_report":{"additionalProperties":true,"type":"object"},"r5_gate":{"enum":["PASS","BLOCK"],"type":"string"},"release_verdict":{"enum":["NOT_READY"],"type":"string"},"run_outcome":{"type":"string"},"schema":{"type":"string"},"stderr_tail":{"type":"string"}},"required":["schema","mode","evidence_run_id","run_outcome","release_verdict","r5_gate","deployment_authorized","completion_marker","operator_report","forge_verification","operator_exit_code"],"type":"object"},{"type":"null"}]},"run_id":{"type":"string"},"status":{"enum":["RUNNING","COMPLETE","FAILED"],"type":"string"}},"required":["run_id","status"],"type":"object"}'::jsonb), ('echo.certforge.r5.submit_async', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"dry_run":{"type":"boolean"},"evidence_run_id":{"pattern":"^[A-Za-z0-9._-]{1,64}$","type":"string"},"evidence_run_nonce":{"maxLength":128,"minLength":16,"type":"string"},"expected_base_model_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"expected_base_model_revision":{"maxLength":128,"minLength":6,"type":"string"},"expected_gs343_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"expected_r2d2_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"expected_registry_revision":{"maxLength":128,"minLength":6,"type":"string"},"expected_registry_snapshot_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"expected_server_build_digest":{"pattern":"^[0-9a-f]{64}$","type":"string"},"expected_signing_key_id":{"maxLength":128,"minLength":8,"type":"string"},"target_family":{"enum":["gs343","r2d2"],"type":"string"}},"required":["command","target_family","expected_server_build_digest","expected_registry_snapshot_digest","expected_registry_revision","expected_signing_key_id","expected_base_model_digest","expected_base_model_revision","expected_gs343_digest","expected_r2d2_digest","evidence_run_id","evidence_run_nonce"],"type":"object"}'::jsonb, '{"additionalProperties":false,"properties":{"idempotent":{"type":"boolean"},"mode":{"enum":["preflight","full"],"type":"string"},"poll_capability":{"enum":["echo.certforge.r5.status"],"type":"string"},"run_id":{"type":"string"},"status":{"enum":["RESERVED","RUNNING"],"type":"string"}},"required":["run_id","status","mode"],"type":"object"}'::jsonb), ('echo.certforge.run', '{"additionalProperties":false,"properties":{"command":{"maxLength":64,"minLength":1,"type":"string"},"run_id":{"pattern":"^[A-Za-z0-9._-]{1,128}$","type":"string"},"tenant":{"pattern":"^[A-Za-z0-9._-]{1,128}$","type":"string"}},"required":["command","run_id","tenant"],"type":"object"}'::jsonb, '{"additionalProperties":false,"properties":{"poll_capability":{"enum":["echo.certforge.status"],"type":"string"},"run_id":{"type":"string"},"sandboxed":{"type":"boolean"},"status":{"enum":["PENDING","CLAIMED"],"type":"string"},"tenant":{"type":"string"}},"required":["run_id","status","tenant","sandboxed","poll_capability"],"type":"object"}'::jsonb), @@ -74,8 +75,8 @@ BEGIN FROM arcanum_sdk.sdk_capabilities WHERE id LIKE 'echo.certforge.%' OR id = 'echo.certification_forge.r5.run_negative_controls'; - IF live_count <> 60 THEN - RAISE EXCEPTION 'Certification Forge SDK surface drift: live %, contract 60', live_count; + IF live_count <> 61 THEN + RAISE EXCEPTION 'Certification Forge SDK surface drift: live %, contract 61', live_count; END IF; IF EXISTS ( SELECT 1 FROM certforge_sdk_contract c diff --git a/tests/test_echo_sdk_manifest.py b/tests/test_echo_sdk_manifest.py index d3dfe13..9848804 100644 --- a/tests/test_echo_sdk_manifest.py +++ b/tests/test_echo_sdk_manifest.py @@ -13,7 +13,7 @@ def test_echo_sdk_manifest_matches_the_authoritative_contract() -> None: contract = json.loads(contract_path.read_text(encoding="utf-8")) assert manifest["version"] == 1 - assert len(manifest["capabilities"]) == contract["capability_count"] == 60 + assert len(manifest["capabilities"]) == contract["capability_count"] == 61 assert set(manifest["capabilities"]) == set(contract["capabilities"]) assert len(manifest["capabilities"]) == len(set(manifest["capabilities"])) assert all(capability.startswith("echo.") for capability in manifest["capabilities"]) diff --git a/tests/test_sdk_capability_contract.py b/tests/test_sdk_capability_contract.py index adcb6ef..a0afa0e 100644 --- a/tests/test_sdk_capability_contract.py +++ b/tests/test_sdk_capability_contract.py @@ -32,7 +32,7 @@ def test_generated_sdk_contract_is_current() -> None: def test_every_certforge_capability_has_command_and_output_schema() -> None: contract = json.loads(CONTRACT.read_text(encoding="utf-8")) capabilities = contract["capabilities"] - assert contract["capability_count"] == 60 == len(capabilities) + assert contract["capability_count"] == 61 == len(capabilities) for capability, item in capabilities.items(): input_schema = item["input_schema"] output_schema = item["output_schema"] @@ -48,6 +48,14 @@ def test_every_certforge_capability_has_command_and_output_schema() -> None: assert "type" in output_schema or "anyOf" in output_schema, capability +def test_github_app_publish_alias_is_exactly_schema_compatible() -> None: + contract = json.loads(CONTRACT.read_text(encoding="utf-8")) + capabilities = contract["capabilities"] + least_privileged = capabilities["echo.certforge.publish"] + administrative = capabilities["echo.certforge.admin.publish"] + assert least_privileged == administrative + + def test_registered_capability_set_equals_sdk_contract() -> None: contract = json.loads(CONTRACT.read_text(encoding="utf-8")) registered: set[str] = set()