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
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Contained, bounded context selection with a public outbound manifest."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import stat
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path, PurePosixPath
|
|
12
|
+
from typing import Any, Final
|
|
13
|
+
|
|
14
|
+
from .contracts import TaskRequest
|
|
15
|
+
from .settings import RoutingSettings
|
|
16
|
+
|
|
17
|
+
CONTEXT_SCHEMA_VERSION: Final = "1"
|
|
18
|
+
_REPARSE_POINT = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
|
|
19
|
+
_EXCLUDED_COMPONENTS = frozenset(
|
|
20
|
+
{
|
|
21
|
+
".git",
|
|
22
|
+
".graphite",
|
|
23
|
+
".cache",
|
|
24
|
+
".pytest_cache",
|
|
25
|
+
".mypy_cache",
|
|
26
|
+
".ruff_cache",
|
|
27
|
+
"__pycache__",
|
|
28
|
+
"graph-out",
|
|
29
|
+
"node_modules",
|
|
30
|
+
"dist",
|
|
31
|
+
"build",
|
|
32
|
+
"coverage",
|
|
33
|
+
"vendor",
|
|
34
|
+
".venv",
|
|
35
|
+
"venv",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
_SENSITIVE_SUFFIXES = frozenset({".pem", ".key", ".p12", ".pfx", ".crt", ".cer", ".der"})
|
|
39
|
+
_SENSITIVE_NAMES = frozenset(
|
|
40
|
+
{
|
|
41
|
+
".npmrc",
|
|
42
|
+
".pypirc",
|
|
43
|
+
"credentials",
|
|
44
|
+
"credentials.json",
|
|
45
|
+
"id_rsa",
|
|
46
|
+
"id_ed25519",
|
|
47
|
+
"keystore",
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
_ENCODED_TOKEN = re.compile(r"(?<![A-Za-z0-9+/])[A-Za-z0-9+/]{24,}={0,2}(?![A-Za-z0-9+/])")
|
|
51
|
+
_SECRET_PATTERNS = (
|
|
52
|
+
re.compile(r"(?i)(?:api[_-]?key|secret|token|password)\s*[:=]\s*[^\s]{8,}"),
|
|
53
|
+
re.compile(r"(?i)authorization\s*:\s*bearer\s+[^\s]{8,}"),
|
|
54
|
+
re.compile(r"-----BEGIN (?:OPENSSH |RSA |EC )?PRIVATE KEY-----"),
|
|
55
|
+
re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ContextError(RuntimeError):
|
|
60
|
+
"""A stable, path-free context construction failure."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, code: str) -> None:
|
|
63
|
+
self.code = code
|
|
64
|
+
super().__init__(code)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class ManifestItem:
|
|
69
|
+
path: str
|
|
70
|
+
byte_count: int
|
|
71
|
+
sha256: str
|
|
72
|
+
reason: str
|
|
73
|
+
redaction_count: int = 0
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> dict[str, Any]:
|
|
76
|
+
return {
|
|
77
|
+
"path": self.path,
|
|
78
|
+
"byte_count": self.byte_count,
|
|
79
|
+
"sha256": self.sha256,
|
|
80
|
+
"reason": self.reason,
|
|
81
|
+
"redaction_count": self.redaction_count,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class PrivateContextItem:
|
|
87
|
+
path: str
|
|
88
|
+
content: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class OutboundManifest:
|
|
93
|
+
schema_version: str
|
|
94
|
+
items: tuple[ManifestItem, ...]
|
|
95
|
+
total_bytes: int
|
|
96
|
+
excluded_count: int
|
|
97
|
+
manifest_hash: str
|
|
98
|
+
|
|
99
|
+
def to_dict(self) -> dict[str, Any]:
|
|
100
|
+
return {
|
|
101
|
+
"schema_version": self.schema_version,
|
|
102
|
+
"items": [item.to_dict() for item in self.items],
|
|
103
|
+
"total_bytes": self.total_bytes,
|
|
104
|
+
"excluded_count": self.excluded_count,
|
|
105
|
+
"manifest_hash": self.manifest_hash,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class ContextBundle:
|
|
111
|
+
manifest: OutboundManifest
|
|
112
|
+
private_items: tuple[PrivateContextItem, ...]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _is_reparse(metadata: os.stat_result) -> bool:
|
|
116
|
+
return bool(getattr(metadata, "st_file_attributes", 0) & _REPARSE_POINT)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _signature(metadata: os.stat_result) -> tuple[int, int, int, int]:
|
|
120
|
+
return (
|
|
121
|
+
int(metadata.st_dev),
|
|
122
|
+
int(metadata.st_ino),
|
|
123
|
+
int(metadata.st_size),
|
|
124
|
+
int(metadata.st_mtime_ns),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _normalize_relative(value: object) -> str | None:
|
|
129
|
+
if not isinstance(value, str) or not value or "\x00" in value:
|
|
130
|
+
return None
|
|
131
|
+
normalized = value.replace("\\", "/")
|
|
132
|
+
pure = PurePosixPath(normalized)
|
|
133
|
+
if normalized.startswith("/") or any(part in {"", ".", ".."} for part in pure.parts):
|
|
134
|
+
return None
|
|
135
|
+
if pure.parts and re.match(r"^[A-Za-z]:$", pure.parts[0]):
|
|
136
|
+
return None
|
|
137
|
+
return pure.as_posix()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _candidate_reasons(request: TaskRequest, graph: Any) -> dict[str, str]:
|
|
141
|
+
reasons: dict[str, str] = {target: "explicit_target" for target in request.targets}
|
|
142
|
+
target_set = set(request.targets)
|
|
143
|
+
matched_nodes: list[str] = []
|
|
144
|
+
try:
|
|
145
|
+
nodes = graph.nodes(data=True)
|
|
146
|
+
except (AttributeError, TypeError):
|
|
147
|
+
return reasons
|
|
148
|
+
for node_id, data in nodes:
|
|
149
|
+
if isinstance(data, dict) and data.get("source_file") in target_set:
|
|
150
|
+
matched_nodes.append(str(node_id))
|
|
151
|
+
for node in sorted(set(matched_nodes)):
|
|
152
|
+
try:
|
|
153
|
+
dependencies = sorted(graph.successors(node), key=str)
|
|
154
|
+
dependents = sorted(graph.predecessors(node), key=str)
|
|
155
|
+
except (AttributeError, KeyError):
|
|
156
|
+
continue
|
|
157
|
+
for neighbor, reason in (
|
|
158
|
+
*((item, "dependency") for item in dependencies),
|
|
159
|
+
*((item, "dependent") for item in dependents),
|
|
160
|
+
):
|
|
161
|
+
try:
|
|
162
|
+
data = graph.nodes[neighbor]
|
|
163
|
+
except (AttributeError, KeyError):
|
|
164
|
+
continue
|
|
165
|
+
relative = _normalize_relative(data.get("source_file") if isinstance(data, dict) else None)
|
|
166
|
+
if relative is not None:
|
|
167
|
+
reasons.setdefault(relative, reason)
|
|
168
|
+
return reasons
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _excluded_by_name(relative: str, exclusions: frozenset[str]) -> bool:
|
|
172
|
+
pure = PurePosixPath(relative)
|
|
173
|
+
lowered_parts = tuple(part.casefold() for part in pure.parts)
|
|
174
|
+
name = pure.name.casefold()
|
|
175
|
+
if any(part in _EXCLUDED_COMPONENTS for part in lowered_parts):
|
|
176
|
+
return True
|
|
177
|
+
if (
|
|
178
|
+
name == ".env"
|
|
179
|
+
or name.startswith(".env.")
|
|
180
|
+
or name in _SENSITIVE_NAMES
|
|
181
|
+
or pure.suffix.casefold() in _SENSITIVE_SUFFIXES
|
|
182
|
+
or pure.suffix.casefold() in {".jks", ".kdbx", ".keystore"}
|
|
183
|
+
):
|
|
184
|
+
return True
|
|
185
|
+
return any(relative == exclusion or relative.startswith(exclusion.rstrip("/") + "/") for exclusion in exclusions)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _read_stable(root: Path, relative: str, limit: int) -> bytes | None:
|
|
189
|
+
candidate = root / PurePosixPath(relative)
|
|
190
|
+
current = root
|
|
191
|
+
for part in PurePosixPath(relative).parts[:-1]:
|
|
192
|
+
current /= part
|
|
193
|
+
try:
|
|
194
|
+
metadata = current.lstat()
|
|
195
|
+
except OSError:
|
|
196
|
+
return None
|
|
197
|
+
if stat.S_ISLNK(metadata.st_mode) or _is_reparse(metadata) or not stat.S_ISDIR(metadata.st_mode):
|
|
198
|
+
return None
|
|
199
|
+
try:
|
|
200
|
+
before = candidate.lstat()
|
|
201
|
+
resolved = candidate.resolve(strict=True)
|
|
202
|
+
resolved.relative_to(root)
|
|
203
|
+
except (OSError, ValueError):
|
|
204
|
+
return None
|
|
205
|
+
if stat.S_ISLNK(before.st_mode) or _is_reparse(before) or not stat.S_ISREG(before.st_mode):
|
|
206
|
+
return None
|
|
207
|
+
if before.st_size > limit:
|
|
208
|
+
return None
|
|
209
|
+
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0)
|
|
210
|
+
try:
|
|
211
|
+
descriptor = os.open(candidate, flags)
|
|
212
|
+
try:
|
|
213
|
+
opened = os.fstat(descriptor)
|
|
214
|
+
if _signature(opened) != _signature(before):
|
|
215
|
+
raise ContextError("context_file_changed")
|
|
216
|
+
chunks: list[bytes] = []
|
|
217
|
+
remaining = limit + 1
|
|
218
|
+
while remaining > 0:
|
|
219
|
+
chunk = os.read(descriptor, min(64 * 1024, remaining))
|
|
220
|
+
if not chunk:
|
|
221
|
+
break
|
|
222
|
+
chunks.append(chunk)
|
|
223
|
+
remaining -= len(chunk)
|
|
224
|
+
after = os.fstat(descriptor)
|
|
225
|
+
finally:
|
|
226
|
+
os.close(descriptor)
|
|
227
|
+
except ContextError:
|
|
228
|
+
raise
|
|
229
|
+
except OSError:
|
|
230
|
+
return None
|
|
231
|
+
data = b"".join(chunks)
|
|
232
|
+
if len(data) > limit:
|
|
233
|
+
return None
|
|
234
|
+
if _signature(after) != _signature(opened):
|
|
235
|
+
raise ContextError("context_file_changed")
|
|
236
|
+
return data
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _safe_text(data: bytes) -> str | None:
|
|
240
|
+
if b"\x00" in data:
|
|
241
|
+
return None
|
|
242
|
+
try:
|
|
243
|
+
text = data.decode("utf-8")
|
|
244
|
+
except UnicodeDecodeError:
|
|
245
|
+
return None
|
|
246
|
+
if any(pattern.search(text) for pattern in _SECRET_PATTERNS):
|
|
247
|
+
return None
|
|
248
|
+
for match in _ENCODED_TOKEN.finditer(text):
|
|
249
|
+
try:
|
|
250
|
+
decoded = base64.b64decode(match.group(0), validate=True).decode("utf-8")
|
|
251
|
+
except (ValueError, UnicodeDecodeError):
|
|
252
|
+
continue
|
|
253
|
+
if any(pattern.search(decoded) for pattern in _SECRET_PATTERNS):
|
|
254
|
+
return None
|
|
255
|
+
return text
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def build_routing_context(
|
|
259
|
+
request: TaskRequest,
|
|
260
|
+
graph: Any,
|
|
261
|
+
settings: RoutingSettings,
|
|
262
|
+
*,
|
|
263
|
+
exclusions: tuple[str, ...] = (),
|
|
264
|
+
) -> ContextBundle:
|
|
265
|
+
"""Build private context plus a source-free public approval manifest."""
|
|
266
|
+
root = request.repository_root
|
|
267
|
+
try:
|
|
268
|
+
metadata = root.lstat()
|
|
269
|
+
except OSError as exc:
|
|
270
|
+
raise ContextError("context_root_invalid") from exc
|
|
271
|
+
if stat.S_ISLNK(metadata.st_mode) or _is_reparse(metadata) or not stat.S_ISDIR(metadata.st_mode):
|
|
272
|
+
raise ContextError("context_root_invalid")
|
|
273
|
+
root = root.resolve(strict=True)
|
|
274
|
+
if not 1 <= settings.max_context_files <= 128:
|
|
275
|
+
raise ContextError("context_file_limit_invalid")
|
|
276
|
+
if not 16_384 <= settings.max_context_bytes <= 4_194_304:
|
|
277
|
+
raise ContextError("context_byte_limit_invalid")
|
|
278
|
+
normalized_exclusions = frozenset(
|
|
279
|
+
item for value in exclusions if (item := _normalize_relative(value)) is not None
|
|
280
|
+
)
|
|
281
|
+
reasons = _candidate_reasons(request, graph)
|
|
282
|
+
items: list[ManifestItem] = []
|
|
283
|
+
private: list[PrivateContextItem] = []
|
|
284
|
+
excluded_count = 0
|
|
285
|
+
total_bytes = 0
|
|
286
|
+
for raw_relative in sorted(reasons):
|
|
287
|
+
relative = _normalize_relative(raw_relative)
|
|
288
|
+
if relative is None or _excluded_by_name(relative, normalized_exclusions):
|
|
289
|
+
excluded_count += 1
|
|
290
|
+
continue
|
|
291
|
+
if len(items) >= settings.max_context_files:
|
|
292
|
+
excluded_count += 1
|
|
293
|
+
continue
|
|
294
|
+
remaining = settings.max_context_bytes - total_bytes
|
|
295
|
+
if remaining <= 0:
|
|
296
|
+
excluded_count += 1
|
|
297
|
+
continue
|
|
298
|
+
data = _read_stable(root, relative, remaining)
|
|
299
|
+
if data is None:
|
|
300
|
+
excluded_count += 1
|
|
301
|
+
continue
|
|
302
|
+
text = _safe_text(data)
|
|
303
|
+
if text is None:
|
|
304
|
+
excluded_count += 1
|
|
305
|
+
continue
|
|
306
|
+
digest = hashlib.sha256(data).hexdigest()
|
|
307
|
+
items.append(ManifestItem(relative, len(data), digest, reasons[raw_relative]))
|
|
308
|
+
total_bytes += len(data)
|
|
309
|
+
if request.data_policy == "source_allowed":
|
|
310
|
+
private.append(PrivateContextItem(relative, text))
|
|
311
|
+
material = {
|
|
312
|
+
"schema_version": CONTEXT_SCHEMA_VERSION,
|
|
313
|
+
"items": [item.to_dict() for item in items],
|
|
314
|
+
"total_bytes": total_bytes,
|
|
315
|
+
"excluded_count": excluded_count,
|
|
316
|
+
}
|
|
317
|
+
manifest_hash = hashlib.sha256(
|
|
318
|
+
json.dumps(material, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
319
|
+
).hexdigest()
|
|
320
|
+
manifest = OutboundManifest(
|
|
321
|
+
CONTEXT_SCHEMA_VERSION,
|
|
322
|
+
tuple(items),
|
|
323
|
+
total_bytes,
|
|
324
|
+
excluded_count,
|
|
325
|
+
manifest_hash,
|
|
326
|
+
)
|
|
327
|
+
return ContextBundle(manifest, tuple(private))
|