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.
Files changed (112) hide show
  1. graphite/__init__.py +41 -0
  2. graphite/__main__.py +7 -0
  3. graphite/_cleanup_worker.py +525 -0
  4. graphite/activation.py +164 -0
  5. graphite/agent_hooks.py +577 -0
  6. graphite/agent_settings.py +226 -0
  7. graphite/analyze.py +146 -0
  8. graphite/answer_contract.py +420 -0
  9. graphite/bootstrap.py +210 -0
  10. graphite/buildlock.py +99 -0
  11. graphite/cache.py +131 -0
  12. graphite/channel.py +1325 -0
  13. graphite/cli.py +3053 -0
  14. graphite/cluster.py +111 -0
  15. graphite/config.py +209 -0
  16. graphite/context.py +355 -0
  17. graphite/daemon.py +745 -0
  18. graphite/daemon_health.py +733 -0
  19. graphite/debt.py +118 -0
  20. graphite/dependency_install.py +1597 -0
  21. graphite/detach.py +33 -0
  22. graphite/doctor.py +678 -0
  23. graphite/doctor_probes.py +2100 -0
  24. graphite/engine_identity.py +238 -0
  25. graphite/export/__init__.py +6 -0
  26. graphite/export/html.py +244 -0
  27. graphite/export/json.py +39 -0
  28. graphite/export/md.py +68 -0
  29. graphite/extract/__init__.py +4 -0
  30. graphite/extract/ast.py +1964 -0
  31. graphite/freshness.py +127 -0
  32. graphite/git.py +406 -0
  33. graphite/graph.py +117 -0
  34. graphite/graph_io.py +188 -0
  35. graphite/health.py +147 -0
  36. graphite/hook_entry.py +68 -0
  37. graphite/hookinstall.py +224 -0
  38. graphite/hookshim.py +86 -0
  39. graphite/incident_ledger.py +247 -0
  40. graphite/ingest.py +279 -0
  41. graphite/init.py +791 -0
  42. graphite/io.py +32 -0
  43. graphite/listing.py +51 -0
  44. graphite/llm.py +518 -0
  45. graphite/llm_probe.py +157 -0
  46. graphite/mcp.py +7 -0
  47. graphite/mcp_server.py +450 -0
  48. graphite/natural_query.py +252 -0
  49. graphite/overlays.py +713 -0
  50. graphite/probe_process.py +879 -0
  51. graphite/probe_workspace.py +728 -0
  52. graphite/process_contracts.py +22 -0
  53. graphite/provider_observer.py +397 -0
  54. graphite/query.py +646 -0
  55. graphite/query_plan.py +97 -0
  56. graphite/replacement_audit.py +291 -0
  57. graphite/resolve.py +660 -0
  58. graphite/review.py +782 -0
  59. graphite/routing/__init__.py +5 -0
  60. graphite/routing/approval.py +362 -0
  61. graphite/routing/classifier.py +169 -0
  62. graphite/routing/claude_executor.py +419 -0
  63. graphite/routing/claude_probe.py +102 -0
  64. graphite/routing/cli_identity.py +84 -0
  65. graphite/routing/codex_executor.py +383 -0
  66. graphite/routing/codex_probe.py +93 -0
  67. graphite/routing/context_builder.py +327 -0
  68. graphite/routing/contracts.py +802 -0
  69. graphite/routing/diff_policy.py +468 -0
  70. graphite/routing/edit_apply.py +166 -0
  71. graphite/routing/effort.py +43 -0
  72. graphite/routing/lifecycle.py +771 -0
  73. graphite/routing/lifecycle_operator.py +227 -0
  74. graphite/routing/lifecycle_service.py +555 -0
  75. graphite/routing/lifecycle_storage.py +977 -0
  76. graphite/routing/ollama_executor.py +341 -0
  77. graphite/routing/ollama_probe.py +72 -0
  78. graphite/routing/openrouter_executor.py +338 -0
  79. graphite/routing/openrouter_probe.py +188 -0
  80. graphite/routing/policy.py +815 -0
  81. graphite/routing/probe_runner.py +543 -0
  82. graphite/routing/process_runner.py +523 -0
  83. graphite/routing/profiles.py +554 -0
  84. graphite/routing/prompt.py +58 -0
  85. graphite/routing/registry.py +444 -0
  86. graphite/routing/route_pool.py +629 -0
  87. graphite/routing/route_pool_execution.py +275 -0
  88. graphite/routing/schema_validation.py +169 -0
  89. graphite/routing/service.py +1263 -0
  90. graphite/routing/settings.py +99 -0
  91. graphite/routing/shadow.py +201 -0
  92. graphite/routing/storage.py +4001 -0
  93. graphite/routing/telemetry.py +346 -0
  94. graphite/routing/worktree.py +259 -0
  95. graphite/routing/zai_edit.py +113 -0
  96. graphite/routing/zai_executor.py +191 -0
  97. graphite/routing/zai_probe.py +126 -0
  98. graphite/savings.py +84 -0
  99. graphite/ts_bridge.py +142 -0
  100. graphite/ts_resolver.mjs +314 -0
  101. graphite/typescript_activation.py +1586 -0
  102. graphite/usage_ledger.py +156 -0
  103. graphite/validation.py +148 -0
  104. graphite/watch.py +167 -0
  105. graphite/windows_job.py +368 -0
  106. graphite/windows_startup.py +144 -0
  107. graphite/windows_task.py +212 -0
  108. graphite_code-0.3.0.dist-info/METADATA +743 -0
  109. graphite_code-0.3.0.dist-info/RECORD +112 -0
  110. graphite_code-0.3.0.dist-info/WHEEL +4 -0
  111. graphite_code-0.3.0.dist-info/entry_points.txt +3 -0
  112. 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
+ )