agenticdome-python-sdk 1.2.35__py3-none-any.whl → 1.2.36__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agenticdome-python-sdk
3
- Version: 1.2.35
3
+ Version: 1.2.36
4
4
  Summary: Official Python SDK for AgenticDome security guardrails, DLP, tool authorization, and multi-agent delegation enforcement.
5
5
  Author: AgenticDome
6
6
  License-Expression: Apache-2.0
@@ -33,7 +33,7 @@ Requires-Dist: requests>=2.31.0
33
33
  Requires-Dist: urllib3>=2.0.0
34
34
  Requires-Dist: cryptography>=42.0.0
35
35
  Provides-Extra: crewai
36
- Requires-Dist: crewai<=1.15.22,>=1.15.5; extra == "crewai"
36
+ Requires-Dist: crewai<=1.15.23,>=1.15.5; extra == "crewai"
37
37
  Provides-Extra: redis
38
38
  Requires-Dist: redis>=4.5.0; extra == "redis"
39
39
  Provides-Extra: pop
@@ -46,7 +46,7 @@ Requires-Dist: pydantic-ai<=2.51.0,>=2.16.0; extra == "pydantic"
46
46
  Requires-Dist: anyio>=4.0.0; extra == "pydantic"
47
47
  Provides-Extra: langgraph
48
48
  Requires-Dist: langgraph<=1.2.12,>=1.2.9; extra == "langgraph"
49
- Requires-Dist: langchain-core<=1.6.5,>=1.5.0; extra == "langgraph"
49
+ Requires-Dist: langchain-core<=1.6.6,>=1.5.0; extra == "langgraph"
50
50
  Requires-Dist: langchain-openai==1.3.5; extra == "langgraph"
51
51
  Requires-Dist: anyio>=4.0.0; extra == "langgraph"
52
52
  Provides-Extra: microsoft
@@ -64,9 +64,9 @@ Requires-Dist: openai-agents<=0.22.3,>=0.18.3; extra == "openai-agents"
64
64
  Provides-Extra: mcp
65
65
  Requires-Dist: mcp<=2.2.0,>=1.26.0; extra == "mcp"
66
66
  Provides-Extra: bedrock
67
- Requires-Dist: boto3<=1.43.103,>=1.43.54; extra == "bedrock"
67
+ Requires-Dist: boto3<=1.43.105,>=1.43.54; extra == "bedrock"
68
68
  Provides-Extra: aws-bedrock
69
- Requires-Dist: boto3<=1.43.103,>=1.43.54; extra == "aws-bedrock"
69
+ Requires-Dist: boto3<=1.43.105,>=1.43.54; extra == "aws-bedrock"
70
70
  Provides-Extra: llamaindex
71
71
  Requires-Dist: llama-index<=0.14.25,>=0.14.23; extra == "llamaindex"
72
72
  Provides-Extra: llama-index
@@ -76,19 +76,19 @@ Requires-Dist: google-adk<=2.10.0,>=2.5.0; extra == "google-adk"
76
76
  Provides-Extra: adk
77
77
  Requires-Dist: google-adk<=2.10.0,>=2.5.0; extra == "adk"
78
78
  Provides-Extra: claude
79
- Requires-Dist: claude-agent-sdk<=0.2.160,>=0.2.126; extra == "claude"
79
+ Requires-Dist: claude-agent-sdk<=0.2.162,>=0.2.126; extra == "claude"
80
80
  Provides-Extra: claude-agent-sdk
81
- Requires-Dist: claude-agent-sdk<=0.2.160,>=0.2.126; extra == "claude-agent-sdk"
81
+ Requires-Dist: claude-agent-sdk<=0.2.162,>=0.2.126; extra == "claude-agent-sdk"
82
82
  Provides-Extra: smolagents
83
83
  Requires-Dist: smolagents==1.26.0; extra == "smolagents"
84
84
  Provides-Extra: all
85
- Requires-Dist: crewai<=1.15.22,>=1.15.5; extra == "all"
85
+ Requires-Dist: crewai<=1.15.23,>=1.15.5; extra == "all"
86
86
  Requires-Dist: redis>=4.5.0; extra == "all"
87
87
  Requires-Dist: cryptography>=42.0.0; extra == "all"
88
88
  Requires-Dist: pydantic-ai<=2.51.0,>=2.16.0; extra == "all"
89
89
  Requires-Dist: anyio>=4.0.0; extra == "all"
90
90
  Requires-Dist: langgraph<=1.2.12,>=1.2.9; extra == "all"
91
- Requires-Dist: langchain-core<=1.6.5,>=1.5.0; extra == "all"
91
+ Requires-Dist: langchain-core<=1.6.6,>=1.5.0; extra == "all"
92
92
  Requires-Dist: langchain-openai==1.3.5; extra == "all"
93
93
  Requires-Dist: autogen-agentchat==0.7.5; extra == "all"
94
94
  Requires-Dist: agno<=3.0.11,>=2.8.0; extra == "all"
@@ -96,10 +96,10 @@ Requires-Dist: azure-ai-projects<=2.7.0,>=2.3.0; extra == "all"
96
96
  Requires-Dist: azure-identity==1.25.3; extra == "all"
97
97
  Requires-Dist: openai-agents<=0.22.3,>=0.18.3; extra == "all"
98
98
  Requires-Dist: mcp<=2.2.0,>=1.26.0; extra == "all"
99
- Requires-Dist: boto3<=1.43.103,>=1.43.54; extra == "all"
99
+ Requires-Dist: boto3<=1.43.105,>=1.43.54; extra == "all"
100
100
  Requires-Dist: llama-index<=0.14.25,>=0.14.23; extra == "all"
101
101
  Requires-Dist: google-adk<=2.10.0,>=2.5.0; extra == "all"
102
- Requires-Dist: claude-agent-sdk<=0.2.160,>=0.2.126; extra == "all"
102
+ Requires-Dist: claude-agent-sdk<=0.2.162,>=0.2.126; extra == "all"
103
103
  Requires-Dist: smolagents==1.26.0; extra == "all"
104
104
  Provides-Extra: dev
105
105
  Requires-Dist: pytest>=8.0.0; extra == "dev"
@@ -177,16 +177,51 @@ agenticdome inspect --output agenticdome-inspection.json
177
177
 
178
178
  `init` writes the secret-free inspection and local configuration. For the
179
179
  certified onboarding path, obtain the assigned sidecar and a dedicated
180
- `integration_copilot` key, run `agenticdome integrate preview`, review its
180
+ `integration_copilot` key, run `agenticdome integrate preview` and
181
+ `agenticdome integrate status`, review the saved
181
182
  local exact diff and change summary, explicitly approve a supported edit or
182
183
  attach other boundaries manually, then run
183
184
  `agenticdome verify --run-tests --output .agenticdome/verification.json`.
184
185
  Preview obtains the private, signed, catalog-bound semantic plan and keeps
185
186
  application source on your machine. `agenticdome integrate apply` creates a
186
187
  local Git review branch but never commits, pushes or deploys. Existing-source
187
- automatic edits are currently limited to one exact certified smolagents run
188
- pattern; all other paths need manual integration. The older `agenticdome plan`
188
+ automatic edits cover unambiguous CrewAI bootstrap, PydanticAI `Agent(...)`,
189
+ LangChain `create_agent(...)`, Google ADK agent construction, Claude options,
190
+ Agno `Agent(...)`, direct LlamaIndex `FunctionTool.from_defaults(...)`, direct
191
+ OpenAI Agents `FunctionTool(...)` handlers, explicit custom-Python tool
192
+ dispatchers, and the exact smolagents run pattern.
193
+ The CLI leaves dynamic construction,
194
+ existing handwritten AgenticDome calls, MCP forwarding and other framework
195
+ paths for explicit review; a patch never proves all routes are intercepted.
196
+ The older `agenticdome plan`
189
197
  and `agenticdome scaffold` remain available as advanced review-only commands.
198
+ Preview auto-selects an application or pure-MCP review path and reports
199
+ existing Python SDK call-site candidates, including handwritten local
200
+ wrappers. Discovery uses the versioned framework hook catalog plus documented
201
+ prompt, tool, A2A/delegation, MCP and output API methods; categories may
202
+ overlap and TypeScript call sites are not parsed by this Python checker. Mixed
203
+ application/MCP workloads get a separate MCP review path.
204
+ Each preview rescans the selected workload. If code added a framework after
205
+ `init`, its certified hook contract is included in the new plan and flagged for
206
+ review without changing `.agenticdome/config.json`; a framework still in that
207
+ config but absent from the current scan is flagged for confirmation. Run
208
+ `agenticdome integrate status` for the saved next action and import a fresh
209
+ `agenticdome inspect --output agenticdome-inspection.json` after reviewing it.
210
+ These static observations do not prove interception; the CLI avoids
211
+ overlapping automatic rewrites and requires real blocked-action tests.
212
+
213
+ For one consequential tool, run your own safe integration test through the
214
+ actual application route with an `ExecutionSpy` around its test handler, then
215
+ correlate the SDK adapter call, live-mode verdict and handler invocation:
216
+
217
+ ```bash
218
+ agenticdome verify-action --tool billing.refund --expect-verdict BLOCKED --expect-executed no -- pytest -q tests/test_blocked_refund.py
219
+ ```
220
+
221
+ The test must call `spy.assert_calls(0)` (or `1` for an allowed test); see the
222
+ [real action-path test guide](docs/customer-onboarding.md#prove-one-real-action-path).
223
+ This opt-in command does not execute a business action itself, and a fixed
224
+ `agenticdome verify` probe is not a substitute for the real-path test.
190
225
 
191
226
  For an MCP client, host, gateway or server workload, use the focused path:
192
227
 
@@ -224,6 +259,7 @@ openclaw plugins install npm:agenticdome-openclaw-security
224
259
  openclaw plugins enable agenticdome-security
225
260
  openclaw config set plugins.entries.agenticdome-security.hooks.allowConversationAccess true
226
261
  openclaw gateway restart
262
+ openclaw gateway status --require-rpc
227
263
  agenticdome openclaw protect
228
264
  # With AGENTICDOME_API_BASE, AGENTICDOME_TENANT_ID and the dedicated
229
265
  # AGENTICDOME_COPILOT_API_KEY set, generate and review the signed plan:
@@ -232,8 +268,11 @@ agenticdome plan
232
268
  agenticdome openclaw verify --output .agenticdome/verification.json
233
269
  ```
234
270
 
235
- `openclaw protect` fails unless the active OpenClaw runtime reports the loaded
236
- AgenticDome plugin, explicit consent and the exact certified typed hooks.
271
+ `openclaw protect` checks the AgenticDome plugin, explicit consent and exact
272
+ typed hooks in the inspecting CLI process. `gateway status` checks the serving
273
+ Gateway's RPC reachability; neither proves that this Gateway ran a hook on a
274
+ customer tool call. Exercise a safe tool through the actual Gateway and inspect
275
+ the matching AgenticDome runtime decision before claiming that route is protected.
237
276
  The official `openclaw plugins install`, `enable`, `config set` and gateway
238
277
  restart commands are the actual runtime configuration change; the Python CLI
239
278
  does not apply or undo them. No application source patch is needed for native
@@ -246,7 +285,8 @@ includes the MCP transport rehearsal and both `mcp_verification` and
246
285
  `openclaw_verification` in one report. Both sections must be ready before the
247
286
  report is imported; `mcp verify` alone is not a combined-workload proof. The
248
287
  customer Control Panel separately confirms tenant binding and retained
249
- telemetry; neither command silently modifies customer source.
288
+ telemetry for its probes; neither command silently modifies customer source or
289
+ proves that every live Gateway tool route was intercepted.
250
290
 
251
291
  When `.agenticdome/inspection.json` is imported into Developer Integration,
252
292
  the Control Panel adds its detected frameworks to the workload. Frameworks,
@@ -1582,6 +1622,13 @@ async def lookup_customer(customer_id: str) -> str:
1582
1622
  )
1583
1623
  ```
1584
1624
 
1625
+ If you construct `agents.FunctionTool` directly, its `on_invoke_tool`
1626
+ handler receives JSON text. Use `handler_args_format="json"` with
1627
+ `wrap_tool_handler` so the approved or sanitized arguments reach the original
1628
+ handler in that same format. The guided integration preview offers this edit
1629
+ only for a direct, statically named `FunctionTool` with an identifiable
1630
+ handler; other tool factories require manual review and a real-route test.
1631
+
1585
1632
  **Delegated specialist tool pattern:**
1586
1633
 
1587
1634
  ```python
@@ -1,42 +1,46 @@
1
- agenticdome_python_sdk-1.2.35.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
2
- agenticdome_python_sdk-1.2.35.dist-info/licenses/NOTICE,sha256=obvKP6kV1Ny5R8_INFOpf0X5um9U2X43LGwQFobO9kg,550
3
- agenticdome_sdk/__init__.py,sha256=UY__0ppEM4MlsTQoLh8alZ7AoMjtegGKe4j_Sj7CpNk,903
1
+ agenticdome_python_sdk-1.2.36.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
2
+ agenticdome_python_sdk-1.2.36.dist-info/licenses/NOTICE,sha256=obvKP6kV1Ny5R8_INFOpf0X5um9U2X43LGwQFobO9kg,550
3
+ agenticdome_sdk/__init__.py,sha256=1JY5ELhtp0zg-82xhgt6NxZ7k5CWL5e_Tm6BrsuCFXc,1102
4
4
  agenticdome_sdk/_framework_firewall.py,sha256=W3gy7WsK50scE7ypJrmVJhHUOBsr0zAWQ85rghqaJk4,31963
5
5
  agenticdome_sdk/_mode.py,sha256=XvOgh5sW-FG8S39V483h3uLiFh8wSt14OmYXz_6eDO8,1283
6
+ agenticdome_sdk/action_path_proof.py,sha256=5Zlghy7-LNhtRJM0Unb4NPUASzIsAC0LaWNVFR2yzzo,13497
6
7
  agenticdome_sdk/agno.py,sha256=L3yfjTBHW47xzT67a1DsjtwmvrAoYBkbxADpsr85Cp0,60766
7
8
  agenticdome_sdk/attestation.py,sha256=ij4cMff7nzLaIS0W02d2WURFcyKe72bcUlSz-dYkaLE,10310
8
9
  agenticdome_sdk/autogen.py,sha256=KP4qlIcsNXB-4ZT9D7Slc1xWEeWXRIWkc5HGW0U6k2c,29018
9
10
  agenticdome_sdk/aws_bedrock.py,sha256=2zD9j7xi0jna0sMeMf-hKZdc3Y4RPwzj6tMVOUGgIDQ,69581
10
- agenticdome_sdk/catalog_snapshot.json,sha256=fPbiJC7IjrPyp8ORunaNQenLCg11W43XIDHcMlINFEg,951
11
+ agenticdome_sdk/catalog_snapshot.json,sha256=xHntC0Cjzli4pP0fyTxVtbH90LthGgaq7ad6YGmy54Q,951
11
12
  agenticdome_sdk/claude.py,sha256=XeY9AUu61SYcJjQVz_AtL_WaN6X4KqP5nprEbbr4Q6A,17456
12
- agenticdome_sdk/client.py,sha256=2uVKZR1uXDBIvwOS-lpNPCAXl9K5vroPqsPNNYhBWeg,69473
13
+ agenticdome_sdk/client.py,sha256=SQk7BROrcedyHAfubGiwGNNGycq7W7L-Ncjroj1ugBg,69815
13
14
  agenticdome_sdk/connect.py,sha256=MY30wXYA6OaZhs__E67KcFXEy7mZMBxMSV06BnlBjRg,18766
14
15
  agenticdome_sdk/copilot_ir.py,sha256=zT13tOX8xu11yrOtIPMRFVud8CQ81Gu3WxsXxyQ4XT0,17928
15
16
  agenticdome_sdk/copilot_ir_collector.cjs,sha256=oxLY8ByONo7Es4YjYiJuRz_A7IKL65Sy486UXnu-4Rs,9681
16
17
  agenticdome_sdk/crewai.py,sha256=sSJQ49O7-8kqOAD2_jrsMtMOmBOcJmFkeOv7S3-ayMs,52490
17
18
  agenticdome_sdk/demo.py,sha256=dyfqt2N_cJoyUWKnjPYuQxuw2hb2DrCNUf1jQKeYYoc,8350
19
+ agenticdome_sdk/existing_integration.py,sha256=be6xFqe3P9d98YOydHzDeyRSbuAim4bLBrfafLOJpJM,11465
20
+ agenticdome_sdk/framework_edits.py,sha256=4BgWHxWKEkex8_DMhjVxjovpUVkrMF7TsCZzlS94mpA,23344
21
+ agenticdome_sdk/generic_python.py,sha256=JtXRcYEZ6_Dgxnnv4QgqeMZR8dFVr8EqFDM-XYQ-GMA,6823
18
22
  agenticdome_sdk/google_adk.py,sha256=2TA-tUhY0lOnkGcs1BN9CUEz67ebVX4HJ7mnNoRPcZs,57258
19
- agenticdome_sdk/guided_integration.py,sha256=JQ5THutpsC1J0yV7gEOJWiLHGLQOKhAlPDFTi5SH5OA,36595
20
- agenticdome_sdk/hook_catalog.py,sha256=0dfFWNDS0KCeP0IozWZb1NkNBWf1omaO2-yZKNVdTf0,29272
23
+ agenticdome_sdk/guided_integration.py,sha256=m7EsgLrPEJhfrVMaVWO2DtjYp6S8PwpCKDYZPNbVZ2o,52259
24
+ agenticdome_sdk/hook_catalog.py,sha256=4cQvWsuPa2nOZdlhp1Ya6SDo2M83aSHHtiLxrM3dkz8,29272
21
25
  agenticdome_sdk/identity.py,sha256=mrymGgZSVsKJ9THRde54qGmMT7j_uZHmPJ-HtWk7PXE,4635
22
26
  agenticdome_sdk/langgraph.py,sha256=FUvF6DSA5aZP3eoBoNeldgGtPrz-dO5xcVmrPPSBEhI,84312
23
27
  agenticdome_sdk/lifecycle.py,sha256=9LvxmUU1LC_tMchb5uVFBnZIkVFUxOv3VH9wG_9gPtA,12855
24
28
  agenticdome_sdk/llamaindex.py,sha256=BHplSUIsgMi7k8tBztprzaV9iaOXiljXbdeMRax_2h8,42860
25
- agenticdome_sdk/mcp_host.py,sha256=bJG0pCfMYkI6ig4tmPvtJKcdFFfDOXNmatLbh_X9lso,63283
26
- agenticdome_sdk/mcp_http_gateway.py,sha256=j7B_IVhr0QLSPdsE8iVKcH0mCtQN7EE622JKY3d2LHI,13533
29
+ agenticdome_sdk/mcp_host.py,sha256=T9ICAyz8SjOhssuhXaiEuav5fduWriDhuZ4U_e8Jh9w,69389
30
+ agenticdome_sdk/mcp_http_gateway.py,sha256=XrTgj46ZHIKJ9xS3EQPsac2C9OGUZnrOXomcLdkMk_Y,13629
27
31
  agenticdome_sdk/mcp_rehearsal_server.py,sha256=cox4XqUTL8d0gaDzqxDZcEALkaz8otjOjOqHvoUeeCo,2118
28
- agenticdome_sdk/mcp_verification.py,sha256=VqQS5SpPVFI3G7qCwrT90JmG8vFE6pRJEB7PViHjKqw,14235
32
+ agenticdome_sdk/mcp_verification.py,sha256=WtRhAFVPTBqGOcHNf_db1m4jX9_TO0oxP98XWk4_ISY,14551
29
33
  agenticdome_sdk/microsoft_agent_framework.py,sha256=GfIIuk0n-VhsZaTyoB7P6ilpz9bFLD95TWuX0rFSWeU,67219
30
34
  agenticdome_sdk/microsoft_ai_foundry.py,sha256=YlO968s1dbAYcaEqhqufL92NFR66GuTtngnCv7-1mYc,56911
31
- agenticdome_sdk/onboarding_cli.py,sha256=1g-wNEzEpAmR1s-oYS6mqCd0TgT3PQuEqNQk9M0hAQc,124440
32
- agenticdome_sdk/openai_agents.py,sha256=nNM-YcsUDcE23sGv1buLudcYFz0NLju2Mwiw_wrVaJg,57326
35
+ agenticdome_sdk/onboarding_cli.py,sha256=jfc4owNPrvlVkYa5eLaQfATA6sbgXGkhxf4IJfg6gWo,145081
36
+ agenticdome_sdk/openai_agents.py,sha256=0z-7FDBIG4usxnILpan6yjQHF4zXIzm9YLsh3-bW8Lw,57678
33
37
  agenticdome_sdk/outcomes.py,sha256=jadWDzQaZXrHNvpPme8lQY0jihYLCoM3s4CihAq0JRo,2507
34
38
  agenticdome_sdk/proof.py,sha256=ECKc14xSE0V1HHmcpZjfVz8-Q2H_fooah3BfdblFV8M,2878
35
39
  agenticdome_sdk/pydantic.py,sha256=WKb8X0Fgydfid-ep8E-M8Yslz2pWcCP6w7n_aBxt1rI,54961
36
40
  agenticdome_sdk/simulation.py,sha256=UCbZy3u9AzfxQCfY38SCV3CxGxhinV9EA4aHMPlK4mc,8426
37
41
  agenticdome_sdk/smolagents.py,sha256=wjMA1HVAQ-9wRNM38Jr9wlCpKMhF-9vxtTk6R-kwQhs,19094
38
- agenticdome_python_sdk-1.2.35.dist-info/METADATA,sha256=Fn0YW1txEeYaaqT9hykQc-38Qe5IkqwLlcoHreKeS2M,141741
39
- agenticdome_python_sdk-1.2.35.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
40
- agenticdome_python_sdk-1.2.35.dist-info/entry_points.txt,sha256=cRfCphkr5gLrkGIzXJEtL50HdHLajoZLbZtcMLE8NoY,113
41
- agenticdome_python_sdk-1.2.35.dist-info/top_level.txt,sha256=LZbbjXco5mqDrGwlTJpIKI5Sp6R_njzHHjExxAs9c3s,16
42
- agenticdome_python_sdk-1.2.35.dist-info/RECORD,,
42
+ agenticdome_python_sdk-1.2.36.dist-info/METADATA,sha256=4gqvhEqeY-7JwahLP92qIULZ8G8-T5eaQsKUL0eqhUk,144718
43
+ agenticdome_python_sdk-1.2.36.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
44
+ agenticdome_python_sdk-1.2.36.dist-info/entry_points.txt,sha256=cRfCphkr5gLrkGIzXJEtL50HdHLajoZLbZtcMLE8NoY,113
45
+ agenticdome_python_sdk-1.2.36.dist-info/top_level.txt,sha256=LZbbjXco5mqDrGwlTJpIKI5Sp6R_njzHHjExxAs9c3s,16
46
+ agenticdome_python_sdk-1.2.36.dist-info/RECORD,,
@@ -3,6 +3,8 @@ from .identity import IDENTITY_CONTEXT_VERSION, canonicalize_identity_context, e
3
3
  from .proof import create_dpop_proof, generate_rsa_proof_key, jwk_thumbprint
4
4
  from ._mode import LIVE_MODE, LOCAL_SIM_MODE, is_local_sim_mode, resolve_mode
5
5
  from .lifecycle import VerifiedActionContext, VerifiedActionDenied, VerifiedActionReporter, verified_action
6
+ from .action_path_proof import ExecutionSpy
7
+ from .generic_python import AgenticDomeActionDenied, guarded_tool_executor
6
8
 
7
9
  __all__ = [
8
10
  "AgenticDomeClient",
@@ -22,4 +24,7 @@ __all__ = [
22
24
  "VerifiedActionDenied",
23
25
  "VerifiedActionReporter",
24
26
  "verified_action",
27
+ "ExecutionSpy",
28
+ "AgenticDomeActionDenied",
29
+ "guarded_tool_executor",
25
30
  ]
@@ -0,0 +1,263 @@
1
+ """Opt-in, source-free evidence for a customer-run action-path test.
2
+
3
+ The test must install ExecutionSpy around the actual business handler. A
4
+ sidecar verdict on its own never proves that the handler did or did not run.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import functools
10
+ import hashlib
11
+ import inspect
12
+ import json
13
+ import os
14
+ import re
15
+ import stat
16
+ import subprocess
17
+ import sys
18
+ import tempfile
19
+ import time
20
+ from datetime import datetime, timezone
21
+ from pathlib import Path
22
+ from typing import Any, Callable, Dict, Optional, Sequence, Tuple
23
+
24
+
25
+ _ADAPTERS = frozenset({
26
+ "crewai", "pydantic", "langgraph", "microsoft_agent_framework",
27
+ "autogen", "microsoft_ai_foundry", "openai_agents", "claude",
28
+ "smolagents", "agno", "google_adk", "llamaindex", "aws_bedrock",
29
+ "mcp_host", "generic_python", "_framework_firewall",
30
+ })
31
+
32
+
33
+ def _hook_frame() -> Tuple[str, str]:
34
+ frame = sys._getframe(2)
35
+ generic = ("", "")
36
+ try:
37
+ while frame:
38
+ module = str(frame.f_globals.get("__name__", ""))
39
+ if module.startswith("agenticdome_sdk.") and module.rsplit(".", 1)[-1] in _ADAPTERS:
40
+ if module != "agenticdome_sdk._framework_firewall":
41
+ return module, frame.f_code.co_name
42
+ if not generic[0]:
43
+ generic = (module, frame.f_code.co_name)
44
+ frame = frame.f_back
45
+ finally:
46
+ del frame
47
+ return generic
48
+
49
+
50
+ def _append_event(event: Dict[str, Any]) -> None:
51
+ path = os.environ.get("AGENTICDOME_ACTION_PROOF_FILE", "")
52
+ if not path or not os.path.isabs(path):
53
+ return
54
+ event = {"schema": "agenticdome.action-path-event.v1", **event}
55
+ encoded = (json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
56
+ if len(encoded) > 2048:
57
+ return
58
+ flags = os.O_WRONLY | os.O_APPEND | os.O_CREAT
59
+ if hasattr(os, "O_NOFOLLOW"):
60
+ flags |= os.O_NOFOLLOW
61
+ try:
62
+ fd = os.open(path, flags, 0o600)
63
+ try:
64
+ info = os.fstat(fd)
65
+ if not stat.S_ISREG(info.st_mode) or info.st_size > 262144:
66
+ return
67
+ os.write(fd, encoded)
68
+ finally:
69
+ os.close(fd)
70
+ except OSError:
71
+ # Proof is optional instrumentation; it must not change enforcement.
72
+ return
73
+
74
+
75
+ def record_policy_decision(*, tool_name: Optional[str], platform: Optional[str],
76
+ direction: str, response: Dict[str, Any], live: bool) -> None:
77
+ """Record no prompt, arguments, identity, token, or raw response."""
78
+ if not os.environ.get("AGENTICDOME_ACTION_PROOF_FILE") or not tool_name:
79
+ return
80
+ module, function = _hook_frame()
81
+ envelope = response.get("result") if isinstance(response.get("result"), dict) else response
82
+ verdict = str(envelope.get("verdict") or envelope.get("decision") or "UNKNOWN").upper()
83
+ decision_id = str(envelope.get("decision_id") or response.get("decision_id") or "")
84
+ _append_event({
85
+ "kind": "policy_decision", "tool_name": str(tool_name)[:160],
86
+ "platform": str(platform or "")[:80], "direction": str(direction)[:24],
87
+ "verdict": verdict if verdict in {"ALLOWED", "BLOCKED", "REDACTED"} else "UNKNOWN",
88
+ "live": bool(live), "hook_module": module, "hook_function": function,
89
+ "decision_id": decision_id if re.fullmatch(r"[A-Za-z0-9._:-]{1,100}", decision_id) else "",
90
+ })
91
+
92
+
93
+ class ExecutionSpy:
94
+ """Wrap the real test handler, then assert its invocation count in the test."""
95
+
96
+ def __init__(self, tool_name: str):
97
+ if not tool_name or len(tool_name) > 160:
98
+ raise ValueError("tool_name must contain 1-160 characters")
99
+ self.tool_name = tool_name
100
+ self.calls = 0
101
+
102
+ def wrap(self, handler: Callable[..., Any]) -> Callable[..., Any]:
103
+ if inspect.iscoroutinefunction(handler):
104
+ @functools.wraps(handler)
105
+ async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
106
+ self.calls += 1
107
+ _append_event({"kind": "handler_attempt", "tool_name": self.tool_name})
108
+ return await handler(*args, **kwargs)
109
+ return async_wrapper
110
+
111
+ @functools.wraps(handler)
112
+ def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
113
+ self.calls += 1
114
+ _append_event({"kind": "handler_attempt", "tool_name": self.tool_name})
115
+ return handler(*args, **kwargs)
116
+ return sync_wrapper
117
+
118
+ def assert_calls(self, expected: int) -> None:
119
+ if not isinstance(expected, int) or expected < 0:
120
+ raise ValueError("expected must be a non-negative integer")
121
+ if self.calls != expected:
122
+ raise AssertionError(f"Expected {expected} handler calls; observed {self.calls}")
123
+ _append_event({"kind": "handler_assertion", "tool_name": self.tool_name, "calls": self.calls})
124
+
125
+
126
+ def run_action_path_test(root: Path, *, tool_name: str, expected_verdict: str,
127
+ expected_calls: int, command: Sequence[str],
128
+ timeout_seconds: int = 600) -> Tuple[int, Dict[str, Any]]:
129
+ """Run an explicit customer test command, never a shell, and inspect evidence."""
130
+ if not tool_name or len(tool_name) > 160:
131
+ raise ValueError("tool_name must contain 1-160 characters")
132
+ expected_verdict = expected_verdict.upper()
133
+ if expected_verdict not in {"ALLOWED", "BLOCKED", "REDACTED"}:
134
+ raise ValueError("expected_verdict must be ALLOWED, BLOCKED or REDACTED")
135
+ if expected_calls not in {0, 1}:
136
+ raise ValueError("expected_calls must be 0 or 1 for an isolated action test")
137
+ if (expected_verdict == "BLOCKED" and expected_calls != 0) or (expected_verdict != "BLOCKED" and expected_calls != 1):
138
+ raise ValueError("Blocked actions must not execute; allowed/redacted actions need one observed test-handler call")
139
+ if not command or command[0] == "--" or any(not item or "\x00" in item for item in command):
140
+ raise ValueError("Provide one explicit test command after --; a shell is not used")
141
+ required = ("AGENTICDOME_API_BASE", "AGENTICDOME_API_KEY", "AGENTICDOME_TENANT_ID")
142
+ missing = [key for key in required if not os.environ.get(key, "").strip()]
143
+ if missing:
144
+ raise ValueError("Live action-path proof requires " + ", ".join(missing))
145
+ if os.environ.get("AGENTICDOME_MODE", "").strip().lower() in {"local_sim", "simulation", "sim"}:
146
+ raise ValueError("Action-path proof requires a live assigned sidecar, not local simulation")
147
+ started = time.monotonic()
148
+ with tempfile.TemporaryDirectory(prefix="agenticdome-action-proof-") as temporary:
149
+ journal = Path(temporary) / "events.jsonl"
150
+ environment = os.environ.copy()
151
+ environment["AGENTICDOME_ACTION_PROOF_FILE"] = str(journal)
152
+ try:
153
+ completed = subprocess.run(list(command), cwd=root, env=environment,
154
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
155
+ timeout=timeout_seconds, check=False)
156
+ test_exit = completed.returncode
157
+ timeout = False
158
+ except subprocess.TimeoutExpired:
159
+ test_exit = None
160
+ timeout = True
161
+ events = []
162
+ if journal.exists():
163
+ for line in journal.read_text(encoding="utf-8").splitlines()[:200]:
164
+ try:
165
+ value = json.loads(line)
166
+ except json.JSONDecodeError:
167
+ continue
168
+ if isinstance(value, dict) and value.get("schema") == "agenticdome.action-path-event.v1":
169
+ events.append(value)
170
+ decisions = [item for item in events if item.get("kind") == "policy_decision" and item.get("tool_name") == tool_name]
171
+ attempts = [item for item in events if item.get("kind") == "handler_attempt" and item.get("tool_name") == tool_name]
172
+ assertions = [item for item in events if item.get("kind") == "handler_assertion" and item.get("tool_name") == tool_name]
173
+ decision = decisions[0] if len(decisions) == 1 else {}
174
+ assertion = assertions[0] if len(assertions) == 1 else {}
175
+ reasons = []
176
+ if timeout:
177
+ reasons.append("The customer test timed out")
178
+ elif test_exit != 0:
179
+ reasons.append("The customer test did not pass")
180
+ if len(decisions) != 1:
181
+ reasons.append("Expected exactly one SDK tool decision for this action")
182
+ elif not decision.get("live") or decision.get("direction") != "output":
183
+ reasons.append("The decision was not a live outbound tool decision")
184
+ elif not decision.get("hook_module"):
185
+ reasons.append("No certified SDK framework hook was observed; a direct client call is insufficient")
186
+ elif decision.get("verdict") != expected_verdict:
187
+ reasons.append("Observed verdict differs from the expected verdict")
188
+ if len(assertions) != 1 or assertion.get("calls") != expected_calls or len(attempts) != expected_calls:
189
+ reasons.append("The real-handler spy did not establish the expected execution count")
190
+ elif len(decisions) == 1:
191
+ decision_index = events.index(decision)
192
+ assertion_index = events.index(assertion)
193
+ if assertion_index <= decision_index or any(events.index(attempt) <= decision_index or events.index(attempt) >= assertion_index for attempt in attempts):
194
+ reasons.append("Handler evidence was not ordered after the policy decision and before the test assertion")
195
+ report = {
196
+ "schema": "agenticdome.action-path-proof.v1", "status": "passed" if not reasons else "incomplete",
197
+ "tool_name": tool_name, "expected_verdict": expected_verdict,
198
+ "observed_verdict": decision.get("verdict"),
199
+ "decision_id": decision.get("decision_id") or None,
200
+ "hook_module": decision.get("hook_module"), "hook_function": decision.get("hook_function"),
201
+ "expected_handler_calls": expected_calls,
202
+ "observed_handler_calls": len(attempts),
203
+ "handler_assertion_passed": bool(assertion) and assertion.get("calls") == expected_calls,
204
+ "customer_test_exit_code": test_exit, "timed_out": timeout,
205
+ "duration_ms": round((time.monotonic() - started) * 1000),
206
+ "observed_at": datetime.now(timezone.utc).isoformat(),
207
+ "reasons": reasons,
208
+ "next_action": (
209
+ "Run the same safe test command directly to inspect its logs, then check the selected hook and spy placement. No test output was retained."
210
+ if reasons else
211
+ "Review the corresponding retained sidecar decision and repeat with an allowed/blocked counterpart before promoting this route."
212
+ ),
213
+ "qualification": "The SDK hook and live verdict were observed locally. Handler execution is established by a customer-installed spy around the actual test handler; verify that this test exercises the production route. No business action is run by the CLI itself.",
214
+ }
215
+ report["proof_sha256"] = hashlib.sha256(
216
+ json.dumps(report, sort_keys=True, separators=(",", ":")).encode("utf-8")
217
+ ).hexdigest()
218
+ return (0 if not reasons else 2), report
219
+
220
+
221
+ def load_action_path_proof(root: Path) -> Optional[Dict[str, Any]]:
222
+ """Load a current, intact customer-run report without trusting free text."""
223
+ path = root / ".agenticdome" / "action-proof.json"
224
+ try:
225
+ if path.is_symlink() or not path.is_file() or path.stat().st_size > 16384:
226
+ return None
227
+ report = json.loads(path.read_text(encoding="utf-8"))
228
+ if not isinstance(report, dict) or report.get("schema") != "agenticdome.action-path-proof.v1":
229
+ return None
230
+ expected = str(report.get("proof_sha256") or "")
231
+ source = {key: value for key, value in report.items() if key != "proof_sha256"}
232
+ digest = hashlib.sha256(json.dumps(source, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
233
+ if digest != expected:
234
+ return None
235
+ observed_at = datetime.fromisoformat(str(report.get("observed_at") or ""))
236
+ age = datetime.now(timezone.utc) - observed_at.astimezone(timezone.utc)
237
+ if age.total_seconds() < -300 or age.total_seconds() > 7 * 86400:
238
+ return None
239
+ if report.get("status") != "passed" or report.get("hook_module") not in {
240
+ "agenticdome_sdk." + adapter for adapter in _ADAPTERS
241
+ }:
242
+ return None
243
+ if report.get("observed_verdict") not in {"ALLOWED", "BLOCKED", "REDACTED"}:
244
+ return None
245
+ if report.get("expected_handler_calls") not in {0, 1} or report.get("observed_handler_calls") != report.get("expected_handler_calls"):
246
+ return None
247
+ return {
248
+ "schema": "agenticdome.action-path-proof.v1", "status": "passed",
249
+ "tool_name": str(report.get("tool_name") or "")[:160],
250
+ "observed_verdict": report["observed_verdict"],
251
+ "decision_id": str(report.get("decision_id") or "")[:100] or None,
252
+ "hook_module": report["hook_module"],
253
+ "hook_function": str(report.get("hook_function") or "")[:100],
254
+ "observed_handler_calls": report["observed_handler_calls"],
255
+ "observed_at": report["observed_at"],
256
+ "proof_sha256": expected,
257
+ "evidence_origin": "customer_test",
258
+ }
259
+ except (OSError, ValueError, TypeError, OverflowError, json.JSONDecodeError):
260
+ return None
261
+
262
+
263
+ __all__ = ["ExecutionSpy", "run_action_path_test", "record_policy_decision", "load_action_path_proof"]
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "schema": "agenticdome.catalog-snapshot.v1",
3
3
  "catalog_schema": "agenticdome.hook-catalog.v1",
4
- "verified_at": "2026-09-26",
5
- "python_sdk_version": "1.2.35",
4
+ "verified_at": "2026-09-29",
5
+ "python_sdk_version": "1.2.36",
6
6
  "published_packages": {
7
7
  "agenticdome-python-sdk": {
8
8
  "registry": "pypi",
9
- "version": "1.2.35",
10
- "url": "https://pypi.org/project/agenticdome-python-sdk/1.2.35/"
9
+ "version": "1.2.36",
10
+ "url": "https://pypi.org/project/agenticdome-python-sdk/1.2.36/"
11
11
  },
12
12
  "agenticdome-sdk": {
13
13
  "registry": "npm",
agenticdome_sdk/client.py CHANGED
@@ -805,6 +805,12 @@ class AgenticDomeClient:
805
805
  raise AgenticDomeError(
806
806
  "AgenticDome execution broker did not return a verified, atomically consumed decision"
807
807
  )
808
+ if tool_name and os.environ.get("AGENTICDOME_ACTION_PROOF_FILE"):
809
+ from .action_path_proof import record_policy_decision
810
+ record_policy_decision(
811
+ tool_name=tool_name, platform=platform, direction=normalized_direction,
812
+ response=response, live=not self.is_simulation,
813
+ )
808
814
  return response
809
815
 
810
816
  @staticmethod