agenticdome-python-sdk 1.2.34__py3-none-any.whl → 1.2.35__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.34
3
+ Version: 1.2.35
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
@@ -177,23 +177,41 @@ agenticdome inspect --output agenticdome-inspection.json
177
177
 
178
178
  `init` writes the secret-free inspection and local configuration. For the
179
179
  certified onboarding path, obtain the assigned sidecar and a dedicated
180
- `integration_copilot` key, run the tenant-bound `agenticdome plan`, attach
181
- and test the selected boundaries, then run
180
+ `integration_copilot` key, run `agenticdome integrate preview`, review its
181
+ local exact diff and change summary, explicitly approve a supported edit or
182
+ attach other boundaries manually, then run
182
183
  `agenticdome verify --run-tests --output .agenticdome/verification.json`.
183
- The private plan is required because production verification consumes its
184
- signed, catalog-bound semantic analysis. `agenticdome scaffold` remains
185
- optional and writes an unapplied patch for review.
184
+ Preview obtains the private, signed, catalog-bound semantic plan and keeps
185
+ application source on your machine. `agenticdome integrate apply` creates a
186
+ local Git review branch but never commits, pushes or deploys. Existing-source
187
+ automatic edits are currently limited to one exact certified smolagents run
188
+ pattern; all other paths need manual integration. The older `agenticdome plan`
189
+ and `agenticdome scaffold` remain available as advanced review-only commands.
186
190
 
187
191
  For an MCP client, host, gateway or server workload, use the focused path:
188
192
 
189
193
  ```bash
190
194
  agenticdome mcp protect
191
- # review the detected role/transport, boundaries, registry and unapplied patch
195
+ # Review the detected role/transport and boundaries, then preview exact local files:
196
+ agenticdome integrate preview --target mcp
197
+ # Review .agenticdome/scaffold/guided-integration.patch and
198
+ # .agenticdome/scaffold/proposed/AGENTICDOME-CHANGES.md
199
+ agenticdome integrate status
200
+ # Optional, after approval: add review files on a local Git branch.
201
+ agenticdome integrate apply
202
+ # Manually attach the wrapper to the real MCP forwarder and reroute clients.
203
+ agenticdome inspect --output agenticdome-inspection.json
192
204
  agenticdome mcp verify
193
205
  ```
194
206
 
195
- `mcp protect` adds no customer source changes. `mcp verify` runs the workload's
196
- tests, a real JSON-RPC stdio subprocess interception rehearsal and live
207
+ `mcp protect` and `integrate preview --target mcp` do not edit customer source.
208
+ An approved `integrate apply` adds only missing MCP review/wrapper files to a
209
+ local Git branch; it does not rewrite an existing forwarder, route clients,
210
+ commit or deploy. If there are no new MCP review files, apply refuses. Undo
211
+ removes only unchanged files added by the approval. Actual interception still
212
+ requires manually connecting the wrapper to the real request/response path,
213
+ trusted identity and purpose, runtime credentials, client routing and tests.
214
+ `mcp verify` runs the workload's tests, a real JSON-RPC stdio subprocess interception rehearsal and live
197
215
  assigned-sidecar decisions. The customer Control Panel combines that evidence
198
216
  with retained runtime evidence before representing production readiness.
199
217
 
@@ -216,6 +234,11 @@ agenticdome openclaw verify --output .agenticdome/verification.json
216
234
 
217
235
  `openclaw protect` fails unless the active OpenClaw runtime reports the loaded
218
236
  AgenticDome plugin, explicit consent and the exact certified typed hooks.
237
+ The official `openclaw plugins install`, `enable`, `config set` and gateway
238
+ restart commands are the actual runtime configuration change; the Python CLI
239
+ does not apply or undo them. No application source patch is needed for native
240
+ hooks. Custom skill paths outside those hooks need a reviewed manual
241
+ `protectedExecute()` integration and workload-specific tests.
219
242
  `agenticdome plan` returns source-free, tenant-bound Integration Copilot
220
243
  guidance for review. `openclaw verify` adds workspace-test and assigned-tenant
221
244
  policy evidence. If the same deployable workload also uses MCP, this command
@@ -1,5 +1,5 @@
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
1
+ agenticdome_python_sdk-1.2.35.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
2
+ agenticdome_python_sdk-1.2.35.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,7 +7,7 @@ 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=aOJMqz8FpPF-q1ynpM2xsEk-O-tMyUCPE5Y51dygtLQ,951
10
+ agenticdome_sdk/catalog_snapshot.json,sha256=fPbiJC7IjrPyp8ORunaNQenLCg11W43XIDHcMlINFEg,951
11
11
  agenticdome_sdk/claude.py,sha256=XeY9AUu61SYcJjQVz_AtL_WaN6X4KqP5nprEbbr4Q6A,17456
12
12
  agenticdome_sdk/client.py,sha256=2uVKZR1uXDBIvwOS-lpNPCAXl9K5vroPqsPNNYhBWeg,69473
13
13
  agenticdome_sdk/connect.py,sha256=MY30wXYA6OaZhs__E67KcFXEy7mZMBxMSV06BnlBjRg,18766
@@ -16,6 +16,7 @@ agenticdome_sdk/copilot_ir_collector.cjs,sha256=oxLY8ByONo7Es4YjYiJuRz_A7IKL65Sy
16
16
  agenticdome_sdk/crewai.py,sha256=sSJQ49O7-8kqOAD2_jrsMtMOmBOcJmFkeOv7S3-ayMs,52490
17
17
  agenticdome_sdk/demo.py,sha256=dyfqt2N_cJoyUWKnjPYuQxuw2hb2DrCNUf1jQKeYYoc,8350
18
18
  agenticdome_sdk/google_adk.py,sha256=2TA-tUhY0lOnkGcs1BN9CUEz67ebVX4HJ7mnNoRPcZs,57258
19
+ agenticdome_sdk/guided_integration.py,sha256=JQ5THutpsC1J0yV7gEOJWiLHGLQOKhAlPDFTi5SH5OA,36595
19
20
  agenticdome_sdk/hook_catalog.py,sha256=0dfFWNDS0KCeP0IozWZb1NkNBWf1omaO2-yZKNVdTf0,29272
20
21
  agenticdome_sdk/identity.py,sha256=mrymGgZSVsKJ9THRde54qGmMT7j_uZHmPJ-HtWk7PXE,4635
21
22
  agenticdome_sdk/langgraph.py,sha256=FUvF6DSA5aZP3eoBoNeldgGtPrz-dO5xcVmrPPSBEhI,84312
@@ -27,15 +28,15 @@ agenticdome_sdk/mcp_rehearsal_server.py,sha256=cox4XqUTL8d0gaDzqxDZcEALkaz8otjOj
27
28
  agenticdome_sdk/mcp_verification.py,sha256=VqQS5SpPVFI3G7qCwrT90JmG8vFE6pRJEB7PViHjKqw,14235
28
29
  agenticdome_sdk/microsoft_agent_framework.py,sha256=GfIIuk0n-VhsZaTyoB7P6ilpz9bFLD95TWuX0rFSWeU,67219
29
30
  agenticdome_sdk/microsoft_ai_foundry.py,sha256=YlO968s1dbAYcaEqhqufL92NFR66GuTtngnCv7-1mYc,56911
30
- agenticdome_sdk/onboarding_cli.py,sha256=yPPlNRPCjuDNpeJpL-0fJK3RZTOZvII8d-qFHJgTILM,120597
31
+ agenticdome_sdk/onboarding_cli.py,sha256=1g-wNEzEpAmR1s-oYS6mqCd0TgT3PQuEqNQk9M0hAQc,124440
31
32
  agenticdome_sdk/openai_agents.py,sha256=nNM-YcsUDcE23sGv1buLudcYFz0NLju2Mwiw_wrVaJg,57326
32
33
  agenticdome_sdk/outcomes.py,sha256=jadWDzQaZXrHNvpPme8lQY0jihYLCoM3s4CihAq0JRo,2507
33
34
  agenticdome_sdk/proof.py,sha256=ECKc14xSE0V1HHmcpZjfVz8-Q2H_fooah3BfdblFV8M,2878
34
35
  agenticdome_sdk/pydantic.py,sha256=WKb8X0Fgydfid-ep8E-M8Yslz2pWcCP6w7n_aBxt1rI,54961
35
36
  agenticdome_sdk/simulation.py,sha256=UCbZy3u9AzfxQCfY38SCV3CxGxhinV9EA4aHMPlK4mc,8426
36
37
  agenticdome_sdk/smolagents.py,sha256=wjMA1HVAQ-9wRNM38Jr9wlCpKMhF-9vxtTk6R-kwQhs,19094
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,,
38
+ agenticdome_python_sdk-1.2.35.dist-info/METADATA,sha256=Fn0YW1txEeYaaqT9hykQc-38Qe5IkqwLlcoHreKeS2M,141741
39
+ agenticdome_python_sdk-1.2.35.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
40
+ agenticdome_python_sdk-1.2.35.dist-info/entry_points.txt,sha256=cRfCphkr5gLrkGIzXJEtL50HdHLajoZLbZtcMLE8NoY,113
41
+ agenticdome_python_sdk-1.2.35.dist-info/top_level.txt,sha256=LZbbjXco5mqDrGwlTJpIKI5Sp6R_njzHHjExxAs9c3s,16
42
+ agenticdome_python_sdk-1.2.35.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.34",
5
+ "python_sdk_version": "1.2.35",
6
6
  "published_packages": {
7
7
  "agenticdome-python-sdk": {
8
8
  "registry": "pypi",
9
- "version": "1.2.34",
10
- "url": "https://pypi.org/project/agenticdome-python-sdk/1.2.34/"
9
+ "version": "1.2.35",
10
+ "url": "https://pypi.org/project/agenticdome-python-sdk/1.2.35/"
11
11
  },
12
12
  "agenticdome-sdk": {
13
13
  "registry": "npm",
@@ -0,0 +1,658 @@
1
+ """Local, review-first integration edits. Source and diffs never leave the workload."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import difflib
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+ import uuid
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path, PurePosixPath
16
+ from typing import Any, Dict, List, Optional, Tuple
17
+
18
+
19
+ SCHEMA = "agenticdome.guided-integration.v1"
20
+ MAX_SOURCE_BYTES = 300_000
21
+ MAX_CANDIDATE_FILES = 200
22
+ GENERATED_FILES = {
23
+ ".env.agenticdome.example",
24
+ "FRAMEWORK-HOOKS.md",
25
+ "SEMANTIC-REVIEW.md",
26
+ }
27
+ MCP_GENERATED_FILES = {
28
+ "MCP-SERVER-REGISTRY.json",
29
+ "MCP-PROTECTION.json",
30
+ "MCP-REVIEW.md",
31
+ "agenticdome_mcp_gateway.py",
32
+ "agenticdome_mcp_gateway.ts",
33
+ }
34
+
35
+
36
+ def _sha(value: bytes) -> str:
37
+ return hashlib.sha256(value).hexdigest()
38
+
39
+
40
+ def _file_mode(path: Path) -> Optional[int]:
41
+ return None if os.name == "nt" else path.stat().st_mode & 0o777
42
+
43
+
44
+ def _expected_new_mode() -> Optional[int]:
45
+ return None if os.name == "nt" else 0o644
46
+
47
+
48
+ def _now() -> str:
49
+ return datetime.now(timezone.utc).isoformat()
50
+
51
+
52
+ def _paths(root: Path) -> Tuple[Path, Path, Path]:
53
+ return (
54
+ _target(root, ".agenticdome/scaffold/guided-integration.json"),
55
+ _target(root, ".agenticdome/scaffold/guided-integration.patch"),
56
+ _target(root, ".agenticdome/scaffold/proposed"),
57
+ )
58
+
59
+
60
+ def _history_paths(root: Path, revision: str) -> Tuple[Path, Path]:
61
+ if not isinstance(revision, str) or len(revision) != 12 or any(char not in "0123456789abcdef" for char in revision):
62
+ raise ValueError("The guided integration revision is not valid.")
63
+ return (
64
+ _target(root, ".agenticdome/scaffold/history/" + revision + "/guided-integration.json"),
65
+ _target(root, ".agenticdome/scaffold/history/" + revision + "/guided-integration.patch"),
66
+ )
67
+
68
+
69
+ def _target(root: Path, relative: str) -> Path:
70
+ path = PurePosixPath(relative)
71
+ if not relative or not path.parts or "\\" in relative or path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts):
72
+ raise ValueError("The proposed file path is not a safe workload-relative path.")
73
+ target = root.joinpath(*path.parts)
74
+ if not target.resolve().is_relative_to(root.resolve()):
75
+ raise ValueError("The proposed file resolves outside the selected workload.")
76
+ for parent in (target, *target.parents):
77
+ if parent == root:
78
+ break
79
+ if parent.is_symlink():
80
+ raise ValueError("A proposed file or parent directory is a symlink.")
81
+ return target
82
+
83
+
84
+ def _atomic_write(path: Path, value: bytes, mode: Optional[int] = None) -> None:
85
+ path.parent.mkdir(parents=True, exist_ok=True)
86
+ temporary: Optional[str] = None
87
+ try:
88
+ with tempfile.NamedTemporaryFile(dir=path.parent, prefix=".agenticdome-", delete=False) as stream:
89
+ temporary = stream.name
90
+ stream.write(value)
91
+ stream.flush()
92
+ os.fsync(stream.fileno())
93
+ if mode is not None:
94
+ os.chmod(temporary, mode)
95
+ os.replace(temporary, path)
96
+ finally:
97
+ if temporary and os.path.exists(temporary):
98
+ os.unlink(temporary)
99
+
100
+
101
+ def _write_json(path: Path, value: Dict[str, Any]) -> None:
102
+ _atomic_write(path, (json.dumps(value, indent=2, sort_keys=True) + "\n").encode("utf-8"), 0o600)
103
+
104
+
105
+ def _git(root: Path, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
106
+ result = subprocess.run(["git", *args], cwd=root, text=True, capture_output=True, timeout=30, check=False)
107
+ if check and result.returncode != 0:
108
+ raise RuntimeError((result.stderr or result.stdout or "Git could not inspect this workload.").strip()[:500])
109
+ return result
110
+
111
+
112
+ def _git_root(root: Path) -> Optional[Path]:
113
+ result = _git(root, "rev-parse", "--show-toplevel", check=False)
114
+ if result.returncode != 0:
115
+ return None
116
+ git_root = Path(result.stdout.strip()).resolve()
117
+ return git_root if root.resolve().is_relative_to(git_root) else None
118
+
119
+
120
+ def _smolagents_edit(source: str) -> Tuple[Optional[str], List[int]]:
121
+ """Rewrite only direct, single-line returns of a fresh local smolagents agent.
122
+
123
+ A function must accept an explicit session_id and construct one CodeAgent or
124
+ ToolCallingAgent locally. Other shapes require human review instead of a
125
+ plausible-looking, potentially bypassable edit.
126
+ """
127
+ try:
128
+ tree = ast.parse(source)
129
+ except SyntaxError:
130
+ return None, []
131
+ constructors: set[str] = set()
132
+ modules: set[str] = set()
133
+ for node in tree.body:
134
+ if isinstance(node, ast.ImportFrom) and node.module == "smolagents":
135
+ constructors.update(alias.asname or alias.name for alias in node.names if alias.name in {"CodeAgent", "ToolCallingAgent"})
136
+ elif isinstance(node, ast.Import):
137
+ modules.update(alias.asname or alias.name for alias in node.names if alias.name == "smolagents")
138
+ if not constructors and not modules:
139
+ return None, []
140
+ if "AgenticDomeSmolagentsFirewall" in source:
141
+ return None, []
142
+
143
+ def is_constructor(value: ast.AST) -> bool:
144
+ if not isinstance(value, ast.Call):
145
+ return False
146
+ call = value.func
147
+ return (isinstance(call, ast.Name) and call.id in constructors) or (
148
+ isinstance(call, ast.Attribute) and call.attr in {"CodeAgent", "ToolCallingAgent"}
149
+ and isinstance(call.value, ast.Name) and call.value.id in modules
150
+ )
151
+
152
+ raw = source.encode("utf-8")
153
+ lines = raw.splitlines(keepends=True)
154
+
155
+ def offset(line: int, column: int) -> int:
156
+ return sum(len(item) for item in lines[:line - 1]) + column
157
+
158
+ replacements: List[Tuple[int, int, bytes]] = []
159
+ changed_lines: List[int] = []
160
+ for function in (node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)):
161
+ parameters = [*function.args.posonlyargs, *function.args.args, *function.args.kwonlyargs]
162
+ if "session_id" not in {item.arg for item in parameters}:
163
+ continue
164
+ assignments = []
165
+ for index, statement in enumerate(function.body):
166
+ if isinstance(statement, ast.Assign) and len(statement.targets) == 1 and isinstance(statement.targets[0], ast.Name) and is_constructor(statement.value):
167
+ assignments.append((index, statement.targets[0].id))
168
+ elif isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name) and is_constructor(statement.value):
169
+ assignments.append((index, statement.target.id))
170
+ if len(assignments) != 1:
171
+ continue
172
+ assigned_at, agent_name = assignments[0]
173
+ direct_returns = []
174
+ for index, statement in enumerate(function.body):
175
+ if index <= assigned_at or not isinstance(statement, ast.Return) or not isinstance(statement.value, ast.Call):
176
+ continue
177
+ call = statement.value
178
+ if (isinstance(call.func, ast.Attribute) and call.func.attr == "run"
179
+ and isinstance(call.func.value, ast.Name) and call.func.value.id == agent_name
180
+ and len(call.args) == 1 and not call.keywords and call.lineno == call.end_lineno):
181
+ direct_returns.append(call)
182
+ calls_for_agent = [node for node in ast.walk(function)
183
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)
184
+ and node.func.attr == "run" and isinstance(node.func.value, ast.Name)
185
+ and node.func.value.id == agent_name]
186
+ if len(direct_returns) != 1 or len(calls_for_agent) != 1:
187
+ continue
188
+ if any(isinstance(node, (ast.Assign, ast.AnnAssign, ast.NamedExpr))
189
+ and any(isinstance(name, ast.Name) and name.id in {agent_name, "session_id"}
190
+ for name in ast.walk(node))
191
+ for node in function.body[assigned_at + 1:] if not isinstance(node, ast.Return)):
192
+ continue
193
+ call = direct_returns[0]
194
+ argument = ast.get_source_segment(source, call.args[0])
195
+ if not argument or "\n" in argument or "\r" in argument:
196
+ continue
197
+ replacement = ("AgenticDomeSmolagentsFirewall().run_agent_securely("
198
+ + agent_name + ", " + argument + ", session_id=session_id)")
199
+ replacements.append((offset(call.lineno, call.col_offset), offset(call.end_lineno, call.end_col_offset), replacement.encode("utf-8")))
200
+ changed_lines.append(call.lineno)
201
+ if not replacements:
202
+ return None, []
203
+
204
+ insertion_line = 0
205
+ body = tree.body
206
+ if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, (ast.Str, ast.Constant)) and isinstance(getattr(body[0].value, "value", None), str):
207
+ insertion_line = body[0].end_lineno
208
+ body = body[1:]
209
+ for node in body:
210
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
211
+ insertion_line = node.end_lineno
212
+ else:
213
+ break
214
+ if insertion_line == 0:
215
+ for line in lines[:2]:
216
+ if line.startswith(b"#!") or b"coding:" in line or b"coding=" in line:
217
+ insertion_line += 1
218
+ else:
219
+ break
220
+ newline = b"\r\n" if b"\r\n" in raw else b"\n"
221
+ import_bytes = b"from agenticdome_sdk.smolagents import AgenticDomeSmolagentsFirewall" + newline
222
+ replacements.append((sum(len(item) for item in lines[:insertion_line]), sum(len(item) for item in lines[:insertion_line]), import_bytes))
223
+ for start, end, value in sorted(replacements, key=lambda item: item[0], reverse=True):
224
+ raw = raw[:start] + value + raw[end:]
225
+ try:
226
+ ast.parse(raw.decode("utf-8"))
227
+ except (SyntaxError, UnicodeDecodeError):
228
+ return None, []
229
+ return raw.decode("utf-8"), changed_lines
230
+
231
+
232
+ def _candidate_paths(plan: Dict[str, Any]) -> Tuple[List[str], bool]:
233
+ points = list(plan.get("candidate_boundaries") or [])
234
+ paths = sorted({str(item.get("path", "")) for item in points
235
+ if isinstance(item, dict) and item.get("boundary") in {"prompt_ingress", "tool_execution"}
236
+ and str(item.get("path", "")).endswith(".py")})
237
+ return paths[:MAX_CANDIDATE_FILES], len(paths) > MAX_CANDIDATE_FILES
238
+
239
+
240
+ def _local_smolagents_paths(root: Path) -> Tuple[List[str], bool]:
241
+ """Find exact local adapter patterns missed by broad boundary heuristics.
242
+
243
+ This never sends source to Copilot. It walks one workload, prunes generated
244
+ and dependency trees, and reads at most 5,000 small Python files.
245
+ """
246
+ from .onboarding_cli import IGNORED_DIRECTORIES, SENSITIVE_FILE_PATTERN, SENSITIVE_FILE_SUFFIXES
247
+
248
+ found: List[str] = []
249
+ inspected = 0
250
+ capped = False
251
+ for directory, subdirs, filenames in os.walk(root, followlinks=False):
252
+ subdirs[:] = sorted(name for name in subdirs if name not in IGNORED_DIRECTORIES
253
+ and not (Path(directory) / name).is_symlink())
254
+ for name in sorted(filenames):
255
+ if name.endswith(".py") and not SENSITIVE_FILE_PATTERN.search(name) and Path(name).suffix not in SENSITIVE_FILE_SUFFIXES:
256
+ inspected += 1
257
+ if inspected > 5_000:
258
+ capped = True
259
+ break
260
+ path = Path(directory) / name
261
+ try:
262
+ if path.is_symlink() or path.stat().st_size > MAX_SOURCE_BYTES:
263
+ continue
264
+ contents = path.read_bytes()
265
+ except OSError:
266
+ continue
267
+ if b"CodeAgent" not in contents and b"ToolCallingAgent" not in contents:
268
+ continue
269
+ if b".run(" not in contents and b".run (" not in contents:
270
+ continue
271
+ found.append(path.relative_to(root).as_posix())
272
+ if len(found) >= MAX_CANDIDATE_FILES:
273
+ capped = True
274
+ break
275
+ if capped:
276
+ break
277
+ return found, capped
278
+
279
+
280
+ def _manual_note(path: str, reason: str) -> Dict[str, str]:
281
+ return {"path": path, "reason": reason}
282
+
283
+
284
+ def preview(root: Path, plan: Optional[Dict[str, Any]] = None, *, target: str = "application") -> Dict[str, Any]:
285
+ from .onboarding_cli import _load_json, _scaffold_files, create_scaffold, integration_plan
286
+
287
+ integration_target = target
288
+ if integration_target not in {"application", "mcp"}:
289
+ raise ValueError("Choose application or mcp for the guided integration target.")
290
+ root = root.resolve()
291
+ manifest_path, patch_path, proposed_root = _paths(root)
292
+ plan = plan or integration_plan(root)
293
+ mcp_plan = plan.get("mcp_protection")
294
+ if integration_target == "mcp" and (not isinstance(mcp_plan, dict) or mcp_plan.get("detected") is not True):
295
+ raise RuntimeError("No MCP boundary was found in the current plan. Run agenticdome mcp protect from the MCP workload first.")
296
+ prior_applied_revision = None
297
+ if manifest_path.exists():
298
+ prior = _load_manifest(root)
299
+ if prior.get("state") == "applying":
300
+ raise RuntimeError("A previous integration apply was interrupted. Run integrate undo before creating another preview.")
301
+ if prior.get("state") == "applied":
302
+ prior_applied_revision = prior["approval_code"]
303
+ history_manifest, history_patch = _history_paths(root, prior_applied_revision)
304
+ if history_patch.exists() and not history_manifest.exists() and _sha(history_patch.read_bytes()) == prior["patch_sha256"]:
305
+ _write_json(history_manifest, prior)
306
+ if history_manifest.exists() and not history_patch.exists():
307
+ try:
308
+ archived_record = json.loads(history_manifest.read_text(encoding="utf-8"))
309
+ except (OSError, json.JSONDecodeError):
310
+ archived_record = None
311
+ if archived_record == prior:
312
+ _atomic_write(history_patch, patch_path.read_bytes(), 0o600)
313
+ if history_manifest.exists() or history_patch.exists():
314
+ try:
315
+ archived = _load_manifest(root, revision=prior_applied_revision)
316
+ except (RuntimeError, ValueError) as exc:
317
+ raise RuntimeError("The previous approved revision has conflicting local history. Review it before creating another preview.") from exc
318
+ if archived != prior:
319
+ raise RuntimeError("The previous approved revision conflicts with its local history. Review it before creating another preview.")
320
+ else:
321
+ _atomic_write(history_patch, patch_path.read_bytes(), 0o600)
322
+ _write_json(history_manifest, prior)
323
+ create_scaffold(root, plan=plan)
324
+ config = _load_json(root / ".agenticdome" / "config.json")
325
+ generated = _scaffold_files(config, plan)
326
+ changes: List[Dict[str, Any]] = []
327
+ patch_parts: List[str] = []
328
+ manual: List[Dict[str, str]] = []
329
+
330
+ review_files = GENERATED_FILES | (MCP_GENERATED_FILES if integration_target == "mcp" else set())
331
+ for relative in sorted(review_files.intersection(generated)):
332
+ try:
333
+ target = _target(root, relative)
334
+ except ValueError as exc:
335
+ manual.append(_manual_note(relative, str(exc)))
336
+ continue
337
+ value = generated[relative].encode("utf-8")
338
+ if target.exists():
339
+ if target.is_file() and _sha(target.read_bytes()) == _sha(value):
340
+ continue
341
+ manual.append(_manual_note(relative, "An existing file uses this name; review it manually. It will not be overwritten."))
342
+ continue
343
+ _atomic_write(_target(proposed_root, relative), value, 0o600)
344
+ patch_parts.extend(difflib.unified_diff([], value.decode("utf-8").splitlines(keepends=True),
345
+ fromfile="/dev/null", tofile="b/" + relative))
346
+ changes.append({"path": relative, "kind": "added", "before_sha256": None,
347
+ "after_sha256": _sha(value), "before_mode": None, "after_mode": _expected_new_mode(),
348
+ "summary": "Add a generated MCP integration review file; forwarding is not connected."
349
+ if relative in MCP_GENERATED_FILES else "Add a generated integration review file."})
350
+
351
+ hook_ready = integration_target == "application" and any(item.get("framework") == "smolagents" and item.get("status") == "ready_for_attachment"
352
+ and "run_agent_securely" in (item.get("adapter") or {}).get("attachment_methods", [])
353
+ for item in plan.get("framework_hook_plans", []) if isinstance(item, dict))
354
+ binding_ready = (plan.get("hook_catalog") or {}).get("sidecar_binding", {}).get("verified") is True
355
+ paths, capped = _candidate_paths(plan)
356
+ if hook_ready and binding_ready:
357
+ local_paths, local_capped = _local_smolagents_paths(root)
358
+ combined_paths = sorted(set(paths).union(local_paths))
359
+ paths = combined_paths[:MAX_CANDIDATE_FILES]
360
+ capped = capped or local_capped or len(combined_paths) > MAX_CANDIDATE_FILES
361
+ if capped:
362
+ manual.append(_manual_note("workload", "The bounded local Python candidate scan reached its 200-candidate or 5,000-file limit; remaining files were not auto-edited."))
363
+ if integration_target == "mcp":
364
+ manual.append(_manual_note("mcp-forwarding", "Generated MCP files are only preparation. Attach the wrapper to each real forwarder, supply trusted identity and purpose, reroute clients, and test allowed/blocked requests. No traffic is protected by copying files alone."))
365
+ elif not hook_ready or not binding_ready:
366
+ manual.append(_manual_note("workload", "Existing-source edits require a verified sidecar catalog and a certified smolagents run_agent_securely adapter. Other frameworks remain manual-review paths."))
367
+ else:
368
+ for relative in paths:
369
+ try:
370
+ target = _target(root, relative)
371
+ except ValueError as exc:
372
+ manual.append(_manual_note(relative, str(exc)))
373
+ continue
374
+ try:
375
+ if not target.is_file() or target.stat().st_size > MAX_SOURCE_BYTES:
376
+ manual.append(_manual_note(relative, "Not a readable Python file within the 300 KB local edit limit."))
377
+ continue
378
+ before = target.read_bytes()
379
+ after_text, line_numbers = _smolagents_edit(before.decode("utf-8"))
380
+ except (OSError, UnicodeDecodeError):
381
+ manual.append(_manual_note(relative, "Source could not be read as UTF-8; no edit proposed."))
382
+ continue
383
+ if after_text is None:
384
+ manual.append(_manual_note(relative, "No exact supported pattern: a fresh local smolagents agent, direct return of agent.run(task), and explicit session_id are required."))
385
+ continue
386
+ after = after_text.encode("utf-8")
387
+ if after == before:
388
+ continue
389
+ _atomic_write(_target(proposed_root, relative), after, 0o600)
390
+ patch_parts.extend(difflib.unified_diff(before.decode("utf-8").splitlines(keepends=True),
391
+ after_text.splitlines(keepends=True),
392
+ fromfile="a/" + relative, tofile="b/" + relative))
393
+ changes.append({"path": relative, "kind": "modified", "before_sha256": _sha(before),
394
+ "after_sha256": _sha(after), "before_mode": _file_mode(target),
395
+ "after_mode": _file_mode(target),
396
+ "summary": "Route a direct smolagents run through input, tool and output enforcement.",
397
+ "lines": line_numbers, "adapter": "AgenticDomeSmolagentsFirewall.run_agent_securely"})
398
+
399
+ if any(item["kind"] == "modified" for item in changes):
400
+ manual.append(_manual_note("deployment", "Add the certified AgenticDome SDK to the actual application deployment and supply a genuine stable session_id and Runtime / SDK key. Installing the CLI in a separate virtual environment is not enough."))
401
+
402
+ summary_path = "AGENTICDOME-CHANGES.md"
403
+ summary_exists = _target(root, summary_path).exists()
404
+ if summary_exists:
405
+ manual.append(_manual_note(summary_path, "An existing change record uses this name; it will not be overwritten. The current proposal summary remains under .agenticdome/scaffold/proposed."))
406
+ summary_lines = [
407
+ "# AgenticDome proposed changes", "",
408
+ "This is a local proposal, not proof that production actions are protected.", "",
409
+ "## Exact file summary", "",
410
+ ]
411
+ for item in changes:
412
+ summary_lines.append("- `" + item["path"] + "` — " + item["kind"] + ": " + item["summary"])
413
+ summary_lines.append(" - Before SHA-256: `" + (item["before_sha256"] or "file absent") + "`; after SHA-256: `" + item["after_sha256"] + "`")
414
+ summary_lines.append(" - Mode: `" + (oct(item["before_mode"]) if item["before_mode"] is not None else "file absent or platform-managed") + "` → `" + (oct(item["after_mode"]) if item["after_mode"] is not None else "platform-managed") + "`")
415
+ if item.get("lines"):
416
+ summary_lines.append(" - Original call line(s): " + ", ".join(str(line) for line in item["lines"]))
417
+ summary_lines.extend(["", "## Still requires review", ""])
418
+ summary_lines.extend("- `" + item["path"] + "`: " + item["reason"] for item in manual)
419
+ if not manual:
420
+ summary_lines.append("- No additional static candidate was flagged; runtime and workload tests are still required.")
421
+ summary_lines.extend(["", "## Before relying on this integration", "",
422
+ "Provision a separate Runtime / SDK key to the real application process through your secret manager.",
423
+ "Review the exact diff and framework hook report; run application tests and `agenticdome mcp verify`." if integration_target == "mcp"
424
+ else "Review the exact diff and framework hook report; run application tests and `agenticdome verify --run-tests`.",
425
+ "Exercise an allowed and blocked action through the actual connected tool boundary.",
426
+ "The optional CI and generic wrapper templates remain under `.agenticdome/scaffold`; they are not applied automatically.", ""])
427
+ summary_bytes = "\n".join(summary_lines).encode("utf-8")
428
+ _atomic_write(_target(proposed_root, summary_path), summary_bytes, 0o600)
429
+ if not summary_exists:
430
+ patch_parts.extend(difflib.unified_diff([], summary_bytes.decode("utf-8").splitlines(keepends=True),
431
+ fromfile="/dev/null", tofile="b/" + summary_path))
432
+ changes.append({"path": summary_path, "kind": "added", "before_sha256": None,
433
+ "after_sha256": _sha(summary_bytes), "before_mode": None, "after_mode": _expected_new_mode(),
434
+ "summary": "Add an exact, reviewable change record to the repository."})
435
+
436
+ patch = "".join(patch_parts).encode("utf-8")
437
+ _atomic_write(patch_path, patch, 0o600)
438
+ digest = _sha(patch)
439
+ result: Dict[str, Any] = {
440
+ "schema": SCHEMA, "state": "preview", "source_upload": False,
441
+ "integration_target": integration_target,
442
+ "created_at": _now(), "plan_sha256": _sha(json.dumps(plan, sort_keys=True, separators=(",", ":")).encode("utf-8")),
443
+ "patch_sha256": digest, "approval_code": digest[:12],
444
+ "patch": ".agenticdome/scaffold/guided-integration.patch",
445
+ "prior_applied_revision": prior_applied_revision,
446
+ "changes": changes, "manual_review": manual,
447
+ "source_edits": sum(item["kind"] == "modified" for item in changes),
448
+ "next_action": ("Review the exact diff and change summary. If approved, run agenticdome integrate apply. "
449
+ "This installs review files only; connect the real MCP forwarder manually and run agenticdome mcp verify before claiming protection."
450
+ if integration_target == "mcp" and any(item["path"] in MCP_GENERATED_FILES for item in changes)
451
+ else "Review the local patch and exact file summary, then run agenticdome integrate apply. "
452
+ "Protection is not proven until the real workload and blocked-tool tests pass."
453
+ if any(item["kind"] == "modified" for item in changes)
454
+ else "No safe existing-source edit was identified. Review SEMANTIC-REVIEW.md and integrate manually; do not claim this workload is protected."),
455
+ }
456
+ _write_json(manifest_path, result)
457
+ return result
458
+
459
+
460
+ def _load_manifest(root: Path, revision: Optional[str] = None) -> Dict[str, Any]:
461
+ if revision is None:
462
+ manifest_path, patch_path, _ = _paths(root)
463
+ else:
464
+ manifest_path, patch_path = _history_paths(root, revision)
465
+ if not manifest_path.is_file():
466
+ raise RuntimeError("No guided integration record exists for this revision. Run agenticdome integrate preview first or inspect local history.")
467
+ try:
468
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
469
+ except (OSError, json.JSONDecodeError) as exc:
470
+ raise RuntimeError("The local guided-integration record is unreadable; generate a new preview.") from exc
471
+ if manifest.get("schema") != SCHEMA or manifest.get("source_upload") is not False:
472
+ raise RuntimeError("The local guided-integration record is not recognized.")
473
+ if not patch_path.is_file() or _sha(patch_path.read_bytes()) != manifest.get("patch_sha256"):
474
+ raise RuntimeError("The proposed patch changed after preview. Generate a new preview before applying.")
475
+ return manifest
476
+
477
+
478
+ def _verified_proposals(root: Path, manifest: Dict[str, Any], applied: bool = False) -> List[Tuple[Path, Path, Dict[str, Any]]]:
479
+ _, _, proposed_root = _paths(root)
480
+ verified = []
481
+ for item in manifest.get("changes", []):
482
+ relative = item.get("path")
483
+ if not isinstance(relative, str) or item.get("kind") not in {"added", "modified"}:
484
+ raise RuntimeError("The guided-integration record contains an invalid change.")
485
+ target = _target(root, relative)
486
+ proposed = _target(proposed_root, relative)
487
+ if not proposed.is_file() or _sha(proposed.read_bytes()) != item.get("after_sha256"):
488
+ raise RuntimeError("A proposed file changed after preview; generate a new preview.")
489
+ if applied:
490
+ if not target.is_file() or _sha(target.read_bytes()) != item.get("after_sha256") or _file_mode(target) != item.get("after_mode"):
491
+ raise RuntimeError("An applied file changed since approval; undo is unsafe. Review the Git diff manually.")
492
+ elif item["kind"] == "added":
493
+ if target.exists():
494
+ raise RuntimeError("A proposed new file now exists. Review it and generate a new preview.")
495
+ elif not target.is_file() or _sha(target.read_bytes()) != item.get("before_sha256") or _file_mode(target) != item.get("before_mode"):
496
+ raise RuntimeError("Existing source changed after preview. No files were updated; generate a fresh preview.")
497
+ verified.append((target, proposed, item))
498
+ return verified
499
+
500
+
501
+ def apply(root: Path, approval_code: Optional[str] = None) -> Dict[str, Any]:
502
+ root = root.resolve()
503
+ manifest_path, _, _ = _paths(root)
504
+ manifest = _load_manifest(root)
505
+ if manifest.get("state") != "preview":
506
+ raise RuntimeError("This preview has already been applied or requires recovery.")
507
+ mcp_review_files = manifest.get("integration_target") == "mcp" and any(
508
+ item.get("kind") == "added" and item.get("path") in MCP_GENERATED_FILES
509
+ for item in manifest.get("changes", [])
510
+ )
511
+ if int(manifest.get("source_edits", 0)) < 1 and not mcp_review_files:
512
+ raise RuntimeError("No supported existing-source edit is available. Review the manual findings instead of applying generated files alone.")
513
+ expected = manifest["approval_code"]
514
+ if approval_code is None:
515
+ if not sys.stdin.isatty():
516
+ raise RuntimeError("Approval is required. Review the patch, then rerun with --approve " + expected + ".")
517
+ approval_code = input("Type approval code " + expected + " to apply these local changes: ").strip()
518
+ if approval_code != expected:
519
+ raise RuntimeError("Approval code did not match the reviewed patch. Nothing was changed.")
520
+ verified = _verified_proposals(root, manifest)
521
+ git_root = _git_root(root)
522
+ if git_root is None:
523
+ raise RuntimeError("Automatic apply requires a Git working tree. The preview remains available for manual review.")
524
+ # Preview refreshes .agenticdome artifacts, which customers may choose to
525
+ # track. Those generated changes must not make the reviewed source edit
526
+ # impossible to apply; all other tracked workload changes still block.
527
+ if _git(root, "status", "--porcelain", "--untracked-files=no", "--", ".", ":(exclude).agenticdome").stdout.strip():
528
+ raise RuntimeError("Tracked workload source changes exist. Commit or stash them before applying the guided integration.")
529
+ for _, _, item in verified:
530
+ if item["kind"] == "modified" and _git(root, "ls-files", "--error-unmatch", "--", item["path"], check=False).returncode != 0:
531
+ raise RuntimeError("An existing source file is not Git-tracked. Track it before automatic apply, or review the patch manually.")
532
+ branch = "agenticdome/integrate-" + manifest["patch_sha256"][:8] + "-" + uuid.uuid4().hex[:6]
533
+ if _git(git_root, "show-ref", "--verify", "--quiet", "refs/heads/" + branch, check=False).returncode == 0:
534
+ raise RuntimeError("The review branch already exists. Inspect it or create a new preview after undoing the earlier application.")
535
+ _git(git_root, "switch", "-c", branch)
536
+ backup_root = _target(root, ".agenticdome/scaffold/backups/" + expected)
537
+ written: List[Tuple[Path, Dict[str, Any]]] = []
538
+ manifest["state"] = "applying"
539
+ manifest["branch"] = branch
540
+ _write_json(manifest_path, manifest)
541
+ try:
542
+ for target, proposed, item in verified:
543
+ if item["kind"] == "modified":
544
+ backup = _target(backup_root, item["path"])
545
+ _atomic_write(backup, target.read_bytes(), 0o600)
546
+ current = target.read_bytes() if target.is_file() else None
547
+ if (current is None and item["kind"] != "added") or (current is not None and (
548
+ _sha(current) != item.get("before_sha256") or _file_mode(target) != item.get("before_mode")
549
+ )):
550
+ raise RuntimeError("A file changed during apply; the completed writes will be rolled back.")
551
+ proposed_bytes = proposed.read_bytes()
552
+ if _sha(proposed_bytes) != item["after_sha256"]:
553
+ raise RuntimeError("A proposed file changed during apply; the completed writes will be rolled back.")
554
+ mode = item["after_mode"]
555
+ _atomic_write(target, proposed_bytes, mode)
556
+ written.append((target, item))
557
+ except Exception:
558
+ for target, item in reversed(written):
559
+ if item["kind"] == "added":
560
+ if target.is_file() and _sha(target.read_bytes()) == item["after_sha256"]:
561
+ target.unlink()
562
+ else:
563
+ backup = _target(backup_root, item["path"])
564
+ if backup.is_file() and target.is_file() and _sha(target.read_bytes()) == item["after_sha256"]:
565
+ _atomic_write(target, backup.read_bytes(), item["before_mode"])
566
+ manifest["state"] = "apply_failed_rolled_back"
567
+ _write_json(manifest_path, manifest)
568
+ raise
569
+ manifest["state"] = "applied"
570
+ manifest["applied_at"] = _now()
571
+ manifest["next_action"] = (
572
+ "MCP review files were added on a local Git branch. No existing forwarder was rewired and no traffic is protected yet. "
573
+ "Attach the wrapper to the real request/response path, reroute clients, run workload tests and agenticdome mcp verify."
574
+ if manifest.get("integration_target") == "mcp" else
575
+ "Inspect the Git diff, run workload tests and agenticdome verify --run-tests, then review the real blocked-tool path. The edit alone is not protection proof."
576
+ )
577
+ _write_json(manifest_path, manifest)
578
+ return manifest
579
+
580
+
581
+ def undo(root: Path, revision: Optional[str] = None) -> Dict[str, Any]:
582
+ root = root.resolve()
583
+ manifest_path = _paths(root)[0] if revision is None else _history_paths(root, revision)[0]
584
+ manifest = _load_manifest(root, revision=revision)
585
+ if manifest.get("state") not in {"applied", "applying"}:
586
+ raise RuntimeError("There is no applied guided integration to undo.")
587
+ backup_root = _target(root, ".agenticdome/scaffold/backups/" + manifest["approval_code"])
588
+ actions: List[Tuple[Path, Dict[str, Any], Optional[bytes]]] = []
589
+ for item in manifest.get("changes", []):
590
+ target = _target(root, item["path"])
591
+ if item["kind"] == "added":
592
+ if target.exists() and (not target.is_file() or _sha(target.read_bytes()) != item["after_sha256"] or _file_mode(target) != item["after_mode"]):
593
+ raise RuntimeError("An applied file changed since approval; undo is unsafe. Review the Git diff manually.")
594
+ if target.exists():
595
+ actions.append((target, item, None))
596
+ else:
597
+ if not target.is_file():
598
+ raise RuntimeError("An edited source file is missing; restore it from Git manually.")
599
+ current_hash = _sha(target.read_bytes())
600
+ if current_hash not in {item["before_sha256"], item["after_sha256"]} or _file_mode(target) != item["after_mode"]:
601
+ raise RuntimeError("An applied file changed since approval; undo is unsafe. Review the Git diff manually.")
602
+ if current_hash == item["after_sha256"]:
603
+ backup = _target(backup_root, item["path"])
604
+ if not backup.is_file() or _sha(backup.read_bytes()) != item["before_sha256"]:
605
+ raise RuntimeError("A local backup is missing or changed. Stop and restore from Git manually.")
606
+ actions.append((target, item, backup.read_bytes()))
607
+ for target, item, backup in reversed(actions):
608
+ if item["kind"] == "added":
609
+ target.unlink()
610
+ elif backup is not None:
611
+ _atomic_write(target, backup, item["before_mode"])
612
+ manifest["state"] = "undone"
613
+ manifest["undone_at"] = _now()
614
+ manifest["next_action"] = "Local edits were restored. The review branch remains available; no Git reset or delete was performed."
615
+ _write_json(manifest_path, manifest)
616
+ return manifest
617
+
618
+
619
+ def record_verification(root: Path, result: Dict[str, Any]) -> None:
620
+ manifest_path, _, _ = _paths(root)
621
+ if not manifest_path.is_file():
622
+ return
623
+ try:
624
+ manifest = _load_manifest(root)
625
+ except RuntimeError:
626
+ return
627
+ if manifest.get("state") != "applied":
628
+ return
629
+ manifest["verification"] = {
630
+ "recorded_at": _now(),
631
+ "ready": result.get("ready") is True,
632
+ "report_sha256": _sha(json.dumps(result, sort_keys=True, separators=(",", ":")).encode("utf-8")),
633
+ }
634
+ _write_json(manifest_path, manifest)
635
+
636
+
637
+ def export_summary(root: Path) -> Optional[Dict[str, Any]]:
638
+ """Return a bounded source-free change ledger for onboarding evidence."""
639
+ manifest_path, _, _ = _paths(root.resolve())
640
+ if not manifest_path.is_file():
641
+ return None
642
+ manifest = _load_manifest(root.resolve())
643
+ return {
644
+ "schema": SCHEMA,
645
+ "state": manifest.get("state"),
646
+ "integration_target": manifest.get("integration_target", "application"),
647
+ "source_upload": False,
648
+ "patch_sha256": manifest.get("patch_sha256"),
649
+ "branch": manifest.get("branch"),
650
+ "prior_applied_revision": manifest.get("prior_applied_revision"),
651
+ "source_edits": manifest.get("source_edits", 0),
652
+ "manual_review_count": len(manifest.get("manual_review", [])),
653
+ "changes": [
654
+ {key: item.get(key) for key in ("path", "kind", "before_sha256", "after_sha256", "before_mode", "after_mode", "summary")}
655
+ for item in manifest.get("changes", [])[:250]
656
+ ],
657
+ "change_list_complete": len(manifest.get("changes", [])) <= 250,
658
+ }
@@ -1912,7 +1912,7 @@ def protect_mcp(root: Path) -> Dict[str, Any]:
1912
1912
  "plan": _relative(plan_path, root),
1913
1913
  "patch": _relative(patch_path, root),
1914
1914
  "customer_source_modified": False,
1915
- "next_action": "Review the MCP plan, supply genuine identity and business-purpose context, resolve every bypass, then manually apply suitable additions and run agenticdome mcp verify.",
1915
+ "next_action": "Review the MCP plan, then run agenticdome integrate preview --target mcp for an exact local file diff. Approval can add review files on a Git branch, but the real MCP forwarder and client routing still require manual attachment and tests before agenticdome mcp verify.",
1916
1916
  }
1917
1917
 
1918
1918
 
@@ -2277,6 +2277,22 @@ def _print(value: Any, as_json: bool = False) -> None:
2277
2277
  print(value)
2278
2278
 
2279
2279
 
2280
+ def _guided_cli_summary(result: Dict[str, Any]) -> Dict[str, Any]:
2281
+ return {
2282
+ "state": result.get("state"),
2283
+ "integration_target": result.get("integration_target", "application"),
2284
+ "existing_source_edits": int(result.get("source_edits", 0)),
2285
+ "new_files": sum(item.get("kind") == "added" for item in result.get("changes", [])),
2286
+ "manual_review_items": len(result.get("manual_review", [])),
2287
+ "patch": result.get("patch"),
2288
+ "change_summary": ".agenticdome/scaffold/proposed/AGENTICDOME-CHANGES.md",
2289
+ "approval_code": result.get("approval_code"),
2290
+ "branch": result.get("branch"),
2291
+ "prior_applied_revision": result.get("prior_applied_revision"),
2292
+ "next_action": result.get("next_action"),
2293
+ }
2294
+
2295
+
2280
2296
  def build_parser() -> argparse.ArgumentParser:
2281
2297
  parser = argparse.ArgumentParser(prog="agenticdome", description="Local-first AgenticDome integration assistant.")
2282
2298
  parser.add_argument(
@@ -2320,6 +2336,16 @@ def build_parser() -> argparse.ArgumentParser:
2320
2336
  plan_parser.add_argument("--output")
2321
2337
 
2322
2338
  subparsers.add_parser("scaffold", help="Generate an unapplied patch and review files under .agenticdome/scaffold.")
2339
+ integrate_parser = subparsers.add_parser("integrate", help="Preview, approve and safely apply local integration edits.")
2340
+ integrate_subparsers = integrate_parser.add_subparsers(dest="integrate_command", required=True)
2341
+ integrate_preview = integrate_subparsers.add_parser("preview", help="Plan once, then generate an exact local diff and change summary without editing application source.")
2342
+ integrate_preview.add_argument("--target", choices=["application", "mcp"], default="application",
2343
+ help="Use mcp for reviewable MCP gateway files; real forwarding still needs manual attachment.")
2344
+ integrate_apply = integrate_subparsers.add_parser("apply", help="Apply the reviewed proposal on a new Git branch; never commit or deploy automatically.")
2345
+ integrate_apply.add_argument("--approve", help="Approval code printed by preview; required when stdin is non-interactive.")
2346
+ integrate_subparsers.add_parser("status", help="Show the saved change summary without rerunning Copilot.")
2347
+ integrate_undo = integrate_subparsers.add_parser("undo", help="Restore only unchanged AgenticDome-applied files from exact local backups.")
2348
+ integrate_undo.add_argument("--revision", help="An earlier approval code shown in status; required when undoing an archived applied revision.")
2323
2349
  verify_parser = subparsers.add_parser("verify", help="Run fixed allowed/blocked decisions and boundary coverage checks.")
2324
2350
  verify_parser.add_argument("--live", action="store_true")
2325
2351
  verify_parser.add_argument(
@@ -2397,6 +2423,17 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2397
2423
  return 0
2398
2424
  if args.command in {"inspect", "doctor"}:
2399
2425
  report = _exportable_inspection(inspect_repository(root))
2426
+ from .guided_integration import export_summary
2427
+ try:
2428
+ guided_summary = export_summary(root)
2429
+ except (RuntimeError, ValueError) as exc:
2430
+ raise SystemExit("The local guided integration record is invalid: " + str(exc)) from exc
2431
+ if guided_summary is not None:
2432
+ report["guided_integration"] = guided_summary
2433
+ report.pop("report_sha256", None)
2434
+ report["report_sha256"] = hashlib.sha256(
2435
+ json.dumps(report, sort_keys=True, separators=(",", ":")).encode("utf-8")
2436
+ ).hexdigest()
2400
2437
  if args.output:
2401
2438
  _write_json(Path(args.output), report)
2402
2439
  _print({
@@ -2417,7 +2454,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2417
2454
  "status": "created",
2418
2455
  "config_path": ".agenticdome/config.json",
2419
2456
  "inspection_path": ".agenticdome/inspection.json",
2420
- "next_action": "Upload .agenticdome/inspection.json in Control Panel Step 1; do not paste this output. If the dot-folder is hidden, run: agenticdome inspect --output agenticdome-inspection.json",
2457
+ "next_action": "Local initialization is complete; do not paste this output. Next, use your tenant's Integration Copilot key to run agenticdome integrate preview, review the exact local diff, approve only safe edits or attach guards manually, then run agenticdome inspect --output agenticdome-inspection.json and import that refreshed file in Onboarding > Integrate.",
2421
2458
  "config": config,
2422
2459
  })
2423
2460
  return 0
@@ -2431,10 +2468,27 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2431
2468
  patch_path = create_scaffold(root)
2432
2469
  _print({"status": "generated_not_applied", "patch": _relative(patch_path, root)})
2433
2470
  return 0
2471
+ if args.command == "integrate":
2472
+ from .guided_integration import _load_manifest, apply, preview, undo
2473
+ try:
2474
+ if args.integrate_command == "preview":
2475
+ result = preview(root, target=args.target)
2476
+ elif args.integrate_command == "apply":
2477
+ result = apply(root, approval_code=args.approve)
2478
+ elif args.integrate_command == "undo":
2479
+ result = undo(root, revision=args.revision)
2480
+ else:
2481
+ result = _load_manifest(root)
2482
+ except (RuntimeError, ValueError) as exc:
2483
+ raise SystemExit(str(exc)) from exc
2484
+ _print(_guided_cli_summary(result))
2485
+ return 0
2434
2486
  if args.command == "verify":
2435
2487
  exit_code, result = verify_project(root, live=args.live, run_tests=args.run_tests)
2436
2488
  if args.output:
2437
2489
  _write_json(Path(args.output), result)
2490
+ from .guided_integration import record_verification
2491
+ record_verification(root, result)
2438
2492
  _print(result)
2439
2493
  return exit_code
2440
2494
  if args.command == "mcp" and args.mcp_command == "protect":
@@ -2454,6 +2508,8 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
2454
2508
  if not output.is_absolute():
2455
2509
  output = root / output
2456
2510
  _write_json(output, result)
2511
+ from .guided_integration import record_verification
2512
+ record_verification(root, result)
2457
2513
  _print(result)
2458
2514
  return exit_code
2459
2515
  if args.command == "openclaw" and args.openclaw_command == "protect":