Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,32 @@ 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 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
31 changes: 21 additions & 10 deletions industrial-embodied-ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
```
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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 |
Expand Down
34 changes: 31 additions & 3 deletions industrial-embodied-ai/agent/material_movement_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import argparse
import base64
import json
import os
from pathlib import Path
Expand All @@ -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"):
Expand Down Expand Up @@ -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",
Expand All @@ -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":
Expand All @@ -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")

Expand Down
7 changes: 7 additions & 0 deletions industrial-embodied-ai/controller-receipt-public-key.json
Original file line number Diff line number Diff line change
@@ -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"
}
53 changes: 53 additions & 0 deletions industrial-embodied-ai/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -157,3 +169,44 @@ 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)

@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]:
"""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
2 changes: 1 addition & 1 deletion industrial-embodied-ai/requirements.txt
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions industrial-embodied-ai/server/mock_robot_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
{
Expand Down
Loading
Loading