ithrl 0.1.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 (108) hide show
  1. ithrl/__init__.py +1 -0
  2. ithrl/__main__.py +6 -0
  3. ithrl/adapters/__init__.py +49 -0
  4. ithrl/adapters/_jsonl.py +266 -0
  5. ithrl/adapters/codex/__init__.py +5 -0
  6. ithrl/adapters/codex/context.py +204 -0
  7. ithrl/adapters/codex/decisions.py +224 -0
  8. ithrl/adapters/codex/discovery.py +60 -0
  9. ithrl/adapters/codex/parser.py +191 -0
  10. ithrl/adapters/contracts.py +106 -0
  11. ithrl/adapters/copilot/__init__.py +5 -0
  12. ithrl/adapters/copilot/context.py +182 -0
  13. ithrl/adapters/copilot/decisions.py +294 -0
  14. ithrl/adapters/copilot/discovery.py +28 -0
  15. ithrl/adapters/copilot/parser.py +283 -0
  16. ithrl/adapters/discovery.py +220 -0
  17. ithrl/adapters/errors.py +52 -0
  18. ithrl/adapters/registry.py +433 -0
  19. ithrl/behavior.py +138 -0
  20. ithrl/calibration.py +434 -0
  21. ithrl/case/__init__.py +33 -0
  22. ithrl/case/contracts.py +88 -0
  23. ithrl/case/evidence.py +169 -0
  24. ithrl/case/filesystem.py +340 -0
  25. ithrl/case/source.py +208 -0
  26. ithrl/case/source_context.py +119 -0
  27. ithrl/case/source_storage.py +234 -0
  28. ithrl/case/storage.py +377 -0
  29. ithrl/cli/__init__.py +968 -0
  30. ithrl/cli/extract.py +478 -0
  31. ithrl/cli/native.py +484 -0
  32. ithrl/cli/output.py +786 -0
  33. ithrl/cli/paths.py +24 -0
  34. ithrl/cli/sampled.py +485 -0
  35. ithrl/cli/sampled_output.py +472 -0
  36. ithrl/cli/sampled_reduction.py +1036 -0
  37. ithrl/cli/sampled_session.py +342 -0
  38. ithrl/cli/shrink.py +523 -0
  39. ithrl/context.py +465 -0
  40. ithrl/decisions.py +271 -0
  41. ithrl/examples/synthetic-trace.json +46 -0
  42. ithrl/execution/__init__.py +68 -0
  43. ithrl/execution/budget.py +146 -0
  44. ithrl/execution/contracts.py +426 -0
  45. ithrl/execution/preflight.py +346 -0
  46. ithrl/experiment.py +453 -0
  47. ithrl/extraction.py +206 -0
  48. ithrl/measurement.py +315 -0
  49. ithrl/observations.py +1148 -0
  50. ithrl/path_safety.py +100 -0
  51. ithrl/reduce/__init__.py +53 -0
  52. ithrl/reduce/contracts.py +508 -0
  53. ithrl/reduce/engine.py +901 -0
  54. ithrl/reduce/search.py +178 -0
  55. ithrl/results/__init__.py +49 -0
  56. ithrl/results/models.py +294 -0
  57. ithrl/results/storage.py +485 -0
  58. ithrl/results/summary.py +420 -0
  59. ithrl/results/validation.py +611 -0
  60. ithrl/run_state.py +1135 -0
  61. ithrl/runner/__init__.py +44 -0
  62. ithrl/runner/codex/__init__.py +17 -0
  63. ithrl/runner/codex/capability.py +708 -0
  64. ithrl/runner/codex/configuration.py +529 -0
  65. ithrl/runner/codex/isolation.py +396 -0
  66. ithrl/runner/codex/materialize.py +358 -0
  67. ithrl/runner/codex/policy.py +67 -0
  68. ithrl/runner/codex/protocol.py +649 -0
  69. ithrl/runner/codex/protocol_contracts.py +171 -0
  70. ithrl/runner/codex/runner.py +533 -0
  71. ithrl/runner/codex/runtime_configuration.py +264 -0
  72. ithrl/runner/codex/schema.py +527 -0
  73. ithrl/runner/codex/transcript.py +130 -0
  74. ithrl/runner/codex/worker.py +175 -0
  75. ithrl/runner/contracts.py +419 -0
  76. ithrl/runner/copilot/__init__.py +18 -0
  77. ithrl/runner/copilot/capability.py +825 -0
  78. ithrl/runner/copilot/configuration.py +201 -0
  79. ithrl/runner/copilot/isolation.py +374 -0
  80. ithrl/runner/copilot/materialize.py +484 -0
  81. ithrl/runner/copilot/policy.py +110 -0
  82. ithrl/runner/copilot/protocol/__init__.py +18 -0
  83. ithrl/runner/copilot/protocol/contracts.py +74 -0
  84. ithrl/runner/copilot/protocol/decoding.py +93 -0
  85. ithrl/runner/copilot/protocol/parser.py +418 -0
  86. ithrl/runner/copilot/protocol/state.py +130 -0
  87. ithrl/runner/copilot/runner.py +425 -0
  88. ithrl/runner/copilot/worker.py +351 -0
  89. ithrl/runner/evidence.py +345 -0
  90. ithrl/runner/fake.py +107 -0
  91. ithrl/runner/registry.py +90 -0
  92. ithrl/runner/runtime/__init__.py +50 -0
  93. ithrl/runner/runtime/contracts.py +157 -0
  94. ithrl/runner/runtime/output.py +184 -0
  95. ithrl/runner/runtime/process.py +954 -0
  96. ithrl/runner/runtime/protection.py +562 -0
  97. ithrl/runner/runtime/state.py +282 -0
  98. ithrl/runner/validation.py +376 -0
  99. ithrl/sampling.py +412 -0
  100. ithrl/statistics.py +276 -0
  101. ithrl/synthetic.py +511 -0
  102. ithrl/timestamps.py +26 -0
  103. ithrl/workflow.py +570 -0
  104. ithrl-0.1.0.dist-info/METADATA +118 -0
  105. ithrl-0.1.0.dist-info/RECORD +108 -0
  106. ithrl-0.1.0.dist-info/WHEEL +4 -0
  107. ithrl-0.1.0.dist-info/entry_points.txt +2 -0
  108. ithrl-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,171 @@
1
+ """Bounds, accepted messages, and outcomes for the Codex app-server exchange."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from dataclasses import dataclass, field
7
+ from typing import Literal
8
+
9
+ MAX_PROTOCOL_LINE_BYTES = 8 * 1024 * 1024
10
+ MAX_PROTOCOL_MESSAGES = 100_000
11
+ MAX_CLIENT_PROTOCOL_MESSAGES = 6
12
+ MAX_PROTOCOL_DEPTH = 128
13
+ MAX_PROTOCOL_ITEMS = 1_000_000
14
+ MAX_WORKER_REQUEST_BYTES = 64 * 1024 * 1024
15
+ SANDBOX_ROOT = "/tmp/ithrl-state" # noqa: S108 - private Bubblewrap tmpfs
16
+ SANDBOX_WORKSPACE = f"{SANDBOX_ROOT}/workspace"
17
+
18
+ type ProtocolStatus = Literal["completed", "error"]
19
+ type ProviderAcceptance = Literal["not-started", "accepted", "unknown"]
20
+
21
+ ACTION_RESPONSES = {
22
+ "item/commandExecution/requestApproval": {"decision": "decline"},
23
+ "item/fileChange/requestApproval": {"decision": "decline"},
24
+ "item/tool/call": {
25
+ "contentItems": [{"type": "inputText", "text": "Tool execution is disabled."}],
26
+ "success": False,
27
+ },
28
+ "mcpServer/elicitation/request": {
29
+ "action": "decline",
30
+ "content": None,
31
+ "_meta": None,
32
+ },
33
+ }
34
+ ACTION_ITEM_TYPES = frozenset(
35
+ {
36
+ "collabAgentToolCall",
37
+ "commandExecution",
38
+ "dynamicToolCall",
39
+ "fileChange",
40
+ "hookPrompt",
41
+ "imageGeneration",
42
+ "imageView",
43
+ "mcpToolCall",
44
+ "sleep",
45
+ "subAgentActivity",
46
+ "webSearch",
47
+ }
48
+ )
49
+ PASSIVE_ITEM_TYPES = frozenset(
50
+ {
51
+ "agentMessage",
52
+ "contextCompaction",
53
+ "enteredReviewMode",
54
+ "exitedReviewMode",
55
+ "functionCallOutput",
56
+ "plan",
57
+ "reasoning",
58
+ "userMessage",
59
+ }
60
+ )
61
+ PASSIVE_NOTIFICATIONS = {
62
+ "item/agentMessage/delta": ("delta",),
63
+ "item/plan/delta": ("delta",),
64
+ "item/reasoning/summaryPartAdded": ("summaryIndex",),
65
+ "item/reasoning/summaryTextDelta": ("delta", "summaryIndex"),
66
+ "item/reasoning/textDelta": ("contentIndex", "delta"),
67
+ }
68
+ THREAD_PASSIVE_NOTIFICATIONS = {
69
+ "model/verification": "verifications",
70
+ "thread/status/changed": "status",
71
+ }
72
+ ACTION_REQUEST_METHODS = frozenset(ACTION_RESPONSES)
73
+ SUPPORTED_NOTIFICATION_METHODS = frozenset(
74
+ {
75
+ *PASSIVE_NOTIFICATIONS,
76
+ *THREAD_PASSIVE_NOTIFICATIONS,
77
+ "account/rateLimits/updated",
78
+ "account/updated",
79
+ "error",
80
+ "item/completed",
81
+ "item/started",
82
+ "model/rerouted",
83
+ "remoteControl/status/changed",
84
+ "thread/started",
85
+ "thread/tokenUsage/updated",
86
+ "turn/completed",
87
+ "turn/started",
88
+ "warning",
89
+ }
90
+ )
91
+
92
+
93
+ class CodexProtocolError(RuntimeError):
94
+ """A content-safe app-server protocol failure."""
95
+
96
+ def __init__(self, code: str, message: str) -> None:
97
+ super().__init__(message)
98
+ self.code = code
99
+
100
+
101
+ @dataclass(frozen=True, slots=True)
102
+ class ProtocolUsage:
103
+ """Token counts reported for the reconstructed turn."""
104
+
105
+ input_tokens: int
106
+ output_tokens: int
107
+ cached_input_tokens: int
108
+
109
+
110
+ @dataclass(frozen=True, slots=True)
111
+ class ProtocolOutcome:
112
+ """Content-safe control facts plus protected final response content."""
113
+
114
+ status: ProtocolStatus
115
+ error_code: str | None
116
+ final_text: str | None = field(repr=False)
117
+ model_returned: str | None
118
+ service_tier: str | None
119
+ usage: ProtocolUsage | None
120
+ provider_acceptance: ProviderAcceptance
121
+ provider_calls_started: int
122
+ provider_calls_completed: int
123
+ actions_requested: int
124
+ actions_denied: int
125
+ behavior_output_observed: bool = True
126
+
127
+ def __post_init__(self) -> None:
128
+ if type(self.behavior_output_observed) is not bool:
129
+ raise ValueError("Codex output visibility must be a boolean")
130
+ if self.status == "completed" and (self.error_code is not None or self.final_text is None):
131
+ raise ValueError("completed Codex outcome requires one final response")
132
+ if self.status == "error" and (self.error_code is None or self.final_text is not None):
133
+ raise ValueError("failed Codex outcome requires an error code and no final response")
134
+ counts = (
135
+ self.provider_calls_started,
136
+ self.provider_calls_completed,
137
+ self.actions_requested,
138
+ self.actions_denied,
139
+ )
140
+ if min(counts) < 0:
141
+ raise ValueError("Codex protocol counts cannot be negative")
142
+ if self.provider_calls_completed > self.provider_calls_started:
143
+ raise ValueError("completed provider calls cannot exceed started calls")
144
+ if self.actions_denied > self.actions_requested:
145
+ raise ValueError("denied actions cannot exceed requested actions")
146
+
147
+
148
+ def validate_protocol_json(value: object) -> None:
149
+ """Reject values that exceed the protocol's JSON shape and resource bounds."""
150
+
151
+ pending = [(value, 0)]
152
+ items = 0
153
+ while pending:
154
+ current, depth = pending.pop()
155
+ if depth > MAX_PROTOCOL_DEPTH:
156
+ raise CodexProtocolError("json-depth", "Codex protocol JSON is too deeply nested")
157
+ if isinstance(current, dict):
158
+ if any(not isinstance(key, str) for key in current):
159
+ raise CodexProtocolError("invalid-json", "Codex protocol JSON has invalid keys")
160
+ items += len(current)
161
+ pending.extend((item, depth + 1) for item in current.values())
162
+ elif isinstance(current, list):
163
+ items += len(current)
164
+ pending.extend((item, depth + 1) for item in current)
165
+ elif isinstance(current, float):
166
+ if not math.isfinite(current):
167
+ raise CodexProtocolError("invalid-json", "Codex protocol JSON has invalid values")
168
+ elif current is not None and not isinstance(current, (bool, int, str)):
169
+ raise CodexProtocolError("invalid-json", "Codex protocol JSON has invalid values")
170
+ if items > MAX_PROTOCOL_ITEMS:
171
+ raise CodexProtocolError("json-items", "Codex protocol JSON has too many items")
@@ -0,0 +1,533 @@
1
+ """Codex final-response runner composed with the shared isolated runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from importlib.metadata import version as distribution_version
7
+ from pathlib import Path
8
+ from typing import cast
9
+
10
+ from ithrl.behavior import MAX_BEHAVIOR_TEXT_LENGTH
11
+ from ithrl.context import JsonObject, JsonValue, content_digest
12
+ from ithrl.execution import CapabilityProfile
13
+ from ithrl.runner.codex.configuration import CodexProviderConfiguration
14
+ from ithrl.runner.codex.isolation import (
15
+ CodexIsolationError,
16
+ CodexRuntime,
17
+ prepare_codex_process,
18
+ program_digest,
19
+ )
20
+ from ithrl.runner.codex.materialize import (
21
+ CodexMaterialization,
22
+ CodexMaterializationError,
23
+ CodexMaterializationErrorCode,
24
+ materialize_codex_attempt,
25
+ )
26
+ from ithrl.runner.codex.policy import (
27
+ CAPTURE_TYPE,
28
+ CONTEXT_OPERATION,
29
+ MATERIALIZATION,
30
+ POLICY_VERSION,
31
+ RECONSTRUCTION_METHOD,
32
+ RUNNER_VERSION,
33
+ SOURCE_SCHEMA,
34
+ TARGET_TYPE,
35
+ configuration_deviation,
36
+ runtime_deviation,
37
+ )
38
+ from ithrl.runner.codex.protocol import CodexProtocolError, ProtocolOutcome
39
+ from ithrl.runner.codex.runtime_configuration import validate_runner_configuration
40
+ from ithrl.runner.codex.transcript import parse_transcript
41
+ from ithrl.runner.contracts import (
42
+ MAX_MODEL_NAME_LENGTH,
43
+ MAX_SERVICE_TIER_LENGTH,
44
+ CandidateInvalidError,
45
+ NativeOutputReference,
46
+ PreparedAttempt,
47
+ ProviderAcceptanceState,
48
+ RunAccounting,
49
+ RunnerCancelled,
50
+ RunnerError,
51
+ RunnerErrorCode,
52
+ RunResult,
53
+ TokenUsage,
54
+ )
55
+ from ithrl.runner.runtime import (
56
+ PROCESS_CANCELLATION_GRACE_SECONDS,
57
+ NativeOutputStoreError,
58
+ ProcessRuntimeCancelled,
59
+ ProcessRuntimeError,
60
+ ProtectedOutputStore,
61
+ ProtectedPath,
62
+ RuntimeReport,
63
+ run_isolated_process,
64
+ )
65
+ from ithrl.runner.validation import validate_run_result
66
+
67
+ _MAX_PROTECTED_ENTRIES = 250_000
68
+ _MAX_PROTECTED_BYTES = 256 * 1024 * 1024
69
+
70
+
71
+ class CodexRunner:
72
+ """Execute one reconstructed Codex turn with every action surface disabled."""
73
+
74
+ def __init__(
75
+ self,
76
+ *,
77
+ capability_profile: CapabilityProfile,
78
+ runtime: CodexRuntime,
79
+ configuration: CodexProviderConfiguration,
80
+ state_parent: Path,
81
+ output_root: Path,
82
+ source_paths: Iterable[ProtectedPath],
83
+ workspace_paths: Iterable[ProtectedPath],
84
+ ) -> None:
85
+ _validate_capability(capability_profile)
86
+ _validate_configuration(capability_profile, configuration, runtime)
87
+ sources = tuple(source_paths)
88
+ workspaces = tuple(workspace_paths)
89
+ if not sources or not workspaces:
90
+ raise ValueError("Codex runner requires protected source and workspace paths")
91
+ state_root = state_parent.resolve(strict=False)
92
+ absolute_output_root = output_root.absolute()
93
+ resolved_output_root = absolute_output_root.resolve(strict=False)
94
+ protected_roots = tuple(
95
+ entry.path.resolve(strict=False) for entry in (*sources, *workspaces)
96
+ )
97
+ if _paths_overlap(state_root, resolved_output_root) or any(
98
+ _paths_overlap(owned, protected)
99
+ for owned in (state_root, resolved_output_root)
100
+ for protected in protected_roots
101
+ ):
102
+ raise ValueError("Codex runner storage overlaps protected state")
103
+ self._capability = capability_profile
104
+ self._runtime = runtime
105
+ self._configuration = configuration
106
+ self._client_version = distribution_version("ithrl")
107
+ self._worker_source = Path(__file__).with_name("worker.py")
108
+ self._worker_digest = program_digest(self._worker_source)
109
+ support_sources = tuple(
110
+ Path(__file__).with_name(name) for name in ("protocol.py", "protocol_contracts.py")
111
+ )
112
+ self._support_programs = tuple(
113
+ (source, program_digest(source)) for source in support_sources
114
+ )
115
+ self._state_parent = state_root
116
+ self._output_store = ProtectedOutputStore.open(absolute_output_root)
117
+ self._source_paths = sources
118
+ self._workspace_paths = workspaces
119
+
120
+ @property
121
+ def capability_profile(self) -> CapabilityProfile:
122
+ return self._capability
123
+
124
+ @property
125
+ def fingerprint(self) -> str:
126
+ identity = {
127
+ "runner": "codex",
128
+ "runner_version": RUNNER_VERSION,
129
+ "client_version": self._client_version,
130
+ "capability": self._capability.digest,
131
+ "materialization": self._capability.reconstruction_method,
132
+ "policy": POLICY_VERSION,
133
+ "provider_configuration": self._configuration.public_digest,
134
+ "runtime": self._runtime.identity,
135
+ "worker": self._worker_digest,
136
+ "support_programs": {source.name: digest for source, digest in self._support_programs},
137
+ }
138
+ return f"codex-v{RUNNER_VERSION}:{content_digest(identity)}"
139
+
140
+ def validate_candidate(self, attempt: PreparedAttempt) -> None:
141
+ """Check source-backed reconstruction without runtime or provider operations."""
142
+
143
+ try:
144
+ materialized = materialize_codex_attempt(attempt)
145
+ _validate_materialized_identity(materialized, attempt)
146
+ except CodexMaterializationError as error:
147
+ if attempt.candidate.removed_context_ids and error.code in {
148
+ CodexMaterializationErrorCode.MISSING_TRIGGER,
149
+ CodexMaterializationErrorCode.UNSUPPORTED_CONTEXT,
150
+ }:
151
+ raise CandidateInvalidError("candidate cannot be reconstructed") from error
152
+ raise
153
+
154
+ async def run(self, attempt: PreparedAttempt) -> RunResult:
155
+ """Execute one prepared source-backed final-response attempt."""
156
+
157
+ self._validate_attempt(attempt)
158
+ try:
159
+ materialized = materialize_codex_attempt(attempt)
160
+ _validate_materialized_identity(materialized, attempt)
161
+ except CodexMaterializationError as error:
162
+ raise RunnerError(
163
+ RunnerErrorCode.PREPARATION,
164
+ str(error),
165
+ provider_acceptance_state=ProviderAcceptanceState.NOT_STARTED,
166
+ model_requested=attempt.execution_plan.model_requested,
167
+ provider=self._capability.provider,
168
+ endpoint=self._capability.endpoint,
169
+ effective_policy_digest=attempt.safety_policy.digest,
170
+ ) from error
171
+
172
+ worker_request = _worker_request(
173
+ materialized, self._configuration.provider_id, self._client_version
174
+ )
175
+ try:
176
+ execution = await run_isolated_process(
177
+ attempt_id=attempt.attempt_id,
178
+ state_parent=self._state_parent,
179
+ output_store=self._output_store,
180
+ prepare=lambda state: prepare_codex_process(
181
+ state,
182
+ runtime=self._runtime,
183
+ configuration=self._configuration,
184
+ request=worker_request,
185
+ worker_source=self._worker_source,
186
+ worker_digest=self._worker_digest,
187
+ support_programs=self._support_programs,
188
+ ),
189
+ resource_limits=attempt.resource_limits,
190
+ source_paths=self._source_paths,
191
+ workspace_paths=self._workspace_paths,
192
+ max_protected_entries=_MAX_PROTECTED_ENTRIES,
193
+ max_protected_bytes=_MAX_PROTECTED_BYTES,
194
+ )
195
+ except ProcessRuntimeCancelled as cancelled:
196
+ error = _runtime_error(
197
+ attempt,
198
+ RunnerErrorCode.CANCELLED,
199
+ "Codex attempt was cancelled",
200
+ cancelled.report,
201
+ cancelled.native_output,
202
+ )
203
+ raise RunnerCancelled(error) from None
204
+ except ProcessRuntimeError as error:
205
+ raise _runtime_error(
206
+ attempt,
207
+ error.code,
208
+ str(error),
209
+ error.report,
210
+ error.native_output,
211
+ ) from error
212
+ except CodexIsolationError as error:
213
+ raise RunnerError(
214
+ RunnerErrorCode.PREPARATION,
215
+ str(error),
216
+ provider_acceptance_state=ProviderAcceptanceState.NOT_STARTED,
217
+ model_requested=attempt.execution_plan.model_requested,
218
+ provider=self._capability.provider,
219
+ endpoint=self._capability.endpoint,
220
+ effective_policy_digest=attempt.safety_policy.digest,
221
+ ) from error
222
+
223
+ if execution.native_output is None:
224
+ raise _runtime_error(
225
+ attempt,
226
+ RunnerErrorCode.CONTRACT,
227
+ "Codex runner produced no protected native output",
228
+ execution.report,
229
+ None,
230
+ )
231
+ try:
232
+ protected = self._output_store.read(execution.native_output)
233
+ outcome = parse_transcript(protected.stdout)
234
+ except (CodexProtocolError, NativeOutputStoreError) as error:
235
+ raise _runtime_error(
236
+ attempt,
237
+ RunnerErrorCode.PROTOCOL,
238
+ "Codex runner output could not be validated",
239
+ execution.report,
240
+ execution.native_output,
241
+ ) from error
242
+ _require_normalized_outcome(attempt, outcome, execution.report, execution.native_output)
243
+ if execution.returncode != 0 or outcome.status != "completed":
244
+ raise _outcome_error(attempt, outcome, execution.report, execution.native_output)
245
+
246
+ result = _run_result(attempt, outcome, execution.report, execution.native_output)
247
+ validate_run_result(attempt, result)
248
+ return result
249
+
250
+ def _validate_attempt(self, attempt: PreparedAttempt) -> None:
251
+ try:
252
+ self._runtime.assert_unchanged()
253
+ programs_unchanged = program_digest(self._worker_source) == self._worker_digest and all(
254
+ program_digest(source) == digest for source, digest in self._support_programs
255
+ )
256
+ except CodexIsolationError as error:
257
+ raise RunnerError(
258
+ RunnerErrorCode.PREPARATION,
259
+ "Codex runner inputs changed after capability detection",
260
+ ) from error
261
+ if not programs_unchanged:
262
+ raise RunnerError(
263
+ RunnerErrorCode.PREPARATION,
264
+ "Codex runner inputs changed after capability detection",
265
+ )
266
+ plan = attempt.execution_plan
267
+ if plan.capability != self._capability or plan.runner_fingerprint != self.fingerprint:
268
+ raise RunnerError(
269
+ RunnerErrorCode.CONTRACT,
270
+ "Codex runner does not match the resolved execution plan",
271
+ )
272
+ if attempt.safety_policy.provider_transport != "allowed":
273
+ raise RunnerError(
274
+ RunnerErrorCode.SAFETY,
275
+ "Codex provider transport is not allowed by the resolved policy",
276
+ )
277
+
278
+
279
+ def _paths_overlap(left: Path, right: Path) -> bool:
280
+ return left.is_relative_to(right) or right.is_relative_to(left)
281
+
282
+
283
+ def _validate_capability(capability: CapabilityProfile) -> None:
284
+ expected = {
285
+ "runner_type": "codex",
286
+ "runner_version": RUNNER_VERSION,
287
+ "harness": "codex",
288
+ "source_schema": SOURCE_SCHEMA,
289
+ "deterministic": False,
290
+ "supported_capture_types": (CAPTURE_TYPE,),
291
+ "supported_materializations": (MATERIALIZATION,),
292
+ "supported_context_operations": (CONTEXT_OPERATION,),
293
+ "reconstruction_method": RECONSTRUCTION_METHOD,
294
+ "supported_targets": (TARGET_TYPE,),
295
+ "provider_transport": "required",
296
+ "provider_calls_per_attempt": 1,
297
+ "tool_network_access": "forbidden",
298
+ "action_visibility": "not-applicable",
299
+ "action_denial": "enforced",
300
+ "action_execution": "denied",
301
+ "source_store_effect": "unchanged",
302
+ "original_workspace_effect": "unchanged",
303
+ "protected_state_enforcement": "preventive",
304
+ "disposable_state_effect": "writes",
305
+ "disposable_state_limit_enforcement": "enforced",
306
+ "process_containment": "process-group",
307
+ "supports_timeout": True,
308
+ "supports_cancellation": True,
309
+ "cancellation_grace_seconds": PROCESS_CANCELLATION_GRACE_SECONDS,
310
+ "reports_usage": True,
311
+ "input_token_limit_enforcement": "unavailable",
312
+ }
313
+ document = capability.model_dump(mode="python")
314
+ if any(document[name] != value for name, value in expected.items()):
315
+ raise ValueError("Codex capability does not satisfy the final-response runner contract")
316
+ if capability.provider is None or capability.endpoint is None:
317
+ raise ValueError("Codex capability requires a verified provider destination")
318
+
319
+
320
+ def _validate_configuration(
321
+ capability: CapabilityProfile,
322
+ configuration: CodexProviderConfiguration,
323
+ runtime: CodexRuntime,
324
+ ) -> None:
325
+ validate_runner_configuration(
326
+ configuration.rendered_config,
327
+ endpoint=configuration.endpoint,
328
+ environment_names=configuration.environment_names,
329
+ )
330
+ if (
331
+ capability.provider != configuration.source_provider_id
332
+ or capability.endpoint != configuration.endpoint
333
+ or configuration_deviation(configuration.public_digest) not in capability.deviations
334
+ or runtime_deviation(runtime.identity) not in capability.deviations
335
+ ):
336
+ raise ValueError("Codex provider configuration does not match its capability profile")
337
+
338
+
339
+ def _validate_materialized_identity(
340
+ materialized: CodexMaterialization,
341
+ attempt: PreparedAttempt,
342
+ ) -> None:
343
+ capability = attempt.execution_plan.capability
344
+ if materialized.model != attempt.execution_plan.model_requested:
345
+ raise CodexMaterializationError(
346
+ code=CodexMaterializationErrorCode.INCOMPATIBLE_CASE,
347
+ message="the selected Codex model differs from the execution plan",
348
+ )
349
+ if materialized.provider != capability.provider:
350
+ raise CodexMaterializationError(
351
+ code=CodexMaterializationErrorCode.INCOMPATIBLE_CASE,
352
+ message="the selected Codex provider differs from the execution plan",
353
+ )
354
+
355
+
356
+ def _worker_request(
357
+ materialized: CodexMaterialization, runtime_provider_id: str, client_version: str
358
+ ) -> JsonObject:
359
+ return {
360
+ "schema_version": 1,
361
+ "client_version": client_version,
362
+ "model": materialized.model,
363
+ "provider": runtime_provider_id,
364
+ "effort": materialized.effort,
365
+ "personality": materialized.personality,
366
+ "baseInstructions": materialized.base_instructions,
367
+ "developerInstructions": materialized.developer_instructions,
368
+ "history": cast(JsonValue, list(materialized.history)),
369
+ "input": cast(JsonValue, list(materialized.turn_input.items)),
370
+ "toolOutput": materialized.turn_input.tool_output,
371
+ }
372
+
373
+
374
+ def _run_result(
375
+ attempt: PreparedAttempt,
376
+ outcome: ProtocolOutcome,
377
+ report: RuntimeReport,
378
+ native_output: NativeOutputReference,
379
+ ) -> RunResult:
380
+ if outcome.usage is None or outcome.final_text is None:
381
+ raise RunnerError(RunnerErrorCode.CONTRACT, "Codex completed without required evidence")
382
+ return RunResult(
383
+ attempt_id=attempt.attempt_id,
384
+ effective_policy_digest=attempt.safety_policy.digest,
385
+ final_text=outcome.final_text,
386
+ model_requested=attempt.execution_plan.model_requested,
387
+ model_returned=outcome.model_returned,
388
+ provider=attempt.execution_plan.capability.provider,
389
+ endpoint=attempt.execution_plan.capability.endpoint,
390
+ service_tier=outcome.service_tier,
391
+ usage=_token_usage(outcome),
392
+ accounting=_accounting(report, outcome),
393
+ native_output=native_output,
394
+ )
395
+
396
+
397
+ def _require_normalized_outcome(
398
+ attempt: PreparedAttempt,
399
+ outcome: ProtocolOutcome,
400
+ report: RuntimeReport,
401
+ native_output: NativeOutputReference,
402
+ ) -> None:
403
+ error: tuple[RunnerErrorCode, str] | None = None
404
+ if outcome.final_text is not None and len(outcome.final_text) > MAX_BEHAVIOR_TEXT_LENGTH:
405
+ error = (
406
+ RunnerErrorCode.OUTPUT_LIMIT,
407
+ "Codex final response exceeds the normalized text limit",
408
+ )
409
+ elif (
410
+ outcome.model_returned is not None
411
+ and not 0 < len(outcome.model_returned) <= MAX_MODEL_NAME_LENGTH
412
+ ) or (
413
+ outcome.service_tier is not None
414
+ and not 0 < len(outcome.service_tier) <= MAX_SERVICE_TIER_LENGTH
415
+ ):
416
+ error = (
417
+ RunnerErrorCode.PROTOCOL,
418
+ "Codex returned invalid model identity metadata",
419
+ )
420
+ if error is None:
421
+ return
422
+ raise RunnerError(
423
+ *error,
424
+ provider_acceptance_state=ProviderAcceptanceState(outcome.provider_acceptance),
425
+ model_requested=attempt.execution_plan.model_requested,
426
+ provider=attempt.execution_plan.capability.provider,
427
+ endpoint=attempt.execution_plan.capability.endpoint,
428
+ usage=_token_usage(outcome),
429
+ accounting=_accounting(report, outcome),
430
+ native_output=native_output,
431
+ effective_policy_digest=attempt.safety_policy.digest,
432
+ )
433
+
434
+
435
+ def _token_usage(outcome: ProtocolOutcome) -> TokenUsage | None:
436
+ if outcome.usage is None:
437
+ return None
438
+ return TokenUsage(
439
+ input_tokens=outcome.usage.input_tokens,
440
+ output_tokens=outcome.usage.output_tokens,
441
+ cached_input_tokens=outcome.usage.cached_input_tokens,
442
+ )
443
+
444
+
445
+ def _outcome_error(
446
+ attempt: PreparedAttempt,
447
+ outcome: ProtocolOutcome,
448
+ report: RuntimeReport,
449
+ native_output: NativeOutputReference,
450
+ ) -> RunnerError:
451
+ if outcome.error_code == "provider-overloaded":
452
+ code = RunnerErrorCode.PROVIDER_OVERLOADED
453
+ elif outcome.error_code in {
454
+ "action-requested",
455
+ "remote-control-active",
456
+ "unintercepted-action",
457
+ "unsupported-server-request",
458
+ }:
459
+ code = RunnerErrorCode.SAFETY
460
+ else:
461
+ code = RunnerErrorCode.PROTOCOL
462
+ acceptance = ProviderAcceptanceState(outcome.provider_acceptance)
463
+ return RunnerError(
464
+ code,
465
+ "Codex final-response execution did not produce a valid completion",
466
+ provider_acceptance_state=acceptance,
467
+ model_requested=attempt.execution_plan.model_requested,
468
+ model_returned=outcome.model_returned,
469
+ provider=attempt.execution_plan.capability.provider,
470
+ endpoint=attempt.execution_plan.capability.endpoint,
471
+ service_tier=outcome.service_tier,
472
+ usage=_token_usage(outcome),
473
+ accounting=_accounting(report, outcome),
474
+ native_output=native_output,
475
+ effective_policy_digest=attempt.safety_policy.digest,
476
+ behavior_output_observed=outcome.behavior_output_observed,
477
+ )
478
+
479
+
480
+ def _runtime_error(
481
+ attempt: PreparedAttempt,
482
+ code: RunnerErrorCode,
483
+ message: str,
484
+ report: RuntimeReport,
485
+ native_output: NativeOutputReference | None,
486
+ ) -> RunnerError:
487
+ acceptance = (
488
+ ProviderAcceptanceState.NOT_STARTED
489
+ if report.process_outcome == "not-started"
490
+ else ProviderAcceptanceState.UNKNOWN
491
+ )
492
+ return RunnerError(
493
+ code,
494
+ message,
495
+ provider_acceptance_state=acceptance,
496
+ model_requested=attempt.execution_plan.model_requested,
497
+ provider=attempt.execution_plan.capability.provider,
498
+ endpoint=attempt.execution_plan.capability.endpoint,
499
+ accounting=_accounting(report, provider_call_may_have_started=True),
500
+ native_output=native_output,
501
+ effective_policy_digest=attempt.safety_policy.digest,
502
+ )
503
+
504
+
505
+ def _accounting(
506
+ report: RuntimeReport,
507
+ outcome: ProtocolOutcome | None = None,
508
+ *,
509
+ provider_call_may_have_started: bool = False,
510
+ ) -> RunAccounting:
511
+ provider_calls_started = (
512
+ outcome.provider_calls_started
513
+ if outcome is not None
514
+ else int(provider_call_may_have_started and report.process_outcome != "not-started")
515
+ )
516
+ return RunAccounting(
517
+ provider_calls_started=provider_calls_started,
518
+ provider_calls_completed=outcome.provider_calls_completed if outcome is not None else 0,
519
+ provider_connections_started=provider_calls_started,
520
+ actions_requested=outcome.actions_requested if outcome is not None else 0,
521
+ actions_denied=outcome.actions_denied if outcome is not None else 0,
522
+ elapsed_milliseconds=report.elapsed_milliseconds,
523
+ native_output_bytes=report.native_output_bytes,
524
+ disposable_state_bytes=report.disposable_state_bytes,
525
+ residual_disposable_state_bytes=report.residual_disposable_state_bytes,
526
+ disposable_state_entries=report.disposable_state_entries,
527
+ residual_disposable_state_entries=report.residual_disposable_state_entries,
528
+ process_outcome=report.process_outcome,
529
+ forced_termination=report.forced_termination,
530
+ cleanup_outcome=report.cleanup_outcome,
531
+ source_store_effect=report.source_store_effect,
532
+ original_workspace_effect=report.original_workspace_effect,
533
+ )