diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bdcb471 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +.eggs/ diff --git a/Makefile b/Makefile index dd6c1f9..527b999 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: validate validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-activation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula doctor probe +.PHONY: validate validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-activation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula validate-runtime-install-receipts doctor probe PYTHON ?= python3 RUBY ?= ruby @@ -20,7 +20,7 @@ DECIDED_AT := 2026-05-04T12:51:00Z PYCLI := PYTHONPATH=src $(PYTHON) -m agent_machine.cli PYMOD := PYTHONPATH=src $(PYTHON) -m -validate: validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-activation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula +validate: validate-json validate-yaml validate-quadlet validate-render validate-evidence validate-governance validate-activation validate-supply-chain validate-release-bundle validate-sourceos-projections validate-package validate-cli validate-formula validate-runtime-install-receipts validate-json: $(PYTHON) scripts/validate-json.py @@ -86,6 +86,9 @@ validate-cli: validate-formula: $(RUBY) -c $(FORMULA) +validate-runtime-install-receipts: + $(PYTHON) scripts/validate-runtime-install-receipts.py + doctor: $(BOOTSTRAP_CLI) doctor --format json diff --git a/contracts/runtime-install-receipt.schema.json b/contracts/runtime-install-receipt.schema.json new file mode 100644 index 0000000..0eaec3c --- /dev/null +++ b/contracts/runtime-install-receipt.schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.srcos.ai/v2/RuntimeInstallReceipt.json", + "title": "RuntimeInstallReceipt", + "description": "Receipt for runtime installation, update, rollback, denial, or partial install flows with artifact verification, compact log references, policy decision linkage, and evidence references.", + "type": "object", + "additionalProperties": false, + "required": ["id", "type", "specVersion", "sessionRef", "capabilityLedgerRef", "runtimeRef", "targetRef", "platform", "installState", "manifest", "artifacts", "logMode", "policyDecisionRef", "evidenceRefs", "startedAt", "capturedAt"], + "properties": { + "id": { "type": "string", "pattern": "^urn:srcos:receipt:runtime-install:" }, + "type": { "const": "RuntimeInstallReceipt" }, + "specVersion": { "type": "string" }, + "sessionRef": { "type": "string", "pattern": "^urn:srcos:session:" }, + "capabilityLedgerRef": { "type": "string", "pattern": "^urn:srcos:capability-ledger:" }, + "agentMachineReceiptRef": { "type": ["string", "null"], "pattern": "^urn:srcos:agent-machine-receipt:" }, + "runtimeRef": { "type": "string", "pattern": "^urn:srcos:runtime:" }, + "runtimeName": { "type": ["string", "null"] }, + "runtimeVersion": { "type": ["string", "null"] }, + "targetRef": { "type": "string", "pattern": "^urn:srcos:target:" }, + "platform": { "type": "string", "enum": ["darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64", "win32-x64"] }, + "installState": { "type": "string", "enum": ["requested", "manifest_resolved", "artifact_verified", "installing", "installed", "failed", "rolled_back", "partial", "denied", "deferred"] }, + "manifest": { + "type": "object", + "additionalProperties": false, + "required": ["manifestRef", "manifestDigest", "resolvedAt"], + "properties": { + "manifestRef": { "type": "string" }, + "manifestDigest": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$" }, + "bundleFormatVersion": { "type": ["string", "integer", "null"] }, + "resolvedAt": { "type": "string", "format": "date-time" } + } + }, + "artifacts": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["artifactRef", "digest", "verificationState"], + "properties": { + "artifactRef": { "type": "string" }, + "digest": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$" }, + "sizeBytes": { "type": ["integer", "null"], "minimum": 0 }, + "verificationState": { "type": "string", "enum": ["not_checked", "verified", "failed", "skipped"] } + } + } + }, + "rollbackRef": { "type": ["string", "null"] }, + "failureReason": { "type": ["string", "null"] }, + "logMode": { "type": "string", "enum": ["compact_receipt_ref", "full_debug_redacted"] }, + "causalRefs": { "type": "array", "items": { "type": "string" } }, + "policyDecisionRef": { "type": "string", "pattern": "^urn:srcos:decision:" }, + "evidenceRefs": { "type": "array", "minItems": 1, "items": { "type": "string" } }, + "startedAt": { "type": "string", "format": "date-time" }, + "finishedAt": { "type": ["string", "null"], "format": "date-time" }, + "capturedAt": { "type": "string", "format": "date-time" } + } +} diff --git a/examples/runtime-install-receipt.deferred.json b/examples/runtime-install-receipt.deferred.json new file mode 100644 index 0000000..dbc54bf --- /dev/null +++ b/examples/runtime-install-receipt.deferred.json @@ -0,0 +1,41 @@ +{ + "id": "urn:srcos:receipt:runtime-install:agent-runtime-deferred-0001", + "type": "RuntimeInstallReceipt", + "specVersion": "0.1.0", + "sessionRef": "urn:srcos:session:runtime-install-demo-0006", + "capabilityLedgerRef": "urn:srcos:capability-ledger:runtime-install-demo-0006", + "agentMachineReceiptRef": null, + "runtimeRef": "urn:srcos:runtime:sourceos-agent-runtime@0.1.0", + "runtimeName": "sourceos-agent-runtime", + "runtimeVersion": "0.1.0", + "targetRef": "urn:srcos:target:developer-workstation-0001", + "platform": "linux-x64", + "installState": "deferred", + "manifest": { + "manifestRef": "urn:srcos:artifact:runtime-manifest-sourceos-agent-runtime-0.1.0", + "manifestDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "bundleFormatVersion": "1", + "resolvedAt": "2026-05-06T09:00:00Z" + }, + "artifacts": [ + { + "artifactRef": "urn:srcos:artifact:sourceos-agent-runtime-linux-x64-0.1.0.tar.xz", + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "sizeBytes": 1048576, + "verificationState": "not_checked" + } + ], + "rollbackRef": null, + "failureReason": "install_window_not_open_deferred_to_maintenance_window", + "logMode": "compact_receipt_ref", + "causalRefs": [ + "urn:srcos:event:runtime-install-requested-0006" + ], + "policyDecisionRef": "urn:srcos:decision:runtime-install-policy-0006", + "evidenceRefs": [ + "urn:srcos:evidence:runtime-install-deferred-0001" + ], + "startedAt": "2026-05-06T09:00:00Z", + "finishedAt": null, + "capturedAt": "2026-05-06T09:00:05Z" +} diff --git a/examples/runtime-install-receipt.denied.json b/examples/runtime-install-receipt.denied.json new file mode 100644 index 0000000..073107c --- /dev/null +++ b/examples/runtime-install-receipt.denied.json @@ -0,0 +1,41 @@ +{ + "id": "urn:srcos:receipt:runtime-install:agent-runtime-denied-0001", + "type": "RuntimeInstallReceipt", + "specVersion": "0.1.0", + "sessionRef": "urn:srcos:session:runtime-install-demo-0003", + "capabilityLedgerRef": "urn:srcos:capability-ledger:runtime-install-demo-0003", + "agentMachineReceiptRef": null, + "runtimeRef": "urn:srcos:runtime:sourceos-agent-runtime@0.1.0", + "runtimeName": "sourceos-agent-runtime", + "runtimeVersion": "0.1.0", + "targetRef": "urn:srcos:target:developer-workstation-0001", + "platform": "linux-x64", + "installState": "denied", + "manifest": { + "manifestRef": "urn:srcos:artifact:runtime-manifest-sourceos-agent-runtime-0.1.0", + "manifestDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "bundleFormatVersion": "1", + "resolvedAt": "2026-05-06T06:00:00Z" + }, + "artifacts": [ + { + "artifactRef": "urn:srcos:artifact:sourceos-agent-runtime-linux-x64-0.1.0.tar.xz", + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "sizeBytes": 1048576, + "verificationState": "not_checked" + } + ], + "rollbackRef": null, + "failureReason": "policy_denied_runtime_install_capability_not_granted", + "logMode": "compact_receipt_ref", + "causalRefs": [ + "urn:srcos:event:runtime-install-requested-0003" + ], + "policyDecisionRef": "urn:srcos:decision:runtime-install-policy-denied-0001", + "evidenceRefs": [ + "urn:srcos:evidence:runtime-install-policy-denial-0001" + ], + "startedAt": "2026-05-06T06:00:00Z", + "finishedAt": "2026-05-06T06:00:05Z", + "capturedAt": "2026-05-06T06:00:10Z" +} diff --git a/examples/runtime-install-receipt.failed.json b/examples/runtime-install-receipt.failed.json new file mode 100644 index 0000000..d555a38 --- /dev/null +++ b/examples/runtime-install-receipt.failed.json @@ -0,0 +1,42 @@ +{ + "id": "urn:srcos:receipt:runtime-install:agent-runtime-failed-artifact-0001", + "type": "RuntimeInstallReceipt", + "specVersion": "0.1.0", + "sessionRef": "urn:srcos:session:runtime-install-demo-0002", + "capabilityLedgerRef": "urn:srcos:capability-ledger:runtime-install-demo-0002", + "agentMachineReceiptRef": null, + "runtimeRef": "urn:srcos:runtime:sourceos-agent-runtime@0.1.0", + "runtimeName": "sourceos-agent-runtime", + "runtimeVersion": "0.1.0", + "targetRef": "urn:srcos:target:developer-workstation-0001", + "platform": "linux-x64", + "installState": "failed", + "manifest": { + "manifestRef": "urn:srcos:artifact:runtime-manifest-sourceos-agent-runtime-0.1.0", + "manifestDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "bundleFormatVersion": "1", + "resolvedAt": "2026-05-06T05:00:00Z" + }, + "artifacts": [ + { + "artifactRef": "urn:srcos:artifact:sourceos-agent-runtime-linux-x64-0.1.0.tar.xz", + "digest": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sizeBytes": 1048576, + "verificationState": "failed" + } + ], + "rollbackRef": null, + "failureReason": "artifact_digest_verification_failed", + "logMode": "compact_receipt_ref", + "causalRefs": [ + "urn:srcos:event:runtime-install-requested-0002" + ], + "policyDecisionRef": "urn:srcos:decision:runtime-install-policy-0002", + "evidenceRefs": [ + "urn:srcos:evidence:runtime-install-artifact-verification-failed-0001", + "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ], + "startedAt": "2026-05-06T05:00:00Z", + "finishedAt": "2026-05-06T05:01:00Z", + "capturedAt": "2026-05-06T05:01:05Z" +} diff --git a/examples/runtime-install-receipt.installed.json b/examples/runtime-install-receipt.installed.json new file mode 100644 index 0000000..0097a91 --- /dev/null +++ b/examples/runtime-install-receipt.installed.json @@ -0,0 +1,43 @@ +{ + "id": "urn:srcos:receipt:runtime-install:agent-runtime-installed-0001", + "type": "RuntimeInstallReceipt", + "specVersion": "0.1.0", + "sessionRef": "urn:srcos:session:runtime-install-demo-0001", + "capabilityLedgerRef": "urn:srcos:capability-ledger:runtime-install-demo-0001", + "agentMachineReceiptRef": "urn:srcos:agent-machine-receipt:runtime-probe-0001", + "runtimeRef": "urn:srcos:runtime:sourceos-agent-runtime@0.1.0", + "runtimeName": "sourceos-agent-runtime", + "runtimeVersion": "0.1.0", + "targetRef": "urn:srcos:target:developer-workstation-0001", + "platform": "linux-x64", + "installState": "installed", + "manifest": { + "manifestRef": "urn:srcos:artifact:runtime-manifest-sourceos-agent-runtime-0.1.0", + "manifestDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "bundleFormatVersion": "1", + "resolvedAt": "2026-05-06T04:34:00Z" + }, + "artifacts": [ + { + "artifactRef": "urn:srcos:artifact:sourceos-agent-runtime-linux-x64-0.1.0.tar.xz", + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "sizeBytes": 1048576, + "verificationState": "verified" + } + ], + "rollbackRef": null, + "failureReason": null, + "logMode": "compact_receipt_ref", + "causalRefs": [ + "urn:srcos:event:runtime-install-requested-0001", + "urn:srcos:capability-ledger:runtime-install-demo-0001" + ], + "policyDecisionRef": "urn:srcos:decision:runtime-install-policy-0001", + "evidenceRefs": [ + "urn:srcos:evidence:runtime-install-verified-0001", + "sha256:3333333333333333333333333333333333333333333333333333333333333333" + ], + "startedAt": "2026-05-06T04:34:00Z", + "finishedAt": "2026-05-06T04:35:00Z", + "capturedAt": "2026-05-06T04:35:05Z" +} diff --git a/examples/runtime-install-receipt.partial.json b/examples/runtime-install-receipt.partial.json new file mode 100644 index 0000000..729ae2a --- /dev/null +++ b/examples/runtime-install-receipt.partial.json @@ -0,0 +1,48 @@ +{ + "id": "urn:srcos:receipt:runtime-install:agent-runtime-partial-0001", + "type": "RuntimeInstallReceipt", + "specVersion": "0.1.0", + "sessionRef": "urn:srcos:session:runtime-install-demo-0004", + "capabilityLedgerRef": "urn:srcos:capability-ledger:runtime-install-demo-0004", + "agentMachineReceiptRef": null, + "runtimeRef": "urn:srcos:runtime:sourceos-agent-runtime@0.1.0", + "runtimeName": "sourceos-agent-runtime", + "runtimeVersion": "0.1.0", + "targetRef": "urn:srcos:target:developer-workstation-0001", + "platform": "linux-x64", + "installState": "partial", + "manifest": { + "manifestRef": "urn:srcos:artifact:runtime-manifest-sourceos-agent-runtime-0.1.0", + "manifestDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "bundleFormatVersion": "1", + "resolvedAt": "2026-05-06T07:00:00Z" + }, + "artifacts": [ + { + "artifactRef": "urn:srcos:artifact:sourceos-agent-runtime-linux-x64-0.1.0.tar.xz", + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "sizeBytes": 1048576, + "verificationState": "verified" + }, + { + "artifactRef": "urn:srcos:artifact:sourceos-agent-runtime-extensions-linux-x64-0.1.0.tar.xz", + "digest": "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "sizeBytes": null, + "verificationState": "failed" + } + ], + "rollbackRef": null, + "failureReason": "extension_artifact_verification_failed_core_installed", + "logMode": "compact_receipt_ref", + "causalRefs": [ + "urn:srcos:event:runtime-install-requested-0004" + ], + "policyDecisionRef": "urn:srcos:decision:runtime-install-policy-0004", + "evidenceRefs": [ + "urn:srcos:evidence:runtime-install-partial-state-0001", + "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" + ], + "startedAt": "2026-05-06T07:00:00Z", + "finishedAt": "2026-05-06T07:05:00Z", + "capturedAt": "2026-05-06T07:05:10Z" +} diff --git a/examples/runtime-install-receipt.rolled_back.json b/examples/runtime-install-receipt.rolled_back.json new file mode 100644 index 0000000..2bed838 --- /dev/null +++ b/examples/runtime-install-receipt.rolled_back.json @@ -0,0 +1,43 @@ +{ + "id": "urn:srcos:receipt:runtime-install:agent-runtime-rolled-back-0001", + "type": "RuntimeInstallReceipt", + "specVersion": "0.1.0", + "sessionRef": "urn:srcos:session:runtime-install-demo-0005", + "capabilityLedgerRef": "urn:srcos:capability-ledger:runtime-install-demo-0005", + "agentMachineReceiptRef": "urn:srcos:agent-machine-receipt:runtime-probe-0005", + "runtimeRef": "urn:srcos:runtime:sourceos-agent-runtime@0.1.0", + "runtimeName": "sourceos-agent-runtime", + "runtimeVersion": "0.1.0", + "targetRef": "urn:srcos:target:developer-workstation-0001", + "platform": "linux-x64", + "installState": "rolled_back", + "manifest": { + "manifestRef": "urn:srcos:artifact:runtime-manifest-sourceos-agent-runtime-0.1.0", + "manifestDigest": "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "bundleFormatVersion": "1", + "resolvedAt": "2026-05-06T08:00:00Z" + }, + "artifacts": [ + { + "artifactRef": "urn:srcos:artifact:sourceos-agent-runtime-linux-x64-0.1.0.tar.xz", + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "sizeBytes": 1048576, + "verificationState": "verified" + } + ], + "rollbackRef": "urn:srcos:receipt:runtime-install:agent-runtime-installed-0000", + "failureReason": "post_install_health_check_failed_rollback_executed", + "logMode": "compact_receipt_ref", + "causalRefs": [ + "urn:srcos:event:runtime-install-requested-0005", + "urn:srcos:event:runtime-health-check-failed-0005" + ], + "policyDecisionRef": "urn:srcos:decision:runtime-install-policy-0005", + "evidenceRefs": [ + "urn:srcos:evidence:runtime-install-rollback-executed-0001", + "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + ], + "startedAt": "2026-05-06T08:00:00Z", + "finishedAt": "2026-05-06T08:10:00Z", + "capturedAt": "2026-05-06T08:10:05Z" +} diff --git a/scripts/validate-runtime-install-receipts.py b/scripts/validate-runtime-install-receipts.py new file mode 100644 index 0000000..3164049 --- /dev/null +++ b/scripts/validate-runtime-install-receipts.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Validate RuntimeInstallReceipt examples and semantic consistency.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +SRC_ROOT = REPO_ROOT / "src" +if str(SRC_ROOT) not in sys.path: + sys.path.insert(0, str(SRC_ROOT)) + +from agent_machine.runtime_install_receipt import ( # noqa: E402 + build_receipt, + emit_compact_log, + validate_receipt, + validate_receipt_file, +) + +EXAMPLE_FILES = [ + REPO_ROOT / "examples" / "runtime-install-receipt.installed.json", + REPO_ROOT / "examples" / "runtime-install-receipt.failed.json", + REPO_ROOT / "examples" / "runtime-install-receipt.denied.json", + REPO_ROOT / "examples" / "runtime-install-receipt.partial.json", + REPO_ROOT / "examples" / "runtime-install-receipt.rolled_back.json", + REPO_ROOT / "examples" / "runtime-install-receipt.deferred.json", +] + + +def validate_examples() -> None: + for path in EXAMPLE_FILES: + validate_receipt_file(path, REPO_ROOT) + print(f"VALID RuntimeInstallReceipt example {path.relative_to(REPO_ROOT)}") + + +def validate_compact_log_output() -> None: + """Confirm emit_compact_log produces a single-line compact reference string.""" + receipt = build_receipt( + receipt_id="urn:srcos:receipt:runtime-install:smoke-test-0001", + session_ref="urn:srcos:session:smoke-0001", + capability_ledger_ref="urn:srcos:capability-ledger:smoke-0001", + runtime_ref="urn:srcos:runtime:smoke-runtime@0.0.1", + target_ref="urn:srcos:target:smoke-target-0001", + platform="linux-x64", + install_state="installed", + manifest_ref="urn:srcos:artifact:smoke-manifest", + manifest_digest="sha256:1111111111111111111111111111111111111111111111111111111111111111", + manifest_resolved_at="2026-05-06T10:00:00Z", + artifacts=[ + { + "artifactRef": "urn:srcos:artifact:smoke-runtime-bin", + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "sizeBytes": 4096, + "verificationState": "verified", + } + ], + policy_decision_ref="urn:srcos:decision:smoke-policy-0001", + evidence_refs=["urn:srcos:evidence:smoke-evidence-0001"], + started_at="2026-05-06T10:00:00Z", + captured_at="2026-05-06T10:00:05Z", + finished_at="2026-05-06T10:00:04Z", + ) + validate_receipt(receipt, source="", root=REPO_ROOT) + log_line = emit_compact_log(receipt) + if "\n" in log_line: + raise AssertionError("emit_compact_log must produce a single-line string") + if "urn:srcos:receipt:runtime-install:smoke-test-0001" not in log_line: + raise AssertionError(f"compact log must include receipt id, got: {log_line!r}") + if "installed" not in log_line: + raise AssertionError(f"compact log must include installState, got: {log_line!r}") + print(f"VALID compact log output: {log_line}") + + +def validate_failure_states() -> None: + """Confirm that failure states require failureReason and missing states are rejected.""" + base = dict( + receipt_id="urn:srcos:receipt:runtime-install:semantics-test-0001", + session_ref="urn:srcos:session:semantics-0001", + capability_ledger_ref="urn:srcos:capability-ledger:semantics-0001", + runtime_ref="urn:srcos:runtime:semantics-runtime@0.0.1", + target_ref="urn:srcos:target:semantics-target-0001", + platform="linux-x64", + manifest_ref="urn:srcos:artifact:semantics-manifest", + manifest_digest="sha256:1111111111111111111111111111111111111111111111111111111111111111", + manifest_resolved_at="2026-05-06T10:00:00Z", + artifacts=[ + { + "artifactRef": "urn:srcos:artifact:semantics-runtime-bin", + "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222", + "verificationState": "not_checked", + } + ], + policy_decision_ref="urn:srcos:decision:semantics-policy-0001", + evidence_refs=["urn:srcos:evidence:semantics-evidence-0001"], + started_at="2026-05-06T10:00:00Z", + captured_at="2026-05-06T10:00:05Z", + ) + + # failure states must have failureReason + for bad_state in ("failed", "partial", "denied", "deferred"): + receipt = build_receipt(**base, install_state=bad_state, failure_reason=None) + try: + validate_receipt(receipt, root=REPO_ROOT) + raise AssertionError(f"Expected semantic error for installState={bad_state!r} without failureReason") + except AssertionError as exc: + if "failureReason" not in str(exc): + raise + print(f"VALID semantic rejection: installState={bad_state!r} without failureReason") + + # rolled_back must have rollbackRef + receipt = build_receipt(**base, install_state="rolled_back", failure_reason="health_check_failed", rollback_ref=None) + try: + validate_receipt(receipt, root=REPO_ROOT) + raise AssertionError("Expected semantic error for installState=rolled_back without rollbackRef") + except AssertionError as exc: + if "rollbackRef" not in str(exc): + raise + print("VALID semantic rejection: installState=rolled_back without rollbackRef") + + # installed must not carry failureReason + receipt = build_receipt(**base, install_state="installed", failure_reason="should_not_be_here") + try: + validate_receipt(receipt, root=REPO_ROOT) + raise AssertionError("Expected semantic error for installState=installed with failureReason") + except AssertionError as exc: + if "failureReason" not in str(exc): + raise + print("VALID semantic rejection: installState=installed with failureReason") + + # empty evidenceRefs must be rejected + bad_receipt = build_receipt(**{**base, "evidence_refs": []}, install_state="installed") + try: + validate_receipt(bad_receipt, root=REPO_ROOT) + raise AssertionError("Expected schema/semantic error for empty evidenceRefs") + except AssertionError: + pass + print("VALID semantic rejection: empty evidenceRefs") + + +def main() -> int: + validate_examples() + validate_compact_log_output() + validate_failure_states() + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (AssertionError, RuntimeError, ValueError) as exc: + print(str(exc), file=sys.stderr) + raise SystemExit(1) from exc diff --git a/src/agent_machine/contracts.py b/src/agent_machine/contracts.py index 3caa256..75a3cc2 100644 --- a/src/agent_machine/contracts.py +++ b/src/agent_machine/contracts.py @@ -56,6 +56,7 @@ def schema_by_kind(root: Path | None = None) -> dict[str, Path]: "InferenceProvider": base / "inference-provider.schema.json", "PolicyAdmission": base / "policy-admission.schema.json", "ReleaseEvidenceBundle": base / "release-evidence-bundle.schema.json", + "RuntimeInstallReceipt": base / "runtime-install-receipt.schema.json", "SignedReleaseBundleEnvelope": base / "signed-release-bundle-envelope.schema.json", "StorageReceipt": base / "storage-receipt.schema.json", } @@ -93,9 +94,9 @@ def validate_by_kind(instance_path: Path, root: Path | None = None) -> Path: instance = load_json(instance_path) if not isinstance(instance, dict): raise AssertionError(f"{instance_path}: example root must be a JSON object") - kind = instance.get("kind") + kind = instance.get("kind") or instance.get("type") if not isinstance(kind, str): - raise AssertionError(f"{instance_path}: missing string `kind` field") + raise AssertionError(f"{instance_path}: missing string `kind` or `type` field") mapping = schema_by_kind(root) schema_path = mapping.get(kind) if schema_path is None: diff --git a/src/agent_machine/runtime_install_receipt.py b/src/agent_machine/runtime_install_receipt.py new file mode 100644 index 0000000..dfc4120 --- /dev/null +++ b/src/agent_machine/runtime_install_receipt.py @@ -0,0 +1,229 @@ +"""RuntimeInstallReceipt emission and validation for Agent Machine install/update flows. + +This module builds, validates, and logs compact RuntimeInstallReceipt records for +runtime installation lifecycle transitions. Full manifests and detailed artifact +payloads must be stored in evidence bundles; ordinary logs emit only compact +receipt ids and evidence references (logMode: compact_receipt_ref). +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + +from agent_machine.contracts import load_json, schema_by_kind, validate_instance + +INSTALL_STATES = { + "requested", + "manifest_resolved", + "artifact_verified", + "installing", + "installed", + "failed", + "rolled_back", + "partial", + "denied", + "deferred", +} + +VERIFICATION_STATES = {"not_checked", "verified", "failed", "skipped"} + +PLATFORMS = {"darwin-arm64", "darwin-x64", "linux-x64", "linux-arm64", "win32-x64"} + +LOG_MODES = {"compact_receipt_ref", "full_debug_redacted"} + +_TERMINAL_STATES = {"installed", "failed", "rolled_back", "partial", "denied", "deferred"} +_FAILURE_STATES = {"failed", "partial", "denied", "deferred"} + + +def receipt_schema_path(root: Path | None = None) -> Path: + return schema_by_kind(root)["RuntimeInstallReceipt"] + + +def validate_receipt_schema(receipt: dict[str, Any], root: Path | None = None) -> None: + """Validate a RuntimeInstallReceipt dict against the JSON Schema.""" + schema = load_json(receipt_schema_path(root)) + try: + from jsonschema.validators import validator_for + except ImportError as exc: # pragma: no cover + raise RuntimeError( + "Missing dependency: jsonschema. Install with `python -m pip install -r requirements-dev.txt`." + ) from exc + validator_cls = validator_for(schema) + validator_cls.check_schema(schema) + validator = validator_cls(schema) + errors = sorted(validator.iter_errors(receipt), key=lambda err: list(err.path)) + if errors: + rendered = [] + for err in errors: + location = "/".join(str(part) for part in err.path) or "" + rendered.append(f" - {location}: {err.message}") + raise AssertionError("RuntimeInstallReceipt failed schema validation:\n" + "\n".join(rendered)) + + +def validate_receipt_semantics(receipt: dict[str, Any], source: str = "") -> None: + """Validate cross-field semantic invariants for a RuntimeInstallReceipt.""" + install_state = receipt.get("installState") + + # failure_reason must be present for failure/denial/deferral/partial states + failure_reason = receipt.get("failureReason") + if install_state in _FAILURE_STATES and not failure_reason: + raise AssertionError( + f"{source}: installState={install_state!r} requires a non-empty failureReason" + ) + if install_state == "installed" and failure_reason is not None: + raise AssertionError( + f"{source}: installState=installed must not carry failureReason" + ) + + # rollback_ref should be present when rolled_back + rollback_ref = receipt.get("rollbackRef") + if install_state == "rolled_back" and not rollback_ref: + raise AssertionError( + f"{source}: installState=rolled_back requires a non-null rollbackRef" + ) + + # logMode default is compact_receipt_ref + log_mode = receipt.get("logMode") + if log_mode not in LOG_MODES: + raise AssertionError(f"{source}: logMode must be one of {sorted(LOG_MODES)!r}, got {log_mode!r}") + + # evidenceRefs must be non-empty + evidence_refs = receipt.get("evidenceRefs") + if not isinstance(evidence_refs, list) or not evidence_refs: + raise AssertionError(f"{source}: evidenceRefs must be a non-empty list") + + +def validate_receipt(receipt: dict[str, Any], source: str = "", root: Path | None = None) -> None: + """Run both schema and semantic validation on a RuntimeInstallReceipt dict.""" + validate_receipt_schema(receipt, root) + validate_receipt_semantics(receipt, source) + + +def build_receipt( + *, + receipt_id: str, + session_ref: str, + capability_ledger_ref: str, + runtime_ref: str, + target_ref: str, + platform: str, + install_state: str, + manifest_ref: str, + manifest_digest: str, + manifest_resolved_at: str, + artifacts: list[dict[str, Any]], + policy_decision_ref: str, + evidence_refs: list[str], + started_at: str, + captured_at: str, + spec_version: str = "0.1.0", + agent_machine_receipt_ref: str | None = None, + runtime_name: str | None = None, + runtime_version: str | None = None, + manifest_bundle_format_version: str | int | None = None, + rollback_ref: str | None = None, + failure_reason: str | None = None, + log_mode: str = "compact_receipt_ref", + causal_refs: list[str] | None = None, + finished_at: str | None = None, +) -> dict[str, Any]: + """Construct a RuntimeInstallReceipt record. + + All full manifests and artifact payloads belong in evidence bundles; this + record carries only compact receipt ids, digests, and evidence references. + """ + if install_state not in INSTALL_STATES: + raise ValueError(f"Unknown installState {install_state!r}; must be one of {sorted(INSTALL_STATES)}") + if platform not in PLATFORMS: + raise ValueError(f"Unknown platform {platform!r}; must be one of {sorted(PLATFORMS)}") + if log_mode not in LOG_MODES: + raise ValueError(f"Unknown logMode {log_mode!r}; must be one of {sorted(LOG_MODES)}") + + manifest: dict[str, Any] = { + "manifestRef": manifest_ref, + "manifestDigest": manifest_digest, + "resolvedAt": manifest_resolved_at, + } + if manifest_bundle_format_version is not None: + manifest["bundleFormatVersion"] = manifest_bundle_format_version + + receipt: dict[str, Any] = { + "id": receipt_id, + "type": "RuntimeInstallReceipt", + "specVersion": spec_version, + "sessionRef": session_ref, + "capabilityLedgerRef": capability_ledger_ref, + "agentMachineReceiptRef": agent_machine_receipt_ref, + "runtimeRef": runtime_ref, + "runtimeName": runtime_name, + "runtimeVersion": runtime_version, + "targetRef": target_ref, + "platform": platform, + "installState": install_state, + "manifest": manifest, + "artifacts": artifacts, + "rollbackRef": rollback_ref, + "failureReason": failure_reason, + "logMode": log_mode, + "causalRefs": causal_refs or [], + "policyDecisionRef": policy_decision_ref, + "evidenceRefs": evidence_refs, + "startedAt": started_at, + "finishedAt": finished_at, + "capturedAt": captured_at, + } + return receipt + + +def emit_compact_log(receipt: dict[str, Any]) -> str: + """Return a compact one-line log string for operational logs. + + Full manifests and artifact payloads remain in evidence bundles. + """ + receipt_id = receipt.get("id", "") + install_state = receipt.get("installState", "") + evidence_refs = receipt.get("evidenceRefs") or [] + evidence_summary = evidence_refs[0] if evidence_refs else "" + return ( + f"RuntimeInstallReceipt id={receipt_id} state={install_state} evidence[0]={evidence_summary}" + ) + + +def validate_receipt_file(path: Path, root: Path | None = None) -> None: + """Load and validate a RuntimeInstallReceipt JSON file.""" + receipt = load_json(path) + if not isinstance(receipt, dict): + raise AssertionError(f"{path}: root must be a JSON object") + source = str(path) + validate_receipt(receipt, source=source, root=root) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Validate or emit RuntimeInstallReceipt records") + subcommands = parser.add_subparsers(dest="command", required=True) + + validate = subcommands.add_parser("validate", help="Validate a RuntimeInstallReceipt JSON file") + validate.add_argument("receipt_json", type=Path) + + return parser.parse_args() + + +def main() -> int: + args = parse_args() + if args.command == "validate": + validate_receipt_file(args.receipt_json) + print(f"VALID RuntimeInstallReceipt {args.receipt_json}") + return 0 + raise AssertionError(f"unsupported command: {args.command}") + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (AssertionError, RuntimeError, ValueError) as exc: + print(str(exc), file=sys.stderr) + raise SystemExit(1) from exc