Skip to content

feat(sandbox): disable the sandbox via config (#687)#746

Merged
gnanam1990 merged 3 commits into
mainfrom
fix/sandbox-enabled-config
Jul 20, 2026
Merged

feat(sandbox): disable the sandbox via config (#687)#746
gnanam1990 merged 3 commits into
mainfrom
fix/sandbox-enabled-config

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #687.

Adds a sandbox.enabled field to config, so "sandbox": {"enabled": false} turns the sandbox off. The engine already had a fully-wired ModeDisabled (it short-circuits evaluation and returns unwrapped command plans), but no config surface reached it.

Changes

  • SandboxConfig.Enabled *bool — a pointer so an explicit false (disable) is distinct from an omitted key (keep the default: enabled). true/omitted keep enforcement.
  • mergeConfig honors it, so global (user) config and CLI can turn the sandbox off; applyConfiguredSandboxPolicy maps enabled:false to sandbox.ModeDisabled.
  • mergeProjectConfig intentionally does NOT merge it — a cloned repo's .zero/config.json must not be able to disable the sandbox that constrains it. Same posture the file already takes for AdditionalWriteRoots (never from project) and Network (project may only tighten to deny).

Scope

internal/config + internal/cli only. Additive field, no behavior change when the key is absent. No performance change expected.

Testing

  • TestResolveSandboxEnabledUserConfigDisables — global config enabled:false resolves to a false pointer.
  • TestResolveSandboxDisableIgnoredFromProjectConfigsecurity: a project config enabled:false is ignored (resolves to nil), so a repo cannot disable the sandbox.
  • TestApplyConfiguredSandboxPolicyEnabledFalseDisablesenabled:falseModeDisabled; omitted/true keep enforcing.
  • The two behavior tests were confirmed to fail without the fix. make fmt-check, go vet ./..., go test -race ./internal/config/... ./internal/cli/..., go run ./cmd/zero-release build, go run ./cmd/zero-release smoke, govulncheck, git diff HEAD --check all clean.
  • Manual end-to-end via the real CLI (zero sandbox check, macos-seatbelt backend): a global config enabled:false reports policy: mode=disabled / "sandbox disabled"; a project .zero/config.json enabled:false still reports policy: mode=enforce / "sandbox is active" — the repo's attempt to disable is ignored.

Summary by CodeRabbit

  • New Features
    • Added a configuration option to explicitly disable sandbox enforcement (sandbox.enabled: false).
    • Sandbox enforcement remains enabled by default when the option is omitted.
  • Bug Fixes
    • Enforces sandbox enablement/disablement precedence correctly during configuration resolution (including overrides vs. lower-precedence settings).
  • Tests
    • Added coverage to verify explicit disable behavior, ignore project-provider changes for this setting, and validate CLI/programmatic override precedence.

Add a sandbox.enabled field (`"sandbox": {"enabled": false}`). ModeDisabled
already short-circuits the engine but had no config surface.

- SandboxConfig.Enabled *bool (pointer distinguishes an explicit false from an
  omitted key).
- mergeConfig honors it (global config / CLI); applyConfiguredSandboxPolicy maps
  enabled:false to ModeDisabled.
- mergeProjectConfig intentionally does NOT merge it: a cloned repo must not be
  able to disable the sandbox that constrains it — same posture as
  AdditionalWriteRoots and the Network tighten-only rule.
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Sandbox configuration now supports an explicit enabled field. Global configuration and CLI overrides can disable sandbox enforcement, while project and provider-command configuration cannot. Resolver and CLI tests cover false, true, and omitted values.

Changes

Sandbox disablement

Layer / File(s) Summary
Sandbox configuration resolution
internal/config/types.go, internal/config/resolver.go, internal/config/resolver_test.go
Adds optional sandbox.enabled handling, merges it from trusted configuration and overrides, excludes project and provider-command values, and tests precedence and boundaries.
CLI policy application
internal/cli/exec.go, internal/cli/sandbox_test.go
Sets the policy to ModeDisabled only when Enabled is explicitly false, with coverage for false, true, and nil values.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ConfigFile
  participant Resolve
  participant CLI
  participant SandboxPolicy
  participant Command
  ConfigFile->>Resolve: provide sandbox.enabled
  Resolve->>CLI: return trusted resolved configuration
  CLI->>SandboxPolicy: disable policy when enabled is false
  SandboxPolicy-->>Command: allow unwrapped or sandboxed execution
Loading

Possibly related PRs

Suggested reviewers: anandh8x, ashwinhegde19, pierrunoyt

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR adds sandbox.enabled and supports disabling the sandbox via trusted config, matching issue #687.
Out of Scope Changes check ✅ Passed The changes stay within sandbox config, resolution, CLI policy, and tests, with no obvious unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the main change: adding config support to disable the sandbox.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sandbox-enabled-config

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

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 19, 2026
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 76ef636f4b96
Changed files (5): internal/cli/exec.go, internal/cli/sandbox_test.go, internal/config/resolver.go, internal/config/resolver_test.go, internal/config/types.go

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@anandh8x anandh8x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please merge sandbox.enabled through the CLI override path before this lands.

mergeConfig now copies Sandbox.Enabled, but ResolveOptions.Overrides is applied later by applyOverrides, which does not copy overrides.Sandbox.Enabled. As a result, an explicit CLI/programmatic override such as Overrides{Sandbox: SandboxConfig{Enabled: &disabled}} is ignored, contradicting the PR description and the new comment that global config / CLI can disable the sandbox.

Please update applyOverrides to preserve the tri-state pointer and add a resolver regression test showing that an explicit false override disables the sandbox (and, ideally, that explicit true can override a lower-precedence false).

…eview)

applyOverrides copied BlockUnixSockets/MonitorDenials but not the new Enabled
tri-state pointer, so a CLI/programmatic Overrides{Sandbox:{Enabled:&false}} was
dropped — contradicting the 'global config / CLI can disable' contract. Preserve
the pointer in applyOverrides (runs after all config sources, so an explicit
override wins) and add a resolver regression test: an explicit false override
disables, and an explicit true override wins over a lower-precedence config false.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Good catch — fixed in 2a68d9c. applyOverrides now preserves the Sandbox.Enabled tri-state pointer (it runs after every config source, so an explicit override wins). Added TestResolveSandboxEnabledCLIOverride: an explicit false override disables the sandbox with no config file, and an explicit true override wins over a lower-precedence config-file false. Verified it fails without the applyOverrides change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
internal/config/resolver.go (1)

219-221: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Block provider-command output from toggling sandbox state.
SandboxConfig.Enabled is documented as user config/CLI only, but Resolve still applies LoadProviderCommand through mergeConfig, so a provider command can clear the sandbox too. Keep enabled out of that merge path or gate it behind an explicit trusted-source flag, and add a regression test.

🤖 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 `@internal/config/resolver.go` around lines 219 - 221, Prevent provider-command
data from changing sandbox state during Resolve: update mergeConfig and the
surrounding SandboxConfig handling so SandboxConfig.Enabled is excluded from
provider-command merges or applied only when an explicit trusted-source flag is
set, while preserving user config/CLI behavior. Add a regression test confirming
LoadProviderCommand cannot enable or clear sandbox state.
🤖 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.

Outside diff comments:
In `@internal/config/resolver.go`:
- Around line 219-221: Prevent provider-command data from changing sandbox state
during Resolve: update mergeConfig and the surrounding SandboxConfig handling so
SandboxConfig.Enabled is excluded from provider-command merges or applied only
when an explicit trusted-source flag is set, while preserving user config/CLI
behavior. Add a regression test confirming LoadProviderCommand cannot enable or
clear sandbox state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d2e43d08-36e0-4b48-8bf6-19ea4946a019

📥 Commits

Reviewing files that changed from the base of the PR and between 0dc81c7 and 2a68d9c.

📒 Files selected for processing (2)
  • internal/config/resolver.go
  • internal/config/resolver_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 19, 2026
@gnanam1990
gnanam1990 requested a review from anandh8x July 19, 2026 12:48
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Vasanthdev2004
Vasanthdev2004 previously approved these changes Jul 20, 2026

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving on the current head (2a68d9c4). anand's requested change is addressed here: applyOverrides now threads Sandbox.Enabled, and TestResolveSandboxEnabledCLIOverride covers both an explicit false override disabling the sandbox and an explicit true winning over a lower-precedence config false. @anandh8x this postdates your review, worth a re-look.

I verified the security posture first-hand (tests pass locally):

  • Off by default: Enabled *bool nil (omitted) keeps enforcement; only an explicit false disables.
  • A cloned repo CANNOT disable the sandbox that constrains it: mergeProjectConfig deliberately skips Sandbox.Enabled, and TestResolveSandboxDisableIgnoredFromProjectConfig locks that in. That is the property that matters most for a disable knob, and it is correct.
  • Precedence is right: global config and CLI can disable, project config cannot, and an explicit true beats a config false. The applyConfiguredSandboxPolicy policy test and all three resolver tests pass.

One non-blocking thought: a disabled sandbox surfaces as "sandbox disabled" in status, which is good, but a more prominent session-start notice (so someone who set enabled:false globally and forgot does not run unsandboxed unaware) might be worth a follow-up. Not blocking.

@anandh8x anandh8x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Request changes: provider-command output still reaches mergeConfig after LoadProviderCommand, and mergeConfig now copies Sandbox.Enabled. A provider command that returns a valid provider plus "sandbox": {"enabled": false} can therefore disable the sandbox, despite this setting being documented as global-config/CLI only. Keep Enabled out of the provider-command merge path (or gate it on an explicit trusted source) and add a regression test that provider-command output cannot enable or disable the sandbox.

 review)

mergeConfig now copies Sandbox.Enabled, and it is shared by two callers: the
global-config merge (trusted) and the provider-command merge (not). A provider
command is an arbitrary executable named in config, and LoadProviderCommand
parses its stdout into a full FileConfig -- so one returning a valid provider
plus {"sandbox":{"enabled":false}} could disable the sandbox meant to constrain
it, despite the setting being documented as global-config/CLI only. Confirmed
reproducible in both directions before the fix.

Clear Sandbox.Enabled from the provider-command config before merging, mirroring
the carve-out mergeProjectConfig already applies for the same reason. Done at the
call site rather than inside mergeConfig because that helper must keep honouring
the setting for the trusted global-config merge.

Adds TestResolveSandboxEnabledIgnoredFromProviderCommand covering both
directions (cannot disable, cannot enable) and asserting the provider itself is
still applied, so the guard cannot be satisfied by dropping the command wholesale.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

Pushed 76ef636 addressing @anandh8x's finding — confirmed reproducible before the fix, in both directions.

A provider command returning the full-config shape ({"providers":[…],"sandbox":{"enabled":false}}) reached Sandbox.Enabled because mergeConfig is shared between the trusted global-config merge and the provider-command merge. The shorthand single-provider shape was already safe — parseProviderCommandJSON discards everything but the profile there — which is why this only bites via the full shape.

Fix clears Sandbox.Enabled from the provider-command config before merging, mirroring the carve-out mergeProjectConfig already applies for the same threat model. Done at the call site, not inside mergeConfig, so the trusted global-config merge keeps honouring the setting.

TestResolveSandboxEnabledIgnoredFromProviderCommand covers both directions and also asserts the provider itself still applies, so the guard can't be satisfied by dropping the command wholesale. Mutation-tested: removing the one-line fix fails it. Global-config disable, CLI override, and the project-config carve-out all still pass.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --stat --oneline 76ef636
git diff 76ef636^ 76ef636 -- internal/config

printf '\n=== Relevant declarations and functions ===\n'
ast-grep outline internal/config --items all --type function,struct --match 'Resolve|mergeConfig|mergeProjectConfig|parseProviderCommandJSON|Provider|Sandbox|applyOverrides'

printf '\n=== Resolver references with context ===\n'
rg -n -C 5 'parseProviderCommandJSON|mergeConfig\(|mergeProjectConfig\(|Sandbox\.Enabled|provider command|ProviderCommand' internal/config

printf '\n=== Changed test sections ===\n'
git diff 76ef636^ 76ef636 --unified=60 -- internal/config/resolver_test.go

Length of output: 51743


@gnanam1990 Reviewed 76ef636; no actionable findings.

Clearing commandConfig.Sandbox.Enabled at the provider-command call site is the correct trust-boundary placement: trusted global config and explicit CLI overrides retain their behavior, while full-shape provider-command output cannot toggle sandbox enforcement. The regression test covers both boolean values and confirms provider resolution still works.


✏️ Learnings added
Learnt from: gnanam1990
URL: https://github.com/Gitlawb/zero/pull/746

Timestamp: 2026-07-20T07:00:33.871Z
Learning: In the Go configuration resolver (`internal/config/resolver.go`), provider-command stdout is untrusted even when it parses as a full `FileConfig`. `SandboxConfig.Enabled` must be cleared from provider-command configuration before `mergeConfig`, because only trusted global user configuration and explicit CLI/programmatic `Overrides` may change sandbox enforcement. Project configuration follows the same restriction.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@anandh8x — ready for another look. Your review was on 2a68d9c4; the fix is 76ef636f.

What you reported: mergeConfig now copies Sandbox.Enabled, and provider-command output flows through it, so a command returning a valid provider plus "sandbox":{"enabled":false} could disable the sandbox.

Confirmed reproducible before the fix, in both directions. One detail worth having: only the full-config shape reaches it — the shorthand single-provider form is already safe, because parseProviderCommandJSON rebuilds a bare FileConfig{ActiveProvider, Providers} on that path and discards everything else.

What changed: Sandbox.Enabled is cleared from the provider-command config before merging, mirroring the carve-out mergeProjectConfig already applies for the same threat model. Done at the call site rather than inside mergeConfig, so the trusted global-config merge keeps honouring the setting — that distinction is why fixing it in the shared helper would have been wrong.

TestResolveSandboxEnabledIgnoredFromProviderCommand covers both directions and also asserts the provider itself still applies, so the guard can't be satisfied by dropping the command wholesale. Mutation-tested: removing the one line fails it. Global-config disable, CLI override, and the project-config carve-out all still pass. CodeRabbit has approved this head; all checks green.

@anandh8x anandh8x left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved on the latest head. Provider-command output now has sandbox.enabled cleared before the shared config merge, while global config and CLI overrides retain the intended behavior. Regression coverage and focused tests pass.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-approving on 76ef636. My earlier approve was auto-dismissed by this push, so I re-checked the whole thing on the current head rather than standing on the old verdict.

The new commit closes a real hole. mergeConfig copies Sandbox.Enabled, and it is shared by two callers: the trusted global-config merge and the provider-command merge. Since LoadProviderCommand runs an arbitrary executable and parses its full stdout into a FileConfig, a command returning a valid provider plus {"sandbox":{"enabled":false}} could have turned off the sandbox meant to constrain it, despite the setting being documented as global-config/CLI only. Clearing commandConfig.Sandbox.Enabled = nil at the call site (not inside the shared helper, which the global-config merge still needs) is the right fix, and it mirrors the existing mergeProjectConfig carve-out.

Verified on 76ef636:

  • mergeConfig only assigns Enabled when the source is non-nil (resolver.go:229), so nil-ing it drops just the toggle while the provider itself still merges.
  • Full posture is green: global config CAN disable, project config CANNOT, provider command CANNOT (both directions), CLI override CAN. Ran the four TestResolveSandbox* cases plus the CLI policy test locally, all pass.
  • The new test also asserts the provider is still applied, so the guard cannot be satisfied by silently dropping the whole command config. Good touch.

One non-blocking heads-up, separate from this PR: the sibling sandbox fields (Network, AdditionalWriteRoots) still flow through the provider-command merge. That only matters if you later decide a provider command should not be able to widen write roots or open network either. Out of scope here; the enabled toggle was the documented contract, and that is what this closes.

LGTM.

@gnanam1990
gnanam1990 merged commit a21a052 into main Jul 20, 2026
9 checks passed
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.

Disable sandbox via config.json

3 participants