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.
- ithrl/__init__.py +1 -0
- ithrl/__main__.py +6 -0
- ithrl/adapters/__init__.py +49 -0
- ithrl/adapters/_jsonl.py +266 -0
- ithrl/adapters/codex/__init__.py +5 -0
- ithrl/adapters/codex/context.py +204 -0
- ithrl/adapters/codex/decisions.py +224 -0
- ithrl/adapters/codex/discovery.py +60 -0
- ithrl/adapters/codex/parser.py +191 -0
- ithrl/adapters/contracts.py +106 -0
- ithrl/adapters/copilot/__init__.py +5 -0
- ithrl/adapters/copilot/context.py +182 -0
- ithrl/adapters/copilot/decisions.py +294 -0
- ithrl/adapters/copilot/discovery.py +28 -0
- ithrl/adapters/copilot/parser.py +283 -0
- ithrl/adapters/discovery.py +220 -0
- ithrl/adapters/errors.py +52 -0
- ithrl/adapters/registry.py +433 -0
- ithrl/behavior.py +138 -0
- ithrl/calibration.py +434 -0
- ithrl/case/__init__.py +33 -0
- ithrl/case/contracts.py +88 -0
- ithrl/case/evidence.py +169 -0
- ithrl/case/filesystem.py +340 -0
- ithrl/case/source.py +208 -0
- ithrl/case/source_context.py +119 -0
- ithrl/case/source_storage.py +234 -0
- ithrl/case/storage.py +377 -0
- ithrl/cli/__init__.py +968 -0
- ithrl/cli/extract.py +478 -0
- ithrl/cli/native.py +484 -0
- ithrl/cli/output.py +786 -0
- ithrl/cli/paths.py +24 -0
- ithrl/cli/sampled.py +485 -0
- ithrl/cli/sampled_output.py +472 -0
- ithrl/cli/sampled_reduction.py +1036 -0
- ithrl/cli/sampled_session.py +342 -0
- ithrl/cli/shrink.py +523 -0
- ithrl/context.py +465 -0
- ithrl/decisions.py +271 -0
- ithrl/examples/synthetic-trace.json +46 -0
- ithrl/execution/__init__.py +68 -0
- ithrl/execution/budget.py +146 -0
- ithrl/execution/contracts.py +426 -0
- ithrl/execution/preflight.py +346 -0
- ithrl/experiment.py +453 -0
- ithrl/extraction.py +206 -0
- ithrl/measurement.py +315 -0
- ithrl/observations.py +1148 -0
- ithrl/path_safety.py +100 -0
- ithrl/reduce/__init__.py +53 -0
- ithrl/reduce/contracts.py +508 -0
- ithrl/reduce/engine.py +901 -0
- ithrl/reduce/search.py +178 -0
- ithrl/results/__init__.py +49 -0
- ithrl/results/models.py +294 -0
- ithrl/results/storage.py +485 -0
- ithrl/results/summary.py +420 -0
- ithrl/results/validation.py +611 -0
- ithrl/run_state.py +1135 -0
- ithrl/runner/__init__.py +44 -0
- ithrl/runner/codex/__init__.py +17 -0
- ithrl/runner/codex/capability.py +708 -0
- ithrl/runner/codex/configuration.py +529 -0
- ithrl/runner/codex/isolation.py +396 -0
- ithrl/runner/codex/materialize.py +358 -0
- ithrl/runner/codex/policy.py +67 -0
- ithrl/runner/codex/protocol.py +649 -0
- ithrl/runner/codex/protocol_contracts.py +171 -0
- ithrl/runner/codex/runner.py +533 -0
- ithrl/runner/codex/runtime_configuration.py +264 -0
- ithrl/runner/codex/schema.py +527 -0
- ithrl/runner/codex/transcript.py +130 -0
- ithrl/runner/codex/worker.py +175 -0
- ithrl/runner/contracts.py +419 -0
- ithrl/runner/copilot/__init__.py +18 -0
- ithrl/runner/copilot/capability.py +825 -0
- ithrl/runner/copilot/configuration.py +201 -0
- ithrl/runner/copilot/isolation.py +374 -0
- ithrl/runner/copilot/materialize.py +484 -0
- ithrl/runner/copilot/policy.py +110 -0
- ithrl/runner/copilot/protocol/__init__.py +18 -0
- ithrl/runner/copilot/protocol/contracts.py +74 -0
- ithrl/runner/copilot/protocol/decoding.py +93 -0
- ithrl/runner/copilot/protocol/parser.py +418 -0
- ithrl/runner/copilot/protocol/state.py +130 -0
- ithrl/runner/copilot/runner.py +425 -0
- ithrl/runner/copilot/worker.py +351 -0
- ithrl/runner/evidence.py +345 -0
- ithrl/runner/fake.py +107 -0
- ithrl/runner/registry.py +90 -0
- ithrl/runner/runtime/__init__.py +50 -0
- ithrl/runner/runtime/contracts.py +157 -0
- ithrl/runner/runtime/output.py +184 -0
- ithrl/runner/runtime/process.py +954 -0
- ithrl/runner/runtime/protection.py +562 -0
- ithrl/runner/runtime/state.py +282 -0
- ithrl/runner/validation.py +376 -0
- ithrl/sampling.py +412 -0
- ithrl/statistics.py +276 -0
- ithrl/synthetic.py +511 -0
- ithrl/timestamps.py +26 -0
- ithrl/workflow.py +570 -0
- ithrl-0.1.0.dist-info/METADATA +118 -0
- ithrl-0.1.0.dist-info/RECORD +108 -0
- ithrl-0.1.0.dist-info/WHEEL +4 -0
- ithrl-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|