Skip to content

feat(codex): restrict the egress of agent-created sandboxes - #5556

Merged
gwarmstrong merged 22 commits into
NVIDIA-NeMo:mainfrom
gwarmstrong:georgea/codex-agent-egress
Nov 6, 2026
Merged

gwarmstrong merged 22 commits into
NVIDIA-NeMo:mainfrom
gwarmstrong:georgea/codex-agent-egress

Conversation

@gwarmstrong

@gwarmstrong gwarmstrong commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Follows #5554, which has merged; this PR now targets main and shows only this harness's changes.

What does this PR do?

  • codex_agent gains network_access (default inherit) for the sandboxes it creates. The value comes from feat(pi): restrict the egress of agent-created sandboxes #5554's restrict_sandbox_network, with the Model Server's host and, for model_and_tools, each MCP grant's host. A session that borrows a task sandbox rejects any value other than inherit.
  • codex_agent gains sandbox_runtime_dir: an absolute sandbox path that holds a preinstalled runtime in the installer's node/ and codex/ layout. Setup checks that it reports codex-cli <codex_version> instead of installing, and a mismatch fails setup and stops the sandbox.
  • README: both settings. feat(codex_agent): run Codex in a task sandbox like a plain codex exec #5156's task-sandbox settings are unchanged.

Why. The same reason as #5554, for Codex. Without this, a sandbox Codex creates has the provider's default network, and a restricted sandbox cannot download Codex. Stacked on #5554, whose branch this PR targets. Part of #5552.

Validation

  • gym env test +entrypoint=responses_api_agents/codex_agent +should_validate_data=true: 481 passed, 7 skipped. The skips already existed: they need python3.8 or a CODEX_TEST_BIN.

  • With the behavior disabled, the new tests fail: model-only and model-and-tools egress, the borrowed-sandbox rejection, both preinstalled-runtime cases and the absolute-path check. The inherit control passes.

  • Review follow-up: feat(pi): restrict the egress of agent-created sandboxes #5554's review fix merged in (8ba3e02), so this adapter's restrict_sandbox_network call refuses an unenforceable network_access with a terminal 422. test_unenforceable_network_access_is_a_terminal_seed_refusal checks the 422 and that no sandbox is started; it fails with feat(pi): restrict the egress of agent-created sandboxes #5554's helper change reverted. Suite in the adapter's own venv: 482 passed, 7 skipped.

  • pre-commit run --all-files: clean.

  • Follow-up to the feat(pi): restrict the egress of agent-created sandboxes #5554 review: merged its latest head; the helper call passes the provider; a borrowed sandbox with a restricted value is refused with AgentSeedRejectedError; the README says a borrowed sandbox relies on its owner's policy. Its benchmark profile sets network_access: model_only and requires sandbox_runtime_dir. Tool egress now leaves out the MCP grants Codex skips, with a regression test. The adapter's own test_benchmark_profile.py expects model_only (removing it fails the test). Suite: Codex: 521 passed, 7 skipped; profile and helper tests pass.

  • Review follow-up (Yu Yao) at 623cf9a: the preinstalled-runtime check runs with CODEX_HOME under the session directory and checks Python first; a missing runtime, another version (exact) or no usable Python is a 422, a timeout stays retryable; sandbox_transport.py imports on Python 3.8 and 3.9 (checked under both); required MCP grants are refused under model_only; README on building sandbox_runtime_dir. Tests: a runtime-check table, route refusal tests per cause, several grants in the egress test. Codex suite with the profile and helper tests: 560 passed, 7 skipped.

Rollouts

On the internal OpenSandbox deployment used for #5554's egress checks, before this PR's review changes, an image with Codex 0.144.4 preinstalled under /opt/codex-runtime reported codex-cli 0.144.4 with egress limited to the model and tool hosts, while nodejs.org, the npm registry and GitHub's DNS name were blocked.

Internal runs on the georgea/nemo-gym fork at ee833ed, which carries this change before its review updates (Nemotron 3.5 Super on vLLM, 5 text tasks each, that image):

  • HLE with model_only: 5/5 rollouts completed, graded and healthy.
  • HLE with model_and_tools and Gym's Tavily server: 5/5 healthy, with the search tools called in 3 of 5.
  • HLE Diamond with model_only: 5/5 healthy.

Compatibility and benchmark impact

Defaults unchanged: network_access: inherit and no sandbox_runtime_dir.

The adapter's own default stays inherit. Its benchmark profile (from #5550) now sets network_access: model_only and requires sandbox_runtime_dir, so a run that uses the profile gets the restricted sandbox; open-network scores are not comparable.

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, or N/A for docs-only / non-code changes.
  • Documentation added or updated, or N/A with justification. The adapter README describes the new settings.
  • Pre-commit checks pass locally (pre-commit run --all-files).
  • New source files include the required Apache-2.0 SPDX header. N/A: no new source files.
  • All commits have DCO sign-off (git commit -s).

Benchmarks whose Resources server supplies no sandbox, such as HLE, have
pi_agent create one per session. pi_sandboxed_agent ran those sandboxes
with model-only egress; pi_agent could not, so a reasoning benchmark moved
to agent sessions would gain open internet.

- network_access (inherit, model_only, model_and_tools) sets an OpenSandbox
  egress policy on a sandbox the agent creates. A borrowed task sandbox
  keeps its owner's network, so a session that borrows one rejects any
  other value. The policy helper, restrict_sandbox_network, lives in
  nemo_gym.agents.utils.sandbox_network and is exported from
  nemo_gym.agents so other session agents can share it.
- sandbox_runtime_dir points setup at a runtime baked into the image and
  checks its version, since a model-only sandbox cannot download Node or Pi.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
As for pi_agent: network_access sets the OpenSandbox egress policy on a
sandbox Codex creates, and a session that borrows a task sandbox rejects
any other value than inherit. The policy comes from the shared
restrict_sandbox_network helper, imported from nemo_gym.agents, so it
cannot be combined with sandbox_config.provider_options.network_policy.
A model-only sandbox cannot download Node or Codex, so
sandbox_runtime_dir points setup at a runtime baked into the image and
checks that it reports codex-cli <codex_version> instead of installing.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
@copy-pr-bot

copy-pr-bot Bot commented Nov 4, 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.

@gwarmstrong

Copy link
Copy Markdown
Contributor Author

/ok to test e3336c7

@svcnvidia-nemo-ci svcnvidia-nemo-ci left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHIP WITH CARE

RISK — Add representative Pi smoke-rollout evidence

The supplied real rollout results all use Codex, but this changeset also changes Pi's network policy and preinstalled-runtime setup. No representative Pi rollout is recorded. The repository quality bar requires model-backed smoke evidence for behavior-changing agent work. This is a validation gap affecting the new Pi session settings, not a demonstrated runtime failure.

Correction: Record a representative Pi rollout using an agent-owned OpenSandbox sandbox, a preinstalled runtime, and restricted egress. Include the Pi version, image, runtime path, model, configuration/commands, and observed agent and verifier results. No source patch is indicated by this gap.

Acceptance: A proposed model_and_tools smoke run should complete a real model request, successfully call a granted MCP tool, produce a verifier result, and destroy the owned sandbox on close. Record any failure and resolve it before claiming validation. I couldn't run this check here.

Preservation: Retain the existing session-test cases: inherit must continue borrowing without stopping the Resources-owned sandbox; restricted settings must reject borrowed access before sandbox execution; model_only must allow only the model host, while model_and_tools adds the grant host; and a matching preinstalled version must skip installation while a mismatch fails setup.

I reviewed all nine supplied changed files, including the stacked shared helper and Pi changes, and traced policy forwarding, runtime launch, and ownership-aware cleanup. I found no additional qualifying source-level defects. This was a static review; I didn't execute tests or rollouts.

Review generated by codex. This was a full review. Request a full review with /review reset=true.

restrict_sandbox_network raised ValueError when a restricted
network_access could not be enforced: an explicit provider network
policy as well, a provider without network policies, or an endpoint
the sandbox cannot reach. The seed route answered it with a 500, which
the Environment Server retries, though no attempt can fix the
configuration. The helper now raises AgentSeedRejectedError, still a
ValueError, so every adapter that calls it refuses the seed once with
HTTP 422, before any sandbox is created.

Pi also built its tool egress from every MCP grant, including an
optional one it leaves out at activation, so such a grant at a loopback
URL failed the seed. Egress now covers only the grants Pi keeps.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
…egress change

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
…th a 422

With NVIDIA-NeMo#5554's restrict_sandbox_network change, a restricted network_access the provider cannot enforce (here an explicit network_policy as well) refuses the seed with HTTP 422 before any sandbox is created, instead of a retried 500.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
@gwarmstrong

Copy link
Copy Markdown
Contributor Author

On the Pi smoke-evidence finding: the Pi changes in this diff are #5554's, which this PR is stacked on, and their rollout evidence is in #5554's description (the Rollouts section): #5554. It covers internal HLE runs of Pi on an agent-owned OpenSandbox with the preinstalled runtime (sandbox_runtime_dir: /opt in a baked image): model_only 5/5 completed, graded and healthy, and model_and_tools with Gym's Tavily server 5/5 healthy with the search tools called. It also covers egress probes on the same cell: the model and tool hosts were reachable, while nodejs.org, the npm registry, an inference API and GitHub's DNS name were blocked.

This PR now also has #5554's review fix merged in (8ba3e02): restrict_sandbox_network refuses an unenforceable network_access with a terminal 422 instead of a retried 500, and Pi builds tool egress only from the MCP grants it keeps. 01c758a adds this adapter's check that an unenforceable network_access is refused with a 422 before any sandbox is created.

@gwarmstrong

Copy link
Copy Markdown
Contributor Author

/ok to test 01c758a

Both sides added public exports to nemo_gym.agents; both are kept.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
…x egress change

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
@gwarmstrong

Copy link
Copy Markdown
Contributor Author

/ok to test 61b28c9

Addresses Yu Yao's review of the Pi egress change:

- restrict_sandbox_network takes the required tool grants and refuses them
  under model_only with AgentSeedRejectedError. The policy blocks their
  hosts, so a session would otherwise fail only after the sandbox and the
  runtime exist, as a retried 5xx. Pi passes its required MCP grants.
- A host written with a trailing dot no longer yields a rule the egress
  sidecar never matches, and provider_options: null is restricted like an
  empty mapping instead of raising a 500.
- Pi's tests cover both restricted values for a borrowed sandbox, a model
  URL that fails to resolve (the provider is closed), and a preinstalled
  runtime that exits nonzero or reports a version prefix.
- The profile comment says that with sandbox_runtime_dir: null the image
  must hold the runtime at the default path with its ready marker.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
Under model_and_tools, a grant at an unreachable address refuses the seed even when optional, and model_only refuses a required grant.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
Addresses Yu Yao's review:

- The preinstalled-runtime check runs with CODEX_HOME under the session
  directory, so it writes nothing into the task's home, and checks for
  Python 3.8+ first, as the installer does. A runtime that is missing, runs
  another version (exact match) or finds no usable Python refuses the seed
  with AgentSeedRejectedError; a transport error or timeout stays retryable.
- sandbox_transport.py postpones annotation evaluation, so it imports on
  the Python 3.8 and 3.9 the README and installer allow (checked under both).
- Codex passes its required MCP grants to restrict_sandbox_network, so
  model_only refuses them at seed instead of failing after setup.
- The README says how to build sandbox_runtime_dir, that the image needs
  bash and python3, that the setting also applies to a borrowed sandbox
  (checked, never stopped), which grants a restricted seed refuses, and
  that full isolation needs OpenSandbox's dns+nft egress mode.
- Tests: several grants (required, optional, direct_http) in the egress
  test, both restricted values for the borrowed refusal, a route test per
  refusal cause with the provider closed and no session record left, and a
  runtime-check table (prefix, failed run, timeout, runtime used).

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
@github-actions github-actions Bot removed the sla:review-overdue Review response is over the one-business-day SLA label Nov 5, 2026
Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>

NVIDIA-NeMo#5554 was squash-merged, so its files conflicted with this branch's copy of them; main's versions are kept. This PR's own changes are unchanged.
@gwarmstrong
gwarmstrong requested a review from a team as a code owner November 5, 2026 22:37
@gwarmstrong
gwarmstrong changed the base branch from georgea/pi-agent-egress to main November 5, 2026 22:37
@gwarmstrong

Copy link
Copy Markdown
Contributor Author

/ok to test 0bfd75b

Brings in main's latest four commits; this PR's own changes are unchanged.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
@gwarmstrong

Copy link
Copy Markdown
Contributor Author

/ok to test 4b94f94

@yaoyu-33 yaoyu-33 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 4b94f94 (the change is git diff main...head now that #5554 has merged). Everything from my earlier review is addressed, and CI is green on this head.

  • The preinstalled-runtime check now separates the two cases: error_type stays a retryable RuntimeError; a non-zero exit or any other version (exact match) is an AgentSeedRejectedError, so a 422.
  • The check runs with CODEX_HOME under the session directory, so nothing lands in the task's home, and it checks Python 3.8+ first as the installer does; sandbox_transport.py now postpones annotation evaluation so the README's 3.8+ holds.
  • Required MCP grants are refused under model_only at seed; the README says how to build sandbox_runtime_dir, that it also applies to a borrowed sandbox, and the dns+nft caveat.
  • The tests are a table (pinned, other version, 0.144.40 prefix, failed run with the right banner, timeout) and the refusal test is parametrized like Pi's.

Not run here: I did not re-run the suites locally; I read the diff and relied on the green CI at this head.

@yaoyu-33 yaoyu-33 added the ready-to-merge PR is approved, current, and waiting for final CI or merge label Nov 6, 2026
@gwarmstrong
gwarmstrong added this pull request to the merge queue Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/42137145215

Merged via the queue into NVIDIA-NeMo:main with commit 7b46b3c Nov 6, 2026
61 checks passed
@gwarmstrong
gwarmstrong deleted the georgea/codex-agent-egress branch November 6, 2026 00:44

This branch was previously deployed

1 inactive deployment
public — 4b94f947 Deployed Nov 5, 2026 by copy-pr-bot[bot] via release / finalize / notify #10546
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge PR is approved, current, and waiting for final CI or merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants