Repository navigation
feat(officeenv): run OfficeEnv episodes in a sandbox, for Gym's sandboxed harnesses - #5010
d-molinari wants to merge 9 commits into
Conversation
64c29c9 to
7f3b747
Compare
|
Review of 20676db. Each item has a failing test I can share. 1. A provider failure reading an attachment reaches the model as a missing file (app.py L1453-L1458) 2. 3. (nit) 4. (nit) |
20676db to
af279fe
Compare
6f6ae28 to
6ea2e28
Compare
6ea2e28 to
199654c
Compare
|
Thanks for the review. All four are fixed on the new head ( 1. Provider failure scored as a missing attachment (
2.
3.
4.
Since your review: episodes waiting with a sandbox lent ( A training run with Pi on an OpenSandbox cell found another lifetime gap:
If your failing tests differ from the ones added here, I'd be glad to run them against the new head. |
f492d3a to
e0f9d7a
Compare
|
Two commits since my reply above, both on the shutdown path from your item 3:
|
e117d54 to
242eff5
Compare
242eff5 to
12836ab
Compare
12836ab to
ea9cfe3
Compare
ea9cfe3 to
6d0de28
Compare
ff660a3 to
80540a8
Compare
…box can reach Three hooks, each defaulting to today's behavior, so every existing server is unchanged: - SimpleServer.get_session_secret(): the key that signs session cookies (SessionMiddleware and the session router that reads the same cookie) and MCP session tokens. It defaults to get_session_middleware_key(), a public value derived from the server's names. A server whose routes a sandbox can reach returns a per-process secret, so code there can forge neither. - SimpleServer.get_session_cookie_name(): the name SessionMiddleware sets and the session router reads, by default get_session_middleware_key(). It must stay unique per server. - BaseResourcesServerConfig.mcp_server_name and mcp_bearer_token, both off by default: the name /seed_session's MCP metadata gives the endpoint (verify-time normalization strips its namespace), and the session token as a standard "Authorization: Bearer" header. With the token, tools/list requires it and lists only the session's tools, a tool outside the session's grant is an unknown tool, and errors name no framework; the MCP endpoint, the session router and the checkpoint participant's token claims read it there. Tests: with two workers, cookies and tokens signed with the server's secret, under its cookie name, and bearer tokens reach the worker that holds the session, while ones signed with the public name reach none; the MCP endpoint with and without both options; verify stripping the advertised namespace; and checkpoint claims read from Authorization. Signed-off-by: Daniele Molinari <dmolinari@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opt-in sandbox support for the OfficeEnv resources server; without sandbox_provider nothing changes. - Each Environment Server episode borrows a sandbox from Gym's providers (Docker, OpenSandbox), lent as SandboxAccess. A repeated seed lends the same sandbox; close, retiring the session and shutdown stop it, and a sandbox that fails to start leaves no world behind. - /verify closes the episode's tools (409) and revokes its file API token, stops its sandbox, and only then grades, so nothing the harness left running can change the reward. A failed stop is logged and grading goes on; /close_session stops the sandbox later. - A per-episode file API moves file content byte for byte between the workspace and the simulated apps; uploads are capped at 20 MB while the body streams in. - The server a sandbox can reach is guarded: seed, close, verify, the app catalog and the tool routes answer only this node and trusted_networks, and sessions and MCP tokens are signed with a per-process secret (get_session_secret). - sandbox_config.block_internet (opt-in, OpenSandbox) refuses every outbound connection except to this server and the model servers, built with Gym's restricted_network_policy. It fails closed on another provider, a server address a sandbox cannot reach, host_relay, or a network policy of the config's own. - configs/officeenv_sandbox.yaml (Pi on Docker) and configs/officeenv_opensandbox.yaml (Pi on OpenSandbox), with sandbox/Dockerfile, the row and probe tools, and data/sandbox_pilot.jsonl. The server's venv gains nemo-gym[sandbox]. Tests use a fake sandbox provider: lending, reuse and release, failed starts and stops, shutdown and retiring during a seed, the session contract and the file API in sandbox mode, the caller check, grading after the stop, the egress policy, and plain mode unchanged. Signed-off-by: Daniele Molinari <dmolinari@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rsation, behind a company's API - In an episode with a sandbox, Drive, Box, SharePoint and OneDrive downloads are saved as workspace files and answered with their path, the content inline only for short text (sandbox_config.inline_file_bytes). An upload that pastes binary content or long text is refused with the file API command that sends the file instead, and the upload parameters say so. - Gmail's attachments too: get_attachment saves one in the workspace, and a message or draft attaches a workspace file by its path, read from the sandbox before the call runs. - Everything a sandbox can read names an ordinary company API, not the environment or Gym: the COMPANY_API_URL and COMPANY_API_TOKEN variables, the routes, the workspace guide (README.md), the errors, and the session cookie's name. - The sandbox rows' prompt sends every file app's content through the workspace, and mail by path. Tests: each file app's downloads and uploads through the workspace, attachments by path, the refusals with their command, and a check of everything a sandbox sees for the environment's names. Signed-off-by: Daniele Molinari <dmolinari@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oints, not only as MCP tools The row's exposure decides how the harness reaches the apps (SANDBOX_SURFACES); the world, the checks and the reward are the same for all three: - tools: one MCP tool per op; - cli: one command per app in the sandbox (`slack post-message --channel=...`), which sends its arguments to /v1/cli, so any harness with a shell runs it; - api_search: the finding tools over MCP lead to endpoints the harness calls with curl at /v1/<app>/<op>, which answer with the app's result or its error's HTTP status. The API's URL and token are also in /etc/company-api.env, for harnesses that start their shell without the sandbox's environment (Codex). Signed-off-by: Daniele Molinari <dmolinari@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…OpenCode, OpenClaw and Terminus 2 - configs/officeenv_sandbox_episodes.yaml holds the resources server for sandboxed episodes. Each harness config composes it with its harness and the Environment Server that binds them: Codex CLI, mini-SWE, OpenCode, OpenClaw and Terminus 2, on rows whose apps are commands (cli), beside Pi's. - A row may say what its task needs in the sandbox (sandbox_needs: documents, office). Its episode gets the first configured image that provides it, and a row no image serves is refused before its world is built (400 unsupported_needs). sandbox/Dockerfile builds an optional documents image (Word, PDF and LibreOffice tools), and sandbox/Dockerfile.harness one image per harness on it, with the harness's runtime preinstalled at its adapter's version and folder, for sandboxes without the internet. - A Drive export to DOCX, XLSX or PPTX is saved under its extension. - sandbox/prepare_rows.py writes rows in every exposure; data/sandbox_pilot.jsonl holds OfficeEnv 1.10.0's nine sandbox tasks. Tests: each harness config composes and validates, the images hold the runtimes their adapters look for, rows get the image their needs ask for, and the sandbox rows seed on the pinned OfficeEnv. Signed-off-by: Daniele Molinari <dmolinari@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t sandbox at shutdown - Admission: each harness config caps the episodes the Environment Server admits at the harness's concurrency, so no episode is seeded, its sandbox's TTL running, while it waits for its harness. - Seeds take turns. A seed builds its world synchronously on the server's one event loop (0.1 to 0.7 s on a training corpus, up to 2 s), so a wave of seeds ran back to back while harness setups waited, and at about 1,000 episodes in flight Pi's MCP setup timed out. Seeds now run one at a time, each after the loop has served what is ready for 100 ms, and each logs its place in the queue, its wait and its build time. - Shutdown stops every lent sandbox at once, 64 at a time, within Gym's server_shutdown_timeout_seconds less 2 s (the sandboxed configs set it to 30 s), and logs at ERROR, for a reaper, any it could not stop: the lent ones apart from those still being created, each with their sessions. Tests: the admission caps follow each harness's concurrency, seeds take turns with the gap, and shutdown stops them all and names what it left, creates in flight apart. Signed-off-by: Daniele Molinari <dmolinari@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…goes by its exposure's name OfficeEnv 1.11.0 names api_search's documentation tool api_docs under the exposure option docs_tool, and refuses api_fetch then. The episode's MCP token admits the finding tools its exposure lists, and MCP serves the documentation tool under each name an exposure may give it (DOCS_TOOLS); a seed whose finding tools MCP does not serve is refused (400 unsupported_exposure) before a sandbox starts. The documentation carries its curl command by the tool's name, and the workspace guide and the rows' prompts name the tool as the exposure does. With OfficeEnv 1.10.0 every episode's tool is api_fetch, as before. The sandbox tests skip without OfficeEnv, as the server's other tests do. Signed-off-by: Daniele Molinari <dmolinari@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /task_state answers {"task_ended": ...} for the session's episode: whether the user app's task row
had left `pending` (complete_task succeeded, either status) when the session's last tool call
returned, whichever surface reached it. The state is published once a whole call is over, under the
episode's lock, so a read during a call, or one after a call that ended the task and reopened it,
never sees an end the call itself reversed. An Environment Server reads it to end the episode there.
Like the other routes it answers only Gym's servers on the node in sandbox mode, and a session the
server no longer holds is refused (404).
close_tools_on_task_end (off by default) refuses a tool call on any surface once a call has
returned with the task ended (409, "The task has ended: its tools are closed"), so the calls a turn
makes after the one that ended its task change nothing, and a task reopened in that call reopens
its tools.
Signed-off-by: Daniele Molinari <dmolinari@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…task ends environment_servers/officeenv runs the single-agent-turn protocol behind the flat /run contract, as single_agent_turn_legacy does, and with max_steps set drives the agent one step at a time: each activation is one model call and the tool calls it makes, with the conversation so far as input. Between steps it reads the OfficeEnv server's GET /task_state, so the episode ends before the next model call once its task has ended, as it does when the model answers without calling a tool, when a step does not complete, or after max_steps. The reply carries every step's output and summed usage, so verification and the length penalty see what one activation would give. It refuses to start when the bound agent's own max_steps is not 1, and a crashed episode starts over. configs/officeenv.yaml now puts it in front of the simple agent (one step per activation, 50 per episode, tools over direct HTTP) in place of the legacy_agent relay, with close_tools_on_task_end on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Daniele Molinari <dmolinari@nvidia.com>
80540a8 to
7f3bb6e
Compare
What does this PR do?
Lets Gym's sandboxed harnesses work on OfficeEnv tasks. With
sandbox_providerset, the OfficeEnv server from #5009 lends each Environment Server episode its own sandbox (Docker or OpenSandbox). The harness works in that sandbox, and OfficeEnv grades the simulated apps' final state as before. Withoutsandbox_provider, nothing changes.#5009 is merged, so this PR now sits on main. It also carries #5373 and #5374, folded in, and OfficeEnv's Environment Server, which ends an episode when its task ends (below; it replaces #5624).
How an episode runs
README.md) into the workspace./verifystops the sandbox before grading, so nothing the harness left running can change the reward.<app> <command> --parameter=value), for any harness with a shell;api_fetch, orapi_docswith OfficeEnv 1.11.0'sdocs_tooloption.COMPANY_API_URL,company_*tools). Nothing this server shows the sandbox names the environment or Gym, and a test checks all of it.trusted_networks) may seed, verify or call the tool routes, and sessions are signed with a per-process secret.block_internetis opt-in and works on OpenSandbox only; it limits a sandbox's network to this server and the model servers.mask_sample), as fix(harness_agent): mask rollouts the sandbox provider reports as failed #5354 does for a lost sandbox.server_shutdown_timeout_secondsless 2 s. It logs any it could not stop at ERROR, for a reaper: the lent ones apart from those still being created, each with their sessions.OfficeEnv's Environment Server (
environment_servers/officeenv, the top two commits), following the review on #5624:single_agent_turn_legacy).max_stepsset, it drives the agent one step per activation (the simple agent withmax_steps: 1), passing the conversation so far as input.GET /task_state. The episode ends before the next model call oncecomplete_taskhas succeeded, when the model answers without a tool call, when a step doesn't complete, or atmax_steps.close_tools_on_task_endit refuses tool calls once the task has ended, so the calls left in that step change nothing.configs/officeenv.yamlnow puts it in front of the simple agent (50 steps) in place oflegacy_agent. The sandboxed configs keepsingle_agent_turn_legacy, so their harnesses still end their own runs.Gym core: three opt-in hooks, with today's behavior as their defaults:
SimpleServer.get_session_secret(): the key that signs session cookies and MCP tokens.SimpleServer.get_session_cookie_name(): the session cookie's name.mcp_server_nameandmcp_bearer_tokenonBaseResourcesServerConfig: an MCP endpoint listed under its own name, with a standardAuthorization: Bearertoken.Shutdown needs no new hook. Gym interrupts each server through its process group (#5141), so this shutdown runs at a job's end. Under NeMo RL, whose Ray workers block that interrupt, servers also need #5623. The sandboxed configs set Gym's
server_shutdown_timeout_seconds(also #5141) to 30 s, because its 1 s default is too short to stop a collection's sandboxes. The setting applies to the whole run that composes those configs; other runs keep the default.Configs, data and images
data/sandbox_pilot.jsonl.Validation
gym env test --resources-server officeenv +should_validate_data=true: 201 passed with OfficeEnv 1.10.0 installed. The tests use a fake sandbox provider. Threeapi_docstests skip on 1.10.0 and pass on OfficeEnv's main branch.gym env test +entrypoint=environment_servers/officeenv: 14 passed.single_agent_turnandsingle_agent_turn_legacypass as on main.pytest -m "not sandbox") fails only where main does (the same 33 tests).pre-commitanddetect-secretspass on the changed files, and every commit is signed off.3a19fead73, which has refactor(sessions): one name per session lifecycle concept before release #5083's session names. The component lock check passes for all 271 projects.Rollouts
Smoke runs with
gpt-5.5on Docker sandboxes, on earlier heads of this branch:block_internetwith a harness image.Compatibility and benchmark impact
sandbox_provider: the routes are feat(officeenv): add the OfficeEnv resources server #5009's, plusGET /task_state. Episodes run through OfficeEnv's Environment Server instead oflegacy_agent, and end at the step that completes the task. A run that raised the simple agent'smax_stepsnow raises the Environment Server's. Gym core's defaults are unchanged.pyproject.tomltakesnemo-gym[dev,sandbox], locked as main's other sandbox servers lock it. OfficeEnv installs apart as in feat(officeenv): add the OfficeEnv resources server #5009, and the sandbox tests skip without it.Checklist
resources_servers/officeenv/README.md, "Sandboxed harnesses"; the MCP tutorial).git commit -s).🤖 Generated with Claude Code