Repository navigation
fix(agent): build Hermes and Stirrup runtimes on the host - #5152
Conversation
The supervisor uploaded into task sandboxes now needs only bash, /proc, sleep and mv, so sandboxed harnesses no longer require Python 3.8+ in the task image. It keeps the CLI and receipt contract: run the harness in its own process group, stop it at the deadline or when the stop file appears (TERM, then KILL the group), and confirm no group member runs. Stops now reach the supervisor through stop.request instead of SIGTERM, so the launch shell no longer ignores TERM and the harness keeps the default disposition. Shared receipt types and timeouts move to supervisor_client; SandboxCommand drops its python field. shell_assignments() renders runner inputs as bash source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
harness_runner.sh replaces per-harness Python runners: it sources the PARAMS written by shell_assignments(), records runtime.json, applies the harness environment and runs the CLI with file-backed stdio. With events set, it stamps each JSON stdout line with the time it was seen, reading a file so a detached tool holding stdout cannot stall it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…rmes tests Point the harness guide, package docs and SandboxSession docstring at process_supervisor.sh and harness_runner.sh, drop the Python-in-sandbox requirement and the subreaper drain the bash supervisor no longer performs. Hermes tests use the bash supervisor. The runner stamps a final unterminated event line with the same fallback time as the others on bash < 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Kilo sessions now run through the shared bash harness runner with PARAMS written by shell_assignments(); sandbox_runner.py is gone. A short setup string removes a stale plugin receipt and records the task's HOME/XDG for the task-environment plugin, unset_env drops inherited KILO_*/OPENCODE_*/ OpenAI variables, and the exit code comes from the cleanup receipt. Path checks are POSIX shell, the bootstrap installs only bash, and the installer drops its Python checks. The SQLite snapshot moves host-side: kilo.db and its WAL files are downloaded and backed up locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…ndbox Claude sessions now launch through the shared bash harness_runner.sh with params.sh instead of sandbox_runner.py and input.json. The offline runtime installer is a bash script with the same SHA-256, version and lock semantics, so task images need bash and coreutils but no Python. Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Launch OpenClaw through the shared bash harness_runner.sh with PARAMS from shell_assignments() and a staged prompt file, replacing sandbox_runner.py. The workdir overlap check is POSIX sh, session setup installs only a missing bash, and the runtime installer drops its Python checks (apk now reports whether musl images have libstdc++; the prepare step already guards the runtime path against the task workdir). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Task images now need only bash. The shared harness_runner.sh replaces sandbox_runner.py: PARAMS carry the CLI argv, environment and stdio files, and a short setup records the task HOME/XDG directories for the shell.env hook, removes a stale initialization receipt and creates the private CLI directories. The SQLite snapshot moves to the agent server: after confirmed cleanup the adapter downloads opencode.db (and its -wal/-shm files when present) and runs the same export there, producing the same export.json and observations.db. The bootstrap no longer installs python3 and checks workdir overlap with POSIX shell, and the runtime installer serializes with flock, or a reclaimable lock directory without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Pi now runs under the shared bash harness runner: the adapter stages params.sh and prompt.txt, and the runner records [seen_at, event] lines. Session setup no longer installs python3, the runtime installer checks for the musl C++ library by path instead of with ctypes, and the host parser skips recorded lines that are not [number, object] records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…available A missing or undownloadable session database again leaves no export, so collection reports "OpenCode sandbox runner returned no valid result" with the output log, as when export.json was missing before. The export no longer depends on copying the observation store: a failed copy, like the failed observations.db download before, only adds observation gaps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
The bash supervisor no longer takes an interpreter, so NOOA drops the removed SandboxCommand.python field (its own pinned Python still runs its worker), and the Codex worker keeps its Python runner under the bash supervisor: its sandbox-local 429 retry proxy still needs Python. OpenCode drops sandbox_python_executable; sandbox_path_prefix now reaches the CLI through the runner setup. Tests and READMEs name process_supervisor.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
… runner names On bash < 4.2 printf has no strftime, so every event was stamped with an empty time and the host parsers dropped them all; fall back to date. Strip a trailing CR before recording a JSON line. Inherited variables named like runner PARAMS (setup, events, stdout, ...) no longer configure the runner or leak into the CLI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…he bash supervisor A stopped CLI's tools that run in their own session (OpenCode and Codex start every shell tool that way) kept writing to the task after a confirmed receipt. Before stopping the harness, the supervisor now records its descendants outside its process group from /proc and kills them, and the groups they lead, with the harness; confirmation waits for them too. Services orphaned before the stop are still left for the sandbox owner. The supervisor also: - reads each /proc/PID/stat whole and never evaluates its fields as arithmetic, so a process name holding a newline cannot run code or hide a group member; - refuses to launch without a readable /proc, and counts unreadable process state as running; - bounds the wait for a KILLed harness and writes a negative receipt instead; - exits on a missing option value or separator instead of looping or running part of the command, and ignores an inherited errexit. Launches render fixed-point seconds, and a deadline shorter than the supervisor's centisecond resolution expires before launch instead of leaving no receipt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…runner descriptors Without job control bash starts an async command with INT and QUIT ignored, and the CLI's tools inherited that. The CLI also inherited the runner's read and events descriptors, so a tool could write raw lines into events.jsonl. Reset the two signals (bash 5 honours the reset) and close both descriptors for the CLI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…e to older bash Without flock, the Claude installer never reclaimed the lock of a killed installer, and OpenCode's reclaim could delete a lock another installer had just taken (or skip an owner killed before it wrote its PID). Both now take a symlink that names the owner by PID and start time, and remove a dead owner's lock only under a second lock after re-reading it. The Claude installer lowercases the trusted digest without bash 4 expansions. The Pi installer no longer fails under set -u on bash < 4.4 when it needs no packages, and checks every default musl library directory for libstdc++. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
sqlite3's context manager commits but does not close, so a failed export kept the copied databases open while the snapshot directory was removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…wn sessions The receipt docstring, the harness guide and the Codex, Pi, OpenCode, OpenClaw, Hermes and Terminal-Bench READMEs now describe what the bash supervisor does: it kills the harness group and, after a forced stop, the descendants that ran in other process groups when the stop began; processes that left the harness earlier stay with the sandbox owner. The receipt documents shell exit statuses. The Kilo and local-provider tests cover a tool in its own session again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
… stop test shell_assignments() now raises for NUL and non-string words, which bash would silently drop or which raised an opaque TypeError. The Hermes close test stops the supervisor through the stop file alone, as production does, and the sandboxed Hermes README states that the supervisor needs the task image's bash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…completed tools The bash port's tests asserted that a tool still running in its own session survives a close, and modelled a completed tool's service as a direct child of the CLI. With the supervisor's stop-time cleanup, an in-flight detached tool is stopped again, as before the port; a service left by a tool that already exited still survives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
The harness guide said Codex starts its CLI through harness_runner.sh. On main Codex keeps its Python sandbox_runner.py for its sandbox-local retry proxy, so it still needs the task image's Python; Hermes, Stirrup and NOOA run their Python shims on interpreters they install privately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…sandbox-runtime # Conflicts: # responses_api_agents/hermes_agent/tests/test_sandbox.py
Resolve the conflicts with #4805, which hid the Python supervisor and opted-in Python runners from `pkill python` by running them through interpreter links. The supervisor here is bash, so its link is dropped. Its runner link survives as SandboxCommand.stdlib_runner on the bash launch: Codex, the one harness that keeps a standard-library Python runner, runs it as bin/gym-runner from `sandbox_runner`. OpenCode, OpenClaw, Pi and Kilo keep the bash runner, whose name never mentions python. The pkill and delayed-launch tests follow the bash supervisor; the READMEs and the harness guide describe the result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…afe task dirs Address review findings on the bash runtime: - Without flock, the OpenCode and Claude installers now queue on numbered owner links that are never removed or reused, and skip a number whose owner is done or gone. There is no second reclaim lock left to strand, and an installer killed at any point cannot block or release another. - A preinstalled OpenCode binary must exit successfully from --version, not only print the version, before it replaces the cached runtime. - OpenCode's and Kilo's task-env.json writers escape every JSON control character, so task HOME/XDG paths with tabs or newlines stay valid JSON. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…l-name tests The pkill tests merged from main called payload() with a timeout, which only the Python runner's input carried; the bash runner takes its timeout from execute(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
The pkill test from main assumed the supervisor's only child is the runner. The bash supervisor also forks its polling sleeps, so take the runner as the parent of the tool it started and check that it is a supervisor child. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Review found that one descendant snapshot before TERM let a confirmed receipt leave processes running: a TERM handler that runs `setsid ... &` and exits, or a tool that keeps forking new sessions during the grace period. - The supervisor gives the harness a NEMO_GYM_SUPERVISOR environment marker unique to this run. On a forced stop it scans /proc when the stop begins, on every grace and drain round, and before the final KILL. It reaches the harness group, descendants of the harness, of its group and of reached processes, and processes carrying the marker. Marked processes that had already left the harness when the stop began are preserved as services, as the subreaper on main did. The Codex runner passes the marker through its env allowlist. - A recorded group is killed or awaited only while its ID is free or still held by the recorded leader (same start time), so a reused group ID is not hit. - Without a process group of its own, the supervisor restarts itself as a job in a new group, so a provider's group KILL cannot end it before the receipt. The shell left behind passes on TERM and the exit status. - Docstrings and READMEs state what a forced stop reaches and what it does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…statuses
- emit() strips a trailing CR without `${1%$'\r'}`, which tried the suffix at
every position and took seconds per MB, long enough for Pi's agent_end event
to outlast the deadline.
- The runner resets errexit, nounset, pipefail and IFS, so a BASH_ENV with
`set -e` no longer stops it at the first non-JSON line.
- A failed exec redirection no longer falls through into events mode or
launches the CLI. The runner exits 125 for its own failures (bad cwd, an
exit in setup) and 126 or 127 when the CLI could not be started.
- Drop `trap - INT QUIT`: it did not change what the CLI inherits, and its test
passed without it.
- Kilo tells runner failures apart from Kilo's own exit status, reports no exit
code for a Kilo the runner never started, and none for a cancelled run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…e Code - The OpenCode installer removes a partly staged binary from its single EXIT trap, however it exits, and a cached binary must exit successfully from --version to be reused. - Claude Code setup installs a missing bash on root images with apk or apt-get, as the other bash-runner adapters do, and otherwise fails with a message that names bash instead of a bare 127. - Reword "native" in the Claude installer docs, docstring and a test name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
|
I'll watch this ready-to-merge PR and report substantive progress. Running on |
Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
|
💬 I couldn't inspect the failed checks or their logs: GitHub reads failed with HTTP 503 because the platform's Git authority was unavailable. I haven't diagnosed the CI failure or requested a rerun. I'll keep the PR watch armed. |
|
💬 GitHub access recovered. The PR is still open, but GitHub now reports merge conflicts with main. I’m checking the current CI and review evidence before preparing a resolution. |
Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
|
/ok to test fb96355 |
|
💬 The run failed: {"container_build":{"correction":"","duties_complete":false,"equivalent_attempt":0,"justification_evidence":"","preflight_complete":false,"preflight_evidence":"","reason":"not_requested","requested":false,"reuse_checked":false,"reuse_evidence":""},"do":[],"say":"","state":"working","wait":[{"item":"watch","on":"retry_turn","retry_kind":"platform"},{"item":"watch","on":"ci_watch_pr","pr":5152,"repo":"NVIDIA-NeMo/Gym"}]}. |
1 similar comment
|
💬 The run failed: {"container_build":{"correction":"","duties_complete":false,"equivalent_attempt":0,"justification_evidence":"","preflight_complete":false,"preflight_evidence":"","reason":"not_requested","requested":false,"reuse_checked":false,"reuse_evidence":""},"do":[],"say":"","state":"working","wait":[{"item":"watch","on":"retry_turn","retry_kind":"platform"},{"item":"watch","on":"ci_watch_pr","pr":5152,"repo":"NVIDIA-NeMo/Gym"}]}. |
yuhezhang-ai
left a comment
There was a problem hiding this comment.
Thanks for the fixes. Both of my earlier findings are addressed at fb96355: archive extraction no longer restores unmapped host ownership, and the interruption tests wait for installer readiness before starting the short deadline. I also checked the remote-failure cleanup and import reconciliation and found no further blocking issues. The focused regressions and required CI checks pass on this head.
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/41767955867 |
|
PR |
|
The PR is no longer an open ready-to-merge PR targeting main. |
#5152 landed on main as a6cd35c with commits this branch had not seen. Five conflicted files belong to #5152 (nemo_gym/agents/__init__.py, sandbox_python.py, nemo_gym/sandbox/api.py, Stirrup's session tests and test_sandbox_python.py); this branch does not change them, so main's version stands. The harness guide is main's text with this branch's four Codex edits: Codex starts through the runner and Node launcher, no adapter depends on the task image's Python, stdlib_runner remains only for existing shims, and the Codex launcher keeps the supervisor marker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Resolve the harness-guide conflicts with NVIDIA-NeMo#5152 by taking its host-built runtime text and dropping its notes that hermes_sandboxed_agent is being deprecated, since this change removes the adapter. Update the Hermes migration notes to match NVIDIA-NeMo#5152: the Agent Server builds and uploads the runtime to /tmp/nemo-gym-hermes-runtime-<key>/python, so network-restricted task images no longer need a preinstalled runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…-in-venv-v2 Hermes and Stirrup now start a host-built standalone runtime with -E -s (NVIDIA-NeMo#5152), so their runners take the session's bin/gym-runner link rather than a venv link. Both keep their suffix-less runners. Stirrup builds the runner path from _PACKAGE_DIR when it is used, so tests that move the package move the runner too. Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
…VIDIA-NeMo#5153) * refactor(agent_utils): run the harness process supervisor in bash The supervisor uploaded into task sandboxes now needs only bash, /proc, sleep and mv, so sandboxed harnesses no longer require Python 3.8+ in the task image. It keeps the CLI and receipt contract: run the harness in its own process group, stop it at the deadline or when the stop file appears (TERM, then KILL the group), and confirm no group member runs. Stops now reach the supervisor through stop.request instead of SIGTERM, so the launch shell no longer ignores TERM and the harness keeps the default disposition. Shared receipt types and timeouts move to supervisor_client; SandboxCommand drops its python field. shell_assignments() renders runner inputs as bash source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * feat(agent_utils): add a shared bash harness runner harness_runner.sh replaces per-harness Python runners: it sources the PARAMS written by shell_assignments(), records runtime.json, applies the harness environment and runs the CLI with file-backed stdio. With events set, it stamps each JSON stdout line with the time it was seen, reading a file so a detached tool holding stdout cannot stall it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * docs(agent_utils): describe the bash supervisor and runner; update hermes tests Point the harness guide, package docs and SandboxSession docstring at process_supervisor.sh and harness_runner.sh, drop the Python-in-sandbox requirement and the subreaper drain the bash supervisor no longer performs. Hermes tests use the bash supervisor. The runner stamps a final unterminated event line with the same fallback time as the others on bash < 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * test(hermes_agent): expect stop requests as the stop file, not a signal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(kilocode): run the harness without Python in the sandbox Kilo sessions now run through the shared bash harness runner with PARAMS written by shell_assignments(); sandbox_runner.py is gone. A short setup string removes a stale plugin receipt and records the task's HOME/XDG for the task-environment plugin, unset_env drops inherited KILO_*/OPENCODE_*/ OpenAI variables, and the exit code comes from the cleanup receipt. Path checks are POSIX shell, the bootstrap installs only bash, and the installer drops its Python checks. The SQLite snapshot moves host-side: kilo.db and its WAL files are downloaded and backed up locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(claude_code_agent): run the harness without Python in the sandbox Claude sessions now launch through the shared bash harness_runner.sh with params.sh instead of sandbox_runner.py and input.json. The offline runtime installer is a bash script with the same SHA-256, version and lock semantics, so task images need bash and coreutils but no Python. Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(openclaw_agent): run the harness without Python in the sandbox Launch OpenClaw through the shared bash harness_runner.sh with PARAMS from shell_assignments() and a staged prompt file, replacing sandbox_runner.py. The workdir overlap check is POSIX sh, session setup installs only a missing bash, and the runtime installer drops its Python checks (apk now reports whether musl images have libstdc++; the prepare step already guards the runtime path against the task workdir). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(opencode): run the harness without Python in the sandbox Task images now need only bash. The shared harness_runner.sh replaces sandbox_runner.py: PARAMS carry the CLI argv, environment and stdio files, and a short setup records the task HOME/XDG directories for the shell.env hook, removes a stale initialization receipt and creates the private CLI directories. The SQLite snapshot moves to the agent server: after confirmed cleanup the adapter downloads opencode.db (and its -wal/-shm files when present) and runs the same export there, producing the same export.json and observations.db. The bootstrap no longer installs python3 and checks workdir overlap with POSIX shell, and the runtime installer serializes with flock, or a reclaimable lock directory without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(pi_agent): run the harness without Python in the sandbox Pi now runs under the shared bash harness runner: the adapter stages params.sh and prompt.txt, and the runner records [seen_at, event] lines. Session setup no longer installs python3, the runtime installer checks for the musl C++ library by path instead of with ctypes, and the host parser skips recorded lines that are not [number, object] records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(opencode): keep pre-port outcomes when the session database is unavailable A missing or undownloadable session database again leaves no export, so collection reports "OpenCode sandbox runner returned no valid result" with the output log, as when export.json was missing before. The export no longer depends on copying the observation store: a failed copy, like the failed observations.db download before, only adds observation gaps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * style: format bash supervisor tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(agent): adapt GitHub-only harness paths to the bash supervisor The bash supervisor no longer takes an interpreter, so NOOA drops the removed SandboxCommand.python field (its own pinned Python still runs its worker), and the Codex worker keeps its Python runner under the bash supervisor: its sandbox-local 429 retry proxy still needs Python. OpenCode drops sandbox_python_executable; sandbox_path_prefix now reaches the CLI through the runner setup. Tests and READMEs name process_supervisor.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): keep runner events on old bash and ignore inherited runner names On bash < 4.2 printf has no strftime, so every event was stamped with an empty time and the host parsers dropped them all; fall back to date. Strip a trailing CR before recording a JSON line. Inherited variables named like runner PARAMS (setup, events, stdout, ...) no longer configure the runner or leak into the CLI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): stop tools that left the harness group and harden the bash supervisor A stopped CLI's tools that run in their own session (OpenCode and Codex start every shell tool that way) kept writing to the task after a confirmed receipt. Before stopping the harness, the supervisor now records its descendants outside its process group from /proc and kills them, and the groups they lead, with the harness; confirmation waits for them too. Services orphaned before the stop are still left for the sandbox owner. The supervisor also: - reads each /proc/PID/stat whole and never evaluates its fields as arithmetic, so a process name holding a newline cannot run code or hide a group member; - refuses to launch without a readable /proc, and counts unreadable process state as running; - bounds the wait for a KILLed harness and writes a negative receipt instead; - exits on a missing option value or separator instead of looping or running part of the command, and ignores an inherited errexit. Launches render fixed-point seconds, and a deadline shorter than the supervisor's centisecond resolution expires before launch instead of leaving no receipt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): give the events-mode CLI default interrupts and no runner descriptors Without job control bash starts an async command with INT and QUIT ignored, and the CLI's tools inherited that. The CLI also inherited the runner's read and events descriptors, so a tool could write raw lines into events.jsonl. Reset the two signals (bash 5 honours the reset) and close both descriptors for the CLI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): make installer locks without flock crash-safe and portable to older bash Without flock, the Claude installer never reclaimed the lock of a killed installer, and OpenCode's reclaim could delete a lock another installer had just taken (or skip an owner killed before it wrote its PID). Both now take a symlink that names the owner by PID and start time, and remove a dead owner's lock only under a second lock after re-reading it. The Claude installer lowercases the trusted digest without bash 4 expansions. The Pi installer no longer fails under set -u on bash < 4.4 when it needs no packages, and checks every default musl library directory for libstdc++. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(opencode,kilocode): close host-side SQLite connections sqlite3's context manager commits but does not close, so a failed export kept the copied databases open while the snapshot directory was removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * docs(agent_utils): describe the stop-time cleanup of tools in their own sessions The receipt docstring, the harness guide and the Codex, Pi, OpenCode, OpenClaw, Hermes and Terminal-Bench READMEs now describe what the bash supervisor does: it kills the harness group and, after a forced stop, the descendants that ran in other process groups when the stop began; processes that left the harness earlier stay with the sandbox owner. The receipt documents shell exit statuses. The Kilo and local-provider tests cover a tool in its own session again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): reject shell words bash cannot hold; tighten Hermes stop test shell_assignments() now raises for NUL and non-string words, which bash would silently drop or which raised an opaque TypeError. The Hermes close test stops the supervisor through the stop file alone, as production does, and the sandboxed Hermes README states that the supervisor needs the task image's bash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * style: apply ruff formatting to the ported tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * test(pi_agent): stop in-flight detached tools and keep services from completed tools The bash port's tests asserted that a tool still running in its own session survives a close, and modelled a completed tool's service as a direct child of the CLI. With the supervisor's stop-time cleanup, an in-flight detached tool is stopped again, as before the port; a service left by a tool that already exited still survives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * docs(agent_utils): say which harnesses keep a Python shim in the sandbox The harness guide said Codex starts its CLI through harness_runner.sh. On main Codex keeps its Python sandbox_runner.py for its sandbox-local retry proxy, so it still needs the task image's Python; Hermes, Stirrup and NOOA run their Python shims on interpreters they install privately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): crash-safe installer queue, checked version probe, JSON-safe task dirs Address review findings on the bash runtime: - Without flock, the OpenCode and Claude installers now queue on numbered owner links that are never removed or reused, and skip a number whose owner is done or gone. There is no second reclaim lock left to strand, and an installer killed at any point cannot block or release another. - A preinstalled OpenCode binary must exit successfully from --version, not only print the version, before it replaces the cached runtime. - OpenCode's and Kilo's task-env.json writers escape every JSON control character, so task HOME/XDG paths with tabs or newlines stay valid JSON. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * test(opencode,openclaw): pass the bash runner's payload to the neutral-name tests The pkill tests merged from main called payload() with a timeout, which only the Python runner's input carried; the bash runner takes its timeout from execute(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * test(agent_utils): find the stdlib runner as the tool's parent The pkill test from main assumed the supervisor's only child is the runner. The bash supervisor also forks its polling sleeps, so take the runner as the parent of the tool it started and check that it is a supervisor child. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): reach processes that leave the harness during a stop Review found that one descendant snapshot before TERM let a confirmed receipt leave processes running: a TERM handler that runs `setsid ... &` and exits, or a tool that keeps forking new sessions during the grace period. - The supervisor gives the harness a NEMO_GYM_SUPERVISOR environment marker unique to this run. On a forced stop it scans /proc when the stop begins, on every grace and drain round, and before the final KILL. It reaches the harness group, descendants of the harness, of its group and of reached processes, and processes carrying the marker. Marked processes that had already left the harness when the stop began are preserved as services, as the subreaper on main did. The Codex runner passes the marker through its env allowlist. - A recorded group is killed or awaited only while its ID is free or still held by the recorded leader (same start time), so a reused group ID is not hit. - Without a process group of its own, the supervisor restarts itself as a job in a new group, so a provider's group KILL cannot end it before the receipt. The shell left behind passes on TERM and the exit status. - Docstrings and READMEs state what a forced stop reaches and what it does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): linear CR strip, errexit guard and distinct runner statuses - emit() strips a trailing CR without `${1%$'\r'}`, which tried the suffix at every position and took seconds per MB, long enough for Pi's agent_end event to outlast the deadline. - The runner resets errexit, nounset, pipefail and IFS, so a BASH_ENV with `set -e` no longer stops it at the first non-JSON line. - A failed exec redirection no longer falls through into events mode or launches the CLI. The runner exits 125 for its own failures (bad cwd, an exit in setup) and 126 or 127 when the CLI could not be started. - Drop `trap - INT QUIT`: it did not change what the CLI inherits, and its test passed without it. - Kilo tells runner failures apart from Kilo's own exit status, reports no exit code for a Kilo the runner never started, and none for a cancelled run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): clean up staged OpenCode copies; bootstrap bash for Claude Code - The OpenCode installer removes a partly staged binary from its single EXIT trap, however it exits, and a cached binary must exit successfully from --version to be reused. - Claude Code setup installs a missing bash on root images with apk or apt-get, as the other bash-runner adapters do, and otherwise fails with a message that names bash instead of a bare 127. - Reword "native" in the Claude installer docs, docstring and a test name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * docs(agent-server): describe sandbox Python and uv paths as exceptions - Codex's task-image Python, stdlib_runner, and Hermes and Stirrup building a uv environment in the sandbox are exceptions being removed; new Python harnesses bring a runtime prepared on the host, as NOOA and hermes_sandboxed_agent do. - The runner row names the utilities it needs and its exit statuses. - The migration note lists the removed python= parameters, the bash requirement, and the receipt and runner status changes. - Harness termination describes what a forced stop reaches, the marker, and supervisor.pid when the supervisor leaves the provider group. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * test(agent_utils): find the supervisor through the runner after it leaves the group The pkill test launches without a new session, so the supervisor now restarts itself in its own process group and supervisor.pid names the process that started it. Take the supervisor as the runner's parent, accept it as the recorded PID or that PID's child, and check that neither bash process matches the python kills. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): build Hermes and Stirrup runtimes on the host Hermes and Stirrup uploaded uv into the task sandbox and built a virtual environment there from the network, with the task workdir as cwd and without --no-config. A task's uv.toml or [tool.uv] could change the install, uv's cache and managed Python landed in the task's HOME, and --python 3.13 could bind the image's own interpreter. Add nemo_gym/agent_utils/sandbox_python.py. On the Agent Server it downloads a pinned python-build-standalone CPython for the sandbox's architecture and C library, checks its SHA-256, and installs the harness's requirements into a site-packages archive with uv --no-config --python-platform --only-binary :all:, constrained to the agent environment's versions. Only direct-URL requirements, such as Hermes at a commit, are built from source. Both archives are cached on the host. install_python_runtime.sh unpacks them under /tmp, checks the harness import with HOME inside the runtime directory, and publishes the runtime with an atomic rename, so concurrent seeds share one runtime and a runtime that fails its check is replaced. The sandbox needs sh and tar, but no network, uv or Python. Remove uv_bootstrap.py and Hermes's uv_cache_dir option, update the tests, and drop Hermes and Stirrup from the harness guide's exceptions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * perf(agent_utils): make the supervisor's forced stop cheaper on busy hosts A forced stop scans every process in /proc several times, and each /proc/PID/stat parse used a longest-prefix match (quadratic in bash) and three regex compilations. On a host with about 1,100 processes a stop took 1.65 s, so Hermes's close test, which allows 2 s and polls for the receipt once a second, failed there while CI passed. Parse with a shortest-prefix loop and glob digit checks, let the forced drain take liveness from its scan instead of a second /proc pass, and skip the extra scan once the harness has exited (the drain scans next). A stop on the same host now takes about 0.55 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(codex): run sandboxed Codex without the task image's Python Codex was the last CLI harness whose sandbox worker ran on the task image's python3, only because its HTTP 429 retry proxy was Python. Run Codex through the shared harness_runner.sh, behind a small Node launcher (sandbox_launcher.mjs) on the Node that the Codex runtime already installs. The launcher keeps the proxy's behaviour (two 429 retries honouring Retry-After up to 30 s, attempt records, transport errors), the environment allowlist and TERM forwarding, and exits with Codex's status as a shell would. The native-session snapshot moves to the same launcher. The bootstrap, workdir overlap check and installer no longer need python3. With no remaining user, stdlib_runner and the bin/gym-runner interpreter link are removed, as the harness docs promised. Codex tests that need Node install it at collection on Linux, through setup_codex.ensure_node(), so CI keeps running them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): close gaps in the flock-less installer queue and root-workdir check Review on NVIDIA-NeMo#5151 found three gaps in the bash ports: - The Claude installer set its queue ticket before linking it, so an installer stopped after losing a number marked the winner's ticket done and let the next installer run beside it. It now takes the ticket only once its link exists, as OpenCode's installer does. - Both flock-less queues treated a killed but unreaped owner as live, so later installers waited until their timeout. A zombie or dead process now has no identity and no longer holds its number. - OpenClaw's overlap check missed a workdir that resolves to /, such as "/.", because "/" plus a separator is "//". Both sides now drop a trailing slash first; the error names / when it is the owned path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(agent_utils): run the sandbox Python installer with bash The supervisor and the harness runner are bash scripts, but the installer ran with POSIX sh, so the sandbox's script requirements differed by step. Hermes and Stirrup need bash for the supervisor anyway, so an image without bash now fails at seed, with "bash: not found", instead of at the first activation. The installer sets errexit, nounset and pipefail explicitly, so an inherited SHELLOPTS cannot change how it fails, and it uses bash tests. It still publishes with the new runtime's os.rename: mv would move the fresh runtime inside one a concurrent seed published first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(codex): keep events with Unicode line separators; record broken 429 bodies Review on NVIDIA-NeMo#5153: - The bash runner copies Codex's JSON event lines verbatim, while the removed Python worker re-serialized them with escaped non-ASCII. Codex leaves U+0085, U+2028 and U+2029 unescaped inside strings, so splitlines() cut such an event in two and its message was lost while the turn still reported completed. Split records on "\n" only and skip blank lines. - A retryable 429 whose body timed out or was cut short destroyed the response with no error listener, which could end the launcher and Codex with it. The drain now catches the error and records it as the attempt's transport_error before retrying. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): serialize runtime publication and harden the host build Address the review of NVIDIA-NeMo#5152, including the two NeMo-CI review threads. Sandbox side: - install_python_runtime.sh checks, sets aside and publishes the runtime under a lock on the runtime directory, held by the fresh runtime's own Python. A seed whose check of a missing or broken runtime failed could move aside, and its EXIT trap delete, a runtime that another seed had just published and returned success on. - install() removes its .setup-<uuid> directory when creating it or an upload fails, times out or is cancelled before the installer, whose EXIT trap owns it from then on, starts. - Hermes and Stirrup start their runner with -s. The runtime is not a virtual environment, so the task HOME's user site-packages would otherwise come ahead of the pinned packages. Agent Server side: - The archives live under sandbox_python in Gym's cache_dir instead of a hard-coded ~/.cache path resolved at import. - The Python build download honors HTTPS_PROXY and UV_PYTHON_INSTALL_MIRROR, stops after 3 attempts instead of retrying until the seed times out, streams to disk in a thread, and reports HTTP errors as RuntimeError. - Concurrent seeds share one build and its failure; a cancelled seed no longer kills the build the others wait on. - The build tree is removed in a thread, and uv runs with --link-mode copy so a host UV_LINK_MODE=symlink cannot cache an archive of links into the host's uv cache. - An install timeout names the harness and the limit. Docs: Stirrup's runtime needs glibc 2.28 or newer and does not support musl (imageio-ffmpeg has no musllinux wheel; the Pillow override has only manylinux_2_28 wheels). The READMEs and harness guide give the cache location, proxy and mirror settings, and the -s requirement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): clean up interrupted local runtime installs Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): isolate harness Python and Hermes startup state Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(codex): honor musl library search paths during setup Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): prefer glibc when musl is also installed Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): follow main's import rules in the sandbox Python runtime main now checks import layering and moved paths. sandbox_python.py read CACHE_DIR_KEY_NAME, maybe_get_global_config_dict and request through old paths, and it and its test imported AsyncSandbox and SandboxSpec through the nemo_gym.sandbox package. Import them from their current, defining modules. uv_bootstrap is deleted on purpose, so drop it from the recorded import surface. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(codex): keep sandbox records whole and cancel abandoned model streams - Host parsing of native session records now splits lines only at \n, \r\n and \r, as the launcher's snapshot does. str.splitlines() also split at U+0085, U+2028 and U+2029, which Codex leaves unescaped inside JSON strings, so a tool output holding one lost its record and left tool timing unknown. - The host now skips an event record nested too deeply to parse. The Python worker dropped such lines in the sandbox; the bash runner copies them verbatim, so the RecursionError failed the response. - The forwarder now cancels the upstream request when Codex hung up while it waited for the response headers. The close listener was registered only after the headers arrived, so the abandoned stream ran to its end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(codex): run the launcher through a symlinked path; snapshot long rollouts - The launcher compared import.meta.url, which Node resolves to the real path, with the path it was started by. Under a symlinked session directory (a task image whose /tmp is a symlink) it skipped both Codex and the snapshot and exited 0, so every rollout failed without an explanation. - The snapshot spread each rollout file into Array.push, which overflows the call stack on a file of a few hundred thousand lines and loses the native transcript. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * test(codex): fail the Linux run when Node cannot be installed The test conftest only warned when ensure_node() failed, so a failed Node download in CI would skip the launcher and runner tests and still pass. Let the error fail the run, as the other servers' install hooks do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> --------- Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> Co-authored-by: Hemil Desai <hemildesai@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…NeMo#5327) * refactor(agent_utils): run the harness process supervisor in bash The supervisor uploaded into task sandboxes now needs only bash, /proc, sleep and mv, so sandboxed harnesses no longer require Python 3.8+ in the task image. It keeps the CLI and receipt contract: run the harness in its own process group, stop it at the deadline or when the stop file appears (TERM, then KILL the group), and confirm no group member runs. Stops now reach the supervisor through stop.request instead of SIGTERM, so the launch shell no longer ignores TERM and the harness keeps the default disposition. Shared receipt types and timeouts move to supervisor_client; SandboxCommand drops its python field. shell_assignments() renders runner inputs as bash source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * feat(agent_utils): add a shared bash harness runner harness_runner.sh replaces per-harness Python runners: it sources the PARAMS written by shell_assignments(), records runtime.json, applies the harness environment and runs the CLI with file-backed stdio. With events set, it stamps each JSON stdout line with the time it was seen, reading a file so a detached tool holding stdout cannot stall it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * docs(agent_utils): describe the bash supervisor and runner; update hermes tests Point the harness guide, package docs and SandboxSession docstring at process_supervisor.sh and harness_runner.sh, drop the Python-in-sandbox requirement and the subreaper drain the bash supervisor no longer performs. Hermes tests use the bash supervisor. The runner stamps a final unterminated event line with the same fallback time as the others on bash < 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * test(hermes_agent): expect stop requests as the stop file, not a signal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(kilocode): run the harness without Python in the sandbox Kilo sessions now run through the shared bash harness runner with PARAMS written by shell_assignments(); sandbox_runner.py is gone. A short setup string removes a stale plugin receipt and records the task's HOME/XDG for the task-environment plugin, unset_env drops inherited KILO_*/OPENCODE_*/ OpenAI variables, and the exit code comes from the cleanup receipt. Path checks are POSIX shell, the bootstrap installs only bash, and the installer drops its Python checks. The SQLite snapshot moves host-side: kilo.db and its WAL files are downloaded and backed up locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(claude_code_agent): run the harness without Python in the sandbox Claude sessions now launch through the shared bash harness_runner.sh with params.sh instead of sandbox_runner.py and input.json. The offline runtime installer is a bash script with the same SHA-256, version and lock semantics, so task images need bash and coreutils but no Python. Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(openclaw_agent): run the harness without Python in the sandbox Launch OpenClaw through the shared bash harness_runner.sh with PARAMS from shell_assignments() and a staged prompt file, replacing sandbox_runner.py. The workdir overlap check is POSIX sh, session setup installs only a missing bash, and the runtime installer drops its Python checks (apk now reports whether musl images have libstdc++; the prepare step already guards the runtime path against the task workdir). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(opencode): run the harness without Python in the sandbox Task images now need only bash. The shared harness_runner.sh replaces sandbox_runner.py: PARAMS carry the CLI argv, environment and stdio files, and a short setup records the task HOME/XDG directories for the shell.env hook, removes a stale initialization receipt and creates the private CLI directories. The SQLite snapshot moves to the agent server: after confirmed cleanup the adapter downloads opencode.db (and its -wal/-shm files when present) and runs the same export there, producing the same export.json and observations.db. The bootstrap no longer installs python3 and checks workdir overlap with POSIX shell, and the runtime installer serializes with flock, or a reclaimable lock directory without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(pi_agent): run the harness without Python in the sandbox Pi now runs under the shared bash harness runner: the adapter stages params.sh and prompt.txt, and the runner records [seen_at, event] lines. Session setup no longer installs python3, the runtime installer checks for the musl C++ library by path instead of with ctypes, and the host parser skips recorded lines that are not [number, object] records. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(opencode): keep pre-port outcomes when the session database is unavailable A missing or undownloadable session database again leaves no export, so collection reports "OpenCode sandbox runner returned no valid result" with the output log, as when export.json was missing before. The export no longer depends on copying the observation store: a failed copy, like the failed observations.db download before, only adds observation gaps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * style: format bash supervisor tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(agent): adapt GitHub-only harness paths to the bash supervisor The bash supervisor no longer takes an interpreter, so NOOA drops the removed SandboxCommand.python field (its own pinned Python still runs its worker), and the Codex worker keeps its Python runner under the bash supervisor: its sandbox-local 429 retry proxy still needs Python. OpenCode drops sandbox_python_executable; sandbox_path_prefix now reaches the CLI through the runner setup. Tests and READMEs name process_supervisor.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): keep runner events on old bash and ignore inherited runner names On bash < 4.2 printf has no strftime, so every event was stamped with an empty time and the host parsers dropped them all; fall back to date. Strip a trailing CR before recording a JSON line. Inherited variables named like runner PARAMS (setup, events, stdout, ...) no longer configure the runner or leak into the CLI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): stop tools that left the harness group and harden the bash supervisor A stopped CLI's tools that run in their own session (OpenCode and Codex start every shell tool that way) kept writing to the task after a confirmed receipt. Before stopping the harness, the supervisor now records its descendants outside its process group from /proc and kills them, and the groups they lead, with the harness; confirmation waits for them too. Services orphaned before the stop are still left for the sandbox owner. The supervisor also: - reads each /proc/PID/stat whole and never evaluates its fields as arithmetic, so a process name holding a newline cannot run code or hide a group member; - refuses to launch without a readable /proc, and counts unreadable process state as running; - bounds the wait for a KILLed harness and writes a negative receipt instead; - exits on a missing option value or separator instead of looping or running part of the command, and ignores an inherited errexit. Launches render fixed-point seconds, and a deadline shorter than the supervisor's centisecond resolution expires before launch instead of leaving no receipt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): give the events-mode CLI default interrupts and no runner descriptors Without job control bash starts an async command with INT and QUIT ignored, and the CLI's tools inherited that. The CLI also inherited the runner's read and events descriptors, so a tool could write raw lines into events.jsonl. Reset the two signals (bash 5 honours the reset) and close both descriptors for the CLI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): make installer locks without flock crash-safe and portable to older bash Without flock, the Claude installer never reclaimed the lock of a killed installer, and OpenCode's reclaim could delete a lock another installer had just taken (or skip an owner killed before it wrote its PID). Both now take a symlink that names the owner by PID and start time, and remove a dead owner's lock only under a second lock after re-reading it. The Claude installer lowercases the trusted digest without bash 4 expansions. The Pi installer no longer fails under set -u on bash < 4.4 when it needs no packages, and checks every default musl library directory for libstdc++. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(opencode,kilocode): close host-side SQLite connections sqlite3's context manager commits but does not close, so a failed export kept the copied databases open while the snapshot directory was removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * docs(agent_utils): describe the stop-time cleanup of tools in their own sessions The receipt docstring, the harness guide and the Codex, Pi, OpenCode, OpenClaw, Hermes and Terminal-Bench READMEs now describe what the bash supervisor does: it kills the harness group and, after a forced stop, the descendants that ran in other process groups when the stop began; processes that left the harness earlier stay with the sandbox owner. The receipt documents shell exit statuses. The Kilo and local-provider tests cover a tool in its own session again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): reject shell words bash cannot hold; tighten Hermes stop test shell_assignments() now raises for NUL and non-string words, which bash would silently drop or which raised an opaque TypeError. The Hermes close test stops the supervisor through the stop file alone, as production does, and the sandboxed Hermes README states that the supervisor needs the task image's bash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * style: apply ruff formatting to the ported tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * test(pi_agent): stop in-flight detached tools and keep services from completed tools The bash port's tests asserted that a tool still running in its own session survives a close, and modelled a completed tool's service as a direct child of the CLI. With the supervisor's stop-time cleanup, an in-flight detached tool is stopped again, as before the port; a service left by a tool that already exited still survives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * docs(agent_utils): say which harnesses keep a Python shim in the sandbox The harness guide said Codex starts its CLI through harness_runner.sh. On main Codex keeps its Python sandbox_runner.py for its sandbox-local retry proxy, so it still needs the task image's Python; Hermes, Stirrup and NOOA run their Python shims on interpreters they install privately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): crash-safe installer queue, checked version probe, JSON-safe task dirs Address review findings on the bash runtime: - Without flock, the OpenCode and Claude installers now queue on numbered owner links that are never removed or reused, and skip a number whose owner is done or gone. There is no second reclaim lock left to strand, and an installer killed at any point cannot block or release another. - A preinstalled OpenCode binary must exit successfully from --version, not only print the version, before it replaces the cached runtime. - OpenCode's and Kilo's task-env.json writers escape every JSON control character, so task HOME/XDG paths with tabs or newlines stay valid JSON. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * test(opencode,openclaw): pass the bash runner's payload to the neutral-name tests The pkill tests merged from main called payload() with a timeout, which only the Python runner's input carried; the bash runner takes its timeout from execute(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * test(agent_utils): find the stdlib runner as the tool's parent The pkill test from main assumed the supervisor's only child is the runner. The bash supervisor also forks its polling sleeps, so take the runner as the parent of the tool it started and check that it is a supervisor child. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): reach processes that leave the harness during a stop Review found that one descendant snapshot before TERM let a confirmed receipt leave processes running: a TERM handler that runs `setsid ... &` and exits, or a tool that keeps forking new sessions during the grace period. - The supervisor gives the harness a NEMO_GYM_SUPERVISOR environment marker unique to this run. On a forced stop it scans /proc when the stop begins, on every grace and drain round, and before the final KILL. It reaches the harness group, descendants of the harness, of its group and of reached processes, and processes carrying the marker. Marked processes that had already left the harness when the stop began are preserved as services, as the subreaper on main did. The Codex runner passes the marker through its env allowlist. - A recorded group is killed or awaited only while its ID is free or still held by the recorded leader (same start time), so a reused group ID is not hit. - Without a process group of its own, the supervisor restarts itself as a job in a new group, so a provider's group KILL cannot end it before the receipt. The shell left behind passes on TERM and the exit status. - Docstrings and READMEs state what a forced stop reaches and what it does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): linear CR strip, errexit guard and distinct runner statuses - emit() strips a trailing CR without `${1%$'\r'}`, which tried the suffix at every position and took seconds per MB, long enough for Pi's agent_end event to outlast the deadline. - The runner resets errexit, nounset, pipefail and IFS, so a BASH_ENV with `set -e` no longer stops it at the first non-JSON line. - A failed exec redirection no longer falls through into events mode or launches the CLI. The runner exits 125 for its own failures (bad cwd, an exit in setup) and 126 or 127 when the CLI could not be started. - Drop `trap - INT QUIT`: it did not change what the CLI inherits, and its test passed without it. - Kilo tells runner failures apart from Kilo's own exit status, reports no exit code for a Kilo the runner never started, and none for a cancelled run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): clean up staged OpenCode copies; bootstrap bash for Claude Code - The OpenCode installer removes a partly staged binary from its single EXIT trap, however it exits, and a cached binary must exit successfully from --version to be reused. - Claude Code setup installs a missing bash on root images with apk or apt-get, as the other bash-runner adapters do, and otherwise fails with a message that names bash instead of a bare 127. - Reword "native" in the Claude installer docs, docstring and a test name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * docs(agent-server): describe sandbox Python and uv paths as exceptions - Codex's task-image Python, stdlib_runner, and Hermes and Stirrup building a uv environment in the sandbox are exceptions being removed; new Python harnesses bring a runtime prepared on the host, as NOOA and hermes_sandboxed_agent do. - The runner row names the utilities it needs and its exit statuses. - The migration note lists the removed python= parameters, the bash requirement, and the receipt and runner status changes. - Harness termination describes what a forced stop reaches, the marker, and supervisor.pid when the supervisor leaves the provider group. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * test(agent_utils): find the supervisor through the runner after it leaves the group The pkill test launches without a new session, so the supervisor now restarts itself in its own process group and supervisor.pid names the process that started it. Take the supervisor as the runner's parent, accept it as the recorded PID or that PID's child, and check that neither bash process matches the python kills. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): build Hermes and Stirrup runtimes on the host Hermes and Stirrup uploaded uv into the task sandbox and built a virtual environment there from the network, with the task workdir as cwd and without --no-config. A task's uv.toml or [tool.uv] could change the install, uv's cache and managed Python landed in the task's HOME, and --python 3.13 could bind the image's own interpreter. Add nemo_gym/agent_utils/sandbox_python.py. On the Agent Server it downloads a pinned python-build-standalone CPython for the sandbox's architecture and C library, checks its SHA-256, and installs the harness's requirements into a site-packages archive with uv --no-config --python-platform --only-binary :all:, constrained to the agent environment's versions. Only direct-URL requirements, such as Hermes at a commit, are built from source. Both archives are cached on the host. install_python_runtime.sh unpacks them under /tmp, checks the harness import with HOME inside the runtime directory, and publishes the runtime with an atomic rename, so concurrent seeds share one runtime and a runtime that fails its check is replaced. The sandbox needs sh and tar, but no network, uv or Python. Remove uv_bootstrap.py and Hermes's uv_cache_dir option, update the tests, and drop Hermes and Stirrup from the harness guide's exceptions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * perf(agent_utils): make the supervisor's forced stop cheaper on busy hosts A forced stop scans every process in /proc several times, and each /proc/PID/stat parse used a longest-prefix match (quadratic in bash) and three regex compilations. On a host with about 1,100 processes a stop took 1.65 s, so Hermes's close test, which allows 2 s and polls for the receipt once a second, failed there while CI passed. Parse with a shortest-prefix loop and glob digit checks, let the forced drain take liveness from its scan instead of a second /proc pass, and skip the extra scan once the harness has exited (the drain scans next). A stop on the same host now takes about 0.55 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(codex): run sandboxed Codex without the task image's Python Codex was the last CLI harness whose sandbox worker ran on the task image's python3, only because its HTTP 429 retry proxy was Python. Run Codex through the shared harness_runner.sh, behind a small Node launcher (sandbox_launcher.mjs) on the Node that the Codex runtime already installs. The launcher keeps the proxy's behaviour (two 429 retries honouring Retry-After up to 30 s, attempt records, transport errors), the environment allowlist and TERM forwarding, and exits with Codex's status as a shell would. The native-session snapshot moves to the same launcher. The bootstrap, workdir overlap check and installer no longer need python3. With no remaining user, stdlib_runner and the bin/gym-runner interpreter link are removed, as the harness docs promised. Codex tests that need Node install it at collection on Linux, through setup_codex.ensure_node(), so CI keeps running them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): close gaps in the flock-less installer queue and root-workdir check Review on NVIDIA-NeMo#5151 found three gaps in the bash ports: - The Claude installer set its queue ticket before linking it, so an installer stopped after losing a number marked the winner's ticket done and let the next installer run beside it. It now takes the ticket only once its link exists, as OpenCode's installer does. - Both flock-less queues treated a killed but unreaped owner as live, so later installers waited until their timeout. A zombie or dead process now has no identity and no longer holds its number. - OpenClaw's overlap check missed a workdir that resolves to /, such as "/.", because "/" plus a separator is "//". Both sides now drop a trailing slash first; the error names / when it is the owned path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(agent_utils): run the sandbox Python installer with bash The supervisor and the harness runner are bash scripts, but the installer ran with POSIX sh, so the sandbox's script requirements differed by step. Hermes and Stirrup need bash for the supervisor anyway, so an image without bash now fails at seed, with "bash: not found", instead of at the first activation. The installer sets errexit, nounset and pipefail explicitly, so an inherited SHELLOPTS cannot change how it fails, and it uses bash tests. It still publishes with the new runtime's os.rename: mv would move the fresh runtime inside one a concurrent seed published first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(codex): keep events with Unicode line separators; record broken 429 bodies Review on NVIDIA-NeMo#5153: - The bash runner copies Codex's JSON event lines verbatim, while the removed Python worker re-serialized them with escaped non-ASCII. Codex leaves U+0085, U+2028 and U+2029 unescaped inside strings, so splitlines() cut such an event in two and its message was lost while the turn still reported completed. Split records on "\n" only and skip blank lines. - A retryable 429 whose body timed out or was cut short destroyed the response with no error listener, which could end the launcher and Codex with it. The drain now catches the error and records it as the attempt's transport_error before retrying. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): serialize runtime publication and harden the host build Address the review of NVIDIA-NeMo#5152, including the two NeMo-CI review threads. Sandbox side: - install_python_runtime.sh checks, sets aside and publishes the runtime under a lock on the runtime directory, held by the fresh runtime's own Python. A seed whose check of a missing or broken runtime failed could move aside, and its EXIT trap delete, a runtime that another seed had just published and returned success on. - install() removes its .setup-<uuid> directory when creating it or an upload fails, times out or is cancelled before the installer, whose EXIT trap owns it from then on, starts. - Hermes and Stirrup start their runner with -s. The runtime is not a virtual environment, so the task HOME's user site-packages would otherwise come ahead of the pinned packages. Agent Server side: - The archives live under sandbox_python in Gym's cache_dir instead of a hard-coded ~/.cache path resolved at import. - The Python build download honors HTTPS_PROXY and UV_PYTHON_INSTALL_MIRROR, stops after 3 attempts instead of retrying until the seed times out, streams to disk in a thread, and reports HTTP errors as RuntimeError. - Concurrent seeds share one build and its failure; a cancelled seed no longer kills the build the others wait on. - The build tree is removed in a thread, and uv runs with --link-mode copy so a host UV_LINK_MODE=symlink cannot cache an archive of links into the host's uv cache. - An install timeout names the harness and the limit. Docs: Stirrup's runtime needs glibc 2.28 or newer and does not support musl (imageio-ffmpeg has no musllinux wheel; the Pillow override has only manylinux_2_28 wheels). The READMEs and harness guide give the cache location, proxy and mirror settings, and the -s requirement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): clean up interrupted local runtime installs Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): isolate harness Python and Hermes startup state Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * refactor(sandbox): remove unused stdlib runner compatibility Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(codex): honor musl library search paths during setup Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent): prefer glibc when musl is also installed Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(agent_utils): follow main's import rules in the sandbox Python runtime main now checks import layering and moved paths. sandbox_python.py read CACHE_DIR_KEY_NAME, maybe_get_global_config_dict and request through old paths, and it and its test imported AsyncSandbox and SandboxSpec through the nemo_gym.sandbox package. Import them from their current, defining modules. uv_bootstrap is deleted on purpose, so drop it from the recorded import surface. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(codex): keep sandbox records whole and cancel abandoned model streams - Host parsing of native session records now splits lines only at \n, \r\n and \r, as the launcher's snapshot does. str.splitlines() also split at U+0085, U+2028 and U+2029, which Codex leaves unescaped inside JSON strings, so a tool output holding one lost its record and left tool timing unknown. - The host now skips an event record nested too deeply to parse. The Python worker dropped such lines in the sandbox; the bash runner copies them verbatim, so the RecursionError failed the response. - The forwarder now cancels the upstream request when Codex hung up while it waited for the response headers. The close listener was registered only after the headers arrived, so the abandoned stream ran to its end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(codex): run the launcher through a symlinked path; snapshot long rollouts - The launcher compared import.meta.url, which Node resolves to the real path, with the path it was started by. Under a symlinked session directory (a task image whose /tmp is a symlink) it skipped both Codex and the snapshot and exited 0, so every rollout failed without an explanation. - The snapshot spread each rollout file into Array.push, which overflows the call stack on a file of a few hundred thousand lines and loses the native transcript. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> --------- Signed-off-by: Hemil Desai <hemildesai@users.noreply.github.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> Co-authored-by: Hemil Desai <hemildesai@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
What and why
ananthsub pointed out on #5094 (Hermes, Stirrup, docs) that Hermes and Stirrup still upload
uvinto the task sandbox and build a virtual environment there from the network. Hermes ran it with the task workdir ascwd, without--no-config,UV_CACHE_DIRorUV_PYTHON_INSTALL_DIR. That meant three problems:uv.tomlor[tool.uv]could change the install;--python 3.13could bind the image's ownpython3.13.Stirrup had the same problem but set the two env vars; it still lacked
--no-config. #5151 lists both as known exceptions. This PR removes them.nemo_gym/agents/utils/sandbox_python.py(new):SandboxPythonRuntime. On the Agent Server host it:probe_sandbox_platform()), preferring glibc reported bygetconforlddover an optionally installed musl loader;20260805,install_only_stripped) for that platform and checks its published SHA-256 on every use. The digests come from the release'sSHA256SUMS, whoseinstall_onlygnu entries match NOOA's existing pins. musl and aarch64 are included;site-packagesarchive withuv pip install --no-config --target … --python-platform <arch>-manylinux_2_<N>|<arch>-unknown-linux-musl --python-version 3.13.14 --only-binary :all:.--no-binary hermes-agentfor Hermes pinned to a commit). Every other package must publish a wheel for the sandbox's platform.lxml[html_clean]with plainlxmland dropslxml_html_clean. Adapter-specific overrides are an explicit field: Stirrup passes itsoverrides.txt(Pillow).sandbox_pythonin Gym'scache_dir. Concurrent cold seeds in a process share one build and its outcome, a seed that is cancelled leaves the build running for the others, writes are atomic across processes, and a failed build is retried by the next seed.HTTPS_PROXY, fromUV_PYTHON_INSTALL_MIRRORwhen set, with at most 3 attempts.nemo_gym/agents/utils/install_python_runtime.sh(new, runs in the sandbox withbash, like the supervisor and the harness runner, plustar/gzip andrm):.setup-<uuid>directory under the runtime directory;python -I,cwd=/, andHOMEinside the runtime directory (importing Hermes writes~/.hermes);bash, before the shell sets its trap). Remote timeout and provider-error results retain the installer's ownership because exec may still be running;rename(2). Checking the runtime in place, setting it aside and publishing run under an exclusive lock on the runtime directory, held by the fresh runtime's own Python, so concurrent seeds share whichever runtime is published first. A runtime that fails its check (for example one from another platform) is set aside and replaced, and a runtime another seed accepted is never moved.The runtime lives at
/tmp/nemo-gym-<harness>-runtime-<key>/python. The key hashes the Python pin, the requirements, the overrides and the agent environment's versions, so a runtime left in the sandbox is reused only when it was built from the same inputs. Seed-time execs run from/, and nothing is written into the task's workdir or HOME.Adapters.
RUNTIME.is_installed()/RUNTIME.install()at seed. Activation uses the absolute runtime interpreter with-E -sand an absolute harness-owned script path. This ignores taskPYTHON*settings and user site-packages while retaining trusted sibling imports. No venv is activated, and the task's Python environment remains available to tool subprocesses.HERMES_HOMEand Gym model routing before the first framework import. This keeps import-time state paths and.envloading out of the task's home and routes import-time MCP sampling through Gym. Seed import checks also override inheritedHERMES_HOMEunder the runtime's privateHOME.uv_bootstrap.pyand its tests are removed; nothing else used it.uv_cache_diroption is removed.Docs. The harness guide no longer lists Hermes and Stirrup as exceptions; Codex stays. Its shared-module table and installation-contract example now describe
sandbox_python.py, and the paragraph that recommended NOOA now points atSandboxPythonRuntime(NOOA's runtime still installs packages from the network in the sandbox). The Hermes and Stirrup READMEs describe the new requirements. The guide explicitly markshermes_sandboxed_agentas being deprecated, removes its read-only mounted runtime and supervisor migration examples, and directs new Hermes development tohermes_agentandSandboxPythonRuntime.No issue was filed; this is the follow-up named in #5151's "Known exceptions".
Compatibility
uvor Python needed for seeding;bashandtarwith gzip are. glibc ≥ 2.17 or musl; x86_64 or aarch64. Stirrup needs glibc ≥ 2.28 and does not support musl:imageio-ffmpegpublishes no musllinux wheel, and the Pillow version in itsoverrides.txtpublishes only manylinux_2_28 wheels. The supervisor already neededbash, so an image without it now fails at seed (bash: not found, exit 127) instead of at the first activation. Hermes still needs the Model Server's address, as before.uv(it already needed it for the old path) and access to GitHub releases and the package index.HTTPS_PROXYapplies to both.UV_PYTHON_INSTALL_MIRRORreplaces the GitHub releases base for HTTP(S) mirrors (file://is not supported), and uv's index settings (UV_*env vars, which--no-configstill honors) select an index mirror. A host without access can be given the Python archive in the cache directory.uv_cache_diris removed.BaseServerConfigignores unknown keys, so configs that set it still load./tmp/nemo-gym-hermes-runtime-<commit>/venv→/tmp/nemo-gym-hermes-runtime-<key>/python. An image that baked the old venv path no longer short-circuits the install.Validation
Import-conflict reconciliation —
fb963551fmainat972def79fafter the package refactors. KeptSandboxPythonRuntimealongside main's new agent exports, and importedraises_agent_failurethrough its new public facade in the Stirrup tests. The runtime implementation, installer, and their regression tests are unchanged from the prior validated revision.pytest tests/unit_tests/test_sandbox_python.py tests/unit_tests/test_sandbox.py tests/unit_tests/test_local_provider.py tests/unit_tests/test_public_api.py tests/unit_tests/test_public_api_facades.py tests/unit_tests/test_moved_paths.py tests/unit_tests/test_import_linter_coverage.py tests/unit_tests/test_fern_docs_links.py; 352 passed forpytest responses_api_agents/hermes_agent/tests; 172 passed forpytest responses_api_agents/stirrup_agent/tests. Each invocation used-q -o addopts= -o asyncio_mode=auto -o log_cli=false -p pytest_asyncio.plugin -p no:cacheproviderwith plugin autoload disabled. 1,661 tests passed overall.pre-commit run --all-filesandgit diff --check: pass.cd fern && npm run check -- --warnings: 0 errors, with the authenticated missing-redirect check skipped.Final main reconciliation and Linux validation —
ff4bf1b58mainatbedd1aaeaafter feat(eval): report causal failures across service boundaries #5072 landed. Preserved bothAsyncSandbox.provider_nameand the new lifecycle observation properties. Updated the Stirrup runtime-failure assertion to use the new agent-route failure wrapper while retaining the actual missing-moviepycause.git archiveof this commit on Linux/Python 3.13.14/OpenAI 2.44.0, checked the archive SHA-256 after transfer, and verified that loaded Gym and adapter modules came from that snapshot.test_sandbox_python.py,test_sandbox.py,test_local_provider.py,test_public_api.py,test_public_api_facades.py,test_moved_paths.py,test_import_linter_coverage.py,test_fern_docs_links.py,test_sandbox_lifecycle_observation.py,test_agent_seed_failure_boundary.py,test_agent_route_failure_boundary.py,test_harness_termination_failure.py, andtest_failure_assertions.pyundertests/unit_tests/. Initially 1,019 passed and four failed because the export lacked a Git index and the runner imported Gym before pytest installed its import hook. Supplied the tracked-file index, corrected runner import timing, and reran those four checks: 4 passed, with repository code unchanged.pytest responses_api_agents/hermes_agent/tests: 352 passed.pytest responses_api_agents/stirrup_agent/tests: 172 passed. Its cached environment initially had aiohttp 3.14.1, below the repository minimum; installed the lockfile's aiohttp 3.14.4 into an isolated test dependency directory and reran successfully.-q -o addopts= -o asyncio_mode=auto -o log_cli=false -p pytest_asyncio.plugin -p no:cacheprovider, with plugin autoload disabled and imports bound to the snapshot. 1,547 tests passed overall. Disabling the cache plugin produces the benign unknown-cache_dirwarning.debian:bookworm-slim, network disabled, real pinned CPython plus cached Hermes package archives, throughAsyncSandboxandDockerProvider. Installed and accepted the runtime, temporarily hid the container's bash, and attempted installation again. Restoring only the previous cleanup logic reproduced exit 127 with retained.setup-*; the fixed logic returned the same error with no retained setup files on two attempts. The accepted runtime and task files remained intact. Restoring bash allowed a successful retry, and the temporary container was removed. This tests real installation/cleanup, not a model rollout.pre-commit run --all-files,git diff --check: pass on the final tree.cd fern && npm run check -- --warnings: 0 errors; the authenticated missing-redirect check was skipped.NIM_API_KEYremains unavailable. Earlier model evidence below retains its original revision limits. New-head CI and reviewer reapproval remain required.Review follow-up —
207e13068mainat3628b3493; resolved the sole conflict innemo_gym/agents/__init__.py, retainingcheck_agent_seed_rejectedandSandboxPythonRuntime.SandboxPlatforminternal and makeSandboxPythonRuntimekeyword-only; consistently documentpython -E -s; document HTTP(S)-only Python mirrors and offline cache prepopulation.uv_bootstrappaths, documentedsandbox_python/in the configuration reference, and added the suggested setup-mkdirandNO_PROXYregression tests.pytest tests/unit_tests/test_sandbox_python.py tests/unit_tests/test_sandbox.py tests/unit_tests/test_local_provider.py tests/unit_tests/test_public_api.py tests/unit_tests/test_public_api_facades.py tests/unit_tests/test_moved_paths.py tests/unit_tests/test_import_linter_coverage.py tests/unit_tests/test_fern_docs_links.py -q -o addopts= -o log_cli=false -p pytest_asyncio.plugin, withPYTEST_DISABLE_PLUGIN_AUTOLOAD=1andPYTHONPATHbound to the final tree: 979 passed, 6 skipped, 24 subtests passed, plus two failures caused by the absent optional OpenSandbox SDK. Installed that SDK into a temporary directory and reran exactly those two failing tests: 2 passed (981 passed overall).NO_PROXYtest fail; ignoring failed setupmkdirmakes its regression fail. The expanded remote-installer test passes for caller timeout, returned timeout, and returned provider error, retaining setup until the detached installer completes.pre-commit run --all-filesandgit diff --check: pass.cd fern && npm run check -- --warnings: 0 errors; authenticated missing-redirect checking was skipped.NIM_API_KEYis unavailable. Earlier Linux/model evidence below retains its revision limits. Full-scope CI and reviewer reapproval remain pending.Conflict and review fixes —
1c9f83f05mainate12458ff0, moved the runtime files intonemo_gym/agents/utils/, and reconciled adapter imports, public exports, and guide links. The final tree also merges cleanly with the latermaintip101a44204.--no-same-owner. Two Linux tests run the actual installer underunshare -Ur, with an unmapped owner in each archive separately. Removing only those flags makes both tests fail withCannot change ownership.pytest tests/unit_tests/test_sandbox_python.py tests/unit_tests/test_sandbox.py tests/unit_tests/test_local_provider.py -q -o addopts= -o asyncio_mode=auto -o log_cli=false -p pytest_asyncio.plugin -p no:cacheprovider: 141 passed, 2 skipped (opt-in provider tests). The same flags withresponses_api_agents/hermes_agent/tests: 352 passed; withresponses_api_agents/stirrup_agent/tests: 172 passed, including the wall-time/partial-output case. A source-binding runner verified that all Gym and adapter imports came from the final tree.pre-commit run --all-filesandgit diff --check: pass.cd fern && npm run check: 0 errors; authenticated redirect checking was skipped. Independent final-tree review found no remaining issue in this fix scope.NIM_API_KEYwas unavailable. Earlier rollout evidence below retains its original revision limits. Full repository CI still needs to qualify the new head.glibc detection with an optional musl loader —
e8efb762egetconforlddtakes precedence over the presence of a musl loader. Thelddfallback also covers slim images withoutgetconf; musl version output is retained even whenlddexits nonzero.pytest tests/unit_tests/test_sandbox_python.py -q -o addopts= -o log_cli=false: 45 passed on Linux/Python 3.13.14. The same source-bound suite passed 45 tests on macOS, with-o asyncio_mode=auto -p no:cacheprovider(three plugin/config warnings).AsyncSandboxandprobe_sandbox_platform(): cleandebian:bookworm-slimremains glibc 2.36; Debian with its officialmuslpackage changes from the old incorrect musl result to glibc 2.36, including after removinggetconffrom PATH.alpine:3.20andbash:5.2still report musl.RUNTIME.install()andis_installed()pass on Debian + musl without getconf, using the pinned CPython and real package archives (31.93 s); the task workdir is unchanged. All smoke containers were removed. This validates runtime seeding; no new model rollout was run for this probe-only change.pre-commit run --all-filesandgit diff --check: pass.Python and Hermes startup isolation —
87ff060b3PYTHONHOMEand a taskPYTHONPATHcontaining shadow modules; trusted sibling imports work, and tool subprocesses still select the task's Python and inherit its original environment. Hermes seed and fresh worker processes use private state, ignore the task's Hermes.env, and establish Gym model routing before the first import.bae2b42df(4 Hermes + 1 Stirrup failures).pytest responses_api_agents/hermes_agent/tests -q -o addopts= -o log_cli=false: 352 passed. After the final model-routing reorder,pytest responses_api_agents/hermes_agent/tests/test_sandbox_runner.py responses_api_agents/hermes_agent/tests/test_model_kwargs.py responses_api_agents/hermes_agent/tests/test_summary.py -q -o addopts= -o log_cli=false: 47 passed.pytest responses_api_agents/stirrup_agent/tests -q -o addopts= -o log_cli=false: 172 passed on Linux with Gym's requiredopenai==2.44.0. The sandbox test double now exposes the newprovider_namecontract.pre-commit run --all-filesandgit diff --check: pass.cd fern && npm run check: 0 errors, the same unauthenticated redirect-check warning.NIM_API_KEY, so the earlier real-model rollouts below were not rerun for this revision.Timeout cleanup review fixes —
bae2b42dfpytest tests/unit_tests/test_sandbox_python.pyon macOS: 37 passed. The new cases exercise timeout and explicit cancellation after extraction, repeated interruptions, preservation of the accepted runtime and task directories, successful retry, and a remote installer that continues after its caller times out.SandboxPythonRuntime.installwith the previous revision makes both new local interruption cases fail on retained.setup-*directories.pytest tests/unit_tests/test_sandbox.py tests/unit_tests/test_local_provider.py tests/unit_tests/test_fern_docs_links.pyon macOS: 121 passed, 4 skipped, 24 subtests passed. Skips require live OpenSandbox or Linux/proc.pytest tests/unit_tests/test_sandbox_python.py tests/unit_tests/test_sandbox.py tests/unit_tests/test_local_provider.pyon Linux: 131 passed, 2 skipped (live OpenSandbox opt-in).-o addopts= -o asyncio_mode=auto -o log_cli=false -p pytest_asyncio.plugin -p no:cacheprovider; the cache plugin exclusion causes one benigncache_dirconfig warning.pre-commit run --all-filesandgit diff --check: pass.cd fern && npm run check: 0 errors, one warning that the authenticated missing-redirect check was skipped.NIM_API_KEY; no successful model call occurred. Earlier model rollout evidence below has not been rerun on this revision.Earlier implementation validation
pre-commit run --files <changed files>: all hooks pass, no changes.tests/unit_tests/test_sandbox_python.py(new, 26 tests), passing on macOS and Linux:uvargv (--no-config, platform,--only-binary,--no-binaryonly for URL requirements, constraints, adapter overrides, scratchcwd);Regression test
test_seeding_ignores_the_task_uv_configuration_and_leaves_the_task_untouched. The task's workdir has auv.tomland apyproject.toml[tool.uv]pointing at a dead index, and its PATH has auvthat records calls and exits 2. Seeding succeeds, the fakeuvis never called, and the workdir and HOME are byte-for-byte unchanged while the harness writes under the runtime's own HOME. I checked that the test fails when the HOME redirect in the installer is removed.responses_api_agents/stirrup_agent/testson Linux: 171 passed.responses_api_agents/hermes_agent/testson Linux: 346 passed, 2 failed. The 2 failures (test_close_preserves_interrupted_observations_before_release[False|True], "launch outcome is unknown; termination deadline exceeded") also fail on refactor(agent_utils): run sandboxed harnesses with only bash in the task image #5151's head with the base Hermes files on the same host, regardless ofTMPDIR. I checked at7908559f8, and they still fail withfb642f9d2on this branch. They exercise only the supervisor's close path, which this PR doesn't touch. refactor(agent_utils): run sandboxed harnesses with only bash in the task image #5151 traced them to slow forced stops on that busy host (about 1,100 processes) and fixed them in05bac0530, which this branch includes; CI's server suite runs the Hermes tests green on this branch.On macOS, the Hermes suite passes (342 passed, 6 Linux-only skipped). The Stirrup end-to-end tests that need
/procfor the bash supervisor fail there; that is unrelated and they pass on Linux.install_python_runtime.shby hand with--network noneand the real Hermes archives, run withbashasinstall()runs it (a11310a77), three rounds each:debian:bookworm-slim(no Python),python:3.13-slim(image has Python 3.13.17) andbash:5.2(Alpine: musl and BusyBox utilities, plus bash);uv.tomland HOME were untouched, the setup directory was removed, and a failing check published nothing. On Alpine,pydantic_coreandyamlimport from musllinux wheels.alpine:3.20, which has no bash, the install fails withsh: bash: not found(exit 127). The earliershversion of the installer passed the same rounds there, but that image cannot run the bash supervisor anyway.a11310a77(installer runs withbash):tests/unit_tests/test_sandbox_python.py26 passed on macOS (bash 3.2);pre-commitpasses. Hermes on macOS: 341 passed, 6 skipped, andtest_close_retires_the_launch_path_before_removing_its_fencefailed once while other suites ran in parallel, then passed alone. Stirrup on macOS: the same/procfailures as before. I did not rerun the Linux suites or the model rollouts below for this commit; the by-hand runs above cover the changed installer with the real archives.e6ea97a99(review fixes; merged withmainin3344e770f, whose tree ismainplus exactly this PR's diff):tests/unit_tests/test_sandbox_python.py34 passed on macOS. Among the new tests,test_a_runtime_another_seed_accepted_is_never_set_asidefails 3/3 against the previous installer. An installer stress run (3 seeds over a broken runtime, 40-module import check) went from 9/30 failed seeds to 0/90. Hermes on macOS: 343 passed, 6 skipped. Stirrup on macOS: 152 passed, with the same 19/proc/uptimefailures as before.pre-commitpasses. I did not rerun the Linux suites, the Docker installer rounds or the model rollouts for this commit.Rollout evidence
Real-model smoke rollouts with
nvidia/nemotron-3-ultra-550b-a55b(NIM) on an x86_64 Linux workstation, using the Docker sandbox provider:docker network create --internalnetwork: it reached only the Gym Model Server proxy on the gateway, with no route to the internet by IP or by name.workdir: /workspace./workspace/answer.txtin the sandbox./tmpwas a per-run bind mount from/dev/shm.docker cpdoes not write into tmpfs mounts.python3/uv/ internet/workspace,~/workspace/answer.txtdebian:bookworm-slim.bashrc .profilecode_exec,code_exec,finish1591debian:bookworm-slim.bashrc .profileterminal,terminal1591python:3.13-slim/usr/local/bin/python3; runtime still reports 3.13.14 under/tmp/nemo-gym-hermes-runtime-…/pythonterminal,terminal1591After merging
main(Hermes venv now hasmcp1.30.0 and the runtime addsmcp>=1.28.1,<2), I reran the same checks ondebian:bookworm-slim. Both passed, with the same sandbox findings and/workspace/answer.txt=1591, and their containers were removed. Hermes seeded in 17.4 s with a fresh host build (terminal×2); Stirrup seeded in 32.0 s (code_exec×2,finish). On Linux after the merge: Stirrup 171 passed;test_sandbox_python.py26 passed; Hermes 346 passed plus the same 2 pre-existing failures.An earlier Hermes run (with the overrides version) seeded in 18.5 s from a cold host cache with a 15 MB site-packages archive. The constraints version is 20 MB because it keeps the extras the overrides dropped.
Not in this PR (follow-ups)
🤖 Generated with Claude Code