Skip to content

feat(officeenv): reach a sandboxed episode's apps as commands or endpoints, for any harness with a shell - #5373

Merged
d-molinari merged 3 commits into
dmolinari/officeenv-sandbox-integrationfrom
dmolinari/officeenv-sandbox-surfaces
Oct 31, 2026
Merged

d-molinari merged 3 commits into
dmolinari/officeenv-sandbox-integrationfrom
dmolinari/officeenv-sandbox-surfaces

Conversation

@d-molinari

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

Copy link
Copy Markdown
Contributor

What does this PR do?

Lets any of Gym's harnesses that borrow an Environment Server episode's sandbox and run a shell work on OfficeEnv's sandbox tasks, not only harnesses that take MCP tool grants. Stacked on #5010, which lends each OfficeEnv episode a sandbox and reaches the apps as one MCP tool per op: this PR adds the two other ways OfficeEnv's exposures offer, as a company's command line tools and as an HTTP API found over MCP. A row's exposure picks one; the world, the checks and the reward are the same for all three. Without sandbox_provider, and for tools rows, the server behaves as in #5010, apart from the workspace guide's first paragraph.

Three commits, all in resources_servers/officeenv:

  1. feat(officeenv): reach a sandboxed episode's apps as commands or endpoints, not only as MCP tools:
    • cli: one command per app in the sandbox, <app> <command> [--parameter=value ...] with <app> --help and <app> <command> --help, read as OfficeEnv's CLI exposure reads a command line. The seed writes sandbox/app_command.sh (POSIX sh and curl, its comments stripped) to /usr/local/lib/company-cli/app, makes it executable and links it as /usr/local/bin/<app> for each connected app before the sandbox is lent. A command sends its arguments, NUL-separated, to POST /v1/cli with the episode's bearer token and prints the result on stdout (exit 0) or the app's error on stderr (exit 1). The MCP grant is empty, so a harness with nothing but a shell runs these rows (resources_tool_transports: []). A call to /v1/cli without the command's <app>-cli/1.0 User-Agent is refused with a pointer to the command and counted (sandbox_commands_refused).
    • api_search: api_search and api_fetch over MCP, with the argument shapes of OfficeEnv's api_search exposure, find and document endpoints; their answers carry each endpoint's HTTP path and the curl command that calls it, and the harness calls POST /v1/<app>/<op> with the op's parameters as a JSON object. An endpoint answers with the app's JSON, or its error under the error's HTTP status. The route is a Starlette route: Gym's MCP auto-exposure takes FastAPI's parameterized POST routes for the tool catch-all and allows one.
    • The file API reaches the file apps whatever the surface (FileApiCalls: a cli episode's only tool is run_command, an api_search episode's are the finding tools and api_call, so it reached no app there). File content and Gmail's attachments stay out of the conversation on every surface, and a command's --help and api_fetch's documentation describe the ops that move content as they behave in the sandbox, Gmail's examples attaching a workspace file by its path (a path already in the workspace named once). Only --help is rewritten, whole lines keyed by app, op and parameter: an op's answer is the app's data and stays as it is. A bare string naming a workspace file attaches it on every surface (--attachments=/workspace/out/totals.xlsx), as feat(officeenv): run OfficeEnv episodes in a sandbox, for Gym's sandboxed harnesses #5010 reads it.
    • The workspace's README.md is written for the episode's surface; sandbox_config.mcp_tool_prefix names MCP tools in it as the harness lists them (company_ in the Pi config). The two routes take the episode's token, answer another surface's episode as routes that do not exist, and run a call as feat(officeenv): run OfficeEnv episodes in a sandbox, for Gym's sandboxed harnesses #5010's tools do (_episode_call): under the episode's lock, so /verify waits for a call under way; refused once verification has begun; and answered 502, with the episode's verification masked, when the provider fails a save or an attachment. An endpoint that fails answers 500 without its exception, which the server logs. /verify adds sandbox_commands, sandbox_commands_refused and sandbox_endpoint_calls. Rows in any other exposure, or with another tool-name separator, are still refused before a sandbox starts.
  2. feat(officeenv): a sandboxed episode's API URL and token in a file too, for shells without its environment. The commands, the file API's curl commands and the endpoints take COMPANY_API_URL and COMPANY_API_TOKEN from the sandbox's environment, but a harness need not pass it to its shell: Codex starts its shell with PATH, LANG, LC_ALL, HOME and the XDG folders alone. The seed also writes the two values to /etc/company-api.env, which a shell sources; the commands read it when the variables are unset, and the guide says so. Its own commit, so it can be dropped if a harness-side option (inherit_sandbox_environment on main) is preferred.
  3. feat(officeenv): sandbox configs for Codex CLI and mini-SWE, and rows in every exposure:
    • configs/officeenv_sandbox_episodes.yaml holds the resources server alone; configs/officeenv_sandbox.yaml composes it with Pi as before (gym env resolve gives the same merged config, Pi's mcp_tool_prefix aside), and configs/officeenv_sandbox_codex.yaml and configs/officeenv_sandbox_miniswe.yaml compose it with Codex CLI and mini-SWE on cli rows. Each config's comments say what the harness installs in the sandbox at session start.
    • sandbox/prepare_rows.py --exposure cli|api_search writes the sandbox tasks in that exposure, with a prompt that says how the apps are reached and how to complete the task, to data/sandbox_pilot_<exposure>_prepare.jsonl (gitignored); --tool-prefix names MCP tools as the harness lists them. Every surface's prompt says where the file API's URL and token are, and now also what a shell without the API's variables runs first (. /etc/company-api.env): Codex's shell has none of them, and in a smoke its first curl failed before it found the file. data/sandbox_pilot.jsonl is emitted again; only that sentence changes.
    • The README says how to run the commands' rows with Codex, mini-SWE or Pi, and lists the names Codex and mini-SWE leave in the sandbox, which their adapters fix (/tmp/nemo-gym-codex-*, /tmp/.nemo-gym/task.json, /tmp/nemo-gym-miniswe-<id>).

Which harnesses

Of the adapters on this base, these borrow an episode's sandbox: Pi, Codex CLI, Hermes, Claude Code, mini-SWE, Terminus 2, OpenCode, Kilo Code, OpenClaw and Stirrup. All of them can run cli rows. Pi, Codex and Hermes also take MCP tool grants in a sandbox session, so they can run tools and api_search rows; OpenCode and Claude Code refuse required MCP grants there, and the others take none. Smoked here: Pi on all three surfaces, Codex CLI and mini-SWE on cli.

What each harness still needs

Found while pairing them; none of it is this server's:

  • A runtime without internet. Each installs at session start: Pi and Codex Node and their package (nodejs.org, npm), mini-SWE its packages from PyPI (Gym uploads its Python and uv), Claude Code, OpenCode, Kilo Code and OpenClaw from npm or GitHub. With block_internet each needs an image with its runtime installed.
  • Tool names on tools rows. With Codex's mcp__company__ and Hermes' mcp_company_, google_workspace_admin_list_licenses_for_product_and_sku is 70 and 68 characters, over the 64 a function name may have; Pi's company_ fits. cli rows avoid it.
  • Pi on the other surfaces takes finish_tool_names=[]: no user_complete_task tool is granted there. Its config's comments and the README say so.
  • The task's date. Pi states the sandbox clock's date in its prompt. The training route pins OpenCode's with a plugin the seed writes; on main, OpenCode's adapter gives OpenCode a private HOME and XDG_CONFIG_HOME, passes its config inline and refuses other opencode_config keys in sandbox sessions, so that needs the adapter.
  • Gym's names in the sandbox, from the adapters: session folders under /tmp/nemo-gym-*, gym-supervisor and gym-runner processes (Pi, Codex), Codex's model_providers.gym argument, mini-SWE's MSWEA_GLOBAL_CONFIG_DIR, OpenCode's --title "NeMo Gym", and the model URL's /ng-rollout/<id> for all of them, as feat(officeenv): run OfficeEnv episodes in a sandbox, for Gym's sandboxed harnesses #5010 lists for Pi.
  • mini-SWE on macOS. Its episode setup builds a deadline from time.monotonic() and hands it to asyncio.timeout_at, which reads the event loop's clock; under uvloop on a Mac that has slept the two differ (1,847 s here) and setup times out at once. Linux is not affected. The smoke below ran mini-SWE with a one-line fix (an asyncio.timeout of the remaining time), which is not in this PR: it belongs to the agent.

Not in this PR

Validation

macOS arm64 and Linux aarch64 (Colossus: the per-commit runs and gym env test), Python 3.13.14, OfficeEnv 1.9.0 (the v1.9.0 tag, as #5009 pins it), on main fc02bfd7f, #5010's head af279fed3, head 0352520d5:

  • gym env test --resources-server officeenv +should_validate_data=true: 151 passed; the data is validated. Each commit passes the server's tests: 147, 147 and 151.
  • The test of what a sandbox reads runs on all three surfaces: the variables, files and setup commands, the MCP endpoint, every command's answer and --help (each app's list too), the endpoints and api_fetch's documentation, the file API, refused and hand-made calls, and the routes it may not use. It finds no environment name; Gmail's own send-as vocabulary (verify_send_as, "verified" addresses) passes by name.
  • The app command as a sandbox has it runs under sh against the server on a socket, with and without the API's variables in its environment, beside the guide's curl commands.
  • Commands and endpoints run a call under the episode's lock, as feat(officeenv): run OfficeEnv episodes in a sandbox, for Gym's sandboxed harnesses #5010's tools do: on both, /verify coming in while a call saves its download waits for the save (without the lock the save meets a released sandbox), and a save the provider fails answers 502 with the verification masked.
  • The server change touches no Gym core file; feat(officeenv): run OfficeEnv episodes in a sandbox, for Gym's sandboxed harnesses #5010's core results stand. The branch merges cleanly into main 402020a86, and the server's 151 tests pass on the merged tree.
  • pre-commit run --all-files: all hooks pass, no changes (the guardwords hook skipped: its private catalog is unavailable here). detect-secrets-hook 1.5.0 over the changed files: no findings.
  • DCO: all three commits are signed off.

Rollouts

Real model smoke, 2026-10-29: macOS with Docker (colima), the officeenv-sandbox:pilot image from sandbox/Dockerfile, azure/openai/gpt-5.5 through the NVIDIA inference gateway, the Docker provider with host_relay, one repeat. The rows: OfficeEnv's nine sandbox tasks (its five released ones and four unreleased ones on files in Box, SharePoint, OneDrive and a Gmail attachment), graded by OfficeEnv's source.

Harness Surface Rows Rewards Median calls Median input tokens
Pi 0.80.2 tools the 4 file tasks 4 of 4 at 1.0 13.5 252K
Pi 0.80.2 api_search 9 9 of 9 at 1.0 21 131K
Pi 0.80.2 cli 9 9 of 9 at 1.0 14 98K
Codex CLI 0.144.4 cli 9 9 of 9 at 1.0 11 189K
mini-SWE 2.4.6 cli 9 9 of 9 at 1.0 13 82K
  • 40 of 40 rollouts at 1.0, every check passing; no failed rollout, no sandbox left running.
  • No tool answer or command output carried file content: downloads landed in the workspace, uploads went through the file API, and the Gmail task's reply attached its workbook by its path on every harness and surface.
  • The cli runs ran 100 to 116 commands each, none refused. Codex, whose shell lacks the API's variables, sourced /etc/company-api.env in seven of nine rollouts. These rollouts predate the prompt's sentence naming that file (commit 3).
  • Scanning each rollout for the names the test checks found none from this server; what matched was Gmail's and the People API's own vocabulary and the models' own words ("verify the totals"). No model listed its environment, its processes or /tmp.

Compatibility

  • Without sandbox_provider: unchanged.
  • tools episodes: the workspace guide now opens with the episode's apps and how the harness names their tools, and Pi's config sets mcp_tool_prefix: company_; the tools, their descriptions and the answers are feat(officeenv): run OfficeEnv episodes in a sandbox, for Gym's sandboxed harnesses #5010's. configs/officeenv_sandbox.yaml includes the new configs/officeenv_sandbox_episodes.yaml, with the same merged result.
  • cli and api_search rows were refused in sandbox mode; they now seed. Other exposures are still refused.

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).
  • 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).

@copy-pr-bot

copy-pr-bot Bot commented Oct 30, 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.

@svcnvidia-nemo-ci svcnvidia-nemo-ci added area:environment Individual environments, benchmarks, verifiers, and environment-specific resources servers feature New capabilities, enhancements, or enablement work labels Oct 30, 2026
@cmunley1
cmunley1 added this pull request to stack #5399 October 31, 2026 00:18
@d-molinari
d-molinari force-pushed the dmolinari/officeenv-sandbox-surfaces branch from 3396629 to 96abc0e Compare October 31, 2026 03:38
d-molinari and others added 3 commits October 30, 2026 20:48
…oints, not only as MCP tools

A sandboxed episode reached its apps only through one MCP tool per op,
so only a harness that takes MCP tool grants in a sandbox session could
run OfficeEnv's tasks. Now 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, as before;
- cli: one command per app in the sandbox (`<app> <command>
  [--parameter=value ...]`, `<app> --help`), read as OfficeEnv's CLI
  exposure reads a command line. The seed writes sandbox/app_command.sh
  (POSIX sh and curl, its comments stripped) to
  /usr/local/lib/company-cli/app, makes it executable and links it as
  /usr/local/bin/<app> for each connected app before the sandbox is
  lent. A command sends its arguments, NUL-separated, to POST /v1/cli
  with the episode's token and prints the result on stdout (exit 0) or
  the app's error on stderr (exit 1). The MCP grant is empty, so any
  harness with a shell and curl runs these rows with
  resources_tool_transports: []. A call to /v1/cli without the command's
  <app>-cli/1.0 User-Agent is refused with a pointer to the command and
  counted;
- api_search: api_search and api_fetch over MCP (the api_search
  exposure's argument shapes) find and document endpoints, with each
  endpoint's HTTP path and the curl command that calls it, and the
  harness calls POST /v1/<app>/<op> with the op's parameters as a JSON
  object. An endpoint answers with the app's JSON, or its error under
  the error's HTTP status. The route is a Starlette one: Gym's MCP
  exposure takes FastAPI's parameterized POST routes for the tool
  catch-all and allows one.

The file API reaches the file apps whatever the surface (FileApiCalls: a
cli episode's only tool is run_command and an api_search episode's are
the finding tools and api_call, so it reached no app there). File
content and Gmail's attachments stay out of the conversation on every
surface, and a command's --help and api_fetch's documentation describe
the ops that move content as they behave here, Gmail's examples
attaching a workspace file by its path (a name already in the workspace
named once). Only --help is rewritten, whole lines keyed by app, op and
parameter: an op's answer is the app's data. The workspace's README.md
is written for the episode's surface, with
sandbox_config.mcp_tool_prefix naming the MCP tools as the harness lists
them (company_ in the Pi config). The two surface routes take the
episode's bearer token, answer other surfaces' episodes as routes that
do not exist, and run a call as the tools do: under the episode's lock,
so /verify waits for a call under way, refused once verification has
begun, and a save or attachment the provider fails answered 502 with the
verification masked. An endpoint that fails answers without its
exception, which the server logs. /verify adds sandbox_commands,
sandbox_commands_refused and sandbox_endpoint_calls. Rows in any other
exposure, or with another tool-name separator, are still refused before
a sandbox starts.

Tests: a cli episode's commands, their setup, answers, exit codes and
refusals; an api_search episode's finding tools, endpoints and their
statuses; the app command as a sandbox gets it, run by sh against a
server on a socket next to the guide's curl commands; the file API on
every surface; file content and Gmail's attachments through commands and
endpoints, a bare string naming a workspace file included
(--attachments=/workspace/out/totals.xlsx), and how --help and api_fetch
describe them, help alone rewritten and whole; the examples' attachment
paths; the guide on each surface; the 409 once verification begins;
/verify waiting for a command or an endpoint saving its download; a save
the provider fails answered 502 on both; a failed setup; an endpoint
that fails. The test of what a sandbox reads runs on all three surfaces
and reads each app's --help too, which names Gmail's own send-as
verification; it lets that vocabulary pass by name. 147 server tests
pass on OfficeEnv 1.9.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Daniele Molinari <dmolinari@nvidia.com>
…o, for shells without its environment

The apps' commands, the file API's curl commands and the endpoints all
take COMPANY_API_URL and COMPANY_API_TOKEN from the sandbox's
environment, but a harness need not pass it to its shell: Codex starts
its shell with PATH, LANG, LC_ALL, HOME and the XDG folders alone, so
on a cli row every command it ran would have called no server.

The seed now also writes the two values to /etc/company-api.env
(CREDENTIALS_PATH), which a shell sources (`. /etc/company-api.env`
sets and exports them); the apps' commands read it when the variables
are unset, and the workspace guide says so on every surface. The file
names the company's API, as the variables do; the token in it is the
one the sandbox's environment already holds.

Tests: the file the seed writes; a command run by sh with no API
variables in its environment, against a server on a socket; a shell
that sources the file; the command and every surface's guide naming
it. 147 server tests pass on OfficeEnv 1.9.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Daniele Molinari <dmolinari@nvidia.com>
… in every exposure

With the apps reachable as commands, any of Gym's harnesses that
borrows an episode's sandbox and runs a shell can run OfficeEnv's
sandbox tasks: it takes the agent's config, the Environment Server's
agent_server.name, and no MCP transports. This adds two, and the rows:

- configs/officeenv_sandbox_episodes.yaml holds the resources server for
  sandboxed episodes alone; configs/officeenv_sandbox.yaml composes it
  with Pi as before (the merged config is the same, Pi's mcp_tool_prefix
  aside);
- configs/officeenv_sandbox_codex.yaml (Codex CLI) and
  configs/officeenv_sandbox_miniswe.yaml (mini-SWE) compose it with
  their harness on rows whose apps are commands,
  resources_tool_transports: []. Their comments say what each installs
  in the sandbox at session start, and for Codex how to run tools rows
  (one tool name is then over 64 characters);
- sandbox/prepare_rows.py --exposure cli|api_search writes the sandbox
  tasks in that exposure, with a prompt that says how the apps are
  reached and how to complete the task there, to
  data/sandbox_pilot_<exposure>_prepare.jsonl (gitignored);
  --tool-prefix names MCP tools as the harness lists them (company_ for
  Pi, the default; mcp__company__ for Codex). Every surface's prompt
  says where the file API's URL and token are, now with the file a shell
  without the API's variables sources first (`. /etc/company-api.env`):
  Codex's shell has none of them, and its first curl failed before it
  found the file. data/sandbox_pilot.jsonl is emitted again; only that
  sentence changes.

The README says how to run a commands' row with Codex, mini-SWE or Pi.

Tests: each harness config binds its harness to the shared episodes and
starts no other; the prompts of every surface name the company's API,
the file for a shell without its variables, and nothing of the
environment; the prepared cli and api_search rows seed as sandboxed
episodes on their surface, with its grant and guide. 151 server tests
pass on OfficeEnv 1.9.0.

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-surfaces branch from 96abc0e to 0352520 Compare October 31, 2026 04:03
@d-molinari
d-molinari merged commit 0352520 into main Oct 31, 2026
5 checks passed
@d-molinari
d-molinari deleted the dmolinari/officeenv-sandbox-surfaces branch October 31, 2026 04:22
@d-molinari

Copy link
Copy Markdown
Contributor Author

Folded into #5010, which now carries these commits (GitHub shows this PR as merged because its commits reached #5010's branch, not main); please review them there.

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