Repository navigation
refactor(agent_utils): run sandboxed harnesses with only bash in the task image - #5151
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>
…kdir check Review on #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>
|
/ok to test 656e173 |
Smoke rollouts with a real model at
|
| Image | Image ID | OS | python3 |
bash |
|---|---|---|---|---|
debian:bookworm-slim |
sha256:8da84df25d28… |
Debian 12 | none | present |
SWE-bench swebench/sweb.eval.x86_64.django_1776_django-11099, plus ENV PATH=/opt/miniconda3/envs/testbed/bin:$PATH, as a harness that activates the testbed sees it |
sha256:8e44fd757da3… |
Ubuntu 22.04 | 3.6.13 | present |
alpine:3.20 |
sha256:bf8527eb54c3… |
Alpine 3.20.10 | none | none |
Each case
- Setup: the owner starts the container and creates
/task. It puts a marker file in each of the task's$XDG_CONFIG_HOME,$XDG_DATA_HOME,$XDG_CACHE_HOMEand$XDG_STATE_HOME(defaults under$HOME), and recordspython3and bash. - Seed and activation: the agent seeds a session on the borrowed container, which includes bootstrap and runtime install. It then runs one activation: "create
/task/deliverable.txtcontaining<random token>with your shell or file tool, then reply done". - Close: the agent closes the session, and the case records the supervisor's cleanup receipt.
- Verification, only after close: the owner checks that
- the deliverable equals the token;
- all four markers are kept;
- the adapter's session directory is gone;
- no harness process (
node,opencode,kilo, the runner or the supervisor) is left; python3is unchanged.
- Teardown: the owner stops the container and confirms
docker inspectno longer finds it.
Results
| Image | Harness | Result | python3 before → after |
Model calls | Time |
|---|---|---|---|---|---|
debian:bookworm-slim |
pi | pass | none → none | 2 | 51 s |
debian:bookworm-slim |
opencode | pass | none → none | 2 | 185 s |
debian:bookworm-slim |
kilo | pass | none → none | 2 | 264 s |
SWE-bench django-11099 |
pi | pass | 3.6.13 → 3.6.13 | 2 | 42 s |
SWE-bench django-11099 |
opencode | pass | 3.6.13 → 3.6.13 | 2 | 54 s |
SWE-bench django-11099 |
kilo | pass | 3.6.13 → 3.6.13 | 2 | 77 s |
alpine:3.20 |
pi | pass | none → none | 2 | 47 s |
alpine:3.20 |
opencode | fail (see below) | none → n/a | 0 | 14 s |
alpine:3.20 |
kilo | pass | none → none | 2 | 51 s |
Every passing case shows
- A real deliverable: the output has a
function_calland itsfunction_call_output, and the file is verified after close. - Confirmed cleanup: the activation and close receipts both read
cleanup_confirmed: true, return_code: 0, timed_out: false. - Task state kept, adapter state gone: all four task HOME/XDG markers survive, the adapter's session directory is removed, and no harness process is left. Owner teardown then removes the container.
- No Python installed: Debian and Alpine have no
python3before or after, and the SWE image keeps its Python 3.6.13. On Alpine, the POSIXshbootstrap installed the missing bash. - Capture:
- Pi: 28 event lines stamped by the runner's events mode.
- OpenCode and Kilo: 3 observation records each, parsed on the agent server from the copied database.
OpenCode on Alpine: the install completes, but OpenCode's musl binary can't start: Error relocating …/opencode: … symbol not found, because it needs libstdc++ and libgcc.
- Not a regression: neither
main's installer nor this PR's installs those packages. In a cleanalpine:3.20, the same 1.17.11 binary fails without them and prints its version with them, so OpenCode has never run on a bare Alpine image. - Coverage: Kilo covers host-side database capture on Alpine.
- Fix: installing
libstdc++andlibgccon musl, as the Pi, Kilo and Codex installers already do forlibstdc++, is a small follow-up.
Limits
- Verifier: the owner's check of the deliverable after close, not a benchmark Resources server.
- Scope: one short task per case and one model.
- Old-Python case: it puts the image's own Python 3.6 testbed first on
PATHinstead of using an image where it's already the default. - Not included: Claude Code and OpenClaw were not part of this run.
The runs used a standalone script, smoke_rollouts.py, invoked as python smoke_rollouts.py <evidence.json> <image>. It reads the API key from a file and redacts it from all output. It isn't in the repo; I can add it if it would help.
Per-case evidence (one JSON object per case)
{"harness": "pi", "image": "debian:bookworm-slim", "image_id": "sha256:8da84df25d28176be84164040cd45e128c6b4c6a80b7d7cbe17fe2b9f198efc2", "passed": true, "seed_seconds": 22.4, "seconds": 50.6, "model_calls": 2, "response_status": "completed", "output_item_types": {"reasoning": 2, "function_call": 1, "function_call_output": 1, "message": 1}, "activation_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "close_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "session_closed": true, "close_observation_records": 2, "close_observation_gaps": ["cached_token_usage_unavailable", "reasoning_token_usage_unavailable", "subagent_hierarchy_unavailable"], "recorded_event_lines": 28, "adapter_session_dir_removed": true, "verifier_passed": true, "owner_teardown_container_removed": true, "error_type": null, "before": {"python3": "none", "bash": "/usr/bin/bash", "os": "debian-12", "markers": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"]}, "after": {"deliverable": "deliverable-04f7d7446df6729f", "markers_kept": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"], "markers_lost": [], "python3": "none", "harness_processes": []}}
{"harness": "opencode", "image": "debian:bookworm-slim", "image_id": "sha256:8da84df25d28176be84164040cd45e128c6b4c6a80b7d7cbe17fe2b9f198efc2", "passed": true, "seed_seconds": 35.6, "seconds": 185.4, "model_calls": 2, "response_status": "completed", "output_item_types": {"message": 2, "function_call": 1, "function_call_output": 1}, "activation_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "close_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "session_closed": true, "close_observation_records": 3, "close_observation_gaps": ["model_usage_reconciliation_unavailable"], "recorded_event_lines": 1, "adapter_session_dir_removed": true, "verifier_passed": true, "owner_teardown_container_removed": true, "error_type": null, "before": {"python3": "none", "bash": "/usr/bin/bash", "os": "debian-12", "markers": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"]}, "after": {"deliverable": "deliverable-10a7a43cd7a09dd6", "markers_kept": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"], "markers_lost": [], "python3": "none", "harness_processes": []}}
{"harness": "kilo", "image": "debian:bookworm-slim", "image_id": "sha256:8da84df25d28176be84164040cd45e128c6b4c6a80b7d7cbe17fe2b9f198efc2", "passed": true, "seed_seconds": 19.4, "seconds": 264.0, "model_calls": 2, "response_status": "completed", "output_item_types": {"message": 2, "function_call": 1, "function_call_output": 1}, "activation_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "close_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "session_closed": true, "close_observation_records": 3, "close_observation_gaps": ["usage_details_unavailable"], "recorded_event_lines": null, "adapter_session_dir_removed": true, "verifier_passed": true, "owner_teardown_container_removed": true, "error_type": null, "before": {"python3": "none", "bash": "/usr/bin/bash", "os": "debian-12", "markers": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"]}, "after": {"deliverable": "deliverable-40ad775d414b366d", "markers_kept": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"], "markers_lost": [], "python3": "none", "harness_processes": []}}
{"harness": "pi", "image": "gym-smoke/sweb-django-11099-py36:20261027", "image_id": "sha256:8e44fd757da3659ec422133fbe4276e3608aa1adb89858f2b4cc8c2f6fe35774", "passed": true, "seed_seconds": 7.5, "seconds": 42.5, "model_calls": 2, "response_status": "completed", "output_item_types": {"reasoning": 2, "function_call": 1, "function_call_output": 1, "message": 1}, "activation_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "close_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "session_closed": true, "close_observation_records": 2, "close_observation_gaps": ["cached_token_usage_unavailable", "reasoning_token_usage_unavailable", "subagent_hierarchy_unavailable"], "recorded_event_lines": 28, "adapter_session_dir_removed": true, "verifier_passed": true, "owner_teardown_container_removed": true, "error_type": null, "before": {"python3": "/opt/miniconda3/envs/testbed/bin/python3 3.6.13", "bash": "/usr/bin/bash", "os": "ubuntu-22.04", "markers": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"]}, "after": {"deliverable": "deliverable-b01ae03447a97a2d", "markers_kept": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"], "markers_lost": [], "python3": "/opt/miniconda3/envs/testbed/bin/python3 3.6.13", "harness_processes": []}}
{"harness": "opencode", "image": "gym-smoke/sweb-django-11099-py36:20261027", "image_id": "sha256:8e44fd757da3659ec422133fbe4276e3608aa1adb89858f2b4cc8c2f6fe35774", "passed": true, "seed_seconds": 12.0, "seconds": 54.0, "model_calls": 2, "response_status": "completed", "output_item_types": {"message": 2, "function_call": 1, "function_call_output": 1}, "activation_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "close_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "session_closed": true, "close_observation_records": 3, "close_observation_gaps": ["model_usage_reconciliation_unavailable", "token_usage_detail_unavailable"], "recorded_event_lines": 1, "adapter_session_dir_removed": true, "verifier_passed": true, "owner_teardown_container_removed": true, "error_type": null, "before": {"python3": "/opt/miniconda3/envs/testbed/bin/python3 3.6.13", "bash": "/usr/bin/bash", "os": "ubuntu-22.04", "markers": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"]}, "after": {"deliverable": "deliverable-d7f255e90e0b9bd5", "markers_kept": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"], "markers_lost": [], "python3": "/opt/miniconda3/envs/testbed/bin/python3 3.6.13", "harness_processes": []}}
{"harness": "kilo", "image": "gym-smoke/sweb-django-11099-py36:20261027", "image_id": "sha256:8e44fd757da3659ec422133fbe4276e3608aa1adb89858f2b4cc8c2f6fe35774", "passed": true, "seed_seconds": 9.9, "seconds": 76.9, "model_calls": 2, "response_status": "completed", "output_item_types": {"message": 2, "function_call": 1, "function_call_output": 1}, "activation_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "close_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "session_closed": true, "close_observation_records": 3, "close_observation_gaps": ["usage_details_unavailable"], "recorded_event_lines": null, "adapter_session_dir_removed": true, "verifier_passed": true, "owner_teardown_container_removed": true, "error_type": null, "before": {"python3": "/opt/miniconda3/envs/testbed/bin/python3 3.6.13", "bash": "/usr/bin/bash", "os": "ubuntu-22.04", "markers": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"]}, "after": {"deliverable": "deliverable-f4a8430b7699a437", "markers_kept": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"], "markers_lost": [], "python3": "/opt/miniconda3/envs/testbed/bin/python3 3.6.13", "harness_processes": []}}
{"harness": "pi", "image": "alpine:3.20", "image_id": "sha256:bf8527eb54c3680e728d5b4b383a8ba730d72dae7236fbc8dff97ed6b224a731", "passed": true, "seed_seconds": 27.8, "seconds": 47.4, "model_calls": 2, "response_status": "completed", "output_item_types": {"reasoning": 2, "function_call": 1, "function_call_output": 1, "message": 1}, "activation_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "close_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "session_closed": true, "close_observation_records": 2, "close_observation_gaps": ["cached_token_usage_unavailable", "reasoning_token_usage_unavailable", "subagent_hierarchy_unavailable"], "recorded_event_lines": 28, "adapter_session_dir_removed": true, "verifier_passed": true, "owner_teardown_container_removed": true, "error_type": null, "before": {"python3": "none", "bash": "none", "os": "alpine-3.20.10", "markers": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"]}, "after": {"deliverable": "deliverable-e306c86a7beba666", "markers_kept": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"], "markers_lost": [], "python3": "none", "harness_processes": []}}
{"harness": "opencode", "image": "alpine:3.20", "image_id": "sha256:bf8527eb54c3680e728d5b4b383a8ba730d72dae7236fbc8dff97ed6b224a731", "passed": false, "seed_seconds": null, "seconds": 14.0, "model_calls": 0, "response_status": null, "output_item_types": null, "activation_receipt": null, "close_receipt": null, "session_closed": null, "close_observation_records": null, "close_observation_gaps": null, "recorded_event_lines": null, "adapter_session_dir_removed": null, "verifier_passed": null, "owner_teardown_container_removed": true, "error_type": "RuntimeError", "before": {"python3": "none", "bash": "none", "os": "alpine-3.20.10", "markers": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"]}, "error_first_lines": ["pencode-runtime-1.17.11/opencode: _ZNKSt8__detail20_Prime_rehash_policy14_M_need_rehashEmmm: symbol not found", "Error relocating /tmp/nemo-gym-opencode-runtime-1.17.11/opencode: _ZSt13set_terminatePFvvE: symbol not found", "Error relocating /tmp/nemo-gym-opencode-runtime-1.17.11/opencode: _ZSt21__glibcxx_assert_failPKciS0_S0_: symbol not found"]}
{"harness": "kilo", "image": "alpine:3.20", "image_id": "sha256:bf8527eb54c3680e728d5b4b383a8ba730d72dae7236fbc8dff97ed6b224a731", "passed": true, "seed_seconds": 21.5, "seconds": 51.0, "model_calls": 2, "response_status": "completed", "output_item_types": {"message": 2, "function_call": 1, "function_call_output": 1}, "activation_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "close_receipt": {"cleanup_confirmed": true, "return_code": 0, "error": null, "timed_out": false}, "session_closed": true, "close_observation_records": 3, "close_observation_gaps": ["usage_details_unavailable"], "recorded_event_lines": null, "adapter_session_dir_removed": true, "verifier_passed": true, "owner_teardown_container_removed": true, "error_type": null, "before": {"python3": "none", "bash": "none", "os": "alpine-3.20.10", "markers": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"]}, "after": {"deliverable": "deliverable-2adebc9f36532759", "markers_kept": ["/root/.config/task-owned/marker", "/root/.local/share/task-owned/marker", "/root/.cache/task-owned/marker", "/root/.local/state/task-owned/marker"], "markers_lost": [], "python3": "none", "harness_processes": []}}🤖 Generated with Claude Code
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/40616294536 |
Resolves modify/delete conflicts in `responses_api_agents/hermes_sandboxed_agent/` (README.md, app.py, tests/test_app.py) by keeping this branch's deletion. NVIDIA-NeMo#5151 adapted that adapter to the bash process supervisor and updated `hermes_agent/sandbox.py` itself, so nothing needs to carry over. Resolves two conflicts in `add-or-update-a-harness.mdx` by taking NVIDIA-NeMo#5151's rewritten text and dropping only the references to the removed adapter. NVIDIA-NeMo#5151 also added two new references to it: one names it as an example of a runtime prepared outside the task container and mounted read-only, and one links to its `prepare_runtime.sh`. Keep that guidance and drop both, since the link would 404 once the adapter is gone. NOOA stays as the worked example. Main also tightened the runtime check in `hermes_agent/sandbox.py` from `import run_agent, mcp` to importing `run_agent` and `mcp.client.streamable_http.streamablehttp_client`. Update the migration notes to state the check offline deployments must now satisfy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Resolve the conflicts with the bash-only harness runner (#5151): - Stage Kilo through harness_runner.sh, still writing input.json with the required MCP servers and preflight flag that the task-environment plugin reads. - Drop sandbox_runner.py; the activation setup now removes the stale required-MCP receipt alongside the task-environment receipt. - Take the preflight's exit status from the cleanup receipt, as activations do, and fall back to the supervisor log when Kilo's stderr is missing. - Run the pinned-CLI preflight test and the fake-sandbox flow tests through the bash runner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
…isor #5151 replaced SandboxCommand's python field with stdlib_runner and moved the process supervisor to bash. Declare Pool's standard-library runner with stdlib_runner=True and upload it as RUNNER_FILE, like Codex, so task-side pkill patterns for python or .py cannot match it. The bash supervisor reports a SIGKILLed harness as 137 rather than -9, and Harbor's materialize_task now reads task_data, so build the Harbor test row from a real task folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
Drop the Pi sandbox_runner.py drain fix and its test: #5151 replaced that runner with harness_runner.sh, which reads Pi's stdout from a file to its end after Pi exits. Signed-off-by: Abhishree <athittenaman@nvidia.com>
Main's #5151 runs sandboxed harnesses with only bash in the task image and deletes OpenCode's Python sandbox runner, which this branch had extended for owner exports, pinned files and session import. Those now need no Python in the sandbox either: the sandbox runs POSIX sh and coreutils, and the agent server reads OpenCode's session store and checks archives itself, as main's host-side snapshot does. - Session export: the stored session tree comes from the store that the capture copies out. Each session's `opencode export` is its own command, bounded by the collection deadline. It writes into the session directory and reaches `export_dir` only once it is a JSON object. - Pinned ripgrep and plugin dependencies: the sandbox checks a private copy's SHA-256 with sha256sum and runs `--version`. The agent server checks an archive's members in a download of the verified copy before the sandbox unpacks it with tar. - Session import: `opencode import` runs from the task workdir without the run's configuration. The agent server then reads the imported session and its messages from a copy of the store. - The plugin-file and export_dir checks are POSIX sh. - Main's runner `cd`s into the task workdir, so every run already gets PWD set to it. The opt-in PWD setting is gone; import and export set PWD themselves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Katherine Huang <katherineh@nvidia.com>
* feat(pool): run Pool in task sandboxes Signed-off-by: Marat Kitov <mkitov@nvidia.com> * fix(pool): preserve failures and drain sessions on shutdown Signed-off-by: Marat Kitov <mkitov@nvidia.com> * fix(pool): retain failure artifacts and clarify runner metadata Signed-off-by: Marat Kitov <mkitov@nvidia.com> * fix(pool): harden session lifecycle and timeout outcomes Signed-off-by: Marat Kitov <mkitov@nvidia.com> * fix(pool): require EULA opt-in and verify pinned release archives Signed-off-by: Marat Kitov <mkitov@nvidia.com> * fix(pool): allowlist pinned release checksums for secret scanning detect-secrets flags the four pinned Pool v1.0.16 archive SHA-256 values as high-entropy strings. They are public release checksums, so mark them as false positives inline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * test(terminus-2): clear PROMPT_COMMAND before the interrupt-exit scenario (NVIDIA-NeMo#4966) On Ubuntu 26.04, systemd's /etc/profile.d/80-systemd-osc-context.sh appends a hook to PROMPT_COMMAND in every login bash. With PROMPT_COMMAND set, an interactive bash returns to the prompt after Ctrl-C instead of exiting under `set -e`, so the interrupt case of test_exited_shell_recovery_discards_pending_input_and_reports_lost_state never sees a dead pane. GitHub's ubuntu-latest runners moved to 26.04, so the test now fails on main and on every PR. Clear PROMPT_COMMAND in the command the test sends, so the shell exits as the scenario requires regardless of the host's login profile. Generated with [Devin](https://devin.ai) Signed-off-by: Marc Romeyn <mromeijn@nvidia.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> (cherry picked from commit c5c4782) Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * test(pool): drain proxy fixture request bodies Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> * fix(pool): launch the runner as a stdlib runner under the bash supervisor NVIDIA-NeMo#5151 replaced SandboxCommand's python field with stdlib_runner and moved the process supervisor to bash. Declare Pool's standard-library runner with stdlib_runner=True and upload it as RUNNER_FILE, like Codex, so task-side pkill patterns for python or .py cannot match it. The bash supervisor reports a SIGKILLed harness as 137 rather than -9, and Harbor's materialize_task now reads task_data, so build the Harbor test row from a real task folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> --------- Signed-off-by: Marat Kitov <mkitov@nvidia.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> Signed-off-by: Marc Romeyn <mromeijn@nvidia.com> Co-authored-by: yaoyu-33 <yaoyu.094@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Marc Romeyn <marcromeyn@gmail.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…o#5152) * 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> * 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(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(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> --------- 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>
…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
Sandboxed harnesses no longer need Python in the task image. The shared process supervisor (
nemo_gym/agent_utils/process_supervisor.sh) and a shared harness runner (harness_runner.sh) are bash scripts, and the OpenCode/Kilo SQLite snapshot moves to the agent server. Claude Code, OpenCode, Kilo, OpenClaw and Pi run their CLIs through the bash runner; Hermes, NOOA and Stirrup keep their own Python workers under the bash supervisor.This ports Hemil Desai's bash sandbox runtime change (his commits are kept with authorship) onto
main, adapts it to code that only exists here, and adds fixes found in review.Before / after
Colour key: amber = Python inside the task sandbox, green = moved from Python to bash, blue = moved out of the sandbox to the agent server, grey = Python worker kept as is.
Before: every sandboxed harness needs Python ≥ 3.8 in the task image.
flowchart LR subgraph host["Agent server"] S["SandboxSession<br/>supervisor_client"] end subgraph box["Task sandbox (task image)"] B["Bootstrap<br/>apt/apk python3 if missing<br/>workdir check with python3 -c"]:::py I["Runtime installers<br/>install_claude_runtime.py<br/>Python ≥ 3.8 assert, fcntl lock,<br/>hashlib digest, ctypes libstdc++"]:::py SUP["process_supervisor.py<br/>subreaper drain"]:::py R["sandbox_runner.py<br/>one per harness"]:::py CLI["Harness CLI<br/>Claude Code · OpenCode · Kilo · OpenClaw · Pi"] W["Codex worker + 429 proxy<br/>Hermes · NOOA · Stirrup workers"]:::keep DB[("opencode.db / kilo.db")] X["SQLite export<br/>sandbox_runner.py --snapshot"]:::py end S -->|"seed"| B --> I S -->|"launch: python3 -I"| SUP --> R --> CLI SUP --> W CLI --> DB --> X -->|"export.json<br/>observations.db"| S classDef py fill:#fde68a,stroke:#b45309,color:#111 classDef keep fill:#e5e7eb,stroke:#6b7280,color:#111After: the shared pieces need only bash; Codex, Hermes, NOOA and Stirrup still bring or use their own Python worker.
flowchart LR subgraph host["Agent server"] S["SandboxSession<br/>supervisor_client<br/>shell_assignments() → params.sh"] X["SQLite export<br/>snapshot.py"]:::host end subgraph box["Task sandbox (task image needs bash)"] B["Bootstrap<br/>apt/apk bash only if missing<br/>POSIX sh workdir check"]:::sh I["Runtime installers<br/>install_claude_runtime.sh<br/>flock or owner-symlink lock,<br/>sha256sum, path / apk checks"]:::sh SUP["process_supervisor.sh<br/>/proc rescans + env marker"]:::sh R["harness_runner.sh<br/>shared by 5 harnesses"]:::sh CLI["Harness CLI<br/>Claude Code · OpenCode · Kilo · OpenClaw · Pi"] W["Codex worker + 429 proxy<br/>Hermes · NOOA · Stirrup workers"]:::keep DB[("opencode.db / kilo.db<br/>+ WAL files")] end S -->|"seed"| B --> I S -->|"launch: bash"| SUP --> R --> CLI SUP --> W CLI --> DB -->|"download raw files"| X classDef sh fill:#bbf7d0,stroke:#15803d,color:#111 classDef host fill:#bfdbfe,stroke:#1d4ed8,color:#111 classDef keep fill:#e5e7eb,stroke:#6b7280,color:#111process_supervisor.pyprocess_supervisor.shsandbox_runner.pyper harnessharness_runner.shinstall_claude_runtime.pyinstall_claude_runtime.shfcntl,hashlib)flockor owner symlink,sha256sumctypesprobeapk infocheckspython3 -csh--snapshot)snapshot.py)sandbox_runner.py+sandbox_transport.pyKnown exceptions (follow-ups, not in this PR)
These still put Python or the network into the task sandbox. No tracking issues are filed yet.
python3≥ 3.8 in the task image and apt/apk-installs it when missing. Python runs its workdir overlap check, the installer's version and libstdc++ probes, its worker, the 429 retry proxy (sandbox_transport.py) and the--snapshotexport. Only the proxy has no bash equivalent; where it should live (for example the agent server'srate_limit_proxy) is still to be decided. Once Codex moves,stdlib_runner,RUNNER_INTERPRETERandbin/gym-runnercan go.uvand runsuv venv --python 3.13anduv pip installinside the sandbox, from the network, withcwdset to the task workdir, without--no-config, and withoutUV_CACHE_DIRorUV_PYTHON_INSTALL_DIR: a task'suv.tomlor[tool.uv]can change the install, uv's cache and managed Python land in the task's home, and--python 3.13can bind the image's own python3.13.uv pip install stirrup==...; it setsUV_CACHE_DIRandUV_PYTHON_INSTALL_DIRbut not--no-config.hermes_sandboxed_agent/prepare_runtime.shand NOOA do. The harness guide now presents these three paths as exceptions, not options.Why
Before this change the supervisor and per-harness runners ran on whatever
python3the task image had:python3older than 3.8 failed the task (seen on SWE images with old Python);python3was apt/apk-installed into the task image, changing what both the agent and the verifier see (for example Go/Rust/JS images in multilingual SWE sets).No public issue; this mirrors internal work on the same change.
Port notes (differences from the original change)
mainthe Codex worker runs a sandbox-local 429 retry proxy (sandbox_transport.py) that has no bash equivalent, so Codex still needs Python ≥ 3.8 in the task image. Where that proxy should live is an open question for a follow-up.main; those parts are left out.sandbox_python_executable;sandbox_path_prefixnow reaches the CLI through the runner setup.benchmarks/swe_together/opencode.yamlis updated.SandboxCommand.pythonfield; its pinned Python still runs its worker.Fixes on top of the port
/procwhen the stop begins and on every polling round. It kills the harness group plus everything it reaches: descendants of the harness, of its group and of reached processes, and processes that inherited the harness'sNEMO_GYM_SUPERVISORenvironment marker (for example a tool that daemonizes, or a TERM handler that runssetsid ... &and exits). Marked processes that had already left the harness when the stop began are preserved as services, as the subreaper did onmain. What is not reached is documented: a process that leaves between two scans after dropping the marker. The Codex runner passes the marker through its env allowlist./proc/PID/statwhole and never evaluates its fields as arithmetic (a crafted process name could run code and hide a member); refuses to launch without a readable/proc; bounds the wait for a KILLed harness; rejects malformed arguments instead of looping; ignores an inherited errexit. Launches render fixed-point seconds, and a deadline under one centisecond expires before launch instead of leaving no receipt./proc/PID/statseveral times, and on a busy host this made stops slow (1.65 s with about 1,100 processes, so Hermes's close test, which allows 2 s, failed there while CI passed). Parsing no longer uses bash's quadratic longest-prefix match or a regex per field, the drain takes liveness from its scan instead of a second pass, and no extra scan runs once the harness has exited. The same stop now takes about 0.55 s.${1%$'\r'}took seconds per MB); an inheritedset -eno longer stops the runner at a non-JSON line; a failedexecredirection no longer falls through into events mode. The runner exits 125 for its own failures (badcwd, anexitin setup) and 126/127 when the CLI could not be started, and Kilo reports these apart from Kilo's own exit status. A cancelled Kilo run reports no exit code, as before.set -uon bash < 4.4 and checks all musl library directories; the OpenCode installer removes a partly staged binary on any failure and no longer reuses a cached binary whose--versionfails.shell_assignments()rejects NUL and non-string words.Compatibility
nemo_gym.agent_utils.process_supervisor(Python) is removed;CleanupReceipt,exec_timeoutand the constants live innemo_gym.agent_utils.supervisor_client.SandboxCommand(python=...)andsupervised_launch_command(python=...)are removed. Out-of-tree adapters need the migration infern/versions/latest/pages/agent-server/add-or-update-a-harness.mdx.return_codeis now the shell status (128+N for a signal death) instead of −N; a missing CLI gives a confirmed receipt with 126/127, and a runner failure before the CLI starts gives 125. The harness guide's migration note lists these together with the removedpython=parameters and the bash requirement.NEMO_GYM_SUPERVISORenvironment variable.Validation
Local (macOS, bash 3.2 and 5.2; Linux-only
/proctests are skipped here and run in CI):pytest tests/unit_tests responses_api_agents/{opencode,kilocode,codex,pi,openclaw,claude_code,hermes_sandboxed}_agent/tests: 13766 passed, 480 skipped, 50 failed, 1 collection error. All 50 failures also fail on unmodifiedmainin the same local venv (missingopensandbox/mlflow, macOS-specific telemetry/CI checks; one flaky Harbor test); the collection error istest_exporters.pyneedingmlflow.pre-commit run --files <changed files>: passed.Review round 2 (local, macOS):
tests/unit_tests/test_{harness_runner,process_supervisor,sandbox_supervisor_client,sandbox_supervisor_bootstrap,sandbox_session}.py,responses_api_agents/{kilocode,claude_code,opencode,codex,pi,openclaw}_agent/tests): 1910 passed, 228 skipped, 3 failed. The 3 failures aretest_opensandbox_sdk_is_available_in_agent_environment, which needsopensandbox, missing from the local venv./proc: harness, tools, orphaned group member and daemons reached; earlier service and its children preserved; reused group not killed.uname.Linux host (bash 5.0, about 1,100 processes), at 05bac05:
responses_api_agents/hermes_agent/tests: 349 passed.test_close_preserves_interrupted_observations_before_releasefailed in both cases at fb642f9 and passed 5 of 5 runs with the fix.tests/unit_tests/test_{process_supervisor,sandbox_supervisor_client,harness_runner}.py, including the Linux-only supervisor tests: all passed. The new name-parsing test fails on a parser that splits at the first") ".Rollout evidence: smoke rollouts with a real model at
656e1739e, with Pi (events mode), OpenCode and Kilo (host-side database capture) ondebian:bookworm-slim, a SWE-bench image with Python 3.6 first onPATH, andalpine:3.20. 8 of 9 pass. OpenCode on Alpine fails because its binary needslibstdc++andlibgcc, whichmaindoesn't install either.🤖 Generated with Claude Code