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,22 @@
|
|
|
1
|
+
"""Pure process-launch contract builders shared across platforms."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
import threading
|
|
6
|
+
|
|
7
|
+
# Cooperative same-process boundary: repository-owned Windows launchers that
|
|
8
|
+
# enable inheritance must hold this lock. Normal launches expose child handles
|
|
9
|
+
# only for CreateProcessW and constrain them with HANDLE_LIST. The lock cannot
|
|
10
|
+
# serialize untrusted native threads; restoration failures are retried and fail
|
|
11
|
+
# the launch, retaining ownership for cleanup rather than claiming isolation.
|
|
12
|
+
WINDOWS_PROCESS_CREATION_LOCK = threading.RLock()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_windows_environment_block(environment: Mapping[str, str]) -> str:
|
|
16
|
+
"""Build a sorted, double-NUL-terminated Unicode environment block."""
|
|
17
|
+
entries: list[str] = []
|
|
18
|
+
for key, value in sorted(environment.items(), key=lambda item: item[0].upper()):
|
|
19
|
+
if not key or "=" in key or "\0" in key or "\0" in value:
|
|
20
|
+
raise ValueError("invalid environment")
|
|
21
|
+
entries.append(f"{key}={value}")
|
|
22
|
+
return "\0".join(entries) + "\0\0"
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""Bounded provider lifecycle observation without graph or execution authority."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
from collections import Counter
|
|
8
|
+
from collections.abc import Callable, Iterable
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from .routing.contracts import Effort
|
|
12
|
+
from .routing.lifecycle import (
|
|
13
|
+
LifecycleProviderId,
|
|
14
|
+
LifecycleReasonCode,
|
|
15
|
+
ProviderCompatibilityPolicy,
|
|
16
|
+
ProviderRuntimeIdentity,
|
|
17
|
+
RuntimeKind,
|
|
18
|
+
)
|
|
19
|
+
from .routing.lifecycle_service import (
|
|
20
|
+
LifecycleServiceError,
|
|
21
|
+
ProviderLifecycleService,
|
|
22
|
+
VerificationManifest,
|
|
23
|
+
)
|
|
24
|
+
from .routing.probe_runner import ProviderProbeError
|
|
25
|
+
|
|
26
|
+
_HEX_64 = re.compile(r"^[0-9a-f]{64}$")
|
|
27
|
+
_TARGET_ID = re.compile(r"^[a-z0-9][a-z0-9._-]{0,127}$")
|
|
28
|
+
_PROBE_REASON_CODES = frozenset(
|
|
29
|
+
{
|
|
30
|
+
"probe_auth_unhealthy",
|
|
31
|
+
"probe_capability_missing",
|
|
32
|
+
"probe_dns_busy",
|
|
33
|
+
"probe_endpoint_invalid",
|
|
34
|
+
"probe_executable_invalid",
|
|
35
|
+
"probe_failed",
|
|
36
|
+
"probe_http_status",
|
|
37
|
+
"probe_identity_changed",
|
|
38
|
+
"probe_model_unavailable",
|
|
39
|
+
"probe_protocol_invalid",
|
|
40
|
+
"probe_request_invalid",
|
|
41
|
+
"probe_response_too_large",
|
|
42
|
+
"probe_timeout",
|
|
43
|
+
"probe_unavailable",
|
|
44
|
+
"probe_version_invalid",
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
_PERSISTENCE_REASONS = frozenset(
|
|
48
|
+
{
|
|
49
|
+
"lifecycle_invalidation_failed",
|
|
50
|
+
"lifecycle_observation_invalid",
|
|
51
|
+
"lifecycle_persistence_failed",
|
|
52
|
+
"verification_manifest_invalid",
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
Probe = Callable[[float, int], ProviderRuntimeIdentity]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, slots=True)
|
|
60
|
+
class VerificationManifestRequest:
|
|
61
|
+
"""Exact hash-only inputs for optional non-executing manifest preparation."""
|
|
62
|
+
|
|
63
|
+
requested_model: str
|
|
64
|
+
expected_effective_model: str
|
|
65
|
+
effort: Effort
|
|
66
|
+
max_input_tokens: int
|
|
67
|
+
max_output_tokens: int
|
|
68
|
+
timeout_seconds: int
|
|
69
|
+
expires_at: int
|
|
70
|
+
fixture_repository_commit: str
|
|
71
|
+
graph_fingerprint: str
|
|
72
|
+
prompt_contract_hash: str
|
|
73
|
+
response_contract_hash: str
|
|
74
|
+
max_cost_microunits: int | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class ProviderObserverOptions:
|
|
79
|
+
"""Hard bounds for a provider observation worker."""
|
|
80
|
+
|
|
81
|
+
enabled_providers: tuple[LifecycleProviderId, ...] = tuple(LifecycleProviderId)
|
|
82
|
+
interval_seconds: float = 300.0
|
|
83
|
+
timeout_seconds: float = 15.0
|
|
84
|
+
max_observations_per_cycle: int = 4
|
|
85
|
+
backoff_cap_seconds: float = 3_600.0
|
|
86
|
+
jitter_ratio: float = 0.1
|
|
87
|
+
|
|
88
|
+
def validate(self) -> None:
|
|
89
|
+
try:
|
|
90
|
+
providers = tuple(LifecycleProviderId(value) for value in self.enabled_providers)
|
|
91
|
+
except (TypeError, ValueError):
|
|
92
|
+
raise ValueError("observer_enabled_providers_invalid") from None
|
|
93
|
+
if len(providers) > len(LifecycleProviderId) or len(set(providers)) != len(providers):
|
|
94
|
+
raise ValueError("observer_enabled_providers_invalid")
|
|
95
|
+
if not _bounded_number(self.interval_seconds, 1.0, 86_400.0):
|
|
96
|
+
raise ValueError("observer_interval_invalid")
|
|
97
|
+
if not _bounded_number(self.timeout_seconds, 0.1, 30.0):
|
|
98
|
+
raise ValueError("observer_timeout_invalid")
|
|
99
|
+
if (
|
|
100
|
+
isinstance(self.max_observations_per_cycle, bool)
|
|
101
|
+
or not isinstance(self.max_observations_per_cycle, int)
|
|
102
|
+
or not 1 <= self.max_observations_per_cycle <= len(LifecycleProviderId)
|
|
103
|
+
):
|
|
104
|
+
raise ValueError("observer_cycle_limit_invalid")
|
|
105
|
+
if (
|
|
106
|
+
not _bounded_number(self.backoff_cap_seconds, self.interval_seconds, 86_400.0)
|
|
107
|
+
or not _bounded_number(self.jitter_ratio, 0.0, 0.5)
|
|
108
|
+
):
|
|
109
|
+
raise ValueError("observer_backoff_invalid")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _bounded_number(value: object, minimum: float, maximum: float) -> bool:
|
|
113
|
+
return (
|
|
114
|
+
not isinstance(value, bool)
|
|
115
|
+
and isinstance(value, (int, float))
|
|
116
|
+
and math.isfinite(value)
|
|
117
|
+
and minimum <= float(value) <= maximum
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True, slots=True)
|
|
122
|
+
class ProviderObservationTarget:
|
|
123
|
+
"""One repository lifecycle boundary and its non-inference probe."""
|
|
124
|
+
|
|
125
|
+
target_id: str
|
|
126
|
+
boundary_digest: str
|
|
127
|
+
provider: LifecycleProviderId
|
|
128
|
+
runtime_kind: RuntimeKind
|
|
129
|
+
policy: ProviderCompatibilityPolicy
|
|
130
|
+
lifecycle_service: ProviderLifecycleService
|
|
131
|
+
probe: Probe
|
|
132
|
+
machine_cache_key: str | None = None
|
|
133
|
+
verification_request: VerificationManifestRequest | None = None
|
|
134
|
+
|
|
135
|
+
def __post_init__(self) -> None:
|
|
136
|
+
try:
|
|
137
|
+
provider = LifecycleProviderId(self.provider)
|
|
138
|
+
runtime_kind = RuntimeKind(self.runtime_kind)
|
|
139
|
+
except (TypeError, ValueError):
|
|
140
|
+
raise ValueError("observer_target_invalid") from None
|
|
141
|
+
if (
|
|
142
|
+
not isinstance(self.target_id, str)
|
|
143
|
+
or _TARGET_ID.fullmatch(self.target_id) is None
|
|
144
|
+
or not isinstance(self.boundary_digest, str)
|
|
145
|
+
or _HEX_64.fullmatch(self.boundary_digest) is None
|
|
146
|
+
or not isinstance(self.policy, ProviderCompatibilityPolicy)
|
|
147
|
+
or self.policy.provider is not provider
|
|
148
|
+
or self.policy.runtime_kind is not runtime_kind
|
|
149
|
+
or not callable(self.probe)
|
|
150
|
+
or (
|
|
151
|
+
self.verification_request is not None
|
|
152
|
+
and not isinstance(
|
|
153
|
+
self.verification_request, VerificationManifestRequest
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
):
|
|
157
|
+
raise ValueError("observer_target_invalid")
|
|
158
|
+
if self.machine_cache_key is not None and (
|
|
159
|
+
runtime_kind is not RuntimeKind.LOCAL_CLI
|
|
160
|
+
or not isinstance(self.machine_cache_key, str)
|
|
161
|
+
or _HEX_64.fullmatch(self.machine_cache_key) is None
|
|
162
|
+
):
|
|
163
|
+
raise ValueError("observer_cache_scope_invalid")
|
|
164
|
+
object.__setattr__(self, "provider", provider)
|
|
165
|
+
object.__setattr__(self, "runtime_kind", runtime_kind)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(frozen=True, slots=True)
|
|
169
|
+
class ProviderObservationSummary:
|
|
170
|
+
attempted: int
|
|
171
|
+
deferred: int
|
|
172
|
+
succeeded: int
|
|
173
|
+
failed: int
|
|
174
|
+
state_counts: dict[str, int]
|
|
175
|
+
reason_counts: dict[str, int]
|
|
176
|
+
prepared_manifests: tuple[VerificationManifest, ...] = ()
|
|
177
|
+
|
|
178
|
+
def to_status(self) -> dict[str, object]:
|
|
179
|
+
"""Return the bounded aggregate safe for daemon status and health."""
|
|
180
|
+
return {
|
|
181
|
+
"attempted": self.attempted,
|
|
182
|
+
"deferred": self.deferred,
|
|
183
|
+
"succeeded": self.succeeded,
|
|
184
|
+
"failed": self.failed,
|
|
185
|
+
"state_counts": dict(self.state_counts),
|
|
186
|
+
"reason_counts": dict(self.reason_counts),
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass(slots=True)
|
|
191
|
+
class _Schedule:
|
|
192
|
+
next_due: float = 0.0
|
|
193
|
+
consecutive_failures: int = 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class ProviderObserver:
|
|
197
|
+
"""Observe due providers once and persist only lifecycle transitions."""
|
|
198
|
+
|
|
199
|
+
def __init__(self, options: ProviderObserverOptions | None = None) -> None:
|
|
200
|
+
self.options = options or ProviderObserverOptions()
|
|
201
|
+
self.options.validate()
|
|
202
|
+
self._enabled = frozenset(
|
|
203
|
+
LifecycleProviderId(value) for value in self.options.enabled_providers
|
|
204
|
+
)
|
|
205
|
+
self._schedules: dict[str, _Schedule] = {}
|
|
206
|
+
|
|
207
|
+
def run_cycle(
|
|
208
|
+
self,
|
|
209
|
+
targets: Iterable[ProviderObservationTarget],
|
|
210
|
+
*,
|
|
211
|
+
now: float,
|
|
212
|
+
) -> ProviderObservationSummary:
|
|
213
|
+
if not _bounded_number(now, 0.0, 10**12):
|
|
214
|
+
raise ValueError("observer_time_invalid")
|
|
215
|
+
normalized = tuple(targets)
|
|
216
|
+
if len(normalized) > 1_000 or any(
|
|
217
|
+
not isinstance(target, ProviderObservationTarget) for target in normalized
|
|
218
|
+
):
|
|
219
|
+
raise ValueError("observer_targets_invalid")
|
|
220
|
+
identifiers = [target.target_id for target in normalized]
|
|
221
|
+
if len(set(identifiers)) != len(identifiers):
|
|
222
|
+
raise ValueError("observer_targets_invalid")
|
|
223
|
+
current_identifiers = set(identifiers)
|
|
224
|
+
for target_id in tuple(self._schedules):
|
|
225
|
+
if target_id not in current_identifiers:
|
|
226
|
+
del self._schedules[target_id]
|
|
227
|
+
|
|
228
|
+
enabled = [target for target in normalized if target.provider in self._enabled]
|
|
229
|
+
due = sorted(
|
|
230
|
+
(
|
|
231
|
+
target
|
|
232
|
+
for target in enabled
|
|
233
|
+
if self._schedules.setdefault(target.target_id, _Schedule()).next_due
|
|
234
|
+
<= float(now)
|
|
235
|
+
),
|
|
236
|
+
key=lambda target: target.target_id,
|
|
237
|
+
)
|
|
238
|
+
selected = due[: self.options.max_observations_per_cycle]
|
|
239
|
+
cache: dict[str, ProviderRuntimeIdentity | ProviderProbeError] = {}
|
|
240
|
+
states: Counter[str] = Counter()
|
|
241
|
+
reasons: Counter[str] = Counter()
|
|
242
|
+
succeeded = 0
|
|
243
|
+
prepared_manifests: list[VerificationManifest] = []
|
|
244
|
+
|
|
245
|
+
for target in selected:
|
|
246
|
+
schedule = self._schedules[target.target_id]
|
|
247
|
+
try:
|
|
248
|
+
identity = self._probe(target, int(now), cache)
|
|
249
|
+
result = target.lifecycle_service.observe(
|
|
250
|
+
boundary_digest=target.boundary_digest,
|
|
251
|
+
identity=identity,
|
|
252
|
+
policy=target.policy,
|
|
253
|
+
)
|
|
254
|
+
if target.verification_request is not None:
|
|
255
|
+
if result.state.value != "verification_required":
|
|
256
|
+
raise LifecycleServiceError("verification_manifest_invalid")
|
|
257
|
+
request = target.verification_request
|
|
258
|
+
prepared_manifests.append(
|
|
259
|
+
target.lifecycle_service.prepare_verification_manifest(
|
|
260
|
+
boundary_digest=target.boundary_digest,
|
|
261
|
+
requested_model=request.requested_model,
|
|
262
|
+
expected_effective_model=request.expected_effective_model,
|
|
263
|
+
effort=request.effort,
|
|
264
|
+
max_input_tokens=request.max_input_tokens,
|
|
265
|
+
max_output_tokens=request.max_output_tokens,
|
|
266
|
+
timeout_seconds=request.timeout_seconds,
|
|
267
|
+
expires_at=request.expires_at,
|
|
268
|
+
fixture_repository_commit=request.fixture_repository_commit,
|
|
269
|
+
graph_fingerprint=request.graph_fingerprint,
|
|
270
|
+
prompt_contract_hash=request.prompt_contract_hash,
|
|
271
|
+
response_contract_hash=request.response_contract_hash,
|
|
272
|
+
max_cost_microunits=request.max_cost_microunits,
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
except ProviderProbeError as exc:
|
|
276
|
+
reason = self._record_unavailable(target, int(now), exc)
|
|
277
|
+
states["unavailable"] += 1
|
|
278
|
+
reasons[reason] += 1
|
|
279
|
+
self._back_off(target.target_id, schedule, float(now))
|
|
280
|
+
except LifecycleServiceError as exc:
|
|
281
|
+
reasons[_lifecycle_reason(exc)] += 1
|
|
282
|
+
self._back_off(target.target_id, schedule, float(now))
|
|
283
|
+
except Exception:
|
|
284
|
+
reasons["lifecycle_persistence_failed"] += 1
|
|
285
|
+
self._back_off(target.target_id, schedule, float(now))
|
|
286
|
+
else:
|
|
287
|
+
states[result.state.value] += 1
|
|
288
|
+
reasons[result.reason.value] += 1
|
|
289
|
+
succeeded += 1
|
|
290
|
+
schedule.consecutive_failures = 0
|
|
291
|
+
schedule.next_due = float(now) + self._jittered_delay(
|
|
292
|
+
target.target_id, self.options.interval_seconds, 0
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
attempted = len(selected)
|
|
296
|
+
return ProviderObservationSummary(
|
|
297
|
+
attempted=attempted,
|
|
298
|
+
deferred=max(0, len(due) - attempted),
|
|
299
|
+
succeeded=succeeded,
|
|
300
|
+
failed=attempted - succeeded,
|
|
301
|
+
state_counts=dict(sorted(states.items())),
|
|
302
|
+
reason_counts=dict(sorted(reasons.items())),
|
|
303
|
+
prepared_manifests=tuple(prepared_manifests),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def _probe(
|
|
307
|
+
self,
|
|
308
|
+
target: ProviderObservationTarget,
|
|
309
|
+
observed_at: int,
|
|
310
|
+
cache: dict[str, ProviderRuntimeIdentity | ProviderProbeError],
|
|
311
|
+
) -> ProviderRuntimeIdentity:
|
|
312
|
+
cache_key = target.machine_cache_key
|
|
313
|
+
if cache_key is not None and cache_key in cache:
|
|
314
|
+
cached = cache[cache_key]
|
|
315
|
+
if isinstance(cached, ProviderProbeError):
|
|
316
|
+
raise cached
|
|
317
|
+
identity = cached
|
|
318
|
+
else:
|
|
319
|
+
try:
|
|
320
|
+
identity = target.probe(float(self.options.timeout_seconds), observed_at)
|
|
321
|
+
except ProviderProbeError as exc:
|
|
322
|
+
sanitized = ProviderProbeError(_probe_reason(exc.code))
|
|
323
|
+
if cache_key is not None:
|
|
324
|
+
cache[cache_key] = sanitized
|
|
325
|
+
raise sanitized from None
|
|
326
|
+
except Exception:
|
|
327
|
+
sanitized = ProviderProbeError("probe_failed")
|
|
328
|
+
if cache_key is not None:
|
|
329
|
+
cache[cache_key] = sanitized
|
|
330
|
+
raise sanitized from None
|
|
331
|
+
if cache_key is not None:
|
|
332
|
+
cache[cache_key] = identity
|
|
333
|
+
if (
|
|
334
|
+
not isinstance(identity, ProviderRuntimeIdentity)
|
|
335
|
+
or identity.provider is not target.provider
|
|
336
|
+
or identity.runtime_kind is not target.runtime_kind
|
|
337
|
+
or identity.policy_version != target.policy.policy_version
|
|
338
|
+
or identity.observed_at != observed_at
|
|
339
|
+
):
|
|
340
|
+
raise ProviderProbeError("probe_protocol_invalid")
|
|
341
|
+
return identity
|
|
342
|
+
|
|
343
|
+
def _record_unavailable(
|
|
344
|
+
self,
|
|
345
|
+
target: ProviderObservationTarget,
|
|
346
|
+
observed_at: int,
|
|
347
|
+
error: ProviderProbeError,
|
|
348
|
+
) -> str:
|
|
349
|
+
probe_reason = _probe_reason(error.code)
|
|
350
|
+
lifecycle_reason = (
|
|
351
|
+
LifecycleReasonCode.CREDENTIAL_UNHEALTHY
|
|
352
|
+
if probe_reason == "probe_auth_unhealthy"
|
|
353
|
+
else LifecycleReasonCode.RUNTIME_MISSING
|
|
354
|
+
)
|
|
355
|
+
try:
|
|
356
|
+
target.lifecycle_service.mark_unavailable(
|
|
357
|
+
boundary_digest=target.boundary_digest,
|
|
358
|
+
provider=target.provider,
|
|
359
|
+
runtime_kind=target.runtime_kind,
|
|
360
|
+
policy_version=target.policy.policy_version,
|
|
361
|
+
observed_at=observed_at,
|
|
362
|
+
reason=lifecycle_reason,
|
|
363
|
+
)
|
|
364
|
+
except LifecycleServiceError as exc:
|
|
365
|
+
return _lifecycle_reason(exc)
|
|
366
|
+
if lifecycle_reason is LifecycleReasonCode.CREDENTIAL_UNHEALTHY:
|
|
367
|
+
return lifecycle_reason.value
|
|
368
|
+
if probe_reason in {"probe_timeout", "probe_dns_busy"}:
|
|
369
|
+
return probe_reason
|
|
370
|
+
return lifecycle_reason.value if probe_reason == "probe_executable_invalid" else probe_reason
|
|
371
|
+
|
|
372
|
+
def _back_off(self, target_id: str, schedule: _Schedule, now: float) -> None:
|
|
373
|
+
schedule.consecutive_failures = min(schedule.consecutive_failures + 1, 30)
|
|
374
|
+
delay = min(
|
|
375
|
+
self.options.backoff_cap_seconds,
|
|
376
|
+
self.options.interval_seconds * (2 ** schedule.consecutive_failures),
|
|
377
|
+
)
|
|
378
|
+
schedule.next_due = now + self._jittered_delay(
|
|
379
|
+
target_id, delay, schedule.consecutive_failures
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
def _jittered_delay(self, target_id: str, delay: float, attempt: int) -> float:
|
|
383
|
+
if self.options.jitter_ratio == 0:
|
|
384
|
+
return float(delay)
|
|
385
|
+
digest = hashlib.sha256(f"{target_id}:{attempt}".encode("ascii")).digest()
|
|
386
|
+
unit = int.from_bytes(digest[:8], "big") / ((1 << 64) - 1)
|
|
387
|
+
factor = 1.0 + ((unit * 2.0) - 1.0) * self.options.jitter_ratio
|
|
388
|
+
return max(0.1, float(delay) * factor)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _probe_reason(code: object) -> str:
|
|
392
|
+
return code if isinstance(code, str) and code in _PROBE_REASON_CODES else "probe_failed"
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _lifecycle_reason(error: LifecycleServiceError) -> str:
|
|
396
|
+
code = str(error)
|
|
397
|
+
return code if code in _PERSISTENCE_REASONS else "lifecycle_persistence_failed"
|