cdx-manager 0.25.3 → 0.25.4
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.
- package/README.md +10 -3
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_25_4.md +14 -0
- package/changelogs/CHANGELOGS_UNRELEASED.md +7 -0
- package/checksums/release-archives.json +11 -0
- package/docs/cli.md +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/commands/runs.py +1 -1
- package/src/remote_run.py +60 -18
- package/src/remote_worker.py +52 -37
- package/src/run_observation.py +13 -4
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<br clear="left"/>
|
|
6
6
|
|
|
7
|
-
[](LICENSE) ](LICENSE)  
|
|
8
8
|
|
|
9
9
|
**The right AI coding account, when you need it.** CDX Manager keeps your Codex and Claude accounts in separate profiles, shows which ones have quota, and helps you pick the next usable session from your terminal.
|
|
10
10
|
|
|
@@ -178,8 +178,15 @@ result. A probe overlapping the short snapshot can still report contention.
|
|
|
178
178
|
Controller renewal does not extend the worker's copied access token; quota remains
|
|
179
179
|
one controller-side account snapshot, never a worker copy. Multiple independent controllers sharing copied OAuth profiles are not
|
|
180
180
|
supported. Costs and budget checks use measured worker usage and observed rollout
|
|
181
|
-
models
|
|
182
|
-
|
|
181
|
+
models, using the same exact-thread reader and 128 MiB rollout limit as local
|
|
182
|
+
isolated runs. Content-free evidence travels in supervisor frames separated from
|
|
183
|
+
provider stdout; raw rollouts and credentials are never returned. Live rollout
|
|
184
|
+
counters can stop an over-budget run before `turn.completed`. Unknown prices and
|
|
185
|
+
delayed provider reporting retain the usual budget limitations. With `--budget`,
|
|
186
|
+
measured but unpriceable usage still fails with `budget_unverifiable` (exit 125),
|
|
187
|
+
even after the provider finishes, preserving the documented fail-closed guarantee.
|
|
188
|
+
Provider output remains in the run artifacts; without a budget, a completed run
|
|
189
|
+
keeps its provider status and unknown cost. A run that outlives the access credential fails with
|
|
183
190
|
`remote_credential_expired`, requiring a new controller launch.
|
|
184
191
|
|
|
185
192
|
The empty-refresh-token behavior was measured with Codex CLI 0.166.0. Real-host
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.25.
|
|
1
|
+
0.25.4
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# CDX Manager 0.25.4
|
|
2
|
+
|
|
3
|
+
Release date: 2026-11-01
|
|
4
|
+
|
|
5
|
+
This patch release measures remote Codex run costs from observed model evidence and applies budgets to those estimates.
|
|
6
|
+
|
|
7
|
+
## Fixes
|
|
8
|
+
|
|
9
|
+
- Attribute remote run costs to the model observed in Codex usage evidence rather than the requested model. Preserve that evidence in run tracking, stop runs when measured usage exceeds the budget, and report unverifiable budgets explicitly when reliable model evidence or pricing is unavailable. (#83)
|
|
10
|
+
|
|
11
|
+
## Maintenance
|
|
12
|
+
|
|
13
|
+
- Record tray companion checksums for v0.25.3. (`222d5127314bab4200bb13b1b364b3ab5253fb1b`)
|
|
14
|
+
- Record release archive checksums for v0.25.3. (`9bac8bf616345e4348507038dc392a51eb0a360f`)
|
|
@@ -12,6 +12,13 @@
|
|
|
12
12
|
- Add `cdx run --budget USD` to stop on measured estimated spend with exit code 125, `budget_exceeded`, and `error.spent_usd` in the final JSON. Share the budget across recovery attempts and failover; support detached runs and fresh budgets for continued runs without charging earlier turns.
|
|
13
13
|
- Document unknown usage/prices and delayed provider reporting, including possible budget overshoot. Cover pricing, unknown costs, validation, live process termination, Claude transcript accumulation, continuation boundaries, and detached supervision in tests.
|
|
14
14
|
|
|
15
|
+
## Remote run cost observations
|
|
16
|
+
|
|
17
|
+
- Use the local exact-thread observation reader, usage normalization and 128 MiB rollout limit on workers; return content-free model/counter evidence before volatile storage cleanup and bind it to controller run/attempt artifacts.
|
|
18
|
+
- Separate supervisor evidence frames from provider stdout, and check live rollout measurements before `turn.completed` so measured remote budget excess still stops the provider. Never price from requested models or controller-side rollouts.
|
|
19
|
+
- Preserve the documented fail-closed budget guarantee: measured but unpriceable remote usage exits 125 with `budget_unverifiable`, including after provider completion. Keep provider output in artifacts; completed runs without a budget retain their actual status and unknown cost. Credential snapshots, redaction, cleanup and workspace isolation remain unchanged.
|
|
20
|
+
- Cover priced remote/isolated runs, Windows transport, rollouts over 16 MiB, live budget stops, missing/inconsistent/mixed model evidence, unknown prices and completion without pricing using the fake transport.
|
|
21
|
+
|
|
15
22
|
## Remote Codex runs
|
|
16
23
|
|
|
17
24
|
- Add `cdx run --remote SSH_TARGET` with controller-owned supervision, registry, history, output, costs, budgets and detached execution.
|
|
@@ -716,6 +716,17 @@
|
|
|
716
716
|
"x86_64-pc-windows-msvc": "ffcd4deb8b3843771659d2c6b92b003822b288329ddcf3b9a13f0f9820fa3df1",
|
|
717
717
|
"x86_64-unknown-linux-musl": "2cefeea0aad5bb0acfcbc99146ffacc5c5da9386f8338e4a01541e97ccf71e07"
|
|
718
718
|
}
|
|
719
|
+
},
|
|
720
|
+
"v0.25.3": {
|
|
721
|
+
"github_tarball_sha256": "34114563f0edf4d8e13d9410d6a37e4c315571a8410de607b8bc541032561a21",
|
|
722
|
+
"github_zip_sha256": "58cabe7d9d48c314e568c317893b460ca8c8853434a68ee3397e23999f823ddf",
|
|
723
|
+
"tray_assets": {
|
|
724
|
+
"aarch64-apple-darwin": "14da8f42193e78c1bfca5ac046a6a23c7ab52b4155ac9eb213bd9af09cd5e6cc",
|
|
725
|
+
"aarch64-unknown-linux-musl": "47b6988f5f361403e2c8e0c2ff5951cb758af3cde3fc6bb02f7eb718042c3027",
|
|
726
|
+
"x86_64-apple-darwin": "aac73ff0abdb76a957ebd7c143ff3ace46d6df7b08f13db922b28e76dcbc82d0",
|
|
727
|
+
"x86_64-pc-windows-msvc": "c81623ffb94b11ae7b7b28be123b49393240bb0edd31c69969ca07d7eab8dff7",
|
|
728
|
+
"x86_64-unknown-linux-musl": "6892112587c1d8c6e805a90148ec8deb6c04b5927c7ed0c18f19479d521c0ced"
|
|
729
|
+
}
|
|
719
730
|
}
|
|
720
731
|
}
|
|
721
732
|
}
|
package/docs/cli.md
CHANGED
|
@@ -254,7 +254,7 @@ At or above the limit, cdx terminates the provider process tree, skips further r
|
|
|
254
254
|
|
|
255
255
|
With `--detach`, the launch still exits successfully and includes `budget_usd`; cdx always uses its own child supervisor for budgeted runs, including when native background delegation is enabled. Inspect `cdx run-status RUN_ID --json` or `cdx run-report RUN_ID --json` for the child's final payload and budget error. `--continue-run` starts a fresh budget for the new run, including in detached mode.
|
|
256
256
|
|
|
257
|
-
Conservative assumption: this is a stop based on observed, priceable usage, not a prepaid billing limit. Provider updates can arrive late or only at completion, and termination can take time, so actual spend can exceed the amount. Before the first measurement, `budget_status` is `usage_not_measured`; absence is not zero spend. Once usage is measured, missing continuation boundaries, mixed/unobserved models, and unknown prices stop further execution/recovery with exit **125**, `error.source: "cdx"`, `error.code: "budget_unverifiable"`, `budget_status: "unverifiable"`, and `cost_usd: null`. This is a measurement failure, not a claim that the dollar limit was exceeded or enforced. Supply a `CDX_TOKEN_PRICES` override for a model missing from the table. Existing profile-level `cdx set --budget` settings remain the provider-native Claude cap and apply independently.
|
|
257
|
+
Conservative assumption: this is a stop based on observed, priceable usage, not a prepaid billing limit. Provider updates can arrive late or only at completion, and termination can take time, so actual spend can exceed the amount. Before the first measurement, `budget_status` is `usage_not_measured`; absence is not zero spend. Once usage is measured, missing continuation boundaries, mixed/unobserved models, and unknown prices stop further execution/recovery with exit **125**, `error.source: "cdx"`, `error.code: "budget_unverifiable"`, `budget_status: "unverifiable"`, and `cost_usd: null`. This is a measurement failure, not a claim that the dollar limit was exceeded or enforced. This fail-closed guarantee also applies to a remote agent that has already completed: the supervisor reports `failed`/125 while retaining its final output and artifacts. Without `--budget`, unknown pricing does not replace the provider's completion status. Supply a `CDX_TOKEN_PRICES` override for a model missing from the table. Existing profile-level `cdx set --budget` settings remain the provider-native Claude cap and apply independently.
|
|
258
258
|
|
|
259
259
|
Every usage record, whichever path produced it, uses one set of definitions:
|
|
260
260
|
|
|
@@ -292,7 +292,7 @@ The figure uses standard public list prices, excluding Fast/priority, Batch/Flex
|
|
|
292
292
|
|
|
293
293
|
Completed results, registry records, and history JSON include `cost_status` (`estimated` or `unknown`) and nullable `cost_reason`: `usage_not_measured`, `model_not_observed`, `model_price_unknown`, `attempt_usage_unavailable`, or `mixed_models_without_counters`. Stats include `unpriced_reasons` counts alongside `priced_runs` and `unpriced_models`. These are list-price estimates, not billing receipts. `budget_status` is `not_requested`, `usage_not_measured`, `estimated`, `exceeded`, or `unverifiable`.
|
|
294
294
|
|
|
295
|
-
New headless attempts have distinct `attempt_id` values and artifact paths, including retries on the same profile. Final payloads and registry reports expose `attempts` with their individual model evidence, counters, and estimates; top-level usage describes the final attempt, while a budget-exceeded spend includes all attempts. New isolated Codex attempts mount only a fresh sessions directory from volatile `/dev/shm` storage. The supervisor reads the exact `thread.started` rollout while it exists and at closeout, requiring matching session identity, turn timestamps within this attempt, and token counters consistent with stdout. It exports `usage_evidence` containing only the stdout attempt identity, thread id, timestamp bounds, turn ids, observed models, and normalized counters; live rollout counters can establish the first measurement before `turn.completed`. No requested model, agent-authored cost report, current profile setting, prompt, message, or tool output is used as model evidence. The raw temporary sessions are removed at closeout, including cancellation and startup failure; they are never copied into persistent run artifacts. Host profiles are not mounted. Mixed serving models stay unpriced without attributable counters. Isolated continuation without comparable launch boundaries stays unmeasured rather than charging an older conversation to the new attempt.
|
|
295
|
+
New headless attempts have distinct `attempt_id` values and artifact paths, including retries on the same profile. Final payloads and registry reports expose `attempts` with their individual model evidence, counters, and estimates; top-level usage describes the final attempt, while a budget-exceeded spend includes all attempts. New isolated Codex attempts mount only a fresh sessions directory from volatile `/dev/shm` storage. The supervisor reads the exact `thread.started` rollout while it exists and at closeout, requiring matching session identity, turn timestamps within this attempt, and token counters consistent with stdout. It exports `usage_evidence` containing only the stdout attempt identity, thread id, timestamp bounds, turn ids, observed models, and normalized counters; live rollout counters can establish the first measurement before `turn.completed`. No requested model, agent-authored cost report, current profile setting, prompt, message, or tool output is used as model evidence. The raw temporary sessions are removed at closeout, including cancellation and startup failure; they are never copied into persistent run artifacts. Host profiles are not mounted. Remote Codex runs ship the same observation reader and normalization code over the existing stdin bootstrap. The worker validates the exact fresh-home thread, timestamp bounds and matching counters with the local 128 MiB limit, and returns only normalized evidence through supervisor frames, separate from provider stdout. The controller attaches its own stdout path, run id and attempt id before live budget checks and final persistence; it never consults a controller profile rollout for remote usage. Worker stdout capture stays in the existing volatile private home and is deleted with it. Credentials, refresh handling and workspace mounts are unchanged. Live rollout counters can establish remote usage before `turn.completed`. Mixed serving models stay unpriced without attributable counters. Isolated continuation without comparable launch boundaries stays unmeasured rather than charging an older conversation to the new attempt.
|
|
296
296
|
|
|
297
297
|
Completed run JSON and registry reports expose `usage_model` separately from the requested `model`. Pricing uses the observed model from provider output; Codex can also supply it through the exact rollout identified by `thread.started.thread_id`. Configured defaults alone never establish serving-model identity, and missing or mixed model evidence stays unpriced. `--by model` groups Claude's `[1m]` context variant with its base model while preserving the original model string and per-run pricing. New runs record this evidence automatically. To recover missing models in existing history, run:
|
|
298
298
|
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/commands/runs.py
CHANGED
|
@@ -584,7 +584,7 @@ def _observe_headless_usage(session, run_info, baseline=None):
|
|
|
584
584
|
|
|
585
585
|
def _budget_observation(session, paths, baseline):
|
|
586
586
|
observation = _observe_headless_usage(session, paths, baseline if session["provider"] == "codex" else None)
|
|
587
|
-
if (session.get("launch") or {}).get("isolate_workspace"):
|
|
587
|
+
if paths.get("remote") or (session.get("launch") or {}).get("isolate_workspace"):
|
|
588
588
|
return observation
|
|
589
589
|
identifier = (extract_run_conversation_id(session["provider"], paths.get("stdout_path"))
|
|
590
590
|
or ((session.get("conversation") or {}).get("id")
|
package/src/remote_run.py
CHANGED
|
@@ -7,6 +7,7 @@ import os
|
|
|
7
7
|
import pathlib
|
|
8
8
|
import re
|
|
9
9
|
import subprocess
|
|
10
|
+
import threading
|
|
10
11
|
import time
|
|
11
12
|
from contextlib import contextmanager
|
|
12
13
|
from datetime import datetime, timezone
|
|
@@ -82,6 +83,11 @@ def worker_input():
|
|
|
82
83
|
# One JSON line frames the source, leaving the next line for the payload.
|
|
83
84
|
directory = pathlib.Path(__file__).parent
|
|
84
85
|
source = 'SANDBOX_SOURCE = ' + repr((directory / 'workspace_sandbox.py').read_text()) + '\n'
|
|
86
|
+
from .interactive_usage import MAX_TRANSCRIPT_BYTES, MAX_TRANSCRIPT_SCAN_SECONDS
|
|
87
|
+
modules = {name: (directory / (name + '.py')).read_text()
|
|
88
|
+
for name in ('errors', 'run_usage', 'run_observation')}
|
|
89
|
+
source += 'OBSERVATION_MODULES = ' + repr(modules) + '\n'
|
|
90
|
+
source += f'OBSERVATION_LIMITS = {MAX_TRANSCRIPT_BYTES, MAX_TRANSCRIPT_SCAN_SECONDS!r}\n'
|
|
85
91
|
source += (directory / 'remote_worker.py').read_text()
|
|
86
92
|
return json.dumps(source).encode() + b'\n'
|
|
87
93
|
|
|
@@ -167,27 +173,57 @@ def run_remote(session, target, version, cwd=None, env_override=None, initial_pr
|
|
|
167
173
|
paths = _headless_artifact_paths(session, run_id, attempt)
|
|
168
174
|
paths['remote'] = target
|
|
169
175
|
start = datetime.now(timezone.utc)
|
|
170
|
-
child = None
|
|
176
|
+
child = receiver = out = err = None
|
|
171
177
|
cancelled = timed_out = budget_exceeded = credential_expired = False
|
|
172
178
|
handlers = _route_termination_to_interrupt()
|
|
179
|
+
observation_ready = threading.Event()
|
|
180
|
+
observation_lock = threading.Lock()
|
|
181
|
+
|
|
182
|
+
def receive(source, output):
|
|
183
|
+
for line in source:
|
|
184
|
+
try:
|
|
185
|
+
frame = json.loads(line)
|
|
186
|
+
if frame.get('stream') == 'stdout':
|
|
187
|
+
with observation_lock:
|
|
188
|
+
observation_ready.clear()
|
|
189
|
+
output.write(frame['data'])
|
|
190
|
+
output.flush()
|
|
191
|
+
elif frame.get('stream') == 'observation':
|
|
192
|
+
evidence = frame['evidence']
|
|
193
|
+
evidence.update(stdout_path=paths['stdout_path'], run_id=paths['run_id'], attempt_id=paths.get('attempt_id'))
|
|
194
|
+
with observation_lock:
|
|
195
|
+
paths['usage_evidence'] = evidence
|
|
196
|
+
observation_ready.set()
|
|
197
|
+
except (ValueError, TypeError, KeyError, AttributeError):
|
|
198
|
+
continue
|
|
199
|
+
|
|
173
200
|
try:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
201
|
+
out = open(paths['stdout_path'], 'w', encoding='utf-8')
|
|
202
|
+
err = open(paths['stderr_path'], 'w', encoding='utf-8')
|
|
203
|
+
child = subprocess.Popen(ssh_command(target, 'run', remote_prefix), env={**os.environ, **(env_override or {})},
|
|
204
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=err)
|
|
205
|
+
receiver = threading.Thread(target=receive, args=(child.stdout, out))
|
|
206
|
+
receiver.start()
|
|
207
|
+
child.stdin.write(worker_input() + json.dumps(payload).encode() + b'\n')
|
|
208
|
+
child.stdin.flush()
|
|
209
|
+
deadline = time.monotonic() + timeout_seconds if timeout_seconds else None
|
|
210
|
+
while child.poll() is None:
|
|
211
|
+
if time.time() >= payload['expires_at']:
|
|
212
|
+
credential_expired = True
|
|
213
|
+
break
|
|
214
|
+
with observation_lock:
|
|
215
|
+
if budget_check and observation_ready.is_set():
|
|
216
|
+
observation_ready.clear()
|
|
217
|
+
exceeded = budget_check(paths)
|
|
218
|
+
else:
|
|
219
|
+
exceeded = False
|
|
220
|
+
if exceeded:
|
|
221
|
+
budget_exceeded = True
|
|
222
|
+
break
|
|
223
|
+
if deadline and time.monotonic() >= deadline:
|
|
224
|
+
timed_out = True
|
|
225
|
+
break
|
|
226
|
+
time.sleep(0.1)
|
|
191
227
|
except KeyboardInterrupt:
|
|
192
228
|
cancelled = True
|
|
193
229
|
except (OSError, ValueError) as error:
|
|
@@ -208,6 +244,12 @@ def run_remote(session, target, version, cwd=None, env_override=None, initial_pr
|
|
|
208
244
|
except subprocess.TimeoutExpired:
|
|
209
245
|
child.kill()
|
|
210
246
|
child.wait()
|
|
247
|
+
if receiver:
|
|
248
|
+
receiver.join()
|
|
249
|
+
if out:
|
|
250
|
+
out.close()
|
|
251
|
+
if err:
|
|
252
|
+
err.close()
|
|
211
253
|
_restore_signal_handlers(handlers)
|
|
212
254
|
_combine_headless_transcript(paths)
|
|
213
255
|
info = {**_headless_run_info(paths, spec, start, child.returncode), 'remote': target,
|
package/src/remote_worker.py
CHANGED
|
@@ -12,9 +12,26 @@ import sys
|
|
|
12
12
|
import tempfile
|
|
13
13
|
import threading
|
|
14
14
|
import time
|
|
15
|
+
import types
|
|
16
|
+
from datetime import datetime, timezone
|
|
15
17
|
|
|
16
18
|
# Supplied by the controller before this module is executed.
|
|
17
19
|
SANDBOX_SOURCE = globals().get('SANDBOX_SOURCE', '')
|
|
20
|
+
OBSERVATION_MODULES = globals().get('OBSERVATION_MODULES', {})
|
|
21
|
+
OBSERVATION_LIMITS = globals().get('OBSERVATION_LIMITS', (128 * 1024 * 1024, 1))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def observation_runtime():
|
|
25
|
+
# Load only controller-owned readers, with no worker installation required.
|
|
26
|
+
package = types.ModuleType('cdx_observation')
|
|
27
|
+
package.__path__ = []
|
|
28
|
+
sys.modules[package.__name__] = package
|
|
29
|
+
for name, source in OBSERVATION_MODULES.items():
|
|
30
|
+
module = types.ModuleType(package.__name__ + '.' + name)
|
|
31
|
+
module.__package__ = package.__name__
|
|
32
|
+
sys.modules[module.__name__] = module
|
|
33
|
+
exec(compile(source, name + '.py', 'exec'), module.__dict__)
|
|
34
|
+
return sys.modules[package.__name__ + '.run_usage'], sys.modules[package.__name__ + '.run_observation']
|
|
18
35
|
|
|
19
36
|
|
|
20
37
|
def version():
|
|
@@ -144,6 +161,7 @@ def main():
|
|
|
144
161
|
['codex', *args], pathlib.Path(payload['cwd']), network=payload['network'],
|
|
145
162
|
writable=payload['writable'], source=env, auth_home=str(home), provider='codex',
|
|
146
163
|
observation_home=str(home / 'sessions'))
|
|
164
|
+
started_at = datetime.now(timezone.utc).isoformat()
|
|
147
165
|
child = subprocess.Popen(command, cwd=payload['cwd'], env=env,
|
|
148
166
|
stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
149
167
|
start_new_session=True)
|
|
@@ -153,36 +171,36 @@ def main():
|
|
|
153
171
|
secrets = [value for value in auth['tokens'].values() if isinstance(value, str) and value]
|
|
154
172
|
|
|
155
173
|
output_lock = threading.Lock()
|
|
156
|
-
|
|
174
|
+
usage_reader, observation_reader = observation_runtime()
|
|
175
|
+
stdout_path = home / 'stdout.jsonl'
|
|
176
|
+
stdout_path.touch(mode=0o600)
|
|
157
177
|
|
|
158
178
|
def snapshot():
|
|
159
|
-
#
|
|
160
|
-
#
|
|
179
|
+
# Read the same exact-thread evidence as the local supervisor before
|
|
180
|
+
# volatile storage is removed. Only normalized metadata is exported.
|
|
161
181
|
with output_lock:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
print(event, flush=True)
|
|
185
|
-
return
|
|
182
|
+
usage = usage_reader.extract_run_usage('codex', stdout_path)
|
|
183
|
+
identifier = usage_reader.extract_run_conversation_id('codex', stdout_path)
|
|
184
|
+
evidence = {'source': 'isolated_codex_rollout', 'thread_id': identifier,
|
|
185
|
+
'started_at': started_at, 'usage': usage, 'models': [], 'turns': []}
|
|
186
|
+
if identifier:
|
|
187
|
+
paths = []
|
|
188
|
+
deadline = time.monotonic() + OBSERVATION_LIMITS[1]
|
|
189
|
+
for directory, subdirs, names in os.walk(home / 'sessions'):
|
|
190
|
+
if time.monotonic() > deadline:
|
|
191
|
+
paths = []
|
|
192
|
+
break
|
|
193
|
+
subdirs[:] = [name for name in subdirs if name != 'subagents']
|
|
194
|
+
paths.extend(pathlib.Path(directory) / name for name in names
|
|
195
|
+
if name.startswith('rollout-') and name.endswith(f'-{identifier}.jsonl'))
|
|
196
|
+
if len(paths) > 1:
|
|
197
|
+
break
|
|
198
|
+
if len(paths) == 1:
|
|
199
|
+
evidence = observation_reader.codex_rollout_evidence(home, paths[0], evidence, OBSERVATION_LIMITS[0])
|
|
200
|
+
event = json.dumps({'stream': 'observation', 'evidence': evidence})
|
|
201
|
+
for secret in secrets:
|
|
202
|
+
event = event.replace(secret, '[REDACTED]')
|
|
203
|
+
print(event, flush=True)
|
|
186
204
|
|
|
187
205
|
def forward(source, target):
|
|
188
206
|
for line in iter(source.readline, b''):
|
|
@@ -190,16 +208,13 @@ def main():
|
|
|
190
208
|
line = line.replace(secret.encode(), b'[REDACTED]')
|
|
191
209
|
with output_lock:
|
|
192
210
|
if target is sys.stdout:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
pass
|
|
201
|
-
target.buffer.write(line)
|
|
202
|
-
target.buffer.flush()
|
|
211
|
+
with stdout_path.open('ab') as capture:
|
|
212
|
+
capture.write(line)
|
|
213
|
+
# Frame provider output so it cannot forge supervisor evidence.
|
|
214
|
+
print(json.dumps({'stream': 'stdout', 'data': line.decode('utf-8', errors='replace')}), flush=True)
|
|
215
|
+
else:
|
|
216
|
+
target.buffer.write(line)
|
|
217
|
+
target.buffer.flush()
|
|
203
218
|
|
|
204
219
|
threads = [threading.Thread(target=forward, args=pair) for pair in
|
|
205
220
|
((child.stdout, sys.stdout), (child.stderr, sys.stderr))]
|
package/src/run_observation.py
CHANGED
|
@@ -6,8 +6,6 @@ import stat
|
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
-
from .history_models import _exact_transcript
|
|
10
|
-
from .interactive_usage import MAX_TRANSCRIPT_BYTES
|
|
11
9
|
from .run_usage import _usage_from_dict, extract_run_conversation_id, extract_run_usage
|
|
12
10
|
|
|
13
11
|
|
|
@@ -31,6 +29,9 @@ def codex_attempt_evidence(auth_home, stdout_path, started_at):
|
|
|
31
29
|
Do not import claimed costs, requested models, messages, or arbitrary files
|
|
32
30
|
from the workspace. The exported object is never writable by the sandbox.
|
|
33
31
|
"""
|
|
32
|
+
from .history_models import _exact_transcript
|
|
33
|
+
from .interactive_usage import MAX_TRANSCRIPT_BYTES
|
|
34
|
+
|
|
34
35
|
usage = extract_run_usage("codex", stdout_path)
|
|
35
36
|
identifier = extract_run_conversation_id("codex", stdout_path)
|
|
36
37
|
evidence = {"source": "isolated_codex_rollout", "stdout_path": str(stdout_path),
|
|
@@ -41,6 +42,14 @@ def codex_attempt_evidence(auth_home, stdout_path, started_at):
|
|
|
41
42
|
path, error = _exact_transcript("codex", auth_home, identifier)
|
|
42
43
|
if error or not path:
|
|
43
44
|
return evidence
|
|
45
|
+
return codex_rollout_evidence(auth_home, path, evidence, MAX_TRANSCRIPT_BYTES)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def codex_rollout_evidence(auth_home, path, evidence, max_bytes):
|
|
49
|
+
"""Validate exact-attempt rollout evidence identically on local and remote hosts."""
|
|
50
|
+
usage = evidence["usage"]
|
|
51
|
+
identifier = evidence["thread_id"]
|
|
52
|
+
started_at = evidence["started_at"]
|
|
44
53
|
models = set()
|
|
45
54
|
turns = []
|
|
46
55
|
identity = False
|
|
@@ -52,8 +61,8 @@ def codex_attempt_evidence(auth_home, stdout_path, started_at):
|
|
|
52
61
|
with os.fdopen(descriptor, encoding="utf-8") as handle:
|
|
53
62
|
if not stat.S_ISREG(os.fstat(handle.fileno()).st_mode):
|
|
54
63
|
return evidence
|
|
55
|
-
text = handle.read(
|
|
56
|
-
if len(text) >
|
|
64
|
+
text = handle.read(max_bytes + 1)
|
|
65
|
+
if len(text) > max_bytes:
|
|
57
66
|
return evidence
|
|
58
67
|
for line in text.splitlines():
|
|
59
68
|
record = json.loads(line)
|