Repository navigation
feat(codex_agent): run Codex in a task sandbox like a plain codex exec - #5156
Conversation
…ec does Evaluating Codex in a task sandbox should send the requests the Codex CLI sends when a harness such as Harbor 0.24 launches `codex exec` directly in the task container. Sandbox sessions refuse extra_config, so add explicit settings. Every default keeps the current behavior: - model_server_forwards_reasoning_effort: sandbox sessions keep rejecting reasoning_effort by default, because most Gym model servers cannot guarantee it reaches the backend. The flag records the operator's check that the model server forwards Codex's `reasoning` field unchanged; the effective value is then visible in captured model calls. - inherit_sandbox_environment: give Codex the sandbox's whole environment instead of PATH, the locale, HOME and the XDG directories, keeping only CODEX_HOME private. Task images set variables such as VIRTUAL_ENV, PYTHONHASHSEED or OMP_NUM_THREADS that a harness started in the container sees. Intended for remote providers, where that environment is the task's. - features: the generated [features] table, which turns off multi_agent and code_mode by default. A null entry is left out, so the CLI default applies, as in a launch without a Gym config. - ephemeral: false drops --ephemeral, so Codex keeps session state and offers its goal tools (create_goal, get_goal, update_goal). A correlated local rollout no longer tries to remove a flag that is not there. - request_max_retries, stream_max_retries: the provider's resends before a stream starts and reconnects after one drops, beside stream_idle_timeout_ms; null leaves Codex's own defaults. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
Harbor runs the verifier after any agent exit that is not a timeout: a trial whose agent died while its sandbox still answers is graded, not rerun. codex_agent instead raised an execution error for every failed Codex run, so the Environment Server skipped verification and the episode was retried from scratch: an agent the kernel's OOM kill hit (once the sandbox kills a single process instead of itself), an agent whose own pkill hit the runner, or a Codex exit 1. verify_after_agent_error (off by default) returns such a run as a `failed` response, with the error in metadata.graded_agent_error, which Resources grades as a nonzero agent exit. It applies only when the supervisor confirmed cleanup, the failure that ended the run names no model-endpoint transport or 429/5xx error, and the sandbox still reports running and answers a probe command. A lost or unresponsive sandbox, an unconfirmed cleanup and an endpoint outage stay execution errors and are retried. The endpoint check reads only the terminal failure after the run's last model output (turn.failed and the last error event before it, or the last event when Codex was killed without turn.failed), so a retry that recovered no longer turns a later unrelated failure into a rerun. It recognizes Codex 0.154.0's messages: unexpected status 429/5xx, the "high demand" message for a 500, exceeded retry limit, the waiting-for-network notice, and stream disconnects from a transport error, a closed stream, a failed send or the SSE idle timeout. The output-cap disconnect (reason: max_output_tokens) stays gradable. raise_oom_score_adj (off by default) makes Codex the OOM killer's preferred victim in its sandbox. In Docker, the leaderboard's runtime, the daemon that runs the agent lives outside the container's memory cgroup, so an OOM kill can only pick the agent's own processes. In an OpenSandbox sandbox the exec daemon and Gym's supervisor share the cgroup and the oom_score_adj with Codex. The runner raises its own score, which Codex and every tool inherit, to 1000, or to 0 when the sandbox starts it below zero (a Guaranteed pod's -997), so the sandbox never becomes the node's first victim in a global OOM. The exec daemon and the supervisor keep the sandbox's own score. TB4's resources-session test covers the `failed` response status: it is graded as a nonzero agent exit. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
codex_agent removes its session directory, and with it CODEX_HOME, when a sandbox session closes. Without the model-call capture, Gym then keeps no per-call token counts for a Codex run: the response usage comes from turn.completed, which a run stopped at its time limit never emits. With the capture on, every request payload is also copied into the rollout row (one image-heavy trial's row was 2.5 GB after 20 minutes), which a full 210-trial run cannot hold in memory or on disk. New sandbox_logs_dir setting: after Codex stops, the session rollouts (CODEX_HOME/sessions/**/rollout-*.jsonl, with per-call token counts and timestamps) are copied to <sandbox_logs_dir>/sessions before the session directory is removed. A failed copy is logged and never fails collection. Harbor's Codex agent copies them to /logs/agent/sessions the same way; with /logs/agent, the TB4 resources server copies them into the trial directory (agent/sessions/). It only acts without --ephemeral. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
A task image without WORKDIR (some Terminal-Bench-Science tasks) lends "/"
as its workdir. codex_agent refused it with HTTP 422 ("runtime/session
files must be outside SandboxAccess.workdir"), so every trial of those
tasks failed at session start, while Harbor runs the agent there.
The root holds every path, so it is accepted. The runtime and session
files stay under /tmp, and any other workdir must still be disjoint from
them: /tmp itself and paths under the adapter's /tmp prefixes are refused
as before, and the in-sandbox check still rejects a workdir that contains
or lies inside an adapter path after resolving symlinks.
Tests: six new cases in codex_agent/tests/test_sandbox_sessions.py cover
the root workdir (in-sandbox check and seeding) and the workdirs that are
still refused.
Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
… session state Main's sandbox sessions always run Codex without --ephemeral, to keep its native session transcript, so the setting no longer changes them. Local runs keep main's --ephemeral. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
|
/ok to test 74d7a2b |
|
🌿 Preview your docs: https://nvidia-preview-pr-5156-4601f68db032.docs.buildwithfern.com/nemo/gym |
svcnvidia-nemo-ci
left a comment
There was a problem hiding this comment.
HOLD
Found one BLOCKER and three RISK issues in the opt-in failure-grading and log-capture paths. Details and acceptance checks are inline.
Reviewed all eight changed files and their directly affected session-lifecycle, outcome-policy, verification, and usage-reporting consumers. Static inspection only; no tests or sandbox/model runs were executed.
Note: formal approval is not available for this request, so this review is posted as a comment only.
Inline comments (escaped v1):
-
responses_api_agents/codex_agent/app.pyline 107: BLOCKER — Preserve retry handling for the supported CLI's timeout and capacity errorsFailure: Enable
verify_after_agent_error, replayturn.startedfollowed byturn.failedwitherror.message="request timed out", and supply confirmed cleanup withreturn_code=-9,timed_out=False, and no cleanup error. On a live sandbox,_terminal_model_errordeclines the non-1 exit and_model_endpoint_errorfinds no marker, so the run becomes an unclassified HTTP-200 failed response. It is then verified rather than retried. The same occurs forSelected model is at capacity. Please try a different model.Both are existing supported endpoint failures in the native-error regression cases. A runner killed after the reported failure therefore lets an endpoint outage enter benchmark scores because unclassified failures verify.Correction: Include these native messages in
_MODEL_ENDPOINT_ERRORS. At this line, the small replacement is:"connection timed out", "request timed out", "selected model is at capacity",The invariant is that these recognized terminal endpoint failures cannot pass the grading liveness gate. This suggestion is source-traced, not executed; please verify before adopting.
Preservation / proposed acceptance: For each message, test the transcript and cleanup above: expect HTTP 502 and no liveness probe. Keep exit-1 reported failures classified with outcome-policy RETRY. Keep recovered retry → model output → unrelated kill gradable; HTTP-400 errors must remain non-endpoint errors, and the output-cap case must remain
incomplete/max_output_tokens. The disabled opt-in must retain its existing behavior. -
responses_api_agents/codex_agent/app.pyline 1117: RISK — Apply conservative usage recovery to newly gradable failuresFailure: With
verify_after_agent_error=True, complete a model/tool call and kill Codex beforeturn.completed. Even if the native transcript contains cumulativetoken_counttotals of input 20/output 8/total 28, the new scored response reports 0/0/0: the CLI parser starts at zero and this fallback runs only forreported_model_error. Reward profiling consumes those numeric usage values directly, understating failed-trial costs despite available measurements.Correction: Extend this condition while retaining its existing body:
if (reported_model_error or graded_error is not None) and not completed:This reuses native cumulative usage, or marks usage unavailable when prior work is possible instead of publishing a fabricated zero. Downstream detail accesses already guard
response_usage is None. Also update the new graded-failure integration assertions that currently require zero usage. Please verify this source-traced suggestion before adopting.Preservation / proposed acceptance: Combine the killed-agent setup with the existing native-usage fixture: expect failed status, null error, no classified outcome, and usage 20/8/28. With the same prior work but no usable native usage, expect
usage=None; the new graded-failure environment integration should likewise expect unavailable usage. Preserve the existing provider-failure native/CLI/missing-usage assertions, known-zero no-work provider failures, completed-run total 22, and ordinary timeout/output-cap behavior. Tests were not executed. -
responses_api_agents/codex_agent/app.pyline 1105: RISK — Store captured observations before awaiting sandbox livenessFailure: Enable the opt-in, let a started failed run finish artifact collection, then block
sandbox.status()or thetrueprobe and close the session concurrently. This new await precedes assignment tostate.observations. Session close removes the session files and cancels the activation, so the close response contains only the fallback interrupted gap rather than the already-collected conversation/tool evidence. This loses diagnostics for cancelled failed trials. The neighboring startup-stderr path deliberately stores observations before its await.Correction: Build and store a provisional failed/incomplete observation bundle before the grading probe; after the probe resolves, update the invocation to the final graded status. Preserve session-close ownership of cancellation—do not keep the sandbox open or extend close deadlines just to finish probing. Exact placement must also preserve native-usage recovery, so a move-only patch is not sufficiently certain to suggest here. The invariant is that cancellation during optional classification cannot discard captured observations.
Proposed acceptance: Start a failed activation containing a partial tool/message transcript. Have
status()signal entry and wait; close the session before releasing it. Expect activation cancellation, borrowed-sandbox disconnect without destruction, and close observations retaining the failed invocation and partial tool/message evidence. Preserve normal live-sandbox grading, stopped/unresponsive-sandbox execution errors, and the existing close-during-startup-stderr assertion that the failed invocation survives cancellation. These checks were not executed. -
responses_api_agents/codex_agent/sandbox.pyline 125: RISK — Keep optional archival within a separate capture sub-budgetFailure: With
sandbox_logs_direnabled, a successful run becomes an execution failure if copying takes longer thansession_close_timeout_seconds(default 60 seconds). The copy allows 120 seconds, but SandboxSession._finalize wraps this entire collector in the shorter close timeout. Cancellation escapes the copy'sexcept Exception; events have not been read, capture recordsTimeoutError, and execution raises instead of returning the otherwise valid response. Verification is skipped, and close can delete the source without retrying capture. This contradicts the best-effort archival contract for large logs or slow mounts.Correction: Thread the actual capture budget/deadline from
CodexSandboxSession.executeinto the collector and archive operation. Enforce and catch a local archive timeout that expires before the enclosing deadline, reserving time for required event/metadata capture. The invariant is that optional copy failure or timeout cannot discard a readable transcript or change grading disposition. Budget allocation needs implementation verification; merely moving the copy after reads or catching outer cancellation still allows collection to abort before returning.Preservation / proposed acceptance: Set a short close budget and make only the archive exec wait past it while events and cleanup remain valid. Expect a completed response with 22 tokens, retained conversation observations, and successful close; archival may be omitted with a warning. Retain byte-for-byte copying in the existing archive test, no-op behavior without a logs/source directory, and successful response capture when copying returns nonzero or raises. Timeout/close must still preserve available partial logs only after confirmed termination; unconfirmed cleanup must continue to prevent capture and grading. Checks were not executed.
Review generated by codex. This was a full review. Request a full review with /review reset=true.
… errors Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
|
/ok to test 4601f68 |
|
All four findings reproduced against the code, and each now has a test that fails on the previous head.
|
|
@gwarmstrong this branch now conflicts with |
|
@gwarmstrong the inline note is non-blocking; the only thing left is that the branch now conflicts with |
Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
|
/ok to test 04ed490 |
|
@yaoyu-33 thanks, Nothing else needed adapting: the Codex runner still runs as a standard-library Python runner under the bash supervisor. |
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/40739984627 |
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/40745253835 |
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/40750164067 |
|
Smoke on this head (04ed490, with PR 6 66d4c71 and the Terminal-Bench-Science Codex profile, on main c155b77): Codex 0.154.0 in two Terminal-Bench-Science task sandboxes on OpenSandbox with
|
#5156 changed Codex's Python worker, which this branch replaces with the bash runner and the Node launcher. Its worker options are ported: - inherit_env: the launcher passes Codex the task's whole environment; the runner then keeps the task's NODE_OPTIONS too, as a plain codex exec in the task would see it. - raise_oom_score_adj: runner setup raises the runner's own score, which the launcher, Codex and every tool inherit, and logs the change to the supervisor's output.log; the supervisor keeps the sandbox's score. - A root workdir is accepted, as #5156 does: the POSIX overlap check skips "adapter path inside the workdir" when the workdir is /. #5156's worker tests are ported to the runner and launcher, and its root-workdir checks now run in POSIX sh. README keeps both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
What does this PR do?
Benchmarks that grade a harness started inside the task container, as Harbor does, need Codex in a sandbox session to send the requests a plain
codex execsends there and to be graded the way Harbor grades it. Sandbox sessions refuseextra_config, so this adds explicit settings tocodex_agent, all off by default:model_server_forwards_reasoning_effortlets a configuredreasoning_effortthrough once the operator has checked that the model server forwards it;inherit_sandbox_environmentgives Codex the task image's environment andHOME;featuresleaves Codex's own feature defaults in place;request_max_retriesandstream_max_retriesset the provider's retries.verify_after_agent_errorreturns a run that failed while its sandbox still runs and answers as a gradablefailedresponse with no classified cause, so the Environment Server's outcome policy verifies it, as Harbor grades any agent exit. A lost sandbox, unconfirmed cleanup, a run that failed before its first turn or a model-endpoint error (Codex's 429/5xx, capacity, request-timeout, retry-limit and stream-disconnect messages, read from the failure that ended the run) stays an execution error and is retried. A provider failure that Codex reports itself (exit 1 onturn.failed) keeps the classified cause from fix(codex): retain session evidence and report model errors #4740, so the outcome policy decides it. A graded run reports Codex's native cumulative usage, or none when work happened that no usage record covers. Its observations are stored before the sandbox liveness check, so a session closed during the check keeps them.raise_oom_score_adjmakes Codex and its tools the OOM killer's preferred victims.sandbox_logs_dircopies Codex's session rollouts (per-call token counts and timestamps) to a directory the resources server collects. The copy runs after the run's events are read and within the capture time left, so a slow copy is cut short instead of failing the run.WORKDIRlends/as its workdir, which sessions refused with HTTP 422. It is now accepted; the runtime and session files stay under/tmp, and any other workdir must still be disjoint from them.No issue: opt-in settings on the existing sandbox sessions, plus a fix found in Terminal-Bench-Science runs.
Validation
codex_agent: 463 passed (7 skipped);pre-commitclean.main: Codex 0.154.0 with self-hosted DeepSeek V4.1 Flash on Terminal-Bench-Science, 70 tasks × 3 trials, 8 h, scored 31/210 against the public leaderboard's 33/210 for the same model and harness; Codex with GPT-6 Astra on 10 tasks scored 7/10 against stock Harbor's 7/9 on the same tasks.Rollouts
N/A: agent settings. Profiles that use them come with the Terminal-Bench-Science harness profiles.
Compatibility and benchmark impact
Defaults unchanged, except that a workdir of
/is now accepted instead of refused.