Files
zero/internal/sandbox/auto_allow_bash_test.go
Anandan 80ab238361 Align sandbox command approval behavior (#322)
* Align shell approval sandbox execution

Make approved shell command-prefix executions use the escalated sandbox path when denied-read policy allows it, so remembered approvals do not still hit the restrictive sandbox.

Allow auto sandbox mode to degrade to a direct command plan when native command wrapping is unavailable, while strict sandbox require continues to fail closed.

Update regression tests for degraded execution, prefix-approved shell calls, and unavailable native sandbox metadata.

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/agent ./internal/sandbox

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/tools -run 'TestRegistryRunsWithDegradedUnavailableNativeSandbox|TestBashToolRunsWithDegradedUnavailableNativeSandbox|TestBashRequireEscalatedBypassesNativeSandboxAfterApproval|TestExecCommandRequireEscalatedBypassesNativeSandboxAfterApproval'

Tested: GOCACHE=/tmp/zero-go-cache go test ./... -run '^$'

Tested: git diff --check

* Align sandbox command approval behavior

Evaluate reusable shell approvals across plain command segments so prefix grants can cover commands with safe read-only tails without relying on command-specific sandbox exceptions.

Keep process inspection commands sandboxed by default; unsandboxed host access continues to require explicit escalation or a narrow command-prefix approval.

Improve shell tool guidance around sandboxed additional permissions, full escalation, justifications, and narrow reusable prefixes.

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./internal/tools ./internal/agent

Tested: env HOME=/tmp/zero-test-home-clean XDG_CONFIG_HOME=/tmp/zero-test-config-clean GOCACHE=/tmp/zero-go-cache go test ./...

* Normalize explicit sandbox backend platforms

Infer the platform from a caller-provided sandbox backend name before choosing the manager runtime platform. This keeps Linux, macOS, and Windows backend plans deterministic across host OSes when tests or callers provide a backend without an explicit Platform field.

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./internal/tools

Tested: env HOME=/tmp/zero-test-home-clean XDG_CONFIG_HOME=/tmp/zero-test-config-clean GOCACHE=/tmp/zero-go-cache go test ./...
2026-06-26 13:20:07 +05:30

121 lines
3.9 KiB
Go

package sandbox
import (
"context"
"testing"
)
func sandboxedShellEngine(t *testing.T, backend Backend) *Engine {
t.Helper()
return NewEngine(EngineOptions{
WorkspaceRoot: t.TempDir(),
Policy: DefaultPolicy(),
Backend: backend,
})
}
func bashRequest() Request {
return Request{
ToolName: "bash",
SideEffect: SideEffectShell,
Permission: PermissionPrompt,
PermissionMode: PermissionModeAsk,
Args: map[string]any{"command": "echo hi"},
}
}
var nativeWrappingBackend = Backend{
Name: BackendLinuxBwrap,
Available: true,
Executable: "/usr/bin/zero-linux-sandbox",
CommandWrapping: true,
NativeIsolation: true,
}
func TestSandboxedBashAutoAllowedWhenSandboxActive(t *testing.T) {
engine := sandboxedShellEngine(t, nativeWrappingBackend)
decision := engine.Evaluate(context.Background(), bashRequest())
if decision.Action != ActionAllow {
t.Fatalf("decision = %#v, want allow (sandbox active)", decision)
}
}
func TestSandboxedBashRequireEscalatedStillPromptsWhenSandboxActive(t *testing.T) {
engine := sandboxedShellEngine(t, nativeWrappingBackend)
request := bashRequest()
request.Args["sandbox_permissions"] = "require_escalated"
decision := engine.Evaluate(context.Background(), request)
if decision.Action != ActionPrompt || decision.Reason != ReasonEscalatedSandboxRequired {
t.Fatalf("decision = %#v, want require_escalated prompt", decision)
}
}
func TestSandboxedBashProcessListingAutoAllowedWhenSandboxActive(t *testing.T) {
engine := sandboxedShellEngine(t, nativeWrappingBackend)
request := bashRequest()
request.Args["command"] = "ps aux"
decision := engine.Evaluate(context.Background(), request)
if decision.Action != ActionAllow {
t.Fatalf("decision = %#v, want allow (sandbox active)", decision)
}
if len(decision.Risk.Categories) != 1 || !HasRiskCategory(decision.Risk, "shell") {
t.Fatalf("risk = %#v, want only shell category", decision.Risk)
}
if !decision.AutoAllowed {
t.Fatalf("sandboxed shell command should be auto-allowed: %#v", decision)
}
}
func TestSandboxedBashStillPromptsWithoutSandbox(t *testing.T) {
engine := sandboxedShellEngine(t, Backend{Name: BackendUnavailable})
decision := engine.Evaluate(context.Background(), bashRequest())
if decision.Action != ActionPrompt {
t.Fatalf("decision = %#v, want prompt (no active sandbox)", decision)
}
}
func TestSandboxedBashAutoAllowDoesNotAffectNonShell(t *testing.T) {
engine := sandboxedShellEngine(t, nativeWrappingBackend)
// A non-shell prompt tool must still prompt; auto-allow is shell-only. Use a
// generic write tool name so the workspace file-tool auto-allow does not
// apply.
decision := engine.Evaluate(context.Background(), Request{
ToolName: "custom_writer",
SideEffect: SideEffectWrite,
Permission: PermissionPrompt,
PermissionMode: PermissionModeAsk,
Args: map[string]any{"path": "notes.txt"},
})
if decision.Action != ActionPrompt {
t.Fatalf("decision = %#v, want prompt (auto-allow is shell-only)", decision)
}
}
// TestShellSandboxActive reports correctly across backends and policy modes.
func TestShellSandboxActive(t *testing.T) {
root := t.TempDir()
native := NewEngine(EngineOptions{WorkspaceRoot: root, Policy: DefaultPolicy(), Backend: nativeWrappingBackend})
if !native.shellSandboxActive(DefaultPolicy()) {
t.Fatal("native wrapping backend must be sandbox-active")
}
unavailable := NewEngine(EngineOptions{WorkspaceRoot: root, Policy: DefaultPolicy(), Backend: Backend{Name: BackendUnavailable}})
if unavailable.shellSandboxActive(DefaultPolicy()) {
t.Fatal("unavailable backend must NOT be sandbox-active")
}
disabled := DefaultPolicy()
disabled.Mode = ModeDisabled
if native.shellSandboxActive(disabled) {
t.Fatal("disabled policy must NOT be sandbox-active")
}
var nilEngine *Engine
if nilEngine.shellSandboxActive(DefaultPolicy()) {
t.Fatal("nil engine must NOT be sandbox-active")
}
}