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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agenticdome-python-sdk
3
- Version: 1.2.36
3
+ Version: 1.2.38
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
@@ -182,7 +182,16 @@ certified onboarding path, obtain the assigned sidecar and a dedicated
182
182
  local exact diff and change summary, explicitly approve a supported edit or
183
183
  attach other boundaries manually, then run
184
184
  `agenticdome verify --run-tests --output .agenticdome/verification.json`.
185
- Preview obtains the private, signed, catalog-bound semantic plan and keeps
185
+
186
+ If a coding assistant has already made and tested the application edits,
187
+ import its canonical `AgenticDome_Integration.md` in the customer portal and
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
191
+ remain AI-reported and a real action-path test is still required before
192
+ claiming protection. See the [customer onboarding guide](docs/customer-onboarding.md).
193
+
194
+ For the Pilot path, preview obtains the private, signed, catalog-bound semantic plan and keeps
186
195
  application source on your machine. `agenticdome integrate apply` creates a
187
196
  local Git review branch but never commits, pushes or deploys. Existing-source
188
197
  automatic edits cover unambiguous CrewAI bootstrap, PydanticAI `Agent(...)`,
@@ -1,5 +1,5 @@
1
- agenticdome_python_sdk-1.2.36.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
2
- agenticdome_python_sdk-1.2.36.dist-info/licenses/NOTICE,sha256=obvKP6kV1Ny5R8_INFOpf0X5um9U2X43LGwQFobO9kg,550
1
+ agenticdome_python_sdk-1.2.38.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
2
+ agenticdome_python_sdk-1.2.38.dist-info/licenses/NOTICE,sha256=obvKP6kV1Ny5R8_INFOpf0X5um9U2X43LGwQFobO9kg,550
3
3
  agenticdome_sdk/__init__.py,sha256=1JY5ELhtp0zg-82xhgt6NxZ7k5CWL5e_Tm6BrsuCFXc,1102
4
4
  agenticdome_sdk/_framework_firewall.py,sha256=W3gy7WsK50scE7ypJrmVJhHUOBsr0zAWQ85rghqaJk4,31963
5
5
  agenticdome_sdk/_mode.py,sha256=XvOgh5sW-FG8S39V483h3uLiFh8wSt14OmYXz_6eDO8,1283
@@ -8,7 +8,7 @@ agenticdome_sdk/agno.py,sha256=L3yfjTBHW47xzT67a1DsjtwmvrAoYBkbxADpsr85Cp0,60766
8
8
  agenticdome_sdk/attestation.py,sha256=ij4cMff7nzLaIS0W02d2WURFcyKe72bcUlSz-dYkaLE,10310
9
9
  agenticdome_sdk/autogen.py,sha256=KP4qlIcsNXB-4ZT9D7Slc1xWEeWXRIWkc5HGW0U6k2c,29018
10
10
  agenticdome_sdk/aws_bedrock.py,sha256=2zD9j7xi0jna0sMeMf-hKZdc3Y4RPwzj6tMVOUGgIDQ,69581
11
- agenticdome_sdk/catalog_snapshot.json,sha256=xHntC0Cjzli4pP0fyTxVtbH90LthGgaq7ad6YGmy54Q,951
11
+ agenticdome_sdk/catalog_snapshot.json,sha256=aVXNwobV40gCMiG-pYgio5fonr2jo8QJRdlGsYJquXA,951
12
12
  agenticdome_sdk/claude.py,sha256=XeY9AUu61SYcJjQVz_AtL_WaN6X4KqP5nprEbbr4Q6A,17456
13
13
  agenticdome_sdk/client.py,sha256=SQk7BROrcedyHAfubGiwGNNGycq7W7L-Ncjroj1ugBg,69815
14
14
  agenticdome_sdk/connect.py,sha256=MY30wXYA6OaZhs__E67KcFXEy7mZMBxMSV06BnlBjRg,18766
@@ -21,7 +21,7 @@ agenticdome_sdk/framework_edits.py,sha256=4BgWHxWKEkex8_DMhjVxjovpUVkrMF7TsCZzlS
21
21
  agenticdome_sdk/generic_python.py,sha256=JtXRcYEZ6_Dgxnnv4QgqeMZR8dFVr8EqFDM-XYQ-GMA,6823
22
22
  agenticdome_sdk/google_adk.py,sha256=2TA-tUhY0lOnkGcs1BN9CUEz67ebVX4HJ7mnNoRPcZs,57258
23
23
  agenticdome_sdk/guided_integration.py,sha256=m7EsgLrPEJhfrVMaVWO2DtjYp6S8PwpCKDYZPNbVZ2o,52259
24
- agenticdome_sdk/hook_catalog.py,sha256=4cQvWsuPa2nOZdlhp1Ya6SDo2M83aSHHtiLxrM3dkz8,29272
24
+ agenticdome_sdk/hook_catalog.py,sha256=XRc_MeEE3LArX1R0xfO3Vo6nZbF3U-4cOkTOMfMOZS8,29272
25
25
  agenticdome_sdk/identity.py,sha256=mrymGgZSVsKJ9THRde54qGmMT7j_uZHmPJ-HtWk7PXE,4635
26
26
  agenticdome_sdk/langgraph.py,sha256=FUvF6DSA5aZP3eoBoNeldgGtPrz-dO5xcVmrPPSBEhI,84312
27
27
  agenticdome_sdk/lifecycle.py,sha256=9LvxmUU1LC_tMchb5uVFBnZIkVFUxOv3VH9wG_9gPtA,12855
@@ -32,15 +32,15 @@ agenticdome_sdk/mcp_rehearsal_server.py,sha256=cox4XqUTL8d0gaDzqxDZcEALkaz8otjOj
32
32
  agenticdome_sdk/mcp_verification.py,sha256=WtRhAFVPTBqGOcHNf_db1m4jX9_TO0oxP98XWk4_ISY,14551
33
33
  agenticdome_sdk/microsoft_agent_framework.py,sha256=GfIIuk0n-VhsZaTyoB7P6ilpz9bFLD95TWuX0rFSWeU,67219
34
34
  agenticdome_sdk/microsoft_ai_foundry.py,sha256=YlO968s1dbAYcaEqhqufL92NFR66GuTtngnCv7-1mYc,56911
35
- agenticdome_sdk/onboarding_cli.py,sha256=jfc4owNPrvlVkYa5eLaQfATA6sbgXGkhxf4IJfg6gWo,145081
35
+ agenticdome_sdk/onboarding_cli.py,sha256=XVI1mYJi1VuMCpVTb1eMbXRbtAi2X3r0xVxWq6Zfv24,155268
36
36
  agenticdome_sdk/openai_agents.py,sha256=0z-7FDBIG4usxnILpan6yjQHF4zXIzm9YLsh3-bW8Lw,57678
37
37
  agenticdome_sdk/outcomes.py,sha256=jadWDzQaZXrHNvpPme8lQY0jihYLCoM3s4CihAq0JRo,2507
38
38
  agenticdome_sdk/proof.py,sha256=ECKc14xSE0V1HHmcpZjfVz8-Q2H_fooah3BfdblFV8M,2878
39
39
  agenticdome_sdk/pydantic.py,sha256=WKb8X0Fgydfid-ep8E-M8Yslz2pWcCP6w7n_aBxt1rI,54961
40
40
  agenticdome_sdk/simulation.py,sha256=UCbZy3u9AzfxQCfY38SCV3CxGxhinV9EA4aHMPlK4mc,8426
41
41
  agenticdome_sdk/smolagents.py,sha256=wjMA1HVAQ-9wRNM38Jr9wlCpKMhF-9vxtTk6R-kwQhs,19094
42
- agenticdome_python_sdk-1.2.36.dist-info/METADATA,sha256=4gqvhEqeY-7JwahLP92qIULZ8G8-T5eaQsKUL0eqhUk,144718
43
- agenticdome_python_sdk-1.2.36.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
44
- agenticdome_python_sdk-1.2.36.dist-info/entry_points.txt,sha256=cRfCphkr5gLrkGIzXJEtL50HdHLajoZLbZtcMLE8NoY,113
45
- agenticdome_python_sdk-1.2.36.dist-info/top_level.txt,sha256=LZbbjXco5mqDrGwlTJpIKI5Sp6R_njzHHjExxAs9c3s,16
46
- agenticdome_python_sdk-1.2.36.dist-info/RECORD,,
42
+ agenticdome_python_sdk-1.2.38.dist-info/METADATA,sha256=L_2D7HRNQMh2-2Ha5mkZhDAmRTCqyucnM884rhMpur4,145282
43
+ agenticdome_python_sdk-1.2.38.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
44
+ agenticdome_python_sdk-1.2.38.dist-info/entry_points.txt,sha256=cRfCphkr5gLrkGIzXJEtL50HdHLajoZLbZtcMLE8NoY,113
45
+ agenticdome_python_sdk-1.2.38.dist-info/top_level.txt,sha256=LZbbjXco5mqDrGwlTJpIKI5Sp6R_njzHHjExxAs9c3s,16
46
+ agenticdome_python_sdk-1.2.38.dist-info/RECORD,,
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "schema": "agenticdome.catalog-snapshot.v1",
3
3
  "catalog_schema": "agenticdome.hook-catalog.v1",
4
- "verified_at": "2026-09-29",
5
- "python_sdk_version": "1.2.36",
4
+ "verified_at": "2026-10-02",
5
+ "python_sdk_version": "1.2.38",
6
6
  "published_packages": {
7
7
  "agenticdome-python-sdk": {
8
8
  "registry": "pypi",
9
- "version": "1.2.36",
10
- "url": "https://pypi.org/project/agenticdome-python-sdk/1.2.36/"
9
+ "version": "1.2.38",
10
+ "url": "https://pypi.org/project/agenticdome-python-sdk/1.2.38/"
11
11
  },
12
12
  "agenticdome-sdk": {
13
13
  "registry": "npm",
@@ -21,8 +21,8 @@
21
21
  },
22
22
  "openclaw": {
23
23
  "registry": "npm",
24
- "version": "2026.9.6",
25
- "url": "https://www.npmjs.com/package/openclaw/v/2026.9.6"
24
+ "version": "2026.9.7",
25
+ "url": "https://www.npmjs.com/package/openclaw/v/2026.9.7"
26
26
  }
27
27
  }
28
28
  }
@@ -39,7 +39,7 @@ CATALOG_VERIFIED_AT = str(_CATALOG_SNAPSHOT["verified_at"])
39
39
 
40
40
 
41
41
  _OPENCLAW_RUNTIME_MIN_VERSION = "2026.7.1-2"
42
- _OPENCLAW_RUNTIME_MAX_VERSION = "2026.9.6"
42
+ _OPENCLAW_RUNTIME_MAX_VERSION = "2026.9.7"
43
43
  _OPENCLAW_RUNTIME_VERSION = _OPENCLAW_RUNTIME_MAX_VERSION
44
44
  _OPENCLAW_NODE_RANGE = ">=24.16.0 <25 || >=26.1.0"
45
45
 
@@ -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
@@ -716,6 +717,82 @@ def _mcp_evidence_id(kind: str, relative: str, line: int) -> str:
716
717
  return f"{kind}-{digest}"
717
718
 
718
719
 
720
+ def _agenticdome_policy_probe_spans(text: str) -> Dict[int, List[Tuple[int, int]]]:
721
+ """Mask only a confirmed AgenticDome policy call's MCP method literal.
722
+
723
+ A policy client can use the MCP JSON-RPC envelope without forwarding
724
+ customer MCP tools. Keep all other MCP calls in the same file visible.
725
+ Unparseable or ambiguous source receives no exception.
726
+ """
727
+ if "tools/call" not in text or "guardrail.validate" not in text:
728
+ return {}
729
+ try:
730
+ module = ast.parse(text)
731
+ except (SyntaxError, ValueError):
732
+ return {}
733
+
734
+ def field(mapping: ast.AST, key: str) -> Optional[ast.AST]:
735
+ if not isinstance(mapping, ast.Dict):
736
+ return None
737
+ return next(
738
+ (value for name, value in zip(mapping.keys, mapping.values)
739
+ if isinstance(name, ast.Constant) and name.value == key),
740
+ None,
741
+ )
742
+
743
+ def is_literal(node: Optional[ast.AST], value: str) -> bool:
744
+ return isinstance(node, ast.Constant) and node.value == value
745
+
746
+ # The legacy urllib client uses an environment-overridable URL whose
747
+ # checked-in default is AgenticDome's own /mcp policy endpoint.
748
+ policy_url_names: set[str] = set()
749
+ for node in ast.walk(module):
750
+ if not isinstance(node, (ast.Assign, ast.AnnAssign)):
751
+ continue
752
+ targets = node.targets if isinstance(node, ast.Assign) else [node.target]
753
+ value = node.value
754
+ if value is None or not any(
755
+ isinstance(part, ast.Constant) and isinstance(part.value, str)
756
+ and re.fullmatch(r"https?://(?:[a-z0-9-]+\.)*agenticdome\.io(?::\d+)?/mcp/?", part.value, re.I)
757
+ for part in ast.walk(value)
758
+ ):
759
+ continue
760
+ policy_url_names.update(target.id for target in targets if isinstance(target, ast.Name))
761
+
762
+ spans: Dict[int, List[Tuple[int, int]]] = {}
763
+ for function in ast.walk(module):
764
+ if not isinstance(function, (ast.FunctionDef, ast.AsyncFunctionDef)):
765
+ continue
766
+ calls = [node for node in ast.walk(function) if isinstance(node, ast.Call)]
767
+ for assignment in ast.walk(function):
768
+ if not isinstance(assignment, (ast.Assign, ast.AnnAssign)):
769
+ continue
770
+ targets = assignment.targets if isinstance(assignment, ast.Assign) else [assignment.target]
771
+ names = {target.id for target in targets if isinstance(target, ast.Name)}
772
+ method = field(assignment.value, "method") if assignment.value is not None else None
773
+ params = field(assignment.value, "params") if assignment.value is not None else None
774
+ if not names or not is_literal(method, "tools/call") or not is_literal(field(params, "name"), "guardrail.validate"):
775
+ continue
776
+
777
+ def sends_policy_payload(call: ast.Call) -> bool:
778
+ if not any(isinstance(part, ast.Name) and part.id in names for part in ast.walk(call)):
779
+ return False
780
+ if (isinstance(call.func, ast.Attribute) and call.func.attr == "_post"
781
+ and call.args and is_literal(call.args[0], "/mcp")):
782
+ return True
783
+ return (
784
+ isinstance(call.func, (ast.Name, ast.Attribute))
785
+ and (call.func.id if isinstance(call.func, ast.Name) else call.func.attr) == "Request"
786
+ and bool(call.args)
787
+ and isinstance(call.args[0], ast.Name)
788
+ and call.args[0].id in policy_url_names
789
+ )
790
+
791
+ if any(sends_policy_payload(call) for call in calls):
792
+ spans.setdefault(method.lineno, []).append((method.col_offset, method.end_col_offset))
793
+ return spans
794
+
795
+
719
796
  def _detect_mcp_protection(root: Path, paths: Sequence[Path], languages: Sequence[str]) -> Dict[str, Any]:
720
797
  """Collect source-free MCP topology and candidate protection evidence.
721
798
 
@@ -735,15 +812,21 @@ def _detect_mcp_protection(root: Path, paths: Sequence[Path], languages: Sequenc
735
812
  for path in paths:
736
813
  relative = _relative(path, root)
737
814
  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 = (
815
+ python_source = path.suffix.lower() in {".py", ".pyi"}
816
+ policy_probe_spans = _agenticdome_policy_probe_spans(text) if python_source else {}
817
+ # Preserve the existing JavaScript/TypeScript policy-client exception.
818
+ # Unlike Python, this scanner has no AST span for those languages, so
819
+ # exempt only a single policy call with no other MCP integration hints.
820
+ if not python_source and (
742
821
  bool(re.search(r"['\"]name['\"]\s*:\s*['\"]guardrail\.validate['\"]", text))
743
822
  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:
823
+ and len(re.findall(r"\btools/call\b", text)) == 1
824
+ and not re.search(
825
+ r"\b(?:FastMCP|McpServer|MCPClient|McpClient|ClientSession|StdioClientTransport|"
826
+ r"SSEClientTransport|call_tool|callTool|registerTool|tools/list|setRequestHandler)\b|@modelcontextprotocol",
827
+ text,
828
+ )
829
+ ):
747
830
  continue
748
831
  if not re.search(
749
832
  r"(?:\b(?:from\s+mcp|import\s+mcp|FastMCP|MCPClient|McpClient|McpServer|ClientSession|"
@@ -753,6 +836,14 @@ def _detect_mcp_protection(root: Path, paths: Sequence[Path], languages: Sequenc
753
836
  ):
754
837
  continue
755
838
  for line_number, line in enumerate(text.splitlines(), start=1):
839
+ # AST offsets are UTF-8 byte offsets. Mask the exact policy method
840
+ # literal, not the whole line or file: a real MCP call alongside
841
+ # the policy probe must still trigger specialist onboarding.
842
+ if line_number in policy_probe_spans:
843
+ encoded = bytearray(line.encode("utf-8"))
844
+ for start, end in policy_probe_spans[line_number]:
845
+ encoded[start:end] = b" " * (end - start)
846
+ line = encoded.decode("utf-8")
756
847
  for role, patterns in MCP_ROLE_PATTERNS.items():
757
848
  if any(pattern.search(line) for pattern in patterns):
758
849
  roles.add(role)
@@ -2471,6 +2562,77 @@ def verify_project(root: Path, live: bool = False, run_tests: bool = False,
2471
2562
  return (0 if result["ready"] else 2), result
2472
2563
 
2473
2564
 
2565
+ def validate_ai_integration_report(root: Path, report_path: Path) -> Tuple[int, Dict[str, Any]]:
2566
+ """Validate an AI-assisted edit with the existing semantic and test gates.
2567
+
2568
+ Markdown is a customer-authored inventory, never an authorization or proof
2569
+ source. Only its digest is added to the source-free evidence exports.
2570
+ """
2571
+ if report_path.name != "AgenticDome_Integration.md" or report_path.is_symlink() or not report_path.is_file():
2572
+ raise SystemExit("Select this workload's AgenticDome_Integration.md (regular file, exact name).")
2573
+ if not report_path.resolve().is_relative_to(root) or report_path.stat().st_size > 524288:
2574
+ raise SystemExit("The integration report must be inside this workload and no larger than 512 KB.")
2575
+ try:
2576
+ markdown = report_path.read_text(encoding="utf-8")
2577
+ except (UnicodeError, OSError) as exc:
2578
+ raise SystemExit("Read AgenticDome_Integration.md as UTF-8 and retry; the file could not be read safely.") from exc
2579
+ if not markdown or "\x00" in markdown:
2580
+ raise SystemExit("AgenticDome_Integration.md is empty or contains unsupported binary content.")
2581
+ scope_match = re.search(r"^Workload scope:\s*([^\r\n]+)", markdown, re.IGNORECASE | re.MULTILINE)
2582
+ if not scope_match:
2583
+ raise SystemExit("Add a Workload scope line naming this deployable workload to AgenticDome_Integration.md.")
2584
+ quoted = re.search(r"`([^`]+)`", scope_match.group(1))
2585
+ scope = (quoted.group(1) if quoted else scope_match.group(1).split()[0]).strip().rstrip("/")
2586
+ if Path(scope).name.casefold() != root.name.casefold() or ".." in Path(scope).parts:
2587
+ raise SystemExit("The report's Workload scope does not match this workload root. Run from the correct deployable service.")
2588
+ rows = re.findall(r"^\|\s*([1-8])\.([^\r\n]*)", markdown, re.MULTILINE)
2589
+ if len(rows) != 8 or {number for number, _ in rows} != {str(index) for index in range(1, 9)}:
2590
+ raise SystemExit("The integration report needs one valid row for each of the eight interception categories.")
2591
+ for _, cells_text in rows:
2592
+ cells = [cell.strip().replace("*", "").replace(",", "") for cell in cells_text.strip().strip("|").split("|")]
2593
+ if len(cells) != 6 or any(not re.fullmatch(r"\d{1,5}", cell) for cell in cells[1:]):
2594
+ raise SystemExit("Each interception row needs five non-negative counts after its category.")
2595
+ found, existing, new, unprotected, live = (int(cell) for cell in cells[1:])
2596
+ if any(value > 10000 for value in (found, existing, new, unprotected, live)) or existing + new + unprotected != found or live > existing + new:
2597
+ raise SystemExit("The report counts do not reconcile. Correct the AI integration inventory before validation.")
2598
+ digest = hashlib.sha256(markdown.encode("utf-8")).hexdigest()
2599
+
2600
+ inspection = _exportable_inspection(inspect_repository(root))
2601
+ if inspection.get("scope", {}).get("complete") is not True:
2602
+ raise SystemExit(_scope_gap_message(inspection))
2603
+ if inspection.get("mcp_protection", {}).get("detected") is True or inspection.get("openclaw_protection", {}).get("ready") is True:
2604
+ raise SystemExit("This workload needs the specialist MCP or OpenClaw onboarding path. Keep the AI report for review, then use that journey's verification commands.")
2605
+ inspection["ai_integration_report_sha256"] = digest
2606
+ inspection.pop("report_sha256", None)
2607
+ inspection["report_sha256"] = hashlib.sha256(
2608
+ json.dumps(inspection, sort_keys=True, separators=(",", ":")).encode("utf-8")
2609
+ ).hexdigest()
2610
+ plan = integration_plan(root)
2611
+ plan["inspection_report_sha256"] = inspection["report_sha256"]
2612
+ exit_code, verification = verify_project(root, run_tests=True, plan=plan)
2613
+ verification["ai_integration_report_sha256"] = digest
2614
+ verification.pop("report_sha256", None)
2615
+ verification["report_sha256"] = hashlib.sha256(
2616
+ json.dumps(verification, sort_keys=True, separators=(",", ":")).encode("utf-8")
2617
+ ).hexdigest()
2618
+
2619
+ inspection_path = root / "agenticdome-inspection.json"
2620
+ verification_path = root / ".agenticdome" / "verification.json"
2621
+ for output in (inspection_path, verification_path):
2622
+ if output.is_symlink() or not output.resolve().is_relative_to(root):
2623
+ raise SystemExit("An evidence output path is a symlink or leaves this workload; remove that path and retry.")
2624
+ _write_json(inspection_path, inspection)
2625
+ _write_json(verification_path, verification)
2626
+ return exit_code, {
2627
+ "status": "validation_passed" if verification["ready"] else "validation_needs_attention",
2628
+ "inspection": str(inspection_path.relative_to(root)),
2629
+ "verification": str(verification_path.relative_to(root)),
2630
+ "ai_integration_report_sha256": digest,
2631
+ "ready": verification["ready"],
2632
+ "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.",
2633
+ }
2634
+
2635
+
2474
2636
  def _print(value: Any, as_json: bool = False) -> None:
2475
2637
  if as_json or isinstance(value, (dict, list)):
2476
2638
  print(json.dumps(value, indent=2, sort_keys=True))
@@ -2575,6 +2737,11 @@ def build_parser() -> argparse.ArgumentParser:
2575
2737
  )
2576
2738
  verify_parser.add_argument("--output")
2577
2739
 
2740
+ ai_validate_parser = subparsers.add_parser(
2741
+ "validate-report", help="Validate a coding-assistant integration report against this workload; no source upload or code edit."
2742
+ )
2743
+ ai_validate_parser.add_argument("--report", default="AgenticDome_Integration.md")
2744
+
2578
2745
  action_parser = subparsers.add_parser(
2579
2746
  "verify-action",
2580
2747
  help="Run one safe customer test and correlate the real SDK hook, live verdict and handler spy.",
@@ -2754,6 +2921,13 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2754
2921
  record_verification(root, result)
2755
2922
  _print(result)
2756
2923
  return exit_code
2924
+ if args.command == "validate-report":
2925
+ report_path = Path(args.report)
2926
+ if not report_path.is_absolute():
2927
+ report_path = root / report_path
2928
+ exit_code, result = validate_ai_integration_report(root, report_path)
2929
+ _print(result)
2930
+ return exit_code
2757
2931
  if args.command == "verify-action":
2758
2932
  from .action_path_proof import run_action_path_test
2759
2933
  command = list(args.test_command)