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,555 @@
|
|
|
1
|
+
"""Fail-closed coordination for provider lifecycle execution authority."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import ClassVar
|
|
8
|
+
|
|
9
|
+
from .contracts import Effort, PublicRecord
|
|
10
|
+
from .lifecycle import (
|
|
11
|
+
IdentityChange,
|
|
12
|
+
LifecycleReasonCode,
|
|
13
|
+
ProviderCompatibilityAssessment,
|
|
14
|
+
ProviderCompatibilityPolicy,
|
|
15
|
+
ProviderLifecycleEvent,
|
|
16
|
+
ProviderLifecycleState,
|
|
17
|
+
ProviderRuntimeIdentity,
|
|
18
|
+
assess_identity_change,
|
|
19
|
+
)
|
|
20
|
+
from .lifecycle_storage import CurrentLifecycleObservation, LifecycleStore, LifecycleStorageError
|
|
21
|
+
from .storage import RepositoryStore, StorageError
|
|
22
|
+
from .route_pool import ApprovedRouteCandidate, RouteAuthority
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LifecycleServiceError(RuntimeError):
|
|
26
|
+
"""Stable lifecycle authority failure without provider diagnostics."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, code: str) -> None:
|
|
29
|
+
self.code = code
|
|
30
|
+
super().__init__(code)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class LifecycleObservationResult(PublicRecord):
|
|
35
|
+
lifecycle_identity_digest: str
|
|
36
|
+
change: IdentityChange
|
|
37
|
+
state: ProviderLifecycleState
|
|
38
|
+
reason: LifecycleReasonCode
|
|
39
|
+
invalidated_snapshots: tuple[str, ...]
|
|
40
|
+
invalidated_approvals: tuple[str, ...]
|
|
41
|
+
carried_snapshots: tuple[str, ...]
|
|
42
|
+
|
|
43
|
+
_public_fields: ClassVar[tuple[str, ...]] = (
|
|
44
|
+
"lifecycle_identity_digest", "change", "state", "reason",
|
|
45
|
+
"invalidated_snapshots", "invalidated_approvals", "carried_snapshots",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class ExecutionLifecycleAuthority(PublicRecord):
|
|
51
|
+
lifecycle_identity_digest: str
|
|
52
|
+
capability_snapshot_digest: str
|
|
53
|
+
approval_id: str
|
|
54
|
+
state: ProviderLifecycleState
|
|
55
|
+
|
|
56
|
+
_public_fields: ClassVar[tuple[str, ...]] = (
|
|
57
|
+
"lifecycle_identity_digest", "capability_snapshot_digest", "approval_id", "state"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class VerificationManifest(PublicRecord):
|
|
63
|
+
provider: str
|
|
64
|
+
runtime_kind: str
|
|
65
|
+
lifecycle_identity_digest: str
|
|
66
|
+
runtime_version: str
|
|
67
|
+
runtime_digest: str
|
|
68
|
+
model_identity_digest: str | None
|
|
69
|
+
routing_policy_digest: str | None
|
|
70
|
+
policy_version: str
|
|
71
|
+
requested_model: str
|
|
72
|
+
expected_effective_model: str
|
|
73
|
+
effort: str
|
|
74
|
+
permission_mode: str
|
|
75
|
+
max_input_tokens: int
|
|
76
|
+
max_output_tokens: int
|
|
77
|
+
timeout_seconds: int
|
|
78
|
+
expires_at: int
|
|
79
|
+
fixture_repository_commit: str
|
|
80
|
+
graph_fingerprint: str
|
|
81
|
+
prompt_contract_hash: str
|
|
82
|
+
response_contract_hash: str
|
|
83
|
+
max_cost_microunits: int | None
|
|
84
|
+
max_attempts: int
|
|
85
|
+
fallback_enabled: bool
|
|
86
|
+
no_resume: bool
|
|
87
|
+
no_substitution: bool
|
|
88
|
+
evidence_fields: tuple[str, ...]
|
|
89
|
+
|
|
90
|
+
_public_fields: ClassVar[tuple[str, ...]] = (
|
|
91
|
+
"provider", "runtime_kind", "lifecycle_identity_digest", "runtime_version",
|
|
92
|
+
"runtime_digest", "model_identity_digest", "routing_policy_digest",
|
|
93
|
+
"policy_version", "requested_model", "expected_effective_model", "effort",
|
|
94
|
+
"permission_mode", "max_input_tokens", "max_output_tokens", "timeout_seconds",
|
|
95
|
+
"expires_at",
|
|
96
|
+
"fixture_repository_commit", "graph_fingerprint", "prompt_contract_hash",
|
|
97
|
+
"response_contract_hash", "max_cost_microunits", "max_attempts",
|
|
98
|
+
"fallback_enabled", "no_resume", "no_substitution", "evidence_fields",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def digest(self) -> str:
|
|
103
|
+
return hashlib.sha256(json.dumps(self.to_dict(), sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def build_verification_manifest(
|
|
107
|
+
current: CurrentLifecycleObservation,
|
|
108
|
+
*,
|
|
109
|
+
requested_model: str,
|
|
110
|
+
expected_effective_model: str,
|
|
111
|
+
effort: Effort,
|
|
112
|
+
max_input_tokens: int,
|
|
113
|
+
max_output_tokens: int,
|
|
114
|
+
timeout_seconds: int,
|
|
115
|
+
expires_at: int,
|
|
116
|
+
fixture_repository_commit: str,
|
|
117
|
+
graph_fingerprint: str,
|
|
118
|
+
prompt_contract_hash: str,
|
|
119
|
+
response_contract_hash: str,
|
|
120
|
+
max_cost_microunits: int | None = None,
|
|
121
|
+
) -> VerificationManifest:
|
|
122
|
+
"""Build a bounded manifest from one already-read exact lifecycle observation."""
|
|
123
|
+
try:
|
|
124
|
+
normalized_effort = Effort(effort)
|
|
125
|
+
except (TypeError, ValueError):
|
|
126
|
+
raise LifecycleServiceError("verification_manifest_invalid") from None
|
|
127
|
+
values = (max_input_tokens, max_output_tokens, timeout_seconds, expires_at)
|
|
128
|
+
digests = (graph_fingerprint, prompt_contract_hash, response_contract_hash)
|
|
129
|
+
models = (requested_model, expected_effective_model)
|
|
130
|
+
safe_models = all(
|
|
131
|
+
isinstance(value, str)
|
|
132
|
+
and 0 < len(value) <= 256
|
|
133
|
+
and all(
|
|
134
|
+
character.isascii() and (character.isalnum() or character in "._:-")
|
|
135
|
+
for character in value
|
|
136
|
+
)
|
|
137
|
+
for value in models
|
|
138
|
+
)
|
|
139
|
+
commit_valid = (
|
|
140
|
+
isinstance(fixture_repository_commit, str)
|
|
141
|
+
and len(fixture_repository_commit) in {40, 64}
|
|
142
|
+
and all(character in "0123456789abcdef" for character in fixture_repository_commit)
|
|
143
|
+
)
|
|
144
|
+
if (
|
|
145
|
+
not isinstance(current, CurrentLifecycleObservation)
|
|
146
|
+
or current.identity is None
|
|
147
|
+
or current.state is not ProviderLifecycleState.VERIFICATION_REQUIRED
|
|
148
|
+
or any(
|
|
149
|
+
isinstance(value, bool) or not isinstance(value, int) or value <= 0
|
|
150
|
+
for value in values
|
|
151
|
+
)
|
|
152
|
+
or not safe_models
|
|
153
|
+
or not commit_valid
|
|
154
|
+
or any(
|
|
155
|
+
not isinstance(value, str)
|
|
156
|
+
or len(value) != 64
|
|
157
|
+
or any(character not in "0123456789abcdef" for character in value)
|
|
158
|
+
for value in digests
|
|
159
|
+
)
|
|
160
|
+
or max_cost_microunits is not None
|
|
161
|
+
and (
|
|
162
|
+
isinstance(max_cost_microunits, bool)
|
|
163
|
+
or not isinstance(max_cost_microunits, int)
|
|
164
|
+
or max_cost_microunits <= 0
|
|
165
|
+
)
|
|
166
|
+
or expires_at <= current.updated_at
|
|
167
|
+
):
|
|
168
|
+
raise LifecycleServiceError("verification_manifest_invalid")
|
|
169
|
+
identity = current.identity
|
|
170
|
+
return VerificationManifest(
|
|
171
|
+
identity.provider.value, identity.runtime_kind.value, identity.digest,
|
|
172
|
+
identity.version, identity.runtime_digest, identity.model_identity_digest,
|
|
173
|
+
identity.routing_policy_digest, identity.policy_version, requested_model,
|
|
174
|
+
expected_effective_model, normalized_effort.value, "read-only",
|
|
175
|
+
max_input_tokens, max_output_tokens, timeout_seconds, expires_at,
|
|
176
|
+
fixture_repository_commit, graph_fingerprint, prompt_contract_hash,
|
|
177
|
+
response_contract_hash, max_cost_microunits, 1, False, True, True,
|
|
178
|
+
(
|
|
179
|
+
"duration_ms", "effective_model", "exit_classification",
|
|
180
|
+
"input_tokens", "output_tokens", "stderr_sha256", "stdout_sha256",
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _event(
|
|
186
|
+
boundary_digest: str,
|
|
187
|
+
identity: ProviderRuntimeIdentity,
|
|
188
|
+
previous: CurrentLifecycleObservation | None,
|
|
189
|
+
state: ProviderLifecycleState,
|
|
190
|
+
reason: LifecycleReasonCode,
|
|
191
|
+
occurred_at: int,
|
|
192
|
+
) -> ProviderLifecycleEvent:
|
|
193
|
+
payload = {
|
|
194
|
+
"boundary": boundary_digest,
|
|
195
|
+
"identity": identity.digest,
|
|
196
|
+
"previous_state": None if previous is None else previous.state.value,
|
|
197
|
+
"state": state.value,
|
|
198
|
+
"reason": reason.value,
|
|
199
|
+
"occurred_at": occurred_at,
|
|
200
|
+
}
|
|
201
|
+
event_id = hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
|
202
|
+
return ProviderLifecycleEvent(
|
|
203
|
+
event_id, identity.provider, identity.runtime_kind,
|
|
204
|
+
None if previous is None or previous.identity is None else previous.identity.digest,
|
|
205
|
+
identity.digest,
|
|
206
|
+
None if previous is None else previous.state,
|
|
207
|
+
state, reason, identity.policy_version, occurred_at,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class ProviderLifecycleService:
|
|
212
|
+
"""Coordinates isolated observations with routing authority bindings."""
|
|
213
|
+
|
|
214
|
+
def __init__(self, lifecycle_store: LifecycleStore, routing_store: RepositoryStore) -> None:
|
|
215
|
+
if not isinstance(lifecycle_store, LifecycleStore) or not isinstance(routing_store, RepositoryStore):
|
|
216
|
+
raise LifecycleServiceError("lifecycle_service_invalid")
|
|
217
|
+
self.lifecycle_store = lifecycle_store
|
|
218
|
+
self.routing_store = routing_store
|
|
219
|
+
|
|
220
|
+
def active_identity_digest(self, boundary_digest: str) -> str:
|
|
221
|
+
try:
|
|
222
|
+
current = self.lifecycle_store.current_observation(boundary_digest)
|
|
223
|
+
except (LifecycleStorageError, ValueError):
|
|
224
|
+
raise LifecycleServiceError("lifecycle_authority_unavailable") from None
|
|
225
|
+
if current is None or current.identity is None or current.state is not ProviderLifecycleState.ACTIVE:
|
|
226
|
+
raise LifecycleServiceError("lifecycle_inactive")
|
|
227
|
+
return current.identity.digest
|
|
228
|
+
|
|
229
|
+
def route_authority(
|
|
230
|
+
self, boundary_digest: str, candidate: ApprovedRouteCandidate
|
|
231
|
+
) -> RouteAuthority:
|
|
232
|
+
if not isinstance(candidate, ApprovedRouteCandidate):
|
|
233
|
+
raise LifecycleServiceError("lifecycle_authority_invalid")
|
|
234
|
+
try:
|
|
235
|
+
current = self.lifecycle_store.current_observation(boundary_digest)
|
|
236
|
+
snapshot_binding = self.routing_store.lifecycle_identity_binding(
|
|
237
|
+
authority_kind="capability_snapshot",
|
|
238
|
+
authority_id=candidate.capability_snapshot_digest,
|
|
239
|
+
)
|
|
240
|
+
except (LifecycleStorageError, StorageError, ValueError):
|
|
241
|
+
raise LifecycleServiceError("lifecycle_authority_unavailable") from None
|
|
242
|
+
if current is None or current.identity is None:
|
|
243
|
+
raise LifecycleServiceError("lifecycle_inactive")
|
|
244
|
+
if snapshot_binding != current.identity.digest:
|
|
245
|
+
raise LifecycleServiceError("lifecycle_binding_missing")
|
|
246
|
+
return RouteAuthority(
|
|
247
|
+
candidate.candidate_id,
|
|
248
|
+
current.identity.provider,
|
|
249
|
+
current.identity.runtime_kind,
|
|
250
|
+
current.identity.digest,
|
|
251
|
+
candidate.capability_snapshot_digest,
|
|
252
|
+
current.state,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def require_snapshot_authority(
|
|
256
|
+
self, *, boundary_digest: str, lifecycle_identity_digest: str,
|
|
257
|
+
capability_snapshot_digest: str, live_identity: ProviderRuntimeIdentity,
|
|
258
|
+
) -> None:
|
|
259
|
+
try:
|
|
260
|
+
current = self.lifecycle_store.current_observation(boundary_digest)
|
|
261
|
+
binding = self.routing_store.lifecycle_identity_binding(
|
|
262
|
+
authority_kind="capability_snapshot",
|
|
263
|
+
authority_id=capability_snapshot_digest,
|
|
264
|
+
)
|
|
265
|
+
except (LifecycleStorageError, StorageError, ValueError):
|
|
266
|
+
raise LifecycleServiceError("lifecycle_authority_unavailable") from None
|
|
267
|
+
if current is None or current.identity is None or current.state is not ProviderLifecycleState.ACTIVE:
|
|
268
|
+
raise LifecycleServiceError("lifecycle_inactive")
|
|
269
|
+
if not isinstance(live_identity, ProviderRuntimeIdentity) or current.identity.digest != lifecycle_identity_digest or live_identity.digest != lifecycle_identity_digest:
|
|
270
|
+
raise LifecycleServiceError("lifecycle_identity_changed")
|
|
271
|
+
if binding != lifecycle_identity_digest:
|
|
272
|
+
raise LifecycleServiceError("lifecycle_binding_missing")
|
|
273
|
+
|
|
274
|
+
def _record(
|
|
275
|
+
self,
|
|
276
|
+
boundary_digest: str,
|
|
277
|
+
identity: ProviderRuntimeIdentity,
|
|
278
|
+
previous: CurrentLifecycleObservation | None,
|
|
279
|
+
state: ProviderLifecycleState,
|
|
280
|
+
reason: LifecycleReasonCode,
|
|
281
|
+
occurred_at: int,
|
|
282
|
+
) -> CurrentLifecycleObservation:
|
|
283
|
+
try:
|
|
284
|
+
self.lifecycle_store.record_transition(
|
|
285
|
+
boundary_digest,
|
|
286
|
+
identity,
|
|
287
|
+
_event(boundary_digest, identity, previous, state, reason, occurred_at),
|
|
288
|
+
)
|
|
289
|
+
current = self.lifecycle_store.current_observation(boundary_digest)
|
|
290
|
+
except (LifecycleStorageError, ValueError):
|
|
291
|
+
raise LifecycleServiceError("lifecycle_persistence_failed") from None
|
|
292
|
+
if current is None:
|
|
293
|
+
raise LifecycleServiceError("lifecycle_persistence_failed")
|
|
294
|
+
return current
|
|
295
|
+
|
|
296
|
+
def _carry_forward(
|
|
297
|
+
self,
|
|
298
|
+
boundary_digest: str,
|
|
299
|
+
identity: ProviderRuntimeIdentity,
|
|
300
|
+
previous: CurrentLifecycleObservation,
|
|
301
|
+
assessment: ProviderCompatibilityAssessment,
|
|
302
|
+
) -> LifecycleObservationResult:
|
|
303
|
+
if previous.identity is None:
|
|
304
|
+
raise LifecycleServiceError("lifecycle_observation_invalid")
|
|
305
|
+
event = _event(
|
|
306
|
+
boundary_digest,
|
|
307
|
+
identity,
|
|
308
|
+
previous,
|
|
309
|
+
ProviderLifecycleState.ACTIVE,
|
|
310
|
+
LifecycleReasonCode.PATCH_CARRIED_FORWARD,
|
|
311
|
+
identity.observed_at,
|
|
312
|
+
)
|
|
313
|
+
try:
|
|
314
|
+
carried = self.routing_store.carry_forward_snapshot_bindings(
|
|
315
|
+
previous_identity_digest=previous.identity.digest,
|
|
316
|
+
new_identity_digest=identity.digest,
|
|
317
|
+
lifecycle_event_id=event.event_id,
|
|
318
|
+
carried_at=identity.observed_at,
|
|
319
|
+
)
|
|
320
|
+
invalidated_approvals = self.routing_store.invalidate_lifecycle_approvals(
|
|
321
|
+
previous.identity.digest
|
|
322
|
+
)
|
|
323
|
+
self.lifecycle_store.record_invalidations(
|
|
324
|
+
boundary_digest=boundary_digest,
|
|
325
|
+
previous_identity_digest=previous.identity.digest,
|
|
326
|
+
current_identity_digest=identity.digest,
|
|
327
|
+
capability_snapshot_digests=(),
|
|
328
|
+
approval_ids=invalidated_approvals,
|
|
329
|
+
reason=LifecycleReasonCode.PATCH_CARRIED_FORWARD,
|
|
330
|
+
invalidated_at=identity.observed_at,
|
|
331
|
+
)
|
|
332
|
+
except (StorageError, LifecycleStorageError, ValueError):
|
|
333
|
+
raise LifecycleServiceError("lifecycle_carry_forward_failed") from None
|
|
334
|
+
try:
|
|
335
|
+
self.lifecycle_store.record_transition(boundary_digest, identity, event)
|
|
336
|
+
current = self.lifecycle_store.current_observation(boundary_digest)
|
|
337
|
+
except (LifecycleStorageError, ValueError):
|
|
338
|
+
raise LifecycleServiceError("lifecycle_persistence_failed") from None
|
|
339
|
+
if current is None:
|
|
340
|
+
raise LifecycleServiceError("lifecycle_persistence_failed")
|
|
341
|
+
return LifecycleObservationResult(
|
|
342
|
+
identity.digest,
|
|
343
|
+
assessment.change,
|
|
344
|
+
current.state,
|
|
345
|
+
assessment.reason,
|
|
346
|
+
(),
|
|
347
|
+
invalidated_approvals,
|
|
348
|
+
carried,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
def observe(
|
|
352
|
+
self,
|
|
353
|
+
*, boundary_digest: str, identity: ProviderRuntimeIdentity,
|
|
354
|
+
policy: ProviderCompatibilityPolicy, standard_probe_passed: bool = True,
|
|
355
|
+
expanded_probe_passed: bool = True,
|
|
356
|
+
) -> LifecycleObservationResult:
|
|
357
|
+
if not isinstance(identity, ProviderRuntimeIdentity) or not isinstance(policy, ProviderCompatibilityPolicy):
|
|
358
|
+
raise LifecycleServiceError("lifecycle_observation_invalid")
|
|
359
|
+
try:
|
|
360
|
+
previous = self.lifecycle_store.current_observation(boundary_digest)
|
|
361
|
+
except (LifecycleStorageError, ValueError):
|
|
362
|
+
raise LifecycleServiceError("lifecycle_persistence_failed") from None
|
|
363
|
+
if previous is not None and previous.identity is not None and (
|
|
364
|
+
previous.identity.provider is not identity.provider
|
|
365
|
+
or previous.identity.runtime_kind is not identity.runtime_kind
|
|
366
|
+
):
|
|
367
|
+
raise LifecycleServiceError("lifecycle_boundary_changed")
|
|
368
|
+
|
|
369
|
+
comparison_identity = None if previous is None else previous.identity
|
|
370
|
+
if previous is None:
|
|
371
|
+
if not standard_probe_passed:
|
|
372
|
+
state = ProviderLifecycleState.INCOMPATIBLE
|
|
373
|
+
reason = LifecycleReasonCode.PROBE_FAILED
|
|
374
|
+
current = self._record(boundary_digest, identity, None, state, reason, identity.observed_at)
|
|
375
|
+
assessment = assess_identity_change(None, identity, policy)
|
|
376
|
+
change = assessment.change
|
|
377
|
+
else:
|
|
378
|
+
assessment = assess_identity_change(None, identity, policy)
|
|
379
|
+
current = self._record(boundary_digest, identity, None, assessment.state, assessment.reason, identity.observed_at)
|
|
380
|
+
change = assessment.change
|
|
381
|
+
elif previous.state is ProviderLifecycleState.UNAVAILABLE:
|
|
382
|
+
current = self._record(boundary_digest, identity, previous, ProviderLifecycleState.DISCOVERED, LifecycleReasonCode.DISCOVERED, identity.observed_at)
|
|
383
|
+
assessment = assess_identity_change(None, identity, policy)
|
|
384
|
+
change = assessment.change
|
|
385
|
+
else:
|
|
386
|
+
assessment = assess_identity_change(
|
|
387
|
+
comparison_identity, identity, policy,
|
|
388
|
+
standard_probe_passed=standard_probe_passed,
|
|
389
|
+
expanded_probe_passed=expanded_probe_passed,
|
|
390
|
+
existing_state=previous.state,
|
|
391
|
+
)
|
|
392
|
+
reason = assessment.reason
|
|
393
|
+
if previous.state is ProviderLifecycleState.INCOMPATIBLE and assessment.state is ProviderLifecycleState.VERIFICATION_REQUIRED:
|
|
394
|
+
reason = LifecycleReasonCode.POLICY_PROMOTED
|
|
395
|
+
if reason is LifecycleReasonCode.PATCH_CARRIED_FORWARD:
|
|
396
|
+
return self._carry_forward(boundary_digest, identity, previous, assessment)
|
|
397
|
+
current = self._record(boundary_digest, identity, previous, assessment.state, reason, identity.observed_at)
|
|
398
|
+
change = assessment.change
|
|
399
|
+
|
|
400
|
+
if current.state is ProviderLifecycleState.DISCOVERED and policy.supports(identity) and standard_probe_passed:
|
|
401
|
+
current = self._record(boundary_digest, identity, current, ProviderLifecycleState.VERIFICATION_REQUIRED, LifecycleReasonCode.COMPATIBILITY_CONFIRMED, identity.observed_at)
|
|
402
|
+
|
|
403
|
+
invalidated_snapshots: tuple[str, ...] = ()
|
|
404
|
+
invalidated_approvals: tuple[str, ...] = ()
|
|
405
|
+
if previous is not None and previous.identity is not None and (
|
|
406
|
+
previous.identity.digest != identity.digest
|
|
407
|
+
or previous.state is ProviderLifecycleState.ACTIVE and current.state is not ProviderLifecycleState.ACTIVE
|
|
408
|
+
):
|
|
409
|
+
try:
|
|
410
|
+
invalidated_snapshots, expected_approvals = self.routing_store.lifecycle_authority_targets(previous.identity.digest)
|
|
411
|
+
invalidated_approvals = self.routing_store.invalidate_lifecycle_approvals(previous.identity.digest)
|
|
412
|
+
if invalidated_approvals != expected_approvals:
|
|
413
|
+
raise StorageError("lifecycle_invalidation_changed")
|
|
414
|
+
self.lifecycle_store.record_invalidations(
|
|
415
|
+
boundary_digest=boundary_digest,
|
|
416
|
+
previous_identity_digest=previous.identity.digest,
|
|
417
|
+
current_identity_digest=identity.digest,
|
|
418
|
+
capability_snapshot_digests=invalidated_snapshots,
|
|
419
|
+
approval_ids=invalidated_approvals,
|
|
420
|
+
reason=assessment.reason,
|
|
421
|
+
invalidated_at=identity.observed_at,
|
|
422
|
+
)
|
|
423
|
+
except (StorageError, LifecycleStorageError, ValueError):
|
|
424
|
+
raise LifecycleServiceError("lifecycle_invalidation_failed") from None
|
|
425
|
+
return LifecycleObservationResult(identity.digest, change, current.state, assessment.reason, invalidated_snapshots, invalidated_approvals, ())
|
|
426
|
+
|
|
427
|
+
def mark_unavailable(
|
|
428
|
+
self,
|
|
429
|
+
*,
|
|
430
|
+
boundary_digest: str,
|
|
431
|
+
provider: object,
|
|
432
|
+
runtime_kind: object,
|
|
433
|
+
policy_version: str,
|
|
434
|
+
observed_at: int,
|
|
435
|
+
reason: LifecycleReasonCode,
|
|
436
|
+
) -> CurrentLifecycleObservation:
|
|
437
|
+
if reason not in {
|
|
438
|
+
LifecycleReasonCode.RUNTIME_MISSING,
|
|
439
|
+
LifecycleReasonCode.CREDENTIAL_UNHEALTHY,
|
|
440
|
+
}:
|
|
441
|
+
raise LifecycleServiceError("lifecycle_observation_invalid")
|
|
442
|
+
try:
|
|
443
|
+
current = self.lifecycle_store.current_observation(boundary_digest)
|
|
444
|
+
if current is not None and current.identity is not None:
|
|
445
|
+
identity = current.identity
|
|
446
|
+
if identity.provider != provider or identity.runtime_kind != runtime_kind or identity.policy_version != policy_version:
|
|
447
|
+
raise LifecycleServiceError("lifecycle_boundary_changed")
|
|
448
|
+
current_digest = identity.digest
|
|
449
|
+
else:
|
|
450
|
+
identity = None
|
|
451
|
+
current_digest = None
|
|
452
|
+
repeated = current is not None and current.state is ProviderLifecycleState.UNAVAILABLE
|
|
453
|
+
event = ProviderLifecycleEvent(
|
|
454
|
+
hashlib.sha256(f"{boundary_digest}:{observed_at}:unavailable".encode()).hexdigest(),
|
|
455
|
+
provider,
|
|
456
|
+
runtime_kind,
|
|
457
|
+
None if current is None or current.identity is None else current.identity.digest,
|
|
458
|
+
current_digest,
|
|
459
|
+
None if current is None else current.state,
|
|
460
|
+
ProviderLifecycleState.UNAVAILABLE,
|
|
461
|
+
LifecycleReasonCode.IDENTITY_UNCHANGED if repeated else reason,
|
|
462
|
+
policy_version,
|
|
463
|
+
observed_at,
|
|
464
|
+
)
|
|
465
|
+
self.lifecycle_store.record_transition(boundary_digest, identity, event)
|
|
466
|
+
updated = self.lifecycle_store.current_observation(boundary_digest)
|
|
467
|
+
if current is not None and current.identity is not None and current.state is ProviderLifecycleState.ACTIVE:
|
|
468
|
+
snapshots, approvals = self.routing_store.lifecycle_authority_targets(current.identity.digest)
|
|
469
|
+
invalidated = self.routing_store.invalidate_lifecycle_approvals(current.identity.digest)
|
|
470
|
+
if approvals != invalidated:
|
|
471
|
+
raise StorageError("lifecycle_invalidation_changed")
|
|
472
|
+
self.lifecycle_store.record_invalidations(
|
|
473
|
+
boundary_digest=boundary_digest,
|
|
474
|
+
previous_identity_digest=current.identity.digest,
|
|
475
|
+
current_identity_digest=current.identity.digest,
|
|
476
|
+
capability_snapshot_digests=snapshots,
|
|
477
|
+
approval_ids=invalidated,
|
|
478
|
+
reason=reason,
|
|
479
|
+
invalidated_at=observed_at,
|
|
480
|
+
)
|
|
481
|
+
except LifecycleServiceError:
|
|
482
|
+
raise
|
|
483
|
+
except (LifecycleStorageError, StorageError, TypeError, ValueError):
|
|
484
|
+
raise LifecycleServiceError("lifecycle_persistence_failed") from None
|
|
485
|
+
if updated is None:
|
|
486
|
+
raise LifecycleServiceError("lifecycle_persistence_failed")
|
|
487
|
+
return updated
|
|
488
|
+
|
|
489
|
+
def activate(
|
|
490
|
+
self, *, boundary_digest: str, lifecycle_identity_digest: str,
|
|
491
|
+
capability_snapshot_digest: str, activated_at: int,
|
|
492
|
+
) -> CurrentLifecycleObservation:
|
|
493
|
+
try:
|
|
494
|
+
current = self.lifecycle_store.current_observation(boundary_digest)
|
|
495
|
+
binding = self.routing_store.lifecycle_identity_binding(authority_kind="capability_snapshot", authority_id=capability_snapshot_digest)
|
|
496
|
+
records = self.routing_store.capability_snapshot_records(limit=64)
|
|
497
|
+
except (LifecycleStorageError, StorageError, ValueError):
|
|
498
|
+
raise LifecycleServiceError("lifecycle_authority_unavailable") from None
|
|
499
|
+
if current is None or current.identity is None or current.state is not ProviderLifecycleState.VERIFICATION_REQUIRED:
|
|
500
|
+
raise LifecycleServiceError("lifecycle_verification_required")
|
|
501
|
+
if current.identity.digest != lifecycle_identity_digest or binding != lifecycle_identity_digest:
|
|
502
|
+
raise LifecycleServiceError("lifecycle_identity_changed")
|
|
503
|
+
matching = [row for row in records if row["digest"] == capability_snapshot_digest]
|
|
504
|
+
if len(matching) != 1 or matching[0]["payload"].get("expires_at", 0) <= activated_at:
|
|
505
|
+
raise LifecycleServiceError("lifecycle_snapshot_invalid")
|
|
506
|
+
return self._record(boundary_digest, current.identity, current, ProviderLifecycleState.ACTIVE, LifecycleReasonCode.VERIFICATION_ACCEPTED, activated_at)
|
|
507
|
+
|
|
508
|
+
def require_execution_authority(
|
|
509
|
+
self, *, boundary_digest: str, lifecycle_identity_digest: str,
|
|
510
|
+
capability_snapshot_digest: str, approval_id: str,
|
|
511
|
+
live_identity: ProviderRuntimeIdentity,
|
|
512
|
+
) -> ExecutionLifecycleAuthority:
|
|
513
|
+
try:
|
|
514
|
+
current = self.lifecycle_store.current_observation(boundary_digest)
|
|
515
|
+
snapshot_binding = self.routing_store.lifecycle_identity_binding(authority_kind="capability_snapshot", authority_id=capability_snapshot_digest)
|
|
516
|
+
approval_binding = self.routing_store.lifecycle_approval_binding_details(approval_id)
|
|
517
|
+
except (LifecycleStorageError, StorageError, ValueError):
|
|
518
|
+
raise LifecycleServiceError("lifecycle_authority_unavailable") from None
|
|
519
|
+
if current is None or current.identity is None or current.state is not ProviderLifecycleState.ACTIVE:
|
|
520
|
+
raise LifecycleServiceError("lifecycle_inactive")
|
|
521
|
+
if not isinstance(live_identity, ProviderRuntimeIdentity) or current.identity.digest != lifecycle_identity_digest or live_identity.digest != lifecycle_identity_digest:
|
|
522
|
+
raise LifecycleServiceError("lifecycle_identity_changed")
|
|
523
|
+
if snapshot_binding != lifecycle_identity_digest or approval_binding != (capability_snapshot_digest, lifecycle_identity_digest):
|
|
524
|
+
raise LifecycleServiceError("lifecycle_binding_missing")
|
|
525
|
+
return ExecutionLifecycleAuthority(lifecycle_identity_digest, capability_snapshot_digest, approval_id, current.state)
|
|
526
|
+
|
|
527
|
+
def prepare_verification_manifest(
|
|
528
|
+
self, *, boundary_digest: str, requested_model: str,
|
|
529
|
+
expected_effective_model: str, effort: Effort, max_input_tokens: int,
|
|
530
|
+
max_output_tokens: int, timeout_seconds: int, expires_at: int,
|
|
531
|
+
fixture_repository_commit: str, graph_fingerprint: str,
|
|
532
|
+
prompt_contract_hash: str, response_contract_hash: str,
|
|
533
|
+
max_cost_microunits: int | None = None,
|
|
534
|
+
) -> VerificationManifest:
|
|
535
|
+
try:
|
|
536
|
+
current = self.lifecycle_store.current_observation(boundary_digest)
|
|
537
|
+
except (LifecycleStorageError, ValueError):
|
|
538
|
+
raise LifecycleServiceError("verification_manifest_invalid") from None
|
|
539
|
+
if current is None:
|
|
540
|
+
raise LifecycleServiceError("verification_manifest_invalid")
|
|
541
|
+
return build_verification_manifest(
|
|
542
|
+
current,
|
|
543
|
+
requested_model=requested_model,
|
|
544
|
+
expected_effective_model=expected_effective_model,
|
|
545
|
+
effort=effort,
|
|
546
|
+
max_input_tokens=max_input_tokens,
|
|
547
|
+
max_output_tokens=max_output_tokens,
|
|
548
|
+
timeout_seconds=timeout_seconds,
|
|
549
|
+
expires_at=expires_at,
|
|
550
|
+
fixture_repository_commit=fixture_repository_commit,
|
|
551
|
+
graph_fingerprint=graph_fingerprint,
|
|
552
|
+
prompt_contract_hash=prompt_contract_hash,
|
|
553
|
+
response_contract_hash=response_contract_hash,
|
|
554
|
+
max_cost_microunits=max_cost_microunits,
|
|
555
|
+
)
|