agenticdome-python-sdk 1.2.6__tar.gz → 1.2.8__tar.gz

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.
Files changed (52) hide show
  1. {agenticdome_python_sdk-1.2.6/agenticdome_python_sdk.egg-info → agenticdome_python_sdk-1.2.8}/PKG-INFO +1 -1
  2. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8/agenticdome_python_sdk.egg-info}/PKG-INFO +1 -1
  3. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_python_sdk.egg-info/SOURCES.txt +1 -0
  4. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/_framework_firewall.py +37 -2
  5. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/client.py +234 -6
  6. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/pyproject.toml +1 -1
  7. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_autogen_integration.py +22 -0
  8. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_client.py +79 -0
  9. agenticdome_python_sdk-1.2.8/tests/test_execution_broker.py +75 -0
  10. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/LICENSE +0 -0
  11. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/MANIFEST.in +0 -0
  12. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/README.md +0 -0
  13. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_python_sdk.egg-info/dependency_links.txt +0 -0
  14. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_python_sdk.egg-info/requires.txt +0 -0
  15. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_python_sdk.egg-info/top_level.txt +0 -0
  16. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/__init__.py +0 -0
  17. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/agno.py +0 -0
  18. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/autogen.py +0 -0
  19. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/aws_bedrock.py +0 -0
  20. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/claude.py +0 -0
  21. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/crewai.py +0 -0
  22. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/google_adk.py +0 -0
  23. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/identity.py +0 -0
  24. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/langgraph.py +0 -0
  25. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/llamaindex.py +0 -0
  26. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/mcp_host.py +0 -0
  27. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/microsoft_agent_framework.py +0 -0
  28. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/microsoft_ai_foundry.py +0 -0
  29. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/openai_agents.py +0 -0
  30. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/proof.py +0 -0
  31. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/pydantic.py +0 -0
  32. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/agenticdome_sdk/smolagents.py +0 -0
  33. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/examples/attack_demo.py +0 -0
  34. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/examples/crewai_agenticdome_firewall.py +0 -0
  35. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/setup.cfg +0 -0
  36. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/setup.py +0 -0
  37. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_agno_integration.py +0 -0
  38. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_attack_demo.py +0 -0
  39. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_aws_bedrock_integration.py +0 -0
  40. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_claude_integration.py +0 -0
  41. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_crewai_integration.py +0 -0
  42. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_google_adk_integration.py +0 -0
  43. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_identity_context.py +0 -0
  44. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_langgraph_integration.py +0 -0
  45. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_live_tenant.py +0 -0
  46. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_llamaindex_integration.py +0 -0
  47. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_mcp_host_integration.py +0 -0
  48. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_microsoft_agent_framework_integration.py +0 -0
  49. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_microsoft_ai_foundry_integration.py +0 -0
  50. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_openai_agents_integration.py +0 -0
  51. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_pydanticai_integration.py +0 -0
  52. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.8}/tests/test_smolagents_integration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agenticdome-python-sdk
3
- Version: 1.2.6
3
+ Version: 1.2.8
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: Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agenticdome-python-sdk
3
- Version: 1.2.6
3
+ Version: 1.2.8
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: Proprietary
@@ -36,6 +36,7 @@ tests/test_aws_bedrock_integration.py
36
36
  tests/test_claude_integration.py
37
37
  tests/test_client.py
38
38
  tests/test_crewai_integration.py
39
+ tests/test_execution_broker.py
39
40
  tests/test_google_adk_integration.py
40
41
  tests/test_identity_context.py
41
42
  tests/test_langgraph_integration.py
@@ -63,6 +63,8 @@ class DecisionTokenRecord:
63
63
  source_agent_id: str
64
64
  created_at: float
65
65
  token_hmac: str = ""
66
+ tool_version: str = ""
67
+ tool_digest: str = ""
66
68
 
67
69
 
68
70
  def _stable_json(value: Any) -> str:
@@ -213,6 +215,22 @@ class FrameworkFirewallBase:
213
215
  except Exception:
214
216
  pass
215
217
 
218
+ def register_tool_provenance(
219
+ self,
220
+ tool_name: str,
221
+ *,
222
+ tool_version: Optional[str] = None,
223
+ tool_digest: Optional[str] = None,
224
+ tool_platform: Optional[str] = None,
225
+ ) -> None:
226
+ """Cache precomputed provenance once for this firewall's client."""
227
+ self.client.register_tool_provenance(
228
+ tool_name,
229
+ tool_version=tool_version,
230
+ tool_digest=tool_digest,
231
+ tool_platform=tool_platform or self.config.default_tool_platform,
232
+ )
233
+
216
234
  @staticmethod
217
235
  def serialize(value: Any) -> str:
218
236
  return value if isinstance(value, str) else _stable_json(value)
@@ -372,6 +390,8 @@ class FrameworkFirewallBase:
372
390
  tool_args: Dict[str, Any],
373
391
  text: str = "",
374
392
  tool_platform: Optional[str] = None,
393
+ tool_version: Optional[str] = None,
394
+ tool_digest: Optional[str] = None,
375
395
  policy_context: Optional[Dict[str, Any]] = None,
376
396
  source_agent_id: Optional[str] = None,
377
397
  ) -> Dict[str, Any]:
@@ -392,6 +412,8 @@ class FrameworkFirewallBase:
392
412
  tool_platform=tool_platform or self.config.default_tool_platform,
393
413
  tool_name=tool_name,
394
414
  tool_args=clean_args,
415
+ tool_version=tool_version,
416
+ tool_digest=tool_digest,
395
417
  policy_context=self._policy_context(session_id, f"{self.config.platform}_tool_call", policy_context, tool_name=tool_name),
396
418
  )
397
419
  if self.verdict(response) == "BLOCKED":
@@ -421,6 +443,8 @@ class FrameworkFirewallBase:
421
443
  tool_args: Dict[str, Any],
422
444
  text: str = "",
423
445
  tool_platform: Optional[str] = None,
446
+ tool_version: Optional[str] = None,
447
+ tool_digest: Optional[str] = None,
424
448
  policy_context: Optional[Dict[str, Any]] = None,
425
449
  ) -> Dict[str, Any]:
426
450
  clean_args = self.strip_private_args(tool_args)
@@ -435,6 +459,8 @@ class FrameworkFirewallBase:
435
459
  tool_platform=tool_platform or self.config.default_tool_platform,
436
460
  tool_name=tool_name,
437
461
  tool_args=clean_args,
462
+ tool_version=tool_version,
463
+ tool_digest=tool_digest,
438
464
  session_id=session_id,
439
465
  direction="outbound",
440
466
  policy_context=self._policy_context(
@@ -456,7 +482,7 @@ class FrameworkFirewallBase:
456
482
  target_agent_id=specialist_agent_id,
457
483
  tool_name=tool_name,
458
484
  tool_args=clean_args,
459
- record=DecisionTokenRecord(token, manager_agent_id, time.time(), self._token_hmac(token)),
485
+ record=DecisionTokenRecord(token, manager_agent_id, time.time(), self._token_hmac(token), tool_version or "", tool_digest or ""),
460
486
  ttl_s=self.config.handoff_token_ttl_s,
461
487
  )
462
488
  return env
@@ -475,6 +501,8 @@ class FrameworkFirewallBase:
475
501
  tool_args: Dict[str, Any],
476
502
  decision_token: Optional[str] = None,
477
503
  source_agent_id: Optional[str] = None,
504
+ tool_version: Optional[str] = None,
505
+ tool_digest: Optional[str] = None,
478
506
  ) -> Dict[str, Any]:
479
507
  clean_args = self.strip_private_args(tool_args)
480
508
  record = None
@@ -488,6 +516,12 @@ class FrameworkFirewallBase:
488
516
  if record:
489
517
  decision_token = record.decision_token
490
518
  source_agent_id = source_agent_id or record.source_agent_id
519
+ if tool_version and record.tool_version and tool_version != record.tool_version:
520
+ raise self.denied_error("AgenticDome delegated tool version differs from the authorized version.")
521
+ if tool_digest and record.tool_digest and tool_digest != record.tool_digest:
522
+ raise self.denied_error("AgenticDome delegated tool digest differs from the authorized digest.")
523
+ tool_version = tool_version or record.tool_version or None
524
+ tool_digest = tool_digest or record.tool_digest or None
491
525
  if record and self.config.token_hmac_secret:
492
526
  expected = self._token_hmac(record.decision_token)
493
527
  if not record.token_hmac or not hmac.compare_digest(record.token_hmac, expected):
@@ -500,6 +534,8 @@ class FrameworkFirewallBase:
500
534
  decision_token,
501
535
  tool_name=tool_name,
502
536
  tool_args=clean_args,
537
+ tool_version=tool_version,
538
+ tool_digest=tool_digest,
503
539
  agent_id=specialist_agent_id,
504
540
  source_agent_id=source_agent_id,
505
541
  platform=self.config.platform,
@@ -611,4 +647,3 @@ class FrameworkFirewallBase:
611
647
  def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
612
648
  return self.wrap_tool_handler(tool_name=tool_name or fn.__name__, handler=fn, **options)
613
649
  return decorator
614
-
@@ -1,6 +1,8 @@
1
1
  import base64
2
+ import hashlib
2
3
  import os
3
- from typing import Optional, Dict, List, Any, Tuple, Union
4
+ import re
5
+ from typing import Optional, Dict, List, Any, Tuple, Union, Mapping
4
6
 
5
7
  import requests
6
8
  from requests.adapters import HTTPAdapter
@@ -58,6 +60,8 @@ class AgentGuardClient:
58
60
  pool_maxsize: int = 100,
59
61
  pool_block: bool = False,
60
62
  service_token: Optional[str] = None,
63
+ tool_provenance: Optional[Mapping[str, Mapping[str, str]]] = None,
64
+ execution_broker_mode: Optional[str] = None,
61
65
  ):
62
66
  self.api_base = self._require_nonempty("api_base", api_base).rstrip("/")
63
67
  self.api_key = self._require_nonempty(
@@ -84,6 +88,21 @@ class AgentGuardClient:
84
88
  default=5.0,
85
89
  )
86
90
  self.user_agent = user_agent
91
+ broker_mode = str(
92
+ execution_broker_mode
93
+ if execution_broker_mode is not None
94
+ else os.getenv("AGENTICDOME_EXECUTION_BROKER_MODE", "off")
95
+ ).strip().lower()
96
+ if broker_mode not in {"off", "observe", "enforce"}:
97
+ raise ValueError("execution_broker_mode must be off, observe, or enforce")
98
+ self.execution_broker_mode = broker_mode
99
+ self._tool_provenance: Dict[str, Dict[str, str]] = {}
100
+ for registered_name, provenance in dict(tool_provenance or {}).items():
101
+ self.register_tool_provenance(
102
+ registered_name,
103
+ tool_version=(provenance or {}).get("tool_version") or (provenance or {}).get("version"),
104
+ tool_digest=(provenance or {}).get("tool_digest") or (provenance or {}).get("digest"),
105
+ )
87
106
 
88
107
  self.session = requests.Session()
89
108
  retry = Retry(
@@ -158,6 +177,55 @@ class AgentGuardClient:
158
177
  s = str(value).strip()
159
178
  return s or None
160
179
 
180
+ def register_tool_provenance(
181
+ self,
182
+ tool_name: str,
183
+ *,
184
+ tool_version: Optional[str] = None,
185
+ tool_digest: Optional[str] = None,
186
+ tool_platform: Optional[str] = None,
187
+ ) -> None:
188
+ """Register precomputed provenance once; request-time lookup is local O(1)."""
189
+ name = self._require_nonempty("tool_name", tool_name)
190
+ version = self._normalize_optional_string(tool_version)
191
+ digest = self._normalize_optional_string(tool_digest)
192
+ if digest is not None and not re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
193
+ raise ValueError("'tool_digest' must be sha256 followed by 64 lowercase hexadecimal characters")
194
+ if version is None and digest is None:
195
+ raise ValueError("tool_version or tool_digest is required")
196
+ key = f"{self._normalize_optional_string(tool_platform)}:{name}" if tool_platform else name
197
+ self._tool_provenance[key] = {"tool_version": version or "", "tool_digest": digest or ""}
198
+
199
+ def unregister_tool_provenance(self, tool_name: str, *, tool_platform: Optional[str] = None) -> None:
200
+ name = self._require_nonempty("tool_name", tool_name)
201
+ key = f"{self._normalize_optional_string(tool_platform)}:{name}" if tool_platform else name
202
+ self._tool_provenance.pop(key, None)
203
+
204
+ def _resolve_tool_provenance(
205
+ self,
206
+ *,
207
+ tool_name: Optional[str],
208
+ tool_version: Optional[str],
209
+ tool_digest: Optional[str],
210
+ tool_platform: Optional[str] = None,
211
+ policy_context: Optional[Dict[str, Any]] = None,
212
+ ) -> Tuple[Optional[str], Optional[str]]:
213
+ if not tool_name:
214
+ return tool_version, tool_digest
215
+ context = policy_context or {}
216
+ context_provenance = context.get("tool_provenance") if isinstance(context.get("tool_provenance"), dict) else {}
217
+ key = f"{tool_platform}:{tool_name}" if tool_platform else ""
218
+ registered = self._tool_provenance.get(key) or self._tool_provenance.get(str(tool_name)) or {}
219
+ version = self._normalize_optional_string(tool_version) or self._normalize_optional_string(
220
+ context.get("tool_version") or context_provenance.get("tool_version") or context_provenance.get("version") or registered.get("tool_version")
221
+ )
222
+ digest = self._normalize_optional_string(tool_digest) or self._normalize_optional_string(
223
+ context.get("tool_digest") or context_provenance.get("tool_digest") or context_provenance.get("digest") or registered.get("tool_digest")
224
+ )
225
+ if digest is not None and not re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
226
+ raise ValueError("'tool_digest' must be sha256 followed by 64 lowercase hexadecimal characters")
227
+ return version, digest
228
+
161
229
  def _positive_float(self, name: str, value: Any, *, default: float) -> float:
162
230
  try:
163
231
  parsed = float(value)
@@ -257,6 +325,7 @@ class AgentGuardClient:
257
325
  token: str,
258
326
  tool_name: Optional[str] = None,
259
327
  tool_args: Optional[Dict[str, Any]] = None,
328
+ tool_digest: Optional[str] = None,
260
329
  ) -> None:
261
330
  """
262
331
  Validate decision-token verification inputs.
@@ -268,6 +337,8 @@ class AgentGuardClient:
268
337
  raise ValueError("'tool_args' is required when 'tool_name' is provided")
269
338
  if tool_args is not None and not tool_name:
270
339
  raise ValueError("'tool_name' is required when 'tool_args' is provided")
340
+ if tool_digest is not None and not re.fullmatch(r"sha256:[0-9a-f]{64}", str(tool_digest)):
341
+ raise ValueError("'tool_digest' must be sha256 followed by 64 lowercase hexadecimal characters")
271
342
 
272
343
  def _request(
273
344
  self,
@@ -453,6 +524,8 @@ class AgentGuardClient:
453
524
  tool_platform: Optional[str] = None,
454
525
  tool_name: Optional[str] = None,
455
526
  tool_args: Optional[Dict[str, Any]] = None,
527
+ tool_version: Optional[str] = None,
528
+ tool_digest: Optional[str] = None,
456
529
  policy_context: Optional[Dict[str, Any]] = None,
457
530
  reasoning_trace: Optional[str] = None,
458
531
  agent_instance_id: Optional[str] = None,
@@ -465,12 +538,23 @@ class AgentGuardClient:
465
538
  actual_role: Optional[str] = None,
466
539
  source_agent_role: Optional[str] = None,
467
540
  target_agent_role: Optional[str] = None,
541
+ actor_chain: Optional[List[Dict[str, Any]]] = None,
542
+ scopes: Optional[List[str]] = None,
543
+ permissions: Optional[List[str]] = None,
544
+ parent_jti: Optional[str] = None,
545
+ root_jti: Optional[str] = None,
546
+ policy_id: Optional[str] = None,
547
+ policy_version: Optional[str] = None,
548
+ policy_hash: Optional[str] = None,
549
+ proof_thumbprint: Optional[str] = None,
468
550
  redact_pii: Optional[bool] = None,
469
551
  redact_secrets: Optional[bool] = None,
470
552
  block_on_sensitive_output: Optional[bool] = None,
471
553
  trusted_destination_domains: Optional[List[str]] = None,
472
554
  allowed_destination_domains: Optional[List[str]] = None,
473
555
  attachments: Optional[List[str]] = None,
556
+ execution_broker: Optional[bool] = None,
557
+ execution_boundary_id: Optional[str] = None,
474
558
  tenant_id: Optional[Union[str, int]] = None,
475
559
  ) -> Dict[str, Any]:
476
560
  """
@@ -491,6 +575,13 @@ class AgentGuardClient:
491
575
  Delegation rule:
492
576
  - source_agent_id requires source_platform
493
577
  """
578
+ tool_version, tool_digest = self._resolve_tool_provenance(
579
+ tool_name=tool_name,
580
+ tool_version=tool_version,
581
+ tool_digest=tool_digest,
582
+ tool_platform=tool_platform,
583
+ policy_context=policy_context,
584
+ )
494
585
  normalized_direction = self._validate_guardrail_args(
495
586
  text=text,
496
587
  agent_id=agent_id,
@@ -502,6 +593,8 @@ class AgentGuardClient:
502
593
  source_platform=source_platform,
503
594
  user_id=user_id,
504
595
  )
596
+ if tool_digest is not None and not re.fullmatch(r"sha256:[0-9a-f]{64}", str(tool_digest)):
597
+ raise ValueError("'tool_digest' must be sha256 followed by 64 lowercase hexadecimal characters")
505
598
 
506
599
  merged_policy_context = self._merge_policy_context(
507
600
  policy_context,
@@ -511,6 +604,8 @@ class AgentGuardClient:
511
604
  tool_platform=tool_platform,
512
605
  tool_name=tool_name,
513
606
  tool_args=tool_args,
607
+ tool_version=tool_version,
608
+ tool_digest=tool_digest,
514
609
  reasoning_trace=reasoning_trace,
515
610
  agent_instance_id=agent_instance_id,
516
611
  user_id=user_id,
@@ -522,6 +617,15 @@ class AgentGuardClient:
522
617
  actual_role=actual_role,
523
618
  source_agent_role=source_agent_role,
524
619
  target_agent_role=target_agent_role,
620
+ actor_chain=actor_chain,
621
+ scopes=scopes,
622
+ permissions=permissions,
623
+ parent_jti=parent_jti,
624
+ root_jti=root_jti,
625
+ policy_id=policy_id,
626
+ policy_version=policy_version,
627
+ policy_hash=policy_hash,
628
+ proof_thumbprint=proof_thumbprint,
525
629
  redact_pii=redact_pii,
526
630
  redact_secrets=redact_secrets,
527
631
  block_on_sensitive_output=block_on_sensitive_output,
@@ -540,6 +644,8 @@ class AgentGuardClient:
540
644
  "tool_platform": tool_platform,
541
645
  "tool_name": tool_name,
542
646
  "tool_args": tool_args,
647
+ "tool_version": tool_version,
648
+ "tool_digest": tool_digest,
543
649
  "policy_context": merged_policy_context,
544
650
  "reasoning_trace": reasoning_trace,
545
651
  "agent_instance_id": agent_instance_id,
@@ -552,6 +658,15 @@ class AgentGuardClient:
552
658
  "actual_role": actual_role,
553
659
  "source_agent_role": source_agent_role,
554
660
  "target_agent_role": target_agent_role,
661
+ "actor_chain": actor_chain,
662
+ "scopes": scopes,
663
+ "permissions": permissions,
664
+ "parent_jti": parent_jti,
665
+ "root_jti": root_jti,
666
+ "policy_id": policy_id,
667
+ "policy_version": policy_version,
668
+ "policy_hash": policy_hash,
669
+ "proof_thumbprint": proof_thumbprint,
555
670
  "redact_pii": redact_pii,
556
671
  "redact_secrets": redact_secrets,
557
672
  "block_on_sensitive_output": block_on_sensitive_output,
@@ -561,12 +676,33 @@ class AgentGuardClient:
561
676
  }
562
677
  )
563
678
 
564
- return self._request(
565
- "POST",
566
- "/tools/guardrail/validate",
567
- json_body=payload,
568
- tenant_id=tenant_id,
679
+ broker_enabled = bool(
680
+ tool_name
681
+ and (self.execution_broker_mode in {"observe", "enforce"} or execution_broker is True)
569
682
  )
683
+ if broker_enabled:
684
+ boundary_id = self._normalize_optional_string(execution_boundary_id)
685
+ if boundary_id is None:
686
+ material = "|".join([
687
+ str(platform or "unknown"),
688
+ str(agent_id),
689
+ str(tool_name),
690
+ str(session_id or "stateless"),
691
+ ])
692
+ boundary_id = "sdk:" + hashlib.sha256(material.encode("utf-8")).hexdigest()[:32]
693
+ payload["boundary_id"] = boundary_id
694
+
695
+ path = "/tools/execution/authorize" if broker_enabled else "/tools/guardrail/validate"
696
+ response = self._request("POST", path, json_body=payload, tenant_id=tenant_id)
697
+ if broker_enabled:
698
+ broker = response.get("broker") if isinstance(response.get("broker"), dict) else {}
699
+ verified = bool(broker.get("verified")) and bool(broker.get("token_consumed"))
700
+ must_enforce = self.execution_broker_mode == "enforce" or execution_broker is True
701
+ if must_enforce and not verified:
702
+ raise AgentGuardError(
703
+ "AgenticDome execution broker did not return a verified, atomically consumed decision"
704
+ )
705
+ return response
570
706
 
571
707
  # Backward compatibility alias
572
708
  def guardrail_check(
@@ -585,6 +721,13 @@ class AgentGuardClient:
585
721
  policy_context=policy_context,
586
722
  )
587
723
 
724
+ def get_tool_provenance_status(
725
+ self,
726
+ tenant_id: Optional[Union[str, int]] = None,
727
+ ) -> Dict[str, Any]:
728
+ """Return the tenant's signed provenance cache readiness and tool count."""
729
+ return self._request("GET", "/tools/provenance/status", tenant_id=tenant_id)
730
+
588
731
  # ------------------------------------------------------------------
589
732
  # Mesh
590
733
  # ------------------------------------------------------------------
@@ -780,6 +923,8 @@ class AgentGuardClient:
780
923
  source_platform: str,
781
924
  tool_name: str,
782
925
  tool_args: Dict[str, Any],
926
+ tool_version: Optional[str] = None,
927
+ tool_digest: Optional[str] = None,
783
928
  tool_platform: Optional[str] = None,
784
929
  policy_context: Optional[Dict[str, Any]] = None,
785
930
  session_id: Optional[str] = None,
@@ -830,6 +975,15 @@ class AgentGuardClient:
830
975
  self._require_nonempty("source_agent_id", source_agent_id)
831
976
  self._require_nonempty("source_platform", source_platform)
832
977
  self._require_nonempty("tool_name", tool_name)
978
+ tool_version, tool_digest = self._resolve_tool_provenance(
979
+ tool_name=tool_name,
980
+ tool_version=tool_version,
981
+ tool_digest=tool_digest,
982
+ tool_platform=tool_platform,
983
+ policy_context=policy_context,
984
+ )
985
+ if tool_digest is not None and not re.fullmatch(r"sha256:[0-9a-f]{64}", str(tool_digest)):
986
+ raise ValueError("'tool_digest' must be sha256 followed by 64 lowercase hexadecimal characters")
833
987
 
834
988
  normalized_direction = self._validate_guardrail_args(
835
989
  text=text,
@@ -851,6 +1005,8 @@ class AgentGuardClient:
851
1005
  source_agent_id=source_agent_id,
852
1006
  tool_name=tool_name,
853
1007
  tool_args=tool_args,
1008
+ tool_version=tool_version,
1009
+ tool_digest=tool_digest,
854
1010
  request_purpose=request_purpose,
855
1011
  purpose=purpose,
856
1012
  intent=intent,
@@ -887,6 +1043,8 @@ class AgentGuardClient:
887
1043
  "tool_platform": tool_platform,
888
1044
  "tool_name": tool_name,
889
1045
  "tool_args": tool_args,
1046
+ "tool_version": tool_version,
1047
+ "tool_digest": tool_digest,
890
1048
  "policy_context": merged_policy_context,
891
1049
  "source_agent_id": source_agent_id,
892
1050
  "request_purpose": request_purpose,
@@ -937,6 +1095,8 @@ class AgentGuardClient:
937
1095
  *,
938
1096
  tool_name: Optional[str] = None,
939
1097
  tool_args: Optional[Dict[str, Any]] = None,
1098
+ tool_version: Optional[str] = None,
1099
+ tool_digest: Optional[str] = None,
940
1100
  agent_id: Optional[str] = None,
941
1101
  source_agent_id: Optional[str] = None,
942
1102
  platform: Optional[str] = None,
@@ -958,10 +1118,17 @@ class AgentGuardClient:
958
1118
  - platform
959
1119
  - source_agent_id (for delegated sensitive flows)
960
1120
  """
1121
+ tool_version, tool_digest = self._resolve_tool_provenance(
1122
+ tool_name=tool_name,
1123
+ tool_version=tool_version,
1124
+ tool_digest=tool_digest,
1125
+ tool_platform=platform,
1126
+ )
961
1127
  self._validate_decision_verify_args(
962
1128
  token=token,
963
1129
  tool_name=tool_name,
964
1130
  tool_args=tool_args,
1131
+ tool_digest=tool_digest,
965
1132
  )
966
1133
 
967
1134
  payload = self._drop_none(
@@ -969,6 +1136,8 @@ class AgentGuardClient:
969
1136
  "token": token,
970
1137
  "tool_name": tool_name,
971
1138
  "tool_args": tool_args,
1139
+ "tool_version": tool_version,
1140
+ "tool_digest": tool_digest,
972
1141
  "agent_id": agent_id,
973
1142
  "source_agent_id": source_agent_id,
974
1143
  "platform": platform,
@@ -989,6 +1158,8 @@ class AgentGuardClient:
989
1158
  *,
990
1159
  tool_name: Optional[str] = None,
991
1160
  tool_args: Optional[Dict[str, Any]] = None,
1161
+ tool_version: Optional[str] = None,
1162
+ tool_digest: Optional[str] = None,
992
1163
  agent_id: Optional[str] = None,
993
1164
  source_agent_id: Optional[str] = None,
994
1165
  platform: Optional[str] = None,
@@ -1011,10 +1182,17 @@ class AgentGuardClient:
1011
1182
  - platform
1012
1183
  - source_agent_id (for delegated sensitive flows)
1013
1184
  """
1185
+ tool_version, tool_digest = self._resolve_tool_provenance(
1186
+ tool_name=tool_name,
1187
+ tool_version=tool_version,
1188
+ tool_digest=tool_digest,
1189
+ tool_platform=platform,
1190
+ )
1014
1191
  self._validate_decision_verify_args(
1015
1192
  token=token,
1016
1193
  tool_name=tool_name,
1017
1194
  tool_args=tool_args,
1195
+ tool_digest=tool_digest,
1018
1196
  )
1019
1197
 
1020
1198
  arguments = self._drop_none(
@@ -1022,6 +1200,8 @@ class AgentGuardClient:
1022
1200
  "token": token,
1023
1201
  "tool_name": tool_name,
1024
1202
  "tool_args": tool_args,
1203
+ "tool_version": tool_version,
1204
+ "tool_digest": tool_digest,
1025
1205
  "agent_id": agent_id,
1026
1206
  "source_agent_id": source_agent_id,
1027
1207
  "platform": platform,
@@ -1111,6 +1291,8 @@ class AgentGuardClient:
1111
1291
  tool_platform: Optional[str] = None,
1112
1292
  tool_name: Optional[str] = None,
1113
1293
  tool_args: Optional[Dict[str, Any]] = None,
1294
+ tool_version: Optional[str] = None,
1295
+ tool_digest: Optional[str] = None,
1114
1296
  policy_context: Optional[Dict[str, Any]] = None,
1115
1297
  direction: str = "outbound",
1116
1298
  source_agent_id: Optional[str] = None,
@@ -1149,6 +1331,13 @@ class AgentGuardClient:
1149
1331
  Delegation rule:
1150
1332
  - source_agent_id requires source_platform
1151
1333
  """
1334
+ tool_version, tool_digest = self._resolve_tool_provenance(
1335
+ tool_name=tool_name,
1336
+ tool_version=tool_version,
1337
+ tool_digest=tool_digest,
1338
+ tool_platform=tool_platform,
1339
+ policy_context=policy_context,
1340
+ )
1152
1341
  normalized_direction = self._validate_guardrail_args(
1153
1342
  text=text,
1154
1343
  agent_id=agent_id,
@@ -1170,6 +1359,8 @@ class AgentGuardClient:
1170
1359
  user_id=user_id,
1171
1360
  tool_name=tool_name,
1172
1361
  tool_args=tool_args,
1362
+ tool_version=tool_version,
1363
+ tool_digest=tool_digest,
1173
1364
  reasoning_trace=reasoning_trace,
1174
1365
  request_purpose=request_purpose,
1175
1366
  purpose=purpose,
@@ -1195,6 +1386,8 @@ class AgentGuardClient:
1195
1386
  "tool_platform": tool_platform,
1196
1387
  "tool_name": tool_name,
1197
1388
  "tool_args": tool_args,
1389
+ "tool_version": tool_version,
1390
+ "tool_digest": tool_digest,
1198
1391
  "policy_context": merged_policy_context,
1199
1392
  "source_agent_id": source_agent_id,
1200
1393
  "user_id": user_id,
@@ -1214,6 +1407,41 @@ class AgentGuardClient:
1214
1407
  }
1215
1408
  )
1216
1409
 
1410
+ # In broker mode the official MCP adapter uses the same one-request
1411
+ # execution boundary as REST. This preserves immediate decision
1412
+ # consumption without adding a second network round trip.
1413
+ if tool_name and self.execution_broker_mode in {"observe", "enforce"}:
1414
+ return self.guardrail_validate(
1415
+ text=text,
1416
+ agent_id=agent_id,
1417
+ platform=platform,
1418
+ source_platform=source_platform,
1419
+ tool_platform=tool_platform,
1420
+ tool_name=tool_name,
1421
+ tool_args=tool_args,
1422
+ tool_version=tool_version,
1423
+ tool_digest=tool_digest,
1424
+ policy_context=policy_context,
1425
+ direction=direction,
1426
+ source_agent_id=source_agent_id,
1427
+ user_id=user_id,
1428
+ reasoning_trace=reasoning_trace,
1429
+ request_purpose=request_purpose,
1430
+ purpose=purpose,
1431
+ intent=intent,
1432
+ claimed_role=claimed_role,
1433
+ actual_role=actual_role,
1434
+ source_agent_role=source_agent_role,
1435
+ target_agent_role=target_agent_role,
1436
+ redact_pii=redact_pii,
1437
+ redact_secrets=redact_secrets,
1438
+ block_on_sensitive_output=block_on_sensitive_output,
1439
+ trusted_destination_domains=trusted_destination_domains,
1440
+ allowed_destination_domains=allowed_destination_domains,
1441
+ execution_broker=True,
1442
+ tenant_id=tenant_id,
1443
+ )
1444
+
1217
1445
  return self.mcp_tool_call(
1218
1446
  "guardrail.validate",
1219
1447
  arguments,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agenticdome-python-sdk"
7
- version = "1.2.6"
7
+ version = "1.2.8"
8
8
  description = "Official Python SDK for AgenticDome security guardrails, DLP, tool authorization, and multi-agent delegation enforcement."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -20,6 +20,10 @@ class FakeClient:
20
20
  self.mesh_calls = []
21
21
  self.incidents = []
22
22
  self.revocations = []
23
+ self.provenance = {}
24
+
25
+ def register_tool_provenance(self, tool_name, **kwargs):
26
+ self.provenance[tool_name] = kwargs
23
27
 
24
28
  def guardrail_validate(self, **kwargs):
25
29
  self.guardrail_calls.append(kwargs)
@@ -69,6 +73,24 @@ def test_autogen_requires_agenticdome_runtime_configuration():
69
73
  AgenticDomeAutoGenFirewall(FirewallConfig(api_base="", api_key="", tenant_id=""))
70
74
 
71
75
 
76
+ def test_autogen_forwards_explicit_tool_provenance_and_supports_cached_registration():
77
+ fw, client = firewall()
78
+ digest = "sha256:" + "c" * 64
79
+ fw.register_tool_provenance("crm.lookup", tool_version="3.0.0", tool_digest=digest)
80
+ fw.authorize_tool_call(
81
+ session_id="tool-provenance",
82
+ agent_id="planner",
83
+ tool_name="crm.lookup",
84
+ tool_args={"customer_id": "42"},
85
+ tool_version="3.0.0",
86
+ tool_digest=digest,
87
+ )
88
+
89
+ assert client.provenance["crm.lookup"]["tool_digest"] == digest
90
+ assert client.guardrail_calls[-1]["tool_version"] == "3.0.0"
91
+ assert client.guardrail_calls[-1]["tool_digest"] == digest
92
+
93
+
72
94
  def test_conversation_window_sends_family_two_metrics_and_redacts_output():
73
95
  fw, client = firewall()
74
96
 
@@ -90,6 +90,65 @@ def test_guardrail_validate_success(mock_request, client):
90
90
  assert payload["platform"] == "microsoft"
91
91
 
92
92
 
93
+ @patch("agenticdome_sdk.client.requests.Session.request")
94
+ def test_registered_tool_provenance_is_forwarded_without_runtime_hashing(mock_request, client):
95
+ digest = "sha256:" + "a" * 64
96
+ mock_request.return_value = make_response(200, {"verdict": "ALLOWED"})
97
+ client.register_tool_provenance("crm.lookup", tool_version="4.2.1", tool_digest=digest, tool_platform="mcp")
98
+
99
+ client.guardrail_validate(
100
+ text="lookup customer",
101
+ agent_id="support-agent",
102
+ direction="outbound",
103
+ platform="langgraph",
104
+ tool_platform="mcp",
105
+ tool_name="crm.lookup",
106
+ tool_args={"customer_id": "42"},
107
+ )
108
+
109
+ payload = mock_request.call_args.kwargs["json"]
110
+ assert payload["tool_version"] == "4.2.1"
111
+ assert payload["tool_digest"] == digest
112
+ assert payload["policy_context"]["tool_digest"] == digest
113
+
114
+
115
+ @patch("agenticdome_sdk.client.requests.Session.request")
116
+ def test_registered_provenance_is_bound_to_authorize_and_verify(mock_request, client):
117
+ digest = "sha256:" + "b" * 64
118
+ mock_request.return_value = make_response(200, {"jsonrpc": "2.0", "id": "1", "result": {"verdict": "ALLOWED", "valid": True}})
119
+ client.register_tool_provenance("payments.refund", tool_version="2.0.0", tool_digest=digest)
120
+
121
+ client.a2a_authorize_tool(
122
+ text="delegate refund",
123
+ agent_id="billing-agent",
124
+ platform="autogen",
125
+ source_agent_id="manager-agent",
126
+ source_platform="autogen",
127
+ tool_name="payments.refund",
128
+ tool_args={"amount": 25},
129
+ )
130
+ authorize = mock_request.call_args.kwargs["json"]["params"]["arguments"]
131
+ assert authorize["tool_version"] == "2.0.0"
132
+ assert authorize["tool_digest"] == digest
133
+
134
+ client.a2a_verify_decision_token_rpc(
135
+ "decision-token",
136
+ tool_name="payments.refund",
137
+ tool_args={"amount": 25},
138
+ agent_id="billing-agent",
139
+ source_agent_id="manager-agent",
140
+ platform="autogen",
141
+ )
142
+ verify = mock_request.call_args.kwargs["json"]["params"]["arguments"]
143
+ assert verify["tool_version"] == "2.0.0"
144
+ assert verify["tool_digest"] == digest
145
+
146
+
147
+ def test_invalid_registered_tool_digest_fails_before_network(client):
148
+ with pytest.raises(ValueError, match="tool_digest"):
149
+ client.register_tool_provenance("unsafe.tool", tool_digest="sha256:not-a-real-digest")
150
+
151
+
93
152
  @patch("agenticdome_sdk.client.requests.Session.request")
94
153
  def test_guardrail_validate_blocks_invalid_tool_pairing(mock_request, client):
95
154
  with pytest.raises(ValueError, match="'tool_args' is required"):
@@ -297,6 +356,26 @@ def test_mcp_guardrail_validate(mock_request, client):
297
356
  assert arguments["tool_name"] == "mcp.execute_remote_patch"
298
357
 
299
358
 
359
+ @patch("agenticdome_sdk.client.requests.Session.request")
360
+ def test_mcp_guardrail_uses_registered_tool_provenance(mock_request, client):
361
+ digest = "sha256:" + "d" * 64
362
+ mock_request.return_value = make_response(200, {"jsonrpc": "2.0", "id": "1", "result": {"verdict": "ALLOWED"}})
363
+ client.register_tool_provenance("mcp.customer.lookup", tool_version="1.8.0", tool_digest=digest, tool_platform="mcp")
364
+
365
+ client.mcp_guardrail_validate(
366
+ text="lookup customer",
367
+ agent_id="mcp-agent",
368
+ platform="mcp",
369
+ tool_platform="mcp",
370
+ tool_name="mcp.customer.lookup",
371
+ tool_args={"customer_id": "42"},
372
+ )
373
+
374
+ arguments = mock_request.call_args.kwargs["json"]["params"]["arguments"]
375
+ assert arguments["tool_version"] == "1.8.0"
376
+ assert arguments["tool_digest"] == digest
377
+
378
+
300
379
  @patch("agenticdome_sdk.client.requests.Session.request")
301
380
  def test_mesh_validate(mock_request, client):
302
381
  mock_request.return_value = make_response(
@@ -0,0 +1,75 @@
1
+ from agenticdome_sdk.client import AgentGuardClient, AgentGuardError
2
+
3
+
4
+ def _client(mode="enforce"):
5
+ return AgentGuardClient(
6
+ "https://sidecar.example",
7
+ api_key="test-key",
8
+ tenant_id="tenant-1",
9
+ execution_broker_mode=mode,
10
+ max_retries=0,
11
+ )
12
+
13
+
14
+ def test_tool_authorization_uses_one_request_broker_and_requires_receipt(monkeypatch):
15
+ client = _client()
16
+ observed = {}
17
+
18
+ def request(method, path, **kwargs):
19
+ observed.update({"method": method, "path": path, **kwargs})
20
+ return {"verdict": "ALLOWED", "broker": {"verified": True, "token_consumed": True}}
21
+
22
+ monkeypatch.setattr(client, "_request", request)
23
+ response = client.guardrail_validate(
24
+ text="lookup customer",
25
+ agent_id="support-agent",
26
+ platform="custom_python",
27
+ source_agent_id="support-manager",
28
+ source_platform="microsoft",
29
+ tool_name="crm.lookup",
30
+ tool_args={"id": "123"},
31
+ actor_chain=[
32
+ {"id": "support-manager", "framework": "microsoft"},
33
+ {"id": "support-agent", "framework": "custom_python"},
34
+ ],
35
+ scopes=["crm.customer.read"],
36
+ permissions=["case.status.read"],
37
+ parent_jti="parent-decision",
38
+ root_jti="root-decision",
39
+ policy_id="support-policy",
40
+ policy_version="3",
41
+ policy_hash="sha256:" + "a" * 64,
42
+ proof_thumbprint="proof-thumbprint",
43
+ )
44
+
45
+ assert response["verdict"] == "ALLOWED"
46
+ assert observed["path"] == "/tools/execution/authorize"
47
+ assert observed["json_body"]["boundary_id"].startswith("sdk:")
48
+ assert observed["json_body"]["actor_chain"][1]["id"] == "support-agent"
49
+ assert observed["json_body"]["scopes"] == ["crm.customer.read"]
50
+ assert observed["json_body"]["permissions"] == ["case.status.read"]
51
+ assert observed["json_body"]["parent_jti"] == "parent-decision"
52
+ assert observed["json_body"]["root_jti"] == "root-decision"
53
+ assert observed["json_body"]["policy_id"] == "support-policy"
54
+ assert observed["json_body"]["policy_version"] == "3"
55
+ assert observed["json_body"]["policy_hash"] == "sha256:" + "a" * 64
56
+ assert observed["json_body"]["proof_thumbprint"] == "proof-thumbprint"
57
+ assert observed["json_body"]["policy_context"]["actor_chain"][0]["id"] == "support-manager"
58
+
59
+
60
+ def test_enforced_broker_fails_closed_without_consumed_receipt(monkeypatch):
61
+ client = _client()
62
+ monkeypatch.setattr(client, "_request", lambda *args, **kwargs: {"verdict": "ALLOWED"})
63
+
64
+ try:
65
+ client.guardrail_validate(
66
+ text="lookup customer",
67
+ agent_id="support-agent",
68
+ platform="custom_python",
69
+ tool_name="crm.lookup",
70
+ tool_args={"id": "123"},
71
+ )
72
+ except AgentGuardError as exc:
73
+ assert "atomically consumed" in str(exc)
74
+ else:
75
+ raise AssertionError("missing broker receipt did not fail closed")