agenticdome-python-sdk 1.2.6__tar.gz → 1.2.7__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 (51) hide show
  1. {agenticdome_python_sdk-1.2.6/agenticdome_python_sdk.egg-info → agenticdome_python_sdk-1.2.7}/PKG-INFO +1 -1
  2. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7/agenticdome_python_sdk.egg-info}/PKG-INFO +1 -1
  3. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/_framework_firewall.py +37 -2
  4. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/client.py +127 -1
  5. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/pyproject.toml +1 -1
  6. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_autogen_integration.py +22 -0
  7. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_client.py +79 -0
  8. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/LICENSE +0 -0
  9. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/MANIFEST.in +0 -0
  10. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/README.md +0 -0
  11. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_python_sdk.egg-info/SOURCES.txt +0 -0
  12. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_python_sdk.egg-info/dependency_links.txt +0 -0
  13. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_python_sdk.egg-info/requires.txt +0 -0
  14. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_python_sdk.egg-info/top_level.txt +0 -0
  15. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/__init__.py +0 -0
  16. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/agno.py +0 -0
  17. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/autogen.py +0 -0
  18. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/aws_bedrock.py +0 -0
  19. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/claude.py +0 -0
  20. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/crewai.py +0 -0
  21. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/google_adk.py +0 -0
  22. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/identity.py +0 -0
  23. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/langgraph.py +0 -0
  24. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/llamaindex.py +0 -0
  25. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/mcp_host.py +0 -0
  26. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/microsoft_agent_framework.py +0 -0
  27. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/microsoft_ai_foundry.py +0 -0
  28. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/openai_agents.py +0 -0
  29. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/proof.py +0 -0
  30. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/pydantic.py +0 -0
  31. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/agenticdome_sdk/smolagents.py +0 -0
  32. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/examples/attack_demo.py +0 -0
  33. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/examples/crewai_agenticdome_firewall.py +0 -0
  34. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/setup.cfg +0 -0
  35. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/setup.py +0 -0
  36. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_agno_integration.py +0 -0
  37. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_attack_demo.py +0 -0
  38. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_aws_bedrock_integration.py +0 -0
  39. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_claude_integration.py +0 -0
  40. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_crewai_integration.py +0 -0
  41. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_google_adk_integration.py +0 -0
  42. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_identity_context.py +0 -0
  43. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_langgraph_integration.py +0 -0
  44. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_live_tenant.py +0 -0
  45. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_llamaindex_integration.py +0 -0
  46. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_mcp_host_integration.py +0 -0
  47. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_microsoft_agent_framework_integration.py +0 -0
  48. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_microsoft_ai_foundry_integration.py +0 -0
  49. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_openai_agents_integration.py +0 -0
  50. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/tests/test_pydanticai_integration.py +0 -0
  51. {agenticdome_python_sdk-1.2.6 → agenticdome_python_sdk-1.2.7}/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.7
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.7
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
@@ -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,7 @@
1
1
  import base64
2
2
  import os
3
- from typing import Optional, Dict, List, Any, Tuple, Union
3
+ import re
4
+ from typing import Optional, Dict, List, Any, Tuple, Union, Mapping
4
5
 
5
6
  import requests
6
7
  from requests.adapters import HTTPAdapter
@@ -58,6 +59,7 @@ class AgentGuardClient:
58
59
  pool_maxsize: int = 100,
59
60
  pool_block: bool = False,
60
61
  service_token: Optional[str] = None,
62
+ tool_provenance: Optional[Mapping[str, Mapping[str, str]]] = None,
61
63
  ):
62
64
  self.api_base = self._require_nonempty("api_base", api_base).rstrip("/")
63
65
  self.api_key = self._require_nonempty(
@@ -84,6 +86,13 @@ class AgentGuardClient:
84
86
  default=5.0,
85
87
  )
86
88
  self.user_agent = user_agent
89
+ self._tool_provenance: Dict[str, Dict[str, str]] = {}
90
+ for registered_name, provenance in dict(tool_provenance or {}).items():
91
+ self.register_tool_provenance(
92
+ registered_name,
93
+ tool_version=(provenance or {}).get("tool_version") or (provenance or {}).get("version"),
94
+ tool_digest=(provenance or {}).get("tool_digest") or (provenance or {}).get("digest"),
95
+ )
87
96
 
88
97
  self.session = requests.Session()
89
98
  retry = Retry(
@@ -158,6 +167,55 @@ class AgentGuardClient:
158
167
  s = str(value).strip()
159
168
  return s or None
160
169
 
170
+ def register_tool_provenance(
171
+ self,
172
+ tool_name: str,
173
+ *,
174
+ tool_version: Optional[str] = None,
175
+ tool_digest: Optional[str] = None,
176
+ tool_platform: Optional[str] = None,
177
+ ) -> None:
178
+ """Register precomputed provenance once; request-time lookup is local O(1)."""
179
+ name = self._require_nonempty("tool_name", tool_name)
180
+ version = self._normalize_optional_string(tool_version)
181
+ digest = self._normalize_optional_string(tool_digest)
182
+ if digest is not None and not re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
183
+ raise ValueError("'tool_digest' must be sha256 followed by 64 lowercase hexadecimal characters")
184
+ if version is None and digest is None:
185
+ raise ValueError("tool_version or tool_digest is required")
186
+ key = f"{self._normalize_optional_string(tool_platform)}:{name}" if tool_platform else name
187
+ self._tool_provenance[key] = {"tool_version": version or "", "tool_digest": digest or ""}
188
+
189
+ def unregister_tool_provenance(self, tool_name: str, *, tool_platform: Optional[str] = None) -> None:
190
+ name = self._require_nonempty("tool_name", tool_name)
191
+ key = f"{self._normalize_optional_string(tool_platform)}:{name}" if tool_platform else name
192
+ self._tool_provenance.pop(key, None)
193
+
194
+ def _resolve_tool_provenance(
195
+ self,
196
+ *,
197
+ tool_name: Optional[str],
198
+ tool_version: Optional[str],
199
+ tool_digest: Optional[str],
200
+ tool_platform: Optional[str] = None,
201
+ policy_context: Optional[Dict[str, Any]] = None,
202
+ ) -> Tuple[Optional[str], Optional[str]]:
203
+ if not tool_name:
204
+ return tool_version, tool_digest
205
+ context = policy_context or {}
206
+ context_provenance = context.get("tool_provenance") if isinstance(context.get("tool_provenance"), dict) else {}
207
+ key = f"{tool_platform}:{tool_name}" if tool_platform else ""
208
+ registered = self._tool_provenance.get(key) or self._tool_provenance.get(str(tool_name)) or {}
209
+ version = self._normalize_optional_string(tool_version) or self._normalize_optional_string(
210
+ context.get("tool_version") or context_provenance.get("tool_version") or context_provenance.get("version") or registered.get("tool_version")
211
+ )
212
+ digest = self._normalize_optional_string(tool_digest) or self._normalize_optional_string(
213
+ context.get("tool_digest") or context_provenance.get("tool_digest") or context_provenance.get("digest") or registered.get("tool_digest")
214
+ )
215
+ if digest is not None and not re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
216
+ raise ValueError("'tool_digest' must be sha256 followed by 64 lowercase hexadecimal characters")
217
+ return version, digest
218
+
161
219
  def _positive_float(self, name: str, value: Any, *, default: float) -> float:
162
220
  try:
163
221
  parsed = float(value)
@@ -257,6 +315,7 @@ class AgentGuardClient:
257
315
  token: str,
258
316
  tool_name: Optional[str] = None,
259
317
  tool_args: Optional[Dict[str, Any]] = None,
318
+ tool_digest: Optional[str] = None,
260
319
  ) -> None:
261
320
  """
262
321
  Validate decision-token verification inputs.
@@ -268,6 +327,8 @@ class AgentGuardClient:
268
327
  raise ValueError("'tool_args' is required when 'tool_name' is provided")
269
328
  if tool_args is not None and not tool_name:
270
329
  raise ValueError("'tool_name' is required when 'tool_args' is provided")
330
+ if tool_digest is not None and not re.fullmatch(r"sha256:[0-9a-f]{64}", str(tool_digest)):
331
+ raise ValueError("'tool_digest' must be sha256 followed by 64 lowercase hexadecimal characters")
271
332
 
272
333
  def _request(
273
334
  self,
@@ -453,6 +514,8 @@ class AgentGuardClient:
453
514
  tool_platform: Optional[str] = None,
454
515
  tool_name: Optional[str] = None,
455
516
  tool_args: Optional[Dict[str, Any]] = None,
517
+ tool_version: Optional[str] = None,
518
+ tool_digest: Optional[str] = None,
456
519
  policy_context: Optional[Dict[str, Any]] = None,
457
520
  reasoning_trace: Optional[str] = None,
458
521
  agent_instance_id: Optional[str] = None,
@@ -491,6 +554,13 @@ class AgentGuardClient:
491
554
  Delegation rule:
492
555
  - source_agent_id requires source_platform
493
556
  """
557
+ tool_version, tool_digest = self._resolve_tool_provenance(
558
+ tool_name=tool_name,
559
+ tool_version=tool_version,
560
+ tool_digest=tool_digest,
561
+ tool_platform=tool_platform,
562
+ policy_context=policy_context,
563
+ )
494
564
  normalized_direction = self._validate_guardrail_args(
495
565
  text=text,
496
566
  agent_id=agent_id,
@@ -502,6 +572,8 @@ class AgentGuardClient:
502
572
  source_platform=source_platform,
503
573
  user_id=user_id,
504
574
  )
575
+ if tool_digest is not None and not re.fullmatch(r"sha256:[0-9a-f]{64}", str(tool_digest)):
576
+ raise ValueError("'tool_digest' must be sha256 followed by 64 lowercase hexadecimal characters")
505
577
 
506
578
  merged_policy_context = self._merge_policy_context(
507
579
  policy_context,
@@ -511,6 +583,8 @@ class AgentGuardClient:
511
583
  tool_platform=tool_platform,
512
584
  tool_name=tool_name,
513
585
  tool_args=tool_args,
586
+ tool_version=tool_version,
587
+ tool_digest=tool_digest,
514
588
  reasoning_trace=reasoning_trace,
515
589
  agent_instance_id=agent_instance_id,
516
590
  user_id=user_id,
@@ -540,6 +614,8 @@ class AgentGuardClient:
540
614
  "tool_platform": tool_platform,
541
615
  "tool_name": tool_name,
542
616
  "tool_args": tool_args,
617
+ "tool_version": tool_version,
618
+ "tool_digest": tool_digest,
543
619
  "policy_context": merged_policy_context,
544
620
  "reasoning_trace": reasoning_trace,
545
621
  "agent_instance_id": agent_instance_id,
@@ -780,6 +856,8 @@ class AgentGuardClient:
780
856
  source_platform: str,
781
857
  tool_name: str,
782
858
  tool_args: Dict[str, Any],
859
+ tool_version: Optional[str] = None,
860
+ tool_digest: Optional[str] = None,
783
861
  tool_platform: Optional[str] = None,
784
862
  policy_context: Optional[Dict[str, Any]] = None,
785
863
  session_id: Optional[str] = None,
@@ -830,6 +908,15 @@ class AgentGuardClient:
830
908
  self._require_nonempty("source_agent_id", source_agent_id)
831
909
  self._require_nonempty("source_platform", source_platform)
832
910
  self._require_nonempty("tool_name", tool_name)
911
+ tool_version, tool_digest = self._resolve_tool_provenance(
912
+ tool_name=tool_name,
913
+ tool_version=tool_version,
914
+ tool_digest=tool_digest,
915
+ tool_platform=tool_platform,
916
+ policy_context=policy_context,
917
+ )
918
+ if tool_digest is not None and not re.fullmatch(r"sha256:[0-9a-f]{64}", str(tool_digest)):
919
+ raise ValueError("'tool_digest' must be sha256 followed by 64 lowercase hexadecimal characters")
833
920
 
834
921
  normalized_direction = self._validate_guardrail_args(
835
922
  text=text,
@@ -851,6 +938,8 @@ class AgentGuardClient:
851
938
  source_agent_id=source_agent_id,
852
939
  tool_name=tool_name,
853
940
  tool_args=tool_args,
941
+ tool_version=tool_version,
942
+ tool_digest=tool_digest,
854
943
  request_purpose=request_purpose,
855
944
  purpose=purpose,
856
945
  intent=intent,
@@ -887,6 +976,8 @@ class AgentGuardClient:
887
976
  "tool_platform": tool_platform,
888
977
  "tool_name": tool_name,
889
978
  "tool_args": tool_args,
979
+ "tool_version": tool_version,
980
+ "tool_digest": tool_digest,
890
981
  "policy_context": merged_policy_context,
891
982
  "source_agent_id": source_agent_id,
892
983
  "request_purpose": request_purpose,
@@ -937,6 +1028,8 @@ class AgentGuardClient:
937
1028
  *,
938
1029
  tool_name: Optional[str] = None,
939
1030
  tool_args: Optional[Dict[str, Any]] = None,
1031
+ tool_version: Optional[str] = None,
1032
+ tool_digest: Optional[str] = None,
940
1033
  agent_id: Optional[str] = None,
941
1034
  source_agent_id: Optional[str] = None,
942
1035
  platform: Optional[str] = None,
@@ -958,10 +1051,17 @@ class AgentGuardClient:
958
1051
  - platform
959
1052
  - source_agent_id (for delegated sensitive flows)
960
1053
  """
1054
+ tool_version, tool_digest = self._resolve_tool_provenance(
1055
+ tool_name=tool_name,
1056
+ tool_version=tool_version,
1057
+ tool_digest=tool_digest,
1058
+ tool_platform=platform,
1059
+ )
961
1060
  self._validate_decision_verify_args(
962
1061
  token=token,
963
1062
  tool_name=tool_name,
964
1063
  tool_args=tool_args,
1064
+ tool_digest=tool_digest,
965
1065
  )
966
1066
 
967
1067
  payload = self._drop_none(
@@ -969,6 +1069,8 @@ class AgentGuardClient:
969
1069
  "token": token,
970
1070
  "tool_name": tool_name,
971
1071
  "tool_args": tool_args,
1072
+ "tool_version": tool_version,
1073
+ "tool_digest": tool_digest,
972
1074
  "agent_id": agent_id,
973
1075
  "source_agent_id": source_agent_id,
974
1076
  "platform": platform,
@@ -989,6 +1091,8 @@ class AgentGuardClient:
989
1091
  *,
990
1092
  tool_name: Optional[str] = None,
991
1093
  tool_args: Optional[Dict[str, Any]] = None,
1094
+ tool_version: Optional[str] = None,
1095
+ tool_digest: Optional[str] = None,
992
1096
  agent_id: Optional[str] = None,
993
1097
  source_agent_id: Optional[str] = None,
994
1098
  platform: Optional[str] = None,
@@ -1011,10 +1115,17 @@ class AgentGuardClient:
1011
1115
  - platform
1012
1116
  - source_agent_id (for delegated sensitive flows)
1013
1117
  """
1118
+ tool_version, tool_digest = self._resolve_tool_provenance(
1119
+ tool_name=tool_name,
1120
+ tool_version=tool_version,
1121
+ tool_digest=tool_digest,
1122
+ tool_platform=platform,
1123
+ )
1014
1124
  self._validate_decision_verify_args(
1015
1125
  token=token,
1016
1126
  tool_name=tool_name,
1017
1127
  tool_args=tool_args,
1128
+ tool_digest=tool_digest,
1018
1129
  )
1019
1130
 
1020
1131
  arguments = self._drop_none(
@@ -1022,6 +1133,8 @@ class AgentGuardClient:
1022
1133
  "token": token,
1023
1134
  "tool_name": tool_name,
1024
1135
  "tool_args": tool_args,
1136
+ "tool_version": tool_version,
1137
+ "tool_digest": tool_digest,
1025
1138
  "agent_id": agent_id,
1026
1139
  "source_agent_id": source_agent_id,
1027
1140
  "platform": platform,
@@ -1111,6 +1224,8 @@ class AgentGuardClient:
1111
1224
  tool_platform: Optional[str] = None,
1112
1225
  tool_name: Optional[str] = None,
1113
1226
  tool_args: Optional[Dict[str, Any]] = None,
1227
+ tool_version: Optional[str] = None,
1228
+ tool_digest: Optional[str] = None,
1114
1229
  policy_context: Optional[Dict[str, Any]] = None,
1115
1230
  direction: str = "outbound",
1116
1231
  source_agent_id: Optional[str] = None,
@@ -1149,6 +1264,13 @@ class AgentGuardClient:
1149
1264
  Delegation rule:
1150
1265
  - source_agent_id requires source_platform
1151
1266
  """
1267
+ tool_version, tool_digest = self._resolve_tool_provenance(
1268
+ tool_name=tool_name,
1269
+ tool_version=tool_version,
1270
+ tool_digest=tool_digest,
1271
+ tool_platform=tool_platform,
1272
+ policy_context=policy_context,
1273
+ )
1152
1274
  normalized_direction = self._validate_guardrail_args(
1153
1275
  text=text,
1154
1276
  agent_id=agent_id,
@@ -1170,6 +1292,8 @@ class AgentGuardClient:
1170
1292
  user_id=user_id,
1171
1293
  tool_name=tool_name,
1172
1294
  tool_args=tool_args,
1295
+ tool_version=tool_version,
1296
+ tool_digest=tool_digest,
1173
1297
  reasoning_trace=reasoning_trace,
1174
1298
  request_purpose=request_purpose,
1175
1299
  purpose=purpose,
@@ -1195,6 +1319,8 @@ class AgentGuardClient:
1195
1319
  "tool_platform": tool_platform,
1196
1320
  "tool_name": tool_name,
1197
1321
  "tool_args": tool_args,
1322
+ "tool_version": tool_version,
1323
+ "tool_digest": tool_digest,
1198
1324
  "policy_context": merged_policy_context,
1199
1325
  "source_agent_id": source_agent_id,
1200
1326
  "user_id": user_id,
@@ -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.7"
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(