graphite-code 0.3.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.
- graphite/__init__.py +41 -0
- graphite/__main__.py +7 -0
- graphite/_cleanup_worker.py +525 -0
- graphite/activation.py +164 -0
- graphite/agent_hooks.py +577 -0
- graphite/agent_settings.py +226 -0
- graphite/analyze.py +146 -0
- graphite/answer_contract.py +420 -0
- graphite/bootstrap.py +210 -0
- graphite/buildlock.py +99 -0
- graphite/cache.py +131 -0
- graphite/channel.py +1325 -0
- graphite/cli.py +3053 -0
- graphite/cluster.py +111 -0
- graphite/config.py +209 -0
- graphite/context.py +355 -0
- graphite/daemon.py +745 -0
- graphite/daemon_health.py +733 -0
- graphite/debt.py +118 -0
- graphite/dependency_install.py +1597 -0
- graphite/detach.py +33 -0
- graphite/doctor.py +678 -0
- graphite/doctor_probes.py +2100 -0
- graphite/engine_identity.py +238 -0
- graphite/export/__init__.py +6 -0
- graphite/export/html.py +244 -0
- graphite/export/json.py +39 -0
- graphite/export/md.py +68 -0
- graphite/extract/__init__.py +4 -0
- graphite/extract/ast.py +1964 -0
- graphite/freshness.py +127 -0
- graphite/git.py +406 -0
- graphite/graph.py +117 -0
- graphite/graph_io.py +188 -0
- graphite/health.py +147 -0
- graphite/hook_entry.py +68 -0
- graphite/hookinstall.py +224 -0
- graphite/hookshim.py +86 -0
- graphite/incident_ledger.py +247 -0
- graphite/ingest.py +279 -0
- graphite/init.py +791 -0
- graphite/io.py +32 -0
- graphite/listing.py +51 -0
- graphite/llm.py +518 -0
- graphite/llm_probe.py +157 -0
- graphite/mcp.py +7 -0
- graphite/mcp_server.py +450 -0
- graphite/natural_query.py +252 -0
- graphite/overlays.py +713 -0
- graphite/probe_process.py +879 -0
- graphite/probe_workspace.py +728 -0
- graphite/process_contracts.py +22 -0
- graphite/provider_observer.py +397 -0
- graphite/query.py +646 -0
- graphite/query_plan.py +97 -0
- graphite/replacement_audit.py +291 -0
- graphite/resolve.py +660 -0
- graphite/review.py +782 -0
- graphite/routing/__init__.py +5 -0
- graphite/routing/approval.py +362 -0
- graphite/routing/classifier.py +169 -0
- graphite/routing/claude_executor.py +419 -0
- graphite/routing/claude_probe.py +102 -0
- graphite/routing/cli_identity.py +84 -0
- graphite/routing/codex_executor.py +383 -0
- graphite/routing/codex_probe.py +93 -0
- graphite/routing/context_builder.py +327 -0
- graphite/routing/contracts.py +802 -0
- graphite/routing/diff_policy.py +468 -0
- graphite/routing/edit_apply.py +166 -0
- graphite/routing/effort.py +43 -0
- graphite/routing/lifecycle.py +771 -0
- graphite/routing/lifecycle_operator.py +227 -0
- graphite/routing/lifecycle_service.py +555 -0
- graphite/routing/lifecycle_storage.py +977 -0
- graphite/routing/ollama_executor.py +341 -0
- graphite/routing/ollama_probe.py +72 -0
- graphite/routing/openrouter_executor.py +338 -0
- graphite/routing/openrouter_probe.py +188 -0
- graphite/routing/policy.py +815 -0
- graphite/routing/probe_runner.py +543 -0
- graphite/routing/process_runner.py +523 -0
- graphite/routing/profiles.py +554 -0
- graphite/routing/prompt.py +58 -0
- graphite/routing/registry.py +444 -0
- graphite/routing/route_pool.py +629 -0
- graphite/routing/route_pool_execution.py +275 -0
- graphite/routing/schema_validation.py +169 -0
- graphite/routing/service.py +1263 -0
- graphite/routing/settings.py +99 -0
- graphite/routing/shadow.py +201 -0
- graphite/routing/storage.py +4001 -0
- graphite/routing/telemetry.py +346 -0
- graphite/routing/worktree.py +259 -0
- graphite/routing/zai_edit.py +113 -0
- graphite/routing/zai_executor.py +191 -0
- graphite/routing/zai_probe.py +126 -0
- graphite/savings.py +84 -0
- graphite/ts_bridge.py +142 -0
- graphite/ts_resolver.mjs +314 -0
- graphite/typescript_activation.py +1586 -0
- graphite/usage_ledger.py +156 -0
- graphite/validation.py +148 -0
- graphite/watch.py +167 -0
- graphite/windows_job.py +368 -0
- graphite/windows_startup.py +144 -0
- graphite/windows_task.py +212 -0
- graphite_code-0.3.0.dist-info/METADATA +743 -0
- graphite_code-0.3.0.dist-info/RECORD +112 -0
- graphite_code-0.3.0.dist-info/WHEEL +4 -0
- graphite_code-0.3.0.dist-info/entry_points.txt +3 -0
- graphite_code-0.3.0.dist-info/licenses/LICENSE +21 -0
graphite/overlays.py
ADDED
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
"""Non-authoritative, identity-bound storage for optional graph enrichment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import stat
|
|
10
|
+
import tempfile
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Callable, Final
|
|
14
|
+
from urllib.parse import urlsplit
|
|
15
|
+
|
|
16
|
+
from .config import Config
|
|
17
|
+
from .freshness import FreshnessLimitError, check_graph_freshness
|
|
18
|
+
from .graph_io import GraphReadError, load_validated_graph_bundle
|
|
19
|
+
from .llm import enrich_report
|
|
20
|
+
|
|
21
|
+
OVERLAY_SCHEMA_VERSION: Final = 1
|
|
22
|
+
MAX_OVERLAY_FILE_BYTES: Final = 1024 * 1024
|
|
23
|
+
MAX_SUMMARY_BYTES: Final = 64 * 1024
|
|
24
|
+
MAX_CANONICAL_MANIFEST_BYTES: Final = 16 * 1024 * 1024
|
|
25
|
+
|
|
26
|
+
_HEX_64 = re.compile(r"^[0-9a-f]{64}$")
|
|
27
|
+
_REPARSE_POINT = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
|
|
28
|
+
_PROVIDERS = frozenset({"ollama", "openrouter"})
|
|
29
|
+
_FAILURE_CATEGORIES = frozenset(
|
|
30
|
+
{"authentication", "configuration", "connection", "provider_error", "timeout"}
|
|
31
|
+
)
|
|
32
|
+
_IDENTITY_FIELDS = (
|
|
33
|
+
"overlay_identity_digest",
|
|
34
|
+
"provider",
|
|
35
|
+
"provider_lifecycle_identity_digest",
|
|
36
|
+
"model_identity_digest",
|
|
37
|
+
"routing_policy_digest",
|
|
38
|
+
)
|
|
39
|
+
_PAYLOAD_FILE = re.compile(r"^payload-([0-9a-f]{64})\.json$")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OverlayError(RuntimeError):
|
|
43
|
+
"""A stable, path-free overlay failure."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _digest(value: object, code: str, *, optional: bool = False) -> str | None:
|
|
47
|
+
if value is None and optional:
|
|
48
|
+
return None
|
|
49
|
+
if not isinstance(value, str) or _HEX_64.fullmatch(value) is None:
|
|
50
|
+
raise ValueError(code)
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _canonical_digest(value: object) -> str:
|
|
55
|
+
encoded = json.dumps(
|
|
56
|
+
value, sort_keys=True, separators=(",", ":"), ensure_ascii=True
|
|
57
|
+
).encode("utf-8")
|
|
58
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _is_reparse(metadata: os.stat_result) -> bool:
|
|
62
|
+
return bool(getattr(metadata, "st_file_attributes", 0) & _REPARSE_POINT)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_link_or_reparse(metadata: os.stat_result) -> bool:
|
|
66
|
+
return stat.S_ISLNK(metadata.st_mode) or _is_reparse(metadata)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class OverlayRequest:
|
|
71
|
+
repository_root: Path
|
|
72
|
+
output_dir: Path
|
|
73
|
+
provider: str
|
|
74
|
+
provider_lifecycle_identity_digest: str
|
|
75
|
+
model_identity_digest: str
|
|
76
|
+
routing_policy_digest: str | None
|
|
77
|
+
created_at: int
|
|
78
|
+
|
|
79
|
+
def __post_init__(self) -> None:
|
|
80
|
+
if not isinstance(self.repository_root, Path) or not self.repository_root.is_absolute():
|
|
81
|
+
raise ValueError("repository_root_invalid")
|
|
82
|
+
if not isinstance(self.output_dir, Path):
|
|
83
|
+
raise ValueError("output_dir_invalid")
|
|
84
|
+
if self.provider not in _PROVIDERS:
|
|
85
|
+
raise ValueError("provider_invalid")
|
|
86
|
+
lifecycle = _digest(
|
|
87
|
+
self.provider_lifecycle_identity_digest,
|
|
88
|
+
"provider_identity_digest_invalid",
|
|
89
|
+
)
|
|
90
|
+
model = _digest(self.model_identity_digest, "model_identity_digest_invalid")
|
|
91
|
+
routing = _digest(
|
|
92
|
+
self.routing_policy_digest,
|
|
93
|
+
"routing_policy_digest_invalid",
|
|
94
|
+
optional=True,
|
|
95
|
+
)
|
|
96
|
+
if self.provider == "openrouter" and routing is None:
|
|
97
|
+
raise ValueError("routing_policy_digest_invalid")
|
|
98
|
+
if self.provider != "openrouter" and routing is not None:
|
|
99
|
+
raise ValueError("routing_policy_digest_invalid")
|
|
100
|
+
if (
|
|
101
|
+
isinstance(self.created_at, bool)
|
|
102
|
+
or not isinstance(self.created_at, int)
|
|
103
|
+
or not 0 <= self.created_at <= 10**12
|
|
104
|
+
):
|
|
105
|
+
raise ValueError("created_at_invalid")
|
|
106
|
+
object.__setattr__(self, "provider_lifecycle_identity_digest", lifecycle)
|
|
107
|
+
object.__setattr__(self, "model_identity_digest", model)
|
|
108
|
+
object.__setattr__(self, "routing_policy_digest", routing)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def overlay_identity_digest(self) -> str:
|
|
112
|
+
return _canonical_digest(
|
|
113
|
+
{
|
|
114
|
+
"provider": self.provider,
|
|
115
|
+
"provider_lifecycle_identity_digest": self.provider_lifecycle_identity_digest,
|
|
116
|
+
"model_identity_digest": self.model_identity_digest,
|
|
117
|
+
"routing_policy_digest": self.routing_policy_digest,
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _lexical_output_root(request: OverlayRequest) -> Path:
|
|
123
|
+
root = request.repository_root
|
|
124
|
+
output = request.output_dir if request.output_dir.is_absolute() else root / request.output_dir
|
|
125
|
+
lexical = output.absolute()
|
|
126
|
+
try:
|
|
127
|
+
relative = lexical.relative_to(root)
|
|
128
|
+
except ValueError as exc:
|
|
129
|
+
raise OverlayError("overlay_output_outside_root") from exc
|
|
130
|
+
if ".." in relative.parts:
|
|
131
|
+
raise OverlayError("overlay_output_outside_root")
|
|
132
|
+
return lexical
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def overlay_directory(request: OverlayRequest) -> Path:
|
|
136
|
+
"""Return the identity-derived overlay directory without touching storage."""
|
|
137
|
+
return (
|
|
138
|
+
_lexical_output_root(request)
|
|
139
|
+
/ "overlays"
|
|
140
|
+
/ request.provider
|
|
141
|
+
/ request.overlay_identity_digest
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _validate_root(root: Path) -> None:
|
|
146
|
+
try:
|
|
147
|
+
metadata = root.lstat()
|
|
148
|
+
resolved = root.resolve(strict=True)
|
|
149
|
+
except OSError as exc:
|
|
150
|
+
raise OverlayError("overlay_root_invalid") from exc
|
|
151
|
+
if _is_link_or_reparse(metadata) or not stat.S_ISDIR(metadata.st_mode) or resolved != root:
|
|
152
|
+
raise OverlayError("overlay_root_invalid")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _validate_directory(path: Path) -> None:
|
|
156
|
+
try:
|
|
157
|
+
metadata = path.lstat()
|
|
158
|
+
except OSError as exc:
|
|
159
|
+
raise OverlayError("overlay_path_unavailable") from exc
|
|
160
|
+
if _is_link_or_reparse(metadata):
|
|
161
|
+
raise OverlayError("overlay_path_reparse")
|
|
162
|
+
if not stat.S_ISDIR(metadata.st_mode):
|
|
163
|
+
raise OverlayError("overlay_path_invalid")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _prepare_directory(request: OverlayRequest) -> Path:
|
|
167
|
+
root = request.repository_root
|
|
168
|
+
_validate_root(root)
|
|
169
|
+
output = _lexical_output_root(request)
|
|
170
|
+
_validate_directory(output)
|
|
171
|
+
current = output
|
|
172
|
+
for part in ("overlays", request.provider, request.overlay_identity_digest):
|
|
173
|
+
current /= part
|
|
174
|
+
if not current.exists() and not current.is_symlink():
|
|
175
|
+
try:
|
|
176
|
+
if os.name == "nt":
|
|
177
|
+
from .probe_workspace import _create_private_windows_directory
|
|
178
|
+
|
|
179
|
+
_create_private_windows_directory(current.parent, current.name)
|
|
180
|
+
else:
|
|
181
|
+
current.mkdir(mode=0o700)
|
|
182
|
+
except FileExistsError:
|
|
183
|
+
pass
|
|
184
|
+
except OSError as exc:
|
|
185
|
+
raise OverlayError("overlay_path_unavailable") from exc
|
|
186
|
+
_validate_directory(current)
|
|
187
|
+
if os.name == "nt":
|
|
188
|
+
try:
|
|
189
|
+
from .probe_workspace import _set_private_windows_dacl
|
|
190
|
+
|
|
191
|
+
_set_private_windows_dacl(current)
|
|
192
|
+
except OSError as exc:
|
|
193
|
+
raise OverlayError("overlay_permissions_failed") from exc
|
|
194
|
+
else:
|
|
195
|
+
try:
|
|
196
|
+
os.chmod(current, 0o700)
|
|
197
|
+
except OSError as exc:
|
|
198
|
+
raise OverlayError("overlay_permissions_failed") from exc
|
|
199
|
+
return current
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _validate_existing_storage_path(request: OverlayRequest) -> None:
|
|
203
|
+
"""Reject an existing hostile component before processing provider config."""
|
|
204
|
+
root = request.repository_root
|
|
205
|
+
_validate_root(root)
|
|
206
|
+
current = _lexical_output_root(request)
|
|
207
|
+
_validate_directory(current)
|
|
208
|
+
for part in ("overlays", request.provider, request.overlay_identity_digest):
|
|
209
|
+
current /= part
|
|
210
|
+
if not current.exists() and not current.is_symlink():
|
|
211
|
+
break
|
|
212
|
+
_validate_directory(current)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _read_json(path: Path) -> dict[str, Any]:
|
|
216
|
+
try:
|
|
217
|
+
metadata = path.lstat()
|
|
218
|
+
except FileNotFoundError:
|
|
219
|
+
raise OverlayError("overlay_missing") from None
|
|
220
|
+
except OSError as exc:
|
|
221
|
+
raise OverlayError("overlay_unreadable") from exc
|
|
222
|
+
if _is_link_or_reparse(metadata):
|
|
223
|
+
raise OverlayError("overlay_path_reparse")
|
|
224
|
+
if not stat.S_ISREG(metadata.st_mode):
|
|
225
|
+
raise OverlayError("overlay_path_invalid")
|
|
226
|
+
if metadata.st_size > MAX_OVERLAY_FILE_BYTES:
|
|
227
|
+
raise OverlayError("overlay_too_large")
|
|
228
|
+
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
229
|
+
try:
|
|
230
|
+
descriptor = os.open(path, flags)
|
|
231
|
+
try:
|
|
232
|
+
opened = os.fstat(descriptor)
|
|
233
|
+
chunks: list[bytes] = []
|
|
234
|
+
remaining = MAX_OVERLAY_FILE_BYTES + 1
|
|
235
|
+
while remaining > 0:
|
|
236
|
+
chunk = os.read(descriptor, min(64 * 1024, remaining))
|
|
237
|
+
if not chunk:
|
|
238
|
+
break
|
|
239
|
+
chunks.append(chunk)
|
|
240
|
+
remaining -= len(chunk)
|
|
241
|
+
raw = b"".join(chunks)
|
|
242
|
+
after = os.fstat(descriptor)
|
|
243
|
+
finally:
|
|
244
|
+
os.close(descriptor)
|
|
245
|
+
except OSError as exc:
|
|
246
|
+
raise OverlayError("overlay_unreadable") from exc
|
|
247
|
+
def signature(item: os.stat_result) -> tuple[int, int, int, int]:
|
|
248
|
+
return (item.st_dev, item.st_ino, item.st_size, item.st_mtime_ns)
|
|
249
|
+
if signature(opened) != signature(metadata) or signature(after) != signature(opened):
|
|
250
|
+
raise OverlayError("overlay_changed")
|
|
251
|
+
if len(raw) > MAX_OVERLAY_FILE_BYTES:
|
|
252
|
+
raise OverlayError("overlay_too_large")
|
|
253
|
+
try:
|
|
254
|
+
payload = json.loads(raw.decode("utf-8"))
|
|
255
|
+
except (UnicodeDecodeError, json.JSONDecodeError, RecursionError) as exc:
|
|
256
|
+
raise OverlayError("overlay_invalid") from exc
|
|
257
|
+
if not isinstance(payload, dict):
|
|
258
|
+
raise OverlayError("overlay_invalid")
|
|
259
|
+
return payload
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _read_bytes(path: Path) -> bytes:
|
|
263
|
+
try:
|
|
264
|
+
metadata = path.lstat()
|
|
265
|
+
except FileNotFoundError:
|
|
266
|
+
raise OverlayError("overlay_missing") from None
|
|
267
|
+
except OSError as exc:
|
|
268
|
+
raise OverlayError("overlay_unreadable") from exc
|
|
269
|
+
if _is_link_or_reparse(metadata):
|
|
270
|
+
raise OverlayError("overlay_path_reparse")
|
|
271
|
+
if not stat.S_ISREG(metadata.st_mode):
|
|
272
|
+
raise OverlayError("overlay_path_invalid")
|
|
273
|
+
if metadata.st_size > MAX_OVERLAY_FILE_BYTES:
|
|
274
|
+
raise OverlayError("overlay_too_large")
|
|
275
|
+
try:
|
|
276
|
+
raw = path.read_bytes()
|
|
277
|
+
except OSError as exc:
|
|
278
|
+
raise OverlayError("overlay_unreadable") from exc
|
|
279
|
+
if len(raw) > MAX_OVERLAY_FILE_BYTES:
|
|
280
|
+
raise OverlayError("overlay_too_large")
|
|
281
|
+
return raw
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _json_bytes(value: dict[str, Any]) -> bytes:
|
|
285
|
+
return (
|
|
286
|
+
json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
287
|
+
+ "\n"
|
|
288
|
+
).encode("utf-8")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _atomic_write_secure(path: Path, payload: dict[str, Any]) -> str:
|
|
292
|
+
raw = _json_bytes(payload)
|
|
293
|
+
if len(raw) > MAX_OVERLAY_FILE_BYTES:
|
|
294
|
+
raise OverlayError("overlay_too_large")
|
|
295
|
+
_validate_directory(path.parent)
|
|
296
|
+
if path.exists() or path.is_symlink():
|
|
297
|
+
try:
|
|
298
|
+
existing = path.lstat()
|
|
299
|
+
except OSError as exc:
|
|
300
|
+
raise OverlayError("overlay_write_failed") from exc
|
|
301
|
+
if _is_link_or_reparse(existing):
|
|
302
|
+
raise OverlayError("overlay_path_reparse")
|
|
303
|
+
if not stat.S_ISREG(existing.st_mode):
|
|
304
|
+
raise OverlayError("overlay_path_invalid")
|
|
305
|
+
try:
|
|
306
|
+
descriptor, temporary_name = tempfile.mkstemp(
|
|
307
|
+
prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent)
|
|
308
|
+
)
|
|
309
|
+
except OSError as exc:
|
|
310
|
+
raise OverlayError("overlay_write_failed") from exc
|
|
311
|
+
temporary = Path(temporary_name)
|
|
312
|
+
try:
|
|
313
|
+
if hasattr(os, "fchmod"):
|
|
314
|
+
os.fchmod(descriptor, 0o600)
|
|
315
|
+
with os.fdopen(descriptor, "wb") as handle:
|
|
316
|
+
descriptor = -1
|
|
317
|
+
handle.write(raw)
|
|
318
|
+
handle.flush()
|
|
319
|
+
os.fsync(handle.fileno())
|
|
320
|
+
_validate_directory(path.parent)
|
|
321
|
+
os.replace(temporary, path)
|
|
322
|
+
if os.name == "nt":
|
|
323
|
+
from .probe_workspace import _set_private_windows_dacl
|
|
324
|
+
|
|
325
|
+
_set_private_windows_dacl(path)
|
|
326
|
+
else:
|
|
327
|
+
os.chmod(path, 0o600)
|
|
328
|
+
except Exception as exc:
|
|
329
|
+
if descriptor >= 0:
|
|
330
|
+
os.close(descriptor)
|
|
331
|
+
try:
|
|
332
|
+
temporary.unlink(missing_ok=True)
|
|
333
|
+
finally:
|
|
334
|
+
if isinstance(exc, OverlayError):
|
|
335
|
+
raise
|
|
336
|
+
raise OverlayError("overlay_write_failed") from exc
|
|
337
|
+
return hashlib.sha256(raw).hexdigest()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def canonical_graph_fingerprint(path: Path, *, root: Path) -> str:
|
|
341
|
+
"""Hash the validated canonical bundle using routing's canonical JSON contract."""
|
|
342
|
+
try:
|
|
343
|
+
bundle, _ = load_validated_graph_bundle(path, root=root)
|
|
344
|
+
except GraphReadError as exc:
|
|
345
|
+
raise OverlayError(f"canonical_{exc.code}") from exc
|
|
346
|
+
return _canonical_digest(bundle)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _canonical_bundle(request: OverlayRequest, cfg: Config) -> tuple[dict[str, Any], str]:
|
|
350
|
+
output = _lexical_output_root(request)
|
|
351
|
+
manifest_path = output / ".graphite_manifest.json"
|
|
352
|
+
try:
|
|
353
|
+
manifest_metadata = manifest_path.lstat()
|
|
354
|
+
except OSError as exc:
|
|
355
|
+
raise OverlayError("canonical_graph_missing") from exc
|
|
356
|
+
if _is_link_or_reparse(manifest_metadata):
|
|
357
|
+
raise OverlayError("canonical_graph_reparse")
|
|
358
|
+
if (
|
|
359
|
+
not stat.S_ISREG(manifest_metadata.st_mode)
|
|
360
|
+
or manifest_metadata.st_size > MAX_CANONICAL_MANIFEST_BYTES
|
|
361
|
+
):
|
|
362
|
+
raise OverlayError("canonical_graph_invalid")
|
|
363
|
+
selected = cfg.canonical_graph()
|
|
364
|
+
selected.output_dir = output
|
|
365
|
+
try:
|
|
366
|
+
freshness = check_graph_freshness(
|
|
367
|
+
request.repository_root,
|
|
368
|
+
selected,
|
|
369
|
+
max_manifest_bytes=MAX_CANONICAL_MANIFEST_BYTES,
|
|
370
|
+
)
|
|
371
|
+
except FreshnessLimitError as exc:
|
|
372
|
+
raise OverlayError("canonical_graph_invalid") from exc
|
|
373
|
+
if freshness.get("stale", True):
|
|
374
|
+
raise OverlayError("canonical_graph_stale")
|
|
375
|
+
try:
|
|
376
|
+
bundle, _ = load_validated_graph_bundle(
|
|
377
|
+
output / "graph.json", root=request.repository_root
|
|
378
|
+
)
|
|
379
|
+
except GraphReadError as exc:
|
|
380
|
+
raise OverlayError(f"canonical_{exc.code}") from exc
|
|
381
|
+
return bundle, _canonical_digest(bundle)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _validate_model_config(request: OverlayRequest, cfg: Config) -> None:
|
|
385
|
+
if cfg.llm_provider.strip().casefold().replace("_", "-") != request.provider:
|
|
386
|
+
raise OverlayError("overlay_provider_mismatch")
|
|
387
|
+
expected_mode = "local" if request.provider == "ollama" else "cloud"
|
|
388
|
+
if cfg.llm_mode != expected_mode:
|
|
389
|
+
raise OverlayError("overlay_mode_invalid")
|
|
390
|
+
if (
|
|
391
|
+
not isinstance(cfg.llm_model, str)
|
|
392
|
+
or not cfg.llm_model
|
|
393
|
+
or len(cfg.llm_model) > 256
|
|
394
|
+
or any(ord(character) < 32 for character in cfg.llm_model)
|
|
395
|
+
):
|
|
396
|
+
raise OverlayError("overlay_model_invalid")
|
|
397
|
+
if (
|
|
398
|
+
not math.isfinite(cfg.llm_timeout_seconds)
|
|
399
|
+
or not 0 < cfg.llm_timeout_seconds <= 300
|
|
400
|
+
or isinstance(cfg.llm_max_input_chars, bool)
|
|
401
|
+
or not 1 <= cfg.llm_max_input_chars <= 1_000_000
|
|
402
|
+
or isinstance(cfg.llm_max_output_tokens, bool)
|
|
403
|
+
or not 1 <= cfg.llm_max_output_tokens <= 4_096
|
|
404
|
+
):
|
|
405
|
+
raise OverlayError("overlay_limits_invalid")
|
|
406
|
+
if request.provider == "ollama":
|
|
407
|
+
endpoint = cfg.llm_base_url or "http://localhost:11434"
|
|
408
|
+
parsed = urlsplit(endpoint)
|
|
409
|
+
if (
|
|
410
|
+
parsed.scheme != "http"
|
|
411
|
+
or parsed.hostname not in {"localhost", "127.0.0.1", "::1"}
|
|
412
|
+
or parsed.username is not None
|
|
413
|
+
or parsed.password is not None
|
|
414
|
+
or parsed.path not in {"", "/"}
|
|
415
|
+
or parsed.query
|
|
416
|
+
or parsed.fragment
|
|
417
|
+
):
|
|
418
|
+
raise OverlayError("overlay_endpoint_invalid")
|
|
419
|
+
elif cfg.llm_base_url is not None and cfg.llm_base_url.rstrip("/") != "https://openrouter.ai/api/v1":
|
|
420
|
+
raise OverlayError("overlay_endpoint_invalid")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _identity_values(request: OverlayRequest) -> dict[str, Any]:
|
|
424
|
+
return {
|
|
425
|
+
"overlay_identity_digest": request.overlay_identity_digest,
|
|
426
|
+
"provider": request.provider,
|
|
427
|
+
"provider_lifecycle_identity_digest": request.provider_lifecycle_identity_digest,
|
|
428
|
+
"model_identity_digest": request.model_identity_digest,
|
|
429
|
+
"routing_policy_digest": request.routing_policy_digest,
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _check_collision(directory: Path, request: OverlayRequest) -> None:
|
|
434
|
+
manifest_path = directory / "manifest.json"
|
|
435
|
+
if not manifest_path.exists() and not manifest_path.is_symlink():
|
|
436
|
+
return
|
|
437
|
+
try:
|
|
438
|
+
existing = _read_json(manifest_path)
|
|
439
|
+
except OverlayError as exc:
|
|
440
|
+
raise OverlayError("overlay_identity_collision") from exc
|
|
441
|
+
expected = _identity_values(request)
|
|
442
|
+
if any(existing.get(field) != expected[field] for field in _IDENTITY_FIELDS):
|
|
443
|
+
raise OverlayError("overlay_identity_collision")
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _validate_success_manifest(manifest: dict[str, Any]) -> None:
|
|
447
|
+
try:
|
|
448
|
+
provider = manifest["provider"]
|
|
449
|
+
payload_file = manifest["payload_file"]
|
|
450
|
+
payload_digest = manifest["payload_sha256"]
|
|
451
|
+
created_at = manifest["created_at"]
|
|
452
|
+
limits = manifest["limits"]
|
|
453
|
+
except KeyError as exc:
|
|
454
|
+
raise OverlayError("overlay_invalid") from exc
|
|
455
|
+
payload_match = _PAYLOAD_FILE.fullmatch(payload_file) if isinstance(payload_file, str) else None
|
|
456
|
+
if (
|
|
457
|
+
manifest.get("schema_version") != OVERLAY_SCHEMA_VERSION
|
|
458
|
+
or manifest.get("non_authoritative") is not True
|
|
459
|
+
or manifest.get("outcome_category") != "succeeded"
|
|
460
|
+
or provider not in _PROVIDERS
|
|
461
|
+
or payload_match is None
|
|
462
|
+
or isinstance(created_at, bool)
|
|
463
|
+
or not isinstance(created_at, int)
|
|
464
|
+
or not 0 <= created_at <= 10**12
|
|
465
|
+
or not isinstance(limits, dict)
|
|
466
|
+
or set(limits)
|
|
467
|
+
!= {"timeout_seconds", "max_input_chars", "max_output_tokens"}
|
|
468
|
+
):
|
|
469
|
+
raise OverlayError("overlay_invalid")
|
|
470
|
+
try:
|
|
471
|
+
normalized_payload_digest = _digest(
|
|
472
|
+
payload_digest, "payload_digest_invalid"
|
|
473
|
+
)
|
|
474
|
+
except ValueError as exc:
|
|
475
|
+
raise OverlayError("overlay_invalid") from exc
|
|
476
|
+
if normalized_payload_digest != payload_match.group(1):
|
|
477
|
+
raise OverlayError("overlay_invalid")
|
|
478
|
+
for field, code in (
|
|
479
|
+
("canonical_graph_fingerprint", "canonical_graph_fingerprint_invalid"),
|
|
480
|
+
("overlay_identity_digest", "overlay_identity_digest_invalid"),
|
|
481
|
+
("provider_lifecycle_identity_digest", "provider_identity_digest_invalid"),
|
|
482
|
+
("model_identity_digest", "model_identity_digest_invalid"),
|
|
483
|
+
):
|
|
484
|
+
try:
|
|
485
|
+
_digest(manifest.get(field), code)
|
|
486
|
+
except ValueError as exc:
|
|
487
|
+
raise OverlayError("overlay_invalid") from exc
|
|
488
|
+
routing = manifest.get("routing_policy_digest")
|
|
489
|
+
try:
|
|
490
|
+
_digest(routing, "routing_policy_digest_invalid", optional=True)
|
|
491
|
+
except ValueError as exc:
|
|
492
|
+
raise OverlayError("overlay_invalid") from exc
|
|
493
|
+
if (provider == "openrouter") != (routing is not None):
|
|
494
|
+
raise OverlayError("overlay_invalid")
|
|
495
|
+
timeout = limits["timeout_seconds"]
|
|
496
|
+
max_input = limits["max_input_chars"]
|
|
497
|
+
max_output = limits["max_output_tokens"]
|
|
498
|
+
if (
|
|
499
|
+
isinstance(timeout, bool)
|
|
500
|
+
or not isinstance(timeout, (int, float))
|
|
501
|
+
or not math.isfinite(timeout)
|
|
502
|
+
or not 0 < timeout <= 300
|
|
503
|
+
or isinstance(max_input, bool)
|
|
504
|
+
or not isinstance(max_input, int)
|
|
505
|
+
or not 1 <= max_input <= 1_000_000
|
|
506
|
+
or isinstance(max_output, bool)
|
|
507
|
+
or not isinstance(max_output, int)
|
|
508
|
+
or not 1 <= max_output <= 4_096
|
|
509
|
+
):
|
|
510
|
+
raise OverlayError("overlay_invalid")
|
|
511
|
+
expected_identity = _canonical_digest(
|
|
512
|
+
{
|
|
513
|
+
"provider": provider,
|
|
514
|
+
"provider_lifecycle_identity_digest": manifest[
|
|
515
|
+
"provider_lifecycle_identity_digest"
|
|
516
|
+
],
|
|
517
|
+
"model_identity_digest": manifest["model_identity_digest"],
|
|
518
|
+
"routing_policy_digest": routing,
|
|
519
|
+
}
|
|
520
|
+
)
|
|
521
|
+
if manifest["overlay_identity_digest"] != expected_identity:
|
|
522
|
+
raise OverlayError("overlay_invalid")
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _failure_category(result: dict[str, Any]) -> str:
|
|
526
|
+
category = result.get("error_category")
|
|
527
|
+
return category if category in _FAILURE_CATEGORIES else "provider_error"
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _result_payload(result: dict[str, Any]) -> dict[str, Any] | None:
|
|
531
|
+
summary = result.get("summary")
|
|
532
|
+
values = (
|
|
533
|
+
result.get("input_tokens", 0),
|
|
534
|
+
result.get("output_tokens", 0),
|
|
535
|
+
result.get("tokens", 0),
|
|
536
|
+
)
|
|
537
|
+
if (
|
|
538
|
+
not isinstance(summary, str)
|
|
539
|
+
or not summary
|
|
540
|
+
or len(summary.encode("utf-8")) > MAX_SUMMARY_BYTES
|
|
541
|
+
or any(isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= 10**9 for value in values)
|
|
542
|
+
):
|
|
543
|
+
return None
|
|
544
|
+
return {
|
|
545
|
+
"schema_version": OVERLAY_SCHEMA_VERSION,
|
|
546
|
+
"non_authoritative": True,
|
|
547
|
+
"summary": summary,
|
|
548
|
+
"input_tokens": values[0],
|
|
549
|
+
"output_tokens": values[1],
|
|
550
|
+
"total_tokens": values[2],
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _base_record(request: OverlayRequest, canonical_fingerprint: str) -> dict[str, Any]:
|
|
555
|
+
return {
|
|
556
|
+
"schema_version": OVERLAY_SCHEMA_VERSION,
|
|
557
|
+
"non_authoritative": True,
|
|
558
|
+
"canonical_graph_fingerprint": canonical_fingerprint,
|
|
559
|
+
**_identity_values(request),
|
|
560
|
+
"created_at": request.created_at,
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _write_failure(
|
|
565
|
+
directory: Path,
|
|
566
|
+
request: OverlayRequest,
|
|
567
|
+
canonical_fingerprint: str,
|
|
568
|
+
category: str,
|
|
569
|
+
) -> dict[str, Any]:
|
|
570
|
+
marker = {
|
|
571
|
+
**_base_record(request, canonical_fingerprint),
|
|
572
|
+
"outcome_category": "failed",
|
|
573
|
+
"failure_category": category if category in _FAILURE_CATEGORIES else "provider_error",
|
|
574
|
+
}
|
|
575
|
+
_atomic_write_secure(directory / "failure.json", marker)
|
|
576
|
+
return marker
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def build_overlay(
|
|
580
|
+
request: OverlayRequest,
|
|
581
|
+
cfg: Config,
|
|
582
|
+
*,
|
|
583
|
+
enrich: Callable[[dict[str, Any], dict[str, Any], dict[str, Any], Config], dict[str, Any]] | None = None,
|
|
584
|
+
) -> dict[str, Any]:
|
|
585
|
+
"""Build one optional overlay without mutating canonical graph artifacts."""
|
|
586
|
+
_validate_existing_storage_path(request)
|
|
587
|
+
_validate_model_config(request, cfg)
|
|
588
|
+
bundle, canonical_fingerprint = _canonical_bundle(request, cfg)
|
|
589
|
+
directory = _prepare_directory(request)
|
|
590
|
+
_check_collision(directory, request)
|
|
591
|
+
|
|
592
|
+
if request.provider == "openrouter" and not cfg.llm_api_key:
|
|
593
|
+
return _write_failure(
|
|
594
|
+
directory, request, canonical_fingerprint, "authentication"
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
graph_data = {
|
|
598
|
+
"nodes": bundle.get("nodes", []),
|
|
599
|
+
"edges": bundle.get("edges", []),
|
|
600
|
+
"metadata": {
|
|
601
|
+
"node_count": (bundle.get("metadata") or {}).get("node_count", 0),
|
|
602
|
+
"edge_count": (bundle.get("metadata") or {}).get("edge_count", 0),
|
|
603
|
+
},
|
|
604
|
+
}
|
|
605
|
+
clusters = {
|
|
606
|
+
"clusters": bundle.get("clusters", []),
|
|
607
|
+
"count": (bundle.get("metadata") or {}).get("community_count", 0),
|
|
608
|
+
}
|
|
609
|
+
analysis = bundle.get("analysis") or {}
|
|
610
|
+
runner = enrich or enrich_report
|
|
611
|
+
try:
|
|
612
|
+
result = runner(graph_data, clusters, analysis, cfg)
|
|
613
|
+
except Exception:
|
|
614
|
+
result = {"status": "error", "error_category": "provider_error"}
|
|
615
|
+
if not isinstance(result, dict) or result.get("status") != "ok":
|
|
616
|
+
selected = result if isinstance(result, dict) else {}
|
|
617
|
+
return _write_failure(
|
|
618
|
+
directory,
|
|
619
|
+
request,
|
|
620
|
+
canonical_fingerprint,
|
|
621
|
+
_failure_category(selected),
|
|
622
|
+
)
|
|
623
|
+
payload = _result_payload(result)
|
|
624
|
+
if payload is None:
|
|
625
|
+
return _write_failure(
|
|
626
|
+
directory, request, canonical_fingerprint, "provider_error"
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
_, current_fingerprint = _canonical_bundle(request, cfg)
|
|
630
|
+
if current_fingerprint != canonical_fingerprint:
|
|
631
|
+
raise OverlayError("canonical_graph_changed")
|
|
632
|
+
|
|
633
|
+
payload_bytes = _json_bytes(payload)
|
|
634
|
+
payload_digest = hashlib.sha256(payload_bytes).hexdigest()
|
|
635
|
+
payload_name = f"payload-{payload_digest}.json"
|
|
636
|
+
payload_path = directory / payload_name
|
|
637
|
+
if payload_path.exists() or payload_path.is_symlink():
|
|
638
|
+
if _read_bytes(payload_path) != payload_bytes:
|
|
639
|
+
raise OverlayError("overlay_payload_collision")
|
|
640
|
+
written_digest = payload_digest
|
|
641
|
+
else:
|
|
642
|
+
written_digest = _atomic_write_secure(payload_path, payload)
|
|
643
|
+
if written_digest != payload_digest:
|
|
644
|
+
raise OverlayError("overlay_write_failed")
|
|
645
|
+
manifest = {
|
|
646
|
+
**_base_record(request, canonical_fingerprint),
|
|
647
|
+
"limits": {
|
|
648
|
+
"timeout_seconds": float(cfg.llm_timeout_seconds),
|
|
649
|
+
"max_input_chars": cfg.llm_max_input_chars,
|
|
650
|
+
"max_output_tokens": cfg.llm_max_output_tokens,
|
|
651
|
+
},
|
|
652
|
+
"outcome_category": "succeeded",
|
|
653
|
+
"payload_file": payload_name,
|
|
654
|
+
"payload_sha256": payload_digest,
|
|
655
|
+
}
|
|
656
|
+
_atomic_write_secure(directory / "manifest.json", manifest)
|
|
657
|
+
return manifest
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def evaluate_overlay_staleness(
|
|
661
|
+
manifest: dict[str, Any],
|
|
662
|
+
*,
|
|
663
|
+
canonical_graph_fingerprint: str,
|
|
664
|
+
provider_lifecycle_identity_digest: str,
|
|
665
|
+
model_identity_digest: str,
|
|
666
|
+
routing_policy_digest: str | None,
|
|
667
|
+
) -> dict[str, Any]:
|
|
668
|
+
"""Evaluate opt-in overlay freshness without affecting canonical freshness."""
|
|
669
|
+
_validate_success_manifest(manifest)
|
|
670
|
+
current_graph = _digest(
|
|
671
|
+
canonical_graph_fingerprint, "canonical_graph_fingerprint_invalid"
|
|
672
|
+
)
|
|
673
|
+
current_provider = _digest(
|
|
674
|
+
provider_lifecycle_identity_digest, "provider_identity_digest_invalid"
|
|
675
|
+
)
|
|
676
|
+
current_model = _digest(model_identity_digest, "model_identity_digest_invalid")
|
|
677
|
+
current_routing = _digest(
|
|
678
|
+
routing_policy_digest, "routing_policy_digest_invalid", optional=True
|
|
679
|
+
)
|
|
680
|
+
reasons: list[str] = []
|
|
681
|
+
if manifest.get("canonical_graph_fingerprint") != current_graph:
|
|
682
|
+
reasons.append("canonical_graph_changed")
|
|
683
|
+
if manifest.get("model_identity_digest") != current_model:
|
|
684
|
+
reasons.append("model_identity_changed")
|
|
685
|
+
if manifest.get("provider_lifecycle_identity_digest") != current_provider:
|
|
686
|
+
reasons.append("provider_identity_changed")
|
|
687
|
+
if manifest.get("routing_policy_digest") != current_routing:
|
|
688
|
+
reasons.append("routing_policy_changed")
|
|
689
|
+
return {
|
|
690
|
+
"status": "stale" if reasons else "current",
|
|
691
|
+
"stale_reasons": sorted(reasons),
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def load_overlay_manifest(request: OverlayRequest) -> dict[str, Any]:
|
|
696
|
+
"""Opt in to reading a bounded manifest for one exact overlay identity."""
|
|
697
|
+
_validate_existing_storage_path(request)
|
|
698
|
+
directory = overlay_directory(request)
|
|
699
|
+
manifest = _read_json(directory / "manifest.json")
|
|
700
|
+
_validate_success_manifest(manifest)
|
|
701
|
+
expected = _identity_values(request)
|
|
702
|
+
if any(manifest.get(field) != expected[field] for field in _IDENTITY_FIELDS):
|
|
703
|
+
raise OverlayError("overlay_identity_collision")
|
|
704
|
+
payload = _read_bytes(directory / manifest["payload_file"])
|
|
705
|
+
if hashlib.sha256(payload).hexdigest() != manifest["payload_sha256"]:
|
|
706
|
+
raise OverlayError("overlay_invalid")
|
|
707
|
+
try:
|
|
708
|
+
decoded = json.loads(payload.decode("utf-8"))
|
|
709
|
+
except (UnicodeDecodeError, json.JSONDecodeError, RecursionError) as exc:
|
|
710
|
+
raise OverlayError("overlay_invalid") from exc
|
|
711
|
+
if not isinstance(decoded, dict):
|
|
712
|
+
raise OverlayError("overlay_invalid")
|
|
713
|
+
return manifest
|