agenticdome-python-sdk 1.2.37__tar.gz → 1.2.39__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 (98) hide show
  1. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/PKG-INFO +7 -3
  2. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/README.md +6 -2
  3. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_python_sdk.egg-info/PKG-INFO +7 -3
  4. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/catalog_snapshot.json +3 -3
  5. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/onboarding_cli.py +233 -12
  6. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/customer-onboarding.md +11 -2
  7. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/pyproject.toml +1 -1
  8. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/CONTRIBUTING.md +0 -0
  9. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/LICENSE +0 -0
  10. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/MANIFEST.in +0 -0
  11. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/NOTICE +0 -0
  12. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/SECURITY.md +0 -0
  13. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_python_sdk.egg-info/SOURCES.txt +0 -0
  14. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_python_sdk.egg-info/dependency_links.txt +0 -0
  15. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_python_sdk.egg-info/entry_points.txt +0 -0
  16. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_python_sdk.egg-info/requires.txt +0 -0
  17. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_python_sdk.egg-info/top_level.txt +0 -0
  18. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/__init__.py +0 -0
  19. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/_framework_firewall.py +0 -0
  20. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/_mode.py +0 -0
  21. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/action_path_proof.py +0 -0
  22. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/agno.py +0 -0
  23. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/attestation.py +0 -0
  24. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/autogen.py +0 -0
  25. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/aws_bedrock.py +0 -0
  26. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/claude.py +0 -0
  27. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/client.py +0 -0
  28. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/connect.py +0 -0
  29. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/copilot_ir.py +0 -0
  30. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/copilot_ir_collector.cjs +0 -0
  31. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/crewai.py +0 -0
  32. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/demo.py +0 -0
  33. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/existing_integration.py +0 -0
  34. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/framework_edits.py +0 -0
  35. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/generic_python.py +0 -0
  36. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/google_adk.py +0 -0
  37. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/guided_integration.py +0 -0
  38. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/hook_catalog.py +0 -0
  39. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/identity.py +0 -0
  40. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/langgraph.py +0 -0
  41. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/lifecycle.py +0 -0
  42. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/llamaindex.py +0 -0
  43. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/mcp_host.py +0 -0
  44. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/mcp_http_gateway.py +0 -0
  45. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/mcp_rehearsal_server.py +0 -0
  46. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/mcp_verification.py +0 -0
  47. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/microsoft_agent_framework.py +0 -0
  48. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/microsoft_ai_foundry.py +0 -0
  49. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/openai_agents.py +0 -0
  50. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/outcomes.py +0 -0
  51. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/proof.py +0 -0
  52. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/pydantic.py +0 -0
  53. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/simulation.py +0 -0
  54. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/agenticdome_sdk/smolagents.py +0 -0
  55. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/README.md +0 -0
  56. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/agno.md +0 -0
  57. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/autogen.md +0 -0
  58. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/aws-bedrock.md +0 -0
  59. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/claude-agent-sdk.md +0 -0
  60. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/crewai.md +0 -0
  61. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/custom-python.md +0 -0
  62. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/google-adk.md +0 -0
  63. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/langgraph.md +0 -0
  64. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/llamaindex.md +0 -0
  65. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/microsoft-agent-framework.md +0 -0
  66. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/microsoft-ai-foundry.md +0 -0
  67. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/openai-agents.md +0 -0
  68. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/pydanticai.md +0 -0
  69. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/frameworks/smolagents.md +0 -0
  70. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/mcp-integration.md +0 -0
  71. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/performance-evidence.md +0 -0
  72. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/docs/runtime-deployment.md +0 -0
  73. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/PRODUCTION_INTEGRATION.md +0 -0
  74. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/README.md +0 -0
  75. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/attack_demo.py +0 -0
  76. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/crewai_agenticdome_firewall.py +0 -0
  77. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/README.md +0 -0
  78. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/_shared.py +0 -0
  79. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/agno.py +0 -0
  80. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/autogen.py +0 -0
  81. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/aws_bedrock.py +0 -0
  82. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/claude_agent_sdk.py +0 -0
  83. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/crewai.py +0 -0
  84. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/custom_python.py +0 -0
  85. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/google_adk.py +0 -0
  86. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/langgraph.py +0 -0
  87. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/llamaindex.py +0 -0
  88. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/mcp.py +0 -0
  89. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/microsoft_agent_framework.py +0 -0
  90. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/microsoft_ai_foundry.py +0 -0
  91. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/openai_agents.py +0 -0
  92. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/pydanticai.py +0 -0
  93. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/frameworks/smolagents.py +0 -0
  94. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/langgraph_agenticdome_firewall.py +0 -0
  95. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/local_simulation_gallery.py +0 -0
  96. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/examples/mcp_gateway_action_firewall.py +0 -0
  97. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/setup.cfg +0 -0
  98. {agenticdome_python_sdk-1.2.37 → agenticdome_python_sdk-1.2.39}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agenticdome-python-sdk
3
- Version: 1.2.37
3
+ Version: 1.2.39
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
@@ -186,8 +186,12 @@ attach other boundaries manually, then run
186
186
  If a coding assistant has already made and tested the application edits,
187
187
  import its canonical `AgenticDome_Integration.md` in the customer portal and
188
188
  run `agenticdome validate-report --report AgenticDome_Integration.md` from
189
- that same deployable workload. Import the two generated, hash-bound JSON files
190
- in Onboarding. This avoids repeating the guided edit plan; the Markdown counts
189
+ that same deployable workload. If it reports `ready=false`, follow the local
190
+ `.agenticdome/RECONCILIATION.md` worklist and rerun after reviewing the real
191
+ source and tests. You may import the two generated, hash-bound JSON files in
192
+ Onboarding even when the gate is incomplete, to retain partial evidence and run
193
+ a fixed tenant-policy diagnostic. They do not count as passing workload proof or
194
+ unlock activation; rerun and reimport after closing the gaps. This avoids repeating the guided edit plan; the Markdown counts
191
195
  remain AI-reported and a real action-path test is still required before
192
196
  claiming protection. See the [customer onboarding guide](docs/customer-onboarding.md).
193
197
 
@@ -73,8 +73,12 @@ attach other boundaries manually, then run
73
73
  If a coding assistant has already made and tested the application edits,
74
74
  import its canonical `AgenticDome_Integration.md` in the customer portal and
75
75
  run `agenticdome validate-report --report AgenticDome_Integration.md` from
76
- that same deployable workload. Import the two generated, hash-bound JSON files
77
- in Onboarding. This avoids repeating the guided edit plan; the Markdown counts
76
+ that same deployable workload. If it reports `ready=false`, follow the local
77
+ `.agenticdome/RECONCILIATION.md` worklist and rerun after reviewing the real
78
+ source and tests. You may import the two generated, hash-bound JSON files in
79
+ Onboarding even when the gate is incomplete, to retain partial evidence and run
80
+ a fixed tenant-policy diagnostic. They do not count as passing workload proof or
81
+ unlock activation; rerun and reimport after closing the gaps. This avoids repeating the guided edit plan; the Markdown counts
78
82
  remain AI-reported and a real action-path test is still required before
79
83
  claiming protection. See the [customer onboarding guide](docs/customer-onboarding.md).
80
84
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agenticdome-python-sdk
3
- Version: 1.2.37
3
+ Version: 1.2.39
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
@@ -186,8 +186,12 @@ attach other boundaries manually, then run
186
186
  If a coding assistant has already made and tested the application edits,
187
187
  import its canonical `AgenticDome_Integration.md` in the customer portal and
188
188
  run `agenticdome validate-report --report AgenticDome_Integration.md` from
189
- that same deployable workload. Import the two generated, hash-bound JSON files
190
- in Onboarding. This avoids repeating the guided edit plan; the Markdown counts
189
+ that same deployable workload. If it reports `ready=false`, follow the local
190
+ `.agenticdome/RECONCILIATION.md` worklist and rerun after reviewing the real
191
+ source and tests. You may import the two generated, hash-bound JSON files in
192
+ Onboarding even when the gate is incomplete, to retain partial evidence and run
193
+ a fixed tenant-policy diagnostic. They do not count as passing workload proof or
194
+ unlock activation; rerun and reimport after closing the gaps. This avoids repeating the guided edit plan; the Markdown counts
191
195
  remain AI-reported and a real action-path test is still required before
192
196
  claiming protection. See the [customer onboarding guide](docs/customer-onboarding.md).
193
197
 
@@ -2,12 +2,12 @@
2
2
  "schema": "agenticdome.catalog-snapshot.v1",
3
3
  "catalog_schema": "agenticdome.hook-catalog.v1",
4
4
  "verified_at": "2026-10-02",
5
- "python_sdk_version": "1.2.37",
5
+ "python_sdk_version": "1.2.39",
6
6
  "published_packages": {
7
7
  "agenticdome-python-sdk": {
8
8
  "registry": "pypi",
9
- "version": "1.2.37",
10
- "url": "https://pypi.org/project/agenticdome-python-sdk/1.2.37/"
9
+ "version": "1.2.39",
10
+ "url": "https://pypi.org/project/agenticdome-python-sdk/1.2.39/"
11
11
  },
12
12
  "agenticdome-sdk": {
13
13
  "registry": "npm",
@@ -7,6 +7,7 @@ paths and boundary locations, never source snippets or file contents.
7
7
  from __future__ import annotations
8
8
 
9
9
  import argparse
10
+ import ast
10
11
  import difflib
11
12
  import hashlib
12
13
  import importlib.metadata
@@ -42,6 +43,10 @@ from .framework_edits import has_explicit_python_tool_dispatcher
42
43
  SCHEMA = "agenticdome.onboarding-report.v1"
43
44
  CONFIG_SCHEMA = "agenticdome.project-config.v1"
44
45
  COPILOT_ANALYSIS_REVISION = 4
46
+ # Local cache revision is separate from the wire contract: changing the
47
+ # analyzer's finding rules must refresh old on-disk results without forcing
48
+ # every already-published SDK to upgrade in lockstep with its sidecar.
49
+ COPILOT_CACHE_REVISION = 2
45
50
  COPILOT_MAX_WORKLOAD_BYTES = 4_000_000
46
51
  COPILOT_MAX_WORKLOAD_PARTS = 24
47
52
  COPILOT_MAX_EVENTS_PER_FUNCTION = 2_048
@@ -574,6 +579,7 @@ def _copilot_semantic_analysis(root: Path, ir: Dict[str, Any], *, required: bool
574
579
  and isinstance(semantic, dict)
575
580
  and semantic.get("ir_sha256") == ir_digest
576
581
  and semantic.get("analysis_revision") == COPILOT_ANALYSIS_REVISION
582
+ and cached.get("cache_revision") == COPILOT_CACHE_REVISION
577
583
  ):
578
584
  return semantic
579
585
 
@@ -591,12 +597,14 @@ def _copilot_semantic_analysis(root: Path, ir: Dict[str, Any], *, required: bool
591
597
  or part_cache.get("api_base") != api_base
592
598
  or part_cache.get("catalog_digest") != expected_catalog_digest
593
599
  or part_cache.get("selected_ir_sha256") != ir_digest
600
+ or part_cache.get("cache_revision") != COPILOT_CACHE_REVISION
594
601
  or not isinstance(part_cache.get("parts"), dict)
595
602
  ):
596
603
  part_cache = {
597
604
  "schema": "agenticdome.copilot-part-cache.v1", "tenant_id": tenant_id,
598
605
  "api_base": api_base, "catalog_digest": expected_catalog_digest,
599
- "selected_ir_sha256": ir_digest, "parts": {},
606
+ "selected_ir_sha256": ir_digest, "cache_revision": COPILOT_CACHE_REVISION,
607
+ "parts": {},
600
608
  }
601
609
  analyzed: List[Tuple[str, Dict[str, Any]]] = []
602
610
  response_binding: Dict[str, Any] = {}
@@ -627,7 +635,7 @@ def _copilot_semantic_analysis(root: Path, ir: Dict[str, Any], *, required: bool
627
635
  if len(body) > 4_500_000:
628
636
  raise SystemExit(f"Bounded Copilot unit {name} exceeds its transport budget; no partial plan was saved.")
629
637
  idempotency_key = hashlib.sha256(
630
- f"{tenant_id}\n{api_base}\n{part_digest}\n{expected_catalog_digest}".encode("utf-8")
638
+ f"{tenant_id}\n{api_base}\n{part_digest}\n{expected_catalog_digest}\n{COPILOT_CACHE_REVISION}".encode("utf-8")
631
639
  ).hexdigest()
632
640
  try:
633
641
  result = _post_copilot(api_base, api_key, tenant_id, "/integration-copilot/v1/analyze", body, idempotency_key=idempotency_key)
@@ -663,6 +671,7 @@ def _copilot_semantic_analysis(root: Path, ir: Dict[str, Any], *, required: bool
663
671
  "source_upload": False,
664
672
  "tenant_id": tenant_id,
665
673
  "api_base": api_base,
674
+ "cache_revision": COPILOT_CACHE_REVISION,
666
675
  "semantic_analysis": semantic,
667
676
  "catalog_binding": response_binding,
668
677
  })
@@ -716,6 +725,82 @@ def _mcp_evidence_id(kind: str, relative: str, line: int) -> str:
716
725
  return f"{kind}-{digest}"
717
726
 
718
727
 
728
+ def _agenticdome_policy_probe_spans(text: str) -> Dict[int, List[Tuple[int, int]]]:
729
+ """Mask only a confirmed AgenticDome policy call's MCP method literal.
730
+
731
+ A policy client can use the MCP JSON-RPC envelope without forwarding
732
+ customer MCP tools. Keep all other MCP calls in the same file visible.
733
+ Unparseable or ambiguous source receives no exception.
734
+ """
735
+ if "tools/call" not in text or "guardrail.validate" not in text:
736
+ return {}
737
+ try:
738
+ module = ast.parse(text)
739
+ except (SyntaxError, ValueError):
740
+ return {}
741
+
742
+ def field(mapping: ast.AST, key: str) -> Optional[ast.AST]:
743
+ if not isinstance(mapping, ast.Dict):
744
+ return None
745
+ return next(
746
+ (value for name, value in zip(mapping.keys, mapping.values)
747
+ if isinstance(name, ast.Constant) and name.value == key),
748
+ None,
749
+ )
750
+
751
+ def is_literal(node: Optional[ast.AST], value: str) -> bool:
752
+ return isinstance(node, ast.Constant) and node.value == value
753
+
754
+ # The legacy urllib client uses an environment-overridable URL whose
755
+ # checked-in default is AgenticDome's own /mcp policy endpoint.
756
+ policy_url_names: set[str] = set()
757
+ for node in ast.walk(module):
758
+ if not isinstance(node, (ast.Assign, ast.AnnAssign)):
759
+ continue
760
+ targets = node.targets if isinstance(node, ast.Assign) else [node.target]
761
+ value = node.value
762
+ if value is None or not any(
763
+ isinstance(part, ast.Constant) and isinstance(part.value, str)
764
+ and re.fullmatch(r"https?://(?:[a-z0-9-]+\.)*agenticdome\.io(?::\d+)?/mcp/?", part.value, re.I)
765
+ for part in ast.walk(value)
766
+ ):
767
+ continue
768
+ policy_url_names.update(target.id for target in targets if isinstance(target, ast.Name))
769
+
770
+ spans: Dict[int, List[Tuple[int, int]]] = {}
771
+ for function in ast.walk(module):
772
+ if not isinstance(function, (ast.FunctionDef, ast.AsyncFunctionDef)):
773
+ continue
774
+ calls = [node for node in ast.walk(function) if isinstance(node, ast.Call)]
775
+ for assignment in ast.walk(function):
776
+ if not isinstance(assignment, (ast.Assign, ast.AnnAssign)):
777
+ continue
778
+ targets = assignment.targets if isinstance(assignment, ast.Assign) else [assignment.target]
779
+ names = {target.id for target in targets if isinstance(target, ast.Name)}
780
+ method = field(assignment.value, "method") if assignment.value is not None else None
781
+ params = field(assignment.value, "params") if assignment.value is not None else None
782
+ if not names or not is_literal(method, "tools/call") or not is_literal(field(params, "name"), "guardrail.validate"):
783
+ continue
784
+
785
+ def sends_policy_payload(call: ast.Call) -> bool:
786
+ if not any(isinstance(part, ast.Name) and part.id in names for part in ast.walk(call)):
787
+ return False
788
+ if (isinstance(call.func, ast.Attribute) and call.func.attr == "_post"
789
+ and call.args and is_literal(call.args[0], "/mcp")):
790
+ return True
791
+ return (
792
+ isinstance(call.func, (ast.Name, ast.Attribute))
793
+ and (call.func.id if isinstance(call.func, ast.Name) else call.func.attr) == "Request"
794
+ and bool(call.args)
795
+ and isinstance(call.args[0], ast.Name)
796
+ and call.args[0].id in policy_url_names
797
+ )
798
+
799
+ if any(sends_policy_payload(call) for call in calls):
800
+ spans.setdefault(method.lineno, []).append((method.col_offset, method.end_col_offset))
801
+ return spans
802
+
803
+
719
804
  def _detect_mcp_protection(root: Path, paths: Sequence[Path], languages: Sequence[str]) -> Dict[str, Any]:
720
805
  """Collect source-free MCP topology and candidate protection evidence.
721
806
 
@@ -735,15 +820,21 @@ def _detect_mcp_protection(root: Path, paths: Sequence[Path], languages: Sequenc
735
820
  for path in paths:
736
821
  relative = _relative(path, root)
737
822
  text = _read_text(path)
738
- # A client asking AgenticDome's own /mcp policy endpoint to run
739
- # guardrail.validate is not a customer MCP forwarder. Do not suggest
740
- # an MCP onboarding path from that protocol-shaped probe alone.
741
- policy_probe_only = (
823
+ python_source = path.suffix.lower() in {".py", ".pyi"}
824
+ policy_probe_spans = _agenticdome_policy_probe_spans(text) if python_source else {}
825
+ # Preserve the existing JavaScript/TypeScript policy-client exception.
826
+ # Unlike Python, this scanner has no AST span for those languages, so
827
+ # exempt only a single policy call with no other MCP integration hints.
828
+ if not python_source and (
742
829
  bool(re.search(r"['\"]name['\"]\s*:\s*['\"]guardrail\.validate['\"]", text))
743
830
  and bool(re.search(r"_post\s*\(\s*['\"]/mcp['\"]", text))
744
- and not re.search(r"\b(?:from\s+mcp|import\s+mcp|FastMCP|McpServer|ClientSession|StdioClientTransport|SSEClientTransport)\b|@modelcontextprotocol", text)
745
- )
746
- if policy_probe_only:
831
+ and len(re.findall(r"\btools/call\b", text)) == 1
832
+ and not re.search(
833
+ r"\b(?:FastMCP|McpServer|MCPClient|McpClient|ClientSession|StdioClientTransport|"
834
+ r"SSEClientTransport|call_tool|callTool|registerTool|tools/list|setRequestHandler)\b|@modelcontextprotocol",
835
+ text,
836
+ )
837
+ ):
747
838
  continue
748
839
  if not re.search(
749
840
  r"(?:\b(?:from\s+mcp|import\s+mcp|FastMCP|MCPClient|McpClient|McpServer|ClientSession|"
@@ -753,6 +844,14 @@ def _detect_mcp_protection(root: Path, paths: Sequence[Path], languages: Sequenc
753
844
  ):
754
845
  continue
755
846
  for line_number, line in enumerate(text.splitlines(), start=1):
847
+ # AST offsets are UTF-8 byte offsets. Mask the exact policy method
848
+ # literal, not the whole line or file: a real MCP call alongside
849
+ # the policy probe must still trigger specialist onboarding.
850
+ if line_number in policy_probe_spans:
851
+ encoded = bytearray(line.encode("utf-8"))
852
+ for start, end in policy_probe_spans[line_number]:
853
+ encoded[start:end] = b" " * (end - start)
854
+ line = encoded.decode("utf-8")
756
855
  for role, patterns in MCP_ROLE_PATTERNS.items():
757
856
  if any(pattern.search(line) for pattern in patterns):
758
857
  roles.add(role)
@@ -1063,6 +1162,23 @@ def _write_json(path: Path, value: Dict[str, Any]) -> None:
1063
1162
  os.unlink(staged)
1064
1163
 
1065
1164
 
1165
+ def _write_local_text(path: Path, value: str) -> None:
1166
+ """Replace a local review artifact atomically without following its symlink."""
1167
+ path.parent.mkdir(parents=True, exist_ok=True)
1168
+ if path.is_symlink():
1169
+ raise SystemExit("A reconciliation output path is a symlink; remove it and retry.")
1170
+ descriptor, staged = tempfile.mkstemp(prefix=".agenticdome-review-", dir=path.parent)
1171
+ try:
1172
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
1173
+ handle.write(value)
1174
+ handle.flush()
1175
+ os.fsync(handle.fileno())
1176
+ os.replace(staged, path)
1177
+ finally:
1178
+ if os.path.exists(staged):
1179
+ os.unlink(staged)
1180
+
1181
+
1066
1182
  def _load_json(path: Path) -> Dict[str, Any]:
1067
1183
  try:
1068
1184
  value = json.loads(path.read_text(encoding="utf-8"))
@@ -2471,6 +2587,84 @@ def verify_project(root: Path, live: bool = False, run_tests: bool = False,
2471
2587
  return (0 if result["ready"] else 2), result
2472
2588
 
2473
2589
 
2590
+ def _reconciliation_markdown(plan: Dict[str, Any], verification: Dict[str, Any], report_digest: str) -> str:
2591
+ """Give a bounded, source-free worklist without accepting AI claims as proof."""
2592
+ semantic = plan.get("semantic_analysis") if isinstance(plan.get("semantic_analysis"), dict) else {}
2593
+ gate = verification.get("semantic_gate") if isinstance(verification.get("semantic_gate"), dict) else {}
2594
+
2595
+ def groups(field: str) -> List[Tuple[str, str, int, List[int]]]:
2596
+ grouped: Dict[Tuple[str, str], List[int]] = {}
2597
+ rows = semantic.get(field) if isinstance(semantic.get(field), list) else []
2598
+ for item in rows[:100]:
2599
+ if not isinstance(item, dict):
2600
+ continue
2601
+ boundary = str(item.get("boundary") or "unknown")
2602
+ if boundary not in {"prompt_ingress", "tool_execution", "output_egress"}:
2603
+ boundary = "unknown"
2604
+ raw_path = str(item.get("path") or "")[:500].replace("\\", "/")
2605
+ # These are display paths only. Do not let a remote result inject
2606
+ # Markdown, an absolute path, source text, or a shell instruction.
2607
+ if not raw_path or raw_path.startswith("/") or ".." in Path(raw_path).parts:
2608
+ path = "[invalid relative path]"
2609
+ else:
2610
+ path = re.sub(r"[^A-Za-z0-9_./ -]", "_", raw_path)
2611
+ try:
2612
+ line = max(1, min(int(item.get("line") or 1), 10_000_000))
2613
+ except (TypeError, ValueError):
2614
+ line = 1
2615
+ grouped.setdefault((boundary, path), []).append(line)
2616
+ return [(boundary, path, len(lines), sorted(set(lines))[:12])
2617
+ for (boundary, path), lines in sorted(grouped.items())]
2618
+
2619
+ guidance = {
2620
+ "prompt_ingress": "Review the final assembled model messages at each call. Screen untrusted user, retrieved and tool content immediately before the model; test a blocked input with zero model calls.",
2621
+ "tool_execution": "Trace the actual executor. Authorize the exact final tool name and arguments immediately before it; test BLOCKED = zero handler calls and ALLOWED = one.",
2622
+ "output_egress": "Trace the return to the external response, stream or persistence boundary. Review or redact there, and test that the released value—not merely an internal value—is safe.",
2623
+ "unknown": "Trace this path and identify its real boundary before claiming protection.",
2624
+ }
2625
+ lines = [
2626
+ "# AgenticDome guided reconciliation", "",
2627
+ "This is a local, source-free worklist for the current validation run. It is not a bypass waiver, a code patch, or live tenant proof.",
2628
+ "The AI-authored AgenticDome_Integration.md remains an assertion until these checks and real-path tests pass.", "",
2629
+ f"- AI report SHA-256: `{report_digest}`",
2630
+ f"- Action-required candidates: {int(gate.get('unresolved_bypasses', 0))}",
2631
+ f"- Review-required internal or indirect paths: {int(gate.get('review_required', 0))}",
2632
+ f"- Semantic confidence: {gate.get('confidence', 'unavailable')}", "",
2633
+ ]
2634
+ for field, title in (("bypass_risks", "Action required"), ("review_findings", "Trace and classify")):
2635
+ lines.extend([f"## {title}", ""])
2636
+ found = groups(field)
2637
+ if not found:
2638
+ lines.append("No findings in this category for this run.")
2639
+ for boundary, path, count, positions in found:
2640
+ references = ", ".join(str(number) for number in positions)
2641
+ if count > len(positions):
2642
+ references += ", …"
2643
+ lines.extend([
2644
+ f"### {boundary} · `{path}` ({count} location{'s' if count != 1 else ''})",
2645
+ f"Lines: {references}. {guidance[boundary]}", "",
2646
+ ])
2647
+ lines.append("")
2648
+ tests = verification.get("application_tests") if isinstance(verification.get("application_tests"), dict) else {}
2649
+ report_scope = verification.get("ai_report_scope") if isinstance(verification.get("ai_report_scope"), dict) else {}
2650
+ lines.extend(["## Finish the reconciliation", ""])
2651
+ if report_scope.get("scope_incomplete") or int(report_scope.get("reported_unprotected", 0)) > 0:
2652
+ lines.append("- The AI report itself says a relevant path was NOT ASSESSED or unprotected. Inspect and protect that path, then update the report; its zero-count rows cannot clear this scope gap.")
2653
+ if plan.get("coverage", {}).get("gaps"):
2654
+ lines.append("- Required boundary categories are missing from the inspected workload. Attach and test them before retrying.")
2655
+ if gate.get("cross_part_review_required"):
2656
+ lines.append("- Analysis was split across parts; trace cross-part calls and alternate entry paths. Separate parts are not a proof of guard dominance.")
2657
+ if tests.get("passed") is not True:
2658
+ lines.append("- Workload tests did not pass or were not detected. Run the local test command directly to see its output; no test log is uploaded.")
2659
+ lines.extend([
2660
+ "- Give these grouped paths to your coding assistant together with the real source. Keep the assistant's changes reviewable and update only the canonical AgenticDome_Integration.md.",
2661
+ "- Rerun `agenticdome validate-report --report AgenticDome_Integration.md` after code and test changes. Do not edit the JSON evidence by hand.",
2662
+ "- Even after local `ready=true`, verify a real action through the assigned tenant sidecar and confirm BLOCKED executes zero times and ALLOWED once before going live.",
2663
+ "",
2664
+ ])
2665
+ return "\n".join(lines)
2666
+
2667
+
2474
2668
  def validate_ai_integration_report(root: Path, report_path: Path) -> Tuple[int, Dict[str, Any]]:
2475
2669
  """Validate an AI-assisted edit with the existing semantic and test gates.
2476
2670
 
@@ -2497,6 +2691,7 @@ def validate_ai_integration_report(root: Path, report_path: Path) -> Tuple[int,
2497
2691
  rows = re.findall(r"^\|\s*([1-8])\.([^\r\n]*)", markdown, re.MULTILINE)
2498
2692
  if len(rows) != 8 or {number for number, _ in rows} != {str(index) for index in range(1, 9)}:
2499
2693
  raise SystemExit("The integration report needs one valid row for each of the eight interception categories.")
2694
+ reported_unprotected = 0
2500
2695
  for _, cells_text in rows:
2501
2696
  cells = [cell.strip().replace("*", "").replace(",", "") for cell in cells_text.strip().strip("|").split("|")]
2502
2697
  if len(cells) != 6 or any(not re.fullmatch(r"\d{1,5}", cell) for cell in cells[1:]):
@@ -2504,6 +2699,8 @@ def validate_ai_integration_report(root: Path, report_path: Path) -> Tuple[int,
2504
2699
  found, existing, new, unprotected, live = (int(cell) for cell in cells[1:])
2505
2700
  if any(value > 10000 for value in (found, existing, new, unprotected, live)) or existing + new + unprotected != found or live > existing + new:
2506
2701
  raise SystemExit("The report counts do not reconcile. Correct the AI integration inventory before validation.")
2702
+ reported_unprotected += unprotected
2703
+ scope_incomplete = bool(re.search(r"\bNOT ASSESSED\b", markdown, re.IGNORECASE))
2507
2704
  digest = hashlib.sha256(markdown.encode("utf-8")).hexdigest()
2508
2705
 
2509
2706
  inspection = _exportable_inspection(inspect_repository(root))
@@ -2519,6 +2716,14 @@ def validate_ai_integration_report(root: Path, report_path: Path) -> Tuple[int,
2519
2716
  plan = integration_plan(root)
2520
2717
  plan["inspection_report_sha256"] = inspection["report_sha256"]
2521
2718
  exit_code, verification = verify_project(root, run_tests=True, plan=plan)
2719
+ local_checks_passed = exit_code == 0 and verification.get("ready") is True
2720
+ verification["ai_report_scope"] = {
2721
+ "scope_incomplete": scope_incomplete,
2722
+ "reported_unprotected": reported_unprotected,
2723
+ }
2724
+ if scope_incomplete or reported_unprotected:
2725
+ verification["ready"] = False
2726
+ exit_code = 2
2522
2727
  verification["ai_integration_report_sha256"] = digest
2523
2728
  verification.pop("report_sha256", None)
2524
2729
  verification["report_sha256"] = hashlib.sha256(
@@ -2527,18 +2732,34 @@ def validate_ai_integration_report(root: Path, report_path: Path) -> Tuple[int,
2527
2732
 
2528
2733
  inspection_path = root / "agenticdome-inspection.json"
2529
2734
  verification_path = root / ".agenticdome" / "verification.json"
2530
- for output in (inspection_path, verification_path):
2735
+ reconciliation_path = root / ".agenticdome" / "RECONCILIATION.md"
2736
+ for output in (inspection_path, verification_path, reconciliation_path):
2531
2737
  if output.is_symlink() or not output.resolve().is_relative_to(root):
2532
2738
  raise SystemExit("An evidence output path is a symlink or leaves this workload; remove that path and retry.")
2533
2739
  _write_json(inspection_path, inspection)
2534
2740
  _write_json(verification_path, verification)
2741
+ _write_local_text(reconciliation_path, _reconciliation_markdown(plan, verification, digest))
2742
+ partial_coverage = local_checks_passed and not verification["ready"]
2535
2743
  return exit_code, {
2536
- "status": "validation_passed" if verification["ready"] else "validation_needs_attention",
2744
+ "status": "validation_passed" if verification["ready"] else (
2745
+ "partial_coverage" if partial_coverage else "validation_needs_attention"
2746
+ ),
2537
2747
  "inspection": str(inspection_path.relative_to(root)),
2538
2748
  "verification": str(verification_path.relative_to(root)),
2749
+ "reconciliation": str(reconciliation_path.relative_to(root)),
2539
2750
  "ai_integration_report_sha256": digest,
2540
2751
  "ready": verification["ready"],
2541
- "next_action": "Import both JSON files in Onboarding; then run the assigned-runtime check and test a real action path. No customer action executes in these fixed decision cases.",
2752
+ "next_action": (
2753
+ "Import both JSON files in Onboarding; then run the assigned-runtime check and test a real action path. No customer action executes in these fixed decision cases."
2754
+ if verification["ready"] else (
2755
+ "Import both JSON files as partial evidence and run a tenant-policy diagnostic if connected. "
2756
+ "Open .agenticdome/RECONCILIATION.md, assess or protect the remaining paths, then rerun validate-report. "
2757
+ "This is not passing workload evidence and does not unlock activation."
2758
+ if partial_coverage else
2759
+ "You may import both JSON files as diagnostic evidence, but not as passing evidence. "
2760
+ "Open .agenticdome/RECONCILIATION.md, fix or investigate grouped paths and failing tests, then rerun validate-report."
2761
+ )
2762
+ ),
2542
2763
  }
2543
2764
 
2544
2765
 
@@ -69,8 +69,17 @@ agenticdome validate-report --report AgenticDome_Integration.md
69
69
 
70
70
  This does not edit the application. It checks current source-free structure,
71
71
  requests the tenant-bound semantic assessment and runs detected workload tests.
72
- Import both generated files, `agenticdome-inspection.json` and
73
- `.agenticdome/verification.json`, together in the same onboarding workload.
72
+ The command also writes a local `.agenticdome/RECONCILIATION.md` worklist. If
73
+ it exits non-zero or says `ready=false`, open that file: it groups possible
74
+ bypasses and uncertain internal paths by source file, with the next check for
75
+ each boundary. Review the actual source, fix or investigate those paths, rerun
76
+ the workload tests and validation, and update the canonical AI report. The
77
+ worklist is not uploaded and cannot waive a finding. You may import both
78
+ generated files, `agenticdome-inspection.json` and
79
+ `.agenticdome/verification.json`, together in the same onboarding workload even
80
+ when validation is incomplete. The portal records this as partial evidence and
81
+ can run fixed tenant-policy diagnostics, but it does not grant full verification
82
+ or activation readiness. Rerun and reimport after closing the gaps.
74
83
  Their hashes bind them to the exact Markdown report. A changed report requires
75
84
  rerunning validation. The portal then performs its separate assigned-runtime
76
85
  check; test the actual customer action path before activation. MCP and
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agenticdome-python-sdk"
7
- version = "1.2.37"
7
+ version = "1.2.39"
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"