From 32b79185756ca092920902bbefe64d59e39d6a24 Mon Sep 17 00:00:00 2001 From: Carlos Hernandez Date: Sun, 14 Jun 2026 08:19:04 +0200 Subject: [PATCH] feat(industrial-embodied-ai): demonstrate scope denial as an A/B against the authorized motion Make the "an individually safe motion is not a trusted one" boundary runnable, not just documented. The agent now requests the same in-envelope motion twice: once under the declared workflow (cMCP authorizes, the controller completes it) and once under an undeclared workflow (cMCP denies on scope before the controller is consulted). The only difference is the workflow scope, not the motion or its physical safety. - agent: add an out-of-scope SCOPE DENY path that reuses the authorized motion parameters with a fresh valid safety token; reorder so the deny runs first and does not perturb the safety-reject choreography. - README: reword the scope-denied scenario as the A/B and update the expected summary. - regenerate the committed TRACE record and audit bundle from a real software-only dev-mode run (validate_artifacts.py and the unit tests pass). - gitignore the local .venv. No policy, catalog, prompt or manifest hash-bound input changes; only the signed evidence outputs are regenerated. --- industrial-embodied-ai/.gitignore | 1 + industrial-embodied-ai/README.md | 20 +-- .../agent/material_movement_agent.py | 55 +++---- .../trace-output/example-audit-bundle.json | 134 ++++++++++-------- .../trace-output/example-trust-record.json | 30 ++-- 5 files changed, 136 insertions(+), 104 deletions(-) diff --git a/industrial-embodied-ai/.gitignore b/industrial-embodied-ai/.gitignore index 4bdd894..652dd80 100644 --- a/industrial-embodied-ai/.gitignore +++ b/industrial-embodied-ai/.gitignore @@ -1,4 +1,5 @@ __pycache__/ *.pyc +.venv/ audit.db trace-output/latest-*.json diff --git a/industrial-embodied-ai/README.md b/industrial-embodied-ai/README.md index 0d67f6e..e5b4c42 100644 --- a/industrial-embodied-ai/README.md +++ b/industrial-embodied-ai/README.md @@ -19,11 +19,13 @@ to produce durable evidence: 1. **Allowed and completed:** cMCP authorizes the declared workflow, then the independent controller accepts and completes the simulated motion. -2. **Scope denied:** the agent requests a motion whose physical parameters sit - inside the safety envelope (an approved zone, an in-limit speed), but under - an undeclared workflow. cMCP denies it on scope before the controller is - consulted. Physical safety was never the question: the trust layer withholds - the action because it falls outside the agent's declared purpose. +2. **Scope denied, the same motion out of scope:** the agent requests the same + in-envelope motion as the authorized path (the same approved zone and + in-limit speed, a fresh valid safety token) but under an undeclared workflow. + cMCP denies it on scope before the controller is consulted, so the safety + token is never checked. The only difference from the authorized path is the + workflow scope, not the motion or its safety: the trust layer withholds the + action because it is outside the agent's declared purpose. 3. **Safety rejected:** cMCP authorizes the declared workflow, but the controller rejects motion after its current state reports a person in the safeguarded area. @@ -162,15 +164,15 @@ python agent/material_movement_agent.py Expected summary: ```text +SCOPE DENY + cMCP policy: denied (out of declared scope) + controller: not consulted + SUCCESS cMCP policy: authorized controller: accepted execution: completed -POLICY DENY - cMCP policy: denied - controller: not invoked - SAFETY REJECT cMCP policy: authorized controller: rejected diff --git a/industrial-embodied-ai/agent/material_movement_agent.py b/industrial-embodied-ai/agent/material_movement_agent.py index 0e41387..3fa6c35 100644 --- a/industrial-embodied-ai/agent/material_movement_agent.py +++ b/industrial-embodied-ai/agent/material_movement_agent.py @@ -90,6 +90,8 @@ def _request_motion( request_id: int, snapshot: dict[str, Any], motion_id: str, + *, + workflow_id: str = WORKFLOW_ID, ) -> dict[str, Any]: return call_tool( client, @@ -102,6 +104,7 @@ def _request_motion( "safety_state_token": snapshot["state_token"], }, request_id, + workflow_id=workflow_id, ) @@ -169,15 +172,39 @@ def run( ) -> None: session_id: str | None = None with httpx.Client(headers=_headers()) as client: - print("SUCCESS") + print("SCOPE DENY") + # The same in-envelope motion as the authorized path below (an approved + # zone, an in-limit speed, a fresh valid safety token), requested under + # an undeclared workflow. cMCP denies it on scope before the controller + # is consulted, so the safety token is never even checked. The only + # difference from the authorized path is the workflow scope, not the + # motion or its physical safety. state = _read_state(client, gateway, 1) session_id = state["session_id"] - success = _request_motion( + denied = _request_motion( client, gateway, 2, state["payload"], "move-0001", + workflow_id="unapproved-diagnostics", + ) + if denied["ok"] or denied["status_code"] != 403: + raise RuntimeError("Out-of-scope motion was not denied by cMCP") + print(" cMCP policy: denied (out of declared scope)") + print(" controller: not consulted") + print() + + print("SUCCESS") + # The identical motion, now under the declared workflow. cMCP authorizes + # it and the independent controller accepts and completes it. + state = _read_state(client, gateway, 3) + success = _request_motion( + client, + gateway, + 4, + state["payload"], + "move-0002", ) if not success["ok"]: raise RuntimeError(f"Success path failed: {success['error']}") @@ -191,32 +218,12 @@ def run( print(f" execution: {success['payload']['execution_status']}") print() - print("POLICY DENY") - denied = call_tool( - client, - gateway, - "robot.request_motion", - { - "motion_id": "move-0002", - "target": "transfer-station-b", - "max_speed_mps": 0.2, - "safety_state_token": "not-forwarded", - }, - 3, - workflow_id="unapproved-diagnostics", - ) - if denied["ok"] or denied["status_code"] != 403: - raise RuntimeError("Unapproved workflow was not denied by cMCP") - print(" cMCP policy: denied") - print(" controller: not invoked") - print() - print("SAFETY REJECT") - state = _read_state(client, gateway, 4) + state = _read_state(client, gateway, 5) rejected = _request_motion( client, gateway, - 5, + 6, state["payload"], "move-0003", ) diff --git a/industrial-embodied-ai/trace-output/example-audit-bundle.json b/industrial-embodied-ai/trace-output/example-audit-bundle.json index 70fb388..7b314af 100644 --- a/industrial-embodied-ai/trace-output/example-audit-bundle.json +++ b/industrial-embodied-ai/trace-output/example-audit-bundle.json @@ -1,11 +1,11 @@ { - "session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1", + "session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464", "entries": [ { - "entry_id": "4ad2ba95-1bdf-4172-95e5-5b3b32c82db9", + "entry_id": "36e22359-cc5f-438c-9c44-1a8b1e88d589", "sequence_number": 0, - "timestamp_utc": "2026-06-11T22:40:55.477088+00:00", - "session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1", + "timestamp_utc": "2026-06-14T06:17:40.761165+00:00", + "session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464", "call_id": null, "entry_type": "session_start", "tool_name": null, @@ -21,20 +21,20 @@ "detail": null, "workflow_id": null, "prev_entry_hash": "genesis", - "entry_hash": "fd9e4d49b299f3088024a69ed1fc1e3d288593c62186ab4e1d8cb152ef226d61" + "entry_hash": "321d0076a1a987074def0fb6603198ec3291a9970feff5bc404918bb5557f7cb" }, { - "entry_id": "9aa358b5-a3c3-4568-8cd3-add87dd6bb45", + "entry_id": "ad9ec034-6203-4393-94f5-d02b718fd30c", "sequence_number": 1, - "timestamp_utc": "2026-06-11T22:41:00.509721+00:00", - "session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1", - "call_id": "a5db9210-0d8f-42b8-accb-1376da5a9dd0", + "timestamp_utc": "2026-06-14T06:17:41.201402+00:00", + "session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464", + "call_id": "b3686ea8-ba04-4c08-8533-7f44df436e90", "entry_type": "tool_call", "tool_name": "cell.read_safety_state", "server_identity": "http://localhost:8080/mcp", "policy_decision": "allow", "policy_rule_matched": "Cedar (cedarpy): allowed", - "latency_us": 26832, + "latency_us": 13893, "request_payload_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", "response_payload_hash": null, "response_inspection_result": null, @@ -42,65 +42,87 @@ "session_sensitivity_after": "confidential", "detail": null, "workflow_id": "industrial-material-movement", - "prev_entry_hash": "fd9e4d49b299f3088024a69ed1fc1e3d288593c62186ab4e1d8cb152ef226d61", - "entry_hash": "6a52c7f67d06c65c960e79bc74873a0fa10cfc006c764d0ef3c1822beafe591c" + "prev_entry_hash": "321d0076a1a987074def0fb6603198ec3291a9970feff5bc404918bb5557f7cb", + "entry_hash": "0e05f8ce5ada7c52af804bfb76b2ed7d62a39e3d75e5306ed274dfd8c3452791" }, { - "entry_id": "000236bc-e37b-4e17-9f27-956707263512", + "entry_id": "82ef16ae-5635-40a5-8088-293838dc2959", "sequence_number": 2, - "timestamp_utc": "2026-06-11T22:41:00.513614+00:00", - "session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1", - "call_id": "244fcfe4-3be8-45e4-ae19-297f13084330", + "timestamp_utc": "2026-06-14T06:17:41.202773+00:00", + "session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464", + "call_id": "f5834ae3-a65a-4390-b50b-98f9b19fac0b", "entry_type": "tool_call", "tool_name": "robot.request_motion", "server_identity": "http://localhost:8080/mcp", + "policy_decision": "deny", + "policy_rule_matched": "Policy denied tool call: robot.request_motion", + "latency_us": null, + "request_payload_hash": "sha256:55f3c91fb6551c28fbddbaba9b8cf3f4c904479c83d2dfdb23972a9bc4fd83d1", + "response_payload_hash": null, + "response_inspection_result": null, + "session_sensitivity_before": "confidential", + "session_sensitivity_after": "confidential", + "detail": null, + "workflow_id": "unapproved-diagnostics", + "prev_entry_hash": "0e05f8ce5ada7c52af804bfb76b2ed7d62a39e3d75e5306ed274dfd8c3452791", + "entry_hash": "bc4412b9ecb817abcdbad6466101992e06cbf41901c5378e5f24273e045f0042" + }, + { + "entry_id": "e313fa3b-ac7c-4b8d-b109-f13f6f2f54c4", + "sequence_number": 3, + "timestamp_utc": "2026-06-14T06:17:41.205384+00:00", + "session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464", + "call_id": "f84f13bd-5886-46d1-84a1-c9871d439e6c", + "entry_type": "tool_call", + "tool_name": "cell.read_safety_state", + "server_identity": "http://localhost:8080/mcp", "policy_decision": "allow", "policy_rule_matched": "Cedar (cedarpy): allowed", - "latency_us": 2273, - "request_payload_hash": "sha256:f4ebe51008ab5b1288beccb5b17a16e0f2fbb5bd644c8902859a261e2d92e426", + "latency_us": 1737, + "request_payload_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", "response_payload_hash": null, "response_inspection_result": null, "session_sensitivity_before": "confidential", "session_sensitivity_after": "confidential", "detail": null, "workflow_id": "industrial-material-movement", - "prev_entry_hash": "6a52c7f67d06c65c960e79bc74873a0fa10cfc006c764d0ef3c1822beafe591c", - "entry_hash": "e8368d7e9fd8e54f233f402a63ee4dfc839931b2d455f6bb45e6a191b3f83137" + "prev_entry_hash": "bc4412b9ecb817abcdbad6466101992e06cbf41901c5378e5f24273e045f0042", + "entry_hash": "61100d5616b3069a33fd3649a44b659fd041f7577fc10053d44d1c6005ce3fbf" }, { - "entry_id": "8e52cfae-1ebe-40bc-ba99-ae934dae68ec", - "sequence_number": 3, - "timestamp_utc": "2026-06-11T22:41:00.514876+00:00", - "session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1", - "call_id": "0fab65d6-6867-44ff-94c5-c0c585d7e855", + "entry_id": "1c20b388-c834-42bb-b109-f0370d764312", + "sequence_number": 4, + "timestamp_utc": "2026-06-14T06:17:41.207728+00:00", + "session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464", + "call_id": "48f80a39-6ac6-4247-ae10-76c9385e085f", "entry_type": "tool_call", "tool_name": "robot.request_motion", "server_identity": "http://localhost:8080/mcp", - "policy_decision": "deny", - "policy_rule_matched": "Policy denied tool call: robot.request_motion", - "latency_us": null, - "request_payload_hash": "sha256:338998c57bf57fcd05cea50a8d463d39f81ccb184ddad984a31fb7dab162f5ce", + "policy_decision": "allow", + "policy_rule_matched": "Cedar (cedarpy): allowed", + "latency_us": 1522, + "request_payload_hash": "sha256:53457ccb81c8c3d1c80811535c7ab5cbe4c5e9c0a2ef244fd7dff9bef066d361", "response_payload_hash": null, "response_inspection_result": null, "session_sensitivity_before": "confidential", "session_sensitivity_after": "confidential", "detail": null, - "workflow_id": "unapproved-diagnostics", - "prev_entry_hash": "e8368d7e9fd8e54f233f402a63ee4dfc839931b2d455f6bb45e6a191b3f83137", - "entry_hash": "6b2ef02224320d6146c269b8d9817a82b72623350a0aa0845b1aab4023f34e23" + "workflow_id": "industrial-material-movement", + "prev_entry_hash": "61100d5616b3069a33fd3649a44b659fd041f7577fc10053d44d1c6005ce3fbf", + "entry_hash": "cd60e91a4a0b519591fafb2b50d08c07fa5a20c0031d16bc3051774e84673c5f" }, { - "entry_id": "af8838a7-7bce-4c03-8352-9ed55ecf0868", - "sequence_number": 4, - "timestamp_utc": "2026-06-11T22:41:00.517758+00:00", - "session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1", - "call_id": "decf2427-c918-4a17-a677-88f049d8e2cb", + "entry_id": "888a950c-c2e6-445d-8fa3-f9d42f85c7a0", + "sequence_number": 5, + "timestamp_utc": "2026-06-14T06:17:41.210018+00:00", + "session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464", + "call_id": "991303a3-0f7c-4748-9609-d7dbc0868250", "entry_type": "tool_call", "tool_name": "cell.read_safety_state", "server_identity": "http://localhost:8080/mcp", "policy_decision": "allow", "policy_rule_matched": "Cedar (cedarpy): allowed", - "latency_us": 1985, + "latency_us": 1590, "request_payload_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", "response_payload_hash": null, "response_inspection_result": null, @@ -108,36 +130,36 @@ "session_sensitivity_after": "confidential", "detail": null, "workflow_id": "industrial-material-movement", - "prev_entry_hash": "6b2ef02224320d6146c269b8d9817a82b72623350a0aa0845b1aab4023f34e23", - "entry_hash": "d8a7985af6b52b903eb11664fe16e29fc78759d2e947f287dc73fb46bf16e181" + "prev_entry_hash": "cd60e91a4a0b519591fafb2b50d08c07fa5a20c0031d16bc3051774e84673c5f", + "entry_hash": "6b3bdc64d0bf88a50e5a6190dff3d43f8bce79947710857000ee6e7ae01739e9" }, { - "entry_id": "0f65e244-2329-4f7e-a0d0-181252fe52a2", - "sequence_number": 5, - "timestamp_utc": "2026-06-11T22:41:00.520467+00:00", - "session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1", - "call_id": "e030ccfc-6ec8-4454-9305-1b5eb48adb0b", + "entry_id": "e1d2accd-ae46-4066-b5b2-be395e6fd29f", + "sequence_number": 6, + "timestamp_utc": "2026-06-14T06:17:41.212275+00:00", + "session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464", + "call_id": "fe27a71c-66bc-40a2-931f-3f9ecd24c781", "entry_type": "tool_call", "tool_name": "robot.request_motion", "server_identity": "http://localhost:8080/mcp", "policy_decision": "allow", "policy_rule_matched": "Cedar (cedarpy): allowed", - "latency_us": 1773, - "request_payload_hash": "sha256:c01b77fa8da1e5875b3b173043d0f4ea1cbf82b1eae2cb8c5547178effb2d3ff", + "latency_us": 1509, + "request_payload_hash": "sha256:309143d21f5916c254793a9f37202ebafda1258ae1eb6de1309de0d1160af3ec", "response_payload_hash": null, "response_inspection_result": null, "session_sensitivity_before": "confidential", "session_sensitivity_after": "confidential", "detail": null, "workflow_id": "industrial-material-movement", - "prev_entry_hash": "d8a7985af6b52b903eb11664fe16e29fc78759d2e947f287dc73fb46bf16e181", - "entry_hash": "214a2ce17f8b0333c2b543493f61f21b205ccc8faa594b877a34108e83d25cd8" + "prev_entry_hash": "6b3bdc64d0bf88a50e5a6190dff3d43f8bce79947710857000ee6e7ae01739e9", + "entry_hash": "d8e3f5030ab24526e5a549eada49a07317c0063164aa7df4ae012f62692a4041" }, { - "entry_id": "c8cc8737-3689-4be5-8b61-09b13e46bbbc", - "sequence_number": 6, - "timestamp_utc": "2026-06-11T22:41:00.521203+00:00", - "session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1", + "entry_id": "221905ea-9db4-4d20-a3f0-765d023a3a14", + "sequence_number": 7, + "timestamp_utc": "2026-06-14T06:17:41.212940+00:00", + "session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464", "call_id": null, "entry_type": "session_end", "tool_name": null, @@ -152,9 +174,9 @@ "session_sensitivity_after": "confidential", "detail": null, "workflow_id": null, - "prev_entry_hash": "214a2ce17f8b0333c2b543493f61f21b205ccc8faa594b877a34108e83d25cd8", - "entry_hash": "278f15cce18a1fcb6a22c121facdab23060c700f7cff55ea622ecc8701c9ed7c" + "prev_entry_hash": "d8e3f5030ab24526e5a549eada49a07317c0063164aa7df4ae012f62692a4041", + "entry_hash": "9cb9d91fb224e7020cd24830651b63524f6fbb69a6539d389cfc315ff21d38aa" } ], - "bundle_signature": "l6En40w_6kLkxc4h5KjOna7SdOvCs3DhWeqriz1qFNr4RaxIVg2_5728A-D8PcThd4t8ce777pGb4XixoIPqAw" + "bundle_signature": "q3iOArOC3lZTXC4kDQAcdzZ9l9PLiPhMFMZhV0nZoux62wFFjSyO7FTF-fSrs1wQMpVL_sUB813jf2WnVQc0AQ" } diff --git a/industrial-embodied-ai/trace-output/example-trust-record.json b/industrial-embodied-ai/trace-output/example-trust-record.json index 0a95ab7..5fd173d 100644 --- a/industrial-embodied-ai/trace-output/example-trust-record.json +++ b/industrial-embodied-ai/trace-output/example-trust-record.json @@ -2,8 +2,8 @@ "cmcp_version": "1.0", "trace": { "eat_profile": "tag:agentrust.io,2026:trace-v0.1", - "iat": 1781217660, - "subject": "spiffe://cmcp.gateway/session/fdbdd187-b276-4c71-8e08-50f839d13bd1", + "iat": 1781417861, + "subject": "spiffe://cmcp.gateway/session/8fe66a2a-836b-4cff-9d05-7518d7f6f464", "runtime": { "platform": "software-only", "measurement": "sha256:0000000000000000000000000000000000000000000000000000000000000000", @@ -16,30 +16,30 @@ }, "data_class": "confidential", "tool_transcript": { - "hash": "sha256:278f15cce18a1fcb6a22c121facdab23060c700f7cff55ea622ecc8701c9ed7c", - "call_count": 5 + "hash": "sha256:9cb9d91fb224e7020cd24830651b63524f6fbb69a6539d389cfc315ff21d38aa", + "call_count": 6 }, "cnf": { "jwk": { "kty": "OKP", "crv": "Ed25519", - "x": "ebBLjGSAJYOTs9npzclQLGqGwZw_2fnG5YZ-Sbc0l54", - "kid": "cmcp-79b04b8c" + "x": "5L_3-8KHdhLKTycgp-WXB85OyEiRGLtoq0CcCzvzNg8", + "kid": "cmcp-e4bff7fb" } } }, "gateway": { - "session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1", + "session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464", "gateway_version": "0.1.0", "sequence_number": 1, "audit_chain": { - "root": "fd9e4d49b299f3088024a69ed1fc1e3d288593c62186ab4e1d8cb152ef226d61", - "tip": "278f15cce18a1fcb6a22c121facdab23060c700f7cff55ea622ecc8701c9ed7c", - "length": 7 + "root": "321d0076a1a987074def0fb6603198ec3291a9970feff5bc404918bb5557f7cb", + "tip": "9cb9d91fb224e7020cd24830651b63524f6fbb69a6539d389cfc315ff21d38aa", + "length": 8 }, "call_summary": { - "tool_calls_total": 5, - "tool_calls_allowed": 4, + "tool_calls_total": 6, + "tool_calls_allowed": 5, "tool_calls_denied": 1, "tool_calls_faulted": 0, "tools_invoked": [ @@ -59,12 +59,12 @@ "hash": "sha256:792c86ff8152fa9713d52584c084611eb4929fa5ebf3ec8271dd21f0e0aa7eeb", "drift_detected": false }, - "attestation_generated_at": "2026-06-11T22:40:55.264023+00:00", + "attestation_generated_at": "2026-06-14T06:17:40.551552+00:00", "attestation_validity_seconds": 86400, "attestation_stale": false, "catalog_exceptions": [], "call_log_summary": { - "total_calls": 5, + "total_calls": 6, "tools_called": [ "cell.read_safety_state", "robot.request_motion" @@ -72,5 +72,5 @@ "suspicious_sequences_detected": 0 } }, - "signature": "i3w2GxrsrGR9pHwO-VLeopxG9WTQTCVpy212-alhZB7bI8jxcH9mpW-AcKaKAu_TQTuGr-50IJo4gGitIKG6Dw" + "signature": "ss46cb_B7LzyqkHH20-76kJBMfz0qqf5CYUoKOvO04m0TwsVf-G4251PAmyJgATZ6cDiv3ZwqpOq-tXu5gNrCg" }