mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 23:40:28 -10:00
fix/safe-command-ast-interactive
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f78b36c770 |
fix(tools): preserve SysProcAttr during PTY fallback (#618)
When falling back from a failed PTY start to a pipe execution, the command attributes are reset. Previously, resetExecCommandForPipeFallback set command.SysProcAttr = nil, which stripped custom settings like Windows CmdLine quoting. Update the fallback logic to restore the original SysProcAttr instance, and add TestStartExecProcessPTYFallbackPreservesSysProcAttr to cover. |
||
|
|
f10ed0c893 |
fix(tools): Windows cmd.exe quoting guidance and clipboard escaping fix (#468)
* merge: rebase onto main, combine Windows shell guidance with #476's MSYS text #476 merged into main and rewrote the same "Shell syntax: Windows cmd.exe..." guidance string in system_prompt.go and shellGuidanceForGOOS in shell_runtime.go that this branch also edits (this PR's fix for the broken single-quoted quoting examples). Combined both into one coherent string in each spot: the double-quote/single-quote metachar rule with correct examples, plus #476's MSYS coreutils warning and require_escalated mention. Merged the corresponding test assertions in loop_test.go the same way. internal/imageinput/clipboard.go, internal/tools/bash_tool_test.go, and internal/tools/exec_command_test.go applied cleanly with no overlap. * test(tools): add host shell syntax description test for exec_command * chore: retrigger CI after infra flake * style(tools): gofmt exec_command_test.go --------- Co-authored-by: Vasanthdev2004 <[email protected]> |
||
|
|
81aad58d97 |
fix(tools): Block MSYS coreutils under Windows sandbox (#476)
* fix(tools): block MSYS coreutils under Windows sandbox Git for Windows usr\bin coreutils (head, grep, cat, ...) are MSYS PE binaries that fail with ACCESS_DENIED and cygheap/signal-pipe errors when Zero runs them in the write-restricted Windows sandbox. Detect and block these commands before execution, surface MSYS runtime failures from stderr, steer agents toward native Zero tools or require_escalated, and exclude MSYS-prone names from known-safe command tails. Refs #458 * fix(tools): anchor the last MSYS failure heuristic to MSYS markers CodeRabbit review feedback: the win32-error-5 plus terminating check had no MSYS-specific anchor, so unrelated access-denied failures could be mislabeled as windows_msys_sandbox. Require an MSYS marker like the neighboring checks do. Co-Authored-By: Claude Fable 5 <[email protected]> * fix(tools): let escalation bypass the MSYS guard and make detection quote-aware Addresses jatmn's review on PR #476: - bash and exec_command called detectShellCommandIssue before resolving sandbox_permissions into a command engine, so a require_escalated retry of a blocked command was still hard-blocked by the same preflight check. Resolve the command engine first and only bypass the windows_msys_sandbox issue kind when that resolves to true unsandboxed execution; other issue kinds (real cmd.exe syntax problems) still block regardless of escalation. - Unify all MSYS-prone command name checks (MsysProneCommandName, the preflight scan, and transitively command_prefix.go's known-safe guard) behind one windowsMsysProneNames set. This fixes expr (in the name set but missing from the old regex alternations, so never proactively flagged) and ls (hit the older windows_shell_syntax branch first, so it got different metadata than every other MSYS-prone name). - Make the preflight scan quote-aware: split the command into cmd.exe- operator-separated segments respecting double-quote grouping, and only check the first word of each segment instead of scanning raw text anywhere. Fixes false positives on quoted text like commit messages or PR comment bodies that merely mention a coreutil name. * test(tools): decouple MSYS-guard bypass tests from real command output TestBashToolRequireEscalatedMsysGuard and TestExecCommandRequireEscalatedBypassesMsysGuardAfterApproval asserted shell_issue == "" to prove the require_escalated bypass worked. Once bypassed, "cat somefile.txt" actually runs unsandboxed, and its real, PATH-dependent output could trip the unrelated post-execution detectShellOutputIssue heuristic, failing the assertion for reasons unrelated to the preflight guard under test. Assert on the exit_code "-1" sentinel (set only by shellIssueBlockResult) instead, which directly reflects whether the preflight guard blocked the command. * fix(tools): anchor MSYS path detection to command position, fix prefix approval gap Address jatmn's latest review on PR #476: - shell_runtime.go: the explicit MSYS binary-path check (usr\bin\, mingw64\bin\, msys-2.0.dll, cygwin1.dll) ran against the raw command string, so a quoted mention (e.g. a commit message describing a usr\bin\head.exe failure) was still hard-blocked. It now runs against the first word of each operator-separated segment, same as the coreutil-name check. windowsCommandSegments also stopped treating ; as a command separator (cmd.exe does not split on ;, unlike bash) and now respects cmd.exe's ^ escape character, so escaped operators like echo ^| head are no longer misread as a real head invocation. - command_prefix.go: proposedCommandPrefix could offer to approve a single segment (e.g. "ps aux" in "ps aux | head -5") without checking that every other segment was independently safe. Since approving a prefix escalates the whole command to bypass the sandbox, an MSYS-prone or otherwise unsafe segment could ride along uncovered. It now only proposes a segment as the prefix when every other segment in the command is known-safe on its own. Added regression tests for quoted MSYS path mentions, caret-escaped operators, semicolon text, and the prefix-approval gap. Co-Authored-By: Claude Sonnet 5 <[email protected]> * fix(tools): treat redirection as a command boundary, stop using command text as output evidence Addresses jatmn's latest review on PR #476: - firstCommandWord only split on whitespace, so cmd.exe redirection attached directly to a command name (head>out.txt, cat<in.txt, grep>matches.txt) was read as one word and never matched against the MSYS-prone name set. It now also stops at < and >. - detectShellOutputIssue scanned command+output together, so a command whose own argument text merely quoted an MSYS failure string (e.g. a gh pr comment --body describing the bug) could be mislabeled after running even though the real stdout/stderr was unrelated. Removed the command parameter entirely from detectShellOutputIssue and both call sites (bash.go, exec_command.go), so the command line can never be used as post-execution evidence again. * fix(tools): remove unused truncateHeadTail wrapper CodeRabbit nitpick: truncateHeadTailWithTotal is called directly everywhere, so this wrapper is dead code that trips golangci-lint's unused check and fails CI. --------- Co-authored-by: Claude Fable 5 <[email protected]> |
||
|
|
c0254e3266 |
fix(tui): context-fill gauge for custom/local Ollama models + related fixes (#356)
* fix(tui): show the context-fill gauge for custom/local Ollama models
The colored context gauge (used/window · N%) only renders when Zero knows
a model's context window. It resolves that two ways: a curated remote
catalog (how Grok/hosted models are known), or live /v1/models discovery —
but parseModelsResponse only ever extracts id/description from that
response; OpenAI-compatible model listings don't carry context-window
metadata at all. A custom/local Ollama model tag (e.g. a locally-pulled
":cloud"-tagged model) has no curated-catalog entry to borrow one from
either, so it always resolved to an unknown window and the gauge silently
fell back to a bare token count with no color grading.
Ollama does expose this, just via a different, Ollama-native endpoint
(/api/show, not /v1/models) whose model_info map reports it under an
architecture-prefixed key like "qwen2.context_length". Added
DiscoverOllamaContextWindow (providermodeldiscovery) to probe it, wired
through an async command scoped to the local "ollama" catalog ID only
(not "ollama-cloud", a different hosted service this endpoint isn't
assumed to exist on) — fired from Init() for the provider active at
launch, and from switchProviderModel for a provider switched to
mid-session (which previously warmed no discovery at all for the new
provider, a separate gap this also fixes).
New tests: DiscoverOllamaContextWindow's endpoint construction and
model_info parsing (including a mixed-type model_info and a missing
*.context_length key), the discovery command's scoping to local Ollama
only, the Update() handler populating modelContextWindow's fallback, and
switchProviderModel warming discovery for the newly active provider.
Local only — not pushed. I can't run a live Ollama daemon in this
sandbox, so the actual /api/show request/response shape (verified against
Ollama's documented API, not a live instance) needs confirming against a
real daemon before this goes out.
* fix(tui): stop showing the token count twice next to the context gauge
contextWindowSegment (the gauge) and usageStatusSegment (the plain "N tok"
figure) both read the exact same value (latestUsageTokens) — the code
already suppressed the plain figure when the sidebar shows it ("the token
figure shows on both sides"), but never extended that same dedup to the
gauge. So as soon as a model's context window becomes known (e.g. after
the previous commit's Ollama fix), the footer showed the identical number
twice: once inside the gauge, once again right next to it.
Fixed by falling back to cost-only whenever the gauge is shown, same as
the sidebar-active branch already does.
* fix: check collect()'s deadline even while output keeps draining
The wait deadline in execSession.collect() was only checked in the branch
reached when drainString() returns empty. A background process producing
output continuously enough to keep the buffer perpetually non-empty could
theoretically starve that check forever, turning the caller's requested
timeout into an unbounded block regardless of yield_time_ms.
Investigated as part of a cross-provider "stuck" report (multiple sessions
across different models/providers all hanging on the same prompt, each
with a background terminal running and no forward progress). Could not
empirically reproduce a hang via a synthetic continuous-writer test under
Go's real scheduler — the reader won the race often enough in practice
that this doesn't appear to be the actual cause of those reports. Keeping
the fix regardless since it closes a real, if hard-to-trigger, gap: the
deadline is now checked in both branches, so continuous output can never
prevent collect() from returning within its requested window.
|
||
|
|
2a8802e39c |
fix: cap unbounded exec_command output buffer to prevent OOM kill (#353)
* fix: cap unbounded exec_command output buffer to prevent OOM kill
execOutputBuffer.data accumulated every byte a backgrounded exec_command
session wrote, with no size cap, unlike buffer.recent which is correctly
bounded to recentExecOutputBytes for the /ps preview. drainString() only
clears data when the agent actually polls that session again — so a
long-lived background session nobody polls (e.g. a dev server left running
after the run that started it was cancelled) grows this buffer forever as
long as the process keeps writing, with no ceiling.
Traced this directly to a real crash: a session that had been running for
~5.5 hours (an earlier turn invoking swarm agents got cancelled mid-flight,
per that session's own event log) accumulated to ~42.7GB of compressed
memory, at which point macOS's memorystatus killed the process outright
("killing zero due to low-swap").
Fixed by capping data at maxExecOutputBufferBytes (2MiB), trimming from the
front like recent already does, and surfacing a one-time truncation notice
on the next drain so data loss is visible rather than silent.
* fix: pre-push review findings on the exec_command buffer-cap fix
Ran an adversarial review before opening the PR. It found the truncation
notice added alongside the buffer cap didn't actually work: prefixing
execOutputTruncatedMessage into drainString()'s text meant the notice
always sat ~2MiB before the end of the combined output, but the tool's
own downstream truncateExecOutput() keeps at most a 400KB tail (even at
the maximum allowed max_output_tokens) — so the notice was reliably
swallowed, or chopped into an unreadable fragment at smaller budgets.
Verified with direct reproductions against the real functions.
Fixed by moving the signal out of the text stream entirely:
- execOutputBuffer gains consumeTruncated() (read+reset, called once per
collect()) and peekTruncated() (read-only, for status views) instead of
injecting a marker into the drained bytes.
- collect() now returns (string, bool); both call sites thread the bool
through to execToolResult, which sets meta["output_buffer_truncated"]
and appends the notice to the response body *after* truncateExecOutput
runs, so it can never be re-truncated or land in a discarded region.
- Also flagged: a session torn down without ever being polled again after
truncation loses the signal entirely, since nothing drains/logs it on
teardown. Full audit-trail logging felt like overkill for a soft
transparency signal on top of a hard memory-safety fix, but exposed
peekTruncated() on ExecSessionSnapshot and surfaced it in the /ps row
(background_terminals.go) so it's visible any time a still-running
session is checked — covers the common case (the review's own scenario:
a long-lived dev server nobody's polling but that's still alive) even
though a session reaped before ever being looked at is still a gap.
New tests: reworked TestExecOutputBufferCapsUndrainedData for the
consume/peek API, and TestExecToolResultSurfacesBufferTruncationOutsideByteBudget
reproducing the review's exact failure case (max_output_tokens=1) and
proving the notice now survives it.
|
||
|
|
d29ab98a79 |
Improve sandbox permission handling (#323)
Retry shell commands outside the sandbox when sandboxed output only reflects the sandbox namespace, and keep network retries sandboxed when a network grant is enough. Show user-facing permission reasons for escalated shell commands instead of exposing internal sandbox policy text. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/agent ./internal/tui -run 'TestRunUsesEscalatedJustificationForPermissionPrompt|TestRunUsesUserFacingEscalatedFallbackForPermissionPrompt|TestPermissionPromptMapsEscalatedSandboxReason|TestRunRetriesShellUnsandboxedAfterSandboxNamespaceLimitedOutput' Tested: env GOCACHE=/tmp/zero-go-cache go test ./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 ./... |
||
|
|
6be50cd3a7 |
fix(sandbox): retry approved network shell commands (#301)
* fix(sandbox): retry approved network shell commands * test(sandbox): fix portable smoke expectations * fix(sandbox): restore Windows offline egress block |
||
|
|
f7ee189ba5 |
fix: resolve 15 findings from the 2026-06-20 deep audit (#281)
* docs(audit): 2026-06-20 deep adversarial audit report
Audit-only deliverable (no source changed). Fresh origin/main (
|
||
|
|
bfbdbb10f6 |
test(tools): make write_stdin "\x03" the trigger in the interrupt test (#273)
The de-flake infra (resilientTempDir + deterministic <-session.done wait) is already on main, but the test still called session.terminate() directly before write_stdin "\x03" — so the session was already reaped when the code under test (exec_command.go's Ctrl-C branch) ran, and the assertion no longer guarded it: the test would pass even if that branch were deleted (caught in review). Capture the session handle before the interrupt (write_stdin removes a finished session from the manager), then make write_stdin "\x03" the operation that terminates, then wait on session.done. Keeps the de-flake; restores the coverage. Verified by mutation: removing session.terminate() from the Ctrl-C branch makes this test fail. Runs 15x locally without flaking. |
||
|
|
f5038151c8 |
Add managed background terminals (#270)
* Add managed background terminal sessions * Stop background terminals on TUI exit * Cover approved local network sandbox mode * Polish background terminal rendering * Tighten exec session cleanup guidance * Address background terminal review feedback * Kill exec session trees on Windows * Trigger CI for Windows cleanup |
||
|
|
c572791edf |
feat: clipboard image paste + provider 400 handler for image rejection (#268)
* fix(agent): abort on image-rejection 400s instead of retrying in a loop
* feat(imageinput): add ReadClipboardImage for OS clipboard image extraction
* feat(tui): clipboard image paste — detect and attach screenshots from OS clipboard
* fix(imageinput): use temp file for Windows clipboard image (PowerShell stdout can't emit raw binary)
* test: handle both image-present and image-absent clipboard states
* fix(vision): add kimi-k2 and qwen-vl to vision-capable name heuristic
* fix(tui): render image errors as red error rows instead of grey system notes
* feat(vision): check models.dev InputModalities for vision support instead of only name heuristics
The vision gate now checks three sources:
1. Curated model registry (catalog authority + name heuristic)
2. Discovered model list from models.dev (InputModalities contains 'image')
3. Name heuristic fallback for unknown models
This means custom/ollama/cloud models (kimi-k2, qwen-vl, etc.) are correctly
detected as vision-capable when models.dev reports them with image input
modality — no more manual name matching needed.
* chore: gofmt clipboard_test.go
* fix(imageinput): remove shell injection in the Linux clipboard reader
The Linux clipboard reader built shell commands by concatenating a
clipboard-derived MIME type into `sh -c`:
exec.Command("sh", "-c", "wl-paste --type "+t+" 2>/dev/null")
exec.Command("sh", "-c", "xclip ... -t "+t+" -o 2>/dev/null")
`t` comes from `wl-paste --list-types` / `xclip TARGETS` and is only filtered by
`HasPrefix(t, "image/")`, so a hostile clipboard offerer could register a target
like `image/png; rm -rf ~` — it passes the prefix check and then executes
through the shell. A real (if low-likelihood) local code-exec vector.
Drop the shell entirely: every helper now runs via exec.Command(prog, args...)
with the MIME type passed as a discrete argument (matching the existing no-shell
pattern in pdf.go). Stderr is discarded by not assigning it. Factored into
runClipboardStdout + imageMIMETypes helpers.
Test: imageMIMETypes filters to image/* lines and returns even an
injection-shaped type verbatim (safe now that it never reaches a shell).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* test(tools): de-flake TestWriteStdinInterruptTerminatesSession
The test had two Windows-CI flakes that reddened unrelated PRs:
1. It relied on write_stdin's 1000ms yield window being long enough for the
async SIGKILL + reap to land before asserting the session was no longer
running. On a loaded runner the reap exceeded that window, so write_stdin
reported "still running" and the assertion failed. Fix: after terminating,
wait on session.done deterministically (30s safety timeout that fails loudly
if the kill genuinely hangs); the common case returns the instant the
process exits.
2. The SIGKILL'd child had the test's t.TempDir() as its cwd, and Windows may
not release the directory handle the instant the process is reaped — so
t.TempDir()'s immediate RemoveAll on cleanup failed with "being used by
another process". Fix: resilientTempDir retries RemoveAll for up to 5s
(best-effort; a leaked temp dir never fails the test).
Verified 20x locally; full internal/tools suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
|
||
|
|
cf54e2cb0e |
Add persistent exec command sessions (#267)
* Add persistent exec command sessions Introduce exec_command and write_stdin tools backed by a shared session manager so long-running shell commands can remain alive and be polled or interrupted across tool calls. Wire exec_command through the existing shell permission, network prompt, command-prefix approval, sandbox grant scope, specialist manifest, and prompt/documentation paths. Keep web_fetch limited to public remote HTTP(S) URLs so localhost/private URLs go through shell commands where sandbox network permission applies. Tested: git diff --cached --check * Fix exec session lifecycle review issues |