Skip to content

Commit 32b7918

Browse files
committed
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.
1 parent 7786d12 commit 32b7918

5 files changed

Lines changed: 136 additions & 104 deletions

File tree

industrial-embodied-ai/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
__pycache__/
22
*.pyc
3+
.venv/
34
audit.db
45
trace-output/latest-*.json

industrial-embodied-ai/README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ to produce durable evidence:
1919

2020
1. **Allowed and completed:** cMCP authorizes the declared workflow, then the
2121
independent controller accepts and completes the simulated motion.
22-
2. **Scope denied:** the agent requests a motion whose physical parameters sit
23-
inside the safety envelope (an approved zone, an in-limit speed), but under
24-
an undeclared workflow. cMCP denies it on scope before the controller is
25-
consulted. Physical safety was never the question: the trust layer withholds
26-
the action because it falls outside the agent's declared purpose.
22+
2. **Scope denied, the same motion out of scope:** the agent requests the same
23+
in-envelope motion as the authorized path (the same approved zone and
24+
in-limit speed, a fresh valid safety token) but under an undeclared workflow.
25+
cMCP denies it on scope before the controller is consulted, so the safety
26+
token is never checked. The only difference from the authorized path is the
27+
workflow scope, not the motion or its safety: the trust layer withholds the
28+
action because it is outside the agent's declared purpose.
2729
3. **Safety rejected:** cMCP authorizes the declared workflow, but the
2830
controller rejects motion after its current state reports a person in the
2931
safeguarded area.
@@ -162,15 +164,15 @@ python agent/material_movement_agent.py
162164
Expected summary:
163165

164166
```text
167+
SCOPE DENY
168+
cMCP policy: denied (out of declared scope)
169+
controller: not consulted
170+
165171
SUCCESS
166172
cMCP policy: authorized
167173
controller: accepted
168174
execution: completed
169175
170-
POLICY DENY
171-
cMCP policy: denied
172-
controller: not invoked
173-
174176
SAFETY REJECT
175177
cMCP policy: authorized
176178
controller: rejected

industrial-embodied-ai/agent/material_movement_agent.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ def _request_motion(
9090
request_id: int,
9191
snapshot: dict[str, Any],
9292
motion_id: str,
93+
*,
94+
workflow_id: str = WORKFLOW_ID,
9395
) -> dict[str, Any]:
9496
return call_tool(
9597
client,
@@ -102,6 +104,7 @@ def _request_motion(
102104
"safety_state_token": snapshot["state_token"],
103105
},
104106
request_id,
107+
workflow_id=workflow_id,
105108
)
106109

107110

@@ -169,15 +172,39 @@ def run(
169172
) -> None:
170173
session_id: str | None = None
171174
with httpx.Client(headers=_headers()) as client:
172-
print("SUCCESS")
175+
print("SCOPE DENY")
176+
# The same in-envelope motion as the authorized path below (an approved
177+
# zone, an in-limit speed, a fresh valid safety token), requested under
178+
# an undeclared workflow. cMCP denies it on scope before the controller
179+
# is consulted, so the safety token is never even checked. The only
180+
# difference from the authorized path is the workflow scope, not the
181+
# motion or its physical safety.
173182
state = _read_state(client, gateway, 1)
174183
session_id = state["session_id"]
175-
success = _request_motion(
184+
denied = _request_motion(
176185
client,
177186
gateway,
178187
2,
179188
state["payload"],
180189
"move-0001",
190+
workflow_id="unapproved-diagnostics",
191+
)
192+
if denied["ok"] or denied["status_code"] != 403:
193+
raise RuntimeError("Out-of-scope motion was not denied by cMCP")
194+
print(" cMCP policy: denied (out of declared scope)")
195+
print(" controller: not consulted")
196+
print()
197+
198+
print("SUCCESS")
199+
# The identical motion, now under the declared workflow. cMCP authorizes
200+
# it and the independent controller accepts and completes it.
201+
state = _read_state(client, gateway, 3)
202+
success = _request_motion(
203+
client,
204+
gateway,
205+
4,
206+
state["payload"],
207+
"move-0002",
181208
)
182209
if not success["ok"]:
183210
raise RuntimeError(f"Success path failed: {success['error']}")
@@ -191,32 +218,12 @@ def run(
191218
print(f" execution: {success['payload']['execution_status']}")
192219
print()
193220

194-
print("POLICY DENY")
195-
denied = call_tool(
196-
client,
197-
gateway,
198-
"robot.request_motion",
199-
{
200-
"motion_id": "move-0002",
201-
"target": "transfer-station-b",
202-
"max_speed_mps": 0.2,
203-
"safety_state_token": "not-forwarded",
204-
},
205-
3,
206-
workflow_id="unapproved-diagnostics",
207-
)
208-
if denied["ok"] or denied["status_code"] != 403:
209-
raise RuntimeError("Unapproved workflow was not denied by cMCP")
210-
print(" cMCP policy: denied")
211-
print(" controller: not invoked")
212-
print()
213-
214221
print("SAFETY REJECT")
215-
state = _read_state(client, gateway, 4)
222+
state = _read_state(client, gateway, 5)
216223
rejected = _request_motion(
217224
client,
218225
gateway,
219-
5,
226+
6,
220227
state["payload"],
221228
"move-0003",
222229
)
Lines changed: 78 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
{
2-
"session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1",
2+
"session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464",
33
"entries": [
44
{
5-
"entry_id": "4ad2ba95-1bdf-4172-95e5-5b3b32c82db9",
5+
"entry_id": "36e22359-cc5f-438c-9c44-1a8b1e88d589",
66
"sequence_number": 0,
7-
"timestamp_utc": "2026-06-11T22:40:55.477088+00:00",
8-
"session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1",
7+
"timestamp_utc": "2026-06-14T06:17:40.761165+00:00",
8+
"session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464",
99
"call_id": null,
1010
"entry_type": "session_start",
1111
"tool_name": null,
@@ -21,123 +21,145 @@
2121
"detail": null,
2222
"workflow_id": null,
2323
"prev_entry_hash": "genesis",
24-
"entry_hash": "fd9e4d49b299f3088024a69ed1fc1e3d288593c62186ab4e1d8cb152ef226d61"
24+
"entry_hash": "321d0076a1a987074def0fb6603198ec3291a9970feff5bc404918bb5557f7cb"
2525
},
2626
{
27-
"entry_id": "9aa358b5-a3c3-4568-8cd3-add87dd6bb45",
27+
"entry_id": "ad9ec034-6203-4393-94f5-d02b718fd30c",
2828
"sequence_number": 1,
29-
"timestamp_utc": "2026-06-11T22:41:00.509721+00:00",
30-
"session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1",
31-
"call_id": "a5db9210-0d8f-42b8-accb-1376da5a9dd0",
29+
"timestamp_utc": "2026-06-14T06:17:41.201402+00:00",
30+
"session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464",
31+
"call_id": "b3686ea8-ba04-4c08-8533-7f44df436e90",
3232
"entry_type": "tool_call",
3333
"tool_name": "cell.read_safety_state",
3434
"server_identity": "http://localhost:8080/mcp",
3535
"policy_decision": "allow",
3636
"policy_rule_matched": "Cedar (cedarpy): allowed",
37-
"latency_us": 26832,
37+
"latency_us": 13893,
3838
"request_payload_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
3939
"response_payload_hash": null,
4040
"response_inspection_result": null,
4141
"session_sensitivity_before": "public",
4242
"session_sensitivity_after": "confidential",
4343
"detail": null,
4444
"workflow_id": "industrial-material-movement",
45-
"prev_entry_hash": "fd9e4d49b299f3088024a69ed1fc1e3d288593c62186ab4e1d8cb152ef226d61",
46-
"entry_hash": "6a52c7f67d06c65c960e79bc74873a0fa10cfc006c764d0ef3c1822beafe591c"
45+
"prev_entry_hash": "321d0076a1a987074def0fb6603198ec3291a9970feff5bc404918bb5557f7cb",
46+
"entry_hash": "0e05f8ce5ada7c52af804bfb76b2ed7d62a39e3d75e5306ed274dfd8c3452791"
4747
},
4848
{
49-
"entry_id": "000236bc-e37b-4e17-9f27-956707263512",
49+
"entry_id": "82ef16ae-5635-40a5-8088-293838dc2959",
5050
"sequence_number": 2,
51-
"timestamp_utc": "2026-06-11T22:41:00.513614+00:00",
52-
"session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1",
53-
"call_id": "244fcfe4-3be8-45e4-ae19-297f13084330",
51+
"timestamp_utc": "2026-06-14T06:17:41.202773+00:00",
52+
"session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464",
53+
"call_id": "f5834ae3-a65a-4390-b50b-98f9b19fac0b",
5454
"entry_type": "tool_call",
5555
"tool_name": "robot.request_motion",
5656
"server_identity": "http://localhost:8080/mcp",
57+
"policy_decision": "deny",
58+
"policy_rule_matched": "Policy denied tool call: robot.request_motion",
59+
"latency_us": null,
60+
"request_payload_hash": "sha256:55f3c91fb6551c28fbddbaba9b8cf3f4c904479c83d2dfdb23972a9bc4fd83d1",
61+
"response_payload_hash": null,
62+
"response_inspection_result": null,
63+
"session_sensitivity_before": "confidential",
64+
"session_sensitivity_after": "confidential",
65+
"detail": null,
66+
"workflow_id": "unapproved-diagnostics",
67+
"prev_entry_hash": "0e05f8ce5ada7c52af804bfb76b2ed7d62a39e3d75e5306ed274dfd8c3452791",
68+
"entry_hash": "bc4412b9ecb817abcdbad6466101992e06cbf41901c5378e5f24273e045f0042"
69+
},
70+
{
71+
"entry_id": "e313fa3b-ac7c-4b8d-b109-f13f6f2f54c4",
72+
"sequence_number": 3,
73+
"timestamp_utc": "2026-06-14T06:17:41.205384+00:00",
74+
"session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464",
75+
"call_id": "f84f13bd-5886-46d1-84a1-c9871d439e6c",
76+
"entry_type": "tool_call",
77+
"tool_name": "cell.read_safety_state",
78+
"server_identity": "http://localhost:8080/mcp",
5779
"policy_decision": "allow",
5880
"policy_rule_matched": "Cedar (cedarpy): allowed",
59-
"latency_us": 2273,
60-
"request_payload_hash": "sha256:f4ebe51008ab5b1288beccb5b17a16e0f2fbb5bd644c8902859a261e2d92e426",
81+
"latency_us": 1737,
82+
"request_payload_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
6183
"response_payload_hash": null,
6284
"response_inspection_result": null,
6385
"session_sensitivity_before": "confidential",
6486
"session_sensitivity_after": "confidential",
6587
"detail": null,
6688
"workflow_id": "industrial-material-movement",
67-
"prev_entry_hash": "6a52c7f67d06c65c960e79bc74873a0fa10cfc006c764d0ef3c1822beafe591c",
68-
"entry_hash": "e8368d7e9fd8e54f233f402a63ee4dfc839931b2d455f6bb45e6a191b3f83137"
89+
"prev_entry_hash": "bc4412b9ecb817abcdbad6466101992e06cbf41901c5378e5f24273e045f0042",
90+
"entry_hash": "61100d5616b3069a33fd3649a44b659fd041f7577fc10053d44d1c6005ce3fbf"
6991
},
7092
{
71-
"entry_id": "8e52cfae-1ebe-40bc-ba99-ae934dae68ec",
72-
"sequence_number": 3,
73-
"timestamp_utc": "2026-06-11T22:41:00.514876+00:00",
74-
"session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1",
75-
"call_id": "0fab65d6-6867-44ff-94c5-c0c585d7e855",
93+
"entry_id": "1c20b388-c834-42bb-b109-f0370d764312",
94+
"sequence_number": 4,
95+
"timestamp_utc": "2026-06-14T06:17:41.207728+00:00",
96+
"session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464",
97+
"call_id": "48f80a39-6ac6-4247-ae10-76c9385e085f",
7698
"entry_type": "tool_call",
7799
"tool_name": "robot.request_motion",
78100
"server_identity": "http://localhost:8080/mcp",
79-
"policy_decision": "deny",
80-
"policy_rule_matched": "Policy denied tool call: robot.request_motion",
81-
"latency_us": null,
82-
"request_payload_hash": "sha256:338998c57bf57fcd05cea50a8d463d39f81ccb184ddad984a31fb7dab162f5ce",
101+
"policy_decision": "allow",
102+
"policy_rule_matched": "Cedar (cedarpy): allowed",
103+
"latency_us": 1522,
104+
"request_payload_hash": "sha256:53457ccb81c8c3d1c80811535c7ab5cbe4c5e9c0a2ef244fd7dff9bef066d361",
83105
"response_payload_hash": null,
84106
"response_inspection_result": null,
85107
"session_sensitivity_before": "confidential",
86108
"session_sensitivity_after": "confidential",
87109
"detail": null,
88-
"workflow_id": "unapproved-diagnostics",
89-
"prev_entry_hash": "e8368d7e9fd8e54f233f402a63ee4dfc839931b2d455f6bb45e6a191b3f83137",
90-
"entry_hash": "6b2ef02224320d6146c269b8d9817a82b72623350a0aa0845b1aab4023f34e23"
110+
"workflow_id": "industrial-material-movement",
111+
"prev_entry_hash": "61100d5616b3069a33fd3649a44b659fd041f7577fc10053d44d1c6005ce3fbf",
112+
"entry_hash": "cd60e91a4a0b519591fafb2b50d08c07fa5a20c0031d16bc3051774e84673c5f"
91113
},
92114
{
93-
"entry_id": "af8838a7-7bce-4c03-8352-9ed55ecf0868",
94-
"sequence_number": 4,
95-
"timestamp_utc": "2026-06-11T22:41:00.517758+00:00",
96-
"session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1",
97-
"call_id": "decf2427-c918-4a17-a677-88f049d8e2cb",
115+
"entry_id": "888a950c-c2e6-445d-8fa3-f9d42f85c7a0",
116+
"sequence_number": 5,
117+
"timestamp_utc": "2026-06-14T06:17:41.210018+00:00",
118+
"session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464",
119+
"call_id": "991303a3-0f7c-4748-9609-d7dbc0868250",
98120
"entry_type": "tool_call",
99121
"tool_name": "cell.read_safety_state",
100122
"server_identity": "http://localhost:8080/mcp",
101123
"policy_decision": "allow",
102124
"policy_rule_matched": "Cedar (cedarpy): allowed",
103-
"latency_us": 1985,
125+
"latency_us": 1590,
104126
"request_payload_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
105127
"response_payload_hash": null,
106128
"response_inspection_result": null,
107129
"session_sensitivity_before": "confidential",
108130
"session_sensitivity_after": "confidential",
109131
"detail": null,
110132
"workflow_id": "industrial-material-movement",
111-
"prev_entry_hash": "6b2ef02224320d6146c269b8d9817a82b72623350a0aa0845b1aab4023f34e23",
112-
"entry_hash": "d8a7985af6b52b903eb11664fe16e29fc78759d2e947f287dc73fb46bf16e181"
133+
"prev_entry_hash": "cd60e91a4a0b519591fafb2b50d08c07fa5a20c0031d16bc3051774e84673c5f",
134+
"entry_hash": "6b3bdc64d0bf88a50e5a6190dff3d43f8bce79947710857000ee6e7ae01739e9"
113135
},
114136
{
115-
"entry_id": "0f65e244-2329-4f7e-a0d0-181252fe52a2",
116-
"sequence_number": 5,
117-
"timestamp_utc": "2026-06-11T22:41:00.520467+00:00",
118-
"session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1",
119-
"call_id": "e030ccfc-6ec8-4454-9305-1b5eb48adb0b",
137+
"entry_id": "e1d2accd-ae46-4066-b5b2-be395e6fd29f",
138+
"sequence_number": 6,
139+
"timestamp_utc": "2026-06-14T06:17:41.212275+00:00",
140+
"session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464",
141+
"call_id": "fe27a71c-66bc-40a2-931f-3f9ecd24c781",
120142
"entry_type": "tool_call",
121143
"tool_name": "robot.request_motion",
122144
"server_identity": "http://localhost:8080/mcp",
123145
"policy_decision": "allow",
124146
"policy_rule_matched": "Cedar (cedarpy): allowed",
125-
"latency_us": 1773,
126-
"request_payload_hash": "sha256:c01b77fa8da1e5875b3b173043d0f4ea1cbf82b1eae2cb8c5547178effb2d3ff",
147+
"latency_us": 1509,
148+
"request_payload_hash": "sha256:309143d21f5916c254793a9f37202ebafda1258ae1eb6de1309de0d1160af3ec",
127149
"response_payload_hash": null,
128150
"response_inspection_result": null,
129151
"session_sensitivity_before": "confidential",
130152
"session_sensitivity_after": "confidential",
131153
"detail": null,
132154
"workflow_id": "industrial-material-movement",
133-
"prev_entry_hash": "d8a7985af6b52b903eb11664fe16e29fc78759d2e947f287dc73fb46bf16e181",
134-
"entry_hash": "214a2ce17f8b0333c2b543493f61f21b205ccc8faa594b877a34108e83d25cd8"
155+
"prev_entry_hash": "6b3bdc64d0bf88a50e5a6190dff3d43f8bce79947710857000ee6e7ae01739e9",
156+
"entry_hash": "d8e3f5030ab24526e5a549eada49a07317c0063164aa7df4ae012f62692a4041"
135157
},
136158
{
137-
"entry_id": "c8cc8737-3689-4be5-8b61-09b13e46bbbc",
138-
"sequence_number": 6,
139-
"timestamp_utc": "2026-06-11T22:41:00.521203+00:00",
140-
"session_id": "fdbdd187-b276-4c71-8e08-50f839d13bd1",
159+
"entry_id": "221905ea-9db4-4d20-a3f0-765d023a3a14",
160+
"sequence_number": 7,
161+
"timestamp_utc": "2026-06-14T06:17:41.212940+00:00",
162+
"session_id": "8fe66a2a-836b-4cff-9d05-7518d7f6f464",
141163
"call_id": null,
142164
"entry_type": "session_end",
143165
"tool_name": null,
@@ -152,9 +174,9 @@
152174
"session_sensitivity_after": "confidential",
153175
"detail": null,
154176
"workflow_id": null,
155-
"prev_entry_hash": "214a2ce17f8b0333c2b543493f61f21b205ccc8faa594b877a34108e83d25cd8",
156-
"entry_hash": "278f15cce18a1fcb6a22c121facdab23060c700f7cff55ea622ecc8701c9ed7c"
177+
"prev_entry_hash": "d8e3f5030ab24526e5a549eada49a07317c0063164aa7df4ae012f62692a4041",
178+
"entry_hash": "9cb9d91fb224e7020cd24830651b63524f6fbb69a6539d389cfc315ff21d38aa"
157179
}
158180
],
159-
"bundle_signature": "l6En40w_6kLkxc4h5KjOna7SdOvCs3DhWeqriz1qFNr4RaxIVg2_5728A-D8PcThd4t8ce777pGb4XixoIPqAw"
181+
"bundle_signature": "q3iOArOC3lZTXC4kDQAcdzZ9l9PLiPhMFMZhV0nZoux62wFFjSyO7FTF-fSrs1wQMpVL_sUB813jf2WnVQc0AQ"
160182
}

0 commit comments

Comments
 (0)