Files
zero/internal/sandbox/windows_shell_autoallow_test.go
Vasanth T ffaa3a9a48 fix(sandbox): stop bricking Windows when the sandbox isn't set up (degrade like Linux/macOS) (#295)
* fix(sandbox): degrade Windows to permission-gating when not set up (stop bricking)

On Windows the sandbox command runner hard-fails EVERY command until a one-time
elevated `zero sandbox setup` writes the setup marker — so a fresh Windows user
can't run a single shell command (the "windows sandbox is not initialized" error).
Linux/macOS already degrade when their sandbox is unavailable; Windows was the
lone outlier that bricks itself.

BuildExecutionRequest now, on Windows when the setup marker is missing:
- default (auto): DEGRADE to EnforcementDegraded — the command runs unwrapped
  under the in-process policy gate (workspace path-confinement) + per-command
  approval, with a one-time downgrade notice pointing at `zero sandbox setup`
  for full WFP/ACL/network isolation.
- `--sandbox require`: still hard-errors with a clear setup hint, so strict
  callers keep their guarantee.
- marker present: unchanged (native, wrapped, fully sandboxed).

Matches every reference agent (codex defaults its Windows sandbox off and runs
under approval; opencode/openclaude/vix degrade on Windows) — none brick the CLI
because a setup step hasn't run. Test covers all three paths; sandbox/doctor/cli
suites green; vet/build clean.

* fix(sandbox): don't auto-allow shell commands while Windows is degraded

Follow-up to the degrade fix in this PR. The permission engine's shellSandboxActive
keyed on backend *capabilities* (CommandWrapping && NativeIsolation), which stay
true on Windows even when setup hasn't run. So once the degrade fix let commands
RUN unwrapped, the engine still auto-allowed ordinary shell commands as if
sandboxed — running them with no OS isolation AND no permission prompt, breaking
the per-command-approval floor the degrade is supposed to keep (the function's own
doc comment promises "ordinary shell auto-allow never runs an unsandboxed
command").

shellSandboxActive now returns false on Windows when the sandbox isn't initialized
(mirrors manager.BuildExecutionRequest), so a degraded shell command falls through
to the normal approval prompt. Network/destructive/out-of-workspace checks already
ran earlier and are unaffected; native (post-setup) still auto-allows.

Regression test covers degraded->prompt and native->auto-allow. Sandbox suite green.
2026-06-21 22:49:39 +05:30

49 lines
1.9 KiB
Go

package sandbox
import (
"context"
"testing"
)
// The engine's shell auto-allow must track the ACTUAL runtime enforcement, not
// just backend capability: on Windows the command is only wrapped once setup has
// run. Degraded (no marker) -> an ordinary shell command must PROMPT (per-command
// approval is the floor); native (marker present) -> auto-allow (the OS sandbox
// is the boundary). Regression guard for the #295 degrade gap.
func TestEngineWindowsShellAutoAllowTracksSetup(t *testing.T) {
restore := windowsSandboxInitialized
t.Cleanup(func() { windowsSandboxInitialized = restore })
eng := NewEngine(EngineOptions{
WorkspaceRoot: `C:\ws`,
Policy: DefaultPolicy(),
Backend: Backend{
Name: BackendWindowsRestrictedToken, Available: true, Executable: "zero.exe",
Platform: "windows", CommandWrapping: true, NativeIsolation: true,
},
})
req := Request{
ToolName: "bash", SideEffect: SideEffectShell,
Permission: PermissionPrompt, PermissionMode: PermissionModeAsk,
Args: map[string]any{"command": "echo hi"},
}
// Degraded: setup not run -> not actually sandboxed -> must prompt.
windowsSandboxInitialized = func() bool { return false }
if eng.shellSandboxActive(DefaultPolicy()) {
t.Fatal("degraded: shellSandboxActive must be false (command runs unwrapped)")
}
if d := eng.Evaluate(context.Background(), req); d.Action != ActionPrompt {
t.Fatalf("degraded ordinary shell command must PROMPT, got action=%q reason=%q", d.Action, d.Reason)
}
// Native: setup done -> the sandbox is the boundary -> auto-allow.
windowsSandboxInitialized = func() bool { return true }
if !eng.shellSandboxActive(DefaultPolicy()) {
t.Fatal("native: shellSandboxActive must be true")
}
if d := eng.Evaluate(context.Background(), req); d.Action != ActionAllow || !d.AutoAllowed {
t.Fatalf("native ordinary shell command must AUTO-ALLOW, got action=%q autoAllowed=%v", d.Action, d.AutoAllowed)
}
}