Skip to content

feat(sandbox): native platform sandbox backends (ADR-0060) - #532

Merged
delete-cloud merged 3 commits into
mainfrom
sandbox/adr-0060-native-backends
Jun 3, 2026
Merged

delete-cloud merged 3 commits into
mainfrom
sandbox/adr-0060-native-backends

Conversation

@delete-cloud

@delete-cloud delete-cloud commented Jun 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Implements ADR-0060: converge sandbox_mode and add platform-native sandbox backends, copying the shape of Codex's macOS/Linux sandboxes without expanding the config surface.

  • sandbox_mode converges to none | native | podman | docker
  • nsjail dropped from validation (configs setting it are now rejected). NsjailSandboxRunner left temporarily, unreachable, slated for removal.
  • native resolves per-platform via _resolve_native() that build_sandbox() delegates to:
    • Darwin → MacosSeatbeltSandboxRunner (sandbox-exec, deny-default profile: reads allowed, writes confined to workspace + scratch, network denied)
    • Linux → LinuxNativeSandboxRunner (bubblewrap: --ro-bind / / + rw workspace bind, --unshare-net, cwd pinned)
    • Windows → fail closed
  • PodmanSandboxRunner added as an explicit container backend (mirrors Docker; --network none, no-new-privileges, workspace bind). Never auto-selected by native.
  • All backends fail closed when their binary is missing (no silent downgrade to none).
  • Default stays none — flipping to native is deferred to a later PR.

Test plan

  • uv run pytest tests/tools/test_shell.py → 42/42 pass (16 new tests)
  • uv run ruff check src/coding_agent/tools/sandbox.py src/coding_agent/tools/shell.py
  • Acceptance criteria in ADR-0060 all checked.

Reviewer notes / follow-ups (deferred per ADR)

  • ⚠️ Seatbelt/bubblewrap profiles follow Codex's shape but are not byte-copied from Codex source (not vendored here). Worth a careful review before the default is ever flipped to native.
  • Breaking change: sandbox_mode = "nsjail" now rejected — needs a changelog note. No in-repo config uses it (agent.toml defaults to none).
  • Future PRs: flip default to native; remove orphaned NsjailSandboxRunner.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced "native" sandbox mode with platform-specific backends (Seatbelt on macOS, bubblewrap on Linux); Windows fails gracefully when unsupported.
    • Added "podman" sandbox mode as an alternative container backend.
    • Removed "nsjail" sandbox mode from supported options.
  • Documentation

    • Added architecture decision record documenting sandbox mode convergence and platform-specific behavior.
  • Tests

    • Added comprehensive test coverage for native sandbox resolution and runner behavior across platforms.

Converge sandbox_mode to none|native|podman|docker and drop nsjail from
validation (configs setting it are now rejected; NsjailSandboxRunner kept
temporarily, unreachable, slated for removal).

native resolves per-platform via _resolve_native() that build_sandbox()
delegates to:
- Darwin  -> MacosSeatbeltSandboxRunner (sandbox-exec, deny-default profile)
- Linux   -> LinuxNativeSandboxRunner (bubblewrap, ro root + rw workspace)
- Windows -> fail closed

Add explicit PodmanSandboxRunner mirroring Docker. All backends fail closed
when their binary is missing rather than silently downgrading. Default
sandbox_mode stays none; flipping to native is deferred to a later PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@delete-cloud, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 32 minutes and 40 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7f9a6032-3c73-4761-8c54-e1c598044412

📥 Commits

Reviewing files that changed from the base of the PR and between 4c965b6 and 5897641.

📒 Files selected for processing (4)
  • coding-agent/docs/adr/0060-native-platform-sandbox-backends.md
  • coding-agent/src/coding_agent/tools/sandbox.py
  • coding-agent/src/coding_agent/tools/shell.py
  • coding-agent/tests/tools/test_shell.py
📝 Walkthrough

Walkthrough

This PR implements ADR-0060, converging sandbox modes by replacing nsjail with native (platform-dispatched to macOS Seatbelt or Linux bubblewrap runners) and podman. The public SandboxMode type, build_sandbox() routing logic, native/podman runner implementations, validation, and comprehensive tests form a cohesive change that preserves existing docker and none behavior while adding platform-native sandboxing.

Changes

Sandbox Mode Convergence with Native Platform Backends

Layer / File(s) Summary
ADR Specification and Public Type Contract
coding-agent/docs/adr/0060-native-platform-sandbox-backends.md, coding-agent/src/coding_agent/tools/sandbox.py
ADR-0060 documents the sandbox mode convergence, platform resolution flow inside build_sandbox(), and safety contract preservation. SandboxMode type changes from ["none", "nsjail", "docker"] to ["none", "native", "podman", "docker"].
Platform Resolution and Native Runners
coding-agent/src/coding_agent/tools/sandbox.py
_resolve_native() dispatcher selects MacosSeatbeltSandboxRunner (builds sandbox-exec profile with deny-by-default rules) on macOS or LinuxNativeSandboxRunner (builds bwrap command with --unshare-net and workspace isolation) on Linux; both fail-closed when required binaries are unavailable. build_sandbox() routes "native" through this dispatcher.
Podman Container Backend
coding-agent/src/coding_agent/tools/sandbox.py
PodmanSandboxRunner instantiates podman run with --network none, workspace bind-mounts, optional CPU/memory limits, and validated environment propagation. Helper _macos_tmpdir() computes the writable scratch subpath for the macOS Seatbelt profile.
Sandbox Mode Validation
coding-agent/src/coding_agent/tools/shell.py
Validation set updated to reject nsjail and accept only none, native, podman, and docker modes.
Comprehensive Test Coverage
coding-agent/tests/tools/test_shell.py
TestNativeSandboxResolution verifies platform dispatch (macOS/Linux selection, Windows fail-closed), rejects nsjail. TestMacosSeatbeltSandbox validates sandbox-exec invocation with network denial and workspace pinning. TestLinuxNativeSandbox validates bwrap flags and network isolation. TestPodmanSandbox validates podman run container construction and fail-closed behavior. All runner tests confirm missing-binary fail-closed behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 In sandboxes deep, both native and cloud,
Platform-aware runners make boundaries loud—
macOS with Seatbelt, Linux with bwrap's might,
Podman joins Docker to wall off the night,
nsjail departs; safety contracts hold tight! 🔒

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: implementing native platform sandbox backends per ADR-0060, with concise, focused phrasing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sandbox/adr-0060-native-backends

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
coding-agent/tests/tools/test_shell.py (1)

626-683: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Lock down the Podman parity contract too.

These tests currently prove --network none and the workspace bind, but the PR contract also says Podman mirrors Docker’s no-new-privileges hardening and explicit/validated env propagation. Without assertions for those, a Podman-specific privilege or env leak regression would still pass.

Suggested follow-up coverage
         assert command[:3] == ["podman", "run", "--rm"]
         assert "--network" in command
         assert command[command.index("--network") + 1] == "none"
+        assert "--security-opt" in command
+        assert command[command.index("--security-opt") + 1] == "no-new-privileges"
         root = str(workspace.resolve())
         assert any(
             f"type=bind,src={root},dst={root}" in part for part in command
         )
         assert command[-2:] == ["echo", "hi"]

I’d also mirror the existing Docker env tests here so Podman is covered for:

  • explicit env only
  • rejection of unsafe env names
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@coding-agent/tests/tools/test_shell.py` around lines 626 - 683, The Podman
test is missing parity checks for Docker hardening and env handling: update
test_podman_runner_builds_podman_run_with_network_none_and_workspace_bind to
also assert that the generated command includes "--no-new-privileges" and that
environment propagation matches the Docker contract (i.e., only explicitly
provided safe env entries from SandboxRequest.env are translated into "-e" /
"--env" parts and unsafe names are not); additionally add two Podman-specific
tests mirroring the Docker env coverage that call PodmanSandboxRunner.run (using
SandboxRequest) to verify explicit env-only propagation and rejection of unsafe
env names. Reference PodmanSandboxRunner.run, SandboxRequest, and the existing
test functions when adding these assertions/tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@coding-agent/src/coding_agent/tools/sandbox.py`:
- Around line 118-133: The native runner's run method ignores
SandboxConfig.limits so cpu_limit_seconds and memory_limit_mb are dropped;
update run (and the analogous native runner at lines 168-183) to apply
configured resource limits by reusing _resource_limit_preexec() as the
subprocess preexec_fn when limits are present, passing the request limits into
that helper, and include the preexec_fn in the subprocess.run call; if a
specific limit cannot be enforced by the native backend, explicitly raise
SandboxUnavailableError or ValueError indicating the unsupported limit instead
of silently dropping it.
- Around line 125-133: The subprocess.run invocation in bash_run (and the
similar call later) currently forwards request.env (which may be None) into
subprocess.run causing inheritance of the host environment; instead construct
and pass an explicit minimal env allowlist (e.g., only PATH, HOME or specific
safe keys) or an empty dict when request.env is None, merging request.env only
after validating/whitelisting keys; update the calls in bash_run and the second
run site to pass that sanitized env dict rather than forwarding None so the
sandbox cannot see host secrets and adheres to the ADR’s explicit/controlled env
contract.

In `@coding-agent/tests/tools/test_shell.py`:
- Around line 565-595: The test
test_linux_native_command_includes_network_off_workspace_bind_and_cwd must also
assert the read-only root bind from ADR-0060; after capturing the generated
command (captured["command"] / variable command) add an assertion that the
command contains the '--ro-bind' token and that the following two entries are
'/' and '/' (i.e., verify the sequence ["--ro-bind", "/", "/"]) so the bwrap
invocation enforces a read-only root mount; keep this check alongside the
existing workspace bind and --chdir assertions.

---

Outside diff comments:
In `@coding-agent/tests/tools/test_shell.py`:
- Around line 626-683: The Podman test is missing parity checks for Docker
hardening and env handling: update
test_podman_runner_builds_podman_run_with_network_none_and_workspace_bind to
also assert that the generated command includes "--no-new-privileges" and that
environment propagation matches the Docker contract (i.e., only explicitly
provided safe env entries from SandboxRequest.env are translated into "-e" /
"--env" parts and unsafe names are not); additionally add two Podman-specific
tests mirroring the Docker env coverage that call PodmanSandboxRunner.run (using
SandboxRequest) to verify explicit env-only propagation and rejection of unsafe
env names. Reference PodmanSandboxRunner.run, SandboxRequest, and the existing
test functions when adding these assertions/tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5cd52334-90c7-47e2-87fe-006fb43982c4

📥 Commits

Reviewing files that changed from the base of the PR and between 4c965b6 and a7c883e.

📒 Files selected for processing (4)
  • coding-agent/docs/adr/0060-native-platform-sandbox-backends.md
  • coding-agent/src/coding_agent/tools/sandbox.py
  • coding-agent/src/coding_agent/tools/shell.py
  • coding-agent/tests/tools/test_shell.py

Comment thread coding-agent/tests/tools/test_shell.py Resolved
delete-cloud and others added 2 commits June 4, 2026 01:24
- Critical: native runners no longer inherit the host environment.
  _native_sandbox_env() exposes only a minimal allowlist when no env is
  supplied, and uses explicit env verbatim (validated) otherwise — honoring
  ADR-0060's explicit/controlled-env contract.
- Apply SandboxConfig.limits in native runners via _resource_limit_preexec();
  reject memory limits on macOS Seatbelt (consistent with none mode) instead
  of silently dropping them.
- Tests: assert bwrap --ro-bind / /, assert native env is not host-inherited,
  cover _native_sandbox_env allowlist/explicit/unsafe-name paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assert podman uses --security-opt no-new-privileges, and add explicit-env-only
and unsafe-env-name tests mirroring the Docker coverage, so a Podman-specific
privilege or env-leak regression is caught.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@delete-cloud

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Addressed all four findings from the previous review:

  • Native runners no longer inherit host env (_native_sandbox_env allowlist) — commit 6938e83
  • Native runners apply SandboxConfig.limits via _resource_limit_preexec; macOS rejects memory limits — commit 6938e83
  • Linux test asserts --ro-bind / / — commit 6938e83
  • Podman parity: assert no-new-privileges, explicit-env-only, unsafe-name rejection — this commit

@coderabbitai

coderabbitai Bot commented Jun 3, 2026 •

Copy link
Copy Markdown
Contributor

@delete-cloud Thanks for the detailed summary of the fixes — the four addressed findings look solid. Triggering a fresh review now.

✅ Action performed

Full review finished.

@delete-cloud
delete-cloud merged commit 249c858 into main Jun 3, 2026
1 check passed
@delete-cloud
delete-cloud deleted the sandbox/adr-0060-native-backends branch June 3, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant