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,4001 @@
|
|
|
1
|
+
"""SQLite evidence storage with repository and aggregate trust boundaries."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import secrets
|
|
10
|
+
import sqlite3
|
|
11
|
+
import stat
|
|
12
|
+
from contextlib import closing, contextmanager, suppress
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Final
|
|
17
|
+
|
|
18
|
+
from .contracts import (
|
|
19
|
+
CapabilitySnapshot,
|
|
20
|
+
EvidenceProvenance,
|
|
21
|
+
Effort,
|
|
22
|
+
ExecutionOutcome,
|
|
23
|
+
ExecutionReceipt,
|
|
24
|
+
PermissionMode,
|
|
25
|
+
ProviderId,
|
|
26
|
+
RiskTier,
|
|
27
|
+
TaskCategory,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
SCHEMA_VERSION: Final = "8"
|
|
31
|
+
BUSY_TIMEOUT_MS: Final = 2_000
|
|
32
|
+
DEFAULT_RECOVERY_PAGE_SIZE: Final = 50
|
|
33
|
+
MAX_RECOVERY_PAGE_SIZE: Final = 100
|
|
34
|
+
_REPARSE_POINT = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
|
|
35
|
+
_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$")
|
|
36
|
+
_MODEL_IDENTIFIER = re.compile(
|
|
37
|
+
r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}(/[A-Za-z0-9][A-Za-z0-9._:+-]{0,127})?$"
|
|
38
|
+
)
|
|
39
|
+
_MODEL_ID = re.compile(r"^[a-z0-9][a-z0-9._-]{0,127}(?::cloud)?$")
|
|
40
|
+
_SNAPSHOT_MODEL_ID = re.compile(
|
|
41
|
+
r"^[a-z0-9][a-z0-9._-]{0,127}(?::[a-z0-9][a-z0-9._-]{0,63})?$"
|
|
42
|
+
)
|
|
43
|
+
_CAPABILITY = re.compile(r"^[a-z][a-z0-9_-]{0,31}$")
|
|
44
|
+
_VERSION = re.compile(r"^[a-z0-9][a-z0-9._-]{0,31}$")
|
|
45
|
+
_HEX_64 = re.compile(r"^[0-9a-f]{64}$")
|
|
46
|
+
_GIT_OBJECT = re.compile(r"^(?:[0-9a-f]{40}|[0-9a-f]{64})$")
|
|
47
|
+
_SEMANTIC_VERSION = re.compile(
|
|
48
|
+
r"^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)"
|
|
49
|
+
r"(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?(?:\+[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$"
|
|
50
|
+
)
|
|
51
|
+
_CLI_TELEMETRY_KEYS: Final = frozenset(
|
|
52
|
+
{
|
|
53
|
+
"provider", "requested_model", "effective_model", "effort",
|
|
54
|
+
"capability_snapshot_digest", "category", "risk", "latency_ms",
|
|
55
|
+
"input_tokens", "output_tokens", "cost_status", "changed_file_count",
|
|
56
|
+
"changed_byte_count", "validation_outcome", "review_defect_classes",
|
|
57
|
+
"rework_count", "human_verdict", "provenance", "observed_at",
|
|
58
|
+
"diff_sha256",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
_CLI_POLICY_KEYS: Final = frozenset(
|
|
62
|
+
{
|
|
63
|
+
"schema_version", "autonomy", "allowed_providers", "permission_ceiling",
|
|
64
|
+
"risk_ceilings", "scoring_weights", "minimum_samples",
|
|
65
|
+
"recency_half_life_days", "source_policy_version",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
_TABLES = frozenset(
|
|
69
|
+
{
|
|
70
|
+
"tasks",
|
|
71
|
+
"decisions",
|
|
72
|
+
"approvals",
|
|
73
|
+
"executions",
|
|
74
|
+
"outcomes",
|
|
75
|
+
"shadow_comparisons",
|
|
76
|
+
"policy_versions",
|
|
77
|
+
"budget_ledger",
|
|
78
|
+
"confidence_stats",
|
|
79
|
+
"registry_snapshots",
|
|
80
|
+
"execution_evidence",
|
|
81
|
+
"execution_attempts",
|
|
82
|
+
"execution_receipts",
|
|
83
|
+
"staged_execution_receipts",
|
|
84
|
+
"incident_reviews",
|
|
85
|
+
"blind_comparisons",
|
|
86
|
+
"capability_snapshots",
|
|
87
|
+
"task_worktrees",
|
|
88
|
+
"cli_execution_attempts",
|
|
89
|
+
"validation_results",
|
|
90
|
+
"review_links",
|
|
91
|
+
"cli_telemetry_events",
|
|
92
|
+
"cli_policy_versions",
|
|
93
|
+
"cli_policy_events",
|
|
94
|
+
"lifecycle_snapshot_bindings",
|
|
95
|
+
"lifecycle_approval_bindings",
|
|
96
|
+
"lifecycle_attempt_bindings",
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class StorageError(RuntimeError):
|
|
102
|
+
"""A stable, path-free persistence failure."""
|
|
103
|
+
|
|
104
|
+
def __init__(self, code: str) -> None:
|
|
105
|
+
self.code = code
|
|
106
|
+
super().__init__(code)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _is_reparse_point(metadata: os.stat_result) -> bool:
|
|
110
|
+
return bool(getattr(metadata, "st_file_attributes", 0) & _REPARSE_POINT)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _selected_root(root: Path) -> Path:
|
|
114
|
+
try:
|
|
115
|
+
metadata = root.lstat()
|
|
116
|
+
except OSError as exc:
|
|
117
|
+
raise StorageError("repository_root_invalid") from exc
|
|
118
|
+
if (
|
|
119
|
+
not stat.S_ISDIR(metadata.st_mode)
|
|
120
|
+
or stat.S_ISLNK(metadata.st_mode)
|
|
121
|
+
or _is_reparse_point(metadata)
|
|
122
|
+
):
|
|
123
|
+
raise StorageError("repository_root_invalid")
|
|
124
|
+
try:
|
|
125
|
+
return root.resolve(strict=True)
|
|
126
|
+
except OSError as exc:
|
|
127
|
+
raise StorageError("repository_root_invalid") from exc
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _identifier(value: object, code: str) -> str:
|
|
131
|
+
if not isinstance(value, str) or not _IDENTIFIER.fullmatch(value):
|
|
132
|
+
raise ValueError(code)
|
|
133
|
+
return value
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _model_identifier(value: object, code: str) -> str:
|
|
137
|
+
"""Model names additionally permit one OpenRouter-style vendor/model slash."""
|
|
138
|
+
if not isinstance(value, str) or not _MODEL_IDENTIFIER.fullmatch(value):
|
|
139
|
+
raise ValueError(code)
|
|
140
|
+
return value
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _nonnegative_integer(value: object, code: str, maximum: int = 10**12) -> int:
|
|
144
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0 or value > maximum:
|
|
145
|
+
raise ValueError(code)
|
|
146
|
+
return value
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _positive_integer(value: object, code: str, maximum: int = 10**12) -> int:
|
|
150
|
+
normalized = _nonnegative_integer(value, code, maximum)
|
|
151
|
+
if normalized == 0:
|
|
152
|
+
raise ValueError(code)
|
|
153
|
+
return normalized
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _semantic_version(value: object, code: str) -> str:
|
|
157
|
+
if not isinstance(value, str) or _SEMANTIC_VERSION.fullmatch(value) is None:
|
|
158
|
+
raise ValueError(code)
|
|
159
|
+
return value
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _translate_database_error(exc: sqlite3.Error) -> StorageError:
|
|
163
|
+
message = str(exc).casefold()
|
|
164
|
+
for code in (
|
|
165
|
+
"lifecycle_snapshot_binding_missing",
|
|
166
|
+
"lifecycle_approval_binding_missing",
|
|
167
|
+
"lifecycle_snapshot_binding_immutable",
|
|
168
|
+
"lifecycle_approval_binding_immutable",
|
|
169
|
+
"lifecycle_attempt_binding_immutable",
|
|
170
|
+
"lifecycle_binding_carry_immutable",
|
|
171
|
+
):
|
|
172
|
+
if code in message:
|
|
173
|
+
return StorageError(code)
|
|
174
|
+
if "locked" in message or "busy" in message:
|
|
175
|
+
return StorageError("storage_locked")
|
|
176
|
+
if "database disk image is malformed" in message or "not a database" in message:
|
|
177
|
+
return StorageError("storage_corrupt")
|
|
178
|
+
return StorageError("storage_unavailable")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _secure_directory(path: Path) -> None:
|
|
182
|
+
try:
|
|
183
|
+
path.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
184
|
+
metadata = path.lstat()
|
|
185
|
+
if (
|
|
186
|
+
not stat.S_ISDIR(metadata.st_mode)
|
|
187
|
+
or stat.S_ISLNK(metadata.st_mode)
|
|
188
|
+
or _is_reparse_point(metadata)
|
|
189
|
+
):
|
|
190
|
+
raise StorageError("storage_path_invalid")
|
|
191
|
+
if os.name != "nt":
|
|
192
|
+
path.chmod(0o700)
|
|
193
|
+
except StorageError:
|
|
194
|
+
raise
|
|
195
|
+
except OSError as exc:
|
|
196
|
+
raise StorageError("storage_unavailable") from exc
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _secure_repository_directory(root: Path, path: Path) -> None:
|
|
200
|
+
try:
|
|
201
|
+
relative = path.relative_to(root)
|
|
202
|
+
except ValueError as exc:
|
|
203
|
+
raise StorageError("storage_path_invalid") from exc
|
|
204
|
+
current = root
|
|
205
|
+
for part in relative.parts:
|
|
206
|
+
current /= part
|
|
207
|
+
try:
|
|
208
|
+
metadata = current.lstat()
|
|
209
|
+
except FileNotFoundError:
|
|
210
|
+
metadata = None
|
|
211
|
+
except OSError as exc:
|
|
212
|
+
raise StorageError("storage_unavailable") from exc
|
|
213
|
+
if metadata is not None:
|
|
214
|
+
if (
|
|
215
|
+
not stat.S_ISDIR(metadata.st_mode)
|
|
216
|
+
or stat.S_ISLNK(metadata.st_mode)
|
|
217
|
+
or _is_reparse_point(metadata)
|
|
218
|
+
):
|
|
219
|
+
raise StorageError("storage_path_invalid")
|
|
220
|
+
else:
|
|
221
|
+
try:
|
|
222
|
+
current.mkdir(mode=0o700)
|
|
223
|
+
except OSError as exc:
|
|
224
|
+
raise StorageError("storage_unavailable") from exc
|
|
225
|
+
if os.name != "nt":
|
|
226
|
+
try:
|
|
227
|
+
current.chmod(0o700)
|
|
228
|
+
except OSError as exc:
|
|
229
|
+
raise StorageError("storage_unavailable") from exc
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _validate_database_file(path: Path) -> None:
|
|
233
|
+
try:
|
|
234
|
+
metadata = path.lstat()
|
|
235
|
+
except FileNotFoundError:
|
|
236
|
+
return
|
|
237
|
+
except OSError as exc:
|
|
238
|
+
raise StorageError("storage_unavailable") from exc
|
|
239
|
+
if (
|
|
240
|
+
not stat.S_ISREG(metadata.st_mode)
|
|
241
|
+
or stat.S_ISLNK(metadata.st_mode)
|
|
242
|
+
or _is_reparse_point(metadata)
|
|
243
|
+
):
|
|
244
|
+
raise StorageError("storage_path_invalid")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _secure_file(path: Path) -> None:
|
|
248
|
+
if os.name == "nt":
|
|
249
|
+
return
|
|
250
|
+
try:
|
|
251
|
+
path.chmod(0o600)
|
|
252
|
+
except OSError as exc:
|
|
253
|
+
raise StorageError("storage_unavailable") from exc
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
_CAPABILITY_SNAPSHOTS_DDL: Final = """CREATE TABLE IF NOT EXISTS capability_snapshots (
|
|
257
|
+
capability_snapshot_digest TEXT PRIMARY KEY CHECK(
|
|
258
|
+
length(capability_snapshot_digest) = 64
|
|
259
|
+
AND capability_snapshot_digest NOT GLOB '*[^0-9a-f]*'
|
|
260
|
+
),
|
|
261
|
+
provider TEXT NOT NULL CHECK(provider IN ('claude-code', 'codex', 'openrouter', 'zai')),
|
|
262
|
+
requested_model TEXT NOT NULL,
|
|
263
|
+
effective_model TEXT NOT NULL,
|
|
264
|
+
effort TEXT NOT NULL CHECK(effort IN ('default', 'low', 'medium', 'high', 'xhigh', 'max')),
|
|
265
|
+
executable_sha256 TEXT NOT NULL CHECK(
|
|
266
|
+
length(executable_sha256) = 64
|
|
267
|
+
AND executable_sha256 NOT GLOB '*[^0-9a-f]*'
|
|
268
|
+
),
|
|
269
|
+
cli_version TEXT NOT NULL,
|
|
270
|
+
adapter_protocol_version TEXT NOT NULL,
|
|
271
|
+
permission_mode TEXT NOT NULL CHECK(permission_mode IN ('read-only', 'workspace-write')),
|
|
272
|
+
payload_json TEXT NOT NULL,
|
|
273
|
+
verified_at INTEGER NOT NULL,
|
|
274
|
+
expires_at INTEGER NOT NULL CHECK(expires_at > verified_at)
|
|
275
|
+
)"""
|
|
276
|
+
_CAPABILITY_SNAPSHOTS_COLUMNS: Final = (
|
|
277
|
+
"capability_snapshot_digest, provider, requested_model, effective_model, "
|
|
278
|
+
"effort, executable_sha256, cli_version, adapter_protocol_version, "
|
|
279
|
+
"permission_mode, payload_json, verified_at, expires_at"
|
|
280
|
+
)
|
|
281
|
+
_CLI_EXECUTION_ATTEMPTS_DDL: Final = """CREATE TABLE IF NOT EXISTS cli_execution_attempts (
|
|
282
|
+
attempt_id TEXT PRIMARY KEY,
|
|
283
|
+
approval_id TEXT NOT NULL UNIQUE REFERENCES approvals(approval_id) ON DELETE RESTRICT,
|
|
284
|
+
task_id TEXT NOT NULL REFERENCES tasks(task_id) ON DELETE RESTRICT,
|
|
285
|
+
decision_id TEXT NOT NULL REFERENCES decisions(decision_id) ON DELETE RESTRICT,
|
|
286
|
+
worktree_id TEXT NOT NULL REFERENCES task_worktrees(worktree_id) ON DELETE RESTRICT,
|
|
287
|
+
provider TEXT NOT NULL CHECK(provider IN ('claude-code', 'codex', 'openrouter', 'zai')),
|
|
288
|
+
requested_model TEXT NOT NULL,
|
|
289
|
+
effective_model TEXT NOT NULL,
|
|
290
|
+
effort TEXT NOT NULL CHECK(effort IN ('default', 'low', 'medium', 'high', 'xhigh', 'max')),
|
|
291
|
+
cli_version TEXT NOT NULL,
|
|
292
|
+
executable_sha256 TEXT NOT NULL CHECK(
|
|
293
|
+
length(executable_sha256) = 64
|
|
294
|
+
AND executable_sha256 NOT GLOB '*[^0-9a-f]*'
|
|
295
|
+
),
|
|
296
|
+
capability_snapshot_digest TEXT NOT NULL REFERENCES capability_snapshots(capability_snapshot_digest) ON DELETE RESTRICT,
|
|
297
|
+
manifest_hash TEXT NOT NULL CHECK(
|
|
298
|
+
length(manifest_hash) = 64 AND manifest_hash NOT GLOB '*[^0-9a-f]*'
|
|
299
|
+
),
|
|
300
|
+
expected_prompt_hash TEXT NOT NULL CHECK(
|
|
301
|
+
length(expected_prompt_hash) = 64 AND expected_prompt_hash NOT GLOB '*[^0-9a-f]*'
|
|
302
|
+
),
|
|
303
|
+
reserved_tokens INTEGER NOT NULL CHECK(reserved_tokens > 0),
|
|
304
|
+
status TEXT NOT NULL CHECK(status IN ('pending', 'completed', 'failed', 'persistence_failed', 'quarantined')),
|
|
305
|
+
failure_reason TEXT,
|
|
306
|
+
execution_id TEXT UNIQUE REFERENCES executions(execution_id) ON DELETE RESTRICT,
|
|
307
|
+
created_at INTEGER NOT NULL,
|
|
308
|
+
updated_at INTEGER NOT NULL
|
|
309
|
+
)"""
|
|
310
|
+
_CLI_EXECUTION_ATTEMPTS_COLUMNS: Final = (
|
|
311
|
+
"attempt_id, approval_id, task_id, decision_id, worktree_id, provider, "
|
|
312
|
+
"requested_model, effective_model, effort, cli_version, executable_sha256, "
|
|
313
|
+
"capability_snapshot_digest, manifest_hash, expected_prompt_hash, "
|
|
314
|
+
"reserved_tokens, status, failure_reason, execution_id, created_at, updated_at"
|
|
315
|
+
)
|
|
316
|
+
_PRE_V6_WIDENED_TABLES: Final = {
|
|
317
|
+
"capability_snapshots": (_CAPABILITY_SNAPSHOTS_DDL, _CAPABILITY_SNAPSHOTS_COLUMNS),
|
|
318
|
+
"cli_execution_attempts": (_CLI_EXECUTION_ATTEMPTS_DDL, _CLI_EXECUTION_ATTEMPTS_COLUMNS),
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
_SCHEMA = (
|
|
322
|
+
"CREATE TABLE IF NOT EXISTS schema_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL)",
|
|
323
|
+
"""CREATE TABLE IF NOT EXISTS tasks (
|
|
324
|
+
task_id TEXT PRIMARY KEY,
|
|
325
|
+
category TEXT NOT NULL,
|
|
326
|
+
risk TEXT NOT NULL,
|
|
327
|
+
objective_hash TEXT NOT NULL,
|
|
328
|
+
created_at INTEGER NOT NULL
|
|
329
|
+
)""",
|
|
330
|
+
"""CREATE TABLE IF NOT EXISTS decisions (
|
|
331
|
+
decision_id TEXT PRIMARY KEY,
|
|
332
|
+
task_id TEXT REFERENCES tasks(task_id) ON DELETE CASCADE,
|
|
333
|
+
model_id TEXT,
|
|
334
|
+
effort TEXT,
|
|
335
|
+
policy_version TEXT NOT NULL,
|
|
336
|
+
evidence_version TEXT NOT NULL,
|
|
337
|
+
created_at INTEGER NOT NULL
|
|
338
|
+
)""",
|
|
339
|
+
"""CREATE TABLE IF NOT EXISTS approvals (
|
|
340
|
+
approval_id TEXT PRIMARY KEY,
|
|
341
|
+
task_id TEXT REFERENCES tasks(task_id) ON DELETE CASCADE,
|
|
342
|
+
decision_id TEXT REFERENCES decisions(decision_id) ON DELETE CASCADE,
|
|
343
|
+
nonce_hash TEXT NOT NULL UNIQUE,
|
|
344
|
+
manifest_hash TEXT NOT NULL,
|
|
345
|
+
status TEXT NOT NULL,
|
|
346
|
+
expires_at INTEGER NOT NULL,
|
|
347
|
+
reserved_tokens INTEGER NOT NULL
|
|
348
|
+
)""",
|
|
349
|
+
"""CREATE TABLE IF NOT EXISTS executions (
|
|
350
|
+
execution_id TEXT PRIMARY KEY,
|
|
351
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
352
|
+
task_id TEXT REFERENCES tasks(task_id) ON DELETE SET NULL,
|
|
353
|
+
decision_id TEXT REFERENCES decisions(decision_id) ON DELETE SET NULL,
|
|
354
|
+
approval_id TEXT REFERENCES approvals(approval_id) ON DELETE SET NULL,
|
|
355
|
+
model_id TEXT NOT NULL,
|
|
356
|
+
effort TEXT NOT NULL,
|
|
357
|
+
status TEXT NOT NULL,
|
|
358
|
+
reserved_tokens INTEGER NOT NULL,
|
|
359
|
+
actual_input_tokens INTEGER,
|
|
360
|
+
actual_output_tokens INTEGER,
|
|
361
|
+
created_at INTEGER NOT NULL,
|
|
362
|
+
completed_at INTEGER
|
|
363
|
+
)""",
|
|
364
|
+
"""CREATE TABLE IF NOT EXISTS outcomes (
|
|
365
|
+
outcome_id TEXT PRIMARY KEY,
|
|
366
|
+
execution_id TEXT NOT NULL REFERENCES executions(execution_id) ON DELETE CASCADE,
|
|
367
|
+
provenance TEXT NOT NULL,
|
|
368
|
+
success INTEGER NOT NULL CHECK (success IN (0, 1)),
|
|
369
|
+
severe_failure INTEGER NOT NULL CHECK (severe_failure IN (0, 1)),
|
|
370
|
+
recorded_at INTEGER NOT NULL
|
|
371
|
+
)""",
|
|
372
|
+
"""CREATE TABLE IF NOT EXISTS shadow_comparisons (
|
|
373
|
+
comparison_id TEXT PRIMARY KEY,
|
|
374
|
+
primary_execution_id TEXT NOT NULL REFERENCES executions(execution_id) ON DELETE CASCADE,
|
|
375
|
+
shadow_execution_id TEXT NOT NULL REFERENCES executions(execution_id) ON DELETE CASCADE,
|
|
376
|
+
verdict TEXT,
|
|
377
|
+
created_at INTEGER NOT NULL
|
|
378
|
+
)""",
|
|
379
|
+
"""CREATE TABLE IF NOT EXISTS policy_versions (
|
|
380
|
+
policy_version TEXT PRIMARY KEY,
|
|
381
|
+
policy_hash TEXT NOT NULL,
|
|
382
|
+
status TEXT NOT NULL,
|
|
383
|
+
created_at INTEGER NOT NULL
|
|
384
|
+
)""",
|
|
385
|
+
"""CREATE TABLE IF NOT EXISTS budget_ledger (
|
|
386
|
+
entry_id TEXT PRIMARY KEY,
|
|
387
|
+
approval_id TEXT REFERENCES approvals(approval_id) ON DELETE SET NULL,
|
|
388
|
+
execution_id TEXT REFERENCES executions(execution_id) ON DELETE SET NULL,
|
|
389
|
+
entry_type TEXT NOT NULL,
|
|
390
|
+
token_amount INTEGER NOT NULL,
|
|
391
|
+
created_at INTEGER NOT NULL
|
|
392
|
+
)""",
|
|
393
|
+
"""CREATE TABLE IF NOT EXISTS confidence_stats (
|
|
394
|
+
model_id TEXT NOT NULL,
|
|
395
|
+
effort TEXT NOT NULL,
|
|
396
|
+
category TEXT NOT NULL,
|
|
397
|
+
risk TEXT NOT NULL,
|
|
398
|
+
sample_count INTEGER NOT NULL,
|
|
399
|
+
success_count INTEGER NOT NULL,
|
|
400
|
+
severe_failure_count INTEGER NOT NULL,
|
|
401
|
+
PRIMARY KEY (model_id, effort, category, risk)
|
|
402
|
+
)""",
|
|
403
|
+
"CREATE INDEX IF NOT EXISTS outcomes_recorded_at_idx ON outcomes(recorded_at)",
|
|
404
|
+
"CREATE INDEX IF NOT EXISTS executions_task_idx ON executions(task_id)",
|
|
405
|
+
"""CREATE TABLE IF NOT EXISTS execution_evidence (
|
|
406
|
+
execution_id TEXT PRIMARY KEY REFERENCES executions(execution_id) ON DELETE CASCADE,
|
|
407
|
+
task_id TEXT NOT NULL,
|
|
408
|
+
decision_id TEXT NOT NULL,
|
|
409
|
+
graph_fingerprint TEXT NOT NULL
|
|
410
|
+
)""",
|
|
411
|
+
"""CREATE TABLE IF NOT EXISTS execution_attempts (
|
|
412
|
+
attempt_id TEXT PRIMARY KEY,
|
|
413
|
+
approval_id TEXT NOT NULL UNIQUE REFERENCES approvals(approval_id) ON DELETE RESTRICT,
|
|
414
|
+
task_id TEXT NOT NULL REFERENCES tasks(task_id) ON DELETE RESTRICT,
|
|
415
|
+
decision_id TEXT NOT NULL REFERENCES decisions(decision_id) ON DELETE RESTRICT,
|
|
416
|
+
manifest_hash TEXT NOT NULL,
|
|
417
|
+
graph_fingerprint TEXT NOT NULL,
|
|
418
|
+
model_id TEXT NOT NULL,
|
|
419
|
+
effort TEXT NOT NULL,
|
|
420
|
+
reserved_tokens INTEGER NOT NULL,
|
|
421
|
+
max_input_tokens INTEGER NOT NULL,
|
|
422
|
+
max_output_tokens INTEGER NOT NULL,
|
|
423
|
+
expected_prompt_hash TEXT NOT NULL,
|
|
424
|
+
inventory_digest TEXT NOT NULL CHECK(
|
|
425
|
+
length(inventory_digest) = 64
|
|
426
|
+
AND inventory_digest NOT GLOB '*[^0-9a-f]*'
|
|
427
|
+
),
|
|
428
|
+
status TEXT NOT NULL CHECK(status IN (
|
|
429
|
+
'pending', 'completed', 'failed', 'persistence_failed', 'legacy_unrecoverable'
|
|
430
|
+
)),
|
|
431
|
+
failure_reason TEXT,
|
|
432
|
+
execution_id TEXT UNIQUE REFERENCES executions(execution_id) ON DELETE RESTRICT,
|
|
433
|
+
created_at INTEGER NOT NULL,
|
|
434
|
+
updated_at INTEGER NOT NULL
|
|
435
|
+
)""",
|
|
436
|
+
"""CREATE TABLE IF NOT EXISTS staged_execution_receipts (
|
|
437
|
+
attempt_id TEXT PRIMARY KEY REFERENCES execution_attempts(attempt_id) ON DELETE CASCADE,
|
|
438
|
+
execution_id TEXT NOT NULL UNIQUE,
|
|
439
|
+
approval_id TEXT NOT NULL,
|
|
440
|
+
model_id TEXT NOT NULL,
|
|
441
|
+
effort TEXT NOT NULL,
|
|
442
|
+
outcome TEXT NOT NULL,
|
|
443
|
+
input_tokens INTEGER NOT NULL,
|
|
444
|
+
output_tokens INTEGER NOT NULL,
|
|
445
|
+
latency_ms INTEGER NOT NULL,
|
|
446
|
+
prompt_hash TEXT NOT NULL,
|
|
447
|
+
response_hash TEXT NOT NULL,
|
|
448
|
+
inventory_digest TEXT NOT NULL CHECK(
|
|
449
|
+
length(inventory_digest) = 64
|
|
450
|
+
AND inventory_digest NOT GLOB '*[^0-9a-f]*'
|
|
451
|
+
),
|
|
452
|
+
failure_reason TEXT,
|
|
453
|
+
staged_at INTEGER NOT NULL
|
|
454
|
+
)""",
|
|
455
|
+
"""CREATE TABLE IF NOT EXISTS execution_receipts (
|
|
456
|
+
execution_id TEXT PRIMARY KEY REFERENCES executions(execution_id) ON DELETE CASCADE,
|
|
457
|
+
approval_id TEXT NOT NULL,
|
|
458
|
+
model_id TEXT NOT NULL,
|
|
459
|
+
effort TEXT NOT NULL,
|
|
460
|
+
outcome TEXT NOT NULL,
|
|
461
|
+
input_tokens INTEGER NOT NULL,
|
|
462
|
+
output_tokens INTEGER NOT NULL,
|
|
463
|
+
latency_ms INTEGER NOT NULL,
|
|
464
|
+
prompt_hash TEXT NOT NULL,
|
|
465
|
+
response_hash TEXT NOT NULL,
|
|
466
|
+
failure_reason TEXT,
|
|
467
|
+
completed_at INTEGER NOT NULL
|
|
468
|
+
)""",
|
|
469
|
+
"""CREATE TABLE IF NOT EXISTS incident_reviews (
|
|
470
|
+
review_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
471
|
+
execution_id TEXT NOT NULL REFERENCES executions(execution_id) ON DELETE CASCADE,
|
|
472
|
+
reviewed_at INTEGER NOT NULL,
|
|
473
|
+
UNIQUE(execution_id, reviewed_at)
|
|
474
|
+
)""",
|
|
475
|
+
"""CREATE TABLE IF NOT EXISTS blind_comparisons (
|
|
476
|
+
comparison_id TEXT PRIMARY KEY,
|
|
477
|
+
primary_execution_id TEXT NOT NULL,
|
|
478
|
+
shadow_execution_id TEXT NOT NULL,
|
|
479
|
+
label_a_hash TEXT NOT NULL,
|
|
480
|
+
label_b_hash TEXT NOT NULL,
|
|
481
|
+
label_a_is_shadow INTEGER NOT NULL CHECK (label_a_is_shadow IN (0, 1)),
|
|
482
|
+
verdict TEXT,
|
|
483
|
+
recorded_at INTEGER,
|
|
484
|
+
created_at INTEGER NOT NULL
|
|
485
|
+
)""",
|
|
486
|
+
"""CREATE TABLE IF NOT EXISTS registry_snapshots (
|
|
487
|
+
snapshot_id INTEGER PRIMARY KEY CHECK (snapshot_id = 1),
|
|
488
|
+
payload_json TEXT NOT NULL,
|
|
489
|
+
payload_hash TEXT NOT NULL,
|
|
490
|
+
refreshed_at INTEGER NOT NULL,
|
|
491
|
+
expires_at INTEGER NOT NULL
|
|
492
|
+
)""",
|
|
493
|
+
_CAPABILITY_SNAPSHOTS_DDL,
|
|
494
|
+
"""CREATE TABLE IF NOT EXISTS task_worktrees (
|
|
495
|
+
worktree_id TEXT PRIMARY KEY,
|
|
496
|
+
task_id TEXT NOT NULL REFERENCES tasks(task_id) ON DELETE RESTRICT,
|
|
497
|
+
baseline_commit TEXT NOT NULL CHECK(
|
|
498
|
+
(length(baseline_commit) = 40 OR length(baseline_commit) = 64)
|
|
499
|
+
AND baseline_commit NOT GLOB '*[^0-9a-f]*'
|
|
500
|
+
),
|
|
501
|
+
canonical_root_hash TEXT NOT NULL CHECK(
|
|
502
|
+
length(canonical_root_hash) = 64
|
|
503
|
+
AND canonical_root_hash NOT GLOB '*[^0-9a-f]*'
|
|
504
|
+
),
|
|
505
|
+
status TEXT NOT NULL CHECK(status IN ('prepared', 'executed', 'accepted', 'rejected', 'quarantined', 'cleaned')),
|
|
506
|
+
created_at INTEGER NOT NULL,
|
|
507
|
+
updated_at INTEGER NOT NULL
|
|
508
|
+
)""",
|
|
509
|
+
_CLI_EXECUTION_ATTEMPTS_DDL,
|
|
510
|
+
"""CREATE TABLE IF NOT EXISTS validation_results (
|
|
511
|
+
attempt_id TEXT PRIMARY KEY REFERENCES cli_execution_attempts(attempt_id) ON DELETE RESTRICT,
|
|
512
|
+
diff_hash TEXT NOT NULL CHECK(
|
|
513
|
+
length(diff_hash) = 64 AND diff_hash NOT GLOB '*[^0-9a-f]*'
|
|
514
|
+
),
|
|
515
|
+
changed_file_count INTEGER NOT NULL CHECK(changed_file_count >= 0),
|
|
516
|
+
changed_byte_count INTEGER NOT NULL CHECK(changed_byte_count >= 0),
|
|
517
|
+
outcome TEXT NOT NULL CHECK(outcome IN ('passed', 'failed', 'blocked', 'not_run')),
|
|
518
|
+
recorded_at INTEGER NOT NULL
|
|
519
|
+
)""",
|
|
520
|
+
"""CREATE TABLE IF NOT EXISTS review_links (
|
|
521
|
+
review_attempt_id TEXT PRIMARY KEY REFERENCES cli_execution_attempts(attempt_id) ON DELETE RESTRICT,
|
|
522
|
+
primary_attempt_id TEXT NOT NULL REFERENCES cli_execution_attempts(attempt_id) ON DELETE RESTRICT,
|
|
523
|
+
primary_diff_hash TEXT NOT NULL CHECK(
|
|
524
|
+
length(primary_diff_hash) = 64 AND primary_diff_hash NOT GLOB '*[^0-9a-f]*'
|
|
525
|
+
),
|
|
526
|
+
created_at INTEGER NOT NULL,
|
|
527
|
+
CHECK(review_attempt_id != primary_attempt_id)
|
|
528
|
+
)""",
|
|
529
|
+
"""CREATE TABLE IF NOT EXISTS cli_telemetry_events (
|
|
530
|
+
event_hash TEXT PRIMARY KEY CHECK(
|
|
531
|
+
length(event_hash) = 64 AND event_hash NOT GLOB '*[^0-9a-f]*'
|
|
532
|
+
),
|
|
533
|
+
payload_json TEXT NOT NULL CHECK(length(payload_json) <= 8192),
|
|
534
|
+
observed_at INTEGER NOT NULL
|
|
535
|
+
)""",
|
|
536
|
+
"""CREATE TABLE IF NOT EXISTS cli_policy_versions (
|
|
537
|
+
policy_version TEXT PRIMARY KEY,
|
|
538
|
+
parent_version TEXT REFERENCES cli_policy_versions(policy_version) ON DELETE RESTRICT,
|
|
539
|
+
payload_json TEXT NOT NULL CHECK(length(payload_json) <= 16384),
|
|
540
|
+
payload_hash TEXT NOT NULL CHECK(
|
|
541
|
+
length(payload_hash) = 64 AND payload_hash NOT GLOB '*[^0-9a-f]*'
|
|
542
|
+
),
|
|
543
|
+
signature TEXT NOT NULL CHECK(
|
|
544
|
+
length(signature) = 64 AND signature NOT GLOB '*[^0-9a-f]*'
|
|
545
|
+
),
|
|
546
|
+
evidence_hash TEXT NOT NULL CHECK(
|
|
547
|
+
length(evidence_hash) = 64 AND evidence_hash NOT GLOB '*[^0-9a-f]*'
|
|
548
|
+
),
|
|
549
|
+
created_at INTEGER NOT NULL
|
|
550
|
+
)""",
|
|
551
|
+
"""CREATE TABLE IF NOT EXISTS cli_policy_events (
|
|
552
|
+
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
553
|
+
policy_version TEXT NOT NULL REFERENCES cli_policy_versions(policy_version) ON DELETE RESTRICT,
|
|
554
|
+
action TEXT NOT NULL CHECK(action IN ('candidate', 'promote', 'rollback')),
|
|
555
|
+
prior_version TEXT REFERENCES cli_policy_versions(policy_version) ON DELETE RESTRICT,
|
|
556
|
+
actor TEXT NOT NULL CHECK(actor = 'human'),
|
|
557
|
+
created_at INTEGER NOT NULL
|
|
558
|
+
)""",
|
|
559
|
+
"""CREATE TABLE IF NOT EXISTS lifecycle_snapshot_bindings (
|
|
560
|
+
capability_snapshot_digest TEXT PRIMARY KEY
|
|
561
|
+
REFERENCES capability_snapshots(capability_snapshot_digest) ON DELETE RESTRICT,
|
|
562
|
+
lifecycle_identity_digest TEXT NOT NULL CHECK(
|
|
563
|
+
length(lifecycle_identity_digest) = 64
|
|
564
|
+
AND lifecycle_identity_digest NOT GLOB '*[^0-9a-f]*'
|
|
565
|
+
),
|
|
566
|
+
bound_at INTEGER NOT NULL CHECK(bound_at >= 0)
|
|
567
|
+
)""",
|
|
568
|
+
"""CREATE TABLE IF NOT EXISTS lifecycle_approval_bindings (
|
|
569
|
+
approval_id TEXT PRIMARY KEY REFERENCES approvals(approval_id) ON DELETE RESTRICT,
|
|
570
|
+
capability_snapshot_digest TEXT NOT NULL
|
|
571
|
+
REFERENCES capability_snapshots(capability_snapshot_digest) ON DELETE RESTRICT,
|
|
572
|
+
lifecycle_identity_digest TEXT NOT NULL CHECK(
|
|
573
|
+
length(lifecycle_identity_digest) = 64
|
|
574
|
+
AND lifecycle_identity_digest NOT GLOB '*[^0-9a-f]*'
|
|
575
|
+
),
|
|
576
|
+
bound_at INTEGER NOT NULL CHECK(bound_at >= 0)
|
|
577
|
+
)""",
|
|
578
|
+
"""CREATE TABLE IF NOT EXISTS lifecycle_attempt_bindings (
|
|
579
|
+
attempt_id TEXT PRIMARY KEY
|
|
580
|
+
REFERENCES cli_execution_attempts(attempt_id) ON DELETE RESTRICT,
|
|
581
|
+
lifecycle_identity_digest TEXT NOT NULL CHECK(
|
|
582
|
+
length(lifecycle_identity_digest) = 64
|
|
583
|
+
AND lifecycle_identity_digest NOT GLOB '*[^0-9a-f]*'
|
|
584
|
+
),
|
|
585
|
+
bound_at INTEGER NOT NULL CHECK(bound_at >= 0)
|
|
586
|
+
)""",
|
|
587
|
+
"""CREATE TRIGGER IF NOT EXISTS lifecycle_snapshot_binding_update_guard
|
|
588
|
+
BEFORE UPDATE ON lifecycle_snapshot_bindings BEGIN
|
|
589
|
+
SELECT RAISE(ABORT, 'lifecycle_snapshot_binding_immutable');
|
|
590
|
+
END""",
|
|
591
|
+
"""CREATE TRIGGER IF NOT EXISTS lifecycle_snapshot_binding_delete_guard
|
|
592
|
+
BEFORE DELETE ON lifecycle_snapshot_bindings BEGIN
|
|
593
|
+
SELECT RAISE(ABORT, 'lifecycle_snapshot_binding_immutable');
|
|
594
|
+
END""",
|
|
595
|
+
"""CREATE TRIGGER IF NOT EXISTS lifecycle_approval_binding_update_guard
|
|
596
|
+
BEFORE UPDATE ON lifecycle_approval_bindings BEGIN
|
|
597
|
+
SELECT RAISE(ABORT, 'lifecycle_approval_binding_immutable');
|
|
598
|
+
END""",
|
|
599
|
+
"""CREATE TRIGGER IF NOT EXISTS lifecycle_approval_binding_delete_guard
|
|
600
|
+
BEFORE DELETE ON lifecycle_approval_bindings BEGIN
|
|
601
|
+
SELECT RAISE(ABORT, 'lifecycle_approval_binding_immutable');
|
|
602
|
+
END""",
|
|
603
|
+
"""CREATE TRIGGER IF NOT EXISTS lifecycle_attempt_binding_update_guard
|
|
604
|
+
BEFORE UPDATE ON lifecycle_attempt_bindings BEGIN
|
|
605
|
+
SELECT RAISE(ABORT, 'lifecycle_attempt_binding_immutable');
|
|
606
|
+
END""",
|
|
607
|
+
"""CREATE TRIGGER IF NOT EXISTS lifecycle_attempt_binding_insert_guard
|
|
608
|
+
BEFORE INSERT ON lifecycle_attempt_bindings
|
|
609
|
+
WHEN NOT EXISTS (
|
|
610
|
+
SELECT 1
|
|
611
|
+
FROM cli_execution_attempts AS attempt
|
|
612
|
+
JOIN lifecycle_approval_bindings AS binding
|
|
613
|
+
ON binding.approval_id = attempt.approval_id
|
|
614
|
+
WHERE attempt.attempt_id = NEW.attempt_id
|
|
615
|
+
AND binding.lifecycle_identity_digest = NEW.lifecycle_identity_digest
|
|
616
|
+
)
|
|
617
|
+
BEGIN
|
|
618
|
+
SELECT RAISE(ABORT, 'lifecycle_approval_binding_missing');
|
|
619
|
+
END""",
|
|
620
|
+
"""CREATE TRIGGER IF NOT EXISTS lifecycle_attempt_binding_delete_guard
|
|
621
|
+
BEFORE DELETE ON lifecycle_attempt_bindings BEGIN
|
|
622
|
+
SELECT RAISE(ABORT, 'lifecycle_attempt_binding_immutable');
|
|
623
|
+
END""",
|
|
624
|
+
"""CREATE TABLE IF NOT EXISTS lifecycle_binding_carries (
|
|
625
|
+
carry_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
626
|
+
capability_snapshot_digest TEXT NOT NULL
|
|
627
|
+
REFERENCES capability_snapshots(capability_snapshot_digest) ON DELETE RESTRICT,
|
|
628
|
+
previous_identity_digest TEXT NOT NULL CHECK(
|
|
629
|
+
length(previous_identity_digest) = 64
|
|
630
|
+
AND previous_identity_digest NOT GLOB '*[^0-9a-f]*'
|
|
631
|
+
),
|
|
632
|
+
new_identity_digest TEXT NOT NULL CHECK(
|
|
633
|
+
length(new_identity_digest) = 64
|
|
634
|
+
AND new_identity_digest NOT GLOB '*[^0-9a-f]*'
|
|
635
|
+
),
|
|
636
|
+
lifecycle_event_id TEXT NOT NULL CHECK(
|
|
637
|
+
length(lifecycle_event_id) = 64
|
|
638
|
+
AND lifecycle_event_id NOT GLOB '*[^0-9a-f]*'
|
|
639
|
+
),
|
|
640
|
+
carried_at INTEGER NOT NULL CHECK(carried_at >= 0)
|
|
641
|
+
)""",
|
|
642
|
+
"""CREATE TRIGGER IF NOT EXISTS lifecycle_binding_carry_update_guard
|
|
643
|
+
BEFORE UPDATE ON lifecycle_binding_carries BEGIN
|
|
644
|
+
SELECT RAISE(ABORT, 'lifecycle_binding_carry_immutable');
|
|
645
|
+
END""",
|
|
646
|
+
"""CREATE TRIGGER IF NOT EXISTS lifecycle_binding_carry_delete_guard
|
|
647
|
+
BEFORE DELETE ON lifecycle_binding_carries BEGIN
|
|
648
|
+
SELECT RAISE(ABORT, 'lifecycle_binding_carry_immutable');
|
|
649
|
+
END""",
|
|
650
|
+
"""CREATE TRIGGER IF NOT EXISTS lifecycle_approval_binding_insert_guard_v8
|
|
651
|
+
BEFORE INSERT ON lifecycle_approval_bindings
|
|
652
|
+
WHEN NOT EXISTS (
|
|
653
|
+
SELECT 1 FROM lifecycle_snapshot_bindings AS binding
|
|
654
|
+
WHERE binding.capability_snapshot_digest = NEW.capability_snapshot_digest
|
|
655
|
+
AND binding.lifecycle_identity_digest = NEW.lifecycle_identity_digest
|
|
656
|
+
) AND NOT EXISTS (
|
|
657
|
+
SELECT 1 FROM lifecycle_binding_carries AS carry
|
|
658
|
+
WHERE carry.capability_snapshot_digest = NEW.capability_snapshot_digest
|
|
659
|
+
AND carry.new_identity_digest = NEW.lifecycle_identity_digest
|
|
660
|
+
)
|
|
661
|
+
BEGIN
|
|
662
|
+
SELECT RAISE(ABORT, 'lifecycle_snapshot_binding_missing');
|
|
663
|
+
END""",
|
|
664
|
+
"""CREATE TRIGGER IF NOT EXISTS cli_attempt_identity_update_guard
|
|
665
|
+
BEFORE UPDATE OF approval_id, task_id, decision_id, worktree_id, provider,
|
|
666
|
+
requested_model, effective_model, effort, cli_version, executable_sha256,
|
|
667
|
+
capability_snapshot_digest, manifest_hash, expected_prompt_hash, reserved_tokens
|
|
668
|
+
ON cli_execution_attempts
|
|
669
|
+
BEGIN
|
|
670
|
+
SELECT RAISE(ABORT, 'cli_attempt_identity_immutable');
|
|
671
|
+
END""",
|
|
672
|
+
"""CREATE TRIGGER IF NOT EXISTS capability_snapshot_update_guard
|
|
673
|
+
BEFORE UPDATE ON capability_snapshots
|
|
674
|
+
BEGIN
|
|
675
|
+
SELECT RAISE(ABORT, 'capability_snapshot_immutable');
|
|
676
|
+
END""",
|
|
677
|
+
"""CREATE TRIGGER IF NOT EXISTS capability_snapshot_delete_guard
|
|
678
|
+
BEFORE DELETE ON capability_snapshots
|
|
679
|
+
BEGIN
|
|
680
|
+
SELECT RAISE(ABORT, 'capability_snapshot_immutable');
|
|
681
|
+
END""",
|
|
682
|
+
"""CREATE TRIGGER IF NOT EXISTS cli_attempt_status_transition_guard
|
|
683
|
+
BEFORE UPDATE OF status ON cli_execution_attempts
|
|
684
|
+
WHEN NOT (
|
|
685
|
+
(OLD.status = 'pending' AND NEW.status IN (
|
|
686
|
+
'completed', 'failed', 'persistence_failed', 'quarantined'
|
|
687
|
+
))
|
|
688
|
+
OR (OLD.status = 'persistence_failed' AND NEW.status IN (
|
|
689
|
+
'completed', 'failed', 'quarantined'
|
|
690
|
+
))
|
|
691
|
+
OR OLD.status = NEW.status
|
|
692
|
+
)
|
|
693
|
+
BEGIN
|
|
694
|
+
SELECT RAISE(ABORT, 'cli_attempt_transition_invalid');
|
|
695
|
+
END""",
|
|
696
|
+
"""CREATE TRIGGER IF NOT EXISTS cli_attempt_delete_guard
|
|
697
|
+
BEFORE DELETE ON cli_execution_attempts
|
|
698
|
+
BEGIN
|
|
699
|
+
SELECT RAISE(ABORT, 'cli_attempt_immutable');
|
|
700
|
+
END""",
|
|
701
|
+
"""CREATE TRIGGER IF NOT EXISTS worktree_identity_update_guard
|
|
702
|
+
BEFORE UPDATE OF worktree_id, task_id, baseline_commit, canonical_root_hash, created_at
|
|
703
|
+
ON task_worktrees
|
|
704
|
+
BEGIN
|
|
705
|
+
SELECT RAISE(ABORT, 'worktree_identity_immutable');
|
|
706
|
+
END""",
|
|
707
|
+
"""CREATE TRIGGER IF NOT EXISTS worktree_status_transition_guard
|
|
708
|
+
BEFORE UPDATE OF status ON task_worktrees
|
|
709
|
+
WHEN NOT (
|
|
710
|
+
(OLD.status = 'prepared' AND NEW.status IN ('executed', 'quarantined'))
|
|
711
|
+
OR (OLD.status = 'executed' AND NEW.status IN ('accepted', 'rejected', 'quarantined'))
|
|
712
|
+
OR (OLD.status IN ('accepted', 'rejected', 'quarantined') AND NEW.status = 'cleaned')
|
|
713
|
+
OR OLD.status = NEW.status
|
|
714
|
+
)
|
|
715
|
+
BEGIN
|
|
716
|
+
SELECT RAISE(ABORT, 'worktree_transition_invalid');
|
|
717
|
+
END""",
|
|
718
|
+
"""CREATE TRIGGER IF NOT EXISTS worktree_delete_guard
|
|
719
|
+
BEFORE DELETE ON task_worktrees
|
|
720
|
+
BEGIN
|
|
721
|
+
SELECT RAISE(ABORT, 'worktree_evidence_immutable');
|
|
722
|
+
END""",
|
|
723
|
+
"""CREATE TRIGGER IF NOT EXISTS validation_result_update_guard
|
|
724
|
+
BEFORE UPDATE ON validation_results
|
|
725
|
+
BEGIN
|
|
726
|
+
SELECT RAISE(ABORT, 'validation_result_immutable');
|
|
727
|
+
END""",
|
|
728
|
+
"""CREATE TRIGGER IF NOT EXISTS validation_result_delete_guard
|
|
729
|
+
BEFORE DELETE ON validation_results
|
|
730
|
+
BEGIN
|
|
731
|
+
SELECT RAISE(ABORT, 'validation_result_immutable');
|
|
732
|
+
END""",
|
|
733
|
+
"""CREATE TRIGGER IF NOT EXISTS review_link_update_guard
|
|
734
|
+
BEFORE UPDATE ON review_links
|
|
735
|
+
BEGIN
|
|
736
|
+
SELECT RAISE(ABORT, 'review_link_immutable');
|
|
737
|
+
END""",
|
|
738
|
+
"""CREATE TRIGGER IF NOT EXISTS review_link_delete_guard
|
|
739
|
+
BEFORE DELETE ON review_links
|
|
740
|
+
BEGIN
|
|
741
|
+
SELECT RAISE(ABORT, 'review_link_immutable');
|
|
742
|
+
END""",
|
|
743
|
+
"""CREATE TRIGGER IF NOT EXISTS cli_telemetry_update_guard
|
|
744
|
+
BEFORE UPDATE ON cli_telemetry_events
|
|
745
|
+
BEGIN
|
|
746
|
+
SELECT RAISE(ABORT, 'cli_telemetry_immutable');
|
|
747
|
+
END""",
|
|
748
|
+
"""CREATE TRIGGER IF NOT EXISTS cli_telemetry_delete_guard
|
|
749
|
+
BEFORE DELETE ON cli_telemetry_events
|
|
750
|
+
BEGIN
|
|
751
|
+
SELECT RAISE(ABORT, 'cli_telemetry_immutable');
|
|
752
|
+
END""",
|
|
753
|
+
"""CREATE TRIGGER IF NOT EXISTS cli_policy_version_update_guard
|
|
754
|
+
BEFORE UPDATE ON cli_policy_versions
|
|
755
|
+
BEGIN
|
|
756
|
+
SELECT RAISE(ABORT, 'cli_policy_version_immutable');
|
|
757
|
+
END""",
|
|
758
|
+
"""CREATE TRIGGER IF NOT EXISTS cli_policy_version_delete_guard
|
|
759
|
+
BEFORE DELETE ON cli_policy_versions
|
|
760
|
+
BEGIN
|
|
761
|
+
SELECT RAISE(ABORT, 'cli_policy_version_immutable');
|
|
762
|
+
END""",
|
|
763
|
+
"""CREATE TRIGGER IF NOT EXISTS cli_policy_event_update_guard
|
|
764
|
+
BEFORE UPDATE ON cli_policy_events
|
|
765
|
+
BEGIN
|
|
766
|
+
SELECT RAISE(ABORT, 'cli_policy_event_immutable');
|
|
767
|
+
END""",
|
|
768
|
+
"""CREATE TRIGGER IF NOT EXISTS cli_policy_event_delete_guard
|
|
769
|
+
BEFORE DELETE ON cli_policy_events
|
|
770
|
+
BEGIN
|
|
771
|
+
SELECT RAISE(ABORT, 'cli_policy_event_immutable');
|
|
772
|
+
END""",
|
|
773
|
+
"""CREATE TRIGGER IF NOT EXISTS execution_attempt_digest_insert_guard
|
|
774
|
+
BEFORE INSERT ON execution_attempts
|
|
775
|
+
WHEN NEW.inventory_digest IS NULL
|
|
776
|
+
OR length(NEW.inventory_digest) != 64
|
|
777
|
+
OR NEW.inventory_digest GLOB '*[^0-9a-f]*'
|
|
778
|
+
BEGIN
|
|
779
|
+
SELECT RAISE(ABORT, 'inventory_digest_invalid');
|
|
780
|
+
END""",
|
|
781
|
+
"""CREATE TRIGGER IF NOT EXISTS execution_attempt_digest_update_guard
|
|
782
|
+
BEFORE UPDATE ON execution_attempts
|
|
783
|
+
WHEN OLD.inventory_digest IS NULL
|
|
784
|
+
OR NEW.inventory_digest IS NULL
|
|
785
|
+
OR length(NEW.inventory_digest) != 64
|
|
786
|
+
OR NEW.inventory_digest GLOB '*[^0-9a-f]*'
|
|
787
|
+
BEGIN
|
|
788
|
+
SELECT RAISE(ABORT, 'inventory_digest_invalid');
|
|
789
|
+
END""",
|
|
790
|
+
"""CREATE TRIGGER IF NOT EXISTS execution_attempt_digest_delete_guard
|
|
791
|
+
BEFORE DELETE ON execution_attempts
|
|
792
|
+
WHEN OLD.inventory_digest IS NULL
|
|
793
|
+
BEGIN
|
|
794
|
+
SELECT RAISE(ABORT, 'inventory_digest_invalid');
|
|
795
|
+
END""",
|
|
796
|
+
"""CREATE TRIGGER IF NOT EXISTS staged_receipt_digest_insert_guard
|
|
797
|
+
BEFORE INSERT ON staged_execution_receipts
|
|
798
|
+
WHEN NEW.inventory_digest IS NULL
|
|
799
|
+
OR length(NEW.inventory_digest) != 64
|
|
800
|
+
OR NEW.inventory_digest GLOB '*[^0-9a-f]*'
|
|
801
|
+
BEGIN
|
|
802
|
+
SELECT RAISE(ABORT, 'inventory_digest_invalid');
|
|
803
|
+
END""",
|
|
804
|
+
"""CREATE TRIGGER IF NOT EXISTS staged_receipt_digest_update_guard
|
|
805
|
+
BEFORE UPDATE ON staged_execution_receipts
|
|
806
|
+
WHEN OLD.inventory_digest IS NULL
|
|
807
|
+
OR NEW.inventory_digest IS NULL
|
|
808
|
+
OR length(NEW.inventory_digest) != 64
|
|
809
|
+
OR NEW.inventory_digest GLOB '*[^0-9a-f]*'
|
|
810
|
+
BEGIN
|
|
811
|
+
SELECT RAISE(ABORT, 'inventory_digest_invalid');
|
|
812
|
+
END""",
|
|
813
|
+
"""CREATE TRIGGER IF NOT EXISTS staged_receipt_digest_delete_guard
|
|
814
|
+
BEFORE DELETE ON staged_execution_receipts
|
|
815
|
+
WHEN OLD.inventory_digest IS NULL
|
|
816
|
+
BEGIN
|
|
817
|
+
SELECT RAISE(ABORT, 'inventory_digest_invalid');
|
|
818
|
+
END""",
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
@dataclass(frozen=True, slots=True)
|
|
823
|
+
class RecoverableAttemptPage:
|
|
824
|
+
"""Bounded public recovery enumeration with an opaque validated cursor."""
|
|
825
|
+
|
|
826
|
+
attempt_ids: tuple[str, ...]
|
|
827
|
+
next_cursor: str | None
|
|
828
|
+
has_more: bool
|
|
829
|
+
|
|
830
|
+
def to_dict(self) -> dict[str, Any]:
|
|
831
|
+
return {
|
|
832
|
+
"attempts": [
|
|
833
|
+
{"attempt_id": attempt_id, "status": "recoverable"}
|
|
834
|
+
for attempt_id in self.attempt_ids
|
|
835
|
+
],
|
|
836
|
+
"has_more": self.has_more,
|
|
837
|
+
"next_cursor": self.next_cursor,
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
class RepositoryStore:
|
|
842
|
+
"""Detailed evidence that never leaves the selected repository by default."""
|
|
843
|
+
|
|
844
|
+
def __init__(self, repository_root: Path, *, busy_timeout_ms: int = BUSY_TIMEOUT_MS) -> None:
|
|
845
|
+
self.root = _selected_root(repository_root)
|
|
846
|
+
if isinstance(busy_timeout_ms, bool) or not 100 <= busy_timeout_ms <= 30_000:
|
|
847
|
+
raise ValueError("busy_timeout_invalid")
|
|
848
|
+
self.busy_timeout_ms = busy_timeout_ms
|
|
849
|
+
self.path = self.root / ".graphite" / "routing" / "events.sqlite3"
|
|
850
|
+
|
|
851
|
+
@property
|
|
852
|
+
def schema_v3_backup_path(self) -> Path:
|
|
853
|
+
return self.path.parent / "backups" / "events-schema-v3.sqlite3"
|
|
854
|
+
|
|
855
|
+
@property
|
|
856
|
+
def schema_v3_backup_marker_path(self) -> Path:
|
|
857
|
+
return self.path.parent / "backups" / "events-schema-v3.sha256.json"
|
|
858
|
+
|
|
859
|
+
@property
|
|
860
|
+
def schema_v4_backup_path(self) -> Path:
|
|
861
|
+
return self.path.parent / "backups" / "events-schema-v4.sqlite3"
|
|
862
|
+
|
|
863
|
+
@property
|
|
864
|
+
def schema_v4_backup_marker_path(self) -> Path:
|
|
865
|
+
return self.path.parent / "backups" / "events-schema-v4.sha256.json"
|
|
866
|
+
|
|
867
|
+
def _pre_v6_backup_path(self, source_version: str) -> Path:
|
|
868
|
+
return self.path.parent / "backups" / f"events-schema-v{source_version}-pre-v6.sqlite3"
|
|
869
|
+
|
|
870
|
+
def _pre_v6_backup_marker_path(self, source_version: str) -> Path:
|
|
871
|
+
return self.path.parent / "backups" / f"events-schema-v{source_version}-pre-v6.sha256.json"
|
|
872
|
+
|
|
873
|
+
def _pre_v7_backup_path(self, source_version: str) -> Path:
|
|
874
|
+
return self.path.parent / "backups" / f"events-schema-v{source_version}-pre-v7.sqlite3"
|
|
875
|
+
|
|
876
|
+
def _pre_v7_backup_marker_path(self, source_version: str) -> Path:
|
|
877
|
+
return self.path.parent / "backups" / f"events-schema-v{source_version}-pre-v7.sha256.json"
|
|
878
|
+
|
|
879
|
+
def _pre_v8_backup_path(self, source_version: str) -> Path:
|
|
880
|
+
return self.path.parent / "backups" / f"events-schema-v{source_version}-pre-v8.sqlite3"
|
|
881
|
+
|
|
882
|
+
def _pre_v8_backup_marker_path(self, source_version: str) -> Path:
|
|
883
|
+
return self.path.parent / "backups" / f"events-schema-v{source_version}-pre-v8.sha256.json"
|
|
884
|
+
|
|
885
|
+
def _connect(self) -> sqlite3.Connection:
|
|
886
|
+
# The handle is opened first and configured second, so a failure in the
|
|
887
|
+
# configuration leaves a live handle that the caller never receives --
|
|
888
|
+
# no `try/finally` upstream can reach it, and only the collector ever
|
|
889
|
+
# frees it. `PRAGMA journal_mode = WAL` is the realistic trigger: it
|
|
890
|
+
# takes a lock, so it is what answers "database is locked" when another
|
|
891
|
+
# connection holds one.
|
|
892
|
+
#
|
|
893
|
+
# This is the `with connection:` defect one level lower. There the
|
|
894
|
+
# transaction ended and the handle stayed open; here the error path drops
|
|
895
|
+
# the reference before anyone can own it. Both leave Windows unable to
|
|
896
|
+
# unlink the database, and both do it on the RECOVERY path.
|
|
897
|
+
connection: sqlite3.Connection | None = None
|
|
898
|
+
try:
|
|
899
|
+
connection = sqlite3.connect(
|
|
900
|
+
self.path,
|
|
901
|
+
timeout=self.busy_timeout_ms / 1_000,
|
|
902
|
+
isolation_level=None,
|
|
903
|
+
)
|
|
904
|
+
connection.row_factory = sqlite3.Row
|
|
905
|
+
connection.execute(f"PRAGMA busy_timeout = {self.busy_timeout_ms}")
|
|
906
|
+
connection.execute("PRAGMA foreign_keys = ON")
|
|
907
|
+
connection.execute("PRAGMA journal_mode = WAL")
|
|
908
|
+
return connection
|
|
909
|
+
except sqlite3.Error as exc:
|
|
910
|
+
if connection is not None:
|
|
911
|
+
# Suppressed, not swallowed: a close that also fails must not
|
|
912
|
+
# replace the diagnosis the caller is about to receive.
|
|
913
|
+
with suppress(sqlite3.Error):
|
|
914
|
+
connection.close()
|
|
915
|
+
raise _translate_database_error(exc) from exc
|
|
916
|
+
|
|
917
|
+
@contextmanager
|
|
918
|
+
def _connection(self) -> Iterator[sqlite3.Connection]:
|
|
919
|
+
"""End the transaction like `with connection:` does, then actually CLOSE it.
|
|
920
|
+
|
|
921
|
+
`sqlite3.Connection.__exit__` commits or rolls back and leaves the handle
|
|
922
|
+
OPEN -- a long-standing trap in that API. `with self._connection() as c:`
|
|
923
|
+
therefore released nothing, and 51 sites across this package used it.
|
|
924
|
+
Measured: 934 `ResourceWarning: unclosed database` in one suite run.
|
|
925
|
+
|
|
926
|
+
Refcounting hides it: the local dies when the method returns and CPython
|
|
927
|
+
finalizes immediately, which is why the suite was green. It stops hiding
|
|
928
|
+
the moment anything else holds a frame alive -- a traceback, a debugger,
|
|
929
|
+
a coverage tracer. Under `pytest --cov` the v3->v4 and v4->v5 rollback
|
|
930
|
+
drills failed with `PermissionError: [WinError 32]`: Windows will not
|
|
931
|
+
unlink a file with an open handle, and rollback REPLACES the live
|
|
932
|
+
database. The recovery path was depending on collector timing.
|
|
933
|
+
|
|
934
|
+
The methods that manage their own transaction keep using `_connect` with
|
|
935
|
+
an explicit `try/finally: connection.close()` -- they were already
|
|
936
|
+
correct, and wrapping them here would add a second commit they did not
|
|
937
|
+
ask for.
|
|
938
|
+
"""
|
|
939
|
+
connection = self._connect()
|
|
940
|
+
try:
|
|
941
|
+
with connection:
|
|
942
|
+
yield connection
|
|
943
|
+
finally:
|
|
944
|
+
connection.close()
|
|
945
|
+
|
|
946
|
+
def initialize(self) -> None:
|
|
947
|
+
_secure_repository_directory(self.root, self.path.parent)
|
|
948
|
+
_validate_database_file(self.path)
|
|
949
|
+
self._migrate_pre_v6_provider_widening()
|
|
950
|
+
self._migrate_v6_to_v7()
|
|
951
|
+
self._migrate_v7_to_v8()
|
|
952
|
+
connection: sqlite3.Connection | None = None
|
|
953
|
+
try:
|
|
954
|
+
connection = self._connect()
|
|
955
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
956
|
+
connection.execute(_SCHEMA[0])
|
|
957
|
+
existing_version = connection.execute(
|
|
958
|
+
"SELECT value FROM schema_meta WHERE key = 'schema_version'"
|
|
959
|
+
).fetchone()
|
|
960
|
+
if existing_version is not None and existing_version[0] not in {
|
|
961
|
+
"1", "2", "3", "4", "5", "6", "7", SCHEMA_VERSION
|
|
962
|
+
}:
|
|
963
|
+
raise StorageError("storage_schema_unsupported")
|
|
964
|
+
if existing_version is not None and existing_version[0] == SCHEMA_VERSION:
|
|
965
|
+
self._validate_v6_schema(connection)
|
|
966
|
+
if existing_version is not None and existing_version[0] == "5":
|
|
967
|
+
self._validate_v5_schema(connection)
|
|
968
|
+
if existing_version is not None and existing_version[0] == "1":
|
|
969
|
+
self._migrate_v1_attempts(connection)
|
|
970
|
+
if existing_version is not None and existing_version[0] in {"1", "2"}:
|
|
971
|
+
self._migrate_v2_digest(connection)
|
|
972
|
+
if existing_version is not None and existing_version[0] == "3":
|
|
973
|
+
self._create_schema_v3_backup()
|
|
974
|
+
self._migrate_v3_cli_cutover(connection)
|
|
975
|
+
if existing_version is not None and existing_version[0] == "4":
|
|
976
|
+
self._validate_v4_schema(connection)
|
|
977
|
+
self._create_schema_v4_backup()
|
|
978
|
+
for statement in _SCHEMA[1:]:
|
|
979
|
+
connection.execute(statement)
|
|
980
|
+
# _migrate_v7_to_v8 only drops this superseded trigger when the
|
|
981
|
+
# on-disk version is exactly "7" at the moment it runs. Stores
|
|
982
|
+
# migrating from v6-or-earlier never pass through that check --
|
|
983
|
+
# _migrate_v6_to_v7 (and the pre-v6 widening) rebuild tables
|
|
984
|
+
# without stamping schema_version, so it stays at the origin
|
|
985
|
+
# version until this transaction's INSERT below. Drop it here,
|
|
986
|
+
# unconditionally and idempotently, so every origin converges on
|
|
987
|
+
# only the v8 guard (which is a strict superset: same protection
|
|
988
|
+
# plus carried-identity acceptance) instead of leaving the two
|
|
989
|
+
# triggers to coexist and the old one to reject valid carries.
|
|
990
|
+
connection.execute(
|
|
991
|
+
"DROP TRIGGER IF EXISTS lifecycle_approval_binding_insert_guard"
|
|
992
|
+
)
|
|
993
|
+
connection.execute(
|
|
994
|
+
"""INSERT INTO schema_meta(key, value) VALUES('schema_version', ?)
|
|
995
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value""",
|
|
996
|
+
(SCHEMA_VERSION,),
|
|
997
|
+
)
|
|
998
|
+
connection.commit()
|
|
999
|
+
result = connection.execute("PRAGMA integrity_check").fetchone()
|
|
1000
|
+
if result is None or result[0] != "ok":
|
|
1001
|
+
raise StorageError("storage_corrupt")
|
|
1002
|
+
except StorageError:
|
|
1003
|
+
if connection is not None and connection.in_transaction:
|
|
1004
|
+
connection.rollback()
|
|
1005
|
+
raise
|
|
1006
|
+
except sqlite3.Error as exc:
|
|
1007
|
+
if connection is not None and connection.in_transaction:
|
|
1008
|
+
connection.rollback()
|
|
1009
|
+
raise _translate_database_error(exc) from exc
|
|
1010
|
+
finally:
|
|
1011
|
+
if connection is not None:
|
|
1012
|
+
connection.close()
|
|
1013
|
+
_validate_database_file(self.path)
|
|
1014
|
+
_secure_file(self.path)
|
|
1015
|
+
|
|
1016
|
+
@staticmethod
|
|
1017
|
+
def _file_sha256(path: Path) -> str:
|
|
1018
|
+
digest = hashlib.sha256()
|
|
1019
|
+
try:
|
|
1020
|
+
with path.open("rb") as stream:
|
|
1021
|
+
while chunk := stream.read(1024 * 1024):
|
|
1022
|
+
digest.update(chunk)
|
|
1023
|
+
except OSError as exc:
|
|
1024
|
+
raise StorageError("storage_backup_failed") from exc
|
|
1025
|
+
return digest.hexdigest()
|
|
1026
|
+
|
|
1027
|
+
@staticmethod
|
|
1028
|
+
def _atomic_private_write(path: Path, payload: bytes) -> None:
|
|
1029
|
+
temporary = path.with_name(f".{path.name}.{secrets.token_hex(8)}.tmp")
|
|
1030
|
+
descriptor: int | None = None
|
|
1031
|
+
try:
|
|
1032
|
+
descriptor = os.open(
|
|
1033
|
+
temporary,
|
|
1034
|
+
os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0),
|
|
1035
|
+
0o600,
|
|
1036
|
+
)
|
|
1037
|
+
written = os.write(descriptor, payload)
|
|
1038
|
+
if written != len(payload):
|
|
1039
|
+
raise StorageError("storage_backup_failed")
|
|
1040
|
+
os.fsync(descriptor)
|
|
1041
|
+
os.close(descriptor)
|
|
1042
|
+
descriptor = None
|
|
1043
|
+
os.replace(temporary, path)
|
|
1044
|
+
_secure_file(path)
|
|
1045
|
+
except StorageError:
|
|
1046
|
+
raise
|
|
1047
|
+
except OSError as exc:
|
|
1048
|
+
raise StorageError("storage_backup_failed") from exc
|
|
1049
|
+
finally:
|
|
1050
|
+
if descriptor is not None:
|
|
1051
|
+
os.close(descriptor)
|
|
1052
|
+
try:
|
|
1053
|
+
temporary.unlink(missing_ok=True)
|
|
1054
|
+
except OSError:
|
|
1055
|
+
pass
|
|
1056
|
+
|
|
1057
|
+
def _create_schema_v3_backup(self) -> None:
|
|
1058
|
+
self._create_schema_backup(
|
|
1059
|
+
source_version="3",
|
|
1060
|
+
backup=self.schema_v3_backup_path,
|
|
1061
|
+
marker=self.schema_v3_backup_marker_path,
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
def _create_schema_v4_backup(self) -> None:
|
|
1065
|
+
self._create_schema_backup(
|
|
1066
|
+
source_version="4",
|
|
1067
|
+
backup=self.schema_v4_backup_path,
|
|
1068
|
+
marker=self.schema_v4_backup_marker_path,
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
def _create_schema_backup(
|
|
1072
|
+
self, *, source_version: str, backup: Path, marker: Path
|
|
1073
|
+
) -> None:
|
|
1074
|
+
_secure_repository_directory(self.root, backup.parent)
|
|
1075
|
+
temporary = backup.with_name(f".{backup.name}.{secrets.token_hex(8)}.tmp")
|
|
1076
|
+
try:
|
|
1077
|
+
_validate_database_file(backup)
|
|
1078
|
+
with closing(
|
|
1079
|
+
sqlite3.connect(self.path, timeout=self.busy_timeout_ms / 1_000)
|
|
1080
|
+
) as source:
|
|
1081
|
+
with closing(sqlite3.connect(temporary)) as destination:
|
|
1082
|
+
source.backup(destination)
|
|
1083
|
+
integrity = destination.execute("PRAGMA integrity_check").fetchone()
|
|
1084
|
+
version = destination.execute(
|
|
1085
|
+
"SELECT value FROM schema_meta WHERE key='schema_version'"
|
|
1086
|
+
).fetchone()
|
|
1087
|
+
if integrity != ("ok",) or version != (source_version,):
|
|
1088
|
+
raise StorageError("storage_backup_failed")
|
|
1089
|
+
_secure_file(temporary)
|
|
1090
|
+
os.replace(temporary, backup)
|
|
1091
|
+
_secure_file(backup)
|
|
1092
|
+
evidence = json.dumps(
|
|
1093
|
+
{
|
|
1094
|
+
"backup_sha256": self._file_sha256(backup),
|
|
1095
|
+
"schema_version": source_version,
|
|
1096
|
+
},
|
|
1097
|
+
sort_keys=True,
|
|
1098
|
+
separators=(",", ":"),
|
|
1099
|
+
).encode("utf-8")
|
|
1100
|
+
self._atomic_private_write(marker, evidence)
|
|
1101
|
+
except StorageError:
|
|
1102
|
+
raise
|
|
1103
|
+
except sqlite3.Error as exc:
|
|
1104
|
+
raise StorageError("storage_backup_failed") from exc
|
|
1105
|
+
except OSError as exc:
|
|
1106
|
+
raise StorageError("storage_backup_failed") from exc
|
|
1107
|
+
finally:
|
|
1108
|
+
try:
|
|
1109
|
+
temporary.unlink(missing_ok=True)
|
|
1110
|
+
except OSError:
|
|
1111
|
+
pass
|
|
1112
|
+
|
|
1113
|
+
@staticmethod
|
|
1114
|
+
def _migrate_v3_cli_cutover(connection: sqlite3.Connection) -> None:
|
|
1115
|
+
required = {
|
|
1116
|
+
"attempt_id",
|
|
1117
|
+
"status",
|
|
1118
|
+
"failure_reason",
|
|
1119
|
+
"inventory_digest",
|
|
1120
|
+
}
|
|
1121
|
+
exists = connection.execute(
|
|
1122
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='execution_attempts'"
|
|
1123
|
+
).fetchone()
|
|
1124
|
+
if exists is None:
|
|
1125
|
+
raise StorageError("storage_migration_quarantined")
|
|
1126
|
+
columns = {
|
|
1127
|
+
str(row[1]) for row in connection.execute("PRAGMA table_info(execution_attempts)")
|
|
1128
|
+
}
|
|
1129
|
+
if not required <= columns:
|
|
1130
|
+
raise StorageError("storage_migration_quarantined")
|
|
1131
|
+
connection.execute(
|
|
1132
|
+
"""UPDATE execution_attempts
|
|
1133
|
+
SET status='legacy_unrecoverable', failure_reason='legacy_provider_retired'
|
|
1134
|
+
WHERE status IN ('pending', 'persistence_failed')"""
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
@staticmethod
|
|
1138
|
+
def _validate_v4_schema(connection: sqlite3.Connection) -> None:
|
|
1139
|
+
required_tables = {
|
|
1140
|
+
"capability_snapshots",
|
|
1141
|
+
"task_worktrees",
|
|
1142
|
+
"cli_execution_attempts",
|
|
1143
|
+
"validation_results",
|
|
1144
|
+
"review_links",
|
|
1145
|
+
}
|
|
1146
|
+
existing_tables = {
|
|
1147
|
+
str(row[0])
|
|
1148
|
+
for row in connection.execute(
|
|
1149
|
+
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
1150
|
+
)
|
|
1151
|
+
}
|
|
1152
|
+
if not required_tables <= existing_tables:
|
|
1153
|
+
raise StorageError("storage_rollback_required")
|
|
1154
|
+
cli_columns = {
|
|
1155
|
+
str(row[1])
|
|
1156
|
+
for row in connection.execute("PRAGMA table_info(cli_execution_attempts)")
|
|
1157
|
+
}
|
|
1158
|
+
if not {
|
|
1159
|
+
"provider",
|
|
1160
|
+
"effective_model",
|
|
1161
|
+
"capability_snapshot_digest",
|
|
1162
|
+
"worktree_id",
|
|
1163
|
+
"status",
|
|
1164
|
+
} <= cli_columns:
|
|
1165
|
+
raise StorageError("storage_rollback_required")
|
|
1166
|
+
lifecycle_tables = {
|
|
1167
|
+
"lifecycle_snapshot_bindings",
|
|
1168
|
+
"lifecycle_approval_bindings",
|
|
1169
|
+
"lifecycle_attempt_bindings",
|
|
1170
|
+
}
|
|
1171
|
+
if lifecycle_tables & existing_tables:
|
|
1172
|
+
raise StorageError("storage_rollback_required")
|
|
1173
|
+
|
|
1174
|
+
@staticmethod
|
|
1175
|
+
def _validate_v5_schema(connection: sqlite3.Connection) -> None:
|
|
1176
|
+
RepositoryStore._validate_v4_core_schema(connection)
|
|
1177
|
+
required_tables = {
|
|
1178
|
+
"lifecycle_snapshot_bindings",
|
|
1179
|
+
"lifecycle_approval_bindings",
|
|
1180
|
+
"lifecycle_attempt_bindings",
|
|
1181
|
+
}
|
|
1182
|
+
existing_tables = {
|
|
1183
|
+
str(row[0])
|
|
1184
|
+
for row in connection.execute(
|
|
1185
|
+
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
1186
|
+
)
|
|
1187
|
+
}
|
|
1188
|
+
if not required_tables <= existing_tables:
|
|
1189
|
+
raise StorageError("storage_rollback_required")
|
|
1190
|
+
|
|
1191
|
+
@staticmethod
|
|
1192
|
+
def _validate_v6_schema(connection: sqlite3.Connection) -> None:
|
|
1193
|
+
RepositoryStore._validate_v5_schema(connection)
|
|
1194
|
+
for table in _PRE_V6_WIDENED_TABLES:
|
|
1195
|
+
row = connection.execute(
|
|
1196
|
+
"SELECT sql FROM sqlite_master WHERE type='table' AND name=?",
|
|
1197
|
+
(table,),
|
|
1198
|
+
).fetchone()
|
|
1199
|
+
if row is None or "'openrouter'" not in str(row[0]):
|
|
1200
|
+
raise StorageError("storage_rollback_required")
|
|
1201
|
+
|
|
1202
|
+
def _migrate_pre_v6_provider_widening(self) -> None:
|
|
1203
|
+
"""Rebuild the two provider-constrained tables to admit 'openrouter'.
|
|
1204
|
+
|
|
1205
|
+
Runs on a dedicated connection before the main initialize transaction
|
|
1206
|
+
because both tables are foreign-key parents: the rebuild copies rows
|
|
1207
|
+
aside, recreates each table under its original name (so child
|
|
1208
|
+
REFERENCES clauses are never rewritten), and restores the rows, with
|
|
1209
|
+
foreign-key enforcement off for the duration and a full
|
|
1210
|
+
foreign_key_check before commit. Guard triggers dropped with the old
|
|
1211
|
+
tables are recreated by the normal schema pass that follows.
|
|
1212
|
+
"""
|
|
1213
|
+
if not self.path.exists():
|
|
1214
|
+
return
|
|
1215
|
+
connection: sqlite3.Connection | None = None
|
|
1216
|
+
try:
|
|
1217
|
+
connection = sqlite3.connect(
|
|
1218
|
+
self.path,
|
|
1219
|
+
timeout=self.busy_timeout_ms / 1_000,
|
|
1220
|
+
isolation_level=None,
|
|
1221
|
+
)
|
|
1222
|
+
try:
|
|
1223
|
+
version = connection.execute(
|
|
1224
|
+
"SELECT value FROM schema_meta WHERE key='schema_version'"
|
|
1225
|
+
).fetchone()
|
|
1226
|
+
except sqlite3.Error:
|
|
1227
|
+
return
|
|
1228
|
+
if version is None or version[0] not in {"4", "5"}:
|
|
1229
|
+
return
|
|
1230
|
+
table_sql: dict[str, str] = {}
|
|
1231
|
+
for table in _PRE_V6_WIDENED_TABLES:
|
|
1232
|
+
row = connection.execute(
|
|
1233
|
+
"SELECT sql FROM sqlite_master WHERE type='table' AND name=?",
|
|
1234
|
+
(table,),
|
|
1235
|
+
).fetchone()
|
|
1236
|
+
if row is None:
|
|
1237
|
+
return
|
|
1238
|
+
table_sql[table] = str(row[0])
|
|
1239
|
+
if all("'openrouter'" in sql for sql in table_sql.values()):
|
|
1240
|
+
return
|
|
1241
|
+
self._create_schema_backup(
|
|
1242
|
+
source_version=version[0],
|
|
1243
|
+
backup=self._pre_v6_backup_path(version[0]),
|
|
1244
|
+
marker=self._pre_v6_backup_marker_path(version[0]),
|
|
1245
|
+
)
|
|
1246
|
+
connection.execute("PRAGMA foreign_keys=OFF")
|
|
1247
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
1248
|
+
for table, (ddl, columns) in _PRE_V6_WIDENED_TABLES.items():
|
|
1249
|
+
before = connection.execute(
|
|
1250
|
+
f"SELECT COUNT(*) FROM {table}"
|
|
1251
|
+
).fetchone()[0]
|
|
1252
|
+
connection.execute(
|
|
1253
|
+
f"CREATE TABLE {table}_pre_v6_copy AS SELECT * FROM {table}"
|
|
1254
|
+
)
|
|
1255
|
+
connection.execute(f"DROP TABLE {table}")
|
|
1256
|
+
connection.execute(ddl)
|
|
1257
|
+
connection.execute(
|
|
1258
|
+
f"INSERT INTO {table} ({columns}) "
|
|
1259
|
+
f"SELECT {columns} FROM {table}_pre_v6_copy"
|
|
1260
|
+
)
|
|
1261
|
+
connection.execute(f"DROP TABLE {table}_pre_v6_copy")
|
|
1262
|
+
after = connection.execute(
|
|
1263
|
+
f"SELECT COUNT(*) FROM {table}"
|
|
1264
|
+
).fetchone()[0]
|
|
1265
|
+
if before != after:
|
|
1266
|
+
raise StorageError("storage_migration_quarantined")
|
|
1267
|
+
if connection.execute("PRAGMA foreign_key_check").fetchall():
|
|
1268
|
+
raise StorageError("storage_migration_quarantined")
|
|
1269
|
+
connection.execute("COMMIT")
|
|
1270
|
+
if connection.execute("PRAGMA integrity_check").fetchone() != ("ok",):
|
|
1271
|
+
raise StorageError("storage_corrupt")
|
|
1272
|
+
except StorageError:
|
|
1273
|
+
if connection is not None and connection.in_transaction:
|
|
1274
|
+
connection.execute("ROLLBACK")
|
|
1275
|
+
raise
|
|
1276
|
+
except sqlite3.Error as exc:
|
|
1277
|
+
if connection is not None and connection.in_transaction:
|
|
1278
|
+
connection.execute("ROLLBACK")
|
|
1279
|
+
raise _translate_database_error(exc) from exc
|
|
1280
|
+
finally:
|
|
1281
|
+
if connection is not None:
|
|
1282
|
+
connection.close()
|
|
1283
|
+
|
|
1284
|
+
def _migrate_v6_to_v7(self) -> None:
|
|
1285
|
+
"""Rebuild provider-CHECK tables to admit 'zai' (schema v6->v7)."""
|
|
1286
|
+
if not self.path.exists():
|
|
1287
|
+
return
|
|
1288
|
+
connection: sqlite3.Connection | None = None
|
|
1289
|
+
try:
|
|
1290
|
+
connection = sqlite3.connect(
|
|
1291
|
+
self.path,
|
|
1292
|
+
timeout=self.busy_timeout_ms / 1_000,
|
|
1293
|
+
isolation_level=None,
|
|
1294
|
+
)
|
|
1295
|
+
try:
|
|
1296
|
+
version = connection.execute(
|
|
1297
|
+
"SELECT value FROM schema_meta WHERE key='schema_version'"
|
|
1298
|
+
).fetchone()
|
|
1299
|
+
except sqlite3.Error:
|
|
1300
|
+
return
|
|
1301
|
+
if version is None or version[0] != "6":
|
|
1302
|
+
return
|
|
1303
|
+
table_sql: dict[str, str] = {}
|
|
1304
|
+
for table in _PRE_V6_WIDENED_TABLES:
|
|
1305
|
+
row = connection.execute(
|
|
1306
|
+
"SELECT sql FROM sqlite_master WHERE type='table' AND name=?",
|
|
1307
|
+
(table,),
|
|
1308
|
+
).fetchone()
|
|
1309
|
+
if row is None:
|
|
1310
|
+
return
|
|
1311
|
+
table_sql[table] = str(row[0])
|
|
1312
|
+
if all("'zai'" in sql for sql in table_sql.values()):
|
|
1313
|
+
return
|
|
1314
|
+
self._create_schema_backup(
|
|
1315
|
+
source_version=version[0],
|
|
1316
|
+
backup=self._pre_v7_backup_path(version[0]),
|
|
1317
|
+
marker=self._pre_v7_backup_marker_path(version[0]),
|
|
1318
|
+
)
|
|
1319
|
+
connection.execute("PRAGMA foreign_keys=OFF")
|
|
1320
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
1321
|
+
for table, (ddl, columns) in _PRE_V6_WIDENED_TABLES.items():
|
|
1322
|
+
before = connection.execute(
|
|
1323
|
+
f"SELECT COUNT(*) FROM {table}"
|
|
1324
|
+
).fetchone()[0]
|
|
1325
|
+
connection.execute(
|
|
1326
|
+
f"CREATE TABLE {table}_pre_v7_copy AS SELECT * FROM {table}"
|
|
1327
|
+
)
|
|
1328
|
+
connection.execute(f"DROP TABLE {table}")
|
|
1329
|
+
connection.execute(ddl)
|
|
1330
|
+
connection.execute(
|
|
1331
|
+
f"INSERT INTO {table} ({columns}) "
|
|
1332
|
+
f"SELECT {columns} FROM {table}_pre_v7_copy"
|
|
1333
|
+
)
|
|
1334
|
+
connection.execute(f"DROP TABLE {table}_pre_v7_copy")
|
|
1335
|
+
after = connection.execute(
|
|
1336
|
+
f"SELECT COUNT(*) FROM {table}"
|
|
1337
|
+
).fetchone()[0]
|
|
1338
|
+
if before != after:
|
|
1339
|
+
raise StorageError("storage_migration_quarantined")
|
|
1340
|
+
if connection.execute("PRAGMA foreign_key_check").fetchall():
|
|
1341
|
+
raise StorageError("storage_migration_quarantined")
|
|
1342
|
+
connection.execute("COMMIT")
|
|
1343
|
+
if connection.execute("PRAGMA integrity_check").fetchone() != ("ok",):
|
|
1344
|
+
raise StorageError("storage_corrupt")
|
|
1345
|
+
except StorageError:
|
|
1346
|
+
if connection is not None and connection.in_transaction:
|
|
1347
|
+
connection.execute("ROLLBACK")
|
|
1348
|
+
raise
|
|
1349
|
+
except sqlite3.Error as exc:
|
|
1350
|
+
if connection is not None and connection.in_transaction:
|
|
1351
|
+
connection.execute("ROLLBACK")
|
|
1352
|
+
raise _translate_database_error(exc) from exc
|
|
1353
|
+
finally:
|
|
1354
|
+
if connection is not None:
|
|
1355
|
+
connection.close()
|
|
1356
|
+
|
|
1357
|
+
def _migrate_v7_to_v8(self) -> None:
|
|
1358
|
+
"""Add the carry table era: swap the approval insert guard (schema v7->v8)."""
|
|
1359
|
+
if not self.path.exists():
|
|
1360
|
+
return
|
|
1361
|
+
connection: sqlite3.Connection | None = None
|
|
1362
|
+
try:
|
|
1363
|
+
connection = sqlite3.connect(
|
|
1364
|
+
self.path,
|
|
1365
|
+
timeout=self.busy_timeout_ms / 1_000,
|
|
1366
|
+
isolation_level=None,
|
|
1367
|
+
)
|
|
1368
|
+
try:
|
|
1369
|
+
version = connection.execute(
|
|
1370
|
+
"SELECT value FROM schema_meta WHERE key='schema_version'"
|
|
1371
|
+
).fetchone()
|
|
1372
|
+
except sqlite3.Error:
|
|
1373
|
+
return
|
|
1374
|
+
if version is None or version[0] != "7":
|
|
1375
|
+
return
|
|
1376
|
+
self._create_schema_backup(
|
|
1377
|
+
source_version=version[0],
|
|
1378
|
+
backup=self._pre_v8_backup_path(version[0]),
|
|
1379
|
+
marker=self._pre_v8_backup_marker_path(version[0]),
|
|
1380
|
+
)
|
|
1381
|
+
except sqlite3.Error as exc:
|
|
1382
|
+
if connection is not None and connection.in_transaction:
|
|
1383
|
+
connection.rollback()
|
|
1384
|
+
raise _translate_database_error(exc) from exc
|
|
1385
|
+
finally:
|
|
1386
|
+
if connection is not None:
|
|
1387
|
+
connection.close()
|
|
1388
|
+
|
|
1389
|
+
@staticmethod
|
|
1390
|
+
def _validate_v4_core_schema(connection: sqlite3.Connection) -> None:
|
|
1391
|
+
required_tables = {
|
|
1392
|
+
"capability_snapshots",
|
|
1393
|
+
"task_worktrees",
|
|
1394
|
+
"cli_execution_attempts",
|
|
1395
|
+
"validation_results",
|
|
1396
|
+
"review_links",
|
|
1397
|
+
}
|
|
1398
|
+
existing_tables = {
|
|
1399
|
+
str(row[0])
|
|
1400
|
+
for row in connection.execute(
|
|
1401
|
+
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
1402
|
+
)
|
|
1403
|
+
}
|
|
1404
|
+
if not required_tables <= existing_tables:
|
|
1405
|
+
raise StorageError("storage_rollback_required")
|
|
1406
|
+
cli_columns = {
|
|
1407
|
+
str(row[1])
|
|
1408
|
+
for row in connection.execute("PRAGMA table_info(cli_execution_attempts)")
|
|
1409
|
+
}
|
|
1410
|
+
if not {
|
|
1411
|
+
"provider",
|
|
1412
|
+
"effective_model",
|
|
1413
|
+
"capability_snapshot_digest",
|
|
1414
|
+
"worktree_id",
|
|
1415
|
+
"status",
|
|
1416
|
+
} <= cli_columns:
|
|
1417
|
+
raise StorageError("storage_rollback_required")
|
|
1418
|
+
|
|
1419
|
+
@staticmethod
|
|
1420
|
+
def _migrate_v1_attempts(connection: sqlite3.Connection) -> None:
|
|
1421
|
+
exists = connection.execute(
|
|
1422
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='execution_attempts'"
|
|
1423
|
+
).fetchone()
|
|
1424
|
+
if exists is None:
|
|
1425
|
+
return
|
|
1426
|
+
columns = {
|
|
1427
|
+
str(row[1]) for row in connection.execute("PRAGMA table_info(execution_attempts)")
|
|
1428
|
+
}
|
|
1429
|
+
if {"max_input_tokens", "max_output_tokens", "expected_prompt_hash"} <= columns:
|
|
1430
|
+
return
|
|
1431
|
+
connection.execute("ALTER TABLE execution_attempts RENAME TO execution_attempts_v1")
|
|
1432
|
+
connection.execute(
|
|
1433
|
+
"""CREATE TABLE execution_attempts (
|
|
1434
|
+
attempt_id TEXT PRIMARY KEY,
|
|
1435
|
+
approval_id TEXT NOT NULL UNIQUE REFERENCES approvals(approval_id) ON DELETE RESTRICT,
|
|
1436
|
+
task_id TEXT NOT NULL REFERENCES tasks(task_id) ON DELETE RESTRICT,
|
|
1437
|
+
decision_id TEXT NOT NULL REFERENCES decisions(decision_id) ON DELETE RESTRICT,
|
|
1438
|
+
manifest_hash TEXT NOT NULL,
|
|
1439
|
+
graph_fingerprint TEXT NOT NULL,
|
|
1440
|
+
model_id TEXT NOT NULL,
|
|
1441
|
+
effort TEXT NOT NULL,
|
|
1442
|
+
reserved_tokens INTEGER NOT NULL,
|
|
1443
|
+
max_input_tokens INTEGER,
|
|
1444
|
+
max_output_tokens INTEGER,
|
|
1445
|
+
expected_prompt_hash TEXT,
|
|
1446
|
+
status TEXT NOT NULL CHECK(status IN (
|
|
1447
|
+
'pending', 'completed', 'failed', 'persistence_failed', 'legacy_unrecoverable'
|
|
1448
|
+
)),
|
|
1449
|
+
failure_reason TEXT,
|
|
1450
|
+
execution_id TEXT UNIQUE REFERENCES executions(execution_id) ON DELETE RESTRICT,
|
|
1451
|
+
created_at INTEGER NOT NULL,
|
|
1452
|
+
updated_at INTEGER NOT NULL,
|
|
1453
|
+
CHECK(
|
|
1454
|
+
status IN ('completed', 'failed', 'legacy_unrecoverable')
|
|
1455
|
+
OR (
|
|
1456
|
+
max_input_tokens IS NOT NULL AND max_input_tokens > 0
|
|
1457
|
+
AND max_output_tokens IS NOT NULL AND max_output_tokens > 0
|
|
1458
|
+
AND expected_prompt_hash IS NOT NULL
|
|
1459
|
+
)
|
|
1460
|
+
)
|
|
1461
|
+
)"""
|
|
1462
|
+
)
|
|
1463
|
+
connection.execute(
|
|
1464
|
+
"""INSERT INTO execution_attempts(
|
|
1465
|
+
attempt_id, approval_id, task_id, decision_id, manifest_hash,
|
|
1466
|
+
graph_fingerprint, model_id, effort, reserved_tokens,
|
|
1467
|
+
max_input_tokens, max_output_tokens, expected_prompt_hash,
|
|
1468
|
+
status, failure_reason, execution_id, created_at, updated_at
|
|
1469
|
+
)
|
|
1470
|
+
SELECT attempt_id, approval_id, task_id, decision_id, manifest_hash,
|
|
1471
|
+
graph_fingerprint, model_id, effort, reserved_tokens,
|
|
1472
|
+
NULL, NULL, NULL,
|
|
1473
|
+
CASE WHEN status IN ('pending', 'persistence_failed')
|
|
1474
|
+
THEN 'legacy_unrecoverable' ELSE status END,
|
|
1475
|
+
CASE WHEN status IN ('pending', 'persistence_failed')
|
|
1476
|
+
THEN 'legacy_attempt_bindings_missing' ELSE failure_reason END,
|
|
1477
|
+
execution_id, created_at, updated_at
|
|
1478
|
+
FROM execution_attempts_v1"""
|
|
1479
|
+
)
|
|
1480
|
+
connection.execute("DROP TABLE execution_attempts_v1")
|
|
1481
|
+
|
|
1482
|
+
@staticmethod
|
|
1483
|
+
def _migrate_v2_digest(connection: sqlite3.Connection) -> None:
|
|
1484
|
+
exists = connection.execute(
|
|
1485
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='execution_attempts'"
|
|
1486
|
+
).fetchone()
|
|
1487
|
+
if exists is None:
|
|
1488
|
+
return
|
|
1489
|
+
attempt_columns = {
|
|
1490
|
+
str(row[1]) for row in connection.execute("PRAGMA table_info(execution_attempts)")
|
|
1491
|
+
}
|
|
1492
|
+
if "inventory_digest" not in attempt_columns:
|
|
1493
|
+
connection.execute("ALTER TABLE execution_attempts ADD COLUMN inventory_digest TEXT")
|
|
1494
|
+
connection.execute(
|
|
1495
|
+
"""UPDATE execution_attempts
|
|
1496
|
+
SET status = 'legacy_unrecoverable',
|
|
1497
|
+
failure_reason = 'legacy_attempt_digest_missing'
|
|
1498
|
+
WHERE status IN ('pending', 'persistence_failed')
|
|
1499
|
+
AND inventory_digest IS NULL"""
|
|
1500
|
+
)
|
|
1501
|
+
staged_exists = connection.execute(
|
|
1502
|
+
"SELECT 1 FROM sqlite_master "
|
|
1503
|
+
"WHERE type='table' AND name='staged_execution_receipts'"
|
|
1504
|
+
).fetchone()
|
|
1505
|
+
if staged_exists is not None:
|
|
1506
|
+
staged_columns = {
|
|
1507
|
+
str(row[1])
|
|
1508
|
+
for row in connection.execute("PRAGMA table_info(staged_execution_receipts)")
|
|
1509
|
+
}
|
|
1510
|
+
if "inventory_digest" not in staged_columns:
|
|
1511
|
+
connection.execute(
|
|
1512
|
+
"ALTER TABLE staged_execution_receipts ADD COLUMN inventory_digest TEXT"
|
|
1513
|
+
)
|
|
1514
|
+
|
|
1515
|
+
def integrity_check(self) -> str:
|
|
1516
|
+
try:
|
|
1517
|
+
with self._connection() as connection:
|
|
1518
|
+
row = connection.execute("PRAGMA integrity_check").fetchone()
|
|
1519
|
+
except sqlite3.Error as exc:
|
|
1520
|
+
raise _translate_database_error(exc) from exc
|
|
1521
|
+
if row is None or row[0] != "ok":
|
|
1522
|
+
raise StorageError("storage_corrupt")
|
|
1523
|
+
return "ok"
|
|
1524
|
+
|
|
1525
|
+
def pragma_state(self) -> dict[str, int | str]:
|
|
1526
|
+
with self._connection() as connection:
|
|
1527
|
+
foreign_keys = int(connection.execute("PRAGMA foreign_keys").fetchone()[0])
|
|
1528
|
+
journal_mode = str(connection.execute("PRAGMA journal_mode").fetchone()[0]).casefold()
|
|
1529
|
+
busy_timeout = int(connection.execute("PRAGMA busy_timeout").fetchone()[0])
|
|
1530
|
+
return {
|
|
1531
|
+
"foreign_keys": foreign_keys,
|
|
1532
|
+
"journal_mode": journal_mode,
|
|
1533
|
+
"busy_timeout": busy_timeout,
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
def record_task(
|
|
1537
|
+
self,
|
|
1538
|
+
task_id: str,
|
|
1539
|
+
category: str,
|
|
1540
|
+
risk: str,
|
|
1541
|
+
objective_hash: str,
|
|
1542
|
+
created_at: int,
|
|
1543
|
+
) -> bool:
|
|
1544
|
+
task_id = _identifier(task_id, "task_id_invalid")
|
|
1545
|
+
category = TaskCategory(category).value
|
|
1546
|
+
risk = RiskTier(risk).value
|
|
1547
|
+
if not _HEX_64.fullmatch(objective_hash):
|
|
1548
|
+
raise ValueError("objective_hash_invalid")
|
|
1549
|
+
created_at = _nonnegative_integer(created_at, "created_at_invalid")
|
|
1550
|
+
try:
|
|
1551
|
+
with self._connection() as connection:
|
|
1552
|
+
cursor = connection.execute(
|
|
1553
|
+
"INSERT OR IGNORE INTO tasks(task_id, category, risk, objective_hash, created_at) "
|
|
1554
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
1555
|
+
(task_id, category, risk, objective_hash, created_at),
|
|
1556
|
+
)
|
|
1557
|
+
return cursor.rowcount == 1
|
|
1558
|
+
except sqlite3.Error as exc:
|
|
1559
|
+
raise _translate_database_error(exc) from exc
|
|
1560
|
+
|
|
1561
|
+
def record_decision(
|
|
1562
|
+
self,
|
|
1563
|
+
decision_id: str,
|
|
1564
|
+
task_id: str,
|
|
1565
|
+
model_id: str | None,
|
|
1566
|
+
effort: str | None,
|
|
1567
|
+
policy_version: str,
|
|
1568
|
+
evidence_version: str,
|
|
1569
|
+
created_at: int,
|
|
1570
|
+
) -> bool:
|
|
1571
|
+
values = (
|
|
1572
|
+
_identifier(decision_id, "decision_id_invalid"),
|
|
1573
|
+
_identifier(task_id, "task_id_invalid"),
|
|
1574
|
+
None if model_id is None else _identifier(model_id, "model_id_invalid"),
|
|
1575
|
+
None if effort is None else Effort(effort).value,
|
|
1576
|
+
_identifier(policy_version, "policy_version_invalid"),
|
|
1577
|
+
_identifier(evidence_version, "evidence_version_invalid"),
|
|
1578
|
+
_nonnegative_integer(created_at, "created_at_invalid"),
|
|
1579
|
+
)
|
|
1580
|
+
try:
|
|
1581
|
+
with self._connection() as connection:
|
|
1582
|
+
cursor = connection.execute(
|
|
1583
|
+
"""INSERT OR IGNORE INTO decisions(
|
|
1584
|
+
decision_id, task_id, model_id, effort, policy_version,
|
|
1585
|
+
evidence_version, created_at
|
|
1586
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
1587
|
+
values,
|
|
1588
|
+
)
|
|
1589
|
+
return cursor.rowcount == 1
|
|
1590
|
+
except sqlite3.Error as exc:
|
|
1591
|
+
raise _translate_database_error(exc) from exc
|
|
1592
|
+
|
|
1593
|
+
def insert_execution(
|
|
1594
|
+
self,
|
|
1595
|
+
*,
|
|
1596
|
+
execution_id: str,
|
|
1597
|
+
idempotency_key: str,
|
|
1598
|
+
task_id: str | None,
|
|
1599
|
+
decision_id: str | None,
|
|
1600
|
+
approval_id: str | None,
|
|
1601
|
+
model_id: str,
|
|
1602
|
+
effort: str,
|
|
1603
|
+
status: str,
|
|
1604
|
+
reserved_tokens: int,
|
|
1605
|
+
created_at: int,
|
|
1606
|
+
) -> bool:
|
|
1607
|
+
values = {
|
|
1608
|
+
"execution_id": _identifier(execution_id, "execution_id_invalid"),
|
|
1609
|
+
"idempotency_key": _identifier(idempotency_key, "idempotency_key_invalid"),
|
|
1610
|
+
"task_id": None if task_id is None else _identifier(task_id, "task_id_invalid"),
|
|
1611
|
+
"decision_id": None if decision_id is None else _identifier(decision_id, "decision_id_invalid"),
|
|
1612
|
+
"approval_id": None if approval_id is None else _identifier(approval_id, "approval_id_invalid"),
|
|
1613
|
+
"model_id": _identifier(model_id, "model_id_invalid"),
|
|
1614
|
+
"effort": Effort(effort).value,
|
|
1615
|
+
"status": _identifier(status, "status_invalid"),
|
|
1616
|
+
"reserved_tokens": _nonnegative_integer(reserved_tokens, "reserved_tokens_invalid"),
|
|
1617
|
+
"created_at": _nonnegative_integer(created_at, "created_at_invalid"),
|
|
1618
|
+
}
|
|
1619
|
+
connection: sqlite3.Connection | None = None
|
|
1620
|
+
try:
|
|
1621
|
+
connection = self._connect()
|
|
1622
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
1623
|
+
existing = connection.execute(
|
|
1624
|
+
"SELECT execution_id FROM executions WHERE idempotency_key = ?",
|
|
1625
|
+
(values["idempotency_key"],),
|
|
1626
|
+
).fetchone()
|
|
1627
|
+
if existing is not None:
|
|
1628
|
+
connection.rollback()
|
|
1629
|
+
return False
|
|
1630
|
+
connection.execute(
|
|
1631
|
+
"""INSERT INTO executions(
|
|
1632
|
+
execution_id, idempotency_key, task_id, decision_id, approval_id,
|
|
1633
|
+
model_id, effort, status, reserved_tokens, created_at
|
|
1634
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
1635
|
+
tuple(values.values()),
|
|
1636
|
+
)
|
|
1637
|
+
connection.commit()
|
|
1638
|
+
return True
|
|
1639
|
+
except sqlite3.Error as exc:
|
|
1640
|
+
if connection is not None and connection.in_transaction:
|
|
1641
|
+
connection.rollback()
|
|
1642
|
+
raise _translate_database_error(exc) from exc
|
|
1643
|
+
finally:
|
|
1644
|
+
if connection is not None:
|
|
1645
|
+
connection.close()
|
|
1646
|
+
|
|
1647
|
+
def record_outcome(
|
|
1648
|
+
self,
|
|
1649
|
+
outcome_id: str,
|
|
1650
|
+
execution_id: str,
|
|
1651
|
+
provenance: str,
|
|
1652
|
+
success: bool,
|
|
1653
|
+
severe_failure: bool,
|
|
1654
|
+
recorded_at: int,
|
|
1655
|
+
) -> bool:
|
|
1656
|
+
outcome_id = _identifier(outcome_id, "outcome_id_invalid")
|
|
1657
|
+
execution_id = _identifier(execution_id, "execution_id_invalid")
|
|
1658
|
+
if provenance not in {"machine_verified", "ci_imported", "human", "pairwise", "reversion", "ambiguous"}:
|
|
1659
|
+
raise ValueError("provenance_invalid")
|
|
1660
|
+
if not isinstance(success, bool) or not isinstance(severe_failure, bool):
|
|
1661
|
+
raise ValueError("outcome_invalid")
|
|
1662
|
+
recorded_at = _nonnegative_integer(recorded_at, "recorded_at_invalid")
|
|
1663
|
+
try:
|
|
1664
|
+
with self._connection() as connection:
|
|
1665
|
+
cursor = connection.execute(
|
|
1666
|
+
"INSERT OR IGNORE INTO outcomes(outcome_id, execution_id, provenance, success, severe_failure, recorded_at) "
|
|
1667
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
1668
|
+
(outcome_id, execution_id, provenance, int(success), int(severe_failure), recorded_at),
|
|
1669
|
+
)
|
|
1670
|
+
return cursor.rowcount == 1
|
|
1671
|
+
except sqlite3.Error as exc:
|
|
1672
|
+
raise _translate_database_error(exc) from exc
|
|
1673
|
+
|
|
1674
|
+
def link_execution_evidence(
|
|
1675
|
+
self,
|
|
1676
|
+
execution_id: str,
|
|
1677
|
+
task_id: str,
|
|
1678
|
+
decision_id: str,
|
|
1679
|
+
graph_fingerprint: str,
|
|
1680
|
+
) -> bool:
|
|
1681
|
+
values = (
|
|
1682
|
+
_identifier(execution_id, "execution_id_invalid"),
|
|
1683
|
+
_identifier(task_id, "task_id_invalid"),
|
|
1684
|
+
_identifier(decision_id, "decision_id_invalid"),
|
|
1685
|
+
graph_fingerprint,
|
|
1686
|
+
)
|
|
1687
|
+
if not _HEX_64.fullmatch(graph_fingerprint):
|
|
1688
|
+
raise ValueError("graph_fingerprint_invalid")
|
|
1689
|
+
try:
|
|
1690
|
+
with self._connection() as connection:
|
|
1691
|
+
cursor = connection.execute(
|
|
1692
|
+
"""INSERT OR IGNORE INTO execution_evidence(
|
|
1693
|
+
execution_id, task_id, decision_id, graph_fingerprint
|
|
1694
|
+
) VALUES (?, ?, ?, ?)""",
|
|
1695
|
+
values,
|
|
1696
|
+
)
|
|
1697
|
+
return cursor.rowcount == 1
|
|
1698
|
+
except sqlite3.Error as exc:
|
|
1699
|
+
raise _translate_database_error(exc) from exc
|
|
1700
|
+
|
|
1701
|
+
def execution_evidence(self, execution_id: str) -> dict[str, str] | None:
|
|
1702
|
+
execution_id = _identifier(execution_id, "execution_id_invalid")
|
|
1703
|
+
with self._connection() as connection:
|
|
1704
|
+
row = connection.execute(
|
|
1705
|
+
"""SELECT ee.task_id, ee.decision_id, ee.graph_fingerprint
|
|
1706
|
+
FROM execution_evidence ee
|
|
1707
|
+
JOIN executions e ON e.execution_id = ee.execution_id
|
|
1708
|
+
WHERE ee.execution_id = ?
|
|
1709
|
+
AND e.task_id = ee.task_id AND e.decision_id = ee.decision_id""",
|
|
1710
|
+
(execution_id,),
|
|
1711
|
+
).fetchone()
|
|
1712
|
+
return None if row is None else dict(row)
|
|
1713
|
+
|
|
1714
|
+
def create_execution_attempt(
|
|
1715
|
+
self,
|
|
1716
|
+
*,
|
|
1717
|
+
attempt_id: str,
|
|
1718
|
+
approval_id: str,
|
|
1719
|
+
task_id: str,
|
|
1720
|
+
decision_id: str,
|
|
1721
|
+
manifest_hash: str,
|
|
1722
|
+
graph_fingerprint: str,
|
|
1723
|
+
model_id: str,
|
|
1724
|
+
effort: str,
|
|
1725
|
+
reserved_tokens: int,
|
|
1726
|
+
max_input_tokens: int,
|
|
1727
|
+
max_output_tokens: int,
|
|
1728
|
+
expected_prompt_hash: str,
|
|
1729
|
+
inventory_digest: str,
|
|
1730
|
+
created_at: int,
|
|
1731
|
+
) -> bool:
|
|
1732
|
+
values = (
|
|
1733
|
+
_identifier(attempt_id, "attempt_id_invalid"),
|
|
1734
|
+
_identifier(approval_id, "approval_id_invalid"),
|
|
1735
|
+
_identifier(task_id, "task_id_invalid"),
|
|
1736
|
+
_identifier(decision_id, "decision_id_invalid"),
|
|
1737
|
+
manifest_hash,
|
|
1738
|
+
graph_fingerprint,
|
|
1739
|
+
_identifier(model_id, "model_id_invalid"),
|
|
1740
|
+
Effort(effort).value,
|
|
1741
|
+
_nonnegative_integer(reserved_tokens, "reserved_tokens_invalid"),
|
|
1742
|
+
_nonnegative_integer(max_input_tokens, "max_input_tokens_invalid"),
|
|
1743
|
+
_nonnegative_integer(max_output_tokens, "max_output_tokens_invalid"),
|
|
1744
|
+
expected_prompt_hash,
|
|
1745
|
+
inventory_digest,
|
|
1746
|
+
_nonnegative_integer(created_at, "created_at_invalid"),
|
|
1747
|
+
)
|
|
1748
|
+
if not _HEX_64.fullmatch(manifest_hash):
|
|
1749
|
+
raise ValueError("manifest_hash_invalid")
|
|
1750
|
+
if not _HEX_64.fullmatch(graph_fingerprint):
|
|
1751
|
+
raise ValueError("graph_fingerprint_invalid")
|
|
1752
|
+
if not _HEX_64.fullmatch(expected_prompt_hash):
|
|
1753
|
+
raise ValueError("prompt_hash_invalid")
|
|
1754
|
+
if not isinstance(inventory_digest, str) or not _HEX_64.fullmatch(inventory_digest):
|
|
1755
|
+
raise ValueError("inventory_digest_invalid")
|
|
1756
|
+
if values[9] < 1 or values[10] < 1 or values[9] + values[10] != values[8]:
|
|
1757
|
+
raise ValueError("token_bounds_invalid")
|
|
1758
|
+
connection: sqlite3.Connection | None = None
|
|
1759
|
+
try:
|
|
1760
|
+
connection = self._connect()
|
|
1761
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
1762
|
+
existing = connection.execute(
|
|
1763
|
+
"SELECT attempt_id, task_id, decision_id, manifest_hash, "
|
|
1764
|
+
"graph_fingerprint, model_id, effort, reserved_tokens, max_input_tokens, "
|
|
1765
|
+
"max_output_tokens, expected_prompt_hash, inventory_digest "
|
|
1766
|
+
"FROM execution_attempts WHERE approval_id = ?",
|
|
1767
|
+
(values[1],),
|
|
1768
|
+
).fetchone()
|
|
1769
|
+
if existing is not None:
|
|
1770
|
+
expected = (values[0], *values[2:13])
|
|
1771
|
+
if tuple(existing) != expected:
|
|
1772
|
+
raise StorageError("execution_attempt_conflict")
|
|
1773
|
+
connection.rollback()
|
|
1774
|
+
return False
|
|
1775
|
+
approval = connection.execute(
|
|
1776
|
+
"SELECT manifest_hash, reserved_tokens, status FROM approvals "
|
|
1777
|
+
"WHERE approval_id = ?",
|
|
1778
|
+
(values[1],),
|
|
1779
|
+
).fetchone()
|
|
1780
|
+
decision = connection.execute(
|
|
1781
|
+
"SELECT task_id, model_id, effort FROM decisions WHERE decision_id = ?",
|
|
1782
|
+
(values[3],),
|
|
1783
|
+
).fetchone()
|
|
1784
|
+
if (
|
|
1785
|
+
approval is None
|
|
1786
|
+
or tuple(approval) != (values[4], values[8], "issued")
|
|
1787
|
+
or decision is None
|
|
1788
|
+
or tuple(decision) != (values[2], values[6], values[7])
|
|
1789
|
+
):
|
|
1790
|
+
raise StorageError("execution_attempt_conflict")
|
|
1791
|
+
connection.execute(
|
|
1792
|
+
"""INSERT INTO execution_attempts(
|
|
1793
|
+
attempt_id, approval_id, task_id, decision_id, manifest_hash,
|
|
1794
|
+
graph_fingerprint, model_id, effort, reserved_tokens,
|
|
1795
|
+
max_input_tokens, max_output_tokens, expected_prompt_hash,
|
|
1796
|
+
inventory_digest, status,
|
|
1797
|
+
failure_reason, execution_id, created_at, updated_at
|
|
1798
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
1799
|
+
'pending', NULL, NULL, ?, ?)""",
|
|
1800
|
+
(*values[:13], values[13], values[13]),
|
|
1801
|
+
)
|
|
1802
|
+
connection.commit()
|
|
1803
|
+
return True
|
|
1804
|
+
except StorageError:
|
|
1805
|
+
if connection is not None and connection.in_transaction:
|
|
1806
|
+
connection.rollback()
|
|
1807
|
+
raise
|
|
1808
|
+
except sqlite3.IntegrityError as exc:
|
|
1809
|
+
if connection is not None and connection.in_transaction:
|
|
1810
|
+
connection.rollback()
|
|
1811
|
+
raise StorageError("execution_attempt_conflict") from exc
|
|
1812
|
+
except sqlite3.Error as exc:
|
|
1813
|
+
if connection is not None and connection.in_transaction:
|
|
1814
|
+
connection.rollback()
|
|
1815
|
+
raise _translate_database_error(exc) from exc
|
|
1816
|
+
finally:
|
|
1817
|
+
if connection is not None:
|
|
1818
|
+
connection.close()
|
|
1819
|
+
|
|
1820
|
+
def mark_execution_attempt_failed(
|
|
1821
|
+
self,
|
|
1822
|
+
attempt_id: str,
|
|
1823
|
+
reason: str,
|
|
1824
|
+
*,
|
|
1825
|
+
persistence_failed: bool = False,
|
|
1826
|
+
updated_at: int,
|
|
1827
|
+
) -> bool:
|
|
1828
|
+
attempt_id = _identifier(attempt_id, "attempt_id_invalid")
|
|
1829
|
+
reason = _identifier(reason, "failure_reason_invalid")
|
|
1830
|
+
updated_at = _nonnegative_integer(updated_at, "updated_at_invalid")
|
|
1831
|
+
status = "persistence_failed" if persistence_failed else "failed"
|
|
1832
|
+
try:
|
|
1833
|
+
with self._connection() as connection:
|
|
1834
|
+
cursor = connection.execute(
|
|
1835
|
+
"""UPDATE execution_attempts
|
|
1836
|
+
SET status = ?, failure_reason = ?, updated_at = ?
|
|
1837
|
+
WHERE attempt_id = ? AND status IN ('pending', 'persistence_failed')""",
|
|
1838
|
+
(status, reason, updated_at, attempt_id),
|
|
1839
|
+
)
|
|
1840
|
+
if cursor.rowcount == 1:
|
|
1841
|
+
return True
|
|
1842
|
+
row = connection.execute(
|
|
1843
|
+
"SELECT status, failure_reason FROM execution_attempts WHERE attempt_id = ?",
|
|
1844
|
+
(attempt_id,),
|
|
1845
|
+
).fetchone()
|
|
1846
|
+
if row is None:
|
|
1847
|
+
raise StorageError("execution_attempt_missing")
|
|
1848
|
+
if tuple(row) == (status, reason):
|
|
1849
|
+
return False
|
|
1850
|
+
raise StorageError("execution_attempt_conflict")
|
|
1851
|
+
except StorageError:
|
|
1852
|
+
raise
|
|
1853
|
+
except sqlite3.Error as exc:
|
|
1854
|
+
raise _translate_database_error(exc) from exc
|
|
1855
|
+
|
|
1856
|
+
@staticmethod
|
|
1857
|
+
def _validate_receipt_shape(receipt: ExecutionReceipt) -> None:
|
|
1858
|
+
if not isinstance(receipt, ExecutionReceipt):
|
|
1859
|
+
raise ValueError("execution_receipt_invalid")
|
|
1860
|
+
if (
|
|
1861
|
+
receipt.outcome is not ExecutionOutcome.SUCCEEDED
|
|
1862
|
+
or receipt.input_tokens is None
|
|
1863
|
+
or receipt.output_tokens is None
|
|
1864
|
+
or receipt.response_hash is None
|
|
1865
|
+
or receipt.failure_reason is not None
|
|
1866
|
+
):
|
|
1867
|
+
raise ValueError("execution_receipt_invalid")
|
|
1868
|
+
|
|
1869
|
+
@staticmethod
|
|
1870
|
+
def _receipt_tuple(receipt: ExecutionReceipt) -> tuple[Any, ...]:
|
|
1871
|
+
return (
|
|
1872
|
+
receipt.approval_id, receipt.model_id, receipt.effort.value,
|
|
1873
|
+
receipt.outcome.value, receipt.input_tokens, receipt.output_tokens,
|
|
1874
|
+
receipt.latency_ms, receipt.prompt_hash, receipt.response_hash, None,
|
|
1875
|
+
)
|
|
1876
|
+
|
|
1877
|
+
@staticmethod
|
|
1878
|
+
def _receipt_from_row(row: sqlite3.Row) -> ExecutionReceipt:
|
|
1879
|
+
return ExecutionReceipt(
|
|
1880
|
+
execution_id=str(row["execution_id"]),
|
|
1881
|
+
approval_id=str(row["approval_id"]),
|
|
1882
|
+
model_id=str(row["model_id"]),
|
|
1883
|
+
effort=Effort(str(row["effort"])),
|
|
1884
|
+
outcome=ExecutionOutcome(str(row["outcome"])),
|
|
1885
|
+
input_tokens=int(row["input_tokens"]),
|
|
1886
|
+
output_tokens=int(row["output_tokens"]),
|
|
1887
|
+
latency_ms=int(row["latency_ms"]),
|
|
1888
|
+
prompt_hash=str(row["prompt_hash"]),
|
|
1889
|
+
response_hash=str(row["response_hash"]),
|
|
1890
|
+
failure_reason=None,
|
|
1891
|
+
)
|
|
1892
|
+
|
|
1893
|
+
@staticmethod
|
|
1894
|
+
def _validate_attempt_binding(attempt: sqlite3.Row, receipt: ExecutionReceipt) -> None:
|
|
1895
|
+
if (
|
|
1896
|
+
attempt["approval_id"] != receipt.approval_id
|
|
1897
|
+
or attempt["model_id"] != receipt.model_id
|
|
1898
|
+
or attempt["effort"] != receipt.effort.value
|
|
1899
|
+
or attempt["max_input_tokens"] is None
|
|
1900
|
+
or attempt["max_output_tokens"] is None
|
|
1901
|
+
or attempt["expected_prompt_hash"] is None
|
|
1902
|
+
or receipt.input_tokens > attempt["max_input_tokens"]
|
|
1903
|
+
or receipt.output_tokens > attempt["max_output_tokens"]
|
|
1904
|
+
or receipt.input_tokens + receipt.output_tokens > attempt["reserved_tokens"]
|
|
1905
|
+
or receipt.prompt_hash != attempt["expected_prompt_hash"]
|
|
1906
|
+
):
|
|
1907
|
+
raise StorageError("execution_attempt_conflict")
|
|
1908
|
+
|
|
1909
|
+
@staticmethod
|
|
1910
|
+
def _assert_approval_consumed(
|
|
1911
|
+
connection: sqlite3.Connection, attempt: sqlite3.Row
|
|
1912
|
+
) -> None:
|
|
1913
|
+
approval = connection.execute(
|
|
1914
|
+
"SELECT status, manifest_hash, reserved_tokens FROM approvals WHERE approval_id = ?",
|
|
1915
|
+
(attempt["approval_id"],),
|
|
1916
|
+
).fetchone()
|
|
1917
|
+
decision = connection.execute(
|
|
1918
|
+
"SELECT task_id, model_id, effort FROM decisions WHERE decision_id = ?",
|
|
1919
|
+
(attempt["decision_id"],),
|
|
1920
|
+
).fetchone()
|
|
1921
|
+
if approval is None or approval["status"] != "consumed":
|
|
1922
|
+
raise StorageError("approval_not_consumed")
|
|
1923
|
+
if (
|
|
1924
|
+
approval["manifest_hash"] != attempt["manifest_hash"]
|
|
1925
|
+
or approval["reserved_tokens"] != attempt["reserved_tokens"]
|
|
1926
|
+
or decision is None
|
|
1927
|
+
or tuple(decision)
|
|
1928
|
+
!= (attempt["task_id"], attempt["model_id"], attempt["effort"])
|
|
1929
|
+
):
|
|
1930
|
+
raise StorageError("execution_attempt_conflict")
|
|
1931
|
+
|
|
1932
|
+
@staticmethod
|
|
1933
|
+
def _finalize_rows(
|
|
1934
|
+
connection: sqlite3.Connection,
|
|
1935
|
+
attempt: sqlite3.Row,
|
|
1936
|
+
receipt: ExecutionReceipt,
|
|
1937
|
+
completed_at: int,
|
|
1938
|
+
) -> None:
|
|
1939
|
+
connection.execute(
|
|
1940
|
+
"""INSERT INTO executions(
|
|
1941
|
+
execution_id, idempotency_key, task_id, decision_id, approval_id,
|
|
1942
|
+
model_id, effort, status, reserved_tokens, actual_input_tokens,
|
|
1943
|
+
actual_output_tokens, created_at, completed_at
|
|
1944
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
1945
|
+
(
|
|
1946
|
+
receipt.execution_id, attempt["attempt_id"], attempt["task_id"],
|
|
1947
|
+
attempt["decision_id"], receipt.approval_id, receipt.model_id,
|
|
1948
|
+
receipt.effort.value, receipt.outcome.value, attempt["reserved_tokens"],
|
|
1949
|
+
receipt.input_tokens, receipt.output_tokens, attempt["created_at"],
|
|
1950
|
+
completed_at,
|
|
1951
|
+
),
|
|
1952
|
+
)
|
|
1953
|
+
connection.execute(
|
|
1954
|
+
"""INSERT INTO execution_receipts(
|
|
1955
|
+
execution_id, approval_id, model_id, effort, outcome, input_tokens,
|
|
1956
|
+
output_tokens, latency_ms, prompt_hash, response_hash, failure_reason,
|
|
1957
|
+
completed_at
|
|
1958
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)""",
|
|
1959
|
+
(
|
|
1960
|
+
receipt.execution_id,
|
|
1961
|
+
*RepositoryStore._receipt_tuple(receipt)[:-1],
|
|
1962
|
+
completed_at,
|
|
1963
|
+
),
|
|
1964
|
+
)
|
|
1965
|
+
connection.execute(
|
|
1966
|
+
"""INSERT INTO execution_evidence(
|
|
1967
|
+
execution_id, task_id, decision_id, graph_fingerprint
|
|
1968
|
+
) VALUES (?, ?, ?, ?)""",
|
|
1969
|
+
(
|
|
1970
|
+
receipt.execution_id, attempt["task_id"], attempt["decision_id"],
|
|
1971
|
+
attempt["graph_fingerprint"],
|
|
1972
|
+
),
|
|
1973
|
+
)
|
|
1974
|
+
connection.execute(
|
|
1975
|
+
"""UPDATE execution_attempts
|
|
1976
|
+
SET status = 'completed', failure_reason = NULL, execution_id = ?, updated_at = ?
|
|
1977
|
+
WHERE attempt_id = ?""",
|
|
1978
|
+
(receipt.execution_id, completed_at, attempt["attempt_id"]),
|
|
1979
|
+
)
|
|
1980
|
+
connection.execute(
|
|
1981
|
+
"DELETE FROM staged_execution_receipts WHERE attempt_id = ?",
|
|
1982
|
+
(attempt["attempt_id"],),
|
|
1983
|
+
)
|
|
1984
|
+
|
|
1985
|
+
def finalize_execution_attempt(
|
|
1986
|
+
self,
|
|
1987
|
+
*,
|
|
1988
|
+
attempt_id: str,
|
|
1989
|
+
receipt: ExecutionReceipt,
|
|
1990
|
+
inventory_digest: str,
|
|
1991
|
+
completed_at: int,
|
|
1992
|
+
) -> bool:
|
|
1993
|
+
attempt_id = _identifier(attempt_id, "attempt_id_invalid")
|
|
1994
|
+
self._validate_receipt_shape(receipt)
|
|
1995
|
+
if not isinstance(inventory_digest, str) or not _HEX_64.fullmatch(inventory_digest):
|
|
1996
|
+
raise ValueError("inventory_digest_invalid")
|
|
1997
|
+
completed_at = _nonnegative_integer(completed_at, "completed_at_invalid")
|
|
1998
|
+
connection: sqlite3.Connection | None = None
|
|
1999
|
+
try:
|
|
2000
|
+
connection = self._connect()
|
|
2001
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
2002
|
+
attempt = connection.execute(
|
|
2003
|
+
"SELECT * FROM execution_attempts WHERE attempt_id = ?",
|
|
2004
|
+
(attempt_id,),
|
|
2005
|
+
).fetchone()
|
|
2006
|
+
if attempt is None:
|
|
2007
|
+
raise StorageError("execution_attempt_missing")
|
|
2008
|
+
if attempt["status"] == "legacy_unrecoverable":
|
|
2009
|
+
raise StorageError(str(attempt["failure_reason"]))
|
|
2010
|
+
if attempt["inventory_digest"] != inventory_digest:
|
|
2011
|
+
raise StorageError("execution_attempt_conflict")
|
|
2012
|
+
self._assert_approval_consumed(connection, attempt)
|
|
2013
|
+
if attempt["status"] == "completed":
|
|
2014
|
+
stored = connection.execute(
|
|
2015
|
+
"SELECT approval_id, model_id, effort, outcome, input_tokens, "
|
|
2016
|
+
"output_tokens, latency_ms, prompt_hash, response_hash, failure_reason "
|
|
2017
|
+
"FROM execution_receipts WHERE execution_id = ?",
|
|
2018
|
+
(receipt.execution_id,),
|
|
2019
|
+
).fetchone()
|
|
2020
|
+
expected = self._receipt_tuple(receipt)
|
|
2021
|
+
if attempt["execution_id"] != receipt.execution_id or tuple(stored or ()) != expected:
|
|
2022
|
+
raise StorageError("execution_attempt_conflict")
|
|
2023
|
+
connection.rollback()
|
|
2024
|
+
return False
|
|
2025
|
+
if attempt["status"] not in {"pending", "persistence_failed"}:
|
|
2026
|
+
raise StorageError("execution_attempt_conflict")
|
|
2027
|
+
self._validate_attempt_binding(attempt, receipt)
|
|
2028
|
+
staged = connection.execute(
|
|
2029
|
+
"SELECT * FROM staged_execution_receipts WHERE attempt_id = ?",
|
|
2030
|
+
(attempt_id,),
|
|
2031
|
+
).fetchone()
|
|
2032
|
+
if (
|
|
2033
|
+
staged is not None
|
|
2034
|
+
and self._receipt_tuple(self._receipt_from_row(staged))
|
|
2035
|
+
!= self._receipt_tuple(receipt)
|
|
2036
|
+
):
|
|
2037
|
+
raise StorageError("execution_attempt_conflict")
|
|
2038
|
+
self._finalize_rows(connection, attempt, receipt, completed_at)
|
|
2039
|
+
connection.commit()
|
|
2040
|
+
return True
|
|
2041
|
+
except StorageError:
|
|
2042
|
+
if connection is not None and connection.in_transaction:
|
|
2043
|
+
connection.rollback()
|
|
2044
|
+
raise
|
|
2045
|
+
except sqlite3.Error as exc:
|
|
2046
|
+
if connection is not None and connection.in_transaction:
|
|
2047
|
+
connection.rollback()
|
|
2048
|
+
raise _translate_database_error(exc) from exc
|
|
2049
|
+
finally:
|
|
2050
|
+
if connection is not None:
|
|
2051
|
+
connection.close()
|
|
2052
|
+
|
|
2053
|
+
def stage_execution_receipt(
|
|
2054
|
+
self,
|
|
2055
|
+
*,
|
|
2056
|
+
attempt_id: str,
|
|
2057
|
+
receipt: ExecutionReceipt,
|
|
2058
|
+
inventory_digest: str,
|
|
2059
|
+
staged_at: int,
|
|
2060
|
+
) -> bool:
|
|
2061
|
+
attempt_id = _identifier(attempt_id, "attempt_id_invalid")
|
|
2062
|
+
self._validate_receipt_shape(receipt)
|
|
2063
|
+
if not isinstance(inventory_digest, str) or not _HEX_64.fullmatch(inventory_digest):
|
|
2064
|
+
raise ValueError("inventory_digest_invalid")
|
|
2065
|
+
staged_at = _nonnegative_integer(staged_at, "staged_at_invalid")
|
|
2066
|
+
connection: sqlite3.Connection | None = None
|
|
2067
|
+
try:
|
|
2068
|
+
connection = self._connect()
|
|
2069
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
2070
|
+
attempt = connection.execute(
|
|
2071
|
+
"SELECT * FROM execution_attempts WHERE attempt_id = ?", (attempt_id,)
|
|
2072
|
+
).fetchone()
|
|
2073
|
+
if attempt is None:
|
|
2074
|
+
raise StorageError("execution_attempt_missing")
|
|
2075
|
+
if attempt["status"] == "legacy_unrecoverable":
|
|
2076
|
+
raise StorageError(str(attempt["failure_reason"]))
|
|
2077
|
+
if attempt["inventory_digest"] != inventory_digest:
|
|
2078
|
+
raise StorageError("execution_attempt_conflict")
|
|
2079
|
+
self._assert_approval_consumed(connection, attempt)
|
|
2080
|
+
if attempt["status"] not in {"pending", "persistence_failed"}:
|
|
2081
|
+
raise StorageError("execution_attempt_conflict")
|
|
2082
|
+
self._validate_attempt_binding(attempt, receipt)
|
|
2083
|
+
existing = connection.execute(
|
|
2084
|
+
"SELECT * FROM staged_execution_receipts WHERE attempt_id = ?",
|
|
2085
|
+
(attempt_id,),
|
|
2086
|
+
).fetchone()
|
|
2087
|
+
if existing is not None:
|
|
2088
|
+
if (
|
|
2089
|
+
existing["inventory_digest"] != inventory_digest
|
|
2090
|
+
or self._receipt_tuple(self._receipt_from_row(existing))
|
|
2091
|
+
!= self._receipt_tuple(receipt)
|
|
2092
|
+
):
|
|
2093
|
+
raise StorageError("execution_attempt_conflict")
|
|
2094
|
+
connection.rollback()
|
|
2095
|
+
return False
|
|
2096
|
+
connection.execute(
|
|
2097
|
+
"""INSERT INTO staged_execution_receipts(
|
|
2098
|
+
attempt_id, execution_id, approval_id, model_id, effort, outcome,
|
|
2099
|
+
input_tokens, output_tokens, latency_ms, prompt_hash, response_hash,
|
|
2100
|
+
inventory_digest, failure_reason, staged_at
|
|
2101
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)""",
|
|
2102
|
+
(
|
|
2103
|
+
attempt_id, receipt.execution_id,
|
|
2104
|
+
*self._receipt_tuple(receipt)[:-1], inventory_digest, staged_at,
|
|
2105
|
+
),
|
|
2106
|
+
)
|
|
2107
|
+
connection.execute(
|
|
2108
|
+
"""UPDATE execution_attempts SET status = 'persistence_failed',
|
|
2109
|
+
failure_reason = 'execution_persistence_failed', updated_at = ?
|
|
2110
|
+
WHERE attempt_id = ?""",
|
|
2111
|
+
(staged_at, attempt_id),
|
|
2112
|
+
)
|
|
2113
|
+
connection.commit()
|
|
2114
|
+
return True
|
|
2115
|
+
except StorageError:
|
|
2116
|
+
if connection is not None and connection.in_transaction:
|
|
2117
|
+
connection.rollback()
|
|
2118
|
+
raise
|
|
2119
|
+
except sqlite3.Error as exc:
|
|
2120
|
+
if connection is not None and connection.in_transaction:
|
|
2121
|
+
connection.rollback()
|
|
2122
|
+
raise _translate_database_error(exc) from exc
|
|
2123
|
+
finally:
|
|
2124
|
+
if connection is not None:
|
|
2125
|
+
connection.close()
|
|
2126
|
+
|
|
2127
|
+
def recoverable_attempts(
|
|
2128
|
+
self,
|
|
2129
|
+
*,
|
|
2130
|
+
limit: int = DEFAULT_RECOVERY_PAGE_SIZE,
|
|
2131
|
+
after: str | None = None,
|
|
2132
|
+
) -> RecoverableAttemptPage:
|
|
2133
|
+
if (
|
|
2134
|
+
isinstance(limit, bool)
|
|
2135
|
+
or not isinstance(limit, int)
|
|
2136
|
+
or not 1 <= limit <= MAX_RECOVERY_PAGE_SIZE
|
|
2137
|
+
):
|
|
2138
|
+
raise ValueError("recovery_limit_invalid")
|
|
2139
|
+
if after is not None:
|
|
2140
|
+
after = _identifier(after, "recovery_cursor_invalid")
|
|
2141
|
+
query = """SELECT a.attempt_id FROM execution_attempts a
|
|
2142
|
+
JOIN staged_execution_receipts s ON s.attempt_id = a.attempt_id
|
|
2143
|
+
WHERE a.status = 'persistence_failed'"""
|
|
2144
|
+
parameters: list[Any] = []
|
|
2145
|
+
if after is not None:
|
|
2146
|
+
query += " AND a.attempt_id > ?"
|
|
2147
|
+
parameters.append(after)
|
|
2148
|
+
query += " ORDER BY a.attempt_id LIMIT ?"
|
|
2149
|
+
parameters.append(limit + 1)
|
|
2150
|
+
try:
|
|
2151
|
+
with self._connection() as connection:
|
|
2152
|
+
rows = connection.execute(query, parameters).fetchall()
|
|
2153
|
+
except sqlite3.Error as exc:
|
|
2154
|
+
raise _translate_database_error(exc) from exc
|
|
2155
|
+
try:
|
|
2156
|
+
validated = tuple(_identifier(row[0], "storage_corrupt") for row in rows)
|
|
2157
|
+
except ValueError as exc:
|
|
2158
|
+
raise StorageError("storage_corrupt") from exc
|
|
2159
|
+
has_more = len(validated) > limit
|
|
2160
|
+
attempt_ids = validated[:limit]
|
|
2161
|
+
return RecoverableAttemptPage(
|
|
2162
|
+
attempt_ids=attempt_ids,
|
|
2163
|
+
next_cursor=attempt_ids[-1] if has_more else None,
|
|
2164
|
+
has_more=has_more,
|
|
2165
|
+
)
|
|
2166
|
+
|
|
2167
|
+
def reconcile_execution_attempt(
|
|
2168
|
+
self, attempt_id: str, *, completed_at: int
|
|
2169
|
+
) -> ExecutionReceipt:
|
|
2170
|
+
attempt_id = _identifier(attempt_id, "attempt_id_invalid")
|
|
2171
|
+
completed_at = _nonnegative_integer(completed_at, "completed_at_invalid")
|
|
2172
|
+
connection: sqlite3.Connection | None = None
|
|
2173
|
+
try:
|
|
2174
|
+
connection = self._connect()
|
|
2175
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
2176
|
+
attempt = connection.execute(
|
|
2177
|
+
"SELECT * FROM execution_attempts WHERE attempt_id = ?", (attempt_id,)
|
|
2178
|
+
).fetchone()
|
|
2179
|
+
if attempt is None:
|
|
2180
|
+
raise StorageError("execution_attempt_missing")
|
|
2181
|
+
if attempt["status"] == "legacy_unrecoverable":
|
|
2182
|
+
raise StorageError(str(attempt["failure_reason"]))
|
|
2183
|
+
self._assert_approval_consumed(connection, attempt)
|
|
2184
|
+
if attempt["status"] == "completed":
|
|
2185
|
+
row = connection.execute(
|
|
2186
|
+
"SELECT * FROM execution_receipts WHERE execution_id = ?",
|
|
2187
|
+
(attempt["execution_id"],),
|
|
2188
|
+
).fetchone()
|
|
2189
|
+
evidence = connection.execute(
|
|
2190
|
+
"SELECT 1 FROM execution_evidence WHERE execution_id = ?",
|
|
2191
|
+
(attempt["execution_id"],),
|
|
2192
|
+
).fetchone()
|
|
2193
|
+
if row is None or evidence is None:
|
|
2194
|
+
raise StorageError("execution_attempt_conflict")
|
|
2195
|
+
receipt = self._receipt_from_row(row)
|
|
2196
|
+
self._validate_attempt_binding(attempt, receipt)
|
|
2197
|
+
connection.rollback()
|
|
2198
|
+
return receipt
|
|
2199
|
+
if attempt["status"] != "persistence_failed":
|
|
2200
|
+
raise StorageError("execution_attempt_conflict")
|
|
2201
|
+
staged = connection.execute(
|
|
2202
|
+
"SELECT * FROM staged_execution_receipts WHERE attempt_id = ?",
|
|
2203
|
+
(attempt_id,),
|
|
2204
|
+
).fetchone()
|
|
2205
|
+
if staged is None:
|
|
2206
|
+
raise StorageError("execution_attempt_conflict")
|
|
2207
|
+
if staged["inventory_digest"] != attempt["inventory_digest"]:
|
|
2208
|
+
raise StorageError("execution_attempt_conflict")
|
|
2209
|
+
receipt = self._receipt_from_row(staged)
|
|
2210
|
+
self._validate_attempt_binding(attempt, receipt)
|
|
2211
|
+
self._finalize_rows(connection, attempt, receipt, completed_at)
|
|
2212
|
+
connection.commit()
|
|
2213
|
+
return receipt
|
|
2214
|
+
except StorageError:
|
|
2215
|
+
if connection is not None and connection.in_transaction:
|
|
2216
|
+
connection.rollback()
|
|
2217
|
+
raise
|
|
2218
|
+
except sqlite3.Error as exc:
|
|
2219
|
+
if connection is not None and connection.in_transaction:
|
|
2220
|
+
connection.rollback()
|
|
2221
|
+
raise _translate_database_error(exc) from exc
|
|
2222
|
+
finally:
|
|
2223
|
+
if connection is not None:
|
|
2224
|
+
connection.close()
|
|
2225
|
+
|
|
2226
|
+
def execution_attempt(self, attempt_id: str) -> dict[str, Any] | None:
|
|
2227
|
+
attempt_id = _identifier(attempt_id, "attempt_id_invalid")
|
|
2228
|
+
with self._connection() as connection:
|
|
2229
|
+
row = connection.execute(
|
|
2230
|
+
"SELECT * FROM execution_attempts WHERE attempt_id = ?",
|
|
2231
|
+
(attempt_id,),
|
|
2232
|
+
).fetchone()
|
|
2233
|
+
return None if row is None else dict(row)
|
|
2234
|
+
|
|
2235
|
+
def outcome_evidence_rows(self) -> list[dict[str, Any]]:
|
|
2236
|
+
with self._connection() as connection:
|
|
2237
|
+
rows = connection.execute(
|
|
2238
|
+
"""SELECT o.outcome_id, o.execution_id, o.provenance, o.success,
|
|
2239
|
+
o.severe_failure, o.recorded_at, e.model_id, e.effort,
|
|
2240
|
+
t.category, t.risk
|
|
2241
|
+
FROM outcomes o
|
|
2242
|
+
JOIN executions e ON e.execution_id = o.execution_id
|
|
2243
|
+
JOIN tasks t ON t.task_id = e.task_id
|
|
2244
|
+
ORDER BY o.recorded_at, o.outcome_id"""
|
|
2245
|
+
).fetchall()
|
|
2246
|
+
return [dict(row) for row in rows]
|
|
2247
|
+
|
|
2248
|
+
def close_incident(self, execution_id: str, reviewed_at: int) -> bool:
|
|
2249
|
+
execution_id = _identifier(execution_id, "execution_id_invalid")
|
|
2250
|
+
reviewed_at = _nonnegative_integer(reviewed_at, "reviewed_at_invalid")
|
|
2251
|
+
try:
|
|
2252
|
+
with self._connection() as connection:
|
|
2253
|
+
cursor = connection.execute(
|
|
2254
|
+
"INSERT OR IGNORE INTO incident_reviews(execution_id, reviewed_at) VALUES (?, ?)",
|
|
2255
|
+
(execution_id, reviewed_at),
|
|
2256
|
+
)
|
|
2257
|
+
return cursor.rowcount == 1
|
|
2258
|
+
except sqlite3.Error as exc:
|
|
2259
|
+
raise _translate_database_error(exc) from exc
|
|
2260
|
+
|
|
2261
|
+
def latest_incident_review(self) -> int | None:
|
|
2262
|
+
with self._connection() as connection:
|
|
2263
|
+
row = connection.execute("SELECT MAX(reviewed_at) FROM incident_reviews").fetchone()
|
|
2264
|
+
return None if row is None or row[0] is None else int(row[0])
|
|
2265
|
+
|
|
2266
|
+
def reserve_shadow_budget(
|
|
2267
|
+
self,
|
|
2268
|
+
entry_id: str,
|
|
2269
|
+
token_amount: int,
|
|
2270
|
+
quota: int,
|
|
2271
|
+
now: int,
|
|
2272
|
+
) -> bool:
|
|
2273
|
+
entry_id = _identifier(entry_id, "entry_id_invalid")
|
|
2274
|
+
token_amount = _nonnegative_integer(token_amount, "token_amount_invalid")
|
|
2275
|
+
quota = _nonnegative_integer(quota, "quota_invalid")
|
|
2276
|
+
now = _nonnegative_integer(now, "created_at_invalid")
|
|
2277
|
+
connection: sqlite3.Connection | None = None
|
|
2278
|
+
try:
|
|
2279
|
+
connection = self._connect()
|
|
2280
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
2281
|
+
used = int(connection.execute(
|
|
2282
|
+
"""SELECT COALESCE(SUM(token_amount), 0) FROM budget_ledger
|
|
2283
|
+
WHERE entry_type = 'shadow_reservation' AND created_at >= ?""",
|
|
2284
|
+
(max(0, now - 86_400),),
|
|
2285
|
+
).fetchone()[0])
|
|
2286
|
+
if token_amount > quota or used + token_amount > quota:
|
|
2287
|
+
connection.rollback()
|
|
2288
|
+
return False
|
|
2289
|
+
connection.execute(
|
|
2290
|
+
"""INSERT INTO budget_ledger(
|
|
2291
|
+
entry_id, approval_id, execution_id, entry_type, token_amount, created_at
|
|
2292
|
+
) VALUES (?, NULL, NULL, 'shadow_reservation', ?, ?)""",
|
|
2293
|
+
(entry_id, token_amount, now),
|
|
2294
|
+
)
|
|
2295
|
+
connection.commit()
|
|
2296
|
+
return True
|
|
2297
|
+
except sqlite3.Error as exc:
|
|
2298
|
+
if connection is not None and connection.in_transaction:
|
|
2299
|
+
connection.rollback()
|
|
2300
|
+
raise _translate_database_error(exc) from exc
|
|
2301
|
+
finally:
|
|
2302
|
+
if connection is not None:
|
|
2303
|
+
connection.close()
|
|
2304
|
+
|
|
2305
|
+
def create_blind_comparison(
|
|
2306
|
+
self,
|
|
2307
|
+
*,
|
|
2308
|
+
comparison_id: str,
|
|
2309
|
+
primary_execution_id: str,
|
|
2310
|
+
shadow_execution_id: str,
|
|
2311
|
+
label_a_hash: str,
|
|
2312
|
+
label_b_hash: str,
|
|
2313
|
+
label_a_is_shadow: bool,
|
|
2314
|
+
created_at: int,
|
|
2315
|
+
) -> None:
|
|
2316
|
+
for value, code in (
|
|
2317
|
+
(comparison_id, "comparison_id_invalid"),
|
|
2318
|
+
(primary_execution_id, "execution_id_invalid"),
|
|
2319
|
+
(shadow_execution_id, "execution_id_invalid"),
|
|
2320
|
+
):
|
|
2321
|
+
_identifier(value, code)
|
|
2322
|
+
if not _HEX_64.fullmatch(label_a_hash) or not _HEX_64.fullmatch(label_b_hash):
|
|
2323
|
+
raise ValueError("response_hash_invalid")
|
|
2324
|
+
if not isinstance(label_a_is_shadow, bool):
|
|
2325
|
+
raise ValueError("comparison_invalid")
|
|
2326
|
+
created_at = _nonnegative_integer(created_at, "created_at_invalid")
|
|
2327
|
+
try:
|
|
2328
|
+
with self._connection() as connection:
|
|
2329
|
+
connection.execute(
|
|
2330
|
+
"""INSERT INTO blind_comparisons(
|
|
2331
|
+
comparison_id, primary_execution_id, shadow_execution_id,
|
|
2332
|
+
label_a_hash, label_b_hash, label_a_is_shadow, created_at
|
|
2333
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
2334
|
+
(comparison_id, primary_execution_id, shadow_execution_id,
|
|
2335
|
+
label_a_hash, label_b_hash, int(label_a_is_shadow), created_at),
|
|
2336
|
+
)
|
|
2337
|
+
except sqlite3.Error as exc:
|
|
2338
|
+
raise _translate_database_error(exc) from exc
|
|
2339
|
+
|
|
2340
|
+
def record_blind_verdict(self, comparison_id: str, verdict: str, recorded_at: int) -> bool:
|
|
2341
|
+
comparison_id = _identifier(comparison_id, "comparison_id_invalid")
|
|
2342
|
+
if verdict not in {"a", "b", "tie", "reject_both"}:
|
|
2343
|
+
raise ValueError("pairwise_verdict_invalid")
|
|
2344
|
+
recorded_at = _nonnegative_integer(recorded_at, "recorded_at_invalid")
|
|
2345
|
+
with self._connection() as connection:
|
|
2346
|
+
cursor = connection.execute(
|
|
2347
|
+
"""UPDATE blind_comparisons SET verdict = ?, recorded_at = ?
|
|
2348
|
+
WHERE comparison_id = ? AND verdict IS NULL""",
|
|
2349
|
+
(verdict, recorded_at, comparison_id),
|
|
2350
|
+
)
|
|
2351
|
+
return cursor.rowcount == 1
|
|
2352
|
+
|
|
2353
|
+
def blind_comparison(self, comparison_id: str) -> dict[str, Any] | None:
|
|
2354
|
+
comparison_id = _identifier(comparison_id, "comparison_id_invalid")
|
|
2355
|
+
with self._connection() as connection:
|
|
2356
|
+
row = connection.execute(
|
|
2357
|
+
"SELECT * FROM blind_comparisons WHERE comparison_id = ?",
|
|
2358
|
+
(comparison_id,),
|
|
2359
|
+
).fetchone()
|
|
2360
|
+
return None if row is None else dict(row)
|
|
2361
|
+
|
|
2362
|
+
def purge_outcomes_before(self, cutoff: int) -> int:
|
|
2363
|
+
cutoff = _nonnegative_integer(cutoff, "cutoff_invalid")
|
|
2364
|
+
connection: sqlite3.Connection | None = None
|
|
2365
|
+
try:
|
|
2366
|
+
connection = self._connect()
|
|
2367
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
2368
|
+
deleted = connection.execute(
|
|
2369
|
+
"DELETE FROM outcomes WHERE recorded_at < ?",
|
|
2370
|
+
(cutoff,),
|
|
2371
|
+
).rowcount
|
|
2372
|
+
connection.execute("DELETE FROM confidence_stats")
|
|
2373
|
+
connection.execute(
|
|
2374
|
+
"""INSERT INTO confidence_stats(
|
|
2375
|
+
model_id, effort, category, risk, sample_count,
|
|
2376
|
+
success_count, severe_failure_count
|
|
2377
|
+
)
|
|
2378
|
+
SELECT e.model_id, e.effort, t.category, t.risk,
|
|
2379
|
+
COUNT(*), SUM(o.success), SUM(o.severe_failure)
|
|
2380
|
+
FROM outcomes o
|
|
2381
|
+
JOIN executions e ON e.execution_id = o.execution_id
|
|
2382
|
+
JOIN tasks t ON t.task_id = e.task_id
|
|
2383
|
+
WHERE o.provenance IN ('machine_verified', 'ci_imported')
|
|
2384
|
+
GROUP BY e.model_id, e.effort, t.category, t.risk"""
|
|
2385
|
+
)
|
|
2386
|
+
connection.commit()
|
|
2387
|
+
return int(deleted)
|
|
2388
|
+
except sqlite3.Error as exc:
|
|
2389
|
+
if connection is not None and connection.in_transaction:
|
|
2390
|
+
connection.rollback()
|
|
2391
|
+
raise _translate_database_error(exc) from exc
|
|
2392
|
+
finally:
|
|
2393
|
+
if connection is not None:
|
|
2394
|
+
connection.close()
|
|
2395
|
+
|
|
2396
|
+
def confidence_rows(self) -> list[dict[str, Any]]:
|
|
2397
|
+
with self._connection() as connection:
|
|
2398
|
+
rows = connection.execute(
|
|
2399
|
+
"""SELECT model_id, effort, category, risk, sample_count,
|
|
2400
|
+
success_count, severe_failure_count
|
|
2401
|
+
FROM confidence_stats
|
|
2402
|
+
ORDER BY model_id, effort, category, risk"""
|
|
2403
|
+
).fetchall()
|
|
2404
|
+
return [dict(row) for row in rows]
|
|
2405
|
+
|
|
2406
|
+
def create_task_worktree_record(
|
|
2407
|
+
self,
|
|
2408
|
+
*,
|
|
2409
|
+
worktree_id: str,
|
|
2410
|
+
task_id: str,
|
|
2411
|
+
baseline_commit: str,
|
|
2412
|
+
canonical_root_hash: str,
|
|
2413
|
+
created_at: int,
|
|
2414
|
+
) -> bool:
|
|
2415
|
+
values = (
|
|
2416
|
+
_identifier(worktree_id, "worktree_id_invalid"),
|
|
2417
|
+
_identifier(task_id, "task_id_invalid"),
|
|
2418
|
+
baseline_commit,
|
|
2419
|
+
canonical_root_hash,
|
|
2420
|
+
_nonnegative_integer(created_at, "created_at_invalid"),
|
|
2421
|
+
)
|
|
2422
|
+
if not _GIT_OBJECT.fullmatch(baseline_commit):
|
|
2423
|
+
raise ValueError("baseline_commit_invalid")
|
|
2424
|
+
if not _HEX_64.fullmatch(canonical_root_hash):
|
|
2425
|
+
raise ValueError("canonical_root_hash_invalid")
|
|
2426
|
+
try:
|
|
2427
|
+
with self._connection() as connection:
|
|
2428
|
+
existing = connection.execute(
|
|
2429
|
+
"SELECT worktree_id,task_id,baseline_commit,canonical_root_hash,created_at "
|
|
2430
|
+
"FROM task_worktrees WHERE worktree_id=?",
|
|
2431
|
+
(values[0],),
|
|
2432
|
+
).fetchone()
|
|
2433
|
+
expected = (*values,)
|
|
2434
|
+
if existing is not None:
|
|
2435
|
+
if tuple(existing) != expected:
|
|
2436
|
+
raise StorageError("worktree_conflict")
|
|
2437
|
+
return False
|
|
2438
|
+
connection.execute(
|
|
2439
|
+
"INSERT INTO task_worktrees(worktree_id,task_id,baseline_commit,"
|
|
2440
|
+
"canonical_root_hash,status,created_at,updated_at) "
|
|
2441
|
+
"VALUES (?,?,?,?,'prepared',?,?)",
|
|
2442
|
+
(*values, values[-1]),
|
|
2443
|
+
)
|
|
2444
|
+
return True
|
|
2445
|
+
except StorageError:
|
|
2446
|
+
raise
|
|
2447
|
+
except sqlite3.Error as exc:
|
|
2448
|
+
raise _translate_database_error(exc) from exc
|
|
2449
|
+
|
|
2450
|
+
def transition_task_worktree(
|
|
2451
|
+
self, worktree_id: str, *, expected_status: str, new_status: str, updated_at: int
|
|
2452
|
+
) -> bool:
|
|
2453
|
+
worktree_id = _identifier(worktree_id, "worktree_id_invalid")
|
|
2454
|
+
allowed = {"prepared", "executed", "accepted", "rejected", "quarantined", "cleaned"}
|
|
2455
|
+
if expected_status not in allowed or new_status not in allowed:
|
|
2456
|
+
raise ValueError("worktree_status_invalid")
|
|
2457
|
+
updated_at = _nonnegative_integer(updated_at, "updated_at_invalid")
|
|
2458
|
+
try:
|
|
2459
|
+
with self._connection() as connection:
|
|
2460
|
+
cursor = connection.execute(
|
|
2461
|
+
"UPDATE task_worktrees SET status=?,updated_at=? "
|
|
2462
|
+
"WHERE worktree_id=? AND status=?",
|
|
2463
|
+
(new_status, updated_at, worktree_id, expected_status),
|
|
2464
|
+
)
|
|
2465
|
+
if cursor.rowcount == 1:
|
|
2466
|
+
return True
|
|
2467
|
+
row = connection.execute(
|
|
2468
|
+
"SELECT status FROM task_worktrees WHERE worktree_id=?", (worktree_id,)
|
|
2469
|
+
).fetchone()
|
|
2470
|
+
if row is None:
|
|
2471
|
+
raise StorageError("worktree_missing")
|
|
2472
|
+
if str(row[0]) == new_status:
|
|
2473
|
+
return False
|
|
2474
|
+
raise StorageError("worktree_transition_invalid")
|
|
2475
|
+
except StorageError:
|
|
2476
|
+
raise
|
|
2477
|
+
except sqlite3.Error as exc:
|
|
2478
|
+
raise _translate_database_error(exc) from exc
|
|
2479
|
+
|
|
2480
|
+
def task_worktree_record(self, worktree_id: str) -> dict[str, Any] | None:
|
|
2481
|
+
worktree_id = _identifier(worktree_id, "worktree_id_invalid")
|
|
2482
|
+
with self._connection() as connection:
|
|
2483
|
+
row = connection.execute(
|
|
2484
|
+
"SELECT * FROM task_worktrees WHERE worktree_id=?", (worktree_id,)
|
|
2485
|
+
).fetchone()
|
|
2486
|
+
return None if row is None else dict(row)
|
|
2487
|
+
|
|
2488
|
+
def latest_task_worktree_record(self, task_id: str) -> dict[str, Any] | None:
|
|
2489
|
+
task_id = _identifier(task_id, "task_id_invalid")
|
|
2490
|
+
with self._connection() as connection:
|
|
2491
|
+
row = connection.execute(
|
|
2492
|
+
"SELECT * FROM task_worktrees WHERE task_id=? "
|
|
2493
|
+
"ORDER BY created_at DESC,worktree_id DESC LIMIT 1",
|
|
2494
|
+
(task_id,),
|
|
2495
|
+
).fetchone()
|
|
2496
|
+
return None if row is None else dict(row)
|
|
2497
|
+
|
|
2498
|
+
def worktree_validation_record(self, worktree_id: str) -> dict[str, Any] | None:
|
|
2499
|
+
worktree_id = _identifier(worktree_id, "worktree_id_invalid")
|
|
2500
|
+
with self._connection() as connection:
|
|
2501
|
+
row = connection.execute(
|
|
2502
|
+
"""SELECT v.*,a.attempt_id,a.execution_id,a.status AS attempt_status,
|
|
2503
|
+
a.provider,a.task_id,a.decision_id,a.worktree_id,t.risk,t.category
|
|
2504
|
+
FROM cli_execution_attempts a
|
|
2505
|
+
JOIN validation_results v ON v.attempt_id=a.attempt_id
|
|
2506
|
+
JOIN tasks t ON t.task_id=a.task_id
|
|
2507
|
+
WHERE a.worktree_id=?
|
|
2508
|
+
ORDER BY a.created_at DESC,a.attempt_id DESC LIMIT 1""",
|
|
2509
|
+
(worktree_id,),
|
|
2510
|
+
).fetchone()
|
|
2511
|
+
return None if row is None else dict(row)
|
|
2512
|
+
|
|
2513
|
+
def create_review_link_record(
|
|
2514
|
+
self,
|
|
2515
|
+
*,
|
|
2516
|
+
review_attempt_id: str,
|
|
2517
|
+
primary_attempt_id: str,
|
|
2518
|
+
primary_diff_hash: str,
|
|
2519
|
+
created_at: int,
|
|
2520
|
+
) -> bool:
|
|
2521
|
+
values = (
|
|
2522
|
+
_identifier(review_attempt_id, "review_attempt_id_invalid"),
|
|
2523
|
+
_identifier(primary_attempt_id, "primary_attempt_id_invalid"),
|
|
2524
|
+
primary_diff_hash,
|
|
2525
|
+
_nonnegative_integer(created_at, "created_at_invalid"),
|
|
2526
|
+
)
|
|
2527
|
+
if values[0] == values[1]:
|
|
2528
|
+
raise ValueError("review_attempt_not_independent")
|
|
2529
|
+
if not _HEX_64.fullmatch(primary_diff_hash):
|
|
2530
|
+
raise ValueError("primary_diff_hash_invalid")
|
|
2531
|
+
try:
|
|
2532
|
+
with self._connection() as connection:
|
|
2533
|
+
cursor = connection.execute(
|
|
2534
|
+
"INSERT OR IGNORE INTO review_links VALUES (?,?,?,?)", values
|
|
2535
|
+
)
|
|
2536
|
+
if cursor.rowcount == 1:
|
|
2537
|
+
return True
|
|
2538
|
+
row = connection.execute(
|
|
2539
|
+
"SELECT review_attempt_id,primary_attempt_id,primary_diff_hash,created_at "
|
|
2540
|
+
"FROM review_links WHERE review_attempt_id=?",
|
|
2541
|
+
(values[0],),
|
|
2542
|
+
).fetchone()
|
|
2543
|
+
if tuple(row or ()) == values:
|
|
2544
|
+
return False
|
|
2545
|
+
raise StorageError("review_link_conflict")
|
|
2546
|
+
except StorageError:
|
|
2547
|
+
raise
|
|
2548
|
+
except sqlite3.Error as exc:
|
|
2549
|
+
raise _translate_database_error(exc) from exc
|
|
2550
|
+
|
|
2551
|
+
def create_cli_execution_attempt_record(
|
|
2552
|
+
self,
|
|
2553
|
+
*,
|
|
2554
|
+
attempt_id: str,
|
|
2555
|
+
approval_id: str,
|
|
2556
|
+
task_id: str,
|
|
2557
|
+
decision_id: str,
|
|
2558
|
+
worktree_id: str,
|
|
2559
|
+
provider: str,
|
|
2560
|
+
requested_model: str,
|
|
2561
|
+
effective_model: str,
|
|
2562
|
+
effort: str,
|
|
2563
|
+
cli_version: str,
|
|
2564
|
+
executable_sha256: str,
|
|
2565
|
+
capability_snapshot_digest: str,
|
|
2566
|
+
manifest_hash: str,
|
|
2567
|
+
expected_prompt_hash: str,
|
|
2568
|
+
reserved_tokens: int,
|
|
2569
|
+
created_at: int,
|
|
2570
|
+
) -> bool:
|
|
2571
|
+
values = (
|
|
2572
|
+
_identifier(attempt_id, "attempt_id_invalid"),
|
|
2573
|
+
_identifier(approval_id, "approval_id_invalid"),
|
|
2574
|
+
_identifier(task_id, "task_id_invalid"),
|
|
2575
|
+
_identifier(decision_id, "decision_id_invalid"),
|
|
2576
|
+
_identifier(worktree_id, "worktree_id_invalid"),
|
|
2577
|
+
ProviderId(provider).value,
|
|
2578
|
+
_model_identifier(requested_model, "requested_model_invalid"),
|
|
2579
|
+
_model_identifier(effective_model, "effective_model_invalid"),
|
|
2580
|
+
Effort(effort).value,
|
|
2581
|
+
_semantic_version(cli_version, "cli_version_invalid"),
|
|
2582
|
+
executable_sha256,
|
|
2583
|
+
capability_snapshot_digest,
|
|
2584
|
+
manifest_hash,
|
|
2585
|
+
expected_prompt_hash,
|
|
2586
|
+
_positive_integer(reserved_tokens, "reserved_tokens_invalid"),
|
|
2587
|
+
_nonnegative_integer(created_at, "created_at_invalid"),
|
|
2588
|
+
)
|
|
2589
|
+
for value, code in (
|
|
2590
|
+
(executable_sha256, "executable_sha256_invalid"),
|
|
2591
|
+
(capability_snapshot_digest, "capability_snapshot_digest_invalid"),
|
|
2592
|
+
(manifest_hash, "manifest_hash_invalid"),
|
|
2593
|
+
(expected_prompt_hash, "prompt_hash_invalid"),
|
|
2594
|
+
):
|
|
2595
|
+
if not _HEX_64.fullmatch(value):
|
|
2596
|
+
raise ValueError(code)
|
|
2597
|
+
try:
|
|
2598
|
+
with self._connection() as connection:
|
|
2599
|
+
cursor = connection.execute(
|
|
2600
|
+
"""INSERT OR IGNORE INTO cli_execution_attempts(
|
|
2601
|
+
attempt_id,approval_id,task_id,decision_id,worktree_id,provider,
|
|
2602
|
+
requested_model,effective_model,effort,cli_version,executable_sha256,
|
|
2603
|
+
capability_snapshot_digest,manifest_hash,expected_prompt_hash,
|
|
2604
|
+
reserved_tokens,status,failure_reason,execution_id,created_at,updated_at
|
|
2605
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'pending',NULL,NULL,?,?)""",
|
|
2606
|
+
(*values, values[-1]),
|
|
2607
|
+
)
|
|
2608
|
+
if cursor.rowcount == 1:
|
|
2609
|
+
return True
|
|
2610
|
+
raise StorageError("cli_attempt_conflict")
|
|
2611
|
+
except StorageError:
|
|
2612
|
+
raise
|
|
2613
|
+
except sqlite3.Error as exc:
|
|
2614
|
+
raise _translate_database_error(exc) from exc
|
|
2615
|
+
|
|
2616
|
+
def mark_cli_execution_attempt(
|
|
2617
|
+
self,
|
|
2618
|
+
attempt_id: str,
|
|
2619
|
+
*,
|
|
2620
|
+
status: str,
|
|
2621
|
+
failure_reason: str | None,
|
|
2622
|
+
execution_id: str | None = None,
|
|
2623
|
+
updated_at: int,
|
|
2624
|
+
) -> bool:
|
|
2625
|
+
attempt_id = _identifier(attempt_id, "attempt_id_invalid")
|
|
2626
|
+
if status not in {"completed", "failed", "persistence_failed", "quarantined"}:
|
|
2627
|
+
raise ValueError("cli_attempt_status_invalid")
|
|
2628
|
+
if failure_reason is not None:
|
|
2629
|
+
failure_reason = _identifier(failure_reason, "failure_reason_invalid")
|
|
2630
|
+
if execution_id is not None:
|
|
2631
|
+
execution_id = _identifier(execution_id, "execution_id_invalid")
|
|
2632
|
+
updated_at = _nonnegative_integer(updated_at, "updated_at_invalid")
|
|
2633
|
+
with self._connection() as connection:
|
|
2634
|
+
cursor = connection.execute(
|
|
2635
|
+
"UPDATE cli_execution_attempts SET status=?,failure_reason=?,execution_id=?,"
|
|
2636
|
+
"updated_at=? WHERE attempt_id=? AND status IN ('pending','persistence_failed')",
|
|
2637
|
+
(status, failure_reason, execution_id, updated_at, attempt_id),
|
|
2638
|
+
)
|
|
2639
|
+
if cursor.rowcount == 1:
|
|
2640
|
+
return True
|
|
2641
|
+
row = connection.execute(
|
|
2642
|
+
"SELECT status,failure_reason,execution_id FROM cli_execution_attempts "
|
|
2643
|
+
"WHERE attempt_id=?",
|
|
2644
|
+
(attempt_id,),
|
|
2645
|
+
).fetchone()
|
|
2646
|
+
if row is None:
|
|
2647
|
+
raise StorageError("cli_attempt_missing")
|
|
2648
|
+
if tuple(row) == (status, failure_reason, execution_id):
|
|
2649
|
+
return False
|
|
2650
|
+
raise StorageError("cli_attempt_transition_invalid")
|
|
2651
|
+
|
|
2652
|
+
def record_validation_result(
|
|
2653
|
+
self,
|
|
2654
|
+
*,
|
|
2655
|
+
attempt_id: str,
|
|
2656
|
+
diff_hash: str,
|
|
2657
|
+
changed_file_count: int,
|
|
2658
|
+
changed_byte_count: int,
|
|
2659
|
+
outcome: str,
|
|
2660
|
+
recorded_at: int,
|
|
2661
|
+
) -> bool:
|
|
2662
|
+
values = (
|
|
2663
|
+
_identifier(attempt_id, "attempt_id_invalid"),
|
|
2664
|
+
diff_hash,
|
|
2665
|
+
_nonnegative_integer(changed_file_count, "changed_file_count_invalid"),
|
|
2666
|
+
_nonnegative_integer(changed_byte_count, "changed_byte_count_invalid"),
|
|
2667
|
+
outcome,
|
|
2668
|
+
_nonnegative_integer(recorded_at, "recorded_at_invalid"),
|
|
2669
|
+
)
|
|
2670
|
+
if not _HEX_64.fullmatch(diff_hash):
|
|
2671
|
+
raise ValueError("diff_hash_invalid")
|
|
2672
|
+
if outcome not in {"passed", "failed", "blocked", "not_run"}:
|
|
2673
|
+
raise ValueError("validation_outcome_invalid")
|
|
2674
|
+
try:
|
|
2675
|
+
with self._connection() as connection:
|
|
2676
|
+
cursor = connection.execute(
|
|
2677
|
+
"INSERT OR IGNORE INTO validation_results VALUES (?,?,?,?,?,?)", values
|
|
2678
|
+
)
|
|
2679
|
+
if cursor.rowcount == 1:
|
|
2680
|
+
return True
|
|
2681
|
+
row = connection.execute(
|
|
2682
|
+
"SELECT attempt_id,diff_hash,changed_file_count,changed_byte_count,"
|
|
2683
|
+
"outcome,recorded_at FROM validation_results WHERE attempt_id=?",
|
|
2684
|
+
(values[0],),
|
|
2685
|
+
).fetchone()
|
|
2686
|
+
if tuple(row or ()) == values:
|
|
2687
|
+
return False
|
|
2688
|
+
raise StorageError("validation_result_conflict")
|
|
2689
|
+
except StorageError:
|
|
2690
|
+
raise
|
|
2691
|
+
except sqlite3.Error as exc:
|
|
2692
|
+
raise _translate_database_error(exc) from exc
|
|
2693
|
+
|
|
2694
|
+
def finalize_cli_execution(
|
|
2695
|
+
self,
|
|
2696
|
+
*,
|
|
2697
|
+
attempt_id: str,
|
|
2698
|
+
receipt: ExecutionReceipt,
|
|
2699
|
+
graph_fingerprint: str,
|
|
2700
|
+
completed_at: int,
|
|
2701
|
+
) -> bool:
|
|
2702
|
+
attempt_id = _identifier(attempt_id, "attempt_id_invalid")
|
|
2703
|
+
if not isinstance(receipt, ExecutionReceipt):
|
|
2704
|
+
raise ValueError("execution_receipt_invalid")
|
|
2705
|
+
if (
|
|
2706
|
+
receipt.outcome is not ExecutionOutcome.SUCCEEDED
|
|
2707
|
+
or receipt.input_tokens is None
|
|
2708
|
+
or receipt.output_tokens is None
|
|
2709
|
+
or receipt.response_hash is None
|
|
2710
|
+
or receipt.failure_reason is not None
|
|
2711
|
+
):
|
|
2712
|
+
raise ValueError("execution_receipt_invalid")
|
|
2713
|
+
if not _HEX_64.fullmatch(graph_fingerprint):
|
|
2714
|
+
raise ValueError("graph_fingerprint_invalid")
|
|
2715
|
+
completed_at = _nonnegative_integer(completed_at, "completed_at_invalid")
|
|
2716
|
+
connection: sqlite3.Connection | None = None
|
|
2717
|
+
try:
|
|
2718
|
+
connection = self._connect()
|
|
2719
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
2720
|
+
attempt = connection.execute(
|
|
2721
|
+
"SELECT * FROM cli_execution_attempts WHERE attempt_id=?", (attempt_id,)
|
|
2722
|
+
).fetchone()
|
|
2723
|
+
if attempt is None:
|
|
2724
|
+
raise StorageError("cli_attempt_missing")
|
|
2725
|
+
if attempt["status"] == "completed":
|
|
2726
|
+
if attempt["execution_id"] != receipt.execution_id:
|
|
2727
|
+
raise StorageError("cli_attempt_conflict")
|
|
2728
|
+
connection.rollback()
|
|
2729
|
+
return False
|
|
2730
|
+
if attempt["status"] != "pending":
|
|
2731
|
+
raise StorageError("cli_attempt_transition_invalid")
|
|
2732
|
+
approval = connection.execute(
|
|
2733
|
+
"SELECT status,manifest_hash,reserved_tokens FROM approvals WHERE approval_id=?",
|
|
2734
|
+
(attempt["approval_id"],),
|
|
2735
|
+
).fetchone()
|
|
2736
|
+
if (
|
|
2737
|
+
approval is None
|
|
2738
|
+
or approval["status"] != "consumed"
|
|
2739
|
+
or approval["manifest_hash"] != attempt["manifest_hash"]
|
|
2740
|
+
or approval["reserved_tokens"] != attempt["reserved_tokens"]
|
|
2741
|
+
or receipt.approval_id != attempt["approval_id"]
|
|
2742
|
+
or receipt.model_id != attempt["requested_model"]
|
|
2743
|
+
or receipt.effective_model != attempt["effective_model"]
|
|
2744
|
+
or receipt.effort.value != attempt["effort"]
|
|
2745
|
+
or receipt.cli_version != attempt["cli_version"]
|
|
2746
|
+
or receipt.prompt_hash != attempt["expected_prompt_hash"]
|
|
2747
|
+
or receipt.input_tokens + receipt.output_tokens > attempt["reserved_tokens"]
|
|
2748
|
+
):
|
|
2749
|
+
raise StorageError("cli_attempt_conflict")
|
|
2750
|
+
connection.execute(
|
|
2751
|
+
"""INSERT INTO executions(
|
|
2752
|
+
execution_id,idempotency_key,task_id,decision_id,approval_id,
|
|
2753
|
+
model_id,effort,status,reserved_tokens,actual_input_tokens,
|
|
2754
|
+
actual_output_tokens,created_at,completed_at
|
|
2755
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
2756
|
+
(
|
|
2757
|
+
receipt.execution_id,
|
|
2758
|
+
attempt_id,
|
|
2759
|
+
attempt["task_id"],
|
|
2760
|
+
attempt["decision_id"],
|
|
2761
|
+
receipt.approval_id,
|
|
2762
|
+
receipt.model_id,
|
|
2763
|
+
receipt.effort.value,
|
|
2764
|
+
receipt.outcome.value,
|
|
2765
|
+
attempt["reserved_tokens"],
|
|
2766
|
+
receipt.input_tokens,
|
|
2767
|
+
receipt.output_tokens,
|
|
2768
|
+
attempt["created_at"],
|
|
2769
|
+
completed_at,
|
|
2770
|
+
),
|
|
2771
|
+
)
|
|
2772
|
+
connection.execute(
|
|
2773
|
+
"""INSERT INTO execution_receipts(
|
|
2774
|
+
execution_id,approval_id,model_id,effort,outcome,input_tokens,
|
|
2775
|
+
output_tokens,latency_ms,prompt_hash,response_hash,failure_reason,completed_at
|
|
2776
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,NULL,?)""",
|
|
2777
|
+
(
|
|
2778
|
+
receipt.execution_id,
|
|
2779
|
+
receipt.approval_id,
|
|
2780
|
+
receipt.model_id,
|
|
2781
|
+
receipt.effort.value,
|
|
2782
|
+
receipt.outcome.value,
|
|
2783
|
+
receipt.input_tokens,
|
|
2784
|
+
receipt.output_tokens,
|
|
2785
|
+
receipt.latency_ms,
|
|
2786
|
+
receipt.prompt_hash,
|
|
2787
|
+
receipt.response_hash,
|
|
2788
|
+
completed_at,
|
|
2789
|
+
),
|
|
2790
|
+
)
|
|
2791
|
+
connection.execute(
|
|
2792
|
+
"INSERT INTO execution_evidence VALUES (?,?,?,?)",
|
|
2793
|
+
(
|
|
2794
|
+
receipt.execution_id,
|
|
2795
|
+
attempt["task_id"],
|
|
2796
|
+
attempt["decision_id"],
|
|
2797
|
+
graph_fingerprint,
|
|
2798
|
+
),
|
|
2799
|
+
)
|
|
2800
|
+
connection.execute(
|
|
2801
|
+
"UPDATE cli_execution_attempts SET status='completed',failure_reason=NULL,"
|
|
2802
|
+
"execution_id=?,updated_at=? WHERE attempt_id=?",
|
|
2803
|
+
(receipt.execution_id, completed_at, attempt_id),
|
|
2804
|
+
)
|
|
2805
|
+
connection.commit()
|
|
2806
|
+
return True
|
|
2807
|
+
except StorageError:
|
|
2808
|
+
if connection is not None and connection.in_transaction:
|
|
2809
|
+
connection.rollback()
|
|
2810
|
+
raise
|
|
2811
|
+
except sqlite3.IntegrityError as exc:
|
|
2812
|
+
if connection is not None and connection.in_transaction:
|
|
2813
|
+
connection.rollback()
|
|
2814
|
+
raise StorageError("cli_attempt_conflict") from exc
|
|
2815
|
+
except sqlite3.Error as exc:
|
|
2816
|
+
if connection is not None and connection.in_transaction:
|
|
2817
|
+
connection.rollback()
|
|
2818
|
+
raise _translate_database_error(exc) from exc
|
|
2819
|
+
finally:
|
|
2820
|
+
if connection is not None:
|
|
2821
|
+
connection.close()
|
|
2822
|
+
|
|
2823
|
+
@staticmethod
|
|
2824
|
+
def _cli_telemetry_values(
|
|
2825
|
+
payload: dict[str, Any],
|
|
2826
|
+
) -> tuple[str, str, int]:
|
|
2827
|
+
if not isinstance(payload, dict) or set(payload) != _CLI_TELEMETRY_KEYS:
|
|
2828
|
+
raise ValueError("cli_telemetry_payload_invalid")
|
|
2829
|
+
try:
|
|
2830
|
+
ProviderId(payload["provider"])
|
|
2831
|
+
Effort(payload["effort"])
|
|
2832
|
+
TaskCategory(payload["category"])
|
|
2833
|
+
RiskTier(payload["risk"])
|
|
2834
|
+
EvidenceProvenance(payload["provenance"])
|
|
2835
|
+
_model_identifier(payload["requested_model"], "cli_telemetry_payload_invalid")
|
|
2836
|
+
_model_identifier(payload["effective_model"], "cli_telemetry_payload_invalid")
|
|
2837
|
+
except (TypeError, ValueError) as exc:
|
|
2838
|
+
raise ValueError("cli_telemetry_payload_invalid") from exc
|
|
2839
|
+
if (
|
|
2840
|
+
not isinstance(payload["capability_snapshot_digest"], str)
|
|
2841
|
+
or _HEX_64.fullmatch(payload["capability_snapshot_digest"]) is None
|
|
2842
|
+
or payload["cost_status"] != "unknown"
|
|
2843
|
+
or payload["validation_outcome"] not in {"passed", "failed", "blocked", "not_run"}
|
|
2844
|
+
or payload["human_verdict"] not in {None, "accepted", "rejected", "rework"}
|
|
2845
|
+
or (
|
|
2846
|
+
payload["diff_sha256"] is not None
|
|
2847
|
+
and (
|
|
2848
|
+
not isinstance(payload["diff_sha256"], str)
|
|
2849
|
+
or _HEX_64.fullmatch(payload["diff_sha256"]) is None
|
|
2850
|
+
)
|
|
2851
|
+
)
|
|
2852
|
+
):
|
|
2853
|
+
raise ValueError("cli_telemetry_payload_invalid")
|
|
2854
|
+
defects = payload["review_defect_classes"]
|
|
2855
|
+
allowed_defects = {
|
|
2856
|
+
"correctness", "security", "reliability", "maintainability",
|
|
2857
|
+
"performance", "test_coverage", "requirement_miss",
|
|
2858
|
+
}
|
|
2859
|
+
if (
|
|
2860
|
+
not isinstance(defects, list)
|
|
2861
|
+
or any(not isinstance(item, str) for item in defects)
|
|
2862
|
+
or len(defects) != len(set(defects))
|
|
2863
|
+
or not set(defects) <= allowed_defects
|
|
2864
|
+
):
|
|
2865
|
+
raise ValueError("cli_telemetry_payload_invalid")
|
|
2866
|
+
for name, maximum, optional in (
|
|
2867
|
+
("latency_ms", 86_400_000, True),
|
|
2868
|
+
("input_tokens", 100_000_000, True),
|
|
2869
|
+
("output_tokens", 100_000_000, True),
|
|
2870
|
+
("changed_file_count", 100_000, False),
|
|
2871
|
+
("changed_byte_count", 10**12, False),
|
|
2872
|
+
("rework_count", 1_000, False),
|
|
2873
|
+
("observed_at", 10**12, False),
|
|
2874
|
+
):
|
|
2875
|
+
value = payload[name]
|
|
2876
|
+
if optional and value is None:
|
|
2877
|
+
continue
|
|
2878
|
+
_nonnegative_integer(value, "cli_telemetry_payload_invalid", maximum)
|
|
2879
|
+
try:
|
|
2880
|
+
canonical = json.dumps(
|
|
2881
|
+
payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True,
|
|
2882
|
+
allow_nan=False,
|
|
2883
|
+
)
|
|
2884
|
+
except (TypeError, ValueError) as exc:
|
|
2885
|
+
raise ValueError("cli_telemetry_payload_invalid") from exc
|
|
2886
|
+
if len(canonical.encode("utf-8")) > 8192:
|
|
2887
|
+
raise ValueError("cli_telemetry_payload_invalid")
|
|
2888
|
+
observed_at = _nonnegative_integer(payload["observed_at"], "observed_at_invalid")
|
|
2889
|
+
event_hash = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
2890
|
+
return event_hash, canonical, observed_at
|
|
2891
|
+
|
|
2892
|
+
def record_cli_telemetry(self, payload: dict[str, Any]) -> bool:
|
|
2893
|
+
"""Persist one immutable, allowlisted telemetry event."""
|
|
2894
|
+
event_hash, canonical, observed_at = self._cli_telemetry_values(payload)
|
|
2895
|
+
try:
|
|
2896
|
+
with self._connection() as connection:
|
|
2897
|
+
cursor = connection.execute(
|
|
2898
|
+
"INSERT OR IGNORE INTO cli_telemetry_events VALUES (?,?,?)",
|
|
2899
|
+
(event_hash, canonical, observed_at),
|
|
2900
|
+
)
|
|
2901
|
+
if cursor.rowcount == 1:
|
|
2902
|
+
return True
|
|
2903
|
+
row = connection.execute(
|
|
2904
|
+
"SELECT payload_json,observed_at FROM cli_telemetry_events "
|
|
2905
|
+
"WHERE event_hash=?", (event_hash,),
|
|
2906
|
+
).fetchone()
|
|
2907
|
+
if tuple(row or ()) == (canonical, observed_at):
|
|
2908
|
+
return False
|
|
2909
|
+
raise StorageError("cli_telemetry_conflict")
|
|
2910
|
+
except StorageError:
|
|
2911
|
+
raise
|
|
2912
|
+
except sqlite3.Error as exc:
|
|
2913
|
+
raise _translate_database_error(exc) from exc
|
|
2914
|
+
|
|
2915
|
+
def promote_bound_capability_snapshot_record(
|
|
2916
|
+
self,
|
|
2917
|
+
*,
|
|
2918
|
+
source_snapshot_digest: str,
|
|
2919
|
+
snapshot: CapabilitySnapshot,
|
|
2920
|
+
lifecycle_identity_digest: str,
|
|
2921
|
+
bound_at: int,
|
|
2922
|
+
telemetry_payload: dict[str, Any],
|
|
2923
|
+
) -> bool:
|
|
2924
|
+
"""Atomically persist one validated edit-smoke promotion and its audit event."""
|
|
2925
|
+
if not _HEX_64.fullmatch(source_snapshot_digest):
|
|
2926
|
+
raise ValueError("source_snapshot_digest_invalid")
|
|
2927
|
+
if not isinstance(snapshot, CapabilitySnapshot) or (
|
|
2928
|
+
snapshot.profile.permission_mode is not PermissionMode.WORKSPACE_WRITE
|
|
2929
|
+
):
|
|
2930
|
+
raise ValueError("capability_snapshot_invalid")
|
|
2931
|
+
if not _HEX_64.fullmatch(lifecycle_identity_digest):
|
|
2932
|
+
raise ValueError("lifecycle_identity_digest_invalid")
|
|
2933
|
+
bound_at = _nonnegative_integer(bound_at, "bound_at_invalid")
|
|
2934
|
+
event_hash, canonical, observed_at = self._cli_telemetry_values(
|
|
2935
|
+
telemetry_payload
|
|
2936
|
+
)
|
|
2937
|
+
if (
|
|
2938
|
+
telemetry_payload["capability_snapshot_digest"] != snapshot.digest
|
|
2939
|
+
or observed_at != bound_at
|
|
2940
|
+
):
|
|
2941
|
+
raise ValueError("cli_telemetry_payload_invalid")
|
|
2942
|
+
payload_json = json.dumps(
|
|
2943
|
+
snapshot.to_dict(),
|
|
2944
|
+
sort_keys=True,
|
|
2945
|
+
separators=(",", ":"),
|
|
2946
|
+
ensure_ascii=True,
|
|
2947
|
+
)
|
|
2948
|
+
if (
|
|
2949
|
+
len(snapshot.profile.supported_efforts) != 1
|
|
2950
|
+
or len(payload_json.encode("utf-8")) > 64 * 1024
|
|
2951
|
+
):
|
|
2952
|
+
raise ValueError("capability_snapshot_invalid")
|
|
2953
|
+
values = (
|
|
2954
|
+
snapshot.digest,
|
|
2955
|
+
snapshot.profile.provider.value,
|
|
2956
|
+
snapshot.profile.requested_model,
|
|
2957
|
+
snapshot.profile.effective_model,
|
|
2958
|
+
snapshot.profile.supported_efforts[0].value,
|
|
2959
|
+
snapshot.identity.executable_sha256,
|
|
2960
|
+
snapshot.identity.cli_version,
|
|
2961
|
+
snapshot.identity.adapter_protocol_version,
|
|
2962
|
+
snapshot.profile.permission_mode.value,
|
|
2963
|
+
payload_json,
|
|
2964
|
+
snapshot.verified_at,
|
|
2965
|
+
snapshot.expires_at,
|
|
2966
|
+
)
|
|
2967
|
+
connection: sqlite3.Connection | None = None
|
|
2968
|
+
try:
|
|
2969
|
+
connection = self._connect()
|
|
2970
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
2971
|
+
source = connection.execute(
|
|
2972
|
+
"SELECT provider,requested_model,effective_model,effort,"
|
|
2973
|
+
"executable_sha256,cli_version,adapter_protocol_version,"
|
|
2974
|
+
"permission_mode,expires_at FROM capability_snapshots "
|
|
2975
|
+
"WHERE capability_snapshot_digest=?",
|
|
2976
|
+
(source_snapshot_digest,),
|
|
2977
|
+
).fetchone()
|
|
2978
|
+
binding = connection.execute(
|
|
2979
|
+
"SELECT lifecycle_identity_digest FROM lifecycle_snapshot_bindings "
|
|
2980
|
+
"WHERE capability_snapshot_digest=?",
|
|
2981
|
+
(source_snapshot_digest,),
|
|
2982
|
+
).fetchone()
|
|
2983
|
+
expected_source = (
|
|
2984
|
+
snapshot.profile.provider.value,
|
|
2985
|
+
snapshot.profile.requested_model,
|
|
2986
|
+
snapshot.profile.effective_model,
|
|
2987
|
+
snapshot.profile.supported_efforts[0].value,
|
|
2988
|
+
snapshot.identity.executable_sha256,
|
|
2989
|
+
snapshot.identity.cli_version,
|
|
2990
|
+
snapshot.identity.adapter_protocol_version,
|
|
2991
|
+
)
|
|
2992
|
+
if (
|
|
2993
|
+
source is None
|
|
2994
|
+
or tuple(source[:7]) != expected_source
|
|
2995
|
+
or source[7] != PermissionMode.READ_ONLY.value
|
|
2996
|
+
or int(source[8]) <= bound_at
|
|
2997
|
+
or binding is None
|
|
2998
|
+
or binding[0] != lifecycle_identity_digest
|
|
2999
|
+
):
|
|
3000
|
+
raise StorageError("capability_promotion_source_invalid")
|
|
3001
|
+
existing = connection.execute(
|
|
3002
|
+
"SELECT capability_snapshot_digest,provider,requested_model,"
|
|
3003
|
+
"effective_model,effort,executable_sha256,cli_version,"
|
|
3004
|
+
"adapter_protocol_version,permission_mode,payload_json,verified_at,"
|
|
3005
|
+
"expires_at FROM capability_snapshots "
|
|
3006
|
+
"WHERE capability_snapshot_digest=?",
|
|
3007
|
+
(snapshot.digest,),
|
|
3008
|
+
).fetchone()
|
|
3009
|
+
if existing is not None:
|
|
3010
|
+
if tuple(existing) != values:
|
|
3011
|
+
raise StorageError("storage_corrupt")
|
|
3012
|
+
existing_binding = connection.execute(
|
|
3013
|
+
"SELECT lifecycle_identity_digest,bound_at "
|
|
3014
|
+
"FROM lifecycle_snapshot_bindings "
|
|
3015
|
+
"WHERE capability_snapshot_digest=?",
|
|
3016
|
+
(snapshot.digest,),
|
|
3017
|
+
).fetchone()
|
|
3018
|
+
existing_event = connection.execute(
|
|
3019
|
+
"SELECT payload_json,observed_at FROM cli_telemetry_events "
|
|
3020
|
+
"WHERE event_hash=?",
|
|
3021
|
+
(event_hash,),
|
|
3022
|
+
).fetchone()
|
|
3023
|
+
if tuple(existing_binding or ()) != (
|
|
3024
|
+
lifecycle_identity_digest,
|
|
3025
|
+
bound_at,
|
|
3026
|
+
) or tuple(existing_event or ()) != (canonical, observed_at):
|
|
3027
|
+
raise StorageError("capability_promotion_conflict")
|
|
3028
|
+
connection.rollback()
|
|
3029
|
+
return False
|
|
3030
|
+
connection.execute(
|
|
3031
|
+
"""INSERT INTO capability_snapshots(
|
|
3032
|
+
capability_snapshot_digest,provider,requested_model,effective_model,
|
|
3033
|
+
effort,executable_sha256,cli_version,adapter_protocol_version,
|
|
3034
|
+
permission_mode,payload_json,verified_at,expires_at
|
|
3035
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
3036
|
+
values,
|
|
3037
|
+
)
|
|
3038
|
+
self._save_lifecycle_binding(
|
|
3039
|
+
connection,
|
|
3040
|
+
table="lifecycle_snapshot_bindings",
|
|
3041
|
+
key_column="capability_snapshot_digest",
|
|
3042
|
+
key_value=snapshot.digest,
|
|
3043
|
+
lifecycle_identity_digest=lifecycle_identity_digest,
|
|
3044
|
+
bound_at=bound_at,
|
|
3045
|
+
)
|
|
3046
|
+
connection.execute(
|
|
3047
|
+
"INSERT INTO cli_telemetry_events VALUES (?,?,?)",
|
|
3048
|
+
(event_hash, canonical, observed_at),
|
|
3049
|
+
)
|
|
3050
|
+
connection.commit()
|
|
3051
|
+
return True
|
|
3052
|
+
except StorageError:
|
|
3053
|
+
if connection is not None and connection.in_transaction:
|
|
3054
|
+
connection.rollback()
|
|
3055
|
+
raise
|
|
3056
|
+
except sqlite3.Error as exc:
|
|
3057
|
+
if connection is not None and connection.in_transaction:
|
|
3058
|
+
connection.rollback()
|
|
3059
|
+
raise _translate_database_error(exc) from exc
|
|
3060
|
+
finally:
|
|
3061
|
+
if connection is not None:
|
|
3062
|
+
connection.close()
|
|
3063
|
+
|
|
3064
|
+
def cli_telemetry_records(self, *, limit: int = 1000) -> tuple[dict[str, Any], ...]:
|
|
3065
|
+
if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= 10_000:
|
|
3066
|
+
raise ValueError("cli_telemetry_limit_invalid")
|
|
3067
|
+
with self._connection() as connection:
|
|
3068
|
+
rows = connection.execute(
|
|
3069
|
+
"SELECT payload_json FROM cli_telemetry_events "
|
|
3070
|
+
"ORDER BY observed_at DESC,event_hash DESC LIMIT ?", (limit,),
|
|
3071
|
+
).fetchall()
|
|
3072
|
+
try:
|
|
3073
|
+
return tuple(json.loads(str(row[0])) for row in rows)
|
|
3074
|
+
except (TypeError, ValueError) as exc:
|
|
3075
|
+
raise StorageError("storage_corrupt") from exc
|
|
3076
|
+
|
|
3077
|
+
@staticmethod
|
|
3078
|
+
def _policy_signature_payload(
|
|
3079
|
+
policy_version: str,
|
|
3080
|
+
parent_version: str | None,
|
|
3081
|
+
payload_hash: str,
|
|
3082
|
+
evidence_hash: str,
|
|
3083
|
+
) -> bytes:
|
|
3084
|
+
return json.dumps(
|
|
3085
|
+
{
|
|
3086
|
+
"evidence_hash": evidence_hash,
|
|
3087
|
+
"parent_version": parent_version,
|
|
3088
|
+
"payload_hash": payload_hash,
|
|
3089
|
+
"policy_version": policy_version,
|
|
3090
|
+
},
|
|
3091
|
+
sort_keys=True, separators=(",", ":"), ensure_ascii=True,
|
|
3092
|
+
).encode("utf-8")
|
|
3093
|
+
|
|
3094
|
+
def record_cli_policy_candidate(
|
|
3095
|
+
self,
|
|
3096
|
+
*,
|
|
3097
|
+
policy_version: str,
|
|
3098
|
+
parent_version: str | None,
|
|
3099
|
+
payload: dict[str, Any],
|
|
3100
|
+
evidence_hash: str,
|
|
3101
|
+
signature: str,
|
|
3102
|
+
signing_key: bytes,
|
|
3103
|
+
created_at: int,
|
|
3104
|
+
) -> bool:
|
|
3105
|
+
"""Record a signed candidate; recording never activates it."""
|
|
3106
|
+
policy_version = _identifier(policy_version, "policy_version_invalid")
|
|
3107
|
+
if parent_version is not None:
|
|
3108
|
+
parent_version = _identifier(parent_version, "parent_policy_version_invalid")
|
|
3109
|
+
if not isinstance(payload, dict) or set(payload) != _CLI_POLICY_KEYS:
|
|
3110
|
+
raise ValueError("cli_policy_payload_invalid")
|
|
3111
|
+
if (
|
|
3112
|
+
payload["schema_version"] != 1
|
|
3113
|
+
or payload["autonomy"] is not False
|
|
3114
|
+
or payload["allowed_providers"] != ["claude-code", "codex"]
|
|
3115
|
+
or payload["permission_ceiling"] != "workspace-write"
|
|
3116
|
+
or payload["risk_ceilings"] != {"claude-code": "medium", "codex": "medium"}
|
|
3117
|
+
or payload["source_policy_version"] != "3"
|
|
3118
|
+
):
|
|
3119
|
+
raise ValueError("cli_policy_guardrail_invalid")
|
|
3120
|
+
weights = payload["scoring_weights"]
|
|
3121
|
+
if (
|
|
3122
|
+
not isinstance(weights, dict)
|
|
3123
|
+
or not weights
|
|
3124
|
+
or not set(weights) <= {
|
|
3125
|
+
"reliability", "tokens", "latency", "scarcity",
|
|
3126
|
+
"effort", "confidence", "recency",
|
|
3127
|
+
}
|
|
3128
|
+
):
|
|
3129
|
+
raise ValueError("cli_policy_payload_invalid")
|
|
3130
|
+
for value in weights.values():
|
|
3131
|
+
_nonnegative_integer(value, "cli_policy_payload_invalid", 1_000)
|
|
3132
|
+
minimum_samples = _nonnegative_integer(
|
|
3133
|
+
payload["minimum_samples"], "cli_policy_payload_invalid", 10_000
|
|
3134
|
+
)
|
|
3135
|
+
half_life = _nonnegative_integer(
|
|
3136
|
+
payload["recency_half_life_days"], "cli_policy_payload_invalid", 365
|
|
3137
|
+
)
|
|
3138
|
+
if minimum_samples < 20 or half_life < 7:
|
|
3139
|
+
raise ValueError("cli_policy_payload_invalid")
|
|
3140
|
+
if not isinstance(signing_key, bytes) or len(signing_key) < 32:
|
|
3141
|
+
raise ValueError("policy_signing_key_invalid")
|
|
3142
|
+
if not isinstance(evidence_hash, str) or not _HEX_64.fullmatch(evidence_hash):
|
|
3143
|
+
raise ValueError("evidence_hash_invalid")
|
|
3144
|
+
if not isinstance(signature, str) or not _HEX_64.fullmatch(signature):
|
|
3145
|
+
raise ValueError("policy_signature_invalid")
|
|
3146
|
+
created_at = _nonnegative_integer(created_at, "created_at_invalid")
|
|
3147
|
+
try:
|
|
3148
|
+
canonical = json.dumps(
|
|
3149
|
+
payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True,
|
|
3150
|
+
allow_nan=False,
|
|
3151
|
+
)
|
|
3152
|
+
except (TypeError, ValueError) as exc:
|
|
3153
|
+
raise ValueError("cli_policy_payload_invalid") from exc
|
|
3154
|
+
if len(canonical.encode("utf-8")) > 16384:
|
|
3155
|
+
raise ValueError("cli_policy_payload_invalid")
|
|
3156
|
+
payload_hash = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
3157
|
+
expected = hmac.new(
|
|
3158
|
+
signing_key,
|
|
3159
|
+
self._policy_signature_payload(
|
|
3160
|
+
policy_version, parent_version, payload_hash, evidence_hash
|
|
3161
|
+
),
|
|
3162
|
+
hashlib.sha256,
|
|
3163
|
+
).hexdigest()
|
|
3164
|
+
if not hmac.compare_digest(expected, signature):
|
|
3165
|
+
raise ValueError("policy_signature_invalid")
|
|
3166
|
+
values = (
|
|
3167
|
+
policy_version, parent_version, canonical, payload_hash,
|
|
3168
|
+
signature, evidence_hash, created_at,
|
|
3169
|
+
)
|
|
3170
|
+
try:
|
|
3171
|
+
with self._connection() as connection:
|
|
3172
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
3173
|
+
cursor = connection.execute(
|
|
3174
|
+
"INSERT OR IGNORE INTO cli_policy_versions VALUES (?,?,?,?,?,?,?)", values
|
|
3175
|
+
)
|
|
3176
|
+
if cursor.rowcount != 1:
|
|
3177
|
+
row = connection.execute(
|
|
3178
|
+
"SELECT policy_version,parent_version,payload_json,payload_hash,"
|
|
3179
|
+
"signature,evidence_hash,created_at FROM cli_policy_versions "
|
|
3180
|
+
"WHERE policy_version=?", (policy_version,),
|
|
3181
|
+
).fetchone()
|
|
3182
|
+
if tuple(row or ()) == values:
|
|
3183
|
+
connection.rollback()
|
|
3184
|
+
return False
|
|
3185
|
+
raise StorageError("cli_policy_version_conflict")
|
|
3186
|
+
connection.execute(
|
|
3187
|
+
"INSERT INTO cli_policy_events(policy_version,action,prior_version,actor,created_at) "
|
|
3188
|
+
"VALUES (?,'candidate',?,'human',?)",
|
|
3189
|
+
(policy_version, parent_version, created_at),
|
|
3190
|
+
)
|
|
3191
|
+
connection.commit()
|
|
3192
|
+
return True
|
|
3193
|
+
except StorageError:
|
|
3194
|
+
raise
|
|
3195
|
+
except sqlite3.Error as exc:
|
|
3196
|
+
raise _translate_database_error(exc) from exc
|
|
3197
|
+
|
|
3198
|
+
@staticmethod
|
|
3199
|
+
def _active_cli_policy(connection: sqlite3.Connection) -> str | None:
|
|
3200
|
+
row = connection.execute(
|
|
3201
|
+
"SELECT policy_version FROM cli_policy_events "
|
|
3202
|
+
"WHERE action IN ('promote','rollback') ORDER BY event_id DESC LIMIT 1"
|
|
3203
|
+
).fetchone()
|
|
3204
|
+
return None if row is None else str(row[0])
|
|
3205
|
+
|
|
3206
|
+
def activate_cli_policy(
|
|
3207
|
+
self,
|
|
3208
|
+
policy_version: str,
|
|
3209
|
+
*,
|
|
3210
|
+
action: str,
|
|
3211
|
+
authority_granted: bool,
|
|
3212
|
+
created_at: int,
|
|
3213
|
+
) -> bool:
|
|
3214
|
+
"""Append a human promotion or rollback without rewriting evidence."""
|
|
3215
|
+
policy_version = _identifier(policy_version, "policy_version_invalid")
|
|
3216
|
+
if action not in {"promote", "rollback"}:
|
|
3217
|
+
raise ValueError("policy_action_invalid")
|
|
3218
|
+
if authority_granted is not True:
|
|
3219
|
+
raise ValueError("human_authority_required")
|
|
3220
|
+
created_at = _nonnegative_integer(created_at, "created_at_invalid")
|
|
3221
|
+
try:
|
|
3222
|
+
with self._connection() as connection:
|
|
3223
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
3224
|
+
exists = connection.execute(
|
|
3225
|
+
"SELECT 1 FROM cli_policy_versions WHERE policy_version=?",
|
|
3226
|
+
(policy_version,),
|
|
3227
|
+
).fetchone()
|
|
3228
|
+
if exists is None:
|
|
3229
|
+
raise StorageError("cli_policy_version_missing")
|
|
3230
|
+
prior = self._active_cli_policy(connection)
|
|
3231
|
+
if prior == policy_version:
|
|
3232
|
+
connection.rollback()
|
|
3233
|
+
return False
|
|
3234
|
+
if action == "rollback":
|
|
3235
|
+
previously_active = connection.execute(
|
|
3236
|
+
"SELECT 1 FROM cli_policy_events WHERE policy_version=? "
|
|
3237
|
+
"AND action IN ('promote','rollback') LIMIT 1", (policy_version,),
|
|
3238
|
+
).fetchone()
|
|
3239
|
+
if previously_active is None:
|
|
3240
|
+
raise StorageError("cli_policy_rollback_target_invalid")
|
|
3241
|
+
connection.execute(
|
|
3242
|
+
"INSERT INTO cli_policy_events(policy_version,action,prior_version,actor,created_at) "
|
|
3243
|
+
"VALUES (?,?,?,'human',?)",
|
|
3244
|
+
(policy_version, action, prior, created_at),
|
|
3245
|
+
)
|
|
3246
|
+
connection.commit()
|
|
3247
|
+
return True
|
|
3248
|
+
except StorageError:
|
|
3249
|
+
raise
|
|
3250
|
+
except sqlite3.Error as exc:
|
|
3251
|
+
raise _translate_database_error(exc) from exc
|
|
3252
|
+
|
|
3253
|
+
def active_cli_policy_version(self) -> str | None:
|
|
3254
|
+
with self._connection() as connection:
|
|
3255
|
+
return self._active_cli_policy(connection)
|
|
3256
|
+
|
|
3257
|
+
def cli_policy_event_records(self) -> tuple[dict[str, Any], ...]:
|
|
3258
|
+
with self._connection() as connection:
|
|
3259
|
+
rows = connection.execute(
|
|
3260
|
+
"SELECT policy_version,action,prior_version,actor,created_at "
|
|
3261
|
+
"FROM cli_policy_events ORDER BY event_id"
|
|
3262
|
+
).fetchall()
|
|
3263
|
+
return tuple(dict(row) for row in rows)
|
|
3264
|
+
|
|
3265
|
+
def row_count(self, table: str) -> int:
|
|
3266
|
+
if table not in _TABLES:
|
|
3267
|
+
raise ValueError("table_invalid")
|
|
3268
|
+
with self._connection() as connection:
|
|
3269
|
+
return int(connection.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0])
|
|
3270
|
+
|
|
3271
|
+
def save_capability_snapshot_record(self, snapshot: CapabilitySnapshot) -> bool:
|
|
3272
|
+
if not isinstance(snapshot, CapabilitySnapshot):
|
|
3273
|
+
raise ValueError("capability_snapshot_invalid")
|
|
3274
|
+
if len(snapshot.profile.supported_efforts) != 1:
|
|
3275
|
+
raise ValueError("capability_snapshot_invalid")
|
|
3276
|
+
payload_json = json.dumps(
|
|
3277
|
+
snapshot.to_dict(), sort_keys=True, separators=(",", ":"), ensure_ascii=True
|
|
3278
|
+
)
|
|
3279
|
+
if len(payload_json.encode("utf-8")) > 64 * 1024:
|
|
3280
|
+
raise ValueError("capability_snapshot_invalid")
|
|
3281
|
+
values = (
|
|
3282
|
+
snapshot.digest,
|
|
3283
|
+
snapshot.profile.provider.value,
|
|
3284
|
+
snapshot.profile.requested_model,
|
|
3285
|
+
snapshot.profile.effective_model,
|
|
3286
|
+
snapshot.profile.supported_efforts[0].value,
|
|
3287
|
+
snapshot.identity.executable_sha256,
|
|
3288
|
+
snapshot.identity.cli_version,
|
|
3289
|
+
snapshot.identity.adapter_protocol_version,
|
|
3290
|
+
snapshot.profile.permission_mode.value,
|
|
3291
|
+
payload_json,
|
|
3292
|
+
snapshot.verified_at,
|
|
3293
|
+
snapshot.expires_at,
|
|
3294
|
+
)
|
|
3295
|
+
try:
|
|
3296
|
+
with self._connection() as connection:
|
|
3297
|
+
existing = connection.execute(
|
|
3298
|
+
"SELECT capability_snapshot_digest,provider,requested_model,"
|
|
3299
|
+
"effective_model,effort,executable_sha256,cli_version,"
|
|
3300
|
+
"adapter_protocol_version,permission_mode,payload_json,verified_at,expires_at "
|
|
3301
|
+
"FROM capability_snapshots WHERE capability_snapshot_digest=?",
|
|
3302
|
+
(snapshot.digest,),
|
|
3303
|
+
).fetchone()
|
|
3304
|
+
if existing is not None:
|
|
3305
|
+
if tuple(existing) != values:
|
|
3306
|
+
raise StorageError("storage_corrupt")
|
|
3307
|
+
return False
|
|
3308
|
+
connection.execute(
|
|
3309
|
+
"""INSERT INTO capability_snapshots(
|
|
3310
|
+
capability_snapshot_digest,provider,requested_model,effective_model,
|
|
3311
|
+
effort,executable_sha256,cli_version,adapter_protocol_version,
|
|
3312
|
+
permission_mode,payload_json,verified_at,expires_at
|
|
3313
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
3314
|
+
values,
|
|
3315
|
+
)
|
|
3316
|
+
return True
|
|
3317
|
+
except StorageError:
|
|
3318
|
+
raise
|
|
3319
|
+
except sqlite3.Error as exc:
|
|
3320
|
+
raise _translate_database_error(exc) from exc
|
|
3321
|
+
|
|
3322
|
+
def capability_snapshot_records(self, *, limit: int) -> tuple[dict[str, Any], ...]:
|
|
3323
|
+
if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= 64:
|
|
3324
|
+
raise ValueError("capability_snapshot_limit_invalid")
|
|
3325
|
+
try:
|
|
3326
|
+
with self._connection() as connection:
|
|
3327
|
+
rows = connection.execute(
|
|
3328
|
+
"SELECT capability_snapshot_digest,payload_json FROM capability_snapshots "
|
|
3329
|
+
"ORDER BY capability_snapshot_digest LIMIT ?",
|
|
3330
|
+
(limit + 1,),
|
|
3331
|
+
).fetchall()
|
|
3332
|
+
except sqlite3.Error as exc:
|
|
3333
|
+
raise _translate_database_error(exc) from exc
|
|
3334
|
+
if len(rows) > limit:
|
|
3335
|
+
raise StorageError("capability_snapshot_limit")
|
|
3336
|
+
result: list[dict[str, Any]] = []
|
|
3337
|
+
for row in rows:
|
|
3338
|
+
payload_json = row["payload_json"]
|
|
3339
|
+
if not isinstance(payload_json, str) or len(payload_json.encode("utf-8")) > 64 * 1024:
|
|
3340
|
+
raise StorageError("storage_corrupt")
|
|
3341
|
+
try:
|
|
3342
|
+
payload = json.loads(payload_json)
|
|
3343
|
+
except (json.JSONDecodeError, RecursionError) as exc:
|
|
3344
|
+
raise StorageError("storage_corrupt") from exc
|
|
3345
|
+
result.append({"digest": str(row["capability_snapshot_digest"]), "payload": payload})
|
|
3346
|
+
return tuple(result)
|
|
3347
|
+
|
|
3348
|
+
@staticmethod
|
|
3349
|
+
def _save_lifecycle_binding(
|
|
3350
|
+
connection: sqlite3.Connection,
|
|
3351
|
+
*,
|
|
3352
|
+
table: str,
|
|
3353
|
+
key_column: str,
|
|
3354
|
+
key_value: str,
|
|
3355
|
+
lifecycle_identity_digest: str,
|
|
3356
|
+
bound_at: int,
|
|
3357
|
+
capability_snapshot_digest: str | None = None,
|
|
3358
|
+
) -> bool:
|
|
3359
|
+
columns = [key_column]
|
|
3360
|
+
values: list[object] = [key_value]
|
|
3361
|
+
if capability_snapshot_digest is not None:
|
|
3362
|
+
columns.append("capability_snapshot_digest")
|
|
3363
|
+
values.append(capability_snapshot_digest)
|
|
3364
|
+
columns.extend(("lifecycle_identity_digest", "bound_at"))
|
|
3365
|
+
values.extend((lifecycle_identity_digest, bound_at))
|
|
3366
|
+
existing = connection.execute(
|
|
3367
|
+
f"SELECT {','.join(columns[1:])} FROM {table} WHERE {key_column}=?",
|
|
3368
|
+
(key_value,),
|
|
3369
|
+
).fetchone()
|
|
3370
|
+
if existing is not None:
|
|
3371
|
+
if tuple(existing) != tuple(values[1:]):
|
|
3372
|
+
raise StorageError("lifecycle_binding_changed")
|
|
3373
|
+
return False
|
|
3374
|
+
placeholders = ",".join("?" for _ in columns)
|
|
3375
|
+
connection.execute(
|
|
3376
|
+
f"INSERT INTO {table}({','.join(columns)}) VALUES({placeholders})",
|
|
3377
|
+
tuple(values),
|
|
3378
|
+
)
|
|
3379
|
+
return True
|
|
3380
|
+
|
|
3381
|
+
def save_lifecycle_snapshot_binding(
|
|
3382
|
+
self,
|
|
3383
|
+
*,
|
|
3384
|
+
capability_snapshot_digest: str,
|
|
3385
|
+
lifecycle_identity_digest: str,
|
|
3386
|
+
bound_at: int,
|
|
3387
|
+
) -> bool:
|
|
3388
|
+
if not _HEX_64.fullmatch(capability_snapshot_digest):
|
|
3389
|
+
raise ValueError("capability_snapshot_digest_invalid")
|
|
3390
|
+
if not _HEX_64.fullmatch(lifecycle_identity_digest):
|
|
3391
|
+
raise ValueError("lifecycle_identity_digest_invalid")
|
|
3392
|
+
bound_at = _nonnegative_integer(bound_at, "bound_at_invalid")
|
|
3393
|
+
try:
|
|
3394
|
+
with self._connection() as connection:
|
|
3395
|
+
return self._save_lifecycle_binding(
|
|
3396
|
+
connection,
|
|
3397
|
+
table="lifecycle_snapshot_bindings",
|
|
3398
|
+
key_column="capability_snapshot_digest",
|
|
3399
|
+
key_value=capability_snapshot_digest,
|
|
3400
|
+
lifecycle_identity_digest=lifecycle_identity_digest,
|
|
3401
|
+
bound_at=bound_at,
|
|
3402
|
+
)
|
|
3403
|
+
except StorageError:
|
|
3404
|
+
raise
|
|
3405
|
+
except sqlite3.Error as exc:
|
|
3406
|
+
raise _translate_database_error(exc) from exc
|
|
3407
|
+
|
|
3408
|
+
def save_lifecycle_approval_binding(
|
|
3409
|
+
self,
|
|
3410
|
+
*,
|
|
3411
|
+
approval_id: str,
|
|
3412
|
+
capability_snapshot_digest: str,
|
|
3413
|
+
lifecycle_identity_digest: str,
|
|
3414
|
+
bound_at: int,
|
|
3415
|
+
) -> bool:
|
|
3416
|
+
approval_id = _identifier(approval_id, "approval_id_invalid")
|
|
3417
|
+
if not _HEX_64.fullmatch(capability_snapshot_digest):
|
|
3418
|
+
raise ValueError("capability_snapshot_digest_invalid")
|
|
3419
|
+
if not _HEX_64.fullmatch(lifecycle_identity_digest):
|
|
3420
|
+
raise ValueError("lifecycle_identity_digest_invalid")
|
|
3421
|
+
bound_at = _nonnegative_integer(bound_at, "bound_at_invalid")
|
|
3422
|
+
try:
|
|
3423
|
+
with self._connection() as connection:
|
|
3424
|
+
return self._save_lifecycle_binding(
|
|
3425
|
+
connection,
|
|
3426
|
+
table="lifecycle_approval_bindings",
|
|
3427
|
+
key_column="approval_id",
|
|
3428
|
+
key_value=approval_id,
|
|
3429
|
+
capability_snapshot_digest=capability_snapshot_digest,
|
|
3430
|
+
lifecycle_identity_digest=lifecycle_identity_digest,
|
|
3431
|
+
bound_at=bound_at,
|
|
3432
|
+
)
|
|
3433
|
+
except StorageError:
|
|
3434
|
+
raise
|
|
3435
|
+
except sqlite3.Error as exc:
|
|
3436
|
+
raise _translate_database_error(exc) from exc
|
|
3437
|
+
|
|
3438
|
+
def save_lifecycle_attempt_binding(
|
|
3439
|
+
self,
|
|
3440
|
+
*,
|
|
3441
|
+
attempt_id: str,
|
|
3442
|
+
lifecycle_identity_digest: str,
|
|
3443
|
+
bound_at: int,
|
|
3444
|
+
) -> bool:
|
|
3445
|
+
attempt_id = _identifier(attempt_id, "attempt_id_invalid")
|
|
3446
|
+
if not _HEX_64.fullmatch(lifecycle_identity_digest):
|
|
3447
|
+
raise ValueError("lifecycle_identity_digest_invalid")
|
|
3448
|
+
bound_at = _nonnegative_integer(bound_at, "bound_at_invalid")
|
|
3449
|
+
try:
|
|
3450
|
+
with self._connection() as connection:
|
|
3451
|
+
return self._save_lifecycle_binding(
|
|
3452
|
+
connection,
|
|
3453
|
+
table="lifecycle_attempt_bindings",
|
|
3454
|
+
key_column="attempt_id",
|
|
3455
|
+
key_value=attempt_id,
|
|
3456
|
+
lifecycle_identity_digest=lifecycle_identity_digest,
|
|
3457
|
+
bound_at=bound_at,
|
|
3458
|
+
)
|
|
3459
|
+
except StorageError:
|
|
3460
|
+
raise
|
|
3461
|
+
except sqlite3.Error as exc:
|
|
3462
|
+
raise _translate_database_error(exc) from exc
|
|
3463
|
+
|
|
3464
|
+
def lifecycle_identity_binding(self, *, authority_kind: str, authority_id: str) -> str | None:
|
|
3465
|
+
tables = {
|
|
3466
|
+
"capability_snapshot": (
|
|
3467
|
+
"lifecycle_snapshot_bindings",
|
|
3468
|
+
"capability_snapshot_digest",
|
|
3469
|
+
),
|
|
3470
|
+
"approval": ("lifecycle_approval_bindings", "approval_id"),
|
|
3471
|
+
"attempt": ("lifecycle_attempt_bindings", "attempt_id"),
|
|
3472
|
+
}
|
|
3473
|
+
try:
|
|
3474
|
+
table, key_column = tables[authority_kind]
|
|
3475
|
+
except (KeyError, TypeError) as exc:
|
|
3476
|
+
raise ValueError("authority_kind_invalid") from exc
|
|
3477
|
+
if authority_kind == "capability_snapshot":
|
|
3478
|
+
authority_id = _identifier(authority_id, "authority_id_invalid")
|
|
3479
|
+
if not _HEX_64.fullmatch(authority_id):
|
|
3480
|
+
raise ValueError("authority_id_invalid")
|
|
3481
|
+
else:
|
|
3482
|
+
authority_id = _identifier(authority_id, "authority_id_invalid")
|
|
3483
|
+
try:
|
|
3484
|
+
with self._connection() as connection:
|
|
3485
|
+
if authority_kind == "capability_snapshot":
|
|
3486
|
+
row = connection.execute(
|
|
3487
|
+
"""SELECT COALESCE(
|
|
3488
|
+
(SELECT carry.new_identity_digest
|
|
3489
|
+
FROM lifecycle_binding_carries AS carry
|
|
3490
|
+
WHERE carry.capability_snapshot_digest = ?
|
|
3491
|
+
ORDER BY carry.carry_id DESC LIMIT 1),
|
|
3492
|
+
(SELECT binding.lifecycle_identity_digest
|
|
3493
|
+
FROM lifecycle_snapshot_bindings AS binding
|
|
3494
|
+
WHERE binding.capability_snapshot_digest = ?)
|
|
3495
|
+
)""",
|
|
3496
|
+
(authority_id, authority_id),
|
|
3497
|
+
).fetchone()
|
|
3498
|
+
if row is None or row[0] is None:
|
|
3499
|
+
return None
|
|
3500
|
+
return str(row[0])
|
|
3501
|
+
row = connection.execute(
|
|
3502
|
+
f"SELECT lifecycle_identity_digest FROM {table} WHERE {key_column}=?",
|
|
3503
|
+
(authority_id,),
|
|
3504
|
+
).fetchone()
|
|
3505
|
+
except sqlite3.Error as exc:
|
|
3506
|
+
raise _translate_database_error(exc) from exc
|
|
3507
|
+
return None if row is None else str(row[0])
|
|
3508
|
+
|
|
3509
|
+
def save_registry_snapshot(self, payload: dict[str, Any]) -> None:
|
|
3510
|
+
if not isinstance(payload, dict) or set(payload) != {
|
|
3511
|
+
"schema_version",
|
|
3512
|
+
"refreshed_at",
|
|
3513
|
+
"expires_at",
|
|
3514
|
+
"models",
|
|
3515
|
+
}:
|
|
3516
|
+
raise ValueError("registry_snapshot_invalid")
|
|
3517
|
+
refreshed = _nonnegative_integer(payload["refreshed_at"], "refreshed_at_invalid")
|
|
3518
|
+
expires = _nonnegative_integer(payload["expires_at"], "expires_at_invalid")
|
|
3519
|
+
if expires <= refreshed:
|
|
3520
|
+
raise ValueError("expires_at_invalid")
|
|
3521
|
+
models = payload["models"]
|
|
3522
|
+
if not isinstance(models, list) or len(models) > 128:
|
|
3523
|
+
raise ValueError("registry_models_invalid")
|
|
3524
|
+
for model in models:
|
|
3525
|
+
if not isinstance(model, dict) or set(model) != {
|
|
3526
|
+
"model_id",
|
|
3527
|
+
"digest",
|
|
3528
|
+
"context_window_tokens",
|
|
3529
|
+
"capabilities",
|
|
3530
|
+
}:
|
|
3531
|
+
raise ValueError("registry_model_invalid")
|
|
3532
|
+
model_id = model["model_id"]
|
|
3533
|
+
digest = model["digest"]
|
|
3534
|
+
context = model["context_window_tokens"]
|
|
3535
|
+
capabilities = model["capabilities"]
|
|
3536
|
+
if not isinstance(model_id, str) or not _SNAPSHOT_MODEL_ID.fullmatch(model_id):
|
|
3537
|
+
raise ValueError("registry_model_invalid")
|
|
3538
|
+
if not isinstance(digest, str) or not _HEX_64.fullmatch(digest):
|
|
3539
|
+
raise ValueError("registry_model_invalid")
|
|
3540
|
+
_nonnegative_integer(context, "registry_model_invalid", 4_194_304)
|
|
3541
|
+
if (
|
|
3542
|
+
not isinstance(capabilities, list)
|
|
3543
|
+
or len(capabilities) > 16
|
|
3544
|
+
or any(
|
|
3545
|
+
not isinstance(item, str) or not _CAPABILITY.fullmatch(item)
|
|
3546
|
+
for item in capabilities
|
|
3547
|
+
)
|
|
3548
|
+
):
|
|
3549
|
+
raise ValueError("registry_model_invalid")
|
|
3550
|
+
serialized = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
|
3551
|
+
if len(serialized.encode("utf-8")) > 2 * 1024 * 1024:
|
|
3552
|
+
raise ValueError("registry_snapshot_too_large")
|
|
3553
|
+
payload_hash = hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
|
3554
|
+
try:
|
|
3555
|
+
with self._connection() as connection:
|
|
3556
|
+
connection.execute(
|
|
3557
|
+
"""INSERT INTO registry_snapshots(
|
|
3558
|
+
snapshot_id, payload_json, payload_hash, refreshed_at, expires_at
|
|
3559
|
+
) VALUES (1, ?, ?, ?, ?)
|
|
3560
|
+
ON CONFLICT(snapshot_id) DO UPDATE SET
|
|
3561
|
+
payload_json = excluded.payload_json,
|
|
3562
|
+
payload_hash = excluded.payload_hash,
|
|
3563
|
+
refreshed_at = excluded.refreshed_at,
|
|
3564
|
+
expires_at = excluded.expires_at""",
|
|
3565
|
+
(serialized, payload_hash, refreshed, expires),
|
|
3566
|
+
)
|
|
3567
|
+
except sqlite3.Error as exc:
|
|
3568
|
+
raise _translate_database_error(exc) from exc
|
|
3569
|
+
|
|
3570
|
+
def load_registry_snapshot(self) -> dict[str, Any] | None:
|
|
3571
|
+
try:
|
|
3572
|
+
with self._connection() as connection:
|
|
3573
|
+
row = connection.execute(
|
|
3574
|
+
"SELECT payload_json, payload_hash FROM registry_snapshots WHERE snapshot_id = 1"
|
|
3575
|
+
).fetchone()
|
|
3576
|
+
except sqlite3.Error as exc:
|
|
3577
|
+
raise _translate_database_error(exc) from exc
|
|
3578
|
+
if row is None:
|
|
3579
|
+
return None
|
|
3580
|
+
payload_text = str(row["payload_json"])
|
|
3581
|
+
if hashlib.sha256(payload_text.encode("utf-8")).hexdigest() != row["payload_hash"]:
|
|
3582
|
+
raise StorageError("storage_corrupt")
|
|
3583
|
+
try:
|
|
3584
|
+
payload = json.loads(payload_text)
|
|
3585
|
+
except (json.JSONDecodeError, RecursionError) as exc:
|
|
3586
|
+
raise StorageError("storage_corrupt") from exc
|
|
3587
|
+
if not isinstance(payload, dict):
|
|
3588
|
+
raise StorageError("storage_corrupt")
|
|
3589
|
+
return payload
|
|
3590
|
+
|
|
3591
|
+
def save_approval_record(
|
|
3592
|
+
self,
|
|
3593
|
+
*,
|
|
3594
|
+
approval_id: str,
|
|
3595
|
+
task_id: str | None,
|
|
3596
|
+
decision_id: str | None,
|
|
3597
|
+
nonce_hash: str,
|
|
3598
|
+
manifest_hash: str,
|
|
3599
|
+
expires_at: int,
|
|
3600
|
+
reserved_tokens: int,
|
|
3601
|
+
) -> None:
|
|
3602
|
+
approval_id = _identifier(approval_id, "approval_id_invalid")
|
|
3603
|
+
if task_id is not None:
|
|
3604
|
+
task_id = _identifier(task_id, "task_id_invalid")
|
|
3605
|
+
if decision_id is not None:
|
|
3606
|
+
decision_id = _identifier(decision_id, "decision_id_invalid")
|
|
3607
|
+
if not _HEX_64.fullmatch(nonce_hash) or not _HEX_64.fullmatch(manifest_hash):
|
|
3608
|
+
raise ValueError("approval_hash_invalid")
|
|
3609
|
+
expires_at = _nonnegative_integer(expires_at, "expires_at_invalid")
|
|
3610
|
+
reserved_tokens = _nonnegative_integer(reserved_tokens, "reserved_tokens_invalid")
|
|
3611
|
+
try:
|
|
3612
|
+
with self._connection() as connection:
|
|
3613
|
+
existing = connection.execute(
|
|
3614
|
+
"SELECT nonce_hash, manifest_hash, expires_at, reserved_tokens FROM approvals WHERE approval_id = ?",
|
|
3615
|
+
(approval_id,),
|
|
3616
|
+
).fetchone()
|
|
3617
|
+
if existing is not None:
|
|
3618
|
+
if tuple(existing) != (nonce_hash, manifest_hash, expires_at, reserved_tokens):
|
|
3619
|
+
raise StorageError("approval_manifest_changed")
|
|
3620
|
+
return
|
|
3621
|
+
connection.execute(
|
|
3622
|
+
"""INSERT INTO approvals(
|
|
3623
|
+
approval_id, task_id, decision_id, nonce_hash, manifest_hash,
|
|
3624
|
+
status, expires_at, reserved_tokens
|
|
3625
|
+
) VALUES (?, ?, ?, ?, ?, 'issued', ?, ?)""",
|
|
3626
|
+
(
|
|
3627
|
+
approval_id,
|
|
3628
|
+
task_id,
|
|
3629
|
+
decision_id,
|
|
3630
|
+
nonce_hash,
|
|
3631
|
+
manifest_hash,
|
|
3632
|
+
expires_at,
|
|
3633
|
+
reserved_tokens,
|
|
3634
|
+
),
|
|
3635
|
+
)
|
|
3636
|
+
except StorageError:
|
|
3637
|
+
raise
|
|
3638
|
+
except sqlite3.Error as exc:
|
|
3639
|
+
raise _translate_database_error(exc) from exc
|
|
3640
|
+
|
|
3641
|
+
def consume_approval_record(
|
|
3642
|
+
self,
|
|
3643
|
+
*,
|
|
3644
|
+
approval_id: str,
|
|
3645
|
+
nonce_hash: str,
|
|
3646
|
+
manifest_hash: str,
|
|
3647
|
+
now: int,
|
|
3648
|
+
token_amount: int,
|
|
3649
|
+
repository_quota: int,
|
|
3650
|
+
) -> None:
|
|
3651
|
+
approval_id = _identifier(approval_id, "approval_id_invalid")
|
|
3652
|
+
now = _nonnegative_integer(now, "now_invalid")
|
|
3653
|
+
token_amount = _nonnegative_integer(token_amount, "token_amount_invalid")
|
|
3654
|
+
repository_quota = _nonnegative_integer(repository_quota, "repository_quota_invalid")
|
|
3655
|
+
connection: sqlite3.Connection | None = None
|
|
3656
|
+
try:
|
|
3657
|
+
connection = self._connect()
|
|
3658
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
3659
|
+
row = connection.execute(
|
|
3660
|
+
"""SELECT nonce_hash, manifest_hash, status, expires_at, reserved_tokens
|
|
3661
|
+
FROM approvals WHERE approval_id = ?""",
|
|
3662
|
+
(approval_id,),
|
|
3663
|
+
).fetchone()
|
|
3664
|
+
if row is None:
|
|
3665
|
+
raise StorageError("approval_missing")
|
|
3666
|
+
if row["nonce_hash"] != nonce_hash or row["manifest_hash"] != manifest_hash:
|
|
3667
|
+
raise StorageError("approval_manifest_changed")
|
|
3668
|
+
if row["status"] == "invalidated":
|
|
3669
|
+
raise StorageError("approval_lifecycle_stale")
|
|
3670
|
+
if row["status"] != "issued":
|
|
3671
|
+
raise StorageError("approval_reused")
|
|
3672
|
+
if now >= int(row["expires_at"]):
|
|
3673
|
+
raise StorageError("approval_expired")
|
|
3674
|
+
if int(row["reserved_tokens"]) != token_amount:
|
|
3675
|
+
raise StorageError("approval_manifest_changed")
|
|
3676
|
+
current = int(
|
|
3677
|
+
connection.execute(
|
|
3678
|
+
"SELECT COALESCE(SUM(token_amount), 0) FROM budget_ledger WHERE entry_type = 'reservation'"
|
|
3679
|
+
).fetchone()[0]
|
|
3680
|
+
)
|
|
3681
|
+
if token_amount > repository_quota or current + token_amount > repository_quota:
|
|
3682
|
+
raise StorageError("budget_exhausted")
|
|
3683
|
+
connection.execute(
|
|
3684
|
+
"UPDATE approvals SET status = 'consumed' WHERE approval_id = ? AND status = 'issued'",
|
|
3685
|
+
(approval_id,),
|
|
3686
|
+
)
|
|
3687
|
+
connection.execute(
|
|
3688
|
+
"""INSERT INTO budget_ledger(
|
|
3689
|
+
entry_id, approval_id, execution_id, entry_type, token_amount, created_at
|
|
3690
|
+
) VALUES (?, ?, NULL, 'reservation', ?, ?)""",
|
|
3691
|
+
(f"reserve:{approval_id}", approval_id, token_amount, now),
|
|
3692
|
+
)
|
|
3693
|
+
connection.commit()
|
|
3694
|
+
except StorageError:
|
|
3695
|
+
if connection is not None and connection.in_transaction:
|
|
3696
|
+
connection.rollback()
|
|
3697
|
+
raise
|
|
3698
|
+
except sqlite3.Error as exc:
|
|
3699
|
+
if connection is not None and connection.in_transaction:
|
|
3700
|
+
connection.rollback()
|
|
3701
|
+
raise _translate_database_error(exc) from exc
|
|
3702
|
+
finally:
|
|
3703
|
+
if connection is not None:
|
|
3704
|
+
connection.close()
|
|
3705
|
+
|
|
3706
|
+
def approval_status(self, approval_id: str) -> str | None:
|
|
3707
|
+
approval_id = _identifier(approval_id, "approval_id_invalid")
|
|
3708
|
+
with self._connection() as connection:
|
|
3709
|
+
row = connection.execute(
|
|
3710
|
+
"SELECT status FROM approvals WHERE approval_id = ?",
|
|
3711
|
+
(approval_id,),
|
|
3712
|
+
).fetchone()
|
|
3713
|
+
return None if row is None else str(row[0])
|
|
3714
|
+
|
|
3715
|
+
def carry_forward_snapshot_bindings(
|
|
3716
|
+
self,
|
|
3717
|
+
*,
|
|
3718
|
+
previous_identity_digest: str,
|
|
3719
|
+
new_identity_digest: str,
|
|
3720
|
+
lifecycle_event_id: str,
|
|
3721
|
+
carried_at: int,
|
|
3722
|
+
) -> tuple[str, ...]:
|
|
3723
|
+
for value in (previous_identity_digest, new_identity_digest, lifecycle_event_id):
|
|
3724
|
+
if not isinstance(value, str) or not _HEX_64.fullmatch(value):
|
|
3725
|
+
raise ValueError("lifecycle_identity_digest_invalid")
|
|
3726
|
+
if previous_identity_digest == new_identity_digest:
|
|
3727
|
+
raise ValueError("lifecycle_identity_digest_invalid")
|
|
3728
|
+
observed = _nonnegative_integer(carried_at, "carried_at_invalid")
|
|
3729
|
+
connection: sqlite3.Connection | None = None
|
|
3730
|
+
try:
|
|
3731
|
+
connection = self._connect()
|
|
3732
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
3733
|
+
rows = connection.execute(
|
|
3734
|
+
"""SELECT binding.capability_snapshot_digest
|
|
3735
|
+
FROM lifecycle_snapshot_bindings AS binding
|
|
3736
|
+
JOIN capability_snapshots AS snapshot
|
|
3737
|
+
ON snapshot.capability_snapshot_digest = binding.capability_snapshot_digest
|
|
3738
|
+
LEFT JOIN (
|
|
3739
|
+
SELECT capability_snapshot_digest, new_identity_digest,
|
|
3740
|
+
MAX(carry_id) AS carry_id
|
|
3741
|
+
FROM lifecycle_binding_carries
|
|
3742
|
+
GROUP BY capability_snapshot_digest
|
|
3743
|
+
) AS carry
|
|
3744
|
+
ON carry.capability_snapshot_digest = binding.capability_snapshot_digest
|
|
3745
|
+
WHERE COALESCE(carry.new_identity_digest, binding.lifecycle_identity_digest) = ?
|
|
3746
|
+
AND snapshot.expires_at > ?
|
|
3747
|
+
ORDER BY binding.capability_snapshot_digest""",
|
|
3748
|
+
(previous_identity_digest, observed),
|
|
3749
|
+
).fetchall()
|
|
3750
|
+
carried = tuple(str(row[0]) for row in rows)
|
|
3751
|
+
for digest in carried:
|
|
3752
|
+
connection.execute(
|
|
3753
|
+
"""INSERT INTO lifecycle_binding_carries(
|
|
3754
|
+
capability_snapshot_digest,previous_identity_digest,
|
|
3755
|
+
new_identity_digest,lifecycle_event_id,carried_at
|
|
3756
|
+
) VALUES (?,?,?,?,?)""",
|
|
3757
|
+
(
|
|
3758
|
+
digest,
|
|
3759
|
+
previous_identity_digest,
|
|
3760
|
+
new_identity_digest,
|
|
3761
|
+
lifecycle_event_id,
|
|
3762
|
+
observed,
|
|
3763
|
+
),
|
|
3764
|
+
)
|
|
3765
|
+
connection.commit()
|
|
3766
|
+
return carried
|
|
3767
|
+
except sqlite3.Error as exc:
|
|
3768
|
+
if connection is not None and connection.in_transaction:
|
|
3769
|
+
connection.rollback()
|
|
3770
|
+
raise _translate_database_error(exc) from exc
|
|
3771
|
+
finally:
|
|
3772
|
+
if connection is not None:
|
|
3773
|
+
connection.close()
|
|
3774
|
+
|
|
3775
|
+
def lifecycle_authority_targets(
|
|
3776
|
+
self, lifecycle_identity_digest: str
|
|
3777
|
+
) -> tuple[tuple[str, ...], tuple[str, ...]]:
|
|
3778
|
+
if not _HEX_64.fullmatch(lifecycle_identity_digest):
|
|
3779
|
+
raise ValueError("lifecycle_identity_digest_invalid")
|
|
3780
|
+
try:
|
|
3781
|
+
with self._connection() as connection:
|
|
3782
|
+
snapshots = tuple(
|
|
3783
|
+
str(row[0])
|
|
3784
|
+
for row in connection.execute(
|
|
3785
|
+
"""SELECT binding.capability_snapshot_digest
|
|
3786
|
+
FROM lifecycle_snapshot_bindings AS binding
|
|
3787
|
+
LEFT JOIN (
|
|
3788
|
+
SELECT capability_snapshot_digest, new_identity_digest,
|
|
3789
|
+
MAX(carry_id) AS carry_id
|
|
3790
|
+
FROM lifecycle_binding_carries
|
|
3791
|
+
GROUP BY capability_snapshot_digest
|
|
3792
|
+
) AS carry
|
|
3793
|
+
ON carry.capability_snapshot_digest = binding.capability_snapshot_digest
|
|
3794
|
+
WHERE COALESCE(
|
|
3795
|
+
carry.new_identity_digest, binding.lifecycle_identity_digest
|
|
3796
|
+
) = ?
|
|
3797
|
+
ORDER BY binding.capability_snapshot_digest""",
|
|
3798
|
+
(lifecycle_identity_digest,),
|
|
3799
|
+
).fetchall()
|
|
3800
|
+
)
|
|
3801
|
+
approvals = tuple(
|
|
3802
|
+
str(row[0])
|
|
3803
|
+
for row in connection.execute(
|
|
3804
|
+
"SELECT binding.approval_id FROM lifecycle_approval_bindings AS binding "
|
|
3805
|
+
"JOIN approvals AS approval ON approval.approval_id=binding.approval_id "
|
|
3806
|
+
"WHERE binding.lifecycle_identity_digest=? AND approval.status='issued' "
|
|
3807
|
+
"ORDER BY binding.approval_id",
|
|
3808
|
+
(lifecycle_identity_digest,),
|
|
3809
|
+
).fetchall()
|
|
3810
|
+
)
|
|
3811
|
+
except sqlite3.Error as exc:
|
|
3812
|
+
raise _translate_database_error(exc) from exc
|
|
3813
|
+
return snapshots, approvals
|
|
3814
|
+
|
|
3815
|
+
def lifecycle_approval_binding_details(
|
|
3816
|
+
self, approval_id: str
|
|
3817
|
+
) -> tuple[str, str] | None:
|
|
3818
|
+
approval_id = _identifier(approval_id, "approval_id_invalid")
|
|
3819
|
+
try:
|
|
3820
|
+
with self._connection() as connection:
|
|
3821
|
+
row = connection.execute(
|
|
3822
|
+
"SELECT capability_snapshot_digest,lifecycle_identity_digest "
|
|
3823
|
+
"FROM lifecycle_approval_bindings WHERE approval_id=?",
|
|
3824
|
+
(approval_id,),
|
|
3825
|
+
).fetchone()
|
|
3826
|
+
except sqlite3.Error as exc:
|
|
3827
|
+
raise _translate_database_error(exc) from exc
|
|
3828
|
+
return None if row is None else (str(row[0]), str(row[1]))
|
|
3829
|
+
|
|
3830
|
+
def invalidate_lifecycle_approvals(
|
|
3831
|
+
self, lifecycle_identity_digest: str
|
|
3832
|
+
) -> tuple[str, ...]:
|
|
3833
|
+
if not _HEX_64.fullmatch(lifecycle_identity_digest):
|
|
3834
|
+
raise ValueError("lifecycle_identity_digest_invalid")
|
|
3835
|
+
connection: sqlite3.Connection | None = None
|
|
3836
|
+
try:
|
|
3837
|
+
connection = self._connect()
|
|
3838
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
3839
|
+
approval_ids = tuple(
|
|
3840
|
+
str(row[0])
|
|
3841
|
+
for row in connection.execute(
|
|
3842
|
+
"SELECT binding.approval_id FROM lifecycle_approval_bindings AS binding "
|
|
3843
|
+
"JOIN approvals AS approval ON approval.approval_id=binding.approval_id "
|
|
3844
|
+
"WHERE binding.lifecycle_identity_digest=? AND approval.status='issued' "
|
|
3845
|
+
"ORDER BY binding.approval_id",
|
|
3846
|
+
(lifecycle_identity_digest,),
|
|
3847
|
+
).fetchall()
|
|
3848
|
+
)
|
|
3849
|
+
if approval_ids:
|
|
3850
|
+
placeholders = ",".join("?" for _ in approval_ids)
|
|
3851
|
+
connection.execute(
|
|
3852
|
+
f"UPDATE approvals SET status='invalidated' WHERE status='issued' "
|
|
3853
|
+
f"AND approval_id IN ({placeholders})",
|
|
3854
|
+
approval_ids,
|
|
3855
|
+
)
|
|
3856
|
+
connection.commit()
|
|
3857
|
+
return approval_ids
|
|
3858
|
+
except sqlite3.Error as exc:
|
|
3859
|
+
if connection is not None and connection.in_transaction:
|
|
3860
|
+
connection.rollback()
|
|
3861
|
+
raise _translate_database_error(exc) from exc
|
|
3862
|
+
finally:
|
|
3863
|
+
if connection is not None:
|
|
3864
|
+
connection.close()
|
|
3865
|
+
|
|
3866
|
+
def reserved_token_total(self) -> int:
|
|
3867
|
+
with self._connection() as connection:
|
|
3868
|
+
return int(
|
|
3869
|
+
connection.execute(
|
|
3870
|
+
"SELECT COALESCE(SUM(token_amount), 0) FROM budget_ledger WHERE entry_type = 'reservation'"
|
|
3871
|
+
).fetchone()[0]
|
|
3872
|
+
)
|
|
3873
|
+
|
|
3874
|
+
|
|
3875
|
+
@dataclass(frozen=True)
|
|
3876
|
+
class AggregateRecord:
|
|
3877
|
+
model_id: str
|
|
3878
|
+
effort: str
|
|
3879
|
+
category: str
|
|
3880
|
+
risk: str
|
|
3881
|
+
outcome: str
|
|
3882
|
+
input_bucket: int
|
|
3883
|
+
output_bucket: int
|
|
3884
|
+
latency_bucket: int
|
|
3885
|
+
policy_version: str
|
|
3886
|
+
recorded_day: int
|
|
3887
|
+
|
|
3888
|
+
def __post_init__(self) -> None:
|
|
3889
|
+
if not isinstance(self.model_id, str) or not _MODEL_ID.fullmatch(self.model_id):
|
|
3890
|
+
raise ValueError("model_id_invalid")
|
|
3891
|
+
object.__setattr__(self, "effort", Effort(self.effort).value)
|
|
3892
|
+
object.__setattr__(self, "category", TaskCategory(self.category).value)
|
|
3893
|
+
object.__setattr__(self, "risk", RiskTier(self.risk).value)
|
|
3894
|
+
object.__setattr__(self, "outcome", ExecutionOutcome(self.outcome).value)
|
|
3895
|
+
for name in ("input_bucket", "output_bucket", "latency_bucket"):
|
|
3896
|
+
object.__setattr__(self, name, _nonnegative_integer(getattr(self, name), f"{name}_invalid", 20))
|
|
3897
|
+
if not isinstance(self.policy_version, str) or not _VERSION.fullmatch(self.policy_version):
|
|
3898
|
+
raise ValueError("policy_version_invalid")
|
|
3899
|
+
object.__setattr__(self, "recorded_day", _nonnegative_integer(self.recorded_day, "recorded_day_invalid", 1_000_000))
|
|
3900
|
+
|
|
3901
|
+
def to_dict(self) -> dict[str, str | int]:
|
|
3902
|
+
return {
|
|
3903
|
+
"model_id": self.model_id,
|
|
3904
|
+
"effort": self.effort,
|
|
3905
|
+
"category": self.category,
|
|
3906
|
+
"risk": self.risk,
|
|
3907
|
+
"outcome": self.outcome,
|
|
3908
|
+
"input_bucket": self.input_bucket,
|
|
3909
|
+
"output_bucket": self.output_bucket,
|
|
3910
|
+
"latency_bucket": self.latency_bucket,
|
|
3911
|
+
"policy_version": self.policy_version,
|
|
3912
|
+
"recorded_day": self.recorded_day,
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
|
|
3916
|
+
def _default_machine_state_dir() -> Path:
|
|
3917
|
+
if os.name == "nt":
|
|
3918
|
+
base = os.environ.get("LOCALAPPDATA")
|
|
3919
|
+
return (Path(base) if base else Path.home() / "AppData" / "Local") / "Graphite" / "routing"
|
|
3920
|
+
xdg = os.environ.get("XDG_STATE_HOME")
|
|
3921
|
+
return (Path(xdg) if xdg else Path.home() / ".local" / "state") / "graphite" / "routing"
|
|
3922
|
+
|
|
3923
|
+
|
|
3924
|
+
class AggregateStore:
|
|
3925
|
+
"""Opt-in machine-wide store containing sanitized typed aggregates only."""
|
|
3926
|
+
|
|
3927
|
+
def __init__(
|
|
3928
|
+
self,
|
|
3929
|
+
repository_root: Path,
|
|
3930
|
+
*,
|
|
3931
|
+
opt_in: bool,
|
|
3932
|
+
state_dir: Path | None = None,
|
|
3933
|
+
) -> None:
|
|
3934
|
+
if not isinstance(opt_in, bool):
|
|
3935
|
+
raise ValueError("aggregate_opt_in_invalid")
|
|
3936
|
+
self.root = _selected_root(repository_root)
|
|
3937
|
+
self.opt_in = opt_in
|
|
3938
|
+
selected_state = (state_dir or _default_machine_state_dir()).resolve(strict=False)
|
|
3939
|
+
try:
|
|
3940
|
+
selected_state.relative_to(self.root)
|
|
3941
|
+
except ValueError:
|
|
3942
|
+
pass
|
|
3943
|
+
else:
|
|
3944
|
+
raise StorageError("aggregate_path_invalid")
|
|
3945
|
+
self.path = selected_state / "aggregate.sqlite3"
|
|
3946
|
+
|
|
3947
|
+
def _initialize(self) -> None:
|
|
3948
|
+
_secure_directory(self.path.parent)
|
|
3949
|
+
try:
|
|
3950
|
+
with closing(sqlite3.connect(self.path, timeout=2.0)) as connection, connection:
|
|
3951
|
+
connection.execute("PRAGMA journal_mode = WAL")
|
|
3952
|
+
connection.execute("PRAGMA busy_timeout = 2000")
|
|
3953
|
+
connection.execute(
|
|
3954
|
+
"""CREATE TABLE IF NOT EXISTS aggregate_events (
|
|
3955
|
+
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3956
|
+
model_id TEXT NOT NULL,
|
|
3957
|
+
effort TEXT NOT NULL,
|
|
3958
|
+
category TEXT NOT NULL,
|
|
3959
|
+
risk TEXT NOT NULL,
|
|
3960
|
+
outcome TEXT NOT NULL,
|
|
3961
|
+
input_bucket INTEGER NOT NULL,
|
|
3962
|
+
output_bucket INTEGER NOT NULL,
|
|
3963
|
+
latency_bucket INTEGER NOT NULL,
|
|
3964
|
+
policy_version TEXT NOT NULL,
|
|
3965
|
+
recorded_day INTEGER NOT NULL
|
|
3966
|
+
)"""
|
|
3967
|
+
)
|
|
3968
|
+
except sqlite3.Error as exc:
|
|
3969
|
+
raise _translate_database_error(exc) from exc
|
|
3970
|
+
_secure_file(self.path)
|
|
3971
|
+
|
|
3972
|
+
def write(self, record: AggregateRecord) -> bool:
|
|
3973
|
+
if not isinstance(record, AggregateRecord):
|
|
3974
|
+
raise ValueError("aggregate_record_invalid")
|
|
3975
|
+
if not self.opt_in:
|
|
3976
|
+
return False
|
|
3977
|
+
self._initialize()
|
|
3978
|
+
values = record.to_dict()
|
|
3979
|
+
try:
|
|
3980
|
+
with closing(sqlite3.connect(self.path, timeout=2.0)) as connection, connection:
|
|
3981
|
+
connection.execute("PRAGMA busy_timeout = 2000")
|
|
3982
|
+
connection.execute(
|
|
3983
|
+
"""INSERT INTO aggregate_events(
|
|
3984
|
+
model_id, effort, category, risk, outcome,
|
|
3985
|
+
input_bucket, output_bucket, latency_bucket,
|
|
3986
|
+
policy_version, recorded_day
|
|
3987
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
3988
|
+
tuple(values.values()),
|
|
3989
|
+
)
|
|
3990
|
+
except sqlite3.Error as exc:
|
|
3991
|
+
raise _translate_database_error(exc) from exc
|
|
3992
|
+
return True
|
|
3993
|
+
|
|
3994
|
+
def row_count(self) -> int:
|
|
3995
|
+
if not self.opt_in or not self.path.exists():
|
|
3996
|
+
return 0
|
|
3997
|
+
try:
|
|
3998
|
+
with closing(sqlite3.connect(self.path, timeout=2.0)) as connection, connection:
|
|
3999
|
+
return int(connection.execute("SELECT COUNT(*) FROM aggregate_events").fetchone()[0])
|
|
4000
|
+
except sqlite3.Error as exc:
|
|
4001
|
+
raise _translate_database_error(exc) from exc
|