Skip to content

feat(sandbox): harden internal/sandbox — re-entrancy guard, SOCKS5 egress, sandbox-aware search, fine-grained path lists#199

Merged
gnanam1990 merged 8 commits into
mainfrom
feat/sandbox-superset-hardening
Jun 14, 2026
Merged

feat(sandbox): harden internal/sandbox — re-entrancy guard, SOCKS5 egress, sandbox-aware search, fine-grained path lists#199
gnanam1990 merged 8 commits into
mainfrom
feat/sandbox-superset-hardening

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Four opt-in, fail-closed capabilities added to internal/sandbox (Go only — no new deps, bin/zero.js untouched). Every addition is gated by a Policy field with a safe default, so an unconfigured policy behaves exactly as before.

1. Re-entrancy guard

  • EnvSandboxed = "ZERO_SANDBOXED" is set on every wrapped command's env.
  • A nested BuildCommandPlan while ZERO_SANDBOXED=1 returns a pass-through plan — no double bwrap/sandbox-exec wrap, no second egress proxy.
  • New: IsAlreadySandboxed() bool.

2. SOCKS5 egress

  • The scoped-egress proxy now also serves SOCKS5 CONNECT on a separate 127.0.0.1:0 listener, routed through the same authorize() / domainAllowed() gate (deny-wins, empty-allowlist → deny, fail-closed).
  • ProxyEnvWithSocks(httpAddr, socksAddr) points ALL_PROXY/all_proxy at socks5://<addr> while HTTP_PROXY/HTTPS_PROXY stay on the HTTP listener; NO_PROXY=localhost,127.0.0.1 preserved. Empty SOCKS addr falls back to the legacy ProxyEnv output.
  • The sandbox-exec profile allows outbound to both proxy ports. Bubblewrap (isolated netns) scoped policy still collapses to deny — backend-awareness preserved.

3. Sandbox-aware search

  • ReadExclusionGlobs(policy, scope) []string returns ripgrep --glob '!<pat>' exclusions for DenyRead subtrees inside the workspace.
  • The Go-native grep/glob tools become sandbox-aware (RunWithSandbox) and skip read-denied subtrees during their walk via Engine.ReadPathExcluded / ReadDirExcluded (the predicate honors more-specific AllowRead re-inclusion). No sandbox / no DenyRead → unchanged.

4. Fine-grained allow/deny path lists

  • New Policy fields: AllowRead, DenyRead, AllowWrite, DenyWrite []string.
  • Read = deny-then-allow: a DenyRead subtree is blocked unless a more-specific AllowRead re-includes it.
  • Write precedence: DenyWrite wins > workspace/scope-writable > AllowWrite > deny.
  • AllowWrite roots are reflected in the OS write binds; DenyWrite is also emitted as explicit sandbox-exec deny rules (last-match-wins) so the precedence holds for shell commands.
  • Layered on top of the workspace + Scope guards; never bypasses the symlink / out-of-workspace protections. Each entry is home-expanded, made absolute, and EvalSymlinks-resolved (non-existent entries dropped).

Defaults / safety

No default weakened: all lists default empty, ZERO_SANDBOXED defaults unset, SOCKS reuses the same allowlist, every gate fails closed.

Tests

internal/sandbox/{reentrancy,egress_socks,pathlists}_test.go, internal/sandbox/runner_test.go (ProxyEnv variants), and internal/tools/read_exclusions_test.go — covering the re-entrancy plan, SOCKS allow/deny + deny-wins, the read/write precedence matrix, glob generation, and grep/glob subtree exclusion.

Gate results (local)

  • gofmt -l . → empty
  • go vet ./... → clean
  • go build ./... → clean
  • go test ./... → all pass (incl. -race on internal/sandbox + internal/tools)
  • staticcheck ./... → no new findings (none in changed files)
  • govulncheck ./... → 0 vulnerabilities
  • deadcode -test=false ./... → no new unreachable funcs

Summary by CodeRabbit

  • New Features
    • Added a SOCKS5 egress front-end alongside existing HTTP proxying; sandboxed proxy networking now routes ALL_PROXY/all_proxy to SOCKS when available and restricts outbound proxy traffic to local loopback proxy ports.
    • Introduced fine-grained sandbox file policies: AllowRead/DenyRead and AllowWrite/DenyWrite, with deny precedence and nested allow re-inclusion.
    • Sandboxed grep/glob execution now respects DenyRead during traversal (skipping denied subtrees).
    • Added EnforceToolNetwork to apply sandbox network rules to first-party tools.
  • Tests
    • Expanded SOCKS5 integration, proxy environment behavior, path-policy precedence/exclusion (including symlink-aware directory pruning), and sandboxed grep/glob coverage.

All four are off by default and fail closed; existing behavior is unchanged
when the new policy fields are empty / ZERO_SANDBOXED is unset.

1. Re-entrancy guard — ZERO_SANDBOXED=1 marks every wrapped command; a nested
   BuildCommandPlan returns a pass-through plan (no double bwrap/sandbox-exec,
   no second egress proxy). New: EnvSandboxed, IsAlreadySandboxed().

2. SOCKS5 egress — the scoped-egress proxy also serves SOCKS5 CONNECT on a
   second loopback port through the SAME authorize()/domainAllowed() gate.
   ProxyEnvWithSocks points ALL_PROXY at socks5://<addr>; HTTP(S)_PROXY stay on
   the HTTP listener; the sandbox-exec profile allows both proxy ports.
   Bubblewrap scoped still collapses to deny.

3. Sandbox-aware search — ReadExclusionGlobs(policy, scope) returns ripgrep
   --glob '!<pat>' for DenyRead subtrees; grep/glob skip read-denied subtrees
   at walk time via Engine.ReadPathExcluded/ReadDirExcluded when sandboxed.

4. Fine-grained path lists — Policy.AllowRead/DenyRead/AllowWrite/DenyWrite.
   Read = deny-then-allow (more-specific AllowRead re-includes); Write =
   DenyWrite > workspace/scope > AllowWrite > deny. AllowWrite is reflected in
   the OS write binds; DenyWrite is emitted as sandbox-exec deny rules. Each
   entry is home-expanded, made absolute, and symlink-resolved.

Tests: internal/sandbox/{reentrancy,egress_socks,pathlists}_test.go and
internal/tools/read_exclusions_test.go.
Gates: gofmt, go vet, go build, go test (incl. -race), staticcheck (no new
findings), govulncheck (0), deadcode -test=false (no new unreachable funcs).
@github-actions

github-actions Bot commented Jun 14, 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: f00e28d434ba
Changed files (17): internal/sandbox/egress.go, internal/sandbox/egress_socks.go, internal/sandbox/egress_socks_test.go, internal/sandbox/engine.go, internal/sandbox/engine_test.go, internal/sandbox/pathlists.go, internal/sandbox/pathlists_test.go, internal/sandbox/reentrancy_test.go, internal/sandbox/runner.go, internal/sandbox/runner_test.go, internal/sandbox/types.go, internal/tools/glob.go, and 5 more

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

@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 34e512b8-2871-4fac-8999-1f6b09d2ebeb

📥 Commits

Reviewing files that changed from the base of the PR and between 995a231 and f00e28d.

📒 Files selected for processing (1)
  • internal/sandbox/pathlists.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/sandbox/pathlists.go

Walkthrough

Adds SOCKS5 front-end to scoped egress proxy with same auth gate as HTTP. Introduces path policy with AllowRead/DenyRead/AllowWrite/DenyWrite and full precedence logic wired through Engine.Evaluate. Adds re-entrancy guard to BuildCommandPlan and propagates SOCKS port and write-root policy into sandbox-exec profile. Integrates DenyRead exclusions into grep and glob tools via RunWithSandbox. Adds optional EnforceToolNetwork gate for first-party tool network access.

Changes

Sandbox SOCKS5 egress, path policy, re-entrancy, and tool exclusions

Layer / File(s) Summary
Policy types and environment markers for re-entrancy detection
internal/sandbox/types.go
Adds AllowRead, DenyRead, AllowWrite, DenyWrite fields and EnforceToolNetwork flag to Policy with JSON tags and documentation. Adds EnvSandboxed and EnvSandboxBackend exported environment markers plus IsAlreadySandboxed() helper checking both markers to prevent nested sandbox wrapping.
Path resolution, normalization, and symlink-aware containment
internal/sandbox/pathlists.go
Implements path entry normalization: trim/expand tilde, convert to absolute, symlink-resolve via EvalSymlinks, filter blanks/non-resolving, deduplicate. Restricts write policy roots to existing non-root directories. Implements symlink-aware containment logic (pathUnderPolicyRoot) with prefix normalization to prevent symlink-evasion attacks.
Read/write precedence enforcement, exclusions, and validation
internal/sandbox/pathlists.go
Implements readDenied with deny-first semantics and nested AllowRead re-inclusion via hasNestedAllowReadResolved. Implements validateWritePath with DenyWrite-first override and AllowWrite scoping. Implements validatePathWithPolicy dispatcher routing read/write/default side effects. Adds ReadExclusions caching type with Active, PathExcluded, DirExcluded predicates for search integration. Exports ReadExclusionGlobs generating ripgrep exclusion arguments.
Path policy tests: precedence, globbing, and directory traversal
internal/sandbox/pathlists_test.go
Tests deny/allow precedence with nested re-inclusion not affecting siblings, denied directory non-skipping when nested allow exists, ReadExclusionGlobs output (inside/outside workspace), DirExcluded descent through symlinks. Tests Evaluate integration for read/write enforcement and writeRoots reflection of AllowWrite. Tests sandboxExecProfile emits DenyWrite rules after corresponding AllowWrite with last-match-wins seatbelt semantics.
Engine: network tool gating, read exclusions, and Evaluate path validation
internal/sandbox/engine.go
Adds Engine.ReadExclusions() and Engine.ReadExclusionGlobs() as nil-safe methods. Updates NetworkHostAllowed to check EnforceToolNetwork flag, returning early when false (exempting first-party in-process tools). Switches Evaluate's workspace path-walk from scope.validate(requested) to validatePathWithPolicy(...) for policy/side-effect-aware enforcement.
Engine tests: network host allowance with and without tool enforcement
internal/sandbox/engine_test.go
Updates TestEngineNetworkHostAllowed to explicitly enable EnforceToolNetwork for policy-aware host blocking. Adds TestEngineNetworkHostAllowedToolNetworkDefaultExempt verifying that when EnforceToolNetwork is off, both NetworkDeny and NetworkScoped modes allow tool host access.
SOCKS5 protocol implementation: negotiation, address parsing, and authorization
internal/sandbox/egress_socks.go
Adds SOCKS5 protocol constants and handshake deadline. Implements serveSocks accept loop spawning per-connection handlers. Implements socksNegotiate reading greeting/methods, selecting no-auth, parsing CONNECT requests (IPv4/IPv6/domain), returning (host, port) or error with appropriate SOCKS replies. Adds writeSocksReply helper.
SOCKS5 connection handler: authorization gate and data relay
internal/sandbox/egress_socks.go
Implements handleSocksConn with fail-closed flow: negotiates, applies proxy authorization gate (same as HTTP), sends "not allowed" reply on denial, dials with timeout on allow, sends success reply, clears deadline, relays traffic bidirectionally.
Egress proxy: SOCKS5 listener lifecycle and address exposure
internal/sandbox/egress.go
Extends egressProxy struct with socksListener field. Binds SOCKS5 listener on second loopback port in newEgressProxy, failing closed if binding fails. Starts serveSocks goroutine. Adds SocksAddr() method exposing listener address or empty string. Updates Close() to additionally close socksListener.
SOCKS5 egress tests: binding, protocol negotiation, and relay verification
internal/sandbox/egress_socks_test.go
Introduces socks5Connect helper performing no-auth greeting, CONNECT request (IPv4 and domain form), response parsing. Tests verify loopback binding with distinct port from HTTP, unsupported auth rejection, allowlisted IPv4 relay with data verification (ping/pong), domain deny, and deny-wins subdomain precedence.
Runner: proxy environments, write roots, and re-entrancy guard
internal/sandbox/runner.go
Adds ProxyEnvWithSocks(httpAddr, socksAddr) routing HTTP_PROXY/HTTPS_PROXY to HTTP, ALL_PROXY to SOCKS (or HTTP fallback). Updates writeRoots to augment base with deduplicated AllowWrite paths. Adds re-entrancy guard in BuildCommandPlan returning pass-through when IsAlreadySandboxed() is true. Extends scopedEgress to track socksAddr and updates sandboxEnvironment to emit EnvSandboxed=1 and EnvSandboxBackend.
Runner: sandbox-exec profile generation with SOCKS and deny-write rules
internal/sandbox/runner.go
Updates sandboxExecCommandPlan to parse SOCKS5 port, pass both proxy ports to sandboxExecProfile, inject env via ProxyEnvWithSocks. Updates sandboxExecProfile signature to accept socksPort. Refactors rules assembly: appends denyWriteRules(policy) before network rule. Updates networkRuleFor (scoped mode) to allow outbound to both localhost ports; fails closed if unresolvable.
Runner tests: proxy environment, re-entrancy detection, and profile wiring
internal/sandbox/runner_test.go, internal/sandbox/reentrancy_test.go
Tests ProxyEnvWithSocks split/fallback behavior. Updates existing profile tests for socksPort argument. Tests IsAlreadySandboxed detection with correlated markers, sandboxEnvironment marking, BuildCommandPlan wrapping (not sandboxed) and pass-through (already sandboxed).
Tool helper: readExcluder and sandbox integration for search walks
internal/tools/read_exclusions.go
Adds readExcluder type with optional file/dir predicates. Implements sandboxReadExcluder(engine) deriving predicates from Engine.ReadExclusions() when active, returning no-op for nil engines (preserving pre-sandbox behavior).
Tool: grep sandbox-aware refactor with RunWithSandbox path
internal/tools/grep.go
Refactors grepTool.Run to delegate through runWith with no-op excluder. Adds grepTool.RunWithSandbox passing sandbox-derived excluder. Updates grepFiles to skip file matches and directory subtrees when excluded via fileExcluded()/dirExcluded() predicates.
Tool: glob sandbox-aware refactor with RunWithSandbox path
internal/tools/glob.go
Refactors globTool.Run to delegate through runWith with no-op excluder. Adds globTool.RunWithSandbox passing sandbox-derived excluder. Updates scanGlob to skip subtrees and filter paths when excluded via dirExcluded()/PathExcluded() predicates.
Tool tests: grep and glob DenyRead integration verification
internal/tools/read_exclusions_test.go
Adds denyReadFixture creating temp workspace with normal and denied files, configures enforced sandbox engine with DenyRead. Tests sandboxed grep/glob omit denied subtrees while non-sandboxed runs include them.
Tool tests: EnforceToolNetwork policy wiring for network tools
internal/tools/web_fetch_test.go, internal/tools/web_search_test.go
Updates web_fetch tests to set Policy.EnforceToolNetwork: true for policy-based host blocking. Splits web_search tests: verifies default allow under NetworkDeny without enforcement, blocking when EnforceToolNetwork is true. Tightens scoped-network tests with EnforceToolNetwork: true validating backend-call behavior against allowlisted hosts.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant serveSocks as serveSocks loop
    participant socksNegotiate
    participant authGate as auth gate
    participant Upstream

    Client->>serveSocks: TCP to socksListener
    serveSocks->>socksNegotiate: negotiate(conn)
    socksNegotiate->>Client: select no-auth method
    Client->>socksNegotiate: CONNECT host:port
    socksNegotiate-->>serveSocks: (host, port)
    
    alt authorization denied
        serveSocks->>authGate: proxy.allowed(host, port)?
        authGate-->>serveSocks: false
        serveSocks->>Client: SOCKS REP 0x02 (not allowed)
    else authorization allowed
        authGate-->>serveSocks: true
        serveSocks->>Upstream: dial(host:port)
        serveSocks->>Client: SOCKS REP 0x00 (success)
        note over serveSocks: bidirectional relay
    end
Loading
sequenceDiagram
    participant BuildCommandPlan
    participant IsAlreadySandboxed
    participant sandboxExecCommandPlan
    participant sandboxExecProfile
    participant ProxyEnvWithSocks

    BuildCommandPlan->>IsAlreadySandboxed: check environment markers
    alt process already sandboxed
        IsAlreadySandboxed-->>BuildCommandPlan: true
        BuildCommandPlan-->>BuildCommandPlan: return pass-through plan
    else process not sandboxed
        IsAlreadySandboxed-->>BuildCommandPlan: false
        BuildCommandPlan->>sandboxExecCommandPlan: build wrapped plan
        sandboxExecCommandPlan->>sandboxExecProfile: (proxyPort, socksPort, writeRoots, policy)
        sandboxExecProfile-->>sandboxExecCommandPlan: seatbelt profile (allow both ports, DenyWrite rules)
        sandboxExecCommandPlan->>ProxyEnvWithSocks: (httpAddr, socksAddr)
        ProxyEnvWithSocks-->>sandboxExecCommandPlan: HTTP_PROXY=http, ALL_PROXY=socks5
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Gitlawb/zero#119: Updates to requestPaths(...) path collection and enforcement logic in sandbox engine.
  • Gitlawb/zero#77: Introduces the core sandbox backend integration that this PR extends with fine-grained path policy and read-exclusion integration.
  • Gitlawb/zero#162: Related work on multi-root write enforcement and AllowWrite seatbelt rules via sandbox-exec profile.

Suggested reviewers

  • Vasanthdev2004
  • anandh8x
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and comprehensively summarizes the four major changes in this PR: re-entrancy guard, SOCKS5 egress, sandbox-aware search, and fine-grained path lists, matching the substantial scope of the changeset.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sandbox-superset-hardening

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

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

Actionable comments posted: 3

🤖 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 `@internal/sandbox/egress_socks.go`:
- Around line 86-92: The SOCKS5 method negotiation violates the protocol by
always selecting socksNoAuth without checking if the client actually offered it.
After reading the greeting and the list of methods offered by the client (the
greeting[1] bytes), check if socksNoAuth (method 0x00) is present in the methods
list. If it is, send back the selection response with socksNoAuth; if not, send
back 0xFF ("no acceptable methods") instead, as required by the SOCKS5 protocol
specification.

In `@internal/sandbox/pathlists.go`:
- Around line 113-119: The readDenied and hasNestedAllowRead functions
repeatedly call resolvePolicyPaths on each invocation, causing expensive
operations like Abs and EvalSymlinks to run multiple times for the same policy
entries. Cache the resolved DenyRead and AllowRead roots once at engine
construction or per tool run, then pass these cached roots to both readDenied
and hasNestedAllowRead instead of resolving them each time. This eliminates
redundant path resolution operations in the walk predicates.

In `@internal/sandbox/runner.go`:
- Around line 187-189: The IsAlreadySandboxed() check at line 187 in the
directCommandPlan call relies only on the ZERO_SANDBOXED environment variable as
proof of nesting, which is insufficient because the variable can be inherited or
manually set by users in non-nested environments, allowing commands to execute
unsandboxed and bypass scoped-egress protections. Replace the single ambient
environment variable check with a stronger re-entrancy detection mechanism that
has provenance beyond just an inherited or user-settable flag, such as verifying
a unique identifier or state that can only be present in an actual nested
execution context created by the sandbox wrapper itself.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0bc0b613-b8ec-4481-8c69-49547ba8f18a

📥 Commits

Reviewing files that changed from the base of the PR and between 7153ec7 and cc6e9f0.

📒 Files selected for processing (14)
  • internal/sandbox/egress.go
  • internal/sandbox/egress_socks.go
  • internal/sandbox/egress_socks_test.go
  • internal/sandbox/engine.go
  • internal/sandbox/pathlists.go
  • internal/sandbox/pathlists_test.go
  • internal/sandbox/reentrancy_test.go
  • internal/sandbox/runner.go
  • internal/sandbox/runner_test.go
  • internal/sandbox/types.go
  • internal/tools/glob.go
  • internal/tools/grep.go
  • internal/tools/read_exclusions.go
  • internal/tools/read_exclusions_test.go

Comment thread internal/sandbox/egress_socks.go Outdated
Comment thread internal/sandbox/pathlists.go
Comment thread internal/sandbox/runner.go

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

I found 3 issues before this should merge:

  1. Windows CI is failing in internal/sandbox/pathlists_test.go (TestSandboxExecProfileEmitsDenyWriteRule). The profile escapes backslashes through sandboxProfileString, but the test expects the raw Windows path. Please fix the assertion or emitted expected path so Windows smoke is green.

  2. internal/sandbox/runner.go re-entrancy guard trusts only ZERO_SANDBOXED=1. Since that env var can be user-set before launching Zero, it can force command plans into direct/pass-through mode and bypass wrapping/scoped egress. Please add stronger provenance than a single ambient env var.

  3. internal/sandbox/egress_socks.go always selects SOCKS no-auth even if the client did not offer method 0x00. It should reply 0xff when no acceptable method is offered, and have a regression test.

I ran go test ./internal/sandbox ./internal/tools -count=1; internal/sandbox fails on the Windows-path DenyWrite profile test, internal/tools passes.

…usion roots

- egress_socks: respect SOCKS5 method negotiation (RFC 1928). The proxy now
  inspects the client's offered methods and replies 0xFF ("no acceptable
  methods") when no-auth was not offered, instead of forcing no-auth.
- pathlists/engine: resolve DenyRead/AllowRead roots ONCE per search walk via a
  new ReadExclusions matcher (Engine.ReadExclusions), replacing the per-path
  ReadPathExcluded/ReadDirExcluded that re-ran Abs/EvalSymlinks for every visited
  path during a grep/glob walk.

Re-entrancy guard left as-is by design: ZERO_SANDBOXED is an internal marker set
only on commands zero wraps; cross-process re-entrancy detection legitimately
relies on the inherited env, and a manually-exported flag is a self-opt-out, not
an external bypass.

Tests: SOCKS no-acceptable-methods rejection; nested-AllowRead descent via the
matcher. Gates: gofmt, vet, build, test (incl. -race), staticcheck (no new
findings), govulncheck (0), deadcode (no new unreachable funcs).

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

Actionable comments posted: 1

🤖 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 `@internal/sandbox/pathlists.go`:
- Around line 182-186: In the DirExcluded function, the abs path is constructed
but not symlink-resolved before being passed to hasNestedAllowReadResolved,
while allowRoots are already resolved. This mismatch causes prefix comparison in
pathWithinRoot to fail for symlinked paths. After constructing abs by joining
with rx.workspaceRoot if needed, resolve it using NormalizePrefixForRoot (or the
equivalent symlink resolution method used for allowRoots) to ensure consistent
path comparison before passing it to hasNestedAllowReadResolved.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 43817e7d-3029-4b1d-b49a-744c74ec8aaf

📥 Commits

Reviewing files that changed from the base of the PR and between cc6e9f0 and 66f611d.

📒 Files selected for processing (6)
  • internal/sandbox/egress_socks.go
  • internal/sandbox/egress_socks_test.go
  • internal/sandbox/engine.go
  • internal/sandbox/pathlists.go
  • internal/sandbox/pathlists_test.go
  • internal/tools/read_exclusions.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/sandbox/egress_socks_test.go
  • internal/sandbox/egress_socks.go
  • internal/sandbox/pathlists_test.go

Comment thread internal/sandbox/pathlists.go
- pathlists_test: TestSandboxExecProfileEmitsDenyWriteRule now builds the
  expected deny clause via sandboxProfileString(secret), so it matches the
  profile's own escaping (Windows doubles backslashes). Fixes the
  windows-latest smoke failure; macOS/Linux unaffected (slash paths, no-op
  escaping).
- pathlists: ReadExclusions.DirExcluded now EvalSymlinks-resolves the candidate
  path before the nested-AllowRead prefix check, matching the resolution applied
  to allowRoots — otherwise a denied dir reached THROUGH a symlink fails to match
  a nested AllowRead root and is wrongly skipped, dropping a re-included subtree.
  Added TestDirExcludedResolvesSymlinkPrefix (skips where symlinks are
  unavailable).

Gates: gofmt, vet, build, test (incl. -race), staticcheck (no new findings),
govulncheck (0), deadcode (no new unreachable funcs).
…rker

Addresses review feedback that the re-entrancy guard trusted a single ambient
env var. IsAlreadySandboxed now requires BOTH correlated markers that
sandboxEnvironment sets together on every wrapped command — ZERO_SANDBOXED=1 AND
a non-empty ZERO_SANDBOX_BACKEND (new EnvSandboxBackend const) — so a lone
stray/hand-exported ZERO_SANDBOXED=1 no longer forces an unsandboxed
pass-through. Pass-through (direct) plans set neither marker, so genuine nesting
still detected and double-wrapping still avoided.

Tests updated: TestIsAlreadySandboxed now asserts each marker alone is
insufficient and both together are required; the re-entrancy and wrapping plan
tests set/clear both markers explicitly.

Gates: gofmt, vet, build, test (incl. -race), staticcheck (no new findings),
govulncheck (0), deadcode (no new unreachable funcs).
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@Vasanthdev2004 thanks for the review — all three addressed:

  1. Windows CI / DenyWrite profile test — fixed in a6afdb9. TestSandboxExecProfileEmitsDenyWriteRule now builds the expected clause via sandboxProfileString(secret), so it matches the profile's own escaping (Windows backslashes doubled). The windows-latest smoke job is green on the latest run.

  2. Re-entrancy provenance — strengthened in c99f337. IsAlreadySandboxed() now requires BOTH correlated markers that sandboxEnvironment sets together on every wrapped command — ZERO_SANDBOXED=1 and a non-empty ZERO_SANDBOX_BACKEND — so a lone hand-exported/inherited ZERO_SANDBOXED=1 can no longer force an unsandboxed pass-through. Pass-through (direct) plans set neither marker, so genuine nesting is still detected and double-wrapping still avoided. TestIsAlreadySandboxed now asserts each marker alone is insufficient.

  3. SOCKS5 method negotiation — fixed in 66f611d. socksNegotiate validates the client's offered methods and replies 0x05 0xFF ("no acceptable methods") when no-auth wasn't offered, instead of forcing it. Regression test TestEgressProxySocksRejectsUnsupportedAuth added.

All gates green locally and in CI (gofmt, vet, build, test incl. -race, staticcheck, govulncheck, deadcode). Could you take another look when you have a moment?

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

🧹 Nitpick comments (1)
internal/sandbox/reentrancy_test.go (1)

30-39: ⚡ Quick win

Consider also asserting EnvSandboxBackend is set.

This test only verifies EnvSandboxed=1 is present. Since IsAlreadySandboxed() now requires both markers, a regression that removes EnvSandboxBackend from sandboxEnvironment would pass this test but break re-entrancy detection.

🔧 Proposed fix
 func TestSandboxEnvironmentMarksSandboxed(t *testing.T) {
 	env := sandboxEnvironment(DefaultPolicy(), BackendBubblewrap, "/home/agent")
 	want := EnvSandboxed + "=1"
+	wantBackend := EnvSandboxBackend + "=" + string(BackendBubblewrap)
+	foundSandboxed, foundBackend := false, false
 	for _, entry := range env {
 		if entry == want {
-			return
+			foundSandboxed = true
+		}
+		if entry == wantBackend {
+			foundBackend = true
 		}
 	}
-	t.Fatalf("sandboxEnvironment must set %s so a wrapped command is detectable; got %#v", want, env)
+	if !foundSandboxed {
+		t.Fatalf("sandboxEnvironment must set %s; got %#v", want, env)
+	}
+	if !foundBackend {
+		t.Fatalf("sandboxEnvironment must set %s for dual-marker detection; got %#v", wantBackend, env)
+	}
 }
🤖 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/sandbox/reentrancy_test.go` around lines 30 - 39, The
TestSandboxEnvironmentMarksSandboxed test only verifies that EnvSandboxed is set
but does not check for EnvSandboxBackend, which is also required by
IsAlreadySandboxed() for proper re-entrancy detection. Add an additional
assertion after the loop that checks for the presence of EnvSandboxBackend in
the env slice returned by sandboxEnvironment(), similar to the existing check
for EnvSandboxed, to prevent regressions where EnvSandboxBackend is accidentally
removed from the environment while the test still passes.
🤖 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.

Nitpick comments:
In `@internal/sandbox/reentrancy_test.go`:
- Around line 30-39: The TestSandboxEnvironmentMarksSandboxed test only verifies
that EnvSandboxed is set but does not check for EnvSandboxBackend, which is also
required by IsAlreadySandboxed() for proper re-entrancy detection. Add an
additional assertion after the loop that checks for the presence of
EnvSandboxBackend in the env slice returned by sandboxEnvironment(), similar to
the existing check for EnvSandboxed, to prevent regressions where
EnvSandboxBackend is accidentally removed from the environment while the test
still passes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bf56244e-5846-4ead-82db-c67a6374adf3

📥 Commits

Reviewing files that changed from the base of the PR and between a6afdb9 and c99f337.

📒 Files selected for processing (3)
  • internal/sandbox/reentrancy_test.go
  • internal/sandbox/runner.go
  • internal/sandbox/types.go

… default

The sandbox was reported as too strict: web_search (and web_fetch) were blocked
with "network access is disabled by the sandbox policy" because the default
policy is network=deny and the in-process web tools were gated by the same
NetworkHostAllowed check as the sandboxed shell's egress.

That network policy exists to confine the sandboxed SHELL's egress, which these
in-process tools don't use. New Policy.EnforceToolNetwork (off by default) makes
NetworkHostAllowed exempt web_search/web_fetch unless an operator opts in. The
sandboxed-shell egress decision (Evaluate / effectiveNetworkMode) is unchanged,
and the tools keep their own SSRF/private-IP, port, redirect, and redaction
safeguards. Set EnforceToolNetwork to hold the tools to the allow/scoped/deny
policy as before.

Tests: engine default-exempt + enforced-table cases; web_search default-allows
and enforced-when-flag; web_fetch enforced cases set the flag. Gates: gofmt, vet,
build, test (incl. -race), staticcheck (no new findings), govulncheck (0),
deadcode (no new unreachable funcs).
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Added f7dbbc9: first-party web tools are no longer subject to the sandbox network policy by default.

Context: with the default network=deny, web_search/web_fetch were blocked ("network access is disabled by the sandbox policy") because the in-process tools went through the same NetworkHostAllowed gate as the sandboxed shell's egress. That gate exists to confine the sandboxed shell, which these tools don't use.

Change: new Policy.EnforceToolNetwork (off by default) — NetworkHostAllowed now exempts web_search/web_fetch unless an operator opts in. The sandboxed-shell egress decision (Evaluate/effectiveNetworkMode) is unchanged, and the tools keep all their own safeguards (SSRF/private-IP blocking, port validation, redirect re-validation, redaction). Setting EnforceToolNetwork: true restores the previous strict gating.

Tests updated/added: engine default-exempt + enforced-table cases; web_search default-allows-under-deny and enforced-when-flagged; web_fetch enforced cases set the flag. All local gates green (gofmt, vet, build, test incl. -race, staticcheck no-new, govulncheck 0, deadcode no-new).

@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 (2)
internal/sandbox/engine.go (2)

73-92: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Disabled policy should not keep filtering grep/glob paths.

ReadExclusions() and ReadExclusionGlobs() ignore policy.Mode. With ModeDisabled, Evaluate() allows the request, but sandbox-aware grep/glob can still skip DenyRead subtrees because they consume these helpers directly. That makes “policy disabled” still hide files.

Suggested fix
 func (engine *Engine) ReadExclusions() *ReadExclusions {
-	if engine == nil {
+	if engine == nil || engine.policy.Mode == ModeDisabled {
 		return nil
 	}
 	return &ReadExclusions{
 		workspaceRoot: engine.workspaceRoot,
 		denyRoots:     resolvePolicyPaths(engine.policy.DenyRead),
 		allowRoots:    resolvePolicyPaths(engine.policy.AllowRead),
 	}
 }
@@
 func (engine *Engine) ReadExclusionGlobs() []string {
-	if engine == nil {
+	if engine == nil || engine.policy.Mode == ModeDisabled {
 		return nil
 	}
 	return ReadExclusionGlobs(engine.policy, engine.scope)
 }
🤖 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/sandbox/engine.go` around lines 73 - 92, The ReadExclusions() and
ReadExclusionGlobs() methods do not check engine.policy.Mode and therefore
return filtering rules even when the policy is disabled, causing files to still
be hidden when they should all be accessible. Add a check in both
ReadExclusions() and ReadExclusionGlobs() to return nil and an empty slice
respectively when engine.policy.Mode is set to ModeDisabled, ensuring that
disabled policies do not filter any paths.

261-267: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Path-list policy is accidentally disabled when EnforceWorkspace is false.

This keeps DenyRead / DenyWrite / AllowWrite out of Evaluate() unless workspace enforcement is also enabled. That undercuts the new explicit path-policy knobs and diverges from the grep/glob path, which already consumes DenyRead directly.

Suggested fix
-	if policy.EnforceWorkspace && request.WorkspaceRoot != "" {
+	if request.WorkspaceRoot != "" && (
+		policy.EnforceWorkspace ||
+		len(policy.AllowRead) > 0 ||
+		len(policy.DenyRead) > 0 ||
+		len(policy.AllowWrite) > 0 ||
+		len(policy.DenyWrite) > 0) {
 		for _, requested := range requestPaths(request) {
 			if violation := validatePathWithPolicy(scope, policy, request.SideEffect, request.WorkspaceRoot, requested); violation != nil {
 				return deny(request, risk, violation.Code, violation.Path, violation.Reason, false)
 			}
 		}
 	}
🤖 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/sandbox/engine.go` around lines 261 - 267, The path-list policy
validation using validatePathWithPolicy is incorrectly gated behind the
condition checking both policy.EnforceWorkspace and request.WorkspaceRoot, which
disables DenyRead, DenyWrite, and AllowWrite policies when EnforceWorkspace is
false. Remove or restructure the workspace enforcement condition so that path
validation occurs independently and always checks the path policies against the
requested paths, similar to how the grep/glob path handles DenyRead directly.
The workspace root validation should remain as a separate workspace-specific
check that does not block the general path policy evaluation.
🤖 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/sandbox/engine.go`:
- Around line 73-92: The ReadExclusions() and ReadExclusionGlobs() methods do
not check engine.policy.Mode and therefore return filtering rules even when the
policy is disabled, causing files to still be hidden when they should all be
accessible. Add a check in both ReadExclusions() and ReadExclusionGlobs() to
return nil and an empty slice respectively when engine.policy.Mode is set to
ModeDisabled, ensuring that disabled policies do not filter any paths.
- Around line 261-267: The path-list policy validation using
validatePathWithPolicy is incorrectly gated behind the condition checking both
policy.EnforceWorkspace and request.WorkspaceRoot, which disables DenyRead,
DenyWrite, and AllowWrite policies when EnforceWorkspace is false. Remove or
restructure the workspace enforcement condition so that path validation occurs
independently and always checks the path policies against the requested paths,
similar to how the grep/glob path handles DenyRead directly. The workspace root
validation should remain as a separate workspace-specific check that does not
block the general path policy evaluation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b48ce849-a84a-4807-9415-8f24b42f5f51

📥 Commits

Reviewing files that changed from the base of the PR and between c99f337 and f7dbbc9.

📒 Files selected for processing (5)
  • internal/sandbox/engine.go
  • internal/sandbox/engine_test.go
  • internal/sandbox/types.go
  • internal/tools/web_fetch_test.go
  • internal/tools/web_search_test.go

…ent of EnforceWorkspace

- engine: ReadExclusions() / ReadExclusionGlobs() now return inert results under
  ModeDisabled, so a disabled policy never filters grep/glob (parity with
  Evaluate, which already allows everything when disabled).
- engine/pathlists: the fine-grained path lists (DenyRead/DenyWrite, with
  AllowRead/AllowWrite) now apply in Evaluate whenever the sandbox is enforcing,
  independent of EnforceWorkspace — matching the grep/glob path that already
  honors DenyRead directly. The workspace boundary (scope.validate) stays gated
  by EnforceWorkspace via a param threaded through validatePathWithPolicy /
  validateWritePath; DenyWrite wins regardless.
- reentrancy_test: TestSandboxEnvironmentMarksSandboxed now asserts BOTH
  correlated markers (ZERO_SANDBOXED=1 and ZERO_SANDBOX_BACKEND) so dropping
  either would fail the test.

Tests added: path lists enforced with EnforceWorkspace off; ReadExclusions inert
under ModeDisabled. Gates: gofmt, vet, build, test (incl. -race), staticcheck
(no new findings), govulncheck (0), deadcode (no new unreachable funcs).
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Addressed the latest re-review in 580e310:

  1. engine.go — disabled policy still filtered grep/glob. ReadExclusions() and ReadExclusionGlobs() now return inert results under ModeDisabled, so a disabled sandbox filters nothing (parity with Evaluate, which already allows everything when disabled). Added TestReadExclusionsInactiveWhenDisabled.

  2. engine.go/pathlists.go — path lists were gated behind EnforceWorkspace. The fine-grained lists (DenyRead/DenyWrite, with AllowRead/AllowWrite) now apply in Evaluate whenever the sandbox is enforcing, independent of EnforceWorkspace — matching the grep/glob path that honors DenyRead directly. I did not take the suggested one-line condition because it would also switch the workspace boundary on when EnforceWorkspace is off; instead I threaded an enforceWorkspace param through validatePathWithPolicy/validateWritePath so the boundary stays gated while DenyWrite wins regardless. Added TestPathListsApplyWithoutWorkspaceEnforcement.

  3. reentrancy_test.go — assert both markers. TestSandboxEnvironmentMarksSandboxed now asserts both ZERO_SANDBOXED=1 and ZERO_SANDBOX_BACKEND, so dropping either from sandboxEnvironment fails the test.

All local gates green (gofmt, vet, build, test incl. -race, staticcheck no-new, govulncheck 0, deadcode no-new).

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

Actionable comments posted: 1

🤖 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 `@internal/sandbox/engine.go`:
- Around line 270-273: The path validation loop starting at line 270 is guarded
by a condition checking if request.WorkspaceRoot is not empty, which causes the
entire validatePathWithPolicy validation to be skipped when WorkspaceRoot is
empty. This allows requests with absolute paths to bypass policy enforcement on
engines without a workspace root. Remove the if request.WorkspaceRoot != ""
condition so that the loop over requestPaths(request) executes unconditionally,
validating every requested path. The validatePathWithPolicy function call should
be invoked for all paths, and the enforcement logic within should handle both
cases appropriately: processing absolute paths normally when WorkspaceRoot is
empty, while failing validation for relative paths that cannot be anchored
without a workspace root.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ada038c3-1bf1-4da4-aba8-39746f4fa3af

📥 Commits

Reviewing files that changed from the base of the PR and between f7dbbc9 and 580e310.

📒 Files selected for processing (4)
  • internal/sandbox/engine.go
  • internal/sandbox/pathlists.go
  • internal/sandbox/pathlists_test.go
  • internal/sandbox/reentrancy_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/sandbox/reentrancy_test.go
  • internal/sandbox/pathlists.go
  • internal/sandbox/pathlists_test.go

Comment thread internal/sandbox/engine.go Outdated
Re-review follow-up: the Evaluate path-validation loop was still guarded by
request.WorkspaceRoot != "", so an engine built without a workspace root skipped
DenyRead/DenyWrite/AllowRead/AllowWrite even for absolute paths. The loop now
runs unconditionally; the workspace boundary (scope.validate) stays gated on
having a root (enforceWorkspace = EnforceWorkspace && WorkspaceRoot != ""), while
the path lists match absolute paths regardless. An unanchorable relative path
(no workspace root) fails closed when anything is configured to enforce, and is a
no-op otherwise (unchanged from prior behavior).

Test: TestEvaluatePathListsWithoutWorkspaceRoot. Gates: gofmt, vet, build, test
(incl. -race), staticcheck (no new findings), govulncheck (0), deadcode (no new
unreachable funcs).

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

Verdict: approve on current head 995a231.

I re-reviewed this against the earlier blockers and against D's sandbox shape. The previous merge blockers look resolved:

  • Windows path assertion now compares the sandbox-exec escaped path via sandboxProfileString, so the DenyWrite profile test is platform-safe.
  • SOCKS5 negotiation now only selects no-auth when the client offered 0x00, otherwise replies 0xff; there is a regression test for that path.
  • The re-entrancy guard no longer trusts only ZERO_SANDBOXED=1; it now requires the correlated backend marker that Zero sets on wrapped commands. This is good enough for a re-entrancy marker. It is not a cryptographic boundary against a user intentionally launching Zero with spoofed env, but that is not the threat model here.
  • Path-list enforcement now runs even without a workspace root, with workspace-boundary enforcement gated separately. That fixes the absolute-path bypass concern while keeping ModeDisabled inert.

Cross-checking with D: D is mostly a pre-check + prompt + persisted-grant service. It is intentionally product/UX-led: detect violations, show a clear prompt, and let the user allow/deny. Zero's Go runner is more protective here because it also wires command planning, OS backend profiles, read exclusions for search, and fail-closed scoped egress. That extra strictness is appropriate for Zero's Go-native terminal-agent backend as long as we keep the UX understandable.

One product note, not a blocker for this PR: the backend is now stronger than the visible permissions UI. We should still follow up with the /permissions manager/card work so users can see and edit these grants/policies without reading raw state.

Validation I ran locally:

  • git diff --check origin/main...HEAD
  • go test ./internal/sandbox ./internal/tools -count=1
  • go test ./...
  • go vet ./...
  • go run ./cmd/zero-release build
  • go run ./cmd/zero-release smoke

At review time, GitHub CI had most jobs green, with Windows smoke and CodeRabbit still pending. Merge after the remaining required checks finish green.

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

Actionable comments posted: 1

🤖 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 `@internal/sandbox/pathlists.go`:
- Around line 347-350: The policyHasPathLists function currently checks only
whether the raw policy slices are non-empty, but this disagrees with how the
rest of the file normalizes path lists by dropping blank, missing, and unusable
entries. Modify policyHasPathLists to check the effective normalized path lists
instead of the raw config slices. This ensures that when workspaceRoot is empty,
the fail-closed check in validatePathWithPolicy accurately reflects which path
restrictions are actually enforceable after normalization, preventing false
positives from typos or invalid paths that would be dropped during resolution.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7b88c824-c463-4c65-b356-0dbbe10b8ea7

📥 Commits

Reviewing files that changed from the base of the PR and between 580e310 and 995a231.

📒 Files selected for processing (3)
  • internal/sandbox/engine.go
  • internal/sandbox/pathlists.go
  • internal/sandbox/pathlists_test.go

Comment thread internal/sandbox/pathlists.go Outdated
@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

Follow-up after the latest CodeRabbit pass: I agree with the new policyHasPathLists comment.

This is not a security regression and it does not change my D-vs-Zero read: Zero being more protective at the backend is still the right direction. But this specific helper should check the effective normalized path lists, not raw non-empty slices, otherwise a blank/missing typoed entry can make no-workspace-root evaluation fail closed even though no enforceable rule remains.

So my verdict is: backend/product direction approved, but please take this tiny correctness cleanup before merge (or explicitly document that raw configured-but-unresolvable entries are intended to fail closed).

…th lists

policyHasPathLists now resolves the lists (resolvePolicyPaths/
resolveWriteRootPaths) instead of counting raw config, matching how the rest of
the file normalizes them. A typo or non-existent entry (dropped during
resolution) therefore no longer makes validatePathWithPolicy fail-close every
relative request when there is no workspace root.

Gates: gofmt, vet, build, test (incl. -race), staticcheck (no new findings),
govulncheck (0), deadcode (no new unreachable funcs).

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

Verdict: APPROVE.

The earlier confusing review/comment from me should be read as superseded by this one.

Current head f00e28d fixes the remaining correctness issue: policyHasPathLists now checks normalized/effective path-list entries instead of raw config slices, so blank/missing typo paths do not create a false fail-closed decision when there is no workspace root.

Cross-check vs D: D handles sandbox mostly as a pre-check + prompt + persisted-grant product layer. Zero is intentionally more protective in the Go backend: command wrapping, OS sandbox profiles, scoped egress proxying, read-denied search exclusions, and fail-closed behavior. That is the right direction for Zero as a terminal coding agent backend. The product/UI follow-up is still /permissions manager polish, but that should be a separate PR and should not block this backend hardening.

Local validation on latest head:

  • go test ./internal/sandbox ./internal/tools -count=1

Merge once the remaining required GitHub checks/CodeRabbit finish green.

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.

2 participants