Skip to content

feat(officeenv): run OfficeEnv episodes in a sandbox, for Gym's sandboxed harnesses - #5010

Draft
d-molinari wants to merge 9 commits into
mainfrom
dmolinari/officeenv-sandbox-integration
Draft

d-molinari wants to merge 9 commits into
mainfrom
dmolinari/officeenv-sandbox-integration

Conversation

@d-molinari

@d-molinari d-molinari commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Lets Gym's sandboxed harnesses work on OfficeEnv tasks. With sandbox_provider set, 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. Without sandbox_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

  • Start and stop. The seed starts the episode's sandbox and writes a short guide (README.md) into the workspace. /verify stops the sandbox before grading, so nothing the harness left running can change the reward.
  • Seeds take turns. Each seed builds its world on the server's single event loop. At about 1,000 episodes in flight, a wave of seeds held the loop for minutes, and Pi's MCP setup timed out after 30 s. Seeds now run one at a time, and before each the loop serves waiting requests for 100 ms, so a burst of harness setups is answered between builds. Each seed logs its place in the queue, its wait and its build time.
  • Reaching the apps. The row picks one of three ways:
    • MCP tools, for Pi and Codex;
    • a command per app in the shell (<app> <command> --parameter=value), for any harness with a shell;
    • HTTP endpoints the model looks up over MCP. The documentation tool goes by the name the row's exposure gives it: api_fetch, or api_docs with OfficeEnv 1.11.0's docs_tool option.
  • Files go through the workspace, not the conversation.
    • Downloads from Drive, Box, SharePoint and OneDrive, and Gmail attachments, are saved as files and answered with their path.
    • Uploads send a workspace file through a per-episode file API, and an email attaches a workspace file by its path.
    • Pasted base64 is refused, with the command to use instead.
  • The sandbox sees an ordinary company API (COMPANY_API_URL, company_* tools). Nothing this server shows the sandbox names the environment or Gym, and a test checks all of it.
  • Guarded. Only Gym's node (and trusted_networks) may seed, verify or call the tool routes, and sessions are signed with a per-process secret. block_internet is opt-in and works on OpenSandbox only; it limits a sandbox's network to this server and the model servers.
  • Provider failures. If the provider fails while saving or reading a file, the call fails (502) and the episode's reward is masked (mask_sample), as fix(harness_agent): mask rollouts the sandbox provider reports as failed #5354 does for a lost sandbox.
  • Shutdown. When the server shuts down (a job's end), it stops every lent sandbox at once, 64 at a time, within Gym's server_shutdown_timeout_seconds less 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:

  • It runs the single-agent-turn protocol unchanged (it subclasses single_agent_turn_legacy).
  • With max_steps set, it drives the agent one step per activation (the simple agent with max_steps: 1), passing the conversation so far as input.
  • Between steps it reads the OfficeEnv server's new GET /task_state. The episode ends before the next model call once complete_task has succeeded, when the model answers without a tool call, when a step doesn't complete, or at max_steps.
  • The resources server only answers the read, with the state each session's last tool call left, published when that whole call is over. With close_tools_on_task_end it refuses tool calls once the task has ended, so the calls left in that step change nothing.
  • configs/officeenv.yaml now puts it in front of the simple agent (50 steps) in place of legacy_agent. The sandboxed configs keep single_agent_turn_legacy, so their harnesses still end their own runs.

Gym core: three opt-in hooks, with today's behavior as their defaults:

  1. SimpleServer.get_session_secret(): the key that signs session cookies and MCP tokens.
  2. SimpleServer.get_session_cookie_name(): the session cookie's name.
  3. mcp_server_name and mcp_bearer_token on BaseResourcesServerConfig: an MCP endpoint listed under its own name, with a standard Authorization: Bearer token.

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

  • Configs for Pi (Docker and OpenSandbox), Codex CLI, mini-SWE, OpenCode, OpenClaw and Terminus 2.
    • Pi, Codex, OpenCode and OpenClaw each run a limited number of episodes at once. Their configs cap the Environment Server's admission to match, so no episode waits for its harness while its sandbox's TTL runs.
  • Data: nine sandbox tasks in data/sandbox_pilot.jsonl.
  • Images:
    • an optional documents image (Word, PDF and LibreOffice tools), for rows that ask for it;
    • one image per harness with its runtime preinstalled, for sandboxes without the internet.
  • Docs: the server README's "Sandboxed harnesses" section covers the setup.

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. Three api_docs tests skip on 1.10.0 and pass on OfficeEnv's main branch.
  • gym env test +entrypoint=environment_servers/officeenv: 14 passed. single_agent_turn and single_agent_turn_legacy pass as on main.
  • End to end, with the real simple agent, Environment Server and OfficeEnv server and a stub policy: a task the model completes takes one model call, and the call left in that step is refused; a task without one takes two (tool call, then answer). Both are graded.
  • Gym's core suite (pytest -m "not sandbox") fails only where main does (the same 33 tests).
  • pre-commit and detect-secrets pass on the changed files, and every commit is signed off.
  • Rebased onto main 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.5 on Docker sandboxes, on earlier heads of this branch:

  • Pi, Codex CLI, mini-SWE, OpenCode, OpenClaw and Terminus 2 across the sandbox tasks: 103 rollouts, all at reward 1.0. mini-SWE needed a one-line fix of its own on macOS, which belongs in the agent.
  • No file content passed through the conversation, and no rollout shows this server's environment names.
  • Not yet run: episodes on an OpenSandbox cell, and block_internet with a harness image.

Compatibility and benchmark impact

  • Without sandbox_provider: the routes are feat(officeenv): add the OfficeEnv resources server #5009's, plus GET /task_state. Episodes run through OfficeEnv's Environment Server instead of legacy_agent, and end at the step that completes the task. A run that raised the simple agent's max_steps now raises the Environment Server's. Gym core's defaults are unchanged.
  • Dependency: the server's pyproject.toml takes nemo-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.
  • Known limits: the harness adapters still show some Gym names (their folders, variables and system prompts). The README lists them.

Checklist

  • I have read the contributing guidelines.
  • The change is focused; unrelated "drive-by" edits are tracked as separate issues/PRs.
  • Tests added or updated and pass locally.
  • Documentation added or updated (resources_servers/officeenv/README.md, "Sandboxed harnesses"; the MCP tutorial).
  • Pre-commit checks pass locally.
  • New source files include the required Apache-2.0 SPDX header.
  • All commits have DCO sign-off (git commit -s).

🤖 Generated with Claude Code

@copy-pr-bot

copy-pr-bot Bot commented Oct 23, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@d-molinari d-molinari added area:environment Individual environments, benchmarks, verifiers, and environment-specific resources servers feature New capabilities, enhancements, or enablement work labels Oct 23, 2026
@d-molinari
d-molinari force-pushed the dmolinari/officeenv-sandbox-integration branch 3 times, most recently from 64c29c9 to 7f3b747 Compare October 29, 2026 03:44
@cmunley1
cmunley1 added this pull request to stack #5399 October 31, 2026 00:18
@cmunley1

Copy link
Copy Markdown
Contributor

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)
_read_attachments catches every exception from episode.sandbox.download, then copied_file reports the path as missing. A timeout or a stopped sandbox becomes "there is no file at /workspace/notes.csv in the workspace", so an infra failure is scored as a model mistake. Suggest catching only the provider's not-found case and letting the rest fail the call.

2. _save_downloads can race /verify releasing the sandbox (app.py L1608-L1616)
After the mkdir await, _stop_before_grading can run _release, which sets episode.sandbox = None. The next episode.sandbox.upload raises AttributeError, answered as 502 "Could not save ... in the workspace" after the world change already happened. Holding the episode lock for the save, or re-checking the sandbox after each await, would close it.

3. (nit) shutdown can miss a sandbox a concurrent seed is still starting (app.py L1333-L1338)
shutdown walks the episodes without the per-episode lock, and the seed inserts the episode before it assigns episode.sandbox. That sandbox is left to the provider TTL.

4. (nit) _locks entries are never removed (app.py L1133)
setdefault at L1247, L1371, L1400 and L1409 with no pop, including close calls for never-seeded ids, so the dict grows for the life of the process.

@d-molinari
d-molinari force-pushed the dmolinari/officeenv-sandbox-integration branch from 20676db to af279fe Compare October 31, 2026 03:38
@d-molinari d-molinari changed the title feat(officeenv): lend each Environment Server episode a sandbox feat(officeenv): run OfficeEnv episodes in a sandbox, for Gym's sandboxed harnesses Oct 31, 2026
@d-molinari
d-molinari force-pushed the dmolinari/officeenv-sandbox-integration branch from 6f6ae28 to 6ea2e28 Compare November 3, 2026 01:40
@d-molinari
d-molinari force-pushed the dmolinari/officeenv-sandbox-integration branch from 6ea2e28 to 199654c Compare November 3, 2026 17:28
@d-molinari

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All four are fixed on the new head (199654c34, rebased onto main 154d77914), each in the commit that introduced the code, with tests that fail without the fix. #5373 and #5374 are now folded into this PR (commits 13–19), so their routes are covered here too.

1. Provider failure scored as a missing attachment (feat(officeenv): keep Gmail attachments out of a sandboxed episode's conversation)

  • Only a file that is gone counts as missing now. After a failed download, the path is checked again: if it still holds a regular file, the provider failed, and the call fails (502). The provider's words go to the server's log and the verify's failure_reason, never to the model. If the file is gone since the check, it is missing, as before.
  • A check that doesn't run fails the call the same way. That covers stat's exec failing, and fewer output lines than paths. Before, such a check read as "every path missing".
  • Either failure also masks the episode's verification (mask_sample, failure_kind: provider_unavailable), the convention fix(harness_agent): mask rollouts the sandbox provider reports as failed #5354 uses on the harness side. So the reward isn't scored against the policy.
  • The same applies to a save the provider fails in _save_downloads. A save the workspace blocks (a folder at the file's path, or a file where its folder goes) stays the model's doing: 502, graded as usual.

2. _save_downloads racing /verify (feat(officeenv): keep Drive file content out of a sandboxed episode's conversation)

  • A call now holds the episode's lock through the whole call: the attachment read, the service call and the saves (_episode_call).
  • _stop_before_grading releases the sandbox under the same lock, so verification waits for a call already under way. A call that comes after verification has begun is refused.
  • The cli and endpoint routes (commit 13, from feat(officeenv): reach a sandboxed episode's apps as commands or endpoints, for any harness with a shell #5373) go through the same helper. The endpoint route re-raises the helper's HTTPExceptions, so a 409 or 502 doesn't turn into its 500.
  • The test drives your scenario: /verify starts at the save's mkdir. Without the lock, it gets the 502 from the AttributeError.

3. shutdown missing a sandbox a seed is still starting (feat(officeenv): lend each Environment Server episode a sandbox)

  • shutdown takes each episode's lock, so it waits for a seed that is still starting a sandbox. That includes a seed still waiting for its lock, which has no episode yet.
  • It sets a flag first, so a seed that comes later is refused (503) before it starts a sandbox.
  • retire_session_state had the same shape: it took the lock only if the episode already had a sandbox. It now takes the lock whenever there's an episode, so it waits for a starting sandbox and stops it too.

4. _locks never shrinking (same commit)

  • _locks is a weakref.WeakValueDictionary: a lock stays only while a request holds or awaits it.
  • So closed sessions leave nothing behind, and neither does a close for an ID that was never seeded.

Since your review: episodes waiting with a sandbox lent (fix(officeenv): admit no more sandboxed episodes than the harness runs at once, commit 21)

A training run with Pi on an OpenSandbox cell found another lifetime gap:

  • The seed lends an episode its sandbox, and the sandbox's TTL runs from its create. Pi runs 8 episodes at once by default, and the Environment Server admitted every episode a collection sent.
  • So the rest waited for Pi with their sandboxes lent: of 128 episodes seeded at once with a 1 h TTL, 30 lost their sandbox ("Sandbox not found").
  • The harness configs now cap the Environment Server's admission at the harness's own concurrency, by interpolation, so raising one raises both. That covers Pi, Codex, OpenCode and OpenClaw. The other episodes wait there, before their seed, holding no sandbox, for up to a day.
  • mini-SWE and Terminus 2 have no cap of their own and run every episode they are given, so their configs are unchanged.
  • tests/test_sandbox_configs.py composes each config as gym env start does. Its admission checks fail on the previous configs.

If your failing tests differ from the ones added here, I'd be glad to run them against the new head.

@d-molinari
d-molinari force-pushed the dmolinari/officeenv-sandbox-integration branch 2 times, most recently from f492d3a to e0f9d7a Compare November 3, 2026 21:22
@d-molinari

Copy link
Copy Markdown
Contributor Author

Two commits since my reply above, both on the shutdown path from your item 3:

  • fix(officeenv): stop every lent sandbox at once when the server shuts down. Shutdown now stops the lent sandboxes concurrently, 64 at a time, within Gym's server_shutdown_timeout_seconds less 2 s; the sandboxed configs set it to 30 s. Before, it stopped them one by one with no deadline. It still waits for a seed that is starting a sandbox, but only until that deadline. Past it, it logs the sandboxes left running or starting at ERROR, with their sessions, for a reaper.
  • fix(officeenv): seeds take turns on the event loop, so a harness's MCP setup is not queued behind them. World builds now run one at a time, and before each the loop serves waiting requests for up to 10 ms. A seed still waiting for its turn when shutdown begins is refused (503) in its turn, before anything is built.

@d-molinari
d-molinari force-pushed the dmolinari/officeenv-sandbox-integration branch from e117d54 to 242eff5 Compare November 4, 2026 19:19
@d-molinari
d-molinari force-pushed the dmolinari/officeenv-sandbox-integration branch from 242eff5 to 12836ab Compare November 4, 2026 19:38
@d-molinari
d-molinari force-pushed the dmolinari/officeenv-sandbox-integration branch from 12836ab to ea9cfe3 Compare November 4, 2026 20:12
Base automatically changed from dmolinari/officeenv-resources-server to main November 5, 2026 00:12
@d-molinari
d-molinari force-pushed the dmolinari/officeenv-sandbox-integration branch from ea9cfe3 to 6d0de28 Compare November 5, 2026 00:41
@d-molinari
d-molinari force-pushed the dmolinari/officeenv-sandbox-integration branch 2 times, most recently from ff660a3 to 80540a8 Compare November 6, 2026 00:28
d-molinari and others added 9 commits November 5, 2026 17:03
…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>
@d-molinari
d-molinari force-pushed the dmolinari/officeenv-sandbox-integration branch from 80540a8 to 7f3bb6e Compare November 6, 2026 01:03

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:environment Individual environments, benchmarks, verifiers, and environment-specific resources servers feature New capabilities, enhancements, or enablement work

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants