mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 18:00:54 -10:00
* fix(sandbox): retry approved network shell commands * test(sandbox): fix portable smoke expectations * fix(sandbox): restore Windows offline egress block
156 lines
5.2 KiB
Go
156 lines
5.2 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Gitlawb/zero/internal/sandbox"
|
|
)
|
|
|
|
// nativeBackendStub reports as an active wrapping sandbox so shellSandboxActive
|
|
// is true. Its executable does not exist, so the command itself fails to launch
|
|
// — that is fine: these tests assert the *permission gate*, not execution.
|
|
func nativeBackendStub() sandbox.Backend {
|
|
return sandbox.Backend{
|
|
Name: sandbox.BackendLinuxBwrap,
|
|
Available: true,
|
|
Executable: "/nonexistent/zero-linux-sandbox-stub",
|
|
CommandWrapping: true,
|
|
NativeIsolation: true,
|
|
}
|
|
}
|
|
|
|
func sandboxedBashPolicy() sandbox.Policy {
|
|
policy := sandbox.DefaultPolicy()
|
|
// Network deny so no proxy is started for these gate-only tests.
|
|
return policy
|
|
}
|
|
|
|
const permissionRequiredFragment = "Permission required for bash"
|
|
|
|
func TestBashAutoAllowedWhenSandboxActive(t *testing.T) {
|
|
root := t.TempDir()
|
|
registry := NewRegistry()
|
|
registry.Register(NewBashTool(root))
|
|
engine := sandbox.NewEngine(sandbox.EngineOptions{
|
|
WorkspaceRoot: root,
|
|
Policy: sandboxedBashPolicy(),
|
|
Backend: nativeBackendStub(),
|
|
})
|
|
|
|
result := registry.RunWithOptions(context.Background(), "bash", map[string]any{
|
|
"command": "echo hi",
|
|
}, RunOptions{
|
|
PermissionGranted: false,
|
|
Sandbox: engine,
|
|
PermissionMode: string(sandbox.PermissionModeAsk),
|
|
Autonomy: "high",
|
|
})
|
|
|
|
if strings.Contains(result.Output, permissionRequiredFragment) {
|
|
t.Fatalf("bash was gated despite auto-allow: %q", result.Output)
|
|
}
|
|
if result.SandboxDecision == nil || result.SandboxDecision.Action != sandbox.ActionAllow || !result.SandboxDecision.AutoAllowed {
|
|
t.Fatalf("sandbox decision = %#v, want auto-allowed allow", result.SandboxDecision)
|
|
}
|
|
}
|
|
|
|
func TestBashRequireEscalatedPromptsWhenSandboxActive(t *testing.T) {
|
|
root := t.TempDir()
|
|
registry := NewRegistry()
|
|
registry.Register(NewBashTool(root))
|
|
engine := sandbox.NewEngine(sandbox.EngineOptions{
|
|
WorkspaceRoot: root,
|
|
Policy: sandboxedBashPolicy(),
|
|
Backend: nativeBackendStub(),
|
|
})
|
|
|
|
result := registry.RunWithOptions(context.Background(), "bash", map[string]any{
|
|
"command": "echo hi",
|
|
"sandbox_permissions": string(SandboxPermissionsRequireEscalated),
|
|
}, RunOptions{
|
|
PermissionGranted: false,
|
|
Sandbox: engine,
|
|
PermissionMode: string(sandbox.PermissionModeAsk),
|
|
Autonomy: "high",
|
|
})
|
|
|
|
if result.Status != StatusError || !strings.Contains(result.Output, sandbox.ReasonEscalatedSandboxRequired) {
|
|
t.Fatalf("expected require_escalated approval prompt, got %s: %q", result.Status, result.Output)
|
|
}
|
|
if result.SandboxDecision == nil || result.SandboxDecision.Action != sandbox.ActionPrompt {
|
|
t.Fatalf("sandbox decision = %#v, want prompt", result.SandboxDecision)
|
|
}
|
|
}
|
|
|
|
func TestBashRequireEscalatedBypassesNativeSandboxAfterApproval(t *testing.T) {
|
|
root := t.TempDir()
|
|
registry := NewRegistry()
|
|
registry.Register(NewBashTool(root))
|
|
engine := sandbox.NewEngine(sandbox.EngineOptions{
|
|
WorkspaceRoot: root,
|
|
Policy: sandboxedBashPolicy(),
|
|
Backend: nativeBackendStub(),
|
|
})
|
|
|
|
result := registry.RunWithOptions(context.Background(), "bash", map[string]any{
|
|
"command": helperCommand("success"),
|
|
"sandbox_permissions": string(SandboxPermissionsRequireEscalated),
|
|
}, RunOptions{
|
|
PermissionGranted: true,
|
|
Sandbox: engine,
|
|
PermissionMode: string(sandbox.PermissionModeAsk),
|
|
Autonomy: "high",
|
|
})
|
|
|
|
if result.Status != StatusOK || !strings.Contains(result.Output, "hello from bash") {
|
|
t.Fatalf("expected approved require_escalated command to run direct, got %s: %q", result.Status, result.Output)
|
|
}
|
|
if result.Meta["sandbox_permissions"] != string(SandboxPermissionsRequireEscalated) {
|
|
t.Fatalf("sandbox_permissions meta = %q", result.Meta["sandbox_permissions"])
|
|
}
|
|
if result.Meta["sandbox_wrapped"] == "true" {
|
|
t.Fatalf("require_escalated command must not be wrapped; meta=%#v", result.Meta)
|
|
}
|
|
}
|
|
|
|
func TestBashRequireEscalatedKeepsSandboxWhenDeniedReadsActive(t *testing.T) {
|
|
root := t.TempDir()
|
|
policy := sandboxedBashPolicy()
|
|
policy.DenyRead = []string{root + "/secret.txt"}
|
|
engine := sandbox.NewEngine(sandbox.EngineOptions{
|
|
WorkspaceRoot: root,
|
|
Policy: policy,
|
|
Backend: nativeBackendStub(),
|
|
})
|
|
|
|
if commandEngineForSandboxPermissions(engine, SandboxPermissionsRequireEscalated) == nil {
|
|
t.Fatal("require_escalated must preserve the sandbox when denied reads are active")
|
|
}
|
|
}
|
|
|
|
func TestBashStillPromptsWithoutActiveSandbox(t *testing.T) {
|
|
root := t.TempDir()
|
|
registry := NewRegistry()
|
|
registry.Register(NewBashTool(root))
|
|
engine := sandbox.NewEngine(sandbox.EngineOptions{
|
|
WorkspaceRoot: root,
|
|
Policy: sandboxedBashPolicy(),
|
|
Backend: sandbox.Backend{Name: sandbox.BackendUnavailable},
|
|
})
|
|
|
|
result := registry.RunWithOptions(context.Background(), "bash", map[string]any{
|
|
"command": "echo hi",
|
|
}, RunOptions{
|
|
PermissionGranted: false,
|
|
Sandbox: engine,
|
|
PermissionMode: string(sandbox.PermissionModeAsk),
|
|
Autonomy: "high",
|
|
})
|
|
|
|
if result.Status != StatusError || !strings.Contains(result.Output, "Sandbox approval required for bash") {
|
|
t.Fatalf("expected bash to be gated when sandbox inactive, got %s: %q", result.Status, result.Output)
|
|
}
|
|
}
|