Commit Graph
2 Commits
Author SHA1 Message Date
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
68facda077 Image (multimodal) input: exec --image, stream-json images, TUI /image across all 3 providers (#143)
* zeroruntime: add ImageBlock type and NormalizeImageMediaType

* zeroruntime: add optional Images field to Message

* zeroruntime: add SeedMessagesWithImages, delegate SeedMessages with nil

* Emit Anthropic image source blocks for user image turns

* Add inlineData part type to Gemini request schema

* Serialize user image attachments as Gemini inlineData parts

* test(openai): pin text-only chat request serialization before content refactor

* feat(openai): allow structured multimodal content parts in chat request

* feat(openai): map image attachments to image_url content parts

* test(openai): assert image content parts serialize over the request body

* modelregistry: add SupportsVision capability gate

* Add shared image-file loader package

* cli: parse repeatable exec --image flag into imagePaths

* cli: resolve exec image attachments via shared loader with sniff and size cap

* agent: thread image attachments through Options into the seeded user turn

* cli: resolve and vision-gate exec image attachments into the agent run

* cli: document exec --image flag in help output

* streamjson: add InputImage type and InputEvent.Images field

* streamjson: whitelist images field on message events only

* streamjson: allow empty content on message events that carry images

* streamjson: add ResolveImages to decode, normalize and cap input images

* streamjson: cover image parse-and-resolve round-trip and text-only invariance

* Add /image command definition to TUI

* Add synchronous vision check for TUI model

* Implement /image attach, clear, and non-vision refusal in TUI

* Render pending image chips above the input in both skins

* Thread pending images into the agent turn and clear on submit

* Wire stream-json images through to the agent run

Stream-json message images were parsed and validated but never reached
agent.Options.Images: streamjson.ResolveImages had no production caller,
so images sent over stream-json input were silently dropped.

resolveExecPrompt now also returns the resolved stream-json images
(nil for text input and for image-free stream-json input). The exec
runner merges them with --image attachments before the vision gate, so
both input sources flow through the same drop+warn and the same
agent.Options.Images wiring.

Adds an exec-level end-to-end test that feeds a base64 image on a
stream-json message event via stdin on a vision model and asserts the
image reaches the agent run (the provider request's user turn).

* Re-check TUI vision support at submit, not only at /image attach

A user could attach an image on a vision model and then switch to a
non-vision model via /model before submitting; the images were threaded
unconditionally into the agent turn, reaching a model that rejects them.

The commandPrompt submit path now re-runs the vision check against the
current effective model. When the active model can't accept images, the
pending images are dropped (not threaded) and an inline notice mirroring
the headless drop+warn wording is appended. Pending image state is still
cleared after submit either way.

Adds a test that attaches on a vision model, switches the model id to a
non-vision one, submits, and asserts the run received no images, an
inline notice was appended, and pending state was cleared.

* Bound memory with an os.Stat size pre-check in imageinput.LoadFile

LoadFile read the whole file into memory before checking the size cap,
so a multi-gigabyte file allocated a huge buffer only to be discarded.

Add an os.Stat size pre-check before os.ReadFile: a file whose size
exceeds MaxImageBytes is rejected with the same 10 MiB-limit error
before any read; a missing file surfaces the same not-found error. The
existing post-read len(data) check is kept as a TOCTOU backstop in case
the file grows between Stat and ReadFile.

* OpenAI: only the user role emits image content-parts

The Anthropic and Gemini providers only attach images in their user
branches, but OpenAI funnels every role through a single mapMessage,
which would emit image content-parts for any role carrying Images.

Guard the content-parts path to the user role: a non-user message that
happens to carry Images keeps the plain string/nil content path. Pins
the invariant with TestMapMessageNonUserRolesNeverCarryImages, asserting
assistant/tool/system messages with Images set still serialize plain
content (no content-parts array).

* Deep-copy image buffers so attachments are never aliased

Add a shared CloneImageBlocks helper in zeroruntime that deep-copies a
slice of ImageBlock including each Data byte slice (returning nil for
nil/empty input). Use it in SeedMessagesWithImages so a seeded user turn
owns an independent copy of the caller's image bytes, and in the agent
loop's copyMessages so image bytes are deep-copied alongside ToolCalls
instead of staying aliased across history/request/result copies.

Tests assert that mutating the caller's original Data bytes after seeding
leaves the seeded message unchanged, and that copyMessages produces
independent image bytes.

* Accept image-only stream-json turns and bound base64 image size

Resolve stream-json images before the prompt in the exec input path and
tolerate an empty prompt when images are present, so an image-only
message event (empty content + one image) proceeds with prompt="" and the
image reaches the agent run instead of being rejected. A turn carrying
neither text nor images still errors.

In ResolveImages, reject an oversized payload from its ENCODED length
(DecodedLen) BEFORE the base64 decode so a huge blob never allocates a
decode buffer just to be capped afterward; the post-decode length check
stays as a backstop.

Tests cover the image-only exec turn reaching the agent and the
pre-decode oversize rejection (asserting the size gate fires before any
decode).

* Bound the image read in imageinput.LoadFile with a LimitReader

Replace os.ReadFile with os.Open + io.ReadAll over an
io.LimitReader(f, MaxImageBytes+1). The os.Stat pre-check is only a
fast-path hint (a non-regular file reports a misleading size and the file
can grow between stat and read); the LimitReader is the real bound, so at
most one byte past the cap is ever buffered and an oversized or unbounded
source can never allocate a multi-gigabyte buffer. The file is closed via
defer and the existing size message is reused.

* Fix RenderChat frame-height off-by-one with image chips

cmdRegion emits TWO rows when ImageChips is set (a chip row above the
input line), but the body/overlay height accounting assumed a one-row
command region, overflowing the fixed frame by one line. Compute the
fixed-row count from the actual command-region height (1 or 2 rows) so
the composed frame stays exactly Height rows in both cases.

A RenderChat test asserts the rendered frame line-count equals the
requested height both with and without ImageChips set.

* Reject an empty inline --image= value

Route the inline --image= form through requiredInlineFlagValue, the same
empty-rejection the other inline flags use, so --image= errors with
"--image requires a value" instead of appending an empty image path. A
parse test covers the empty inline value.

* imageinput: reject non-regular files (FIFO/dir) before opening

---------

Co-authored-by: KRATOS <[email protected]>
Co-authored-by: Gnanam <[email protected]>
2026-06-08 18:49:09 +05:30