Skip to content

feat: add M6 sandbox backend#77

Merged
gnanam1990 merged 4 commits into
mainfrom
feat/m6-sandbox-backend
Jun 5, 2026
Merged

feat: add M6 sandbox backend#77
gnanam1990 merged 4 commits into
mainfrom
feat/m6-sandbox-backend

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add the M6 sandbox backend package with a shared policy engine, risk classifier, structured violation errors, persistent allow/deny grants, and Linux/macOS/policy-only backend selection.
  • Enforce sandbox decisions through the Go tool registry and agent runtime so unsafe/high-autonomy runs still receive structured policy denials for out-of-workspace, symlink, network, and destructive shell risks.
  • Add zero sandbox policy and zero sandbox grants backend commands for policy inspection plus persistent grant allow/deny/list/revoke/clear workflows.

Scope

  • PRD slice: M6 Gnanam sandbox backend.
  • This does not implement Windows sandboxing or TUI permission prompts; it provides the shared backend contracts those later slices can consume.

Validation

  • git diff --check origin/main...HEAD
  • go test -count=1 -p 1 ./...
  • bun run typecheck
  • bun test ./tests --timeout 15000
  • bun run build
  • bun run smoke:build
  • bun run smoke:go
  • ZERO_SANDBOX_GRANTS_PATH=$(mktemp -d)/grants.json ./zero sandbox policy --json
  • ZERO_SANDBOX_GRANTS_PATH=$(mktemp -d)/grants.json ./zero sandbox grants allow write_file --auto medium --reason final-local-check --json
  • ZERO_SANDBOX_GRANTS_PATH=$(mktemp -d)/grants.json ./zero sandbox grants list --json
  • ZERO_SANDBOX_GRANTS_PATH=$(mktemp -d)/grants.json ./zero sandbox grants clear --confirm --json

Size

  • 20 files changed, about 2.2k inserted lines, split into 3 focused commits.

Summary by CodeRabbit

  • New Features

    • Added sandbox functionality to restrict and control tool execution (workspace path enforcement, network/destructive-shell blocking).
    • Added persistent grant store to save tool approvals and autonomy levels.
    • Introduced zero sandbox CLI with policy (view policy/backend) and grants (allow/deny/list/revoke/clear) commands and JSON/text output options.
  • Tests

    • Added extensive tests covering sandbox policy evaluation, path/symlink protections, backend selection, CLI flows, and grant persistence.

@github-actions

github-actions Bot commented Jun 5, 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] Typecheck: bun run typecheck
  • [pass] Tests: bun run test
  • [pass] Build: bun run build
  • [pass] Smoke build: bun run smoke:build

Scope

Head: e78bdf1833f3
Changed files (20): internal/agent/loop.go, internal/agent/loop_test.go, internal/agent/types.go, internal/cli/app.go, internal/cli/app_test.go, internal/cli/exec.go, internal/cli/sandbox.go, internal/cli/sandbox_test.go, internal/sandbox/adapters.go, internal/sandbox/adapters_test.go, internal/sandbox/engine.go, internal/sandbox/engine_test.go, and 8 more

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.

What's good

  • The internal/sandbox package is well-factored. types.go is pure data (typed strings for every classification, plus Policy, Request, Decision, Risk, Violation, Grant). normalize.go is a small input-validation layer. risk.go does classification. paths.go does workspace enforcement. grants.go does persistence. engine.go is the policy evaluator. adapters.go is platform selection. Each file is single-purpose and easy to test in isolation, and the package depends only on redaction from earlier slices.
  • The engine is conservative by default and degrades predictably. DefaultPolicy is Mode: enforce, Network: deny, EnforceWorkspace: true, DenyDestructiveShell: true, with a policy-only backend on platforms that do not have bwrap or sandbox-exec. SelectBackend reports Available: false and a Message for the fallback case, and BuildPlan always documents the active restrictions, including "platform sandbox unavailable; policy engine still evaluates tool requests". Users running on Windows or a stripped-down Linux image get a clear signal that the OS-level sandbox is not engaged.
  • Workspace enforcement is symlink-aware and consistent with the read/write tools. validateWorkspacePaths resolves the workspace root via filepath.EvalSymlinks, joins and Abs-normalizes the target, and then walks the relative path segment-by-segment, rejecting any Lstat symlink whose resolved target escapes the root. The same outside_workspace and symlink_traversal violations are raised whether the path is absolute, relative, or traverses a pre-existing symlink. This matches the gate logic in the existing write_file / read_file tools and gives the agent runtime defense-in-depth.
  • Risk classification is a separate concern from policy. Classify reads SideEffect, scans command for networkCommandPattern, destructiveCommandPattern, and piped installers (| sh, | bash), and flags path_escape for .. and absolute paths. Categories are sorted for stable JSON. The engine then uses HasRiskCategory to decide which policy clauses apply. This split means a future slice can add a new risk category without touching the engine.
  • Persistent grants are first-class and well-protected. GrantStore validates tool names against ^[A-Za-z0-9._-]+$ (rejecting empty and ../escape), normalizes decision and autonomy, redacts Reason on both write and load, persists with atomic temp+rename under a 0o600 file in a 0o700 directory, and protects writers with a sync.Mutex. Schema versioning is in place (schemaVersion: 1 with explicit rejection of other versions), so a v2 migration is one branch away. Lookup matches grants only when requestedAutonomy <= grant.MaxAutonomy (low/medium/high), so a medium grant does not satisfy a high request.
  • Sandbox is enforced in the right places. tools.Registry.RunWithOptions evaluates the sandbox before the tool runs and turns ActionDeny into a structured error result ("Sandbox violation [code] for tool at path: reason"). ActionPrompt without PermissionGranted returns a structured "Sandbox approval required" error, so the agent sees a clear next step. The agent runtime forwards Autonomy and Sandbox to the registry, and TestRunAppliesSandboxEvenInUnsafeMode locks in that the sandbox still applies when PermissionModeUnsafe is set, which is exactly the M6 promise.
  • CLI surface is consistent with the rest of the codebase. zero sandbox policy prints either text (Zero sandbox policy / root / mode / network / backend / backend_available / grants) or JSON with {policy, backend, plan, grantsPath}. zero sandbox grants {list,allow,deny,revoke,clear} mirrors the MCP grants surface from #40 and uses the same --json and --confirm patterns. --confirm is required for clear, and TestRunSandboxHelpDoesNotOpenStore proves that help paths do not touch the store. nextFlagValue is reused for --auto and --reason, so the parser is consistent with zero exec and does not regress the flag-shaped-value gap from #54.
  • Redaction is applied at the persistence boundary. Grant.Reason is redacted in createGrant (on the way in) and again in normalizeStoredGrant (on the way out), so a secret reason saved by a user or hand-edited in the JSON file is rewritten as [REDACTED] on next load. The grants JSON file therefore never stores raw secrets even if a user pastes one in.
  • Backend selection is injectable. SelectBackend takes BackendOptions{GOOS, LookupExecutable} with sensible defaults (runtime.GOOS, exec.LookPath), and appDeps.selectSandboxBackend lets tests pin the backend to policy-only. TestRunSandboxPolicyInspectTextAndJSON exercises both text and JSON without depending on the host's bwrap/sandbox-exec install, which keeps CI portable.
  • Context cancellation is handled explicitly. Engine.Evaluate checks ctx.Err() at entry and returns ViolationContextCanceled with a redacted reason, and TestEngineReportsContextCancellation exercises it. The grant store's Lookup path is not in the cancellation critical section (it is a fast read), which is appropriate.

Observations (non-blocking)

  1. Lookup is not mutex-protected. Grant/Revoke/Clear hold the mutex while reading, mutating, and rewriting the file, but Lookup reads the file outside the lock. For a single-process CLI this is fine, but a concurrent Grant + Lookup can race: Lookup may see the pre-grant state and return Matched: false, or Grant may read pre-revoke state and rewrite an entry the caller intended to remove. If internal/agent ever calls Lookup from a background goroutine, prefer sync.RWMutex with a cached grantFile snapshot, or move the read inside the mutex.

  2. TOCTOU between sandbox check and tool execution. Engine.Evaluate checks the path, but the actual write happens in internal/tools/write_file.go after the registry returns. A pre-created symlink placed between the two checks (or created by a parallel process) could still escape the workspace. The existing write tool's per-segment Lstat rejection is the real defense; the sandbox check is a fast pre-filter. Worth a comment on the engine noting that the write tool is the source of truth for the file boundary.

  3. Risk regex is best-effort. networkCommandPattern covers curl, wget, scp, ssh, rsync, nc/netcat, python -m http.server, npm/pnpm/yarn/bun add|install|publish|login, pip install, go get, git clone, gh release download. A payload like python3 -c "import urllib.request, os; os.system('curl ...')" does not match, and neither does bash -c 'curl ...'. The shell tool is SideEffectShell so the engine still classifies it as RiskHigh; users in unsafe mode opt into this. A future slice should consider a stronger detection story (e.g., a shell-aware AST or a vetted allowlist of commands).

  4. Piped-installer detection uses strings.Contains. | sh and | bash are caught, but > sh, exec sh, backticks, or eval "curl" are not. Again, classified as RiskHigh; the user explicitly approves by being in unsafe mode. The detection is a hint, not a guarantee.

  5. RiskReason always emits the level, but the level is duplicated with Decision.Risk.Level. A consumer that prints a JSON decision already has both fields. Keeping RiskReason is fine for text output, but a future refactor could derive it at format time and drop the field.

  6. Engine does not use the platform backend at runtime. SelectBackend and BuildPlan are informational; the engine does not actually wrap bash invocations in bwrap/sandbox-exec. The M6 PR description explicitly says this is the backend contract and that platform integration comes later. The current BackendPlan is a good debug surface for zero sandbox policy, but the engine should be revisited when a future slice actually wires the platform sandbox.

  7. AllowPolicyOnlyRunner is in Policy but unused by the engine. The default is true and there is no engine branch that reads it. Either it is a future knob (e.g., fail closed when the platform sandbox is unavailable and the policy says "no") or it should be removed. Keeping it is fine if a follow-up PR will reference it, but the current engine does not enforce it.

  8. EngineOptions.Backend has no default for the zero value. NewEngine accepts a Backend value, and buildExecSandboxEngine always passes a result from SelectBackend, but a test that constructs NewEngine(EngineOptions{WorkspaceRoot: ...}) directly will see Backend.Name == "". The engine does not look at Backend for evaluation, so this is harmless today, but worth a defaultBackend() helper for future engine branches that do.

  9. runExec builds a new GrantStore on every invocation via deps.newSandboxStore. That is fine (the file is small, the reads are infrequent), but if a future slice needs to cache a per-process snapshot, the store has no Snapshot() helper. A small addition would be a Lookup that takes a snapshot to avoid file I/O on every tool call.

  10. runExec does not pass the per-exec autonomy to the engine. The agent runtime reads Options.Autonomy and forwards it on every RunWithOptions call, so the engine does see it. The split between buildExecSandboxEngine (no autonomy) and agent.Run (autonomy per request) is correct, but a reader of cli/exec.go may wonder why autonomy is not threaded at engine construction. A small comment would help.

  11. runWithDeps does not add a sandbox test to the no-TUI list at app_test.go. It does (TestRunCommandsDoNotLaunchTUI includes {"sandbox"}), and assertHelpOutput includes "sandbox". The follow-up is just noting the test coverage is consistent with the other commands.

  12. TestRunSandboxGrantsAllowListDenyRevokeAndClear asserts len(listPayload.Grants) != 2 and a specific sort order. The Store.List helper sorts tool names alphabetically, so bash comes before write_file. The test relies on that ordering. A test that adds apt and cat after bash would confirm the sort is stable across more than two entries. Minor.

  13. parseSandboxPositionalOptions defaults autonomy to AutonomyLow if --auto is not provided. The CLI then sends a Low-autonomy request to the store, which means a tool that was granted at Medium will not be matched for a Low request... wait, autonomyAllowed uses requested <= max, so a Low request matches a Medium grant. Good. But if a tool was granted at Low only, a Medium request would not match — that is the intended semantic. Worth documenting on the grant help text.

  14. runSandboxGrants uses parseSandboxCommandOptions for the list subcommand but the parser disallows any positional argument. If a user runs zero sandbox grants list extra, they get a usage error. That is the right behavior; the test does not cover it, but the production code rejects the input. No action needed.

  15. BuildPlan returns a flat []string of restrictions. The text format prints each on a separate line, but a JSON consumer sees a list of free-form strings. A typed Restriction enum (filesystem, network, destructive, platform_unavailable) would be more discoverable for tools that consume the JSON. Cosmetic; can wait for a real consumer.

  16. Engine.Evaluate always overrides request.WorkspaceRoot with engine.workspaceRoot via firstNonEmpty. That is the right precedence (engine-level wins), but a per-request override is silently ignored. If a future slice wants to evaluate a tool against a sub-workspace (e.g., a worktree), the precedence needs an explicit Request.WorkspaceRoot field that the engine respects.

  17. runExec does not currently pass --auto to the sandbox at construction time. A user running zero exec --auto high would set options.autonomy = "high" (per earlier exec wiring), which is forwarded to agent.Options.Autonomy and then to the registry. The path is correct; the gap is that the sandbox engine itself is not parameterized by autonomy, so a future "autonomy ceiling" knob would need to be added to EngineOptions.

  18. The WriteFile tool is exercised by the sandbox test, but ReadFile, Bash, UpdatePlan, etc. are not. The engine tests cover all of these via direct Request construction, which is the right level for engine coverage. A future PR that registers more tools should add a TestRegistryAppliesSandboxFor<EveryTool> matrix test if the per-tool safety metadata diverges from the engine's risk classification.

  19. Engine.Evaluate returns Decision{Action: ActionAllow, Risk: ..., Reason: "sandbox policy disabled"} when policy.Mode == ModeDisabled. That is correct, but the Risk field is still computed. Consumers that print Decision.Risk for a disabled sandbox will see a populated risk object even though the decision is unconditional allow. Either drop Risk in the disabled branch or document the invariant.

  20. grantSchemaVersion = 1 is a private constant. If a future migration script wants to introspect the version, it has to read the file. A small exported CurrentGrantSchemaVersion would help tooling. Minor.

No blockers. The M6 backend contract is in place: structured policy engine, risk classifier, persistent allow/deny grants, Linux/macOS/policy-only backend selection, enforcement in the tool registry and agent runtime, and a consistent zero sandbox {policy,grants ...} CLI surface. Tests cover workspace symlinks, out-of-workspace paths, network/destructive shell risk, persistent allow/deny matching, autonomy rank filtering, schema-version rejection, malformed-file rejection, atomic write durability (via reopen), context cancellation, and the unsafe-mode invariant. The two follow-ups I would prioritize next: (1) make Lookup concurrency-safe (RWMutex + cached snapshot) before the agent runtime calls it from background goroutines, and (2) document or remove AllowPolicyOnlyRunner (currently unused) so the policy struct matches what the engine actually enforces.

@coderabbitai

coderabbitai Bot commented Jun 5, 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: 9fe3cd2a-8a9e-43e5-98b9-2004dc2c39e5

📥 Commits

Reviewing files that changed from the base of the PR and between 6ef55cc and e78bdf1.

📒 Files selected for processing (6)
  • internal/sandbox/adapters.go
  • internal/sandbox/engine_test.go
  • internal/sandbox/grants.go
  • internal/sandbox/risk.go
  • internal/tools/registry.go
  • internal/tools/registry_test.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • internal/tools/registry.go
  • internal/sandbox/engine_test.go
  • internal/sandbox/risk.go
  • internal/sandbox/adapters.go
  • internal/sandbox/grants.go

Walkthrough

Adds a sandbox subsystem: typed policy model, risk classifier, workspace/path validation, persistent grant store, backend selection, CLI management commands, and integration that enforces sandbox decisions before tool execution across agent and exec flows.

Changes

Sandbox Core System and Integration

Layer / File(s) Summary
Sandbox types and policy model
internal/sandbox/types.go
Exports enums and structs for policies, requests, decisions, risks, violations, backends, and helpers for formatting and default policy.
Risk classification and severity detection
internal/sandbox/risk.go
Classifies request risk using regex patterns and path heuristics; returns level, categories, and human-readable reason.
Workspace path validation and symlink traversal detection
internal/sandbox/paths.go
Validates requested paths against the normalized workspace root and rejects symlink traversal that escapes the workspace.
Sandbox backend selection and planning
internal/sandbox/adapters.go, internal/sandbox/adapters_test.go
Selects platform adapter (bwrap/sandbox-exec) or policy-only fallback; builds backend plan documenting restrictions and workspace context.
Enum validation and normalization
internal/sandbox/normalize.go
Normalizes and validates Autonomy, Permission, SideEffect, and GrantDecision inputs and provides autonomy rank checks.
Sandbox evaluation engine
internal/sandbox/engine.go, internal/sandbox/engine_test.go
Evaluates requests against policy, risk, workspace validation, and persistent grants to return allow/prompt/deny decisions; handles context cancellation and disabled engine.
Persistent grant store for tool approval decisions
internal/sandbox/grants.go, internal/sandbox/grants_test.go
Thread-safe JSON-backed grant store with resolveable file path, schema versioning, atomic writes, Grant/Lookup/List/Revoke/Clear APIs, validation, and deterministic formatting.
Tool registry sandbox enforcement checkpoint
internal/tools/registry.go, internal/tools/registry_test.go
Runs sandbox.Evaluate before tool execution, blocks or prompts based on Decision, and honors persistent-grant authorization for prompt tools; tests verify enforcement and persistent grant behavior.
Agent loop sandbox and autonomy propagation
internal/agent/types.go, internal/agent/loop.go, internal/agent/loop_test.go
Adds Autonomy and Sandbox to agent Options and forwards them into tool execution; test ensures sandbox still prevents outside-workspace writes even in unsafe permission mode.
Exec command sandbox initialization and propagation
internal/cli/exec.go
Builds a sandbox Engine for the exec workspace (store + default policy + selected backend) and injects it and autonomy into agent.Run, returning sandbox_error if engine creation fails.
CLI app dependency and command routing
internal/cli/app.go, internal/cli/app_test.go
Adds sandbox dependency hooks to appDeps, fills defaults, routes sandbox subcommand, updates help and non-TUI lists; tests updated to include sandbox.
Sandbox CLI command implementations
internal/cli/sandbox.go, internal/cli/sandbox_test.go
Implements zero sandbox command with policy and grants subcommands (list/allow/deny/revoke/clear), flag parsing for --json/--auto/--reason/--confirm, help output, and integration tests for lifecycle and help behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Gitlawb/zero#48: Both PRs modify internal/tools/registry.go’s RunWithOptions/tool-execution permission gating.
  • Gitlawb/zero#60: Both PRs modify internal/agent/loop.go's executeToolCall wiring for tool-run option propagation and filtering.
  • Gitlawb/zero#47: Both touch the tools registry execution path and permission handling.

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 'feat: add M6 sandbox backend' directly aligns with the main objective of this PR, which is implementing a sandbox backend package with policy enforcement, risk classification, and CLI commands.
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/m6-sandbox-backend

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

🧹 Nitpick comments (2)
internal/cli/sandbox_test.go (1)

13-100: ⚡ Quick win

Consider adding error path coverage in follow-up work.

The test validates the happy path lifecycle comprehensively. For completeness, consider adding tests for error scenarios: invalid autonomy values (e.g., --auto invalid), store failures, revoking non-existent grants, and clearing an empty store. These would increase confidence that error messages are user-friendly.

🤖 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/cli/sandbox_test.go` around lines 13 - 100, Add negative-path tests
to internal/cli/sandbox_test.go alongside
TestRunSandboxGrantsAllowListDenyRevokeAndClear: cover (1) invalid autonomy by
calling runWithDeps with e.g.
{"sandbox","grants","allow","write_file","--auto=invalid","--json"} and assert a
usage/validation exit code and stderr contains an autonomy error, (2) store
failures by providing appDeps.newSandboxStore that returns an error and
asserting commands return failure and surface the store error, (3) revoking a
non-existent grant by calling runWithDeps revoke on a name not added and
asserting the JSON revoked==0, and (4) clearing an already-empty store by
ensuring clear with --confirm returns cleared==0; reuse runWithDeps,
sandbox.GrantStore/test helpers (e.g. newSandboxTestStore) to inject the failing
store behavior.
internal/sandbox/adapters.go (1)

52-55: ⚡ Quick win

Avoid mutating input parameters.

Line 54 reassigns the policy parameter directly, which violates Go's convention of treating input parameters as read-only. While this doesn't cause a bug (since Policy is passed by value), it reduces clarity.

♻️ Proposed fix using a local variable
 func (backend Backend) BuildPlan(workspaceRoot string, policy Policy) BackendPlan {
+	effectivePolicy := policy
 	if policy.Mode == "" {
-		policy = DefaultPolicy()
+		effectivePolicy = DefaultPolicy()
 	}
 	restrictions := []string{}
-	if policy.EnforceWorkspace {
+	if effectivePolicy.EnforceWorkspace {
 		restrictions = append(restrictions, "filesystem writes must stay inside workspace")
 	}
-	if policy.Network == NetworkDeny {
+	if effectivePolicy.Network == NetworkDeny {
 		restrictions = append(restrictions, "network access denied unless a future adapter grants it explicitly")
 	}
-	if policy.DenyDestructiveShell {
+	if effectivePolicy.DenyDestructiveShell {
 		restrictions = append(restrictions, "destructive shell patterns denied before execution")
 	}
 	if backend.Name == BackendPolicyOnly {
 		restrictions = append(restrictions, "platform sandbox unavailable; policy engine still evaluates tool requests")
 	}
 	return BackendPlan{
 		Backend:       backend,
 		WorkspaceRoot: workspaceRoot,
-		Policy:        policy,
+		Policy:        effectivePolicy,
 		Restrictions:  restrictions,
 	}
 }
🤖 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/adapters.go` around lines 52 - 55, The BuildPlan function
currently reassigns the input parameter policy; change this to use a new local
variable (e.g., p) so you don't mutate the parameter: copy policy into a local
(p := policy), check/assign default with p if needed (if p.Mode == "" { p =
DefaultPolicy() }), and then use p throughout the rest of BuildPlan instead of
policy; this keeps the input parameter immutable and follows Go conventions.
🤖 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/grants.go`:
- Around line 151-166: The List method reads shared state without holding the
store.mu lock, causing a race; modify GrantStore.List to acquire store.mu.Lock()
before calling readState() and release it with defer store.mu.Unlock() (mirror
the locking pattern used in Lookup), ensuring the entire read and subsequent
iteration over state.Grants occurs under the mutex to prevent concurrent
read/write races.
- Around line 132-149: Acquire the store mutex at the start of Lookup before
calling readState to prevent concurrent mutation races: in Lookup (the method
using readState and returning GrantLookup) call store.mu.Lock() and defer
store.mu.Unlock() prior to invoking store.readState(), mirroring how
Grant/Revoke/Clear protect state; keep ValidateToolName/NormalizeAutonomy usage
as appropriate but ensure the readState access is guarded by the lock.

In `@internal/sandbox/risk.go`:
- Around line 47-49: The piped installer detection is case-sensitive (uses
strings.Contains(command, "| sh") / "| bash") and can be bypassed with
capitalized variants; update the check that triggers add("piped_installer",
RiskCritical) to perform a case-insensitive match (e.g., convert command to
lower-case before checking or use a (?i) regex like the other
network/destructive patterns) so "| SH", "| Sh", "| BASH", etc. are caught
consistently.

---

Nitpick comments:
In `@internal/cli/sandbox_test.go`:
- Around line 13-100: Add negative-path tests to internal/cli/sandbox_test.go
alongside TestRunSandboxGrantsAllowListDenyRevokeAndClear: cover (1) invalid
autonomy by calling runWithDeps with e.g.
{"sandbox","grants","allow","write_file","--auto=invalid","--json"} and assert a
usage/validation exit code and stderr contains an autonomy error, (2) store
failures by providing appDeps.newSandboxStore that returns an error and
asserting commands return failure and surface the store error, (3) revoking a
non-existent grant by calling runWithDeps revoke on a name not added and
asserting the JSON revoked==0, and (4) clearing an already-empty store by
ensuring clear with --confirm returns cleared==0; reuse runWithDeps,
sandbox.GrantStore/test helpers (e.g. newSandboxTestStore) to inject the failing
store behavior.

In `@internal/sandbox/adapters.go`:
- Around line 52-55: The BuildPlan function currently reassigns the input
parameter policy; change this to use a new local variable (e.g., p) so you don't
mutate the parameter: copy policy into a local (p := policy), check/assign
default with p if needed (if p.Mode == "" { p = DefaultPolicy() }), and then use
p throughout the rest of BuildPlan instead of policy; this keeps the input
parameter immutable and follows Go conventions.
🪄 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: 2e83454a-1c60-4981-a842-ddbebb0b9616

📥 Commits

Reviewing files that changed from the base of the PR and between b973084 and 6ef55cc.

📒 Files selected for processing (20)
  • internal/agent/loop.go
  • internal/agent/loop_test.go
  • internal/agent/types.go
  • internal/cli/app.go
  • internal/cli/app_test.go
  • internal/cli/exec.go
  • internal/cli/sandbox.go
  • internal/cli/sandbox_test.go
  • internal/sandbox/adapters.go
  • internal/sandbox/adapters_test.go
  • internal/sandbox/engine.go
  • internal/sandbox/engine_test.go
  • internal/sandbox/grants.go
  • internal/sandbox/grants_test.go
  • internal/sandbox/normalize.go
  • internal/sandbox/paths.go
  • internal/sandbox/risk.go
  • internal/sandbox/types.go
  • internal/tools/registry.go
  • internal/tools/registry_test.go

Comment thread internal/sandbox/grants.go
Comment thread internal/sandbox/grants.go
Comment thread internal/sandbox/risk.go Outdated

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

Blockers

  • Persistent sandbox grants do not currently authorize prompt-gated core tools through the actual registry/agent execution path. internal/tools/registry.go returns at the prompt-permission check before it calls options.Sandbox.Evaluate, so a saved zero sandbox grants allow write_file --auto medium is persisted and listed, but Registry.RunWithOptions(... Sandbox: engine, PermissionGranted: false ...) still fails with Permission required for write_file. That makes the new zero sandbox grants allow workflow ineffective for write_file, edit_file, apply_patch, and bash, and leaves the M6 backend contract different from the CLI/API promise. I confirmed this with a local probe test that creates a grant store, grants write_file, routes through Registry.RunWithOptions, and still gets the prompt-denial error before sandbox grant lookup.

Non-Blocking

  • None.

Looks Good

  • The sandbox package has a clean split between policy evaluation, risk classification, workspace/symlink checks, backend selection, and grant persistence.
  • CLI coverage for zero sandbox policy / zero sandbox grants is solid, and the persisted grant file uses schema validation, sorted output, redacted reasons, and 0600 writes.
  • Validation passed locally: targeted Go packages, full go test -count=1 -p 1 ./..., bun run typecheck, 212/212 Bun tests, build, smoke build, smoke Go, sandbox CLI JSON/list/clear probes, and git diff --check. PR base is current with origin/main; no extra stale-base merge was needed.

Verdict: Changes Requested — clean structure and tests, but the persistent grant path needs to authorize prompt-gated tools before this can land.

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

Blockers

None found. The prior sandbox grant blocker is fixed at e78bdf1833f345d9bb61e57864b11fb4233c5529: Registry.RunWithOptions now evaluates sandbox decisions before the prompt-permission fallback and treats a matched persistent grant as authorization for prompt-gated tools.

Non-Blocking

None.

Looks Good

  • Added a regression test that exercises the real registry path: persistent write_file grants now authorize execution without PermissionGranted.
  • The incremental fix is focused: registry ordering, grant-store read locking, policy default handling, and a small shell-risk case-insensitivity hardening.
  • Validation passed locally: targeted Go packages, full go test -count=1 -p 1 ./..., TypeScript typecheck, 212/212 Bun tests, build, smoke:build, smoke:go, whitespace check, and sandbox grant CLI smokes.
  • GitHub checks are green for Ubuntu, macOS, Windows, performance smoke, Zero Review, and CodeRabbit.

Verdict: Approve — clean fix for the authorization-path blocker; ready from my side.

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

Approved after rereview: prior sandbox grant authorization blocker is fixed and validation is 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.

3 participants