agenticdome-python-sdk 1.2.33__py3-none-any.whl → 1.2.34__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.33
3
+ Version: 1.2.34
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
@@ -218,7 +218,10 @@ agenticdome openclaw verify --output .agenticdome/verification.json
218
218
  AgenticDome plugin, explicit consent and the exact certified typed hooks.
219
219
  `agenticdome plan` returns source-free, tenant-bound Integration Copilot
220
220
  guidance for review. `openclaw verify` adds workspace-test and assigned-tenant
221
- policy evidence. The
221
+ policy evidence. If the same deployable workload also uses MCP, this command
222
+ includes the MCP transport rehearsal and both `mcp_verification` and
223
+ `openclaw_verification` in one report. Both sections must be ready before the
224
+ report is imported; `mcp verify` alone is not a combined-workload proof. The
222
225
  customer Control Panel separately confirms tenant binding and retained
223
226
  telemetry; neither command silently modifies customer source.
224
227
 
@@ -228,6 +231,17 @@ languages and candidate boundaries can be discovered locally; business
228
231
  purpose, sensitive actions and managed-versus-sovereign deployment remain
229
232
  explicit customer decisions and are not guessed from source code.
230
233
 
234
+ `agenticdome init` creates local configuration without contacting Copilot.
235
+ `inspect` and `plan` send source-free structural metadata to the assigned
236
+ tenant sidecar. A selection spanning several deployable packages is analyzed
237
+ as bounded, sequential workload units; successful units are checkpointed
238
+ locally so a retry does not resubmit them. The report lists analyzed parts
239
+ and any result-list caps. It does not claim that calls or guard placement
240
+ across separate workload units were proven; review those boundaries before
241
+ production. If the selection exceeds the 24-unit safety limit, select the
242
+ application or package that actually executes agent actions. No unexamined
243
+ unit is silently omitted or reported as protected.
244
+
231
245
  Use **Customer Control Panel → Activate Action Firewall → Core Config** to
232
246
  choose the Developer Integration path and import the two evidence files. This
233
247
  path is for SDK, MCP, framework or custom-code integrations; Microsoft
@@ -1,5 +1,5 @@
1
- agenticdome_python_sdk-1.2.33.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
2
- agenticdome_python_sdk-1.2.33.dist-info/licenses/NOTICE,sha256=obvKP6kV1Ny5R8_INFOpf0X5um9U2X43LGwQFobO9kg,550
1
+ agenticdome_python_sdk-1.2.34.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
2
+ agenticdome_python_sdk-1.2.34.dist-info/licenses/NOTICE,sha256=obvKP6kV1Ny5R8_INFOpf0X5um9U2X43LGwQFobO9kg,550
3
3
  agenticdome_sdk/__init__.py,sha256=UY__0ppEM4MlsTQoLh8alZ7AoMjtegGKe4j_Sj7CpNk,903
4
4
  agenticdome_sdk/_framework_firewall.py,sha256=W3gy7WsK50scE7ypJrmVJhHUOBsr0zAWQ85rghqaJk4,31963
5
5
  agenticdome_sdk/_mode.py,sha256=XvOgh5sW-FG8S39V483h3uLiFh8wSt14OmYXz_6eDO8,1283
@@ -7,11 +7,11 @@ agenticdome_sdk/agno.py,sha256=L3yfjTBHW47xzT67a1DsjtwmvrAoYBkbxADpsr85Cp0,60766
7
7
  agenticdome_sdk/attestation.py,sha256=ij4cMff7nzLaIS0W02d2WURFcyKe72bcUlSz-dYkaLE,10310
8
8
  agenticdome_sdk/autogen.py,sha256=KP4qlIcsNXB-4ZT9D7Slc1xWEeWXRIWkc5HGW0U6k2c,29018
9
9
  agenticdome_sdk/aws_bedrock.py,sha256=2zD9j7xi0jna0sMeMf-hKZdc3Y4RPwzj6tMVOUGgIDQ,69581
10
- agenticdome_sdk/catalog_snapshot.json,sha256=5yFLZ3l1i_qciOd8WlCcSMIoUHX5oJEmxhRyQwdA8OQ,951
10
+ agenticdome_sdk/catalog_snapshot.json,sha256=aOJMqz8FpPF-q1ynpM2xsEk-O-tMyUCPE5Y51dygtLQ,951
11
11
  agenticdome_sdk/claude.py,sha256=XeY9AUu61SYcJjQVz_AtL_WaN6X4KqP5nprEbbr4Q6A,17456
12
12
  agenticdome_sdk/client.py,sha256=2uVKZR1uXDBIvwOS-lpNPCAXl9K5vroPqsPNNYhBWeg,69473
13
- agenticdome_sdk/connect.py,sha256=yNRh84mqdWr73fXW3S8Zc_lV1QN6gce5T6mZ20cr8ps,16726
14
- agenticdome_sdk/copilot_ir.py,sha256=XkDw2E59ulCw4wd1Cwzcc82ibj4DI43ooTtwzdWwsy4,17476
13
+ agenticdome_sdk/connect.py,sha256=MY30wXYA6OaZhs__E67KcFXEy7mZMBxMSV06BnlBjRg,18766
14
+ agenticdome_sdk/copilot_ir.py,sha256=zT13tOX8xu11yrOtIPMRFVud8CQ81Gu3WxsXxyQ4XT0,17928
15
15
  agenticdome_sdk/copilot_ir_collector.cjs,sha256=oxLY8ByONo7Es4YjYiJuRz_A7IKL65Sy486UXnu-4Rs,9681
16
16
  agenticdome_sdk/crewai.py,sha256=sSJQ49O7-8kqOAD2_jrsMtMOmBOcJmFkeOv7S3-ayMs,52490
17
17
  agenticdome_sdk/demo.py,sha256=dyfqt2N_cJoyUWKnjPYuQxuw2hb2DrCNUf1jQKeYYoc,8350
@@ -27,15 +27,15 @@ agenticdome_sdk/mcp_rehearsal_server.py,sha256=cox4XqUTL8d0gaDzqxDZcEALkaz8otjOj
27
27
  agenticdome_sdk/mcp_verification.py,sha256=VqQS5SpPVFI3G7qCwrT90JmG8vFE6pRJEB7PViHjKqw,14235
28
28
  agenticdome_sdk/microsoft_agent_framework.py,sha256=GfIIuk0n-VhsZaTyoB7P6ilpz9bFLD95TWuX0rFSWeU,67219
29
29
  agenticdome_sdk/microsoft_ai_foundry.py,sha256=YlO968s1dbAYcaEqhqufL92NFR66GuTtngnCv7-1mYc,56911
30
- agenticdome_sdk/onboarding_cli.py,sha256=Ezd0fGy8XNKQUKzik4mjIE80Wh0h5zyo7dHxzaoTxNU,102017
30
+ agenticdome_sdk/onboarding_cli.py,sha256=yPPlNRPCjuDNpeJpL-0fJK3RZTOZvII8d-qFHJgTILM,120597
31
31
  agenticdome_sdk/openai_agents.py,sha256=nNM-YcsUDcE23sGv1buLudcYFz0NLju2Mwiw_wrVaJg,57326
32
32
  agenticdome_sdk/outcomes.py,sha256=jadWDzQaZXrHNvpPme8lQY0jihYLCoM3s4CihAq0JRo,2507
33
33
  agenticdome_sdk/proof.py,sha256=ECKc14xSE0V1HHmcpZjfVz8-Q2H_fooah3BfdblFV8M,2878
34
34
  agenticdome_sdk/pydantic.py,sha256=WKb8X0Fgydfid-ep8E-M8Yslz2pWcCP6w7n_aBxt1rI,54961
35
35
  agenticdome_sdk/simulation.py,sha256=UCbZy3u9AzfxQCfY38SCV3CxGxhinV9EA4aHMPlK4mc,8426
36
36
  agenticdome_sdk/smolagents.py,sha256=wjMA1HVAQ-9wRNM38Jr9wlCpKMhF-9vxtTk6R-kwQhs,19094
37
- agenticdome_python_sdk-1.2.33.dist-info/METADATA,sha256=iLGdaO-ciz992R_WZKNeAqBwvpMTjEVwcCOyfmRkkVo,139116
38
- agenticdome_python_sdk-1.2.33.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
39
- agenticdome_python_sdk-1.2.33.dist-info/entry_points.txt,sha256=cRfCphkr5gLrkGIzXJEtL50HdHLajoZLbZtcMLE8NoY,113
40
- agenticdome_python_sdk-1.2.33.dist-info/top_level.txt,sha256=LZbbjXco5mqDrGwlTJpIKI5Sp6R_njzHHjExxAs9c3s,16
41
- agenticdome_python_sdk-1.2.33.dist-info/RECORD,,
37
+ agenticdome_python_sdk-1.2.34.dist-info/METADATA,sha256=hOOXfVmbkCRG1AKVWBUcSqHa1i6G92XEomK47iBKSiU,140120
38
+ agenticdome_python_sdk-1.2.34.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
39
+ agenticdome_python_sdk-1.2.34.dist-info/entry_points.txt,sha256=cRfCphkr5gLrkGIzXJEtL50HdHLajoZLbZtcMLE8NoY,113
40
+ agenticdome_python_sdk-1.2.34.dist-info/top_level.txt,sha256=LZbbjXco5mqDrGwlTJpIKI5Sp6R_njzHHjExxAs9c3s,16
41
+ agenticdome_python_sdk-1.2.34.dist-info/RECORD,,
@@ -2,12 +2,12 @@
2
2
  "schema": "agenticdome.catalog-snapshot.v1",
3
3
  "catalog_schema": "agenticdome.hook-catalog.v1",
4
4
  "verified_at": "2026-09-26",
5
- "python_sdk_version": "1.2.33",
5
+ "python_sdk_version": "1.2.34",
6
6
  "published_packages": {
7
7
  "agenticdome-python-sdk": {
8
8
  "registry": "pypi",
9
- "version": "1.2.33",
10
- "url": "https://pypi.org/project/agenticdome-python-sdk/1.2.33/"
9
+ "version": "1.2.34",
10
+ "url": "https://pypi.org/project/agenticdome-python-sdk/1.2.34/"
11
11
  },
12
12
  "agenticdome-sdk": {
13
13
  "registry": "npm",
@@ -17,9 +17,6 @@ from pathlib import Path
17
17
  from typing import Any, Dict, Optional
18
18
 
19
19
  from .attestation import ensure_attestation_key
20
- from .hook_catalog import CATALOG_SCHEMA, CATALOG_VERIFIED_AT, catalog_digest
21
-
22
-
23
20
  SCOPES = ["onboarding:read", "metadata:write", "evidence:write", "workload:write", "repository:prepare"]
24
21
 
25
22
 
@@ -124,7 +121,10 @@ def _approve_metadata(ir: Dict[str, Any], assume_yes: bool) -> None:
124
121
 
125
122
  def run_connect(root: Path, args: Any) -> Dict[str, Any]:
126
123
  # Import lazily to avoid a module cycle and keep the public client purely orchestration code.
127
- from .onboarding_cli import _installed_sdk_version, inspect_repository
124
+ from .onboarding_cli import (
125
+ _active_copilot_catalog_binding, _copilot_semantic_analysis,
126
+ _ensure_workload_id, _installed_sdk_version, _scope_gap_message, inspect_repository,
127
+ )
128
128
 
129
129
  portal = str(args.portal).rstrip("/")
130
130
  if not portal.startswith("https://") and not getattr(args, "allow_insecure_http", False):
@@ -155,12 +155,23 @@ def run_connect(root: Path, args: Any) -> Dict[str, Any]:
155
155
  if not session.get("runtime") or not session.get("copilot"):
156
156
  raise SystemExit("Authentication succeeded, but no eligible runtime is assigned. The saved portal session can be resumed after an administrator enables an assignment policy.")
157
157
 
158
- report = inspect_repository(root)
158
+ local_workload_id = _ensure_workload_id(root)
159
+ report = inspect_repository(root, remote_analysis=False)
160
+ if report.get("scope", {}).get("complete") is not True:
161
+ raise SystemExit(_scope_gap_message(report))
159
162
  ir = report["copilot_ir"]
160
163
  _approve_metadata(ir, bool(args.yes))
161
164
  private_pem, public_pem = ensure_attestation_key(root / ".agenticdome")
162
165
  del private_pem
163
- workload_uuid = str(uuid.uuid5(uuid.NAMESPACE_URL, session["tenant_id"] + ":" + root.name))
166
+ workload_uuid = str(uuid.uuid5(uuid.NAMESPACE_URL, session["tenant_id"] + ":" + local_workload_id))
167
+ previous_session_path = root / ".agenticdome" / "connect-session.json"
168
+ if previous_session_path.is_file():
169
+ try:
170
+ previous_session = json.loads(previous_session_path.read_text(encoding="utf-8"))
171
+ if previous_session.get("tenant_id") == session["tenant_id"]:
172
+ workload_uuid = str(uuid.UUID(str(previous_session["workload_uuid"])))
173
+ except (OSError, ValueError, KeyError, TypeError, AttributeError, json.JSONDecodeError):
174
+ raise SystemExit("The previous Connect workload identity is unreadable. Restore its connect-session.json before retrying to avoid creating a duplicate workload.")
164
175
  frameworks = [item["key"] for item in report.get("frameworks", [])]
165
176
  _request(portal + "/api/agentguard/connect/workloads", {
166
177
  "session_uuid": session["session_uuid"], "workload_uuid": workload_uuid,
@@ -171,38 +182,51 @@ def run_connect(root: Path, args: Any) -> Dict[str, Any]:
171
182
 
172
183
  runtime_base = str(session["runtime"]["api_base"]).rstrip("/")
173
184
  copilot_key = str(session["copilot"]["api_key"])
174
- ir_digest = hashlib.sha256(json.dumps(ir, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
175
- plan = _request(runtime_base + "/integration-copilot/v1/analyze", {
176
- "schema": "agenticdome.copilot-request.v1", "ir": ir,
177
- "catalog_binding": {"schema": CATALOG_SCHEMA, "catalog_digest": catalog_digest(), "verified_at": CATALOG_VERIFIED_AT},
178
- }, headers={"X-API-Key": copilot_key, "X-Tenant-Id": session["tenant_id"], "Idempotency-Key": hashlib.sha256((session["tenant_id"] + ir_digest + catalog_digest()).encode()).hexdigest()})
179
- if plan.get("schema") != "agenticdome.copilot-plan.v1" or plan.get("tenant_id") != session["tenant_id"]:
180
- raise SystemExit("The assigned private Copilot returned an invalid tenant-bound plan.")
181
- semantic = plan.get("semantic_analysis") or {}
182
- if semantic.get("ir_sha256") != ir_digest:
183
- raise SystemExit("The private Copilot plan was not bound to the submitted metadata.")
184
- _request(portal + "/api/agentguard/connect/sessions/" + session["session_uuid"] + "/metadata", {"ir": ir, "signed_plan": plan}, session["access_token"])
185
-
185
+ from .onboarding_cli import _ir_sha256
186
+ ir_digest = _ir_sha256(ir)
186
187
  state_dir = root / ".agenticdome"
187
188
  state_dir.mkdir(parents=True, exist_ok=True)
188
- cache = {"schema": "agenticdome.copilot-cache.v1", "source_upload": False, "tenant_id": session["tenant_id"], "api_base": runtime_base, "semantic_analysis": semantic, "catalog_binding": plan.get("catalog_binding")}
189
- (state_dir / "copilot-analysis.json").write_text(json.dumps(cache, indent=2, sort_keys=True) + "\n", encoding="utf-8")
190
189
  config_path = state_dir / "config.json"
191
190
  if not config_path.exists():
192
191
  config_path.write_text(json.dumps({
193
- "schema": "agenticdome.project-config.v1", "frameworks": frameworks or ["custom-python"],
192
+ "schema": "agenticdome.project-config.v1", "workload_id": local_workload_id,
193
+ "frameworks": frameworks or ["custom-python"],
194
194
  "execution_broker_mode": "policy",
195
195
  "business_purpose": "REVIEW_REQUIRED_NOT_INVENTED", "sensitive_tools": [],
196
196
  "deployment": {"preference": "managed", "region": session["runtime"].get("region") or "auto"},
197
197
  "source_upload": False,
198
198
  }, indent=2, sort_keys=True) + "\n", encoding="utf-8")
199
- resumable = {"schema": "agenticdome.connect-state.v1", "portal": portal, "tenant_id": session["tenant_id"], "session_uuid": session["session_uuid"], "workload_uuid": workload_uuid, "token_expires_at": session["expires_at"], "access_token": session["access_token"]}
200
- state_path = state_dir / "connect-session.json"
201
- state_path.write_text(json.dumps(resumable, indent=2, sort_keys=True) + "\n", encoding="utf-8")
202
- os.chmod(state_path, 0o600)
203
199
  previous = {name: os.environ.get(name) for name in ("AGENTICDOME_API_BASE", "AGENTICDOME_COPILOT_API_KEY", "AGENTICDOME_TENANT_ID")}
204
200
  os.environ.update({"AGENTICDOME_API_BASE": runtime_base, "AGENTICDOME_COPILOT_API_KEY": copilot_key, "AGENTICDOME_TENANT_ID": session["tenant_id"]})
205
201
  try:
202
+ semantic = _copilot_semantic_analysis(root, ir, required=True)
203
+ binding = _active_copilot_catalog_binding(root)
204
+ if semantic.get("ir_sha256") != ir_digest or not binding:
205
+ raise SystemExit("The private Copilot result was not bound to this workload and the installed SDK catalog.")
206
+ # The portal stores a compact inventory, never the potentially large
207
+ # per-function graph. Every selected part was analyzed at the sidecar.
208
+ portal_ir = {
209
+ "schema": "agenticdome.copilot-ir.v1", "source_upload": False,
210
+ "metadata_kind": "bounded_workload_summary", "selected_ir_sha256": ir_digest,
211
+ "files": [], "functions": [],
212
+ "features": {"functions": len(ir.get("functions", []))},
213
+ "scope": {
214
+ "complete": True, "fingerprint": ir.get("scope", {}).get("fingerprint"),
215
+ "eligible_files_scanned": ir.get("scope", {}).get("eligible_files_scanned", 0),
216
+ },
217
+ "workload_coverage": {
218
+ "selected_parts": semantic.get("workload_coverage", {}).get("selected_parts", 1),
219
+ "analyzed_parts": semantic.get("workload_coverage", {}).get("analyzed_parts", 1),
220
+ "cross_part_flow_proven": semantic.get("workload_coverage", {}).get("cross_part_flow_proven", True),
221
+ },
222
+ }
223
+ plan = {
224
+ "schema": "agenticdome.copilot-plan.v1", "tenant_id": session["tenant_id"],
225
+ "semantic_analysis": semantic, "catalog_binding": binding,
226
+ "aggregation": {"kind": "sdk_local_bounded_parts", "signature_scope": "individual_sidecar_parts_only"},
227
+ }
228
+ _request(portal + "/api/agentguard/connect/sessions/" + session["session_uuid"] + "/metadata", {"ir": portal_ir, "signed_plan": plan}, session["access_token"])
229
+
206
230
  from .onboarding_cli import create_scaffold
207
231
  patch_path = create_scaffold(root)
208
232
  finally:
@@ -211,6 +235,11 @@ def run_connect(root: Path, args: Any) -> Dict[str, Any]:
211
235
  os.environ.pop(name, None)
212
236
  else:
213
237
  os.environ[name] = value
238
+
239
+ resumable = {"schema": "agenticdome.connect-state.v1", "portal": portal, "tenant_id": session["tenant_id"], "session_uuid": session["session_uuid"], "workload_uuid": workload_uuid, "token_expires_at": session["expires_at"], "access_token": session["access_token"]}
240
+ state_path = state_dir / "connect-session.json"
241
+ state_path.write_text(json.dumps(resumable, indent=2, sort_keys=True) + "\n", encoding="utf-8")
242
+ os.chmod(state_path, 0o600)
214
243
  pull_request = None
215
244
  if args.open_pr:
216
245
  if not args.repository_connection:
@@ -222,6 +251,15 @@ def run_connect(root: Path, args: Any) -> Dict[str, Any]:
222
251
  "next_action": "Ask AgenticDome to confirm policy-aware sidecar readiness before live verification.",
223
252
  })
224
253
  broker_setup["sdk_mode"] = effective_config.get("execution_broker_mode", "existing_sdk_or_environment_default")
254
+ cross_part_review_required = semantic.get("workload_coverage", {}).get("cross_part_flow_proven") is False
255
+ next_action = (
256
+ "Review the generated pull request and run agenticdome verify. Then review cross-part call paths and workload-specific tests in the portal before activation."
257
+ if args.open_pr else
258
+ "Review the generated patch, apply it on a branch, and run agenticdome verify. Then review cross-part call paths and workload-specific tests in the portal before activation."
259
+ ) if cross_part_review_required else (
260
+ "Review the generated pull request and run agenticdome verify."
261
+ if args.open_pr else "Review the generated patch, apply it on a branch, then run agenticdome verify."
262
+ )
225
263
  return {
226
264
  "status": "changeset_ready", "source_upload": False, "tenant_id": session["tenant_id"],
227
265
  "session_uuid": session["session_uuid"], "workload_uuid": workload_uuid,
@@ -229,9 +267,10 @@ def run_connect(root: Path, args: Any) -> Dict[str, Any]:
229
267
  "execution_broker": broker_setup,
230
268
  "attachment_points": len(semantic.get("attachment_points") or []),
231
269
  "bypass_risks": len(semantic.get("bypass_risks") or []),
270
+ "cross_part_review_required": cross_part_review_required,
232
271
  "patch": str(patch_path.relative_to(root)),
233
272
  "pull_request": pull_request,
234
- "next_action": "Review the generated pull request and run agenticdome verify." if pull_request else "Review the generated patch, apply it on a branch, then run agenticdome verify.",
273
+ "next_action": next_action,
235
274
  "sdk_version": _installed_sdk_version(),
236
275
  }
237
276
 
@@ -350,11 +350,13 @@ def _collect_typescript(root: Path, paths: Sequence[Path]) -> Tuple[List[Dict[st
350
350
  def _typescript_fallback(root: Path, paths: Sequence[Path], reason: str) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
351
351
  functions: List[Dict[str, Any]] = []
352
352
  parsed = 0
353
+ read_errors = 0
353
354
  call_pattern = re.compile(r"\b([A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*)\s*\(")
354
355
  for path in paths:
355
356
  try:
356
357
  lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
357
358
  except OSError:
359
+ read_errors += 1
358
360
  continue
359
361
  events = []
360
362
  for line_number, line in enumerate(lines, start=1):
@@ -368,7 +370,7 @@ def _typescript_fallback(root: Path, paths: Sequence[Path], reason: str) -> Tupl
368
370
  "engine": "typescript-structural-fallback",
369
371
  "available": False,
370
372
  "files_parsed": parsed,
371
- "parse_errors": 0,
373
+ "parse_errors": read_errors,
372
374
  "reason": reason,
373
375
  "claim": "Generic structural collection only; install TypeScript for compiler AST evidence",
374
376
  }
@@ -382,7 +384,10 @@ def collect_repository_ir(root: Path, paths: Sequence[Path]) -> Dict[str, Any]:
382
384
  python_functions, python_engine = _collect_python(root, python_paths)
383
385
  typescript_functions, typescript_engine = _collect_typescript(root, typescript_paths)
384
386
  functions = python_functions + typescript_functions
385
- complete = len(paths) <= MAX_IR_FILES and len(functions) <= MAX_IR_FUNCTIONS
387
+ collection_errors = int(python_engine.get("parse_errors", 0)) + int(typescript_engine.get("parse_errors", 0))
388
+ files_parsed = int(python_engine.get("files_parsed", 0)) + int(typescript_engine.get("files_parsed", 0))
389
+ complete = (len(paths) <= MAX_IR_FILES and len(functions) <= MAX_IR_FUNCTIONS
390
+ and collection_errors == 0 and files_parsed == len(paths))
386
391
  return {
387
392
  "schema": IR_SCHEMA,
388
393
  "source_upload": False,
@@ -394,7 +399,8 @@ def collect_repository_ir(root: Path, paths: Sequence[Path]) -> Dict[str, Any]:
394
399
  "files_selected": len(selected),
395
400
  "symbols_found": len(functions),
396
401
  "complete": complete,
397
- "limit_reason": "file_limit" if len(paths) > MAX_IR_FILES else "symbol_limit" if len(functions) > MAX_IR_FUNCTIONS else None,
402
+ "limit_reason": "file_limit" if len(paths) > MAX_IR_FILES else "symbol_limit" if len(functions) > MAX_IR_FUNCTIONS
403
+ else "parse_or_read_error" if collection_errors or files_parsed != len(paths) else None,
398
404
  },
399
405
  "privacy": {
400
406
  "source_text": False,
@@ -16,9 +16,11 @@ import re
16
16
  import shutil
17
17
  import subprocess
18
18
  import sys
19
+ import tempfile
19
20
  import time
20
21
  import urllib.error
21
22
  import urllib.request
23
+ import uuid
22
24
  from pathlib import Path
23
25
  from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
24
26
 
@@ -38,14 +40,21 @@ from .copilot_ir import collect_repository_ir
38
40
  SCHEMA = "agenticdome.onboarding-report.v1"
39
41
  CONFIG_SCHEMA = "agenticdome.project-config.v1"
40
42
  COPILOT_ANALYSIS_REVISION = 4
43
+ COPILOT_MAX_WORKLOAD_BYTES = 4_000_000
44
+ COPILOT_MAX_WORKLOAD_PARTS = 24
45
+ COPILOT_MAX_EVENTS_PER_FUNCTION = 2_048
46
+ COPILOT_MAX_EVENTS_PER_PART = 100_000
41
47
  MAX_FILES = 5_000
42
- MAX_TEXT_BYTES = 512_000
48
+ # Parse one source file at a time. Large generated files still fail with an
49
+ # explicit scope gap rather than being silently counted as protected.
50
+ MAX_TEXT_BYTES = 2_000_000
43
51
  IGNORED_DIRECTORIES = {
44
52
  ".git", ".hg", ".svn", ".idea", ".vscode", ".tox", ".venv", "venv",
45
53
  "node_modules", "dist", "build", "coverage", "__pycache__", ".mypy_cache",
46
54
  ".pytest_cache", ".ruff_cache", ".next", ".agenticdome", ".harness_runtime",
47
55
  ".harness_runtime_ts", ".nuxt", ".turbo", ".cache", ".yarn",
48
- ".pnpm-store", "target", "tests", "test", "__tests__", "spec",
56
+ ".pnpm-store", ".nvm", ".npm", ".cargo", ".rustup",
57
+ "target", "tests", "test", "__tests__", "spec",
49
58
  }
50
59
  TEXT_SUFFIXES = {".py", ".pyi", ".js", ".jsx", ".ts", ".tsx", ".json", ".toml", ".txt", ".yaml", ".yml"}
51
60
  SENSITIVE_FILE_PATTERN = re.compile(
@@ -229,19 +238,28 @@ def _post_copilot(api_base: str, api_key: str, tenant_id: str, path: str, body:
229
238
  if idempotency_key:
230
239
  headers["Idempotency-Key"] = idempotency_key
231
240
  request = urllib.request.Request(api_base + path, data=body, method=method, headers=headers)
232
- try:
233
- with urllib.request.urlopen(request, timeout=60) as response: # noqa: S310 - tenant sidecar URL is explicit configuration
234
- result = json.loads(response.read().decode("utf-8"))
235
- except urllib.error.HTTPError as exc:
236
- detail = ""
241
+ for attempt in range(3):
237
242
  try:
238
- payload = json.loads(exc.read().decode("utf-8"))
239
- detail = str(payload.get("detail") or "")[:240] if isinstance(payload, dict) else ""
240
- except (OSError, UnicodeDecodeError, json.JSONDecodeError):
241
- pass
242
- raise SystemExit(f"Integration Copilot sidecar request failed ({exc.code}): {detail or exc.reason}") from exc
243
- except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
244
- raise SystemExit(f"Integration Copilot sidecar request failed safely: {exc}") from exc
243
+ with urllib.request.urlopen(request, timeout=60) as response: # noqa: S310 - tenant sidecar URL is explicit configuration
244
+ result = json.loads(response.read().decode("utf-8"))
245
+ break
246
+ except urllib.error.HTTPError as exc:
247
+ if exc.code == 429 and attempt < 2:
248
+ try:
249
+ retry_after = max(1, min(int(exc.headers.get("Retry-After", "2")), 65))
250
+ except (ValueError, TypeError):
251
+ retry_after = 2
252
+ time.sleep(retry_after)
253
+ continue
254
+ detail = ""
255
+ try:
256
+ payload = json.loads(exc.read().decode("utf-8"))
257
+ detail = str(payload.get("detail") or "")[:240] if isinstance(payload, dict) else ""
258
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError):
259
+ pass
260
+ raise SystemExit(f"Integration Copilot sidecar request failed ({exc.code}): {detail or exc.reason}") from exc
261
+ except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
262
+ raise SystemExit(f"Integration Copilot sidecar request failed safely: {exc}") from exc
245
263
  if not isinstance(result, dict):
246
264
  raise SystemExit("Integration Copilot returned an invalid JSON response.")
247
265
  return result
@@ -332,6 +350,200 @@ def _copilot_catalog_binding_matches_sdk(binding: Any) -> bool:
332
350
  )
333
351
 
334
352
 
353
+ def _copilot_workload_parts(root: Path, ir: Dict[str, Any]) -> List[Tuple[str, Dict[str, Any]]]:
354
+ """Partition structural metadata by the nearest deployable package.
355
+
356
+ This is a transport/analysis boundary, not a claim that calls between
357
+ packages have been proven safe. Every collected function is assigned once.
358
+ No source contents are sent or used to decide the grouping.
359
+ """
360
+ markers = ("pyproject.toml", "package.json", "requirements.txt", "setup.py", "setup.cfg", "Dockerfile")
361
+ grouped: Dict[str, Dict[str, List[Dict[str, Any]]]] = {}
362
+ package_cache: Dict[str, str] = {}
363
+ functions = ir.get("functions", [])
364
+ if not isinstance(functions, list):
365
+ raise SystemExit("The local Copilot collector did not return a function inventory.")
366
+ for function in functions:
367
+ if not isinstance(function, dict):
368
+ raise SystemExit("The local Copilot collector returned an invalid function entry.")
369
+ relative = Path(str(function.get("path") or ""))
370
+ if not relative.parts or relative.is_absolute() or ".." in relative.parts:
371
+ raise SystemExit("The local Copilot collector returned a non-relative source path.")
372
+ events = function.get("events", [])
373
+ if not isinstance(events, list) or len(events) > COPILOT_MAX_EVENTS_PER_FUNCTION:
374
+ raise SystemExit(
375
+ f"{relative} contains a function above the {COPILOT_MAX_EVENTS_PER_FUNCTION}-event "
376
+ "analysis boundary. Split that function or select a narrower deployable workload."
377
+ )
378
+ directory = relative.parent.as_posix()
379
+ key = package_cache.get(directory)
380
+ if key is None:
381
+ package = Path(".")
382
+ for parent in relative.parents:
383
+ if parent == Path("."):
384
+ break
385
+ if any((root / parent / marker).is_file() for marker in markers):
386
+ package = parent
387
+ break
388
+ key = package.as_posix()
389
+ package_cache[directory] = key
390
+ grouped.setdefault(key, {}).setdefault(relative.as_posix(), []).append(function)
391
+
392
+ parts: List[Tuple[str, Dict[str, Any]]] = []
393
+ for package, files in sorted(grouped.items()):
394
+ batch: List[Dict[str, Any]] = []
395
+ byte_count = 0
396
+ event_count = 0
397
+ ordinal = 0
398
+ for filename, file_functions in sorted(files.items()):
399
+ file_bytes = sum(len(json.dumps(item, sort_keys=True, separators=(",", ":")).encode("utf-8")) + 1 for item in file_functions)
400
+ file_events = sum(len(item.get("events", [])) for item in file_functions)
401
+ if file_bytes > COPILOT_MAX_WORKLOAD_BYTES or file_events > COPILOT_MAX_EVENTS_PER_PART:
402
+ raise SystemExit(
403
+ f"{filename} alone exceeds the bounded Copilot analysis unit. "
404
+ "Select a smaller package or split this source file; no coverage has been claimed."
405
+ )
406
+ if batch and (byte_count + file_bytes > COPILOT_MAX_WORKLOAD_BYTES or event_count + file_events > COPILOT_MAX_EVENTS_PER_PART):
407
+ parts.append((f"{package}#{ordinal}", _copilot_part_ir(ir, batch)))
408
+ ordinal += 1
409
+ batch, byte_count, event_count = [], 0, 0
410
+ batch.extend(file_functions)
411
+ byte_count += file_bytes
412
+ event_count += file_events
413
+ if batch:
414
+ parts.append((f"{package}#{ordinal}", _copilot_part_ir(ir, batch)))
415
+ if not parts:
416
+ return [(".#0", ir)]
417
+ if len(parts) > COPILOT_MAX_WORKLOAD_PARTS:
418
+ raise SystemExit(
419
+ f"The selection contains {len(parts)} bounded Copilot analysis units "
420
+ f"(limit {COPILOT_MAX_WORKLOAD_PARTS}). Select a deployable application or package; "
421
+ "the tool will not silently omit the remaining units."
422
+ )
423
+ if sum(len(part["functions"]) for _, part in parts) != len(functions):
424
+ raise SystemExit("Copilot workload coverage did not account for every collected function.")
425
+ return parts
426
+
427
+
428
+ def _copilot_part_ir(ir: Dict[str, Any], functions: List[Dict[str, Any]]) -> Dict[str, Any]:
429
+ part = {key: value for key, value in ir.items() if key not in {"functions", "coverage"}}
430
+ part["functions"] = functions
431
+ paths = {str(function.get("path") or "") for function in functions}
432
+ part["coverage"] = {
433
+ "candidate_source_files": len(paths), "files_selected": len(paths),
434
+ "symbols_found": len(functions), "complete": True, "limit_reason": None,
435
+ }
436
+ return part
437
+
438
+
439
+ def _merge_copilot_parts(
440
+ ir: Dict[str, Any], results: List[Tuple[str, Dict[str, Any]]],
441
+ parts: Optional[List[Tuple[str, Dict[str, Any]]]] = None,
442
+ ) -> Dict[str, Any]:
443
+ result_caps = {"attachment_points": 100, "bypass_risks": 60, "review_findings": 100, "execution_paths": 40}
444
+ at_cap = sorted({field for _, result in results for field, cap in result_caps.items() if len(result.get(field, [])) >= cap})
445
+ if len(results) == 1:
446
+ semantic = dict(results[0][1])
447
+ semantic["ir_sha256"] = _ir_sha256(ir)
448
+ if at_cap:
449
+ semantic["limitations"] = list(semantic.get("limitations", [])) + [
450
+ "One or more result lists reached the per-workload display cap; additional findings may exist."
451
+ ]
452
+ semantic["workload_coverage"] = {
453
+ "complete": True, "selected_parts": 1, "analyzed_parts": 1,
454
+ "cross_part_flow_proven": True,
455
+ "result_lists_at_cap": at_cap,
456
+ "parts": [{"name": results[0][0], "symbols": semantic.get("symbols_indexed", 0)}],
457
+ }
458
+ return semantic
459
+ first = results[0][1]
460
+ merged = dict(first)
461
+ for field in ("attachment_points", "bypass_risks", "review_findings", "execution_paths"):
462
+ merged[field] = [item for _, result in results for item in result.get(field, [])]
463
+ for field in ("symbols_indexed", "call_edges", "protected_sinks", "events_analyzed"):
464
+ merged[field] = sum(int(result.get(field, 0) or 0) for _, result in results)
465
+ coverage: Dict[str, Dict[str, Any]] = {}
466
+ for _, result in results:
467
+ for boundary, values in result.get("coverage", {}).items():
468
+ if not isinstance(values, dict):
469
+ continue
470
+ totals = coverage.setdefault(boundary, {})
471
+ for key, value in values.items():
472
+ if isinstance(value, int) and not isinstance(value, bool):
473
+ totals[key] = totals.get(key, 0) + value
474
+ merged["coverage"] = coverage
475
+ merged["scope"] = ir.get("scope", {})
476
+ merged["ir_sha256"] = _ir_sha256(ir)
477
+ merged["analysis_mode"] = "private_bounded_workload_flow"
478
+ merged["confidence"] = "partial"
479
+ merged["workload_coverage"] = {
480
+ "complete": True, "selected_parts": len(results), "analyzed_parts": len(results),
481
+ "cross_part_flow_proven": False,
482
+ "cross_part_review_required": True,
483
+ "cross_part_edges": _cross_part_edges(ir, parts or []),
484
+ "result_lists_at_cap": at_cap,
485
+ "parts": [{"name": name, "symbols": result.get("symbols_indexed", 0), "ir_sha256": result.get("ir_sha256")} for name, result in results],
486
+ }
487
+ merged["limitations"] = list(dict.fromkeys(
488
+ [item for _, result in results for item in result.get("limitations", [])]
489
+ + ["All selected parts were analyzed separately. Cross-part call flow and guard dominance are not proven; review integration boundaries before claiming production readiness."]
490
+ + (["One or more result lists reached a per-workload display cap; additional findings may exist."] if at_cap else [])
491
+ ))
492
+ return merged
493
+
494
+
495
+ def _cross_part_edges(ir: Dict[str, Any], parts: List[Tuple[str, Dict[str, Any]]]) -> Dict[str, Any]:
496
+ """Inventory resolvable inter-part calls for human review, not proof of safety.
497
+
498
+ Qualified or unambiguous local names are linked using the same conservative
499
+ shape as Core. Dynamic calls and external dependencies are not certified.
500
+ """
501
+ functions = ir.get("functions", [])
502
+ if not isinstance(functions, list):
503
+ return {"observed_count": 0, "display_capped": False, "examples": []}
504
+ owners: Dict[str, str] = {}
505
+ for name, part in parts:
506
+ for function in part.get("functions", []):
507
+ owners[str(function.get("path") or "") + "\0" + str(function.get("symbol") or "")] = name
508
+ by_symbol: Dict[str, List[Dict[str, Any]]] = {}
509
+ by_tail: Dict[str, List[Dict[str, Any]]] = {}
510
+ for function in functions:
511
+ symbol = str(function.get("symbol") or "").lower()
512
+ by_symbol.setdefault(symbol, []).append(function)
513
+ by_tail.setdefault(symbol.rsplit(".", 1)[-1], []).append(function)
514
+ examples: List[Dict[str, Any]] = []
515
+ observed = 0
516
+ for caller in functions:
517
+ caller_symbol = str(caller.get("symbol") or "")
518
+ caller_part = owners.get(str(caller.get("path") or "") + "\0" + caller_symbol)
519
+ for event in caller.get("events", []):
520
+ if not isinstance(event, dict) or event.get("event") != "call":
521
+ continue
522
+ callee = str(event.get("callee") or "").lower()
523
+ targets = by_symbol.get(callee, [])
524
+ if not targets and (callee.startswith("self.") or callee.startswith("cls.")):
525
+ resolved = caller_symbol.lower().rsplit(".", 1)[0] + "." + callee.rsplit(".", 1)[-1]
526
+ targets = by_symbol.get(resolved, [])
527
+ if not targets and "." not in callee:
528
+ tail = by_tail.get(callee, [])
529
+ targets = tail if len(tail) == 1 else []
530
+ if len(targets) != 1:
531
+ continue
532
+ target = targets[0]
533
+ target_part = owners.get(str(target.get("path") or "") + "\0" + str(target.get("symbol") or ""))
534
+ if not caller_part or not target_part or caller_part == target_part:
535
+ continue
536
+ observed += 1
537
+ if len(examples) < 40:
538
+ examples.append({
539
+ "from_part": caller_part, "from_path": str(caller.get("path") or ""),
540
+ "from_symbol": caller_symbol[:240], "line": int(event.get("line") or caller.get("line") or 1),
541
+ "to_part": target_part, "to_path": str(target.get("path") or ""),
542
+ "to_symbol": str(target.get("symbol") or "")[:240],
543
+ })
544
+ return {"observed_count": observed, "display_capped": observed > len(examples), "examples": examples}
545
+
546
+
335
547
  def _copilot_semantic_analysis(root: Path, ir: Dict[str, Any], *, required: bool) -> Dict[str, Any]:
336
548
  ir_digest = _ir_sha256(ir)
337
549
  api_base = os.getenv("AGENTICDOME_API_BASE", "").strip().rstrip("/")
@@ -363,45 +575,87 @@ def _copilot_semantic_analysis(root: Path, ir: Dict[str, Any], *, required: bool
363
575
  ):
364
576
  return semantic
365
577
 
366
- body = json.dumps({
367
- "schema": "agenticdome.copilot-request.v1",
368
- "ir": ir,
369
- "catalog_binding": {
370
- "schema": CATALOG_SCHEMA,
371
- "catalog_digest": expected_catalog_digest,
372
- "verified_at": CATALOG_VERIFIED_AT,
373
- },
374
- }, separators=(",", ":")).encode("utf-8")
375
- idempotency_key = hashlib.sha256(
376
- f"{tenant_id}\n{api_base}\n{ir_digest}\n{expected_catalog_digest}".encode("utf-8")
377
- ).hexdigest()
378
- result = (
379
- _chunked_copilot_request(api_base, api_key, tenant_id, ir, ir_digest, idempotency_key)
380
- if len(body) > 8_000_000 else
381
- _post_copilot(api_base, api_key, tenant_id, "/integration-copilot/v1/analyze", body, idempotency_key=idempotency_key)
382
- )
383
-
384
- if not isinstance(result, dict) or result.get("schema") != "agenticdome.copilot-plan.v1":
385
- raise SystemExit("Integration Copilot returned an unsupported response contract.")
386
- if str(result.get("tenant_id") or "") != tenant_id:
387
- raise SystemExit("Integration Copilot tenant binding did not match the requested tenant.")
388
- semantic = result.get("semantic_analysis")
389
- if not isinstance(semantic, dict) or semantic.get("ir_sha256") != ir_digest:
390
- raise SystemExit("Integration Copilot response is not bound to the submitted structural IR.")
391
- if semantic.get("analysis_revision") != COPILOT_ANALYSIS_REVISION:
392
- raise SystemExit(
393
- "Integration Copilot Core is older than this SDK's required analysis revision. "
394
- "Update the assigned control plane and sidecar, then rerun the Copilot."
395
- )
396
- response_binding = result.get("catalog_binding")
578
+ parts = _copilot_workload_parts(root, ir)
579
+ part_cache_path = _agenticdome_dir(root) / "copilot-parts.json"
580
+ part_cache: Dict[str, Any] = {}
581
+ if part_cache_path.exists():
582
+ try:
583
+ part_cache = _load_json(part_cache_path)
584
+ except SystemExit:
585
+ part_cache = {}
397
586
  if (
398
- not _copilot_catalog_binding_matches_sdk(response_binding)
587
+ part_cache.get("schema") != "agenticdome.copilot-part-cache.v1"
588
+ or part_cache.get("tenant_id") != tenant_id
589
+ or part_cache.get("api_base") != api_base
590
+ or part_cache.get("catalog_digest") != expected_catalog_digest
591
+ or part_cache.get("selected_ir_sha256") != ir_digest
592
+ or not isinstance(part_cache.get("parts"), dict)
399
593
  ):
400
- raise SystemExit(
401
- "Integration Copilot's signed hook catalog does not match this installed SDK. "
402
- "Upgrade the SDK; if it is already current, ask the control-plane administrator "
403
- "to refresh the assigned sidecar's signed catalog, then rerun the Copilot."
404
- )
594
+ part_cache = {
595
+ "schema": "agenticdome.copilot-part-cache.v1", "tenant_id": tenant_id,
596
+ "api_base": api_base, "catalog_digest": expected_catalog_digest,
597
+ "selected_ir_sha256": ir_digest, "parts": {},
598
+ }
599
+ analyzed: List[Tuple[str, Dict[str, Any]]] = []
600
+ response_binding: Dict[str, Any] = {}
601
+ for ordinal, (name, part) in enumerate(parts, start=1):
602
+ part_digest = _ir_sha256(part)
603
+ cached_part = part_cache.get("parts", {}).get(part_digest)
604
+ if isinstance(cached_part, dict):
605
+ cached_semantic = cached_part.get("semantic_analysis")
606
+ cached_binding = cached_part.get("catalog_binding")
607
+ if (
608
+ isinstance(cached_semantic, dict)
609
+ and cached_semantic.get("ir_sha256") == part_digest
610
+ and cached_semantic.get("analysis_revision") == COPILOT_ANALYSIS_REVISION
611
+ and _copilot_catalog_binding_matches_sdk(cached_binding)
612
+ ):
613
+ print(f"Integration Copilot: part {ordinal}/{len(parts)} already analyzed; resuming.", file=sys.stderr)
614
+ analyzed.append((name, cached_semantic))
615
+ response_binding = cached_binding
616
+ continue
617
+ print(f"Integration Copilot: analyzing part {ordinal}/{len(parts)}; completed parts are saved locally.", file=sys.stderr)
618
+ body = json.dumps({
619
+ "schema": "agenticdome.copilot-request.v1", "ir": part,
620
+ "catalog_binding": {
621
+ "schema": CATALOG_SCHEMA, "catalog_digest": expected_catalog_digest,
622
+ "verified_at": CATALOG_VERIFIED_AT,
623
+ },
624
+ }, separators=(",", ":")).encode("utf-8")
625
+ if len(body) > 4_500_000:
626
+ raise SystemExit(f"Bounded Copilot unit {name} exceeds its transport budget; no partial plan was saved.")
627
+ idempotency_key = hashlib.sha256(
628
+ f"{tenant_id}\n{api_base}\n{part_digest}\n{expected_catalog_digest}".encode("utf-8")
629
+ ).hexdigest()
630
+ try:
631
+ result = _post_copilot(api_base, api_key, tenant_id, "/integration-copilot/v1/analyze", body, idempotency_key=idempotency_key)
632
+ except SystemExit as exc:
633
+ raise SystemExit(
634
+ f"Integration Copilot stopped at part {ordinal}/{len(parts)}. "
635
+ f"The first {len(analyzed)} completed part(s) are saved for retry. {exc}"
636
+ ) from exc
637
+ if not isinstance(result, dict) or result.get("schema") != "agenticdome.copilot-plan.v1":
638
+ raise SystemExit("Integration Copilot returned an unsupported response contract.")
639
+ if str(result.get("tenant_id") or "") != tenant_id:
640
+ raise SystemExit("Integration Copilot tenant binding did not match the requested tenant.")
641
+ part_semantic = result.get("semantic_analysis")
642
+ if not isinstance(part_semantic, dict) or part_semantic.get("ir_sha256") != part_digest:
643
+ raise SystemExit("Integration Copilot response is not bound to the submitted workload metadata.")
644
+ if part_semantic.get("analysis_revision") != COPILOT_ANALYSIS_REVISION:
645
+ raise SystemExit("Integration Copilot Core is older than this SDK. Update the assigned control plane and sidecar.")
646
+ response_binding = result.get("catalog_binding")
647
+ if not _copilot_catalog_binding_matches_sdk(response_binding):
648
+ raise SystemExit("Integration Copilot's signed hook catalog does not match this installed SDK.")
649
+ analyzed.append((name, part_semantic))
650
+ part_cache["parts"][part_digest] = {
651
+ "semantic_analysis": part_semantic, "catalog_binding": response_binding,
652
+ }
653
+ _write_json(part_cache_path, part_cache)
654
+ # The sidecar allows ten expensive analyses per tenant-minute. Pace
655
+ # unusually broad selections rather than overwhelming its Core queue.
656
+ if len(parts) > 10 and len(analyzed) < len(parts):
657
+ time.sleep(6.1)
658
+ semantic = _merge_copilot_parts(ir, analyzed, parts)
405
659
  _write_json(cached_path, {
406
660
  "schema": "agenticdome.copilot-cache.v1",
407
661
  "source_upload": False,
@@ -614,7 +868,7 @@ def _detect_mcp_protection(root: Path, paths: Sequence[Path], languages: Sequenc
614
868
  }
615
869
 
616
870
 
617
- def inspect_repository(root: Path) -> Dict[str, Any]:
871
+ def inspect_repository(root: Path, *, remote_analysis: bool = True) -> Dict[str, Any]:
618
872
  root = root.resolve()
619
873
  languages = set()
620
874
  framework_hits: Dict[str, set[str]] = {key: set() for key in FRAMEWORK_MARKERS}
@@ -699,7 +953,7 @@ def inspect_repository(root: Path) -> Dict[str, Any]:
699
953
  copilot_ir["scope"] = scope
700
954
  semantic = (
701
955
  _copilot_semantic_analysis(root, copilot_ir, required=False)
702
- if scope["complete"] else _pending_semantic_analysis(copilot_ir)
956
+ if scope["complete"] and remote_analysis else _pending_semantic_analysis(copilot_ir)
703
957
  )
704
958
  if not scope["complete"]:
705
959
  semantic["limitations"].append("The selected workload was not completely collected; narrow the scope before requesting a placement plan.")
@@ -714,7 +968,7 @@ def inspect_repository(root: Path) -> Dict[str, Any]:
714
968
  "schema": SCHEMA,
715
969
  "generated_by": "agenticdome local CLI",
716
970
  "source_upload": False,
717
- "project": {"name": root.name, "root_disclosed": False},
971
+ "project": {"name": root.name, "workload_id": _read_workload_id(root), "root_disclosed": False},
718
972
  "languages": sorted(languages),
719
973
  "frameworks": detected,
720
974
  "scanned_files": scanned_files,
@@ -751,13 +1005,46 @@ def _exportable_inspection(report: Dict[str, Any]) -> Dict[str, Any]:
751
1005
  return exported
752
1006
 
753
1007
 
1008
+ def _scope_gap_message(report: Dict[str, Any]) -> str:
1009
+ gaps = report.get("scope", {}).get("unexamined_source_counts", {})
1010
+ if int(gaps.get("oversized_source_files", 0)):
1011
+ return (
1012
+ f"{gaps['oversized_source_files']} source file(s) exceed the {MAX_TEXT_BYTES // 1_000_000} MB per-file analysis limit. "
1013
+ "Split or isolate the required agent code, then rerun. Do not omit an execution path just to pass onboarding."
1014
+ )
1015
+ if int(gaps.get("unreadable_directories", 0)) or int(gaps.get("unreadable_files", 0)):
1016
+ return "Some source paths were unreadable. Fix local permissions and rerun; no source was sent."
1017
+ if report.get("copilot_ir", {}).get("coverage", {}).get("limit_reason") == "parse_or_read_error":
1018
+ return (
1019
+ "One or more source files could not be parsed for structural analysis. "
1020
+ "Fix the local syntax or language tooling and rerun; the uncovered paths are not certified."
1021
+ )
1022
+ if report.get("scan_limit_reached") or report.get("copilot_ir", {}).get("coverage", {}).get("limit_reason"):
1023
+ return (
1024
+ "The selected root exceeded a file or symbol limit. Choose the independently deployable application or package "
1025
+ "that owns the agent path, then rerun. If that complete workload still exceeds the limit, contact support; "
1026
+ "unexamined code must not be reported as protected."
1027
+ )
1028
+ return "The selected workload was not completely inspected. Resolve the local coverage gap before planning or importing."
1029
+
1030
+
754
1031
  def _agenticdome_dir(root: Path) -> Path:
755
1032
  return root / ".agenticdome"
756
1033
 
757
1034
 
758
1035
  def _write_json(path: Path, value: Dict[str, Any]) -> None:
759
1036
  path.parent.mkdir(parents=True, exist_ok=True)
760
- path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
1037
+ descriptor, staged = tempfile.mkstemp(prefix=".agenticdome-json-", dir=path.parent)
1038
+ try:
1039
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
1040
+ json.dump(value, handle, indent=2, sort_keys=True)
1041
+ handle.write("\n")
1042
+ handle.flush()
1043
+ os.fsync(handle.fileno())
1044
+ os.replace(staged, path)
1045
+ finally:
1046
+ if os.path.exists(staged):
1047
+ os.unlink(staged)
761
1048
 
762
1049
 
763
1050
  def _load_json(path: Path) -> Dict[str, Any]:
@@ -770,12 +1057,43 @@ def _load_json(path: Path) -> Dict[str, Any]:
770
1057
  return value
771
1058
 
772
1059
 
1060
+ def _read_workload_id(root: Path) -> Optional[str]:
1061
+ config_path = _agenticdome_dir(root) / "config.json"
1062
+ identity_path = _agenticdome_dir(root) / "workload.json"
1063
+ for path in (config_path, identity_path):
1064
+ if not path.exists():
1065
+ continue
1066
+ value = _load_json(path).get("workload_id")
1067
+ if value is None:
1068
+ continue
1069
+ try:
1070
+ canonical = str(uuid.UUID(str(value)))
1071
+ except (ValueError, AttributeError) as exc:
1072
+ raise SystemExit(f"{path} contains an invalid workload ID; restore its original value before inspecting.") from exc
1073
+ if str(value) != canonical:
1074
+ raise SystemExit(f"{path} contains a non-canonical workload ID; use the UUID shown when the workload was initialized.")
1075
+ return canonical
1076
+ return None
1077
+
1078
+
1079
+ def _ensure_workload_id(root: Path) -> str:
1080
+ existing = _read_workload_id(root)
1081
+ if existing:
1082
+ return existing
1083
+ value = str(uuid.uuid4())
1084
+ _write_json(_agenticdome_dir(root) / "workload.json", {"workload_id": value, "source_upload": False})
1085
+ return value
1086
+
1087
+
773
1088
  def init_project(root: Path, args: argparse.Namespace) -> Dict[str, Any]:
774
1089
  target = _agenticdome_dir(root) / "config.json"
775
1090
  # Reassessment must not silently migrate an established integration.
776
1091
  if target.exists():
1092
+ _ensure_workload_id(root)
777
1093
  return _load_json(target)
778
- report = inspect_repository(root)
1094
+ # Creating local configuration must not depend on sidecar availability.
1095
+ # The explicit inspect/plan steps perform authenticated remote analysis.
1096
+ report = inspect_repository(root, remote_analysis=False)
779
1097
  detected = [item["key"] for item in report["frameworks"]]
780
1098
  frameworks = list(dict.fromkeys(args.framework or detected or ["custom-python"]))
781
1099
  unknown = sorted(set(frameworks) - set(FRAMEWORK_MARKERS))
@@ -783,6 +1101,7 @@ def init_project(root: Path, args: argparse.Namespace) -> Dict[str, Any]:
783
1101
  raise SystemExit("Unsupported framework key(s): " + ", ".join(unknown))
784
1102
  config = {
785
1103
  "schema": CONFIG_SCHEMA,
1104
+ "workload_id": str(uuid.uuid4()),
786
1105
  "frameworks": frameworks,
787
1106
  "business_purpose": args.business_purpose or "Protect agent prompts, tools, delegation and output",
788
1107
  "sensitive_tools": list(dict.fromkeys(args.sensitive_tool or [])),
@@ -797,6 +1116,7 @@ def init_project(root: Path, args: argparse.Namespace) -> Dict[str, Any]:
797
1116
  "execution_broker_mode": "policy",
798
1117
  }
799
1118
  _write_json(target, config)
1119
+ report["project"]["workload_id"] = config["workload_id"]
800
1120
  _write_json(_agenticdome_dir(root) / "inspection.json", _exportable_inspection(report))
801
1121
  return config
802
1122
 
@@ -991,10 +1311,7 @@ def _hook_plans(
991
1311
  def integration_plan(root: Path) -> Dict[str, Any]:
992
1312
  report = inspect_repository(root)
993
1313
  if report.get("scope", {}).get("complete") is not True:
994
- raise SystemExit(
995
- "The selected workload exceeds the complete local analysis boundary. "
996
- "Choose a narrower application or package root; unexamined code must not be reported as protected."
997
- )
1314
+ raise SystemExit(_scope_gap_message(report))
998
1315
  config_path = _agenticdome_dir(root) / "config.json"
999
1316
  config = _load_json(config_path) if config_path.exists() else {
1000
1317
  "schema": CONFIG_SCHEMA,
@@ -1070,7 +1387,12 @@ def integration_plan(root: Path) -> Dict[str, Any]:
1070
1387
  "unresolved_bypasses": len(semantic_bypasses),
1071
1388
  "high_severity_bypasses": sum(1 for item in semantic_bypasses if item.get("severity") == "high"),
1072
1389
  "review_required": len(semantic_reviews),
1073
- "production_ready": not semantic_bypasses and not semantic_reviews and semantic.get("confidence") in {"high", "partial"},
1390
+ "production_ready": not semantic_bypasses and not semantic_reviews and semantic.get("confidence") in {"high", "partial"}
1391
+ and semantic.get("workload_coverage", {}).get("cross_part_flow_proven", True)
1392
+ and not any(item in {"attachment_points", "bypass_risks", "review_findings"}
1393
+ for item in semantic.get("workload_coverage", {}).get("result_lists_at_cap", [])),
1394
+ "cross_workload_review_required": semantic.get("workload_coverage", {}).get("cross_part_flow_proven") is False,
1395
+ "cross_part_edges_observed": int(semantic.get("workload_coverage", {}).get("cross_part_edges", {}).get("observed_count", 0)),
1074
1396
  },
1075
1397
  "coverage": {"counts": counts, "required": required, "gaps": gaps},
1076
1398
  "recommended_order": [
@@ -1594,13 +1916,14 @@ def protect_mcp(root: Path) -> Dict[str, Any]:
1594
1916
  }
1595
1917
 
1596
1918
 
1597
- def verify_mcp_project(root: Path, *, live: bool = True, run_tests: bool = True) -> Tuple[int, Dict[str, Any]]:
1919
+ def verify_mcp_project(root: Path, *, live: bool = True, run_tests: bool = True,
1920
+ plan: Optional[Dict[str, Any]] = None) -> Tuple[int, Dict[str, Any]]:
1598
1921
  from .mcp_verification import run_mcp_transport_verification
1599
1922
 
1923
+ plan = plan or integration_plan(root)
1600
1924
  transport = run_mcp_transport_verification()
1601
- exit_code, result = verify_project(root, live=live, run_tests=run_tests)
1602
- report = inspect_repository(root)
1603
- topology = report.get("mcp_protection") if isinstance(report.get("mcp_protection"), dict) else {}
1925
+ exit_code, result = verify_project(root, live=live, run_tests=run_tests, plan=plan)
1926
+ topology = plan.get("mcp_protection") if isinstance(plan.get("mcp_protection"), dict) else {}
1604
1927
  known_transport = topology.get("detected") is True and "unknown_dynamic" not in topology.get("transports", [])
1605
1928
  # Raw forwarding locations are candidates until the signed semantic plan
1606
1929
  # proves whether the protected wrapper dominates them. The production gate
@@ -1772,9 +2095,15 @@ def protect_openclaw(root: Path) -> Dict[str, Any]:
1772
2095
 
1773
2096
 
1774
2097
  def verify_openclaw_project(root: Path, *, live: bool = True, run_tests: bool = True) -> Tuple[int, Dict[str, Any]]:
1775
- """Verify the real OpenClaw hook contract plus tenant policy decisions."""
2098
+ """Verify OpenClaw hooks and, when present, MCP transport in one report."""
1776
2099
  protection = _openclaw_runtime_inspection(root)
1777
- exit_code, result = verify_project(root, live=live, run_tests=run_tests)
2100
+ plan = integration_plan(root)
2101
+ mcp_topology = plan.get("mcp_protection")
2102
+ mcp_detected = isinstance(mcp_topology, dict) and mcp_topology.get("detected") is True
2103
+ if mcp_detected:
2104
+ exit_code, result = verify_mcp_project(root, live=live, run_tests=run_tests, plan=plan)
2105
+ else:
2106
+ exit_code, result = verify_project(root, live=live, run_tests=run_tests, plan=plan)
1778
2107
  decisions_ready = live and all(item.get("passed") for item in result.get("decision_cases", []))
1779
2108
  openclaw_ready = bool(protection.get("ready")) and decisions_ready
1780
2109
  result["openclaw_verification"] = {
@@ -1850,10 +2179,11 @@ def _run_existing_tests(root: Path) -> Dict[str, Any]:
1850
2179
  }
1851
2180
 
1852
2181
 
1853
- def verify_project(root: Path, live: bool = False, run_tests: bool = False) -> Tuple[int, Dict[str, Any]]:
2182
+ def verify_project(root: Path, live: bool = False, run_tests: bool = False,
2183
+ *, plan: Optional[Dict[str, Any]] = None) -> Tuple[int, Dict[str, Any]]:
1854
2184
  from .client import AgenticDomeClient
1855
2185
 
1856
- plan = integration_plan(root)
2186
+ plan = plan or integration_plan(root)
1857
2187
  config_path = _agenticdome_dir(root) / "config.json"
1858
2188
  broker_options = _onboarding_broker_options(_load_json(config_path)) if config_path.exists() else {}
1859
2189
  required_env = ["AGENTICDOME_API_BASE", "AGENTICDOME_API_KEY", "AGENTICDOME_TENANT_ID"]
@@ -1901,6 +2231,7 @@ def verify_project(root: Path, live: bool = False, run_tests: bool = False) -> T
1901
2231
  "clarification": "Use --run-tests for the production onboarding gate.",
1902
2232
  }
1903
2233
  semantic_gate = plan.get("semantic_gate", {})
2234
+ cross_part_review_required = bool(semantic_gate.get("cross_workload_review_required"))
1904
2235
  semantic_ready = (
1905
2236
  semantic_gate.get("confidence") in {"high", "partial"}
1906
2237
  and int(semantic_gate.get("unresolved_bypasses", 0)) == 0
@@ -1923,13 +2254,16 @@ def verify_project(root: Path, live: bool = False, run_tests: bool = False) -> T
1923
2254
  "high_severity_bypasses": int(semantic_gate.get("high_severity_bypasses", 0)),
1924
2255
  "review_required": int(semantic_gate.get("review_required", 0)),
1925
2256
  "passed": semantic_ready,
2257
+ "cross_part_review_required": cross_part_review_required,
2258
+ "cross_part_edges_observed": int(semantic_gate.get("cross_part_edges_observed", 0)),
2259
+ "production_ready": semantic_ready and not cross_part_review_required,
1926
2260
  },
1927
2261
  "application_tests": application_tests,
1928
2262
  "ready": all(item["passed"] for item in outcomes)
1929
2263
  and not plan["coverage"]["gaps"]
1930
2264
  and (not run_tests or semantic_ready)
1931
2265
  and (not run_tests or application_tests["passed"] is True),
1932
- "clarification": "Decision cases use fixed payloads. Production verification also requires no statically observable semantic bypass and passing workload tests; dynamic paths still require runtime coverage.",
2266
+ "clarification": "Decision cases use fixed payloads. ready=true means local checks passed, not production approval. Split analyses also require a tenant-admin review of cross-part paths, workload-specific integration tests and live runtime evidence; dynamic paths remain outside static proof.",
1933
2267
  }
1934
2268
  canonical = json.dumps(result, sort_keys=True, separators=(",", ":")).encode("utf-8")
1935
2269
  result["report_sha256"] = hashlib.sha256(canonical).hexdigest()