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.
Files changed (65) hide show
  1. codevanta/__init__.py +1 -0
  2. codevanta/analysis/__init__.py +1 -0
  3. codevanta/analysis/binary_diff.py +1168 -0
  4. codevanta/analysis/callgraph.py +542 -0
  5. codevanta/analysis/cfg.py +1201 -0
  6. codevanta/analysis/dataflow.py +762 -0
  7. codevanta/analysis/debugger.py +289 -0
  8. codevanta/analysis/debuginfo.py +304 -0
  9. codevanta/analysis/decompile.py +1094 -0
  10. codevanta/analysis/decompilers/__init__.py +250 -0
  11. codevanta/analysis/decompilers/angr_backend.py +251 -0
  12. codevanta/analysis/decompilers/base.py +278 -0
  13. codevanta/analysis/decompilers/ghidra_backend.py +316 -0
  14. codevanta/analysis/dynamic.py +756 -0
  15. codevanta/analysis/emulate.py +1349 -0
  16. codevanta/analysis/evidence_bundle.py +736 -0
  17. codevanta/analysis/function_hash.py +562 -0
  18. codevanta/analysis/graph_export.py +441 -0
  19. codevanta/analysis/html_view.py +695 -0
  20. codevanta/analysis/jump_tables.py +546 -0
  21. codevanta/analysis/solver.py +715 -0
  22. codevanta/analysis/symbolic.py +1809 -0
  23. codevanta/analysis/trace.py +646 -0
  24. codevanta/analysis/type_propagation.py +1055 -0
  25. codevanta/analysis/type_recovery.py +1214 -0
  26. codevanta/analysis/vtables.py +572 -0
  27. codevanta/analysis/xrefs.py +434 -0
  28. codevanta/api/__init__.py +1 -0
  29. codevanta/cli/__init__.py +1 -0
  30. codevanta/cli/main.py +3038 -0
  31. codevanta/core/__init__.py +31 -0
  32. codevanta/core/disassembler.py +121 -0
  33. codevanta/core/entropy.py +237 -0
  34. codevanta/core/functions.py +176 -0
  35. codevanta/core/loader.py +1351 -0
  36. codevanta/core/report.py +306 -0
  37. codevanta/core/strings.py +109 -0
  38. codevanta/evidence/__init__.py +7 -0
  39. codevanta/evidence/confidence.py +36 -0
  40. codevanta/evidence/models.py +43 -0
  41. codevanta/evidence/provenance.py +37 -0
  42. codevanta/gui/__init__.py +46 -0
  43. codevanta/gui/layout.py +1384 -0
  44. codevanta/gui/server.py +1305 -0
  45. codevanta/ir/__init__.py +6 -0
  46. codevanta/ir/adapters/__init__.py +31 -0
  47. codevanta/ir/base.py +31 -0
  48. codevanta/ir/instruction.py +42 -0
  49. codevanta/mcp_server/__init__.py +5 -0
  50. codevanta/mcp_server/server.py +710 -0
  51. codevanta/mcp_server/tools.py +1364 -0
  52. codevanta/plugins/__init__.py +748 -0
  53. codevanta/setup/__init__.py +28 -0
  54. codevanta/setup/apply.py +22 -0
  55. codevanta/setup/clients.py +285 -0
  56. codevanta/setup/plan.py +79 -0
  57. codevanta/setup/verify.py +43 -0
  58. codevanta/storage/__init__.py +1 -0
  59. codevanta/storage/session.py +514 -0
  60. codevanta-0.1.0.dist-info/METADATA +688 -0
  61. codevanta-0.1.0.dist-info/RECORD +65 -0
  62. codevanta-0.1.0.dist-info/WHEEL +5 -0
  63. codevanta-0.1.0.dist-info/entry_points.txt +2 -0
  64. codevanta-0.1.0.dist-info/licenses/LICENSE +21 -0
  65. 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
+ ]
@@ -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
+
@@ -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+."""