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,362 @@
|
|
|
1
|
+
"""Default-No consent, HMAC integrity, and single-use quota coordination."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import secrets
|
|
9
|
+
import sqlite3
|
|
10
|
+
from contextlib import closing
|
|
11
|
+
import stat
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Callable, TextIO
|
|
16
|
+
|
|
17
|
+
from .contracts import ApprovalManifest, CliApprovalManifest
|
|
18
|
+
from .route_pool import ApprovedRoutePool
|
|
19
|
+
from .storage import RepositoryStore, StorageError
|
|
20
|
+
|
|
21
|
+
_REPARSE_POINT = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ApprovalError(RuntimeError):
|
|
25
|
+
"""A stable, path-free approval failure."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, code: str) -> None:
|
|
28
|
+
self.code = code
|
|
29
|
+
super().__init__(code)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class SignedApproval:
|
|
34
|
+
manifest: ApprovalManifest | CliApprovalManifest | ApprovedRoutePool
|
|
35
|
+
signature: str
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, object]:
|
|
38
|
+
return {
|
|
39
|
+
"manifest": self.manifest.to_dict(),
|
|
40
|
+
"signature": self.signature,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def approval_prompt(
|
|
45
|
+
*,
|
|
46
|
+
stdin: TextIO,
|
|
47
|
+
stdout: TextIO,
|
|
48
|
+
stdin_is_tty: bool,
|
|
49
|
+
stdout_is_tty: bool,
|
|
50
|
+
json_mode: bool,
|
|
51
|
+
assume_yes: bool,
|
|
52
|
+
ci: bool,
|
|
53
|
+
) -> bool:
|
|
54
|
+
"""Request one explicit interactive approval; every other mode declines."""
|
|
55
|
+
if (
|
|
56
|
+
not stdin_is_tty
|
|
57
|
+
or not stdout_is_tty
|
|
58
|
+
or json_mode
|
|
59
|
+
or assume_yes
|
|
60
|
+
or ci
|
|
61
|
+
):
|
|
62
|
+
return False
|
|
63
|
+
stdout.write("Approve this development model call? [y/N] ")
|
|
64
|
+
stdout.flush()
|
|
65
|
+
try:
|
|
66
|
+
answer = stdin.readline(16)
|
|
67
|
+
except (EOFError, OSError):
|
|
68
|
+
return False
|
|
69
|
+
return answer.strip().casefold() in {"y", "yes"}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _canonical_manifest(
|
|
73
|
+
manifest: ApprovalManifest | CliApprovalManifest | ApprovedRoutePool,
|
|
74
|
+
) -> bytes:
|
|
75
|
+
return json.dumps(
|
|
76
|
+
manifest.to_dict(),
|
|
77
|
+
sort_keys=True,
|
|
78
|
+
separators=(",", ":"),
|
|
79
|
+
).encode("utf-8")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _is_reparse(metadata: os.stat_result) -> bool:
|
|
83
|
+
return bool(getattr(metadata, "st_file_attributes", 0) & _REPARSE_POINT)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _secure_parent(path: Path) -> None:
|
|
87
|
+
try:
|
|
88
|
+
path.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
89
|
+
metadata = path.lstat()
|
|
90
|
+
if stat.S_ISLNK(metadata.st_mode) or _is_reparse(metadata) or not stat.S_ISDIR(metadata.st_mode):
|
|
91
|
+
raise ApprovalError("approval_state_path_invalid")
|
|
92
|
+
if os.name != "nt":
|
|
93
|
+
path.chmod(0o700)
|
|
94
|
+
except ApprovalError:
|
|
95
|
+
raise
|
|
96
|
+
except OSError as exc:
|
|
97
|
+
raise ApprovalError("approval_state_unavailable") from exc
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _load_or_create_key(path: Path, repository_root: Path) -> bytes:
|
|
101
|
+
resolved_parent = path.parent.resolve(strict=False)
|
|
102
|
+
try:
|
|
103
|
+
resolved_parent.relative_to(repository_root)
|
|
104
|
+
except ValueError:
|
|
105
|
+
pass
|
|
106
|
+
else:
|
|
107
|
+
raise ApprovalError("approval_key_path_invalid")
|
|
108
|
+
_secure_parent(path.parent)
|
|
109
|
+
try:
|
|
110
|
+
metadata = path.lstat()
|
|
111
|
+
except FileNotFoundError:
|
|
112
|
+
metadata = None
|
|
113
|
+
except OSError as exc:
|
|
114
|
+
raise ApprovalError("approval_key_unavailable") from exc
|
|
115
|
+
if metadata is None:
|
|
116
|
+
key = secrets.token_bytes(32)
|
|
117
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0)
|
|
118
|
+
try:
|
|
119
|
+
descriptor = os.open(path, flags, 0o600)
|
|
120
|
+
try:
|
|
121
|
+
written = os.write(descriptor, key)
|
|
122
|
+
if written != len(key):
|
|
123
|
+
raise ApprovalError("approval_key_unavailable")
|
|
124
|
+
os.fsync(descriptor)
|
|
125
|
+
finally:
|
|
126
|
+
os.close(descriptor)
|
|
127
|
+
except FileExistsError:
|
|
128
|
+
return _load_or_create_key(path, repository_root)
|
|
129
|
+
except ApprovalError:
|
|
130
|
+
raise
|
|
131
|
+
except OSError as exc:
|
|
132
|
+
raise ApprovalError("approval_key_unavailable") from exc
|
|
133
|
+
if os.name != "nt":
|
|
134
|
+
path.chmod(0o600)
|
|
135
|
+
return key
|
|
136
|
+
if stat.S_ISLNK(metadata.st_mode) or _is_reparse(metadata) or not stat.S_ISREG(metadata.st_mode):
|
|
137
|
+
raise ApprovalError("approval_key_path_invalid")
|
|
138
|
+
if metadata.st_size != 32:
|
|
139
|
+
raise ApprovalError("approval_key_invalid")
|
|
140
|
+
try:
|
|
141
|
+
key = path.read_bytes()
|
|
142
|
+
except OSError as exc:
|
|
143
|
+
raise ApprovalError("approval_key_unavailable") from exc
|
|
144
|
+
if len(key) != 32:
|
|
145
|
+
raise ApprovalError("approval_key_invalid")
|
|
146
|
+
return key
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class _MachineQuotaStore:
|
|
150
|
+
def __init__(self, path: Path, repository_root: Path) -> None:
|
|
151
|
+
resolved = path.resolve(strict=False)
|
|
152
|
+
try:
|
|
153
|
+
resolved.relative_to(repository_root)
|
|
154
|
+
except ValueError:
|
|
155
|
+
pass
|
|
156
|
+
else:
|
|
157
|
+
raise ApprovalError("quota_path_invalid")
|
|
158
|
+
self.path = path
|
|
159
|
+
_secure_parent(path.parent)
|
|
160
|
+
try:
|
|
161
|
+
with closing(sqlite3.connect(path, timeout=2.0)) as connection, connection:
|
|
162
|
+
connection.execute("PRAGMA busy_timeout = 2000")
|
|
163
|
+
connection.execute(
|
|
164
|
+
"""CREATE TABLE IF NOT EXISTS reservations (
|
|
165
|
+
nonce_hash TEXT PRIMARY KEY,
|
|
166
|
+
token_amount INTEGER NOT NULL,
|
|
167
|
+
status TEXT NOT NULL CHECK(status IN ('reserved', 'released', 'settled')),
|
|
168
|
+
created_at INTEGER NOT NULL
|
|
169
|
+
)"""
|
|
170
|
+
)
|
|
171
|
+
except sqlite3.Error as exc:
|
|
172
|
+
raise ApprovalError("quota_unavailable") from exc
|
|
173
|
+
if os.name != "nt":
|
|
174
|
+
path.chmod(0o600)
|
|
175
|
+
|
|
176
|
+
def reserve(self, nonce_hash: str, amount: int, quota: int, created_at: int) -> None:
|
|
177
|
+
connection: sqlite3.Connection | None = None
|
|
178
|
+
try:
|
|
179
|
+
connection = sqlite3.connect(self.path, timeout=2.0, isolation_level=None)
|
|
180
|
+
connection.execute("PRAGMA busy_timeout = 2000")
|
|
181
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
182
|
+
existing = connection.execute(
|
|
183
|
+
"SELECT status FROM reservations WHERE nonce_hash = ?",
|
|
184
|
+
(nonce_hash,),
|
|
185
|
+
).fetchone()
|
|
186
|
+
if existing is not None:
|
|
187
|
+
raise ApprovalError("approval_reused")
|
|
188
|
+
total = int(
|
|
189
|
+
connection.execute(
|
|
190
|
+
"SELECT COALESCE(SUM(token_amount), 0) FROM reservations WHERE status = 'reserved'"
|
|
191
|
+
).fetchone()[0]
|
|
192
|
+
)
|
|
193
|
+
if amount > quota or total + amount > quota:
|
|
194
|
+
raise ApprovalError("budget_exhausted")
|
|
195
|
+
connection.execute(
|
|
196
|
+
"INSERT INTO reservations(nonce_hash, token_amount, status, created_at) VALUES (?, ?, 'reserved', ?)",
|
|
197
|
+
(nonce_hash, amount, created_at),
|
|
198
|
+
)
|
|
199
|
+
connection.commit()
|
|
200
|
+
except ApprovalError:
|
|
201
|
+
if connection is not None and connection.in_transaction:
|
|
202
|
+
connection.rollback()
|
|
203
|
+
raise
|
|
204
|
+
except sqlite3.Error as exc:
|
|
205
|
+
if connection is not None and connection.in_transaction:
|
|
206
|
+
connection.rollback()
|
|
207
|
+
raise ApprovalError("quota_unavailable") from exc
|
|
208
|
+
finally:
|
|
209
|
+
if connection is not None:
|
|
210
|
+
connection.close()
|
|
211
|
+
|
|
212
|
+
def release(self, nonce_hash: str) -> None:
|
|
213
|
+
try:
|
|
214
|
+
with closing(sqlite3.connect(self.path, timeout=2.0)) as connection, connection:
|
|
215
|
+
connection.execute(
|
|
216
|
+
"UPDATE reservations SET status = 'released' WHERE nonce_hash = ? AND status = 'reserved'",
|
|
217
|
+
(nonce_hash,),
|
|
218
|
+
)
|
|
219
|
+
except sqlite3.Error as exc:
|
|
220
|
+
raise ApprovalError("quota_unavailable") from exc
|
|
221
|
+
|
|
222
|
+
def reserved_total(self) -> int:
|
|
223
|
+
try:
|
|
224
|
+
with closing(sqlite3.connect(self.path, timeout=2.0)) as connection, connection:
|
|
225
|
+
return int(
|
|
226
|
+
connection.execute(
|
|
227
|
+
"SELECT COALESCE(SUM(token_amount), 0) FROM reservations WHERE status = 'reserved'"
|
|
228
|
+
).fetchone()[0]
|
|
229
|
+
)
|
|
230
|
+
except sqlite3.Error as exc:
|
|
231
|
+
raise ApprovalError("quota_unavailable") from exc
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class ApprovalAuthority:
|
|
235
|
+
"""Issues and consumes signed approvals.
|
|
236
|
+
|
|
237
|
+
HMAC protects cross-file and accidental tampering. A hostile process running
|
|
238
|
+
as the same operating-system user remains outside this trust boundary.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def __init__(
|
|
242
|
+
self,
|
|
243
|
+
store: RepositoryStore,
|
|
244
|
+
*,
|
|
245
|
+
key_path: Path,
|
|
246
|
+
quota_path: Path,
|
|
247
|
+
now: Callable[[], int] | None = None,
|
|
248
|
+
) -> None:
|
|
249
|
+
self.store = store
|
|
250
|
+
self._key = _load_or_create_key(key_path, store.root)
|
|
251
|
+
self._quota = _MachineQuotaStore(quota_path, store.root)
|
|
252
|
+
self._now = now or (lambda: int(time.time()))
|
|
253
|
+
|
|
254
|
+
def issue(
|
|
255
|
+
self,
|
|
256
|
+
manifest: ApprovalManifest | CliApprovalManifest | ApprovedRoutePool,
|
|
257
|
+
*,
|
|
258
|
+
lifecycle_identity_digest: str | None = None,
|
|
259
|
+
capability_snapshot_digest: str | None = None,
|
|
260
|
+
bound_at: int | None = None,
|
|
261
|
+
) -> SignedApproval:
|
|
262
|
+
lifecycle_values = (
|
|
263
|
+
lifecycle_identity_digest,
|
|
264
|
+
capability_snapshot_digest,
|
|
265
|
+
bound_at,
|
|
266
|
+
)
|
|
267
|
+
if any(value is not None for value in lifecycle_values) and (
|
|
268
|
+
not isinstance(manifest, CliApprovalManifest)
|
|
269
|
+
or lifecycle_identity_digest is None
|
|
270
|
+
or capability_snapshot_digest != manifest.capability_snapshot_digest
|
|
271
|
+
or bound_at is None
|
|
272
|
+
):
|
|
273
|
+
raise ApprovalError("approval_lifecycle_binding_invalid")
|
|
274
|
+
payload = _canonical_manifest(manifest)
|
|
275
|
+
manifest_hash = hashlib.sha256(payload).hexdigest()
|
|
276
|
+
nonce_hash = hashlib.sha256(manifest.nonce.encode("utf-8")).hexdigest()
|
|
277
|
+
signature = hmac.new(self._key, payload, hashlib.sha256).hexdigest()
|
|
278
|
+
try:
|
|
279
|
+
self.store.save_approval_record(
|
|
280
|
+
approval_id=manifest.approval_id,
|
|
281
|
+
task_id=None,
|
|
282
|
+
decision_id=None,
|
|
283
|
+
nonce_hash=nonce_hash,
|
|
284
|
+
manifest_hash=manifest_hash,
|
|
285
|
+
expires_at=manifest.expires_at,
|
|
286
|
+
reserved_tokens=manifest.max_input_tokens + manifest.max_output_tokens,
|
|
287
|
+
)
|
|
288
|
+
except StorageError as exc:
|
|
289
|
+
raise ApprovalError(exc.code) from exc
|
|
290
|
+
if any(value is not None for value in lifecycle_values):
|
|
291
|
+
assert isinstance(manifest, CliApprovalManifest)
|
|
292
|
+
assert lifecycle_identity_digest is not None
|
|
293
|
+
assert capability_snapshot_digest is not None
|
|
294
|
+
assert bound_at is not None
|
|
295
|
+
try:
|
|
296
|
+
self.store.save_lifecycle_approval_binding(
|
|
297
|
+
approval_id=manifest.approval_id,
|
|
298
|
+
capability_snapshot_digest=capability_snapshot_digest,
|
|
299
|
+
lifecycle_identity_digest=lifecycle_identity_digest,
|
|
300
|
+
bound_at=bound_at,
|
|
301
|
+
)
|
|
302
|
+
except (StorageError, ValueError):
|
|
303
|
+
raise ApprovalError("approval_lifecycle_binding_invalid") from None
|
|
304
|
+
return SignedApproval(manifest, signature)
|
|
305
|
+
|
|
306
|
+
def verify(
|
|
307
|
+
self,
|
|
308
|
+
signed: SignedApproval,
|
|
309
|
+
current_manifest: ApprovalManifest | CliApprovalManifest | ApprovedRoutePool,
|
|
310
|
+
) -> None:
|
|
311
|
+
if not isinstance(signed, SignedApproval):
|
|
312
|
+
raise ApprovalError("approval_invalid")
|
|
313
|
+
signed_payload = _canonical_manifest(signed.manifest)
|
|
314
|
+
current_payload = _canonical_manifest(current_manifest)
|
|
315
|
+
if not hmac.compare_digest(signed_payload, current_payload):
|
|
316
|
+
raise ApprovalError("approval_manifest_changed")
|
|
317
|
+
expected = hmac.new(self._key, signed_payload, hashlib.sha256).hexdigest()
|
|
318
|
+
if not hmac.compare_digest(expected, signed.signature):
|
|
319
|
+
raise ApprovalError("approval_signature_invalid")
|
|
320
|
+
|
|
321
|
+
def consume(
|
|
322
|
+
self,
|
|
323
|
+
signed: SignedApproval,
|
|
324
|
+
current_manifest: ApprovalManifest | CliApprovalManifest | ApprovedRoutePool,
|
|
325
|
+
*,
|
|
326
|
+
repository_quota_tokens: int,
|
|
327
|
+
machine_quota_tokens: int,
|
|
328
|
+
lifecycle_identity_digest: str | None = None,
|
|
329
|
+
capability_snapshot_digest: str | None = None,
|
|
330
|
+
) -> None:
|
|
331
|
+
self.verify(signed, current_manifest)
|
|
332
|
+
if lifecycle_identity_digest is not None or capability_snapshot_digest is not None:
|
|
333
|
+
try:
|
|
334
|
+
binding = self.store.lifecycle_approval_binding_details(
|
|
335
|
+
current_manifest.approval_id
|
|
336
|
+
)
|
|
337
|
+
except (StorageError, ValueError):
|
|
338
|
+
raise ApprovalError("approval_lifecycle_stale") from None
|
|
339
|
+
if not isinstance(current_manifest, CliApprovalManifest) or lifecycle_identity_digest is None or capability_snapshot_digest != current_manifest.capability_snapshot_digest or binding != (capability_snapshot_digest, lifecycle_identity_digest):
|
|
340
|
+
raise ApprovalError("approval_lifecycle_stale")
|
|
341
|
+
now = self._now()
|
|
342
|
+
if now >= current_manifest.expires_at:
|
|
343
|
+
raise ApprovalError("approval_expired")
|
|
344
|
+
amount = current_manifest.max_input_tokens + current_manifest.max_output_tokens
|
|
345
|
+
nonce_hash = hashlib.sha256(current_manifest.nonce.encode("utf-8")).hexdigest()
|
|
346
|
+
manifest_hash = hashlib.sha256(_canonical_manifest(current_manifest)).hexdigest()
|
|
347
|
+
self._quota.reserve(nonce_hash, amount, machine_quota_tokens, now)
|
|
348
|
+
try:
|
|
349
|
+
self.store.consume_approval_record(
|
|
350
|
+
approval_id=current_manifest.approval_id,
|
|
351
|
+
nonce_hash=nonce_hash,
|
|
352
|
+
manifest_hash=manifest_hash,
|
|
353
|
+
now=now,
|
|
354
|
+
token_amount=amount,
|
|
355
|
+
repository_quota=repository_quota_tokens,
|
|
356
|
+
)
|
|
357
|
+
except StorageError as exc:
|
|
358
|
+
self._quota.release(nonce_hash)
|
|
359
|
+
raise ApprovalError(exc.code) from exc
|
|
360
|
+
|
|
361
|
+
def machine_reserved_token_total(self) -> int:
|
|
362
|
+
return self._quota.reserved_total()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Deterministic task classification from requests and bounded graph evidence."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from collections import deque
|
|
7
|
+
from typing import Any, Final
|
|
8
|
+
|
|
9
|
+
from .contracts import RiskTier, TaskCategory, TaskProfile, TaskRequest
|
|
10
|
+
|
|
11
|
+
MAX_IMPACT_NODES: Final = 10_000
|
|
12
|
+
|
|
13
|
+
_CATEGORY_RULES: tuple[tuple[TaskCategory, tuple[str, ...]], ...] = (
|
|
14
|
+
(TaskCategory.TENANT_ISOLATION, ("tenant isolation", "cross-tenant", "multi-tenant")),
|
|
15
|
+
(TaskCategory.AUTHORIZATION, ("authorization", "authorisation", "permission", "role access")),
|
|
16
|
+
(TaskCategory.AUTHENTICATION, ("authentication", "login", "sign in", "oauth", "session")),
|
|
17
|
+
(TaskCategory.MIGRATION, ("database migration", "schema migration", "rollback migration")),
|
|
18
|
+
(TaskCategory.DEPLOYMENT, ("deployment", "deploy", "release pipeline", "production release")),
|
|
19
|
+
(TaskCategory.INFRASTRUCTURE, ("infrastructure", "terraform", "kubernetes", "cloud provision")),
|
|
20
|
+
(TaskCategory.CONCURRENCY, ("concurrent", "concurrency", "race condition", "deadlock", "transaction race")),
|
|
21
|
+
(TaskCategory.FINANCIAL, ("financial", "money", "mortgage", "payment", "billing", "invoice")),
|
|
22
|
+
(TaskCategory.LEGAL, ("legal", "contract clause", "compliance policy")),
|
|
23
|
+
(TaskCategory.ARCHITECTURE, ("architecture", "system design", "service boundary")),
|
|
24
|
+
(TaskCategory.REFACTOR, ("refactor", "restructure", "extract module")),
|
|
25
|
+
(TaskCategory.FEATURE, ("feature", "add a", "implement", "customer search", "endpoint")),
|
|
26
|
+
(TaskCategory.ISOLATED_CODE, ("isolated", "formatting helper", "small helper", "typo fix")),
|
|
27
|
+
(TaskCategory.DOCUMENTATION, ("documentation", "readme", "guide", "docs")),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_CATEGORY_RISK: Final[dict[TaskCategory, RiskTier]] = {
|
|
31
|
+
TaskCategory.DOCUMENTATION: RiskTier.LOW,
|
|
32
|
+
TaskCategory.ISOLATED_CODE: RiskTier.LOW,
|
|
33
|
+
TaskCategory.FEATURE: RiskTier.MEDIUM,
|
|
34
|
+
TaskCategory.REFACTOR: RiskTier.MEDIUM,
|
|
35
|
+
TaskCategory.ARCHITECTURE: RiskTier.HIGH,
|
|
36
|
+
TaskCategory.AUTHENTICATION: RiskTier.HIGH,
|
|
37
|
+
TaskCategory.AUTHORIZATION: RiskTier.HIGH,
|
|
38
|
+
TaskCategory.TENANT_ISOLATION: RiskTier.HIGH,
|
|
39
|
+
TaskCategory.MIGRATION: RiskTier.HIGH,
|
|
40
|
+
TaskCategory.DEPLOYMENT: RiskTier.HIGH,
|
|
41
|
+
TaskCategory.INFRASTRUCTURE: RiskTier.HIGH,
|
|
42
|
+
TaskCategory.CONCURRENCY: RiskTier.HIGH,
|
|
43
|
+
TaskCategory.FINANCIAL: RiskTier.HIGH,
|
|
44
|
+
TaskCategory.LEGAL: RiskTier.HIGH,
|
|
45
|
+
TaskCategory.UNKNOWN: RiskTier.HIGH,
|
|
46
|
+
}
|
|
47
|
+
_RISK_ORDER = {RiskTier.LOW: 0, RiskTier.MEDIUM: 1, RiskTier.HIGH: 2}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _detect_category(objective: str) -> TaskCategory:
|
|
51
|
+
normalized = " ".join(objective.casefold().split())
|
|
52
|
+
for category, keywords in _CATEGORY_RULES:
|
|
53
|
+
if any(keyword in normalized for keyword in keywords):
|
|
54
|
+
return category
|
|
55
|
+
return TaskCategory.UNKNOWN
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _target_nodes(graph: Any, targets: tuple[str, ...]) -> list[str]:
|
|
59
|
+
selected: list[str] = []
|
|
60
|
+
target_set = set(targets)
|
|
61
|
+
try:
|
|
62
|
+
nodes = graph.nodes(data=True)
|
|
63
|
+
except (AttributeError, TypeError):
|
|
64
|
+
return []
|
|
65
|
+
for node_id, data in nodes:
|
|
66
|
+
if isinstance(data, dict) and data.get("source_file") in target_set:
|
|
67
|
+
selected.append(str(node_id))
|
|
68
|
+
return sorted(set(selected))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _bounded_impact(graph: Any, starts: list[str]) -> tuple[set[str], bool]:
|
|
72
|
+
visited = set(starts)
|
|
73
|
+
queue = deque(starts)
|
|
74
|
+
truncated = False
|
|
75
|
+
while queue:
|
|
76
|
+
node = queue.popleft()
|
|
77
|
+
try:
|
|
78
|
+
neighbors = set(graph.predecessors(node)) | set(graph.successors(node))
|
|
79
|
+
except (AttributeError, KeyError):
|
|
80
|
+
continue
|
|
81
|
+
for neighbor in sorted(neighbors, key=str):
|
|
82
|
+
normalized = str(neighbor)
|
|
83
|
+
if normalized in visited:
|
|
84
|
+
continue
|
|
85
|
+
if len(visited) >= MAX_IMPACT_NODES:
|
|
86
|
+
truncated = True
|
|
87
|
+
return visited, truncated
|
|
88
|
+
visited.add(normalized)
|
|
89
|
+
queue.append(normalized)
|
|
90
|
+
return visited, truncated
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def classify_task(request: TaskRequest, graph: Any) -> TaskProfile:
|
|
94
|
+
"""Classify a request using fixed rules and bounded graph features."""
|
|
95
|
+
detected = _detect_category(request.objective)
|
|
96
|
+
category = detected
|
|
97
|
+
detected_risk = _CATEGORY_RISK[detected]
|
|
98
|
+
if request.category_hint is not None:
|
|
99
|
+
hint_risk = _CATEGORY_RISK[request.category_hint]
|
|
100
|
+
if _RISK_ORDER[hint_risk] > _RISK_ORDER[detected_risk]:
|
|
101
|
+
category = request.category_hint
|
|
102
|
+
risk = _CATEGORY_RISK[category]
|
|
103
|
+
|
|
104
|
+
starts = _target_nodes(graph, request.targets)
|
|
105
|
+
impacted, truncated = _bounded_impact(graph, starts)
|
|
106
|
+
flags: list[str] = []
|
|
107
|
+
communities: set[object] = set()
|
|
108
|
+
tests_nearby = False
|
|
109
|
+
for node in sorted(impacted):
|
|
110
|
+
try:
|
|
111
|
+
data = graph.nodes[node]
|
|
112
|
+
except (AttributeError, KeyError):
|
|
113
|
+
continue
|
|
114
|
+
source_file = data.get("source_file") if isinstance(data, dict) else None
|
|
115
|
+
if isinstance(source_file, str) and (
|
|
116
|
+
source_file.startswith("tests/")
|
|
117
|
+
or ".test." in source_file
|
|
118
|
+
or ".spec." in source_file
|
|
119
|
+
or source_file.endswith("_test.py")
|
|
120
|
+
):
|
|
121
|
+
tests_nearby = True
|
|
122
|
+
if isinstance(data, dict) and data.get("community") is not None:
|
|
123
|
+
communities.add(data["community"])
|
|
124
|
+
if tests_nearby:
|
|
125
|
+
flags.append("tests_nearby")
|
|
126
|
+
if len(communities) > 1:
|
|
127
|
+
flags.append("community_crossing")
|
|
128
|
+
if len(impacted) >= 25 or truncated:
|
|
129
|
+
flags.append("broad_impact")
|
|
130
|
+
if risk is RiskTier.LOW:
|
|
131
|
+
risk = RiskTier.MEDIUM
|
|
132
|
+
if detected is TaskCategory.UNKNOWN:
|
|
133
|
+
flags.append("unknown_scope")
|
|
134
|
+
if risk is RiskTier.HIGH:
|
|
135
|
+
flags.append("high_risk_category")
|
|
136
|
+
|
|
137
|
+
verification = ["tests"]
|
|
138
|
+
if risk is RiskTier.HIGH:
|
|
139
|
+
verification.extend(("security_review", "manual_frontier_review"))
|
|
140
|
+
if category is TaskCategory.MIGRATION:
|
|
141
|
+
verification.append("rollback_test")
|
|
142
|
+
complexity = min(
|
|
143
|
+
10,
|
|
144
|
+
max(1, 1 + len(request.targets) + len(impacted) // 5 + _RISK_ORDER[risk] * 2),
|
|
145
|
+
)
|
|
146
|
+
task_material = json.dumps(
|
|
147
|
+
{
|
|
148
|
+
"objective": request.objective,
|
|
149
|
+
"targets": list(request.targets),
|
|
150
|
+
"category": category.value,
|
|
151
|
+
"risk": risk.value,
|
|
152
|
+
},
|
|
153
|
+
sort_keys=True,
|
|
154
|
+
separators=(",", ":"),
|
|
155
|
+
).encode("utf-8")
|
|
156
|
+
task_id = "task-" + hashlib.sha256(task_material).hexdigest()[:24]
|
|
157
|
+
context_requirements = ["explicit_targets"]
|
|
158
|
+
if impacted:
|
|
159
|
+
context_requirements.append("dependency_neighbors")
|
|
160
|
+
return TaskProfile(
|
|
161
|
+
task_id=task_id,
|
|
162
|
+
category=category,
|
|
163
|
+
risk=risk,
|
|
164
|
+
complexity=complexity,
|
|
165
|
+
impact_radius=len(impacted),
|
|
166
|
+
risk_flags=tuple(flags),
|
|
167
|
+
context_requirements=tuple(context_requirements),
|
|
168
|
+
verification_requirements=tuple(verification),
|
|
169
|
+
)
|