codevanta 0.1.0__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.
- codevanta/__init__.py +1 -0
- codevanta/analysis/__init__.py +1 -0
- codevanta/analysis/binary_diff.py +1168 -0
- codevanta/analysis/callgraph.py +542 -0
- codevanta/analysis/cfg.py +1201 -0
- codevanta/analysis/dataflow.py +762 -0
- codevanta/analysis/debugger.py +289 -0
- codevanta/analysis/debuginfo.py +304 -0
- codevanta/analysis/decompile.py +1094 -0
- codevanta/analysis/decompilers/__init__.py +250 -0
- codevanta/analysis/decompilers/angr_backend.py +251 -0
- codevanta/analysis/decompilers/base.py +278 -0
- codevanta/analysis/decompilers/ghidra_backend.py +316 -0
- codevanta/analysis/dynamic.py +756 -0
- codevanta/analysis/emulate.py +1349 -0
- codevanta/analysis/evidence_bundle.py +736 -0
- codevanta/analysis/function_hash.py +562 -0
- codevanta/analysis/graph_export.py +441 -0
- codevanta/analysis/html_view.py +695 -0
- codevanta/analysis/jump_tables.py +546 -0
- codevanta/analysis/solver.py +715 -0
- codevanta/analysis/symbolic.py +1809 -0
- codevanta/analysis/trace.py +646 -0
- codevanta/analysis/type_propagation.py +1055 -0
- codevanta/analysis/type_recovery.py +1214 -0
- codevanta/analysis/vtables.py +572 -0
- codevanta/analysis/xrefs.py +434 -0
- codevanta/api/__init__.py +1 -0
- codevanta/cli/__init__.py +1 -0
- codevanta/cli/main.py +3038 -0
- codevanta/core/__init__.py +31 -0
- codevanta/core/disassembler.py +121 -0
- codevanta/core/entropy.py +237 -0
- codevanta/core/functions.py +176 -0
- codevanta/core/loader.py +1351 -0
- codevanta/core/report.py +306 -0
- codevanta/core/strings.py +109 -0
- codevanta/evidence/__init__.py +7 -0
- codevanta/evidence/confidence.py +36 -0
- codevanta/evidence/models.py +43 -0
- codevanta/evidence/provenance.py +37 -0
- codevanta/gui/__init__.py +46 -0
- codevanta/gui/layout.py +1384 -0
- codevanta/gui/server.py +1305 -0
- codevanta/ir/__init__.py +6 -0
- codevanta/ir/adapters/__init__.py +31 -0
- codevanta/ir/base.py +31 -0
- codevanta/ir/instruction.py +42 -0
- codevanta/mcp_server/__init__.py +5 -0
- codevanta/mcp_server/server.py +710 -0
- codevanta/mcp_server/tools.py +1364 -0
- codevanta/plugins/__init__.py +748 -0
- codevanta/setup/__init__.py +28 -0
- codevanta/setup/apply.py +22 -0
- codevanta/setup/clients.py +285 -0
- codevanta/setup/plan.py +79 -0
- codevanta/setup/verify.py +43 -0
- codevanta/storage/__init__.py +1 -0
- codevanta/storage/session.py +514 -0
- codevanta-0.1.0.dist-info/METADATA +688 -0
- codevanta-0.1.0.dist-info/RECORD +65 -0
- codevanta-0.1.0.dist-info/WHEEL +5 -0
- codevanta-0.1.0.dist-info/entry_points.txt +2 -0
- codevanta-0.1.0.dist-info/licenses/LICENSE +21 -0
- codevanta-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""CODEVANTA setup helpers for coding-agent MCP configuration."""
|
|
2
|
+
|
|
3
|
+
from .apply import apply_plan
|
|
4
|
+
from .clients import (
|
|
5
|
+
CLIENTS,
|
|
6
|
+
ClientAdapter,
|
|
7
|
+
SetupContext,
|
|
8
|
+
SetupError,
|
|
9
|
+
detected_clients,
|
|
10
|
+
get_client,
|
|
11
|
+
)
|
|
12
|
+
from .plan import FileChange, SetupPlan, make_change
|
|
13
|
+
from .verify import verify_entry, verify_tools
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"CLIENTS",
|
|
17
|
+
"ClientAdapter",
|
|
18
|
+
"FileChange",
|
|
19
|
+
"SetupContext",
|
|
20
|
+
"SetupError",
|
|
21
|
+
"SetupPlan",
|
|
22
|
+
"apply_plan",
|
|
23
|
+
"detected_clients",
|
|
24
|
+
"get_client",
|
|
25
|
+
"make_change",
|
|
26
|
+
"verify_entry",
|
|
27
|
+
"verify_tools",
|
|
28
|
+
]
|
codevanta/setup/apply.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Apply setup plans safely (backups, idempotent writes)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .plan import SetupPlan
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def apply_plan(plan: SetupPlan) -> list[Path]:
|
|
11
|
+
"""Write every file in ``plan`` that actually changes, with a backup."""
|
|
12
|
+
written: list[Path] = []
|
|
13
|
+
for change in plan.changes:
|
|
14
|
+
if not change.will_write:
|
|
15
|
+
continue
|
|
16
|
+
change.path.parent.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
if change.path.exists():
|
|
18
|
+
backup = change.path.with_name(change.path.name + ".codevanta.bak")
|
|
19
|
+
backup.write_text(change.previous_content, encoding="utf-8")
|
|
20
|
+
change.path.write_text(change.new_content, encoding="utf-8")
|
|
21
|
+
written.append(change.path)
|
|
22
|
+
return written
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Client adapters that know where each coding agent stores MCP settings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .plan import SetupPlan, make_change
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SetupError(Exception):
|
|
16
|
+
"""Raised when a setup request cannot be planned safely."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
SERVER_KEY = "codevanta"
|
|
20
|
+
SERVER_COMMAND = "codevanta"
|
|
21
|
+
SERVER_ARGS = ("mcp", "serve")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class SetupContext:
|
|
26
|
+
"""Filesystem roots used to locate client configuration files."""
|
|
27
|
+
|
|
28
|
+
home: Path
|
|
29
|
+
project_dir: Path
|
|
30
|
+
appdata: Path
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_environment(cls, project_dir: str | Path | None = None) -> SetupContext:
|
|
34
|
+
home_env = os.environ.get("CODEVANTA_HOME")
|
|
35
|
+
home = Path(home_env).expanduser() if home_env else Path.home()
|
|
36
|
+
appdata_env = os.environ.get("APPDATA")
|
|
37
|
+
appdata = Path(appdata_env) if appdata_env else home / "AppData" / "Roaming"
|
|
38
|
+
project = Path(project_dir).expanduser() if project_dir else Path.cwd()
|
|
39
|
+
return cls(home=home, project_dir=project, appdata=appdata)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _dump(data: dict[str, Any]) -> str:
|
|
43
|
+
return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse(previous: str, path: Path) -> dict[str, Any]:
|
|
47
|
+
if not previous.strip():
|
|
48
|
+
return {}
|
|
49
|
+
try:
|
|
50
|
+
data = json.loads(previous)
|
|
51
|
+
except json.JSONDecodeError as exc:
|
|
52
|
+
raise SetupError(f"{path}: invalid JSON ({exc.msg} at line {exc.lineno})") from exc
|
|
53
|
+
if not isinstance(data, dict):
|
|
54
|
+
raise SetupError(f"{path}: expected a JSON object at the top level")
|
|
55
|
+
return data
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ClientAdapter(ABC):
|
|
59
|
+
"""Base class for a coding-agent MCP configuration adapter."""
|
|
60
|
+
|
|
61
|
+
name: str
|
|
62
|
+
display_name: str
|
|
63
|
+
supports_user_scope: bool = True
|
|
64
|
+
supports_project_scope: bool = True
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def container_key(self) -> str:
|
|
69
|
+
"""Top-level JSON object that holds MCP servers."""
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def config_path(self, ctx: SetupContext, scope: str) -> Path:
|
|
73
|
+
"""Return the configuration file for ``scope``."""
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def build_entry(self) -> dict[str, Any]:
|
|
77
|
+
"""Return the MCP server entry for this client."""
|
|
78
|
+
|
|
79
|
+
def plan(self, ctx: SetupContext, scope: str) -> SetupPlan:
|
|
80
|
+
return self._plan(ctx, scope, remove=False)
|
|
81
|
+
|
|
82
|
+
def removal_plan(self, ctx: SetupContext, scope: str) -> SetupPlan:
|
|
83
|
+
return self._plan(ctx, scope, remove=True)
|
|
84
|
+
|
|
85
|
+
def _plan(self, ctx: SetupContext, scope: str, remove: bool) -> SetupPlan:
|
|
86
|
+
if scope not in ("user", "project"):
|
|
87
|
+
raise SetupError(f"unknown scope {scope!r}; use 'user' or 'project'")
|
|
88
|
+
if scope == "user" and not self.supports_user_scope:
|
|
89
|
+
raise SetupError(f"{self.display_name} does not support user scope")
|
|
90
|
+
if scope == "project" and not self.supports_project_scope:
|
|
91
|
+
raise SetupError(f"{self.display_name} does not support project scope")
|
|
92
|
+
path = self.config_path(ctx, scope)
|
|
93
|
+
if remove and not path.exists():
|
|
94
|
+
return SetupPlan(
|
|
95
|
+
client=self.name,
|
|
96
|
+
scope=scope,
|
|
97
|
+
changes=(make_change(path, "", ""),),
|
|
98
|
+
notes=("configuration file not found",),
|
|
99
|
+
)
|
|
100
|
+
previous = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
101
|
+
data = _parse(previous, path)
|
|
102
|
+
servers = data.get(self.container_key)
|
|
103
|
+
if servers is None:
|
|
104
|
+
servers = {}
|
|
105
|
+
data[self.container_key] = servers
|
|
106
|
+
if not isinstance(servers, dict):
|
|
107
|
+
raise SetupError(f"{path}: '{self.container_key}' must be a JSON object")
|
|
108
|
+
notes: list[str] = []
|
|
109
|
+
if remove:
|
|
110
|
+
if SERVER_KEY in servers:
|
|
111
|
+
del servers[SERVER_KEY]
|
|
112
|
+
notes.append("removed CODEVANTA entry")
|
|
113
|
+
else:
|
|
114
|
+
notes.append("CODEVANTA is not registered here")
|
|
115
|
+
else:
|
|
116
|
+
desired = self.build_entry()
|
|
117
|
+
if servers.get(SERVER_KEY) == desired:
|
|
118
|
+
notes.append("already configured")
|
|
119
|
+
else:
|
|
120
|
+
servers[SERVER_KEY] = desired
|
|
121
|
+
notes.append("registered CODEVANTA")
|
|
122
|
+
change = make_change(path, previous, _dump(data))
|
|
123
|
+
return SetupPlan(client=self.name, scope=scope, changes=(change,), notes=tuple(notes))
|
|
124
|
+
|
|
125
|
+
def detected(self, ctx: SetupContext) -> bool:
|
|
126
|
+
scopes = []
|
|
127
|
+
if self.supports_user_scope:
|
|
128
|
+
scopes.append("user")
|
|
129
|
+
if self.supports_project_scope:
|
|
130
|
+
scopes.append("project")
|
|
131
|
+
for scope in scopes:
|
|
132
|
+
try:
|
|
133
|
+
if self.config_path(ctx, scope).exists():
|
|
134
|
+
return True
|
|
135
|
+
except SetupError:
|
|
136
|
+
continue
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ClaudeCodeClient(ClientAdapter):
|
|
141
|
+
"""Claude Code: ~/.claude.json or project .mcp.json."""
|
|
142
|
+
|
|
143
|
+
name = "claude"
|
|
144
|
+
display_name = "Claude Code"
|
|
145
|
+
container_key = "mcpServers"
|
|
146
|
+
|
|
147
|
+
def config_path(self, ctx: SetupContext, scope: str) -> Path:
|
|
148
|
+
if scope == "user":
|
|
149
|
+
return ctx.home / ".claude.json"
|
|
150
|
+
return ctx.project_dir / ".mcp.json"
|
|
151
|
+
|
|
152
|
+
def build_entry(self) -> dict[str, Any]:
|
|
153
|
+
return {"command": SERVER_COMMAND, "args": list(SERVER_ARGS)}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class CursorClient(ClientAdapter):
|
|
157
|
+
"""Cursor: ~/.cursor/mcp.json or project .cursor/mcp.json."""
|
|
158
|
+
|
|
159
|
+
name = "cursor"
|
|
160
|
+
display_name = "Cursor"
|
|
161
|
+
container_key = "mcpServers"
|
|
162
|
+
|
|
163
|
+
def config_path(self, ctx: SetupContext, scope: str) -> Path:
|
|
164
|
+
if scope == "user":
|
|
165
|
+
return ctx.home / ".cursor" / "mcp.json"
|
|
166
|
+
return ctx.project_dir / ".cursor" / "mcp.json"
|
|
167
|
+
|
|
168
|
+
def build_entry(self) -> dict[str, Any]:
|
|
169
|
+
return {"command": SERVER_COMMAND, "args": list(SERVER_ARGS)}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ClineClient(ClientAdapter):
|
|
173
|
+
"""Cline (VS Code extension): global storage settings, user scope only."""
|
|
174
|
+
|
|
175
|
+
name = "cline"
|
|
176
|
+
display_name = "Cline (VS Code)"
|
|
177
|
+
supports_project_scope = False
|
|
178
|
+
container_key = "mcpServers"
|
|
179
|
+
|
|
180
|
+
def config_path(self, ctx: SetupContext, scope: str) -> Path:
|
|
181
|
+
if scope != "user":
|
|
182
|
+
raise SetupError("Cline only supports user scope")
|
|
183
|
+
return (
|
|
184
|
+
ctx.appdata
|
|
185
|
+
/ "Code"
|
|
186
|
+
/ "User"
|
|
187
|
+
/ "globalStorage"
|
|
188
|
+
/ "saoudrizwan.claude-dev"
|
|
189
|
+
/ "settings"
|
|
190
|
+
/ "cline_mcp_settings.json"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def build_entry(self) -> dict[str, Any]:
|
|
194
|
+
return {
|
|
195
|
+
"command": SERVER_COMMAND,
|
|
196
|
+
"args": list(SERVER_ARGS),
|
|
197
|
+
"disabled": False,
|
|
198
|
+
"autoApprove": [],
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class VsCodeClient(ClientAdapter):
|
|
203
|
+
"""VS Code (Copilot): project-scope .vscode/mcp.json only."""
|
|
204
|
+
|
|
205
|
+
name = "vscode"
|
|
206
|
+
display_name = "VS Code (Copilot)"
|
|
207
|
+
supports_user_scope = False
|
|
208
|
+
container_key = "servers"
|
|
209
|
+
|
|
210
|
+
def config_path(self, ctx: SetupContext, scope: str) -> Path:
|
|
211
|
+
if scope != "project":
|
|
212
|
+
raise SetupError("VS Code support writes project-scope .vscode/mcp.json only")
|
|
213
|
+
return ctx.project_dir / ".vscode" / "mcp.json"
|
|
214
|
+
|
|
215
|
+
def build_entry(self) -> dict[str, Any]:
|
|
216
|
+
return {"type": "stdio", "command": SERVER_COMMAND, "args": list(SERVER_ARGS)}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
CLIENTS: dict[str, type[ClientAdapter]] = {
|
|
220
|
+
"claude": ClaudeCodeClient,
|
|
221
|
+
"cursor": CursorClient,
|
|
222
|
+
"cline": ClineClient,
|
|
223
|
+
"vscode": VsCodeClient,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
ALIASES: dict[str, str] = {
|
|
227
|
+
"claude-code": "claude",
|
|
228
|
+
"claude_code": "claude",
|
|
229
|
+
"vs-code": "vscode",
|
|
230
|
+
"code": "vscode",
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def get_client(name: str) -> ClientAdapter:
|
|
235
|
+
"""Return a client adapter by name or alias."""
|
|
236
|
+
key = ALIASES.get(name.lower(), name.lower())
|
|
237
|
+
try:
|
|
238
|
+
return CLIENTS[key]()
|
|
239
|
+
except KeyError as exc:
|
|
240
|
+
options = ", ".join(sorted(CLIENTS))
|
|
241
|
+
raise SetupError(f"unknown client {name!r}; choose from {options}") from exc
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def detected_clients(ctx: SetupContext) -> list[ClientAdapter]:
|
|
245
|
+
"""Return adapters whose configuration files already exist."""
|
|
246
|
+
return [cls() for cls in CLIENTS.values() if cls().detected(ctx)]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def describe_clients(ctx: SetupContext) -> list[dict[str, Any]]:
|
|
250
|
+
"""Return a status row for every supported client and scope."""
|
|
251
|
+
rows: list[dict[str, Any]] = []
|
|
252
|
+
for cls in CLIENTS.values():
|
|
253
|
+
adapter = cls()
|
|
254
|
+
for scope in ("user", "project"):
|
|
255
|
+
if scope == "user" and not adapter.supports_user_scope:
|
|
256
|
+
continue
|
|
257
|
+
if scope == "project" and not adapter.supports_project_scope:
|
|
258
|
+
continue
|
|
259
|
+
path = adapter.config_path(ctx, scope)
|
|
260
|
+
status = "missing"
|
|
261
|
+
if path.exists():
|
|
262
|
+
try:
|
|
263
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
264
|
+
except json.JSONDecodeError:
|
|
265
|
+
status = "invalid JSON"
|
|
266
|
+
else:
|
|
267
|
+
servers = data.get(adapter.container_key) if isinstance(data, dict) else None
|
|
268
|
+
if isinstance(servers, dict) and servers.get(SERVER_KEY) == adapter.build_entry():
|
|
269
|
+
status = "registered"
|
|
270
|
+
elif isinstance(servers, dict) and SERVER_KEY in servers:
|
|
271
|
+
status = "different entry"
|
|
272
|
+
else:
|
|
273
|
+
status = "not registered"
|
|
274
|
+
rows.append(
|
|
275
|
+
{
|
|
276
|
+
"client": adapter.name,
|
|
277
|
+
"display": adapter.display_name,
|
|
278
|
+
"scope": scope,
|
|
279
|
+
"path": str(path),
|
|
280
|
+
"status": status,
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
return rows
|
|
284
|
+
|
|
285
|
+
|
codevanta/setup/plan.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Setup plan model: describe every change before writing anything."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import difflib
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
ACTION_CREATE = "create"
|
|
11
|
+
ACTION_UPDATE = "update"
|
|
12
|
+
ACTION_NOOP = "noop"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class FileChange:
|
|
17
|
+
"""One configuration file change inside a setup plan."""
|
|
18
|
+
|
|
19
|
+
path: Path
|
|
20
|
+
action: str
|
|
21
|
+
previous_content: str
|
|
22
|
+
new_content: str
|
|
23
|
+
diff: str
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def will_write(self) -> bool:
|
|
27
|
+
return self.action in (ACTION_CREATE, ACTION_UPDATE)
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict[str, Any]:
|
|
30
|
+
return {"path": str(self.path), "action": self.action, "diff": self.diff}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class SetupPlan:
|
|
35
|
+
"""The complete set of changes for one client and scope."""
|
|
36
|
+
|
|
37
|
+
client: str
|
|
38
|
+
scope: str
|
|
39
|
+
changes: tuple[FileChange, ...] = field(default_factory=tuple)
|
|
40
|
+
notes: tuple[str, ...] = field(default_factory=tuple)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def has_changes(self) -> bool:
|
|
44
|
+
return any(change.will_write for change in self.changes)
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict[str, Any]:
|
|
47
|
+
return {
|
|
48
|
+
"client": self.client,
|
|
49
|
+
"scope": self.scope,
|
|
50
|
+
"has_changes": self.has_changes,
|
|
51
|
+
"changes": [change.to_dict() for change in self.changes],
|
|
52
|
+
"notes": list(self.notes),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_diff(path: Path, previous: str, new: str) -> str:
|
|
57
|
+
"""Return a unified diff between ``previous`` and ``new``."""
|
|
58
|
+
return "".join(
|
|
59
|
+
difflib.unified_diff(
|
|
60
|
+
previous.splitlines(keepends=True),
|
|
61
|
+
new.splitlines(keepends=True),
|
|
62
|
+
fromfile=f"a/{path.name}",
|
|
63
|
+
tofile=f"b/{path.name}",
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def make_change(path: Path, previous: str, new: str) -> FileChange:
|
|
69
|
+
"""Classify a change as create, update, or noop and attach its diff."""
|
|
70
|
+
if previous == new:
|
|
71
|
+
return FileChange(path=path, action=ACTION_NOOP, previous_content=previous, new_content=new, diff="")
|
|
72
|
+
action = ACTION_UPDATE if previous else ACTION_CREATE
|
|
73
|
+
return FileChange(
|
|
74
|
+
path=path,
|
|
75
|
+
action=action,
|
|
76
|
+
previous_content=previous,
|
|
77
|
+
new_content=new,
|
|
78
|
+
diff=build_diff(path, previous, new),
|
|
79
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Post-setup verification: configuration entry and tool layer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import struct
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .clients import SERVER_KEY, ClientAdapter, SetupContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def verify_entry(adapter: ClientAdapter, ctx: SetupContext, scope: str) -> dict[str, Any]:
|
|
15
|
+
"""Confirm that the client configuration contains the expected entry."""
|
|
16
|
+
path = adapter.config_path(ctx, scope)
|
|
17
|
+
if not path.exists():
|
|
18
|
+
return {"ok": False, "path": str(path), "detail": "configuration file not found"}
|
|
19
|
+
try:
|
|
20
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
21
|
+
except json.JSONDecodeError as exc:
|
|
22
|
+
return {"ok": False, "path": str(path), "detail": f"invalid JSON: {exc.msg}"}
|
|
23
|
+
servers = data.get(adapter.container_key) or {}
|
|
24
|
+
entry = servers.get(SERVER_KEY) if isinstance(servers, dict) else None
|
|
25
|
+
ok = entry == adapter.build_entry()
|
|
26
|
+
detail = "entry present" if ok else "entry missing or different"
|
|
27
|
+
return {"ok": ok, "path": str(path), "detail": detail}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def verify_tools() -> dict[str, Any]:
|
|
31
|
+
"""Run one tool call on a synthetic ELF to prove the tool layer works."""
|
|
32
|
+
from codevanta.mcp_server import tools
|
|
33
|
+
|
|
34
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
35
|
+
sample = Path(tmp) / "sample_elf64.bin"
|
|
36
|
+
ident = b"\x7fELF" + bytes([2, 1, 1, 0]) + bytes(8)
|
|
37
|
+
header = struct.pack(
|
|
38
|
+
"<HHIQQQIHHHHHH", 2, 62, 1, 0x401000, 0, 0, 0, 64, 56, 0, 64, 0, 0
|
|
39
|
+
)
|
|
40
|
+
sample.write_bytes(ident + header)
|
|
41
|
+
info = tools.binary_info(str(sample))
|
|
42
|
+
ok = info.get("format") == "ELF" and info.get("architecture") == "x86_64"
|
|
43
|
+
return {"ok": ok, "detail": "tool layer responded", "format": info.get("format")}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Storage layer (SQLite sessions) — phase 2+."""
|