Files
zero/internal/tui/command_output_test.go
Anandan ac6addaa83 Align sandbox permission blocks and backend states (#253)
* Prompt for shell network permissions

Route network-risk shell calls through the existing permission prompt instead of hard-denying them at sandbox preflight. Approved network prompts apply the current turn/session request-permissions overlay so the effective sandbox policy is widened only after user approval.

Move network command detection into the shell AST analyzer for package installers, gh/git network subcommands, and python http.server so quoted process patterns such as pkill -f "python3 -m http.server 8000" are not misclassified as network activity.

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestAnalyzeCommand|TestClassifyDoesNotFlagQuotedHttpServerPattern|TestEvaluateExemptsNetworkToolsFromDenyByDefault|TestEngineClassifiesNetworkAndDestructiveShellCommands|TestGrantRequestPermissionsNetworkOverlaysPolicyForTurn'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/agent -run 'TestRunPersistentCommandPrefixStillPromptsForNetwork|TestRunApprovedNetworkBashPromptAppliesTurnNetworkGrant'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/tools -run 'TestBash|TestRegistryRunsBashThroughSandboxEngine'

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

* Auto-allow native-sandboxed shell commands

Remove the old hidden bash auto-allow environment/config path. Ordinary shell commands now use the active native sandbox as the permission boundary by default, while network, destructive, and workspace policy checks still run first.

Policy-only and disabled sandbox backends continue to require approval for shell commands, so unsandboxed execution is not silently allowed.

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestSandboxedBash|TestShellSandboxActive|TestEvaluateExemptsNetworkToolsFromDenyByDefault|TestGrantRequestPermissionsNetworkOverlaysPolicyForTurn|TestAnalyzeCommand|TestClassifyDoesNotFlagQuotedHttpServerPattern'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/tools -run 'TestBashAuto|TestBashStillPromptsWithoutActiveSandbox|TestRegistryRunsBashThroughSandboxEngine'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/cli -run 'TestApplyConfiguredSandboxPolicy|TestApplyConfiguredAutonomyCeiling'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/agent -run 'TestRunPersistentCommandPrefixStillPromptsForNetwork|TestRunApprovedNetworkBashPromptAppliesTurnNetworkGrant'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/daemon -run 'TestScrubWorkerEnvRemovesReentrancyMarkers'

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

* Prompt for destructive shell approval

Remove the separate DenyDestructiveShell policy layer from active sandbox decisions. Destructive shell classification now produces an approval prompt in ask/auto flows instead of a sandbox hard-deny, while no-approval paths remain blocked.

Drop the removed destructive-shell policy field from sandbox policy output, backend capabilities, snapshots, and Windows golden data.

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/agent -run 'TestRunPromptsForDestructiveShellInsteadOfSandboxDeny|TestRunPersistentCommandPrefixStillPromptsForNetwork|TestRunApprovedNetworkBashPromptAppliesTurnNetworkGrant'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestEngineClassifiesNetworkAndDestructiveShellCommands|TestBackendCapabilitiesReflectDisabledPolicy'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/cli -run 'TestRunSandboxPolicy|TestEffectiveSandboxPolicy'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/zerocommands -run 'TestSandboxPolicySnapshotFromPolicyFillsEffectiveMode'

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

* Remove web-tool network policy switch

Remove the old EnforceToolNetwork policy option and the NetworkHostAllowed per-tool gate so sandbox network policy only controls sandboxed shell egress.

Keep web_search and web_fetch on their own tool permission and URL safety paths, with tests proving deny/scoped shell network policy no longer blocks in-process web tools.

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestEvaluateExemptsNetworkToolsFromShellNetworkPolicy|TestEngineAllowsNetworkSideEffectWhenShellPolicyBlocksNetwork|TestGrantRequestPermissionsNetworkOverlaysPolicyForTurn|TestScopedNetworkGateInEvaluate'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/tools -run 'TestWebSearchRunWithSandbox|TestWebFetchRunWithSandbox|TestRegistryRoutesWebFetchThroughSandboxPath'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/agent -run 'TestRunPersistentCommandPrefixStillPromptsForNetwork|TestRunApprovedNetworkBashPromptAppliesTurnNetworkGrant'

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

Tested: git diff --check

* Prompt for outside-workspace file access

Convert recoverable outside-workspace file path violations into permission prompts instead of hard-denying in ask/auto modes.

Approved prompts now grant the requested filesystem scope through the existing request_permissions overlay for the current turn or session, so the follow-up sandbox evaluation and scoped file tools use the same policy path.

Keep unsafe mode, apply_patch escapes, symlink traversal, and non-recoverable path cases hard-denied.

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestEngineAutoAllowsWorkspaceFileMutationTools|TestEngineDeniesOutOfWorkspacePaths|TestGrantRequestPermissionsReadDoesNotGrantWrite|TestGrantRequestPermissionsSessionPersists'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/agent -run 'TestRunPromptsAndAllowsOutsideWorkspaceWrite|TestRunSessionAllowsLaterOutsideWorkspaceWrite|TestRunAppliesSandboxEvenInUnsafeMode'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/tools -run 'TestRegistryAppliesSandboxBeforeToolExecution|TestRegistrySandboxGatesPathAliasKeys|TestRegistryAllowsPromptToolWithPersistentSandboxGrant'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/cli -run 'TestRunSandboxCheckJSONDeniesOutOfWorkspaceWrite'

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

Tested: git diff --check

* Fix Windows network prompt test fixture

Use a Windows curl.cmd shim and cmd.exe PATH syntax for TestRunApprovedNetworkBashPromptAppliesTurnNetworkGrant while keeping the POSIX fake curl fixture for Linux and macOS.

This keeps the test focused on network prompt approval and turn network grants instead of relying on a POSIX shell script being executable on Windows.

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/agent -run 'TestRunApprovedNetworkBashPromptAppliesTurnNetworkGrant|TestRunPersistentCommandPrefixStillPromptsForNetwork'

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestClassifyASTCatchesNetworkProgramsRegexMisses|TestClassifyFlagsPipedInstallerVariants'

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

Tested: git diff --check

* Remove sandbox autonomy ceilings

Sandbox permission decisions now match grants by tool and scope only, without MaxAutonomy or request autonomy checks. Persistent/session grants, permission approvals, workspace-write auto-allow, and native-sandbox shell auto-allow no longer route through an autonomy ceiling.

Remove the sandbox.maxAutonomy config and CLI surfaces, including grant --auto, policy output, sandbox check autonomy input, and sandbox grant snapshots.

MCP permission autonomy is intentionally unchanged because it is a separate MCP permission model.

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

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

* Fail closed without native sandbox

Remove AllowPolicyOnlyRunner from sandbox policy and snapshots. A restricted/managed command plan now errors when native sandbox support is unavailable instead of running through a policy-only direct runner.

Policy-only backend data remains as diagnostics for capability/reporting paths, but it no longer authorizes command execution when a platform sandbox is required.

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./internal/zerocommands ./internal/cli ./internal/tui

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

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

* Remove scoped shell network policy

Drop the domain-scoped shell network mode and the local scoped-egress proxy path from the active sandbox model.

Shell network policy now serializes and evaluates only deny or allow. Request-permissions network grants widen to allow without domain lists, and platform profiles consume the binary mode directly.

The Linux helper no longer accepts proxy-route flags, Windows network hashes only the binary mode, and web tool tests assert they remain separate from shell network policy.

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

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

* Align permission grant ordering and prompt text

Move persistent and session grant matching ahead of recoverable sandbox prompts while keeping explicit denies and non-recoverable path violations fail-closed.

Clean TUI permission transcript/card text so normal prompts show action, reason, scope, and readable block details instead of raw permission/risk/violation key-value metadata.

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

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

* Remove policy-only sandbox terminology

Replace the unavailable native-backend diagnostic path with explicit unavailable support/backend states, and stop presenting missing native isolation as a policy-only fallback.

Rename structured sandbox denial metadata from violation to block across permission events, stream-json, snapshots, TUI rendering, and sandbox-check output so user-facing flow matches blocked action language.

Update README, work split docs, smoke test selectors, and Windows sandbox policy golden output for the new backend/support names and block payloads.

Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./internal/agent ./internal/tools ./internal/tui ./internal/cli ./internal/doctor ./internal/zerocommands ./internal/streamjson

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

Tested: scripts/sandbox-smoke.sh

Tested: git diff --check

* Fix sandbox review follow-ups

Fail closed for unknown network modes, keep network prompts ahead of bash allow grants, preserve v1 grant-file reads, and classify unparseable commands with obvious network programs as network-risk.

Bump stream-json schema to v2 and document the block metadata field after the violation-to-block rename.

Tests: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./internal/streamjson ./internal/specialist ./internal/cli -run 'TestExec.*Stream|TestStreamJSON|TestExecProtocol|TestExecReadsStreamJSONImages|TestExecAcceptsImageOnlyStreamJSONMessage|TestExecInputFormatStreamJSON'

Tests: GOCACHE=/tmp/zero-go-cache go test ./...

Tests: scripts/sandbox-smoke.sh

Tests: git diff --check
2026-06-19 19:50:12 +05:30

247 lines
5.9 KiB
Go

package tui
import (
"strings"
"testing"
)
func TestFormatCommandOutputRendersSectionsFieldsRowsAndHints(t *testing.T) {
got := formatCommandOutput(commandOutput{
Title: "Model",
Status: commandStatusOK,
Sections: []commandSection{
{
Title: "Active",
Fields: []commandField{
{Key: "provider", Value: "openai"},
{Key: "model", Value: "gpt-5"},
},
Rows: []commandRow{
{Text: "model shell is ready"},
},
Hints: []string{"use /model list to compare options"},
},
{
Title: "Available",
Rows: []commandRow{
{Text: "gpt-5"},
{Text: "claude-sonnet-4.5"},
},
},
},
})
want := strings.Join([]string{
"Model",
"status: ok",
"Active",
" provider: openai",
" model: gpt-5",
" - model shell is ready",
" hint: use /model list to compare options",
"Available",
" - gpt-5",
" - claude-sonnet-4.5",
}, "\n")
if got != want {
t.Fatalf("unexpected command output:\nwant:\n%s\n\ngot:\n%s", want, got)
}
}
func TestFormatCommandCardRendersBoundedDashboard(t *testing.T) {
got := renderCommandCard(commandCard{
Title: "Context",
Summary: []string{"go runtime", "ask permissions", "1 tool"},
Sections: []commandCardSection{
{
Title: "Runtime",
Fields: []commandField{
{Key: "cwd", Value: `D:\repo`},
{Key: "provider", Value: "openai"},
},
},
{
Title: "Tools",
Fields: []commandField{
{Key: "registered", Value: "1"},
},
},
},
Actions: []string{"/permissions manage access", "/tools inspect catalog"},
})
want := strings.Join([]string{
"Context",
"go runtime | ask permissions | 1 tool",
"Runtime",
" cwd D:\\repo",
" provider openai",
"Tools",
" registered 1",
"actions: /permissions manage access | /tools inspect catalog",
}, "\n")
if got != want {
t.Fatalf("unexpected command card:\nwant:\n%s\n\ngot:\n%s", want, got)
}
}
func TestFormatCommandCardRedactsTokenLikeText(t *testing.T) {
got := renderCommandCard(commandCard{
Title: "Context",
Summary: []string{"provider sk-proj-summary-secret-value"},
Sections: []commandCardSection{
{
Title: "Runtime",
Fields: []commandField{
{Key: "api key", Value: "sk-ant-api03-abcdefghijklmnopqrstuvwxyz"},
},
Lines: []string{
"google token AIza1234567890abcdef",
},
Rows: []commandRow{
{Text: "shell used sk-proj-row-secret-value"},
},
},
},
Actions: []string{"retry with sk-proj-action-secret-value"},
})
for _, secret := range []string{
"sk-proj-summary-secret-value",
"sk-ant-api03-abcdefghijklmnopqrstuvwxyz",
"AIza1234567890abcdef",
"sk-proj-row-secret-value",
"sk-proj-action-secret-value",
} {
if strings.Contains(got, secret) {
t.Fatalf("expected token-like text to be redacted, got:\n%s", got)
}
}
if !strings.Contains(got, "[REDACTED]") {
t.Fatalf("expected redacted marker in command card, got:\n%s", got)
}
}
func TestFormatCommandOutputSupportsAllStatuses(t *testing.T) {
tests := []struct {
name string
status commandStatus
want string
}{
{name: "ok", status: commandStatusOK, want: "Status\nstatus: ok"},
{name: "warning", status: commandStatusWarning, want: "Status\nstatus: warning"},
{name: "blocked", status: commandStatusBlocked, want: "Status\nstatus: blocked"},
{name: "info", status: commandStatusInfo, want: "Status\nstatus: info"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := formatCommandOutput(commandOutput{
Title: "Status",
Status: tt.status,
})
if got != tt.want {
t.Fatalf("expected %q, got %q", tt.want, got)
}
})
}
}
func TestFormatCommandOutputRendersPlainLinesAndCommandBullets(t *testing.T) {
got := formatCommandOutput(commandOutput{
Title: "Commands",
Status: commandStatusInfo,
Sections: []commandSection{
{
Title: "Model",
Lines: []string{
"/model [list|id] - Show the active model.",
commandBullet("gpt-5"),
},
},
},
})
want := strings.Join([]string{
"Commands",
"status: info",
"Model",
" /model [list|id] - Show the active model.",
" - gpt-5",
}, "\n")
if got != want {
t.Fatalf("unexpected command lines:\nwant:\n%s\n\ngot:\n%s", want, got)
}
}
func TestFormatCommandOutputRedactsTokenLikeText(t *testing.T) {
got := formatCommandOutput(commandOutput{
Title: "Permissions",
Status: commandStatusOK,
Sections: []commandSection{{
Title: "Sandbox grants",
Lines: []string{
"bash [allow] - sk-proj-sensitive-token-value approved shell",
"anthropic: sk-ant-api03-abcdefghijklmnopqrstuvwxyz",
"google: AIza1234567890abcdef",
},
}},
})
for _, secret := range []string{
"sk-proj-sensitive-token-value",
"sk-ant-api03-abcdefghijklmnopqrstuvwxyz",
"AIza1234567890abcdef",
} {
if strings.Contains(got, secret) {
t.Fatalf("expected token-like text to be redacted, got:\n%s", got)
}
}
if !strings.Contains(got, "[REDACTED] approved shell") {
t.Fatalf("expected token-like text to be redacted, got:\n%s", got)
}
}
func TestFormatCommandOutputCompactsWhitespaceAndSkipsEmptyItems(t *testing.T) {
got := formatCommandOutput(commandOutput{
Title: " Doctor ",
Status: commandStatusWarning,
Sections: []commandSection{
{
Title: " ",
Fields: []commandField{
{Key: " ", Value: "skip me"},
{Key: "sandbox", Value: " danger-full-access\r\n(no prompt) "},
},
Rows: []commandRow{
{Text: " "},
{Text: "checks complete\nneeds review"},
},
Hints: []string{
" use /doctor for details\r\nsoon ",
},
},
},
Hints: []string{
" top-level hint\nis compacted ",
},
})
want := strings.Join([]string{
"Doctor",
"status: warning",
" sandbox: danger-full-access (no prompt)",
" - checks complete needs review",
" hint: use /doctor for details soon",
"hint: top-level hint is compacted",
}, "\n")
if got != want {
t.Fatalf("unexpected compact command output:\nwant:\n%s\n\ngot:\n%s", want, got)
}
}