From 5418ed16b155fb0e5c4fa3f389c1b2c2d4611d1e Mon Sep 17 00:00:00 2001 From: Carlos Hernandez Date: Wed, 17 Jun 2026 11:42:45 +0200 Subject: [PATCH 1/4] feat(industrial-embodied-ai): controller-signed execution receipts (prep for #301 example) Add the example-side half of the #301 follow-up: the independent controller can now sign an execution receipt for its decision in the cMCP external_execution_evidence format (issuer, issuer_key_id, signature, evidence_hash, evidence_type, linked_call_id), using a deterministic development-only Ed25519 key so committed evidence stays reproducible. - controller.py: receipt-signing key plus sign_execution_receipt(), receipt_key_id, receipt_public_key_b64. - tests: verify the receipt against cMCP's checks (linked_call_id binding and Ed25519 over the canonical receipt), tampered-fails, deterministic key. This is the standalone part that does not depend on the merged runtime. Held on this branch until cmcp#301 and cmcp#302 merge. Post-merge TODO (do not run until the cmcp PRs are merged): 1. Re-pin requirements.txt to the merged cmcp #301/#302 commits (and trace-spec). 2. #301: wire the controller receipt into the safety-reject path per the agreed proxy transport, then run the live stack and regenerate trace-output/example-* so the safety-reject audit entry carries external_execution_evidence; verify with cmcp_verify external_evidence_keys = {receipt_key_id: controller pubkey}. 3. #302: add the agent_manifest section (path, trust_anchor_path, authenticated_subject) to cmcp-config.yaml, regenerate evidence so the TRACE record carries gateway.agent_identity; verify with cmcp verify --agent-manifest. 4. Update the README evidence-boundaries table for both bindings. --- industrial-embodied-ai/controller.py | 43 +++++++++ .../tests/test_execution_receipt.py | 96 +++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 industrial-embodied-ai/tests/test_execution_receipt.py diff --git a/industrial-embodied-ai/controller.py b/industrial-embodied-ai/controller.py index be5ff63..d8bccfd 100644 --- a/industrial-embodied-ai/controller.py +++ b/industrial-embodied-ai/controller.py @@ -14,6 +14,9 @@ import time from typing import Any, Callable +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + CONTROLLER_ID = "spiffe://factory.example/controller/robot-cell-7" MAX_STATE_AGE_MS = 5_000 @@ -61,6 +64,15 @@ def __init__( self._token_key = token_key or secrets.token_bytes(32) self._sequence = 0 self._consumed_sequences: set[int] = set() + # #301: deterministic development-only Ed25519 key the controller uses to + # sign execution receipts (external_execution_evidence). Fixed seed so the + # public key and any committed evidence are reproducible. Not for production. + self._receipt_key = Ed25519PrivateKey.from_private_bytes( + hashlib.sha256(b"development-only-mock-controller-receipt-key").digest() + ) + self._receipt_pub = self._receipt_key.public_key().public_bytes( + encoding=Encoding.Raw, format=PublicFormat.Raw + ) self._state = { "operating_mode": "automatic", "emergency_stop_active": False, @@ -157,3 +169,34 @@ def request_motion(self, request: dict[str, Any]) -> dict[str, Any]: "controller_decision": "accepted", "execution_status": "completed", } + + @property + def receipt_key_id(self) -> str: + """Stable id for the receipt-signing public key (sha256 hex of the key).""" + return hashlib.sha256(self._receipt_pub).hexdigest() + + @property + def receipt_public_key_b64(self) -> str: + """base64url of the raw Ed25519 receipt-signing public key.""" + return _b64url_encode(self._receipt_pub) + + def sign_execution_receipt( + self, *, call_id: str, decision: dict[str, Any] + ) -> dict[str, str]: + """Return a controller-signed execution receipt for a decision. + + Matches the cMCP external_execution_evidence format (issue #301): the + signature is Ed25519 over the canonical receipt with the signature field + absent, and a verifier checks linked_call_id against the audit entry + call_id. This attests what the independent controller decided. It is not + a claim that a physical action was safe, accepted, or certified. + """ + receipt = { + "issuer": CONTROLLER_ID, + "issuer_key_id": self.receipt_key_id, + "evidence_hash": "sha256:" + hashlib.sha256(canonical_bytes(decision)).hexdigest(), + "evidence_type": "controller-execution-receipt/v1", + "linked_call_id": call_id, + } + receipt["signature"] = _b64url_encode(self._receipt_key.sign(canonical_bytes(receipt))) + return receipt diff --git a/industrial-embodied-ai/tests/test_execution_receipt.py b/industrial-embodied-ai/tests/test_execution_receipt.py new file mode 100644 index 0000000..3643e77 --- /dev/null +++ b/industrial-embodied-ai/tests/test_execution_receipt.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import base64 +import json +import sys +import unittest +from pathlib import Path + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + + +EXAMPLE_DIR = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(EXAMPLE_DIR)) + +from controller import IndependentSafetyController # noqa: E402 + + +def _verify_like_cmcp(receipt: dict, public_key_b64: str, call_id: str) -> None: + """Reproduce cmcp_verify's external_execution_evidence check: the + linked_call_id binding, and an Ed25519 signature over the canonical receipt + with the signature field absent. Raises if the signature does not verify. + """ + if receipt["linked_call_id"] != call_id: + raise AssertionError("linked_call_id does not match the entry call_id") + pad = 4 - (len(public_key_b64) % 4) + pub = Ed25519PublicKey.from_public_bytes( + base64.urlsafe_b64decode(public_key_b64 + ("=" * pad if pad != 4 else "")) + ) + signing_input = json.dumps( + {k: v for k, v in receipt.items() if k != "signature"}, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=True, + ).encode() + sig_b64 = receipt["signature"] + pad = 4 - (len(sig_b64) % 4) + sig = base64.urlsafe_b64decode(sig_b64 + ("=" * pad if pad != 4 else "")) + pub.verify(sig, signing_input) + + +class ExecutionReceiptTests(unittest.TestCase): + def setUp(self) -> None: + self.controller = IndependentSafetyController( + token_key=b"unit-test-only-controller-key" + ) + + def test_receipt_has_required_fields(self) -> None: + receipt = self.controller.sign_execution_receipt( + call_id="c1", + decision={"controller_decision": "rejected", "reason": "human_detected"}, + ) + for field in ( + "issuer", + "issuer_key_id", + "signature", + "evidence_hash", + "evidence_type", + "linked_call_id", + ): + self.assertIn(field, receipt) + self.assertTrue(receipt["evidence_hash"].startswith("sha256:")) + self.assertEqual(receipt["evidence_type"], "controller-execution-receipt/v1") + + def test_receipt_verifies_like_cmcp(self) -> None: + receipt = self.controller.sign_execution_receipt( + call_id="c1", + decision={"controller_decision": "rejected", "reason": "human_detected"}, + ) + # Should not raise. + _verify_like_cmcp(receipt, self.controller.receipt_public_key_b64, "c1") + + def test_tampered_receipt_fails(self) -> None: + receipt = self.controller.sign_execution_receipt( + call_id="c1", decision={"controller_decision": "rejected"} + ) + receipt["evidence_hash"] = "sha256:" + "cd" * 32 # tamper after signing + with self.assertRaises(InvalidSignature): + _verify_like_cmcp(receipt, self.controller.receipt_public_key_b64, "c1") + + def test_linked_call_id_is_bound(self) -> None: + receipt = self.controller.sign_execution_receipt( + call_id="c1", decision={"controller_decision": "rejected"} + ) + with self.assertRaises(AssertionError): + _verify_like_cmcp(receipt, self.controller.receipt_public_key_b64, "other") + + def test_receipt_key_is_deterministic(self) -> None: + # Fixed dev seed: the receipt key id is reproducible across instances, + # so committed evidence stays stable. + other = IndependentSafetyController(token_key=b"different-token-key") + self.assertEqual(self.controller.receipt_key_id, other.receipt_key_id) + + +if __name__ == "__main__": + unittest.main() From 41c0d650611e2a35571e62e5775c9c979f5e9731 Mon Sep 17 00:00:00 2001 From: Carlos Hernandez Date: Wed, 17 Jun 2026 22:10:34 +0200 Subject: [PATCH 2/4] feat(industrial-embodied-ai): verify controller execution receipts --- industrial-embodied-ai/README.md | 31 ++++--- .../agent/material_movement_agent.py | 34 +++++++- .../controller-receipt-public-key.json | 7 ++ industrial-embodied-ai/controller.py | 14 +++- industrial-embodied-ai/requirements.txt | 2 +- .../server/mock_robot_controller.py | 10 +++ .../tests/test_execution_receipt.py | 82 +++++++++++++++---- industrial-embodied-ai/validate_artifacts.py | 23 +++++- 8 files changed, 172 insertions(+), 31 deletions(-) create mode 100644 industrial-embodied-ai/controller-receipt-public-key.json diff --git a/industrial-embodied-ai/README.md b/industrial-embodied-ai/README.md index e5b4c42..8fd61ea 100644 --- a/industrial-embodied-ai/README.md +++ b/industrial-embodied-ai/README.md @@ -8,6 +8,9 @@ The example also demonstrates evidence continuity: after the governed session closes, its saved TRACE Trust Record and audit bundle remain verifiable after the agent, cMCP Runtime and mock controller stop. This is continuity of evidence, not continuity of agent memory, reputation or process identity. +Live runs also attach a controller-signed `external_execution_evidence` +receipt to each motion decision when run with a cMCP Runtime that supports +issue #301 external evidence binding. The scenario is synthetic. It uses no robot hardware, vendor SDK, production endpoint, or proprietary industrial data. @@ -29,7 +32,10 @@ to produce durable evidence: 3. **Safety rejected:** cMCP authorizes the declared workflow, but the controller rejects motion after its current state reports a person in the safeguarded area. -4. **Closed-session evidence:** cMCP signs a TRACE Trust Record and audit +4. **Controller-signed execution receipts:** each motion decision includes a + mock controller receipt that cMCP can bind to the audit entry for that + `call_id`. +5. **Closed-session evidence:** cMCP signs a TRACE Trust Record and audit bundle that can be verified from the saved files without a running agent, runtime or controller. @@ -72,6 +78,7 @@ individually safe motion is not, by itself, a trusted one. | - validates fresh state token | | - rechecks current cell state | | - enforces speed and zone | + | - signs motion decisions | +---------------+---------------+ | | accepted command @@ -94,6 +101,7 @@ rather than a native runtime binding. | Governed tool access | cMCP intercepts each MCP request and evaluates the active Cedar policy before forwarding | | Scope over safety | cMCP denies an in-envelope motion that falls outside the declared workflow, a check the safety controller does not perform | | Physical authority | The independent controller rechecks current state and remains authoritative for simulated execution | +| External controller evidence | Motion outcomes can include controller-signed receipts bound to the cMCP audit call id | | Durable session evidence | TRACE and the signed audit bundle bind the cMCP session, policy, catalog and tool-call transcript | The example composes these boundaries without claiming that the developer @@ -128,9 +136,9 @@ Prerequisites: - Python 3.11 or newer - Git -The project is a developer preview. `requirements.txt` pins the cMCP and TRACE -commits used for this reproducible example until the summit release stack is -available from PyPI. +The project is a developer preview. `requirements.txt` pins the TRACE commit +and a cMCP external-evidence PR commit used for this reproducible example +until the summit release stack is available from PyPI. ```bash cd industrial-embodied-ai @@ -182,6 +190,7 @@ SAFETY REJECT TRACE VERIFICATION schema/signature/hashes/freshness: verified audit bundle: verified + controller receipts: verified (2) runtime platform: software-only hardware attestation: not verified (development mode) ``` @@ -248,6 +257,7 @@ The validator checks: - Agent Manifest artifact bindings and Ed25519 signature - runtime-issued TRACE schema and signature - signed audit-bundle integrity and binding to the TRACE record +- controller execution receipts when present in the audit bundle ## Evidence boundaries @@ -256,15 +266,15 @@ The validator checks: | Agent Manifest | The signed agent identity declaration and hashes of the approved prompt, policy and tools | That cMCP loaded the manifest or bound its agent identity to the runtime session | | cMCP decision | The active policy authorized or denied a cataloged tool request | That an authorized physical request was safe | | Controller accept | The motion was inside the safety envelope for this run | That the action was authorized, in declared scope, or issued by the reviewed agent | +| Controller execution receipt | The mock controller signed a specific accepted or rejected motion decision and linked it to a cMCP audit `call_id` | Hardware-backed execution, physical completion, or functional-safety compliance | | TRACE Trust Record | cMCP session identity, runtime, policy hash, catalog hash and tool-call transcript integrity | The Agent Manifest identity, controller acceptance, physical completion or functional-safety compliance | | Saved TRACE and audit files | The closed session can be checked after the processes stop | Continuity of agent memory, reputation or logical identity across a restart or replacement | -| Client-observed controller response | The mock controller's decision returned to the agent during this run | A signed, hardware-backed or independently retained execution record | -The current cMCP audit bundle records request hashes and authorization -decisions, but does not populate a response hash for the controller outcome. -The example therefore does not claim that TRACE proves controller acceptance -or physical completion. Binding independent controller evidence is a -follow-up design question, not something this example silently invents. +When run against a cMCP build with issue #301 support, the audit bundle records +the response hash and can bind the mock controller's signed receipt for each +motion decision. The receipt proves the mock controller signed a decision for +that call id. It does not claim that TRACE proves physical completion, +hardware-backed execution, or compliance with industrial safety standards. The committed TRACE subject identifies the cMCP session, while the Agent Manifest declares a separate agent identity. The validator confirms that the @@ -280,6 +290,7 @@ agent. | `agent-manifest.json` | Signed development declaration binding the prompt, policy bundle and tool catalog | | `manifest-public-key.json` | Public verification key for the Agent Manifest | | `artifact-hashes.json` | Approved cMCP and Agent Manifest artifact hashes | +| `controller-receipt-public-key.json` | Development-only public key for verifying mock controller receipts | | `catalog.json` | Attested definitions for safety-state and motion-request tools | | `cmcp-config.yaml` | cMCP configuration shared by development and hardware runs | | `policy/allow.cedar` | Explicit workflow-scoped permits with default deny | diff --git a/industrial-embodied-ai/agent/material_movement_agent.py b/industrial-embodied-ai/agent/material_movement_agent.py index 3fa6c35..9e8a4ba 100644 --- a/industrial-embodied-ai/agent/material_movement_agent.py +++ b/industrial-embodied-ai/agent/material_movement_agent.py @@ -4,6 +4,7 @@ from __future__ import annotations import argparse +import base64 import json import os from pathlib import Path @@ -19,6 +20,18 @@ WORKFLOW_ID = "industrial-material-movement" +def _b64url_decode(value: str) -> bytes: + return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) + + +def _external_evidence_keys() -> dict[str, bytes]: + key_doc = json.loads( + (EXAMPLE_DIR / "controller-receipt-public-key.json").read_text() + ) + key_id = key_doc["key_id"] + return {key_id: _b64url_decode(key_doc["public_key_base64url"])} + + def _headers() -> dict[str, str]: headers = {"Content-Type": "application/json"} if token := os.environ.get("CMCP_BEARER_TOKEN"): @@ -127,7 +140,11 @@ def _verify_evidence( tool_catalog_hash=expected["cmcp_catalog_hash"], ), ) - bundle_result = verify_audit_bundle(bundle, claim) + bundle_result = verify_audit_bundle( + bundle, + claim, + external_evidence_keys=_external_evidence_keys(), + ) required = { "schema", "signature", @@ -139,8 +156,18 @@ def _verify_evidence( missing = sorted(required - set(result.verified_fields)) print("TRACE VERIFICATION") - print(f" schema/signature/hashes/freshness: {'verified' if not missing else 'failed'}") + schema_status = "verified" if not missing else "failed" + print(f" schema/signature/hashes/freshness: {schema_status}") print(f" audit bundle: {'verified' if bundle_result.verified else 'failed'}") + receipt_count = sum( + 1 + for entry in bundle.get("entries", []) + if entry.get("external_execution_evidence") + ) + if receipt_count: + print(f" controller receipts: verified ({receipt_count})") + else: + print(" controller receipts: not present in this bundle") platform = claim.get("trace", {}).get("runtime", {}).get("platform", "unknown") print(f" runtime platform: {platform}") if platform == "software-only": @@ -149,7 +176,8 @@ def _verify_evidence( missing.append("hardware_attestation") else: hardware_verified = "hardware_attestation" in result.verified_fields - print(f" hardware attestation: {'verified' if hardware_verified else 'failed'}") + hardware_status = "verified" if hardware_verified else "failed" + print(f" hardware attestation: {hardware_status}") if require_hardware and not hardware_verified: missing.append("hardware_attestation") diff --git a/industrial-embodied-ai/controller-receipt-public-key.json b/industrial-embodied-ai/controller-receipt-public-key.json new file mode 100644 index 0000000..7891b7a --- /dev/null +++ b/industrial-embodied-ai/controller-receipt-public-key.json @@ -0,0 +1,7 @@ +{ + "key_id": "c1b47b97b024ed8ef89572fb0ed92a15fdd48496d6138ec260e018bd345d3d2d", + "public_key_base64url": "eOBimyX_-wLLvWhQ3Jl2KnRULU8ZU-vK0z7eAn2gNoo", + "algorithm": "Ed25519", + "issuer": "spiffe://factory.example/controller/robot-cell-7", + "usage": "development-only external_execution_evidence verification key" +} diff --git a/industrial-embodied-ai/controller.py b/industrial-embodied-ai/controller.py index d8bccfd..6c8413c 100644 --- a/industrial-embodied-ai/controller.py +++ b/industrial-embodied-ai/controller.py @@ -180,6 +180,11 @@ def receipt_public_key_b64(self) -> str: """base64url of the raw Ed25519 receipt-signing public key.""" return _b64url_encode(self._receipt_pub) + @property + def receipt_public_key_bytes(self) -> bytes: + """Raw Ed25519 receipt-signing public key bytes for cMCP verification.""" + return self._receipt_pub + def sign_execution_receipt( self, *, call_id: str, decision: dict[str, Any] ) -> dict[str, str]: @@ -194,9 +199,14 @@ def sign_execution_receipt( receipt = { "issuer": CONTROLLER_ID, "issuer_key_id": self.receipt_key_id, - "evidence_hash": "sha256:" + hashlib.sha256(canonical_bytes(decision)).hexdigest(), + "evidence_hash": ( + "sha256:" + + hashlib.sha256(canonical_bytes(decision)).hexdigest() + ), "evidence_type": "controller-execution-receipt/v1", "linked_call_id": call_id, } - receipt["signature"] = _b64url_encode(self._receipt_key.sign(canonical_bytes(receipt))) + receipt["signature"] = _b64url_encode( + self._receipt_key.sign(canonical_bytes(receipt)) + ) return receipt diff --git a/industrial-embodied-ai/requirements.txt b/industrial-embodied-ai/requirements.txt index e89453c..68f4f6f 100644 --- a/industrial-embodied-ai/requirements.txt +++ b/industrial-embodied-ai/requirements.txt @@ -1,4 +1,4 @@ agentrust-trace @ git+https://github.com/agentrust-io/trace-spec.git@ae152e8baf6ba7c7443cdf73fb5d9ef1660526c0 -cmcp-runtime @ git+https://github.com/agentrust-io/cmcp.git@3a7f49fea5f0c97500ffe0c3c49ec2ff68d09555 +cmcp-runtime @ git+https://github.com/carloshvp/cmcp.git@29ef0379882a4d79505ffeeaf066498c43f5c35e cedarpy==4.8.4 httpx>=0.27,<1 diff --git a/industrial-embodied-ai/server/mock_robot_controller.py b/industrial-embodied-ai/server/mock_robot_controller.py index 2a0dec5..674ece6 100644 --- a/industrial-embodied-ai/server/mock_robot_controller.py +++ b/industrial-embodied-ai/server/mock_robot_controller.py @@ -57,6 +57,14 @@ def _request_motion(arguments: dict[str, Any]) -> dict[str, Any]: return result +def _with_execution_receipt(result: dict[str, Any], call_id: object) -> dict[str, Any]: + receipt = controller.sign_execution_receipt( + call_id=str(call_id), + decision=result, + ) + return {**result, "external_execution_evidence": receipt} + + TOOLS = { "cell.read_safety_state": _read_safety_state, "robot.request_motion": _request_motion, @@ -94,6 +102,8 @@ def do_POST(self) -> None: # noqa: N802 return result = handler(params.get("arguments", {})) + if tool_name == "robot.request_motion": + result = _with_execution_receipt(result, request.get("id")) self._reply( 200, { diff --git a/industrial-embodied-ai/tests/test_execution_receipt.py b/industrial-embodied-ai/tests/test_execution_receipt.py index 3643e77..332ffa6 100644 --- a/industrial-embodied-ai/tests/test_execution_receipt.py +++ b/industrial-embodied-ai/tests/test_execution_receipt.py @@ -1,6 +1,7 @@ from __future__ import annotations import base64 +import hashlib import json import sys import unittest @@ -14,29 +15,37 @@ sys.path.insert(0, str(EXAMPLE_DIR)) from controller import IndependentSafetyController # noqa: E402 +from server import mock_robot_controller # noqa: E402 -def _verify_like_cmcp(receipt: dict, public_key_b64: str, call_id: str) -> None: +def _b64url_decode(value: str) -> bytes: + return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) + + +def _verify_like_cmcp( + receipt: dict, + external_evidence_keys: dict[str, bytes], + call_id: str, +) -> None: """Reproduce cmcp_verify's external_execution_evidence check: the - linked_call_id binding, and an Ed25519 signature over the canonical receipt - with the signature field absent. Raises if the signature does not verify. + trusted key-id map, linked_call_id binding, and Ed25519 signature over the + canonical receipt with the signature field absent. Raises on verification + failure. """ if receipt["linked_call_id"] != call_id: raise AssertionError("linked_call_id does not match the entry call_id") - pad = 4 - (len(public_key_b64) % 4) - pub = Ed25519PublicKey.from_public_bytes( - base64.urlsafe_b64decode(public_key_b64 + ("=" * pad if pad != 4 else "")) - ) + key_id = receipt["issuer_key_id"] + public_key = external_evidence_keys[key_id] + if hashlib.sha256(public_key).hexdigest() != key_id: + raise AssertionError("issuer_key_id does not match trusted public key") + pub = Ed25519PublicKey.from_public_bytes(public_key) signing_input = json.dumps( {k: v for k, v in receipt.items() if k != "signature"}, sort_keys=True, separators=(",", ":"), ensure_ascii=True, ).encode() - sig_b64 = receipt["signature"] - pad = 4 - (len(sig_b64) % 4) - sig = base64.urlsafe_b64decode(sig_b64 + ("=" * pad if pad != 4 else "")) - pub.verify(sig, signing_input) + pub.verify(_b64url_decode(receipt["signature"]), signing_input) class ExecutionReceiptTests(unittest.TestCase): @@ -61,6 +70,10 @@ def test_receipt_has_required_fields(self) -> None: self.assertIn(field, receipt) self.assertTrue(receipt["evidence_hash"].startswith("sha256:")) self.assertEqual(receipt["evidence_type"], "controller-execution-receipt/v1") + self.assertEqual( + receipt["issuer_key_id"], + hashlib.sha256(self.controller.receipt_public_key_bytes).hexdigest(), + ) def test_receipt_verifies_like_cmcp(self) -> None: receipt = self.controller.sign_execution_receipt( @@ -68,7 +81,11 @@ def test_receipt_verifies_like_cmcp(self) -> None: decision={"controller_decision": "rejected", "reason": "human_detected"}, ) # Should not raise. - _verify_like_cmcp(receipt, self.controller.receipt_public_key_b64, "c1") + _verify_like_cmcp( + receipt, + {self.controller.receipt_key_id: self.controller.receipt_public_key_bytes}, + "c1", + ) def test_tampered_receipt_fails(self) -> None: receipt = self.controller.sign_execution_receipt( @@ -76,20 +93,57 @@ def test_tampered_receipt_fails(self) -> None: ) receipt["evidence_hash"] = "sha256:" + "cd" * 32 # tamper after signing with self.assertRaises(InvalidSignature): - _verify_like_cmcp(receipt, self.controller.receipt_public_key_b64, "c1") + _verify_like_cmcp( + receipt, + { + self.controller.receipt_key_id: ( + self.controller.receipt_public_key_bytes + ) + }, + "c1", + ) def test_linked_call_id_is_bound(self) -> None: receipt = self.controller.sign_execution_receipt( call_id="c1", decision={"controller_decision": "rejected"} ) with self.assertRaises(AssertionError): - _verify_like_cmcp(receipt, self.controller.receipt_public_key_b64, "other") + _verify_like_cmcp( + receipt, + { + self.controller.receipt_key_id: ( + self.controller.receipt_public_key_bytes + ) + }, + "other", + ) def test_receipt_key_is_deterministic(self) -> None: # Fixed dev seed: the receipt key id is reproducible across instances, # so committed evidence stays stable. other = IndependentSafetyController(token_key=b"different-token-key") self.assertEqual(self.controller.receipt_key_id, other.receipt_key_id) + self.assertEqual( + self.controller.receipt_public_key_b64, + "eOBimyX_-wLLvWhQ3Jl2KnRULU8ZU-vK0z7eAn2gNoo", + ) + + def test_mock_server_attaches_receipt_to_motion_decision(self) -> None: + result = mock_robot_controller._with_execution_receipt( + {"controller_decision": "rejected", "reason": "human_detected"}, + "call-42", + ) + receipt = result["external_execution_evidence"] + self.assertEqual(receipt["linked_call_id"], "call-42") + _verify_like_cmcp( + receipt, + { + mock_robot_controller.controller.receipt_key_id: ( + mock_robot_controller.controller.receipt_public_key_bytes + ) + }, + "call-42", + ) if __name__ == "__main__": diff --git a/industrial-embodied-ai/validate_artifacts.py b/industrial-embodied-ai/validate_artifacts.py index bcf4613..c335cc9 100644 --- a/industrial-embodied-ai/validate_artifacts.py +++ b/industrial-embodied-ai/validate_artifacts.py @@ -34,6 +34,14 @@ def b64url_decode(value: str) -> bytes: return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) +def external_evidence_keys() -> dict[str, bytes]: + key_doc = json.loads( + (BASE / "controller-receipt-public-key.json").read_text() + ) + key_id = key_doc["key_id"] + return {key_id: b64url_decode(key_doc["public_key_base64url"])} + + def compute_policy_bundle_hash() -> str: policy_dir = BASE / "policy" manifest = json.loads((policy_dir / "manifest.json").read_text()) @@ -160,12 +168,25 @@ def main() -> None: assert required <= set(verification.verified_fields) assert claim["trace"]["runtime"]["platform"] == "software-only" - bundle_verification = verify_audit_bundle(audit_bundle, claim) + bundle_verification = verify_audit_bundle( + audit_bundle, + claim, + external_evidence_keys=external_evidence_keys(), + ) assert bundle_verification.verified, bundle_verification.failures + receipt_count = sum( + 1 + for entry in audit_bundle.get("entries", []) + if entry.get("external_execution_evidence") + ) print("Configuration and artifact hashes: valid") print("Agent Manifest signature: valid") print("Runtime-issued TRACE signature and audit bundle: valid") + if receipt_count: + print(f"Controller execution receipts: valid ({receipt_count})") + else: + print("Controller execution receipts: not present in committed fixture") print("Hardware attestation: not present in committed development fixture") From 3b3b86d676bf72d7b8dea7b5212aed351eb5c298 Mon Sep 17 00:00:00 2001 From: Carlos Hernandez Date: Wed, 17 Jun 2026 22:32:14 +0200 Subject: [PATCH 3/4] ci: validate industrial embodied ai example --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 56c131c..bcbf66d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,3 +25,27 @@ jobs: find . -name "*.yaml" -o -name "*.yml" | grep -v ".git" | while read f; do python3 -c "import yaml, sys; yaml.safe_load(open(sys.argv[1]))" "$f" && echo "OK: $f" || exit 1 done + + industrial-embodied-ai: + runs-on: ubuntu-latest + defaults: + run: + working-directory: industrial-embodied-ai + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + cache-dependency-path: industrial-embodied-ai/requirements.txt + + - name: Install dependencies + run: python -m pip install -r requirements.txt + + - name: Run controller tests + run: python -m unittest discover -s tests -v + + - name: Verify committed evidence artifacts + run: python validate_artifacts.py From 566e965fb6001824ae19b3b4b37fc7a6dcd5d814 Mon Sep 17 00:00:00 2001 From: Carlos Hernandez Date: Wed, 17 Jun 2026 22:33:42 +0200 Subject: [PATCH 4/4] ci: make industrial example checks fork-safe --- .github/workflows/ci.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcbf66d..1b95487 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,11 +41,16 @@ jobs: cache: "pip" cache-dependency-path: industrial-embodied-ai/requirements.txt - - name: Install dependencies - run: python -m pip install -r requirements.txt + - name: Install controller test dependencies + run: python -m pip install cryptography - name: Run controller tests run: python -m unittest discover -s tests -v + - name: Install artifact validation dependencies + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository + run: python -m pip install -r requirements.txt + - name: Verify committed evidence artifacts + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository run: python validate_artifacts.py