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,383 @@
1
+ """Hardened adapter for an authenticated Codex ChatGPT subscription CLI."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import json
6
+ import re
7
+ import stat
8
+ from collections.abc import Callable
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Final
12
+
13
+ from .claude_executor import AdapterError, _canonical_file, _file_sha256, _identifier
14
+ from .cli_identity import CliIdentityPrimitiveError, parse_semantic_version_output
15
+ from .contracts import CliIdentity, Effort, PermissionMode, ProviderId
16
+ from .process_runner import (
17
+ CliProcessError,
18
+ CliProcessResult,
19
+ QUOTA_MARKERS,
20
+ decode_cli_output,
21
+ run_cli_process,
22
+ )
23
+
24
+ ADAPTER_PROTOCOL_VERSION: Final = "1.0.0"
25
+ # Windows write authority depends on Codex's [windows] sandbox setting, which
26
+ # normally lives in user config. `--ignore-user-config` strips it, so a
27
+ # workspace-write execution must bind the sandbox mode explicitly in argv or
28
+ # every write tool call is denied under `-a never` and the edit becomes a
29
+ # silent no-op. The binding is passed on every platform for one deterministic
30
+ # argv contract; non-Windows Codex ignores the [windows] section.
31
+ WINDOWS_SANDBOX_MODE: Final = "elevated"
32
+ PREFLIGHT_TIMEOUT_SECONDS: Final = 15.0
33
+ EXECUTION_TIMEOUT_SECONDS: Final = 1_800.0
34
+ MAX_EVENT_COUNT: Final = 10_000
35
+ MAX_MESSAGE_LENGTH: Final = 1_048_576
36
+ MAX_TOKEN_COUNT: Final = 10_000_000
37
+ MAX_OUTPUT_SCHEMA_BYTES: Final = 65_536
38
+ _VERSION = re.compile(r"^codex-cli (0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\r?\n?$")
39
+ _ALLOWED_EVENTS: Final = frozenset(
40
+ {
41
+ "thread.started",
42
+ "turn.started",
43
+ "item.started",
44
+ "item.updated",
45
+ "item.completed",
46
+ "turn.completed",
47
+ "turn.failed",
48
+ "error",
49
+ }
50
+ )
51
+ _TRANSPORT_ERRORS: Final = {
52
+ "timeout": "timeout",
53
+ "cancelled": "cancelled",
54
+ "response_limit": "response_limit",
55
+ "process_containment_unavailable": "containment",
56
+ "process_containment_failed": "containment",
57
+ }
58
+ _PREFLIGHT_WARNING_PREFIXES: Final = (
59
+ "WARNING: failed to clean up stale arg0 temp dirs:",
60
+ "WARNING: proceeding, even though we could not create PATH aliases:",
61
+ )
62
+ _CAPACITY_MESSAGE: Final = "selected model is at capacity. please try a different model."
63
+ _SHA256: Final = re.compile(r"^[0-9a-f]{64}$")
64
+ _REPARSE_POINT = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
65
+
66
+
67
+ @dataclass(frozen=True, slots=True)
68
+ class CodexExecutionResult:
69
+ effective_model: str
70
+ message: str = field(repr=False)
71
+ input_tokens: int | None
72
+ output_tokens: int | None
73
+ duration_seconds: float
74
+ input_sha256: str
75
+ stdout_sha256: str
76
+ stderr_sha256: str
77
+
78
+
79
+ Transport = Callable[..., CliProcessResult]
80
+
81
+
82
+ def _is_reparse(metadata: object) -> bool:
83
+ attributes = getattr(metadata, "st_file_attributes", 0)
84
+ return bool(attributes & _REPARSE_POINT)
85
+
86
+
87
+ def _output_schema(
88
+ path: Path | None,
89
+ expected_sha256: str | None,
90
+ *,
91
+ workspace: Path,
92
+ ) -> tuple[Path, str] | None:
93
+ if path is None and expected_sha256 is None:
94
+ return None
95
+ if (
96
+ not isinstance(path, Path)
97
+ or not path.is_absolute()
98
+ or not isinstance(expected_sha256, str)
99
+ or _SHA256.fullmatch(expected_sha256) is None
100
+ ):
101
+ raise AdapterError("request_invalid")
102
+ try:
103
+ metadata = path.lstat()
104
+ resolved = path.resolve(strict=True)
105
+ resolved_metadata = resolved.stat()
106
+ workspace_root = workspace.resolve(strict=True)
107
+ if (
108
+ not stat.S_ISREG(metadata.st_mode)
109
+ or stat.S_ISLNK(metadata.st_mode)
110
+ or _is_reparse(metadata)
111
+ or not stat.S_ISREG(resolved_metadata.st_mode)
112
+ or _is_reparse(resolved_metadata)
113
+ or not 0 < metadata.st_size <= MAX_OUTPUT_SCHEMA_BYTES
114
+ or resolved.is_relative_to(workspace_root)
115
+ ):
116
+ raise OSError
117
+ body = resolved.read_bytes()
118
+ parsed = json.loads(body.decode("utf-8"))
119
+ except (OSError, RuntimeError, UnicodeDecodeError, json.JSONDecodeError):
120
+ raise AdapterError("response_contract_invalid") from None
121
+ if not isinstance(parsed, dict):
122
+ raise AdapterError("response_contract_invalid")
123
+ digest = hashlib.sha256(body).hexdigest()
124
+ if digest != expected_sha256:
125
+ raise AdapterError("response_contract_mismatch")
126
+ return resolved, digest
127
+
128
+
129
+ def _invoke(transport: Transport, **kwargs: object) -> CliProcessResult:
130
+ try:
131
+ result = transport(**kwargs)
132
+ except CliProcessError as exc:
133
+ raise AdapterError(
134
+ _TRANSPORT_ERRORS.get(exc.code, "unavailable"),
135
+ process_diagnostics=exc.diagnostics,
136
+ ) from None
137
+ except AdapterError:
138
+ raise
139
+ except Exception:
140
+ raise AdapterError("unavailable") from None
141
+ if not isinstance(result, CliProcessResult) or result.returncode != 0:
142
+ raise AdapterError("unavailable")
143
+ return result
144
+
145
+
146
+ def preflight_codex(
147
+ *,
148
+ executable: Path,
149
+ workspace: Path,
150
+ credential_home: Path | None,
151
+ transport: Transport = run_cli_process,
152
+ ) -> CliIdentity:
153
+ """Verify executable identity and ChatGPT subscription authentication."""
154
+ resolved = _canonical_file(executable, workspace=workspace)
155
+ executable_digest = _file_sha256(resolved)
156
+ common = {
157
+ "cwd": workspace,
158
+ "provider": ProviderId.CODEX,
159
+ "credential_home": credential_home,
160
+ "timeout_seconds": PREFLIGHT_TIMEOUT_SECONDS,
161
+ }
162
+ version_result = _invoke(
163
+ transport, argv=(str(resolved), "--version"), stdin=b"", **common
164
+ )
165
+ try:
166
+ version_text = decode_cli_output(version_result.stdout)
167
+ except CliProcessError:
168
+ raise AdapterError("version") from None
169
+ try:
170
+ version = parse_semantic_version_output(version_text, _VERSION)
171
+ except CliIdentityPrimitiveError:
172
+ raise AdapterError("version")
173
+ status_result = _invoke(
174
+ transport, argv=(str(resolved), "login", "status"), stdin=b"", **common
175
+ )
176
+ try:
177
+ streams = (
178
+ decode_cli_output(status_result.stdout),
179
+ decode_cli_output(status_result.stderr),
180
+ )
181
+ except CliProcessError:
182
+ raise AdapterError("auth_required") from None
183
+ lines = [line.strip() for stream in streams for line in stream.splitlines() if line.strip()]
184
+ status_count = lines.count("Logged in using ChatGPT")
185
+ noise = [line for line in lines if line != "Logged in using ChatGPT"]
186
+ if status_count != 1 or any(
187
+ not line.startswith(_PREFLIGHT_WARNING_PREFIXES) for line in noise
188
+ ):
189
+ raise AdapterError("auth_required")
190
+ if _file_sha256(resolved) != executable_digest:
191
+ raise AdapterError("version")
192
+ return CliIdentity(
193
+ ProviderId.CODEX,
194
+ executable_digest,
195
+ version,
196
+ ADAPTER_PROTOCOL_VERSION,
197
+ )
198
+
199
+
200
+ def _token(value: object) -> int:
201
+ if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= MAX_TOKEN_COUNT:
202
+ raise AdapterError("protocol")
203
+ return value
204
+
205
+
206
+ def _failure_code(event: dict[str, object]) -> str:
207
+ text = json.dumps(event, ensure_ascii=True, separators=(",", ":")).lower()
208
+ if "auth" in text or "login" in text or "unauthorized" in text:
209
+ return "auth_required"
210
+ if any(marker in text for marker in QUOTA_MARKERS):
211
+ return "quota"
212
+ return "unavailable"
213
+
214
+
215
+ def _parse_jsonl(
216
+ stdout: bytes,
217
+ expected_model: str,
218
+ *,
219
+ final_message_only: bool = False,
220
+ ) -> tuple[str, str, int, int]:
221
+ try:
222
+ text = decode_cli_output(stdout)
223
+ except CliProcessError:
224
+ raise AdapterError("protocol") from None
225
+ lines = text.splitlines()
226
+ if not lines or len(lines) > MAX_EVENT_COUNT or any(not line.strip() for line in lines):
227
+ raise AdapterError("protocol")
228
+ messages: list[str] = []
229
+ completion: dict[str, object] | None = None
230
+ terminal_index: int | None = None
231
+ for index, line in enumerate(lines):
232
+ try:
233
+ event = json.loads(line)
234
+ except json.JSONDecodeError:
235
+ raise AdapterError("protocol") from None
236
+ if not isinstance(event, dict) or event.get("type") not in _ALLOWED_EVENTS:
237
+ raise AdapterError("protocol")
238
+ event_type = event["type"]
239
+ if event_type in {"turn.failed", "error"}:
240
+ if index != len(lines) - 1:
241
+ raise AdapterError("protocol")
242
+ raise AdapterError(_failure_code(event))
243
+ if event_type == "item.completed":
244
+ item = event.get("item")
245
+ if isinstance(item, dict) and item.get("type") == "agent_message":
246
+ message = item.get("text")
247
+ if not isinstance(message, str) or not message or len(message) > MAX_MESSAGE_LENGTH:
248
+ raise AdapterError("protocol")
249
+ messages.append(message)
250
+ if event_type == "turn.completed":
251
+ if completion is not None:
252
+ raise AdapterError("protocol")
253
+ completion = event
254
+ terminal_index = index
255
+ if completion is None or terminal_index != len(lines) - 1:
256
+ raise AdapterError("protocol")
257
+ effective_model = completion.get("model")
258
+ if effective_model is None:
259
+ # Codex's documented exec JSONL terminal event does not echo the model.
260
+ # Identity remains bound by the full requested slug, strict config, and
261
+ # the immutable capability snapshot. A future conflicting echo fails.
262
+ effective_model = expected_model
263
+ else:
264
+ if not isinstance(effective_model, str):
265
+ raise AdapterError("model_identity_unverified")
266
+ try:
267
+ _identifier(effective_model)
268
+ except AdapterError:
269
+ raise AdapterError("model_identity_unverified") from None
270
+ if effective_model != expected_model:
271
+ raise AdapterError("model_mismatch")
272
+ usage = completion.get("usage")
273
+ if not isinstance(usage, dict):
274
+ raise AdapterError("protocol")
275
+ message = messages[-1] if final_message_only else "\n".join(messages)
276
+ if not message:
277
+ raise AdapterError("protocol")
278
+ if message.strip().lower() == _CAPACITY_MESSAGE:
279
+ raise AdapterError("capacity_unavailable")
280
+ return (
281
+ message,
282
+ effective_model,
283
+ _token(usage.get("input_tokens")),
284
+ _token(usage.get("output_tokens")),
285
+ )
286
+
287
+
288
+ def execute_codex(
289
+ *,
290
+ executable: Path,
291
+ workspace: Path,
292
+ credential_home: Path | None,
293
+ prompt: bytes,
294
+ requested_model: str,
295
+ expected_effective_model: str,
296
+ effort: Effort,
297
+ permission_mode: PermissionMode,
298
+ output_schema_path: Path | None = None,
299
+ output_schema_sha256: str | None = None,
300
+ transport: Transport = run_cli_process,
301
+ timeout_seconds: float = EXECUTION_TIMEOUT_SECONDS,
302
+ ) -> CodexExecutionResult:
303
+ """Execute exactly one ephemeral, sandboxed Codex task."""
304
+ resolved = _canonical_file(executable, workspace=workspace)
305
+ requested = _identifier(requested_model)
306
+ expected = _identifier(expected_effective_model)
307
+ try:
308
+ normalized_effort = Effort(effort)
309
+ permission = PermissionMode(permission_mode)
310
+ except (TypeError, ValueError):
311
+ raise AdapterError("request_invalid") from None
312
+ if normalized_effort in {Effort.DEFAULT, Effort.MAX}:
313
+ raise AdapterError("request_invalid")
314
+ schema = _output_schema(
315
+ output_schema_path,
316
+ output_schema_sha256,
317
+ workspace=workspace,
318
+ )
319
+ sandbox = "workspace-write" if permission is PermissionMode.WORKSPACE_WRITE else "read-only"
320
+ argv_prefix = (
321
+ str(resolved),
322
+ "--strict-config",
323
+ "-a",
324
+ "never",
325
+ "-s",
326
+ sandbox,
327
+ "-C",
328
+ str(workspace.resolve()),
329
+ "-m",
330
+ requested,
331
+ "-c",
332
+ f'model_reasoning_effort="{normalized_effort.value}"',
333
+ *(
334
+ ("-c", f'windows.sandbox="{WINDOWS_SANDBOX_MODE}"')
335
+ if permission is PermissionMode.WORKSPACE_WRITE
336
+ else ()
337
+ ),
338
+ "exec",
339
+ "--json",
340
+ "--ephemeral",
341
+ "--ignore-user-config",
342
+ "--ignore-rules",
343
+ )
344
+ argv = (
345
+ *argv_prefix,
346
+ *(("--output-schema", str(schema[0])) if schema is not None else ()),
347
+ "-",
348
+ )
349
+ result = _invoke(
350
+ transport,
351
+ argv=argv,
352
+ cwd=workspace,
353
+ stdin=prompt,
354
+ provider=ProviderId.CODEX,
355
+ credential_home=credential_home,
356
+ timeout_seconds=timeout_seconds,
357
+ )
358
+ if schema is not None:
359
+ try:
360
+ current = _output_schema(
361
+ schema[0],
362
+ schema[1],
363
+ workspace=workspace,
364
+ )
365
+ except AdapterError:
366
+ raise AdapterError("response_contract_changed") from None
367
+ if current is None or current[1] != schema[1]:
368
+ raise AdapterError("response_contract_changed")
369
+ message, effective_model, input_tokens, output_tokens = _parse_jsonl(
370
+ result.stdout,
371
+ expected,
372
+ final_message_only=schema is not None,
373
+ )
374
+ return CodexExecutionResult(
375
+ effective_model,
376
+ message,
377
+ input_tokens,
378
+ output_tokens,
379
+ result.duration_seconds,
380
+ result.input_sha256,
381
+ result.stdout_sha256,
382
+ result.stderr_sha256,
383
+ )
@@ -0,0 +1,93 @@
1
+ """Bounded, non-inference Codex CLI lifecycle observation."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ import re
6
+ import time
7
+ from collections.abc import Callable
8
+ from pathlib import Path
9
+ from typing import Final
10
+
11
+ from .cli_identity import CliIdentityPrimitiveError, canonical_executable, executable_sha256, parse_semantic_version_output
12
+ from .lifecycle import LifecycleProviderId, ProviderRuntimeIdentity, RuntimeKind
13
+ from .probe_runner import ProviderProbeError, run_process_probe
14
+ from .process_runner import CliProcessError, CliProcessResult, decode_cli_output
15
+
16
+ _VERSION: Final = re.compile(r"^codex-cli (0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\r?\n?$")
17
+ _GLOBAL_FLAGS: Final = frozenset({"--strict-config", "-a", "-s", "-C", "-m", "-c"})
18
+ _EXEC_FLAGS: Final = frozenset(
19
+ {"--json", "--ephemeral", "--ignore-user-config", "--ignore-rules"}
20
+ )
21
+ _GLOBAL_FLAG_PATTERNS: Final = tuple(
22
+ re.compile(rf"(?<![A-Za-z0-9_-]){re.escape(flag)}(?![A-Za-z0-9_-])")
23
+ for flag in sorted(_GLOBAL_FLAGS)
24
+ )
25
+ _EXEC_FLAG_PATTERNS: Final = tuple(
26
+ re.compile(rf"(?<![A-Za-z0-9_-]){re.escape(flag)}(?![A-Za-z0-9_-])")
27
+ for flag in sorted(_EXEC_FLAGS)
28
+ )
29
+ _WARNING_PREFIXES: Final = ("WARNING: failed to clean up stale arg0 temp dirs:", "WARNING: proceeding, even though we could not create PATH aliases:")
30
+ ProcessProbe = Callable[..., CliProcessResult]
31
+
32
+
33
+ def _streams(result: CliProcessResult, code: str) -> tuple[str, str]:
34
+ if not isinstance(result, CliProcessResult) or result.returncode != 0:
35
+ raise ProviderProbeError(code)
36
+ try:
37
+ return decode_cli_output(result.stdout), decode_cli_output(result.stderr)
38
+ except CliProcessError:
39
+ raise ProviderProbeError(code) from None
40
+
41
+
42
+ def observe_codex(
43
+ *, executable: Path, workspace: Path, credential_home: Path | None,
44
+ observed_at: int, policy_version: str, timeout_seconds: float = 15.0,
45
+ transport: ProcessProbe = run_process_probe,
46
+ clock: Callable[[], float] = time.monotonic,
47
+ ) -> ProviderRuntimeIdentity:
48
+ """Observe one Codex CLI through fixed local metadata commands only."""
49
+ if isinstance(timeout_seconds, bool) or not isinstance(timeout_seconds, (int, float)) or not math.isfinite(timeout_seconds) or not 0.1 <= timeout_seconds <= 30:
50
+ raise ProviderProbeError("probe_request_invalid")
51
+ try:
52
+ resolved = canonical_executable(executable, workspace=workspace)
53
+ runtime_digest = executable_sha256(resolved)
54
+ except CliIdentityPrimitiveError:
55
+ raise ProviderProbeError("probe_executable_invalid") from None
56
+ deadline = clock() + float(timeout_seconds)
57
+
58
+ def call(*args: str) -> CliProcessResult:
59
+ remaining = deadline - clock()
60
+ if remaining <= 0:
61
+ raise ProviderProbeError("probe_timeout")
62
+ try:
63
+ return transport(argv=(str(resolved), *args), cwd=workspace, provider=LifecycleProviderId.CODEX, credential_home=credential_home, timeout_seconds=remaining)
64
+ except ProviderProbeError:
65
+ raise
66
+ except Exception:
67
+ raise ProviderProbeError("probe_failed") from None
68
+
69
+ try:
70
+ version = parse_semantic_version_output(_streams(call("--version"), "probe_version_invalid")[0], _VERSION)
71
+ except CliIdentityPrimitiveError:
72
+ raise ProviderProbeError("probe_version_invalid") from None
73
+ help_stdout, help_stderr = _streams(call("--help"), "probe_capability_missing")
74
+ global_help = help_stdout + "\n" + help_stderr
75
+ exec_stdout, exec_stderr = _streams(call("exec", "--help"), "probe_capability_missing")
76
+ exec_help = exec_stdout + "\n" + exec_stderr
77
+ if not all(pattern.search(global_help) for pattern in _GLOBAL_FLAG_PATTERNS) or not all(
78
+ pattern.search(exec_help) for pattern in _EXEC_FLAG_PATTERNS
79
+ ):
80
+ raise ProviderProbeError("probe_capability_missing")
81
+ stdout, stderr = _streams(call("login", "status"), "probe_auth_unhealthy")
82
+ lines = [line.strip() for stream in (stdout, stderr) for line in stream.splitlines() if line.strip()]
83
+ if lines.count("Logged in using ChatGPT") != 1 or any(line != "Logged in using ChatGPT" and not line.startswith(_WARNING_PREFIXES) for line in lines):
84
+ raise ProviderProbeError("probe_auth_unhealthy")
85
+ try:
86
+ if executable_sha256(resolved) != runtime_digest:
87
+ raise ProviderProbeError("probe_identity_changed")
88
+ except CliIdentityPrimitiveError:
89
+ raise ProviderProbeError("probe_identity_changed") from None
90
+ try:
91
+ return ProviderRuntimeIdentity(LifecycleProviderId.CODEX, RuntimeKind.LOCAL_CLI, version, runtime_digest, None, None, ("credential_health", "structured_output", "version"), policy_version, observed_at)
92
+ except ValueError:
93
+ raise ProviderProbeError("probe_request_invalid") from None