Repository navigation
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 intoOct 31, 2026
Conversation
This was referenced Oct 30, 2026
d-molinari
force-pushed
the
dmolinari/officeenv-sandbox-surfaces
branch
from
October 31, 2026 03:38
3396629 to
96abc0e
Compare
…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
force-pushed
the
dmolinari/officeenv-sandbox-surfaces
branch
from
October 31, 2026 04:03
96abc0e to
0352520
Compare
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 fortoolsrows, the server behaves as in #5010, apart from the workspace guide's first paragraph.Three commits, all in
resources_servers/officeenv: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> --helpand<app> <command> --help, read as OfficeEnv's CLI exposure reads a command line. The seed writessandbox/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, toPOST /v1/cliwith 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/cliwithout the command's<app>-cli/1.0User-Agent is refused with a pointer to the command and counted (sandbox_commands_refused).api_search:api_searchandapi_fetchover 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 callsPOST /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.FileApiCalls: acliepisode's only tool isrun_command, anapi_searchepisode's are the finding tools andapi_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--helpandapi_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--helpis 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.README.mdis written for the episode's surface;sandbox_config.mcp_tool_prefixnames 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/verifywaits 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./verifyaddssandbox_commands,sandbox_commands_refusedandsandbox_endpoint_calls. Rows in any other exposure, or with another tool-name separator, are still refused before a sandbox starts.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 takeCOMPANY_API_URLandCOMPANY_API_TOKENfrom the sandbox's environment, but a harness need not pass it to its shell: Codex starts its shell withPATH,LANG,LC_ALL,HOMEand 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_environmenton main) is preferred.feat(officeenv): sandbox configs for Codex CLI and mini-SWE, and rows in every exposure:configs/officeenv_sandbox_episodes.yamlholds the resources server alone;configs/officeenv_sandbox.yamlcomposes it with Pi as before (gym env resolvegives the same merged config, Pi'smcp_tool_prefixaside), andconfigs/officeenv_sandbox_codex.yamlandconfigs/officeenv_sandbox_miniswe.yamlcompose it with Codex CLI and mini-SWE onclirows. Each config's comments say what the harness installs in the sandbox at session start.sandbox/prepare_rows.py --exposure cli|api_searchwrites the sandbox tasks in that exposure, with a prompt that says how the apps are reached and how to complete the task, todata/sandbox_pilot_<exposure>_prepare.jsonl(gitignored);--tool-prefixnames 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.jsonlis emitted again; only that sentence changes./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
clirows. Pi, Codex and Hermes also take MCP tool grants in a sandbox session, so they can runtoolsandapi_searchrows; 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 oncli.What each harness still needs
Found while pairing them; none of it is this server's:
block_interneteach needs an image with its runtime installed.toolsrows. With Codex'smcp__company__and Hermes'mcp_company_,google_workspace_admin_list_licenses_for_product_and_skuis 70 and 68 characters, over the 64 a function name may have; Pi'scompany_fits.clirows avoid it.finish_tool_names=[]: nouser_complete_tasktool is granted there. Its config's comments and the README say so.HOMEandXDG_CONFIG_HOME, passes its config inline and refuses otheropencode_configkeys in sandbox sessions, so that needs the adapter./tmp/nemo-gym-*,gym-supervisorandgym-runnerprocesses (Pi, Codex), Codex'smodel_providers.gymargument, mini-SWE'sMSWEA_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.time.monotonic()and hands it toasyncio.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 (anasyncio.timeoutof 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 (thev1.9.0tag, as #5009 pins it), on mainfc02bfd7f, #5010's headaf279fed3, head0352520d5: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.--help(each app's list too), the endpoints andapi_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.shagainst the server on a socket, with and without the API's variables in its environment, beside the guide's curl commands./verifycoming 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.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-hook1.5.0 over the changed files: no findings.Rollouts
Real model smoke, 2026-10-29: macOS with Docker (colima), the
officeenv-sandbox:pilotimage fromsandbox/Dockerfile,azure/openai/gpt-5.5through the NVIDIA inference gateway, the Docker provider withhost_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.toolsapi_searchclicliclicliruns ran 100 to 116 commands each, none refused. Codex, whose shell lacks the API's variables, sourced/etc/company-api.envin seven of nine rollouts. These rollouts predate the prompt's sentence naming that file (commit 3)./tmp.Compatibility
sandbox_provider: unchanged.toolsepisodes: the workspace guide now opens with the episode's apps and how the harness names their tools, and Pi's config setsmcp_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.yamlincludes the newconfigs/officeenv_sandbox_episodes.yaml, with the same merged result.cliandapi_searchrows were refused in sandbox mode; they now seed. Other exposures are still refused.Checklist
resources_servers/officeenv/README.md).git commit -s).