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.
- {agenticdome_python_sdk-1.2.34.dist-info → agenticdome_python_sdk-1.2.35.dist-info}/METADATA +32 -9
- {agenticdome_python_sdk-1.2.34.dist-info → agenticdome_python_sdk-1.2.35.dist-info}/RECORD +10 -9
- agenticdome_sdk/catalog_snapshot.json +3 -3
- agenticdome_sdk/guided_integration.py +658 -0
- agenticdome_sdk/onboarding_cli.py +58 -2
- {agenticdome_python_sdk-1.2.34.dist-info → agenticdome_python_sdk-1.2.35.dist-info}/WHEEL +0 -0
- {agenticdome_python_sdk-1.2.34.dist-info → agenticdome_python_sdk-1.2.35.dist-info}/entry_points.txt +0 -0
- {agenticdome_python_sdk-1.2.34.dist-info → agenticdome_python_sdk-1.2.35.dist-info}/licenses/LICENSE +0 -0
- {agenticdome_python_sdk-1.2.34.dist-info → agenticdome_python_sdk-1.2.35.dist-info}/licenses/NOTICE +0 -0
- {agenticdome_python_sdk-1.2.34.dist-info → agenticdome_python_sdk-1.2.35.dist-info}/top_level.txt +0 -0
{agenticdome_python_sdk-1.2.34.dist-info → agenticdome_python_sdk-1.2.35.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agenticdome-python-sdk
|
|
3
|
-
Version: 1.2.
|
|
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
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
#
|
|
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`
|
|
196
|
-
|
|
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.
|
|
2
|
-
agenticdome_python_sdk-1.2.
|
|
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=
|
|
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=
|
|
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.
|
|
38
|
-
agenticdome_python_sdk-1.2.
|
|
39
|
-
agenticdome_python_sdk-1.2.
|
|
40
|
-
agenticdome_python_sdk-1.2.
|
|
41
|
-
agenticdome_python_sdk-1.2.
|
|
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.
|
|
5
|
+
"python_sdk_version": "1.2.35",
|
|
6
6
|
"published_packages": {
|
|
7
7
|
"agenticdome-python-sdk": {
|
|
8
8
|
"registry": "pypi",
|
|
9
|
-
"version": "1.2.
|
|
10
|
-
"url": "https://pypi.org/project/agenticdome-python-sdk/1.2.
|
|
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,
|
|
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": "
|
|
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":
|
|
File without changes
|
{agenticdome_python_sdk-1.2.34.dist-info → agenticdome_python_sdk-1.2.35.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{agenticdome_python_sdk-1.2.34.dist-info → agenticdome_python_sdk-1.2.35.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{agenticdome_python_sdk-1.2.34.dist-info → agenticdome_python_sdk-1.2.35.dist-info}/licenses/NOTICE
RENAMED
|
File without changes
|
{agenticdome_python_sdk-1.2.34.dist-info → agenticdome_python_sdk-1.2.35.dist-info}/top_level.txt
RENAMED
|
File without changes
|