mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 05:01:28 -10:00
fix/opencode-session-header
61
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
79fa1422ef |
fix(providers): register Atomic Chat as local OpenAI-compatible runtime (#550)
* fix(providers): register Atomic Chat as local OpenAI-compatible runtime Point atomic-chat at the default local API (http://127.0.0.1:1337/v1), mark it Local with no API key, and auto-detect it alongside Ollama and LM Studio. Keep opencode-go-anthropic-compatible in the provider catalog and curated model list after rebasing onto latest main. Co-authored-by: Cursor <[email protected]> * docs(zh): sync Atomic Chat local runtime into README_ZH The English README lists Atomic Chat in the provider list and tells local-model users to run its desktop app on http://127.0.0.1:1337/v1, but the Chinese README still mentioned only Ollama and LM Studio. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(providers): give the local Atomic Chat runtime its own catalog ID Repurposing the existing atomic-chat ID from the hosted, key-gated preset to a keyless local one silently downgraded credential checks for profiles already saved against the remote endpoint: MissingCredentialEnv resolves RequiresAuth from the catalog descriptor, so those profiles were reported as ready and only failed later with an unauthenticated call to api.atomic.chat. Keep the hosted atomic-chat descriptor as it was and register the local runtime under a separate atomic-chat-local ID, mirroring the existing ollama-cloud/ollama split. Stored profiles keep their identity and no migration is needed. Co-Authored-By: Claude Opus 4.8 <[email protected]> * test(onboarding): assert the full Atomic Chat local endpoint and default model Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(models): drop the placeholder curated list for atomic-chat-local Atomic Chat's model catalog is a Hugging Face scrape refreshed every 12h plus arbitrary owner/repo lookups, so the served model ID is whichever model the user loaded and no hardcoded list stays accurate. The single local-model entry short-circuited Models() without adding anything: the descriptor is openai-compatible, so modelMatchesProvider never matches a registry entry and the fallback already yields the descriptor's DefaultModel. Output is unchanged; the difference is that local-model is no longer presented as a curated contract. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(onboarding): pin the probed model in the local-runtime adopt command probeLocalRuntime already reads the runtime's /v1/models, but SetupAction dropped the result and emitted 'zero providers add <id> --set-active' with no --model. The add path then persisted the catalog DefaultModel, which for a local runtime is the placeholder 'local-model', and the first completion failed with an unknown-model response. SetupAction now pins the probed id via SetupCommandWithModel. The catalog default wins when the server actually serves it, so an existing Ollama setup does not switch models; otherwise the first advertised id is used. An empty probe result leaves the command on the catalog default rather than inventing an id. Covers a server whose returned id is not 'local-model'. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(onboarding): shell-safe adopt command + reject atomic-chat-local placeholder model commandArg now POSIX single-quotes rendered args instead of strconv.Quote, so a model id from an untrusted /v1/models response cannot expand when the adopt command is pasted into a shell. providerProfileForAdd now errors when atomic-chat-local is added without --model instead of persisting the local-model placeholder that fails the first completion. Adds regression tests for command substitution, backticks, and the require-model guard. * fix(onboarding): finish Atomic Chat setup across supported shells Rebase on current main and keep the hosted Atomic Chat preset intact. Only emit setup arguments that are portable across POSIX, cmd.exe and PowerShell; show interactive guidance for other model IDs. Reject the Atomic Chat placeholder before both CLI and TUI persistence, and stop advertising adoption when discovery finds no usable model. Validation: full Go test suite, focused race suite, vet, release build, smoke, formatting, advisory lint and vulncheck pass with isolated config and inherited Anthropic overrides removed. Onboarding, CLI and TUI test packages cross-compile for Windows and Linux. Regression tests fail on the rebased pre-fix code and pass with these changes. Refs Gitlawb/zero#550 and Gitlawb/zero#592. * fix(onboarding): preserve option-looking model IDs in adopt commands Use the existing --model=value grammar when a discovered model begins with a hyphen. Validate the model separately from the fixed separator so shell safety remains unchanged and '=' is not added to the allowlist. Exercise generated detection actions through shell tokenization, the actual add parser, and profile construction for Atomic Chat, LM Studio and Ollama. Cover leading hyphens, flag-looking IDs, ordinary IDs, spaces, and unsafe inline values. Before the fix, the new regression failed with: generated adoption command rejected by add parser: --model requires a value Validation: full Go suite, focused race tests, formatting, vet, release build, smoke, lint-static and vulncheck passed. Affected test packages cross-compiled for Windows and Linux. Refs Gitlawb/zero#550. * fix(onboarding): select eligible chat models and preserve Ollama defaults P2: apply the existing known-noncoding exclusion before default preference or first-model fallback. When a nonempty list has no eligible model, offer commandless guidance instead of persisting the rejected/default model. P3: recognize an Ollama untagged default's :latest spelling, preserving the advertised ID. Prefer exact matches and leave other tags/providers alone. Add selector tests plus a mock-server integration test through discovery, the generated action, the real add parser, profile persistence, a fresh process, and the chat completion request. Before this fix, that test failed: fresh process sent model "text-embedding-local", want eligible model "-loaded chat model" The Ollama alias case also failed before the fix: AdoptModel() = "qwen3:8b", want "llama3.1:latest" Explicit model choices, shell checks, hosted/local provider identities and Atomic placeholder guards retain their existing behavior. Validation: full Go suite, focused race tests, formatting, vet, release build, smoke, lint-static and vulncheck passed. Affected test packages cross-compiled for Windows and Linux. Integration evidence uses a mock server; native Windows shell and actual runtime behavior are not claimed. Refs Gitlawb/zero#550 and Gitlawb/zero#592. --------- Co-authored-by: Cursor <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]> Co-authored-by: alex <[email protected]> |
||
|
|
1e4db7c664 |
fix: let the configured forward proxy through the loopback guard (#1025)
* fix(providerhealth,tools): let the configured forward proxy through the loopback guard With HTTPS_PROXY pointing at a local forward proxy, the transport dials the proxy first and tunnels the request through it. The SSRF-guarded dialer in providerhealth refused that dial as a loopback address, so `zero doctor --connectivity` failed with "proxyconnect tcp: provider connectivity URL is unsafe: loopback hosts are blocked" for a public provider that had already been validated, and web_fetch had no proxy support at all (#569). Each dialer is now handed its transport's own Proxy function and lets a dial through unpinned only when the address is exactly the proxy endpoint that function names for the request scheme: same host, same port, with a missing proxy port taken as the scheme default. A sibling loopback port is still refused. The shared check lives in internal/proxydial since providerhealth and tools otherwise share nothing. web_fetch honours http.ProxyFromEnvironment again. The guarded fetch had set Proxy to nil as part of its SSRF hardening; the target is still validated before the request, and what a forward proxy removes is only the dial-time rebind pin, which is inherent to using any forward proxy. The pre-permission URL validation stays in place. Started from #594 by baoyu0. This version keeps the pre-permission check, matches the proxy port exactly instead of any port when the proxy URL has none, and injects the transport's Proxy function instead of reading the environment a second time. Closes #569 Co-authored-by: baoyu0 <[email protected]> * fix(proxydial): have the dialer ask its transport for the proxy at dial time Copying transport.Proxy into the dialer at construction left the wiring between the two unpinned: a dialer handed nil would pass every unit test while a proxied request still died as loopback. The dialer now consults the transport it is installed on, and each package drives a real request through a loopback listener acting as the proxy and checks that the CONNECT arrives. The proxy is substituted on the transport rather than through HTTPS_PROXY, since http.ProxyFromEnvironment reads the environment once per process and an env-based test would depend on test order. * fix(proxydial): bind the proxy exemption to the route the transport chose Asking the Proxy function a question of our own invention answered for a request the transport never made. With NO_PROXY excluding a host, the real request to that host goes direct while a synthetic probe still comes back proxied, so a direct dial whose address happened to equal the configured proxy endpoint skipped the guard and reached whatever the name resolved to at connect time. A probe on the previous head dials rebind.example:80 with zero safety lookups; the guard now runs and refuses its private answer. Host-dependent proxy callbacks make the mismatch general, and no spelling of the probe host closes it, because the two calls are about different requests. The decision is now recorded where it is made. proxydial.Transport wraps the transport, asks its Proxy function about the actual request, and puts the answer in that request's context; the transport derives the dial context from the request context, so the dialer reads back the route this very connection is being opened for. Per-request, so concurrent requests cannot overwrite each other, and the context key is unexported so nothing outside the package can plant one. Address matching is gone with it. A transport opens one connection dial, and when a proxy is selected that dial goes to the proxy, so the recorded route already identifies the dial. Comparing the address on top could only disagree with the transport, which is what a Unicode proxy hostname did: Go dials xn--bro-hoa.example while the URL still spells it as configured, and the proxy was refused as an ordinary private address. Both spellings are now one endpoint with no comparison to get wrong. Documents where destination enforcement ends: with a proxy configured, the target hostname is sent to the proxy and the proxy resolves it, so the local pre-request checks no longer bind the address that is reached. A forward proxy already sees and can rewrite every request through it, so that trust comes with configuring one; the alternative is refusing to use proxies at all, which is the outage in #569. --------- Co-authored-by: baoyu0 <[email protected]> |
||
|
|
1fde941803 |
perf: reduce startup and turn overhead (#932)
* perf(tui): disable duplicate composer cursor blink * perf(tui): stop composer wakeups after idle * refactor(tools): publish immutable registry snapshots * perf(startup): defer optional tool discovery * perf(provider): reuse compatible response sessions * feat(provider): add native patch tool input * fix(provider): recognize saved chat sessions * perf(tui): render only on screen changes * fix(runtime): address performance review findings * fix(usage): expose cache efficiency details * perf(agent): reduce redundant bounded-task turns * fix: harden runtime review edge cases * fix: address runtime review findings * build: restore official Bubble Tea dependency * fix(cli): redact MCP registration errors * fix: harden turn session regressions * fix: keep run tools and patch roots consistent * fix: bind freeform patch writes to scoped roots * test: keep patch fixtures out of worktree * fix(tools): reject mixed-root freeform patches |
||
|
|
01c3498205 |
feat(update): recognise Homebrew installs and add mise, go install docs (#910)
* feat(update): recognise Homebrew installs and add mise, go install docs
* fix(update): match the keg shape, correct the docs, cover Apply and cross-target
* test(update): build the release fixture from the running platform
* fix(update): require a Homebrew receipt before refusing self-update
Review found the keg check was shape-only, and it was right: a directory
tree a user built themselves can have exactly a keg's shape. Classifying
one as Homebrew refuses a `zero upgrade` that would have worked
perfectly, on a machine Homebrew has never touched.
Two changes, cheap one first. The shape rule now requires the full
<prefix>/Cellar/<formula>/<version>/bin/<binary> depth rather than one
segment fewer, which already excludes the exact path the review raised.
Then, only for a path that still looks like a keg, one Stat for
INSTALL_RECEIPT.json — the file Homebrew writes into every keg and
nothing else does.
The receipt is what the old comment argued against, on the grounds that
it would put filesystem I/O on a path that runs at every version check.
That is no longer true of it: the Stat happens only after the shape test
has already matched, so an ordinary install still reaches no filesystem
here at all.
A directory named INSTALL_RECEIPT.json is not a receipt, which a bare
Stat would have accepted silently.
NOT taken: the same review asked for the Homebrew branch to be moved
ahead of the cross-target one in upgradeGuidance. That ordering is
deliberate and already pinned by
TestUpgradeGuidanceKeepsCrossTargetAnswerForHomebrew, whose comment
predicted exactly this reading — "it reads like an ordering bug until you
see which question is being answered". Homebrew is a property of the
binary on THIS machine; when the check was asked about a different
target the answer belongs to that other machine, and `brew upgrade zero`
would change this one instead. The reasoning is now recorded at the
branch as well as in the test, so the next reader finds it in both
places.
The install docs claimed detection was the Cellar path alone, which
undersold it, and stated the self-update rule absolutely in one paragraph
before excepting mise in the next. Both say the same thing now.
* test(update): give the Homebrew fixtures a receipt, and stop gating the receipt rule on the host
The previous commit made a receipt the thing that distinguishes a keg from any
directory sitting under a path containing "Cellar". Two fixtures still built a
keg without one, so detection correctly answered standalone and both failed:
TestDetectInstallMethodRecognisesAHomebrewKeg keg path: got "standalone", want "homebrew"
TestApplyRefusesToUpdateAHomebrewKeg refusal does not name the command that works:
download release archive: dial tcp: lookup example.test
The second is worth reading twice: with detection returning standalone, Apply
stopped refusing and went to the network, so the failure surfaced as a DNS error
rather than as a missing refusal. Both fixtures write a receipt now.
The reason this reached CI at all is the second half. Every test of the receipt
rule passed runtime.GOOS to isHomebrewPath, which already takes the target as a
parameter, so all of them skipped on Windows and the rule could only ever break
somewhere else. They pass an explicit target now and run everywhere. The two
that exercise DetectInstallMethod stay host-gated, because those are about the
wired entry point rather than the rule, but the rule underneath them is no
longer invisible here.
Verified the receipt path arithmetic on both non-Windows targets rather than
trusting the host-gated tests: isHomebrewPath is false before the receipt is
written and true after, with the receipt at the keg directory hasHomebrewReceipt
actually reads.
|
||
|
|
1057d74512 |
docs(readme,cli): document /new & zero upgrade, add Discord link, and sync help text (#489)
* docs(readme): document /new, zero upgrade, and add Discord link Add the /new slash command and zero upgrade command to the README's command references, expand docs/UPDATE.md to cover the --apply install flow, and link the project Discord server. * docs(update): escape pipe in --endpoint table cell CodeRabbit flagged the unescaped | in the --endpoint placeholder breaking the Markdown table layout. * docs(readme-zh): bring /new and zero upgrade in line with README.md README_ZH.md had picked up the Discord link but was still missing the /new slash command and zero upgrade entries added to README.md, letting the translation drift out of sync. * cli: add upgrade to root help output * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <[email protected]> * docs: make required update flag explicit in common commands zero update errors without --check or --apply, so the bare form in the common-commands examples was misleading. Addresses the Copilot finding from the PR #489 review. * docs: use a permanent (non-expiring) Discord invite in the READMEs * docs(cli): match upgrade help summary to download/verify/install wording Copilot flagged the root zero --help summary for upgrade as less specific than the download/verify/install phrasing already used in README.md and docs/UPDATE.md for the same command. Co-Authored-By: Claude Sonnet 5 <[email protected]> * docs: use discord.gg short link for the community invite discord.com/invite/<code> and discord.gg/<code> resolve to the same invite; discord.gg is the canonical short form to publish. Co-Authored-By: Claude Sonnet 5 <[email protected]> * fix(update,readme-zh): keep npm --json stdout clean, add ZH Discussions route `zero upgrade --json` (and `zero update --apply --json`) on an npm-managed install wired npm's own stdout directly to the process stdout, so its progress output landed ahead of the JSON result and made it unparseable. Route npm's stdout into a buffer when JSON output is requested, only surfacing it in the error message if the install fails. README_ZH.md's Community section only pointed to Discord, unlike the English README, which reserves Discord for real-time chat and routes Q&A/ideas/sharing to GitHub Discussions per SUPPORT.md/CONTRIBUTING.md policy. Bring the Chinese section in line with the same split. Addresses the outstanding findings from jatmn's review on PR #489. Co-Authored-By: Claude Sonnet 5 <[email protected]> * docs(update): qualify npm-managed JSON output Document the npm subprocess output caveat instead of promising a single parseable JSON document for apply operations. Drop the JSON-specific updater plumbing and tests so the PR remains focused on documentation and help discovery. Amp-Thread-ID: https://ampcode.com/threads/T-019fa55a-8bcd-71bc-9bde-9b4c28b78b38 Co-authored-by: Pierre Bruno <[email protected]> * docs(update): align upgrade guidance Amp-Thread-ID: https://ampcode.com/threads/T-019fa7a0-1223-701d-9529-48ba5d7cf8c8 Co-authored-by: Pierre Bruno <[email protected]> * fix(update): keep the upgrade recommendation about this machine `zero update --check --target windows-x64` answers a question about a different machine, but the new call to action told the user to run `zero upgrade`, which only ever installs onto this one — replacing the target-specific download guidance the flag used to print. The recommendation is now conditional: a current-platform check keeps it, and a cross-target check gets the manual instruction plus a note naming the machine `zero upgrade` would have touched instead. Also align two flag contracts with what the code does: `zero update --help` described --repo/--endpoint as check-only although apply honors them, and UPDATE.md did not mention that both are ignored on an npm-managed apply, which takes its release from the npm registry rather than GitHub. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> * fix(update): do not send a custom-source check to the default source `zero upgrade` is a fresh invocation: it does not inherit the --repo/--endpoint of the check that suggested it, so recommending it bare after a custom-source check told the user to install from a repository they never asked about. The check now records the flag it was given and the guidance repeats it, naming where a bare upgrade would install from instead. ZERO_UPDATE_RELEASE_URL is deliberately not recorded: every Check reads it from the environment, including the one inside Apply, so a bare upgrade already follows it and telling the user to repeat it would be wrong. Two smaller edges from the same review: a cross-target check on a host with no published release target no longer ends by describing what `zero upgrade` does there, since it does not work there at all; and the flag table and help text now carry the npm caveat the prose bullet already had, so docs and help agree. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> * fix(update): keep source guidance accurate Amp-Thread-ID: https://ampcode.com/threads/T-019fafa2-7d9d-75bc-8801-6d0efc8db1df Co-authored-by: Pierre Bruno <[email protected]> --------- Co-authored-by: Copilot Autofix powered by AI <[email protected]> Co-authored-by: Vasanthdev2004 <[email protected]> Co-authored-by: Claude Sonnet 5 <[email protected]> Co-authored-by: Amp <[email protected]> |
||
|
|
58daea7a48 |
feat(providers): add Fireworks AI provider preset (#772)
* feat(providers): add Fireworks AI provider preset Add an OpenAI-compatible Fireworks catalog entry with curated coding models, models.dev alias mapping, onboarding tests, and README setup. * fix(provider): use correct Fireworks DeepSeek V3 model ID Update default and catalog model IDs to deepseek-v3-0324 per Fireworks API. |
||
|
|
0125e70d0f |
Fix pinned Go quality tools on Go 1.26 (#689)
* fix pinned Go quality toolchain * fix(make): use a POSIX GOTOOLCHAIN prefix for pinned tool targets The Windows_NT branch used cmd.exe syntax (set "..." &&), but GNU Make runs recipes through sh even on Windows (MSYS/Git Bash), where that form fails and breaks make lint-static/deadcode/vulncheck. A plain GOTOOLCHAIN=... prefix works everywhere, so the OS check is gone. Also switch GO_VERSION/GO_TOOLCHAIN to recursive expansion so 'go list -m' only runs when a tool target actually needs it, instead of at parse time for every target. Addresses PR review from CodeRabbit and Copilot. Co-Authored-By: Claude Fable 5 <[email protected]> * fix(build): make pinned-tool targets shell-neutral, fix README drift Address code review on PR #689: - lint-static/deadcode/vulncheck previously set GOTOOLCHAIN via a POSIX `VAR=value cmd` recipe prefix. GNU Make on Windows runs recipes through whatever SHELL is configured, and under a native cmd.exe setup that syntax is invalid — cmd.exe treats the whole "GOTOOLCHAIN=go1.26.5" token as the program to run and fails before go ever executes (reproduced locally: cmd.exe errors with "'MYVAR' is not recognized..." for the equivalent pattern). Switched to a target-specific `export`, which sets the variable in the child process environment via Make itself, before is invoked — works under sh, cmd.exe, and PowerShell alike (verified against all three targets with SHELL := cmd.exe). - README.md's Code Quality and Security Checks section still told contributors to run the raw `go run ...@version` commands this PR identifies as selecting the wrong (older) toolchain and failing to load this Go 1.26.5 module — only CI and AGENTS.md had been updated to the new make targets. Points steps 3-4 at `make lint-static` / `make vulncheck` instead, matching AGENTS.md. Verified end-to-end: `make vulncheck`, `make deadcode`, and `make lint-static` all run successfully and reach their tools (deadcode's advisory report and lint-static's 35 pre-existing findings match the PR's documented baseline). Co-Authored-By: Claude Sonnet 5 <[email protected]> * fix(build): support stock macOS make --------- Co-authored-by: Claude Fable 5 <[email protected]> |
||
|
|
c0b6b9e5c9 |
docs(readme): note govulncheck/golangci-lint install to GOPATH/bin (#711)
* docs(readme): note govulncheck/golangci-lint install to GOPATH/bin The installed binaries land in $GOPATH/bin (default ~/go/bin), which must be on PATH to run govulncheck directly. Document the export snippet so the lint/security tooling from the contributor setup actually resolves. * docs(readme): say "them" instead of singling out govulncheck golangci-lint installs to the same $GOPATH/bin as govulncheck, so the PATH note applies to both, not just the one named. Co-Authored-By: Claude Sonnet 5 <[email protected]> * docs(readme): honor GOBIN and make the PATH snippet idempotent go install writes to GOBIN when set, with GOPATH/bin as the fallback, and rerunning the old snippet appended the same directory repeatedly. Co-Authored-By: Claude Fable 5 <[email protected]> --------- Co-authored-by: Claude Sonnet 5 <[email protected]> |
||
|
|
2e267bdbee |
feat(tui): add isolated /btw conversations (#748)
* feat(tui): add isolated btw conversations Add /btw with inline-question support, isolated non-resumable session forks, background main-run routing, and safe return behavior. Refs #637 * test(tui): cover btw return controls Preserve drafts when Ctrl+C is pressed during an active BTW run and cover returning to the parent through the documented /btw toggle. * Address BTW isolation review feedback * Reject explicit resumes of side sessions * Harden BTW isolation lifecycle |
||
|
|
2cc6f43bf7 |
docs(community): wire Discussions as the front door for questions and ideas (#732)
* docs(community): wire Discussions as the front door for questions and ideas * docs(community): restore LF endings and make feature routing coherent |
||
|
|
60dc84e7a3 |
perf(openai): optimized turn session — background prewarm and prefix telemetry (PR8) (#723)
* trace(perf): add provider prewarm span and prefix-stability counters * feat(openai): optimized turn session with prewarm and prefix telemetry * feat(providers): gate optimized OpenAI turn sessions behind env flag * feat(cli): wire optimized turn sessions into headless exec * docs(readme): document the optimized OpenAI turn-session toggle * fix(openai): include tool descriptions in prefix digest, harden gate and test bounds * fix(openai): isolate prewarm tracing, skip non-reusable transports, fingerprint wire order and cache key * test(openai): pin keep-alive transports in probe-expecting tests * feat(providers): preserve resolved capabilities on switched-model fallback sessions |
||
|
|
7f669f4550 |
feat(tui): Ctrl+X leader chords and emacs menu navigation (#699)
* feat(tui): Ctrl+X leader chords and emacs menu navigation Add Ctrl+X leader shortcuts for common slash commands (with Ctrl+X ? chord map), and Ctrl+P/N previous/next selection in modals and pickers. * fix(tui): drop Ctrl+X e /edit leader chord /edit replaces the composer and would discard an in-progress draft; leave it unbound so leader shortcuts stay draft-preserving. * fix(tui): consume idle Ctrl+N as a no-op Reserve Ctrl+N for emacs menu navigation so it never falls through to remapped global bindings when no selection surface is open. * docs: drop manual CHANGELOG entries for TUI shortcuts |
||
|
|
665bd4b3d0 |
WIP: feat(tui): TUI theme presets (Dune and Neon) (#634)
* draft(tui): add theme interface and Claude/Codex presets proposal Introduces the Theme interface alongside the initial specifications and styling models for the Claude (card-based, warm) and Codex (high-density, split-screen cyberpunk) layout presets. Detailed design plans are documented in docs/THEMES.md. * fix(tui): implement Claude/Codex themes on the existing palette system theme.go had replaced the tuiTheme/buildTheme/palette architecture every renderer depends on with a standalone Theme interface using the wrong lipgloss import, breaking the build and orphaning the existing theme registry, /theme command, and picker. Revert theme.go to the working implementation and add claude and codex as real palette entries in theme_palettes.go, following the file's own extension pattern. Both pass the WCAG AA contrast and gray-ramp invariants asserted across the whole registry, and are picked up automatically by --theme, ZERO_THEME, /theme, and the picker. Rewrite docs/THEMES.md to describe the actual palette-registry architecture instead of the removed interface. * fix(tui): rename Claude/Codex to neutral Dune/Neon, ensure WCAG AA contrast compliance, and add tests * style(tui): gofmt theme_select_test.go (drop trailing whitespace) The ubuntu smoke run fails on gofmt -l; this clears the only unformatted file in the draft so CI goes green. * test(tui): cover the CLI/env resolution path for the dune and neon presets TestNewThemePresetsWired only checked lookupTheme and the IsDark grouping. Add coverage for the actual --theme/ZERO_THEME precedence path (resolveThemeMode) and confirm applyTheme swaps zeroTheme to each preset's own palette rather than a built-in fallback. * fix(tui): raise Dune/Neon palette contrast and fix theme docs Darken dune's accent, blue, and red tokens so the selected-row caret, local-model dot, and diff deletion text clear WCAG AA against selBg and delBg, including after ANSI-256 downsampling. Also correct THEMES.md's description of bare /theme (it opens a live-preview picker, not a list), and add the dune/neon names to the ZERO_THEME/--theme tables in README.md and README_ZH.md. * fix(tui): keep Dune and Neon readable after ANSI-256 downsampling - Dune's accent becomes #724028: still AA on selBg and panel in truecolor (5.46:1 and 7.03:1), and its xterm-256 quantization (#444444) now clears AA on quantized selBg at 6.47:1 where the previous #7c4712 quantized to #875f00 at 3.81:1. Blue already survived quantization at 4.67:1. - Neon's diff bands move to hexes that quantize to distinct green and red xterm entries (addBg #083c10 -> #005f00, delBg #3c0810 -> #5f0000, addBgWord #147828 -> #008700, delBgWord #74202e -> #870000) instead of collapsing to the same grays, while keeping every truecolor invariant. - New TestExtendedThemeANSI256Contrast quantizes the palette tokens the way a 256-color terminal does and asserts the Dune selected-row pairs stay AA and the Neon bands keep their green/red identity and mutual separation. - docs/THEMES.md is now linked from the Documentation sections of both READMEs, and the Unreleased changelog inventory says twelve themes and names dune and neon. * fix(tui): keep Neon diff content AA after quantization, fix error border - faintest becomes #74c468: line numbers now quantize to #87d75f, which clears AA on the xterm-green addBg (#005f00) where the old #58af69 quantized to 2.95:1, while staying dimmer than faint so the gray ramp stays monotonic. - addInk becomes #ecffdc, quantizing to #ffffd7 for 4.60:1 on the word band's #008700 (was 4.29:1). Both rendered diff-content pairs are now asserted in the ANSI-256 test instead of documented as omissions. - cardErr becomes #9a4042: 3.01:1 against the panel (2.43:1 before) and 3.65:1 quantized, meeting the 3:1 non-text component threshold, with the border surface asserted in both renderings. - docs/THEMES.md no longer claims the extended invariants run against the whole registry: it now says new palettes must be added to the per-palette contrast tests or given equivalent coverage. |
||
|
|
d66a9dda69 |
feat(providers): add AI/ML API preset (rebased onto main) (#621)
* feat(providers): add AI/ML API preset * fix(providers): relocate aimlapi preset and drop referral header Addresses the two review asks on #402: - Move the AI/ML API descriptor out of catalog slot #2 (it sat above the first-party OpenAI/Anthropic/Google entries) down next to openrouter in the aggregator cluster. Re-sync expectedCatalogIDs and the TransportOpenAICompat order-list to match. - Drop the always-on X-AIMLAPI-Partner-ID / Integration-Repo / Integration-Version attribution headers. There's no partner arrangement behind the preset, so a non-overridable referral tag on every user's request isn't something to ship on a community preset. aimlapi now rides through plain CopyHeaders like every other provider. Removes HeadersForCatalog (it existed only for this special-case), reverts the factory and discovery call sites to CopyHeaders, and drops the header test. The factory/discovery tests now assert the endpoint, auth, and the user's own custom headers survive while no attribution header is sent. * fix(providers): address CodeRabbit review on #621 - Document AIMLAPIID for docstring coverage. - Apply providerio.CopyHeaders for Anthropic and Google provider paths, matching the OpenAI path introduced for aimlapi header isolation. Co-authored-by: Cursor <[email protected]> * fix(providers): gofmt factory.go to unblock CI smoke Remove mixed tab/space indentation in the openai.New options block so ubuntu-latest fmt-check passes. --------- Co-authored-by: Dmitry Tumanov <[email protected]> Co-authored-by: Vasanthdev2004 <[email protected]> Co-authored-by: Cursor <[email protected]> Co-authored-by: KRATOS <[email protected]> |
||
|
|
fa3052a142 |
feat(providers): refresh MiniMax model coverage (#665)
Co-authored-by: octo-patch <[email protected]> |
||
|
|
5a5b7fd5b8 |
docs: add Go code quality & security checks and bump Go version to 1.26+ (#635)
* docs: add Go code quality & security checks and bump Go version to 1.26+ * docs: address CodeRabbit review comments and use pinned versions and Go 1.26.5+ * docs: remove trailing blank lines at end of AGENTS.md * docs: add missing /v2/ segment to golangci-lint install path The go install command pointed at the v1 module path (golangci-lint/cmd/golangci-lint), which does not resolve for v2.12.2. The go run invocation earlier in the same section already had the correct v2 path. * docs: trim AGENTS.md below 8 KiB guideline and clean up README cross-compile header * docs: address CodeRabbit review feedback on extending guide |
||
|
|
5e1405d0b7 |
feat(npm): ship the native binary as platform optionalDependencies (#626)
* feat(npm): ship the native binary as platform optionalDependencies Replace the postinstall downloader with Codex-style platform packages: suffixed versions of @gitlawb/zero (X.Y.Z-<platform>-<arch>) carrying the binary, sandbox helpers, and the vendored agent-browser/tuistory tree, referenced from the wrapper via npm: aliases in optionalDependencies. The published wrapper has no lifecycle scripts and no dependencies, so npm/Bun/pnpm installs are silent: no EBADENGINE from agent-browser's node>=24 engines pin, no allow-scripts warnings, no network outside the npm registry. The old downloader remains as a checksum-verified first-run fallback for --omit=optional installs and unsupported platforms. scripts/npm/build-platform-packages.mjs assembles the payloads from the release archives at publish time: it materializes npm's symlink .bin shims into regular sh shims (npm pack silently drops symlinks) and prunes agent-browser's other-platform binaries (~65 MB per package). The publish job uploads platform versions first under --tag platform, asserts the latest dist-tag survived (platform versions are semver prereleases of the wrapper version), then publishes the wrapper. See docs/NPM_PACKAGING.md for the architecture and publishing rules. * fix(npm): address PR review — LICENSE in payloads, dist-tag lookup guard, accurate fallback and platform docs - Ship the repo LICENSE in both the wrapper and platform payloads (npm only includes a license file physically present in the packed directory). - Guard the platform-publish dist-tag lookup with || true so a transient npm view failure (or a bootstrap publish with no latest tag) cannot abort the job between the platform and wrapper publishes. - Gate the wrapper's fallback downloader on the release matrix so platforms with no asset (windows-arm64 and unknown combos) skip the spawn instead of attempting it every run; keep deliberate retry-on-failure for supported platforms and say so honestly in the notice and docs. - Correct Windows support wording everywhere: x64 only, ARM runs under emulation (README, README_ZH, INSTALL.md incl. the release-target list). |
||
|
|
387fe67ee7 |
feat(tui): /loop — repeat a prompt or command on an interval or self-paced (#502)
* feat(tui): /loop foundation — command parsing, loop state, safety bounds First slice of the /loop feature (#501): the self-contained core with no TUI wiring yet. parseLoopCommand handles the leading-interval, trailing-"every", bare-self-paced, and list/stop forms; loopState models one session-scoped loop with doom-loop + iteration-cap fields; interval/self-pace clamps and duration formatting round it out, with unit tests. Because Zero drives loops off a Go timer rather than a cron backend, arbitrary intervals (90s, 7m) are supported with no whitelist limitation. * feat(tui): wire /loop into the turn lifecycle — dispatch, idle tick, continuation Second slice of #501. Adds the controller and wires it in: - /loop command registered (commands.go) and dispatched to handleLoopCommand. - Loop state on the model (loops, activeLoopID, loopSeq/loopTicking poll guards, loopNextWake/loopDone self-pace channel). - An idle poll tick (loopTickMsg, ~1s) fires the earliest due loop only when the session is idle — mirrors the launchQueuedMessageIfReady guard so loops and a user's queued prompt never collide; a self-perpetuating tea.Tick that stops when no loops remain. - The agentResponseMsg completion seam advances a loop iteration (schedule next wake for fixed, model delay for self-paced) with doom-loop, iteration-cap, and done stop conditions, restarting the ticker if other loops remain. - /loop list|stop|stop all, footer summary + list rendering, and a guard that only a prompt or custom /command may loop (not a built-in). Controller tests cover start/advance/self-paced-delay/done/doom/cap/stop/busy. * feat(tui): /loop self-paced adaptive cadence + footer summary + /clear note Third slice of #501. - Self-paced loops now use an adaptive cadence (2m early -> 30m heartbeat as iterations accrue), with the loopNextWake channel kept as an override for a future explicit model signal. - Persistent footer segment "↻ N loops · next 3:05pm" while loops are active. - /clear notes that loops keep running (it wipes the transcript, not the session) so a loop never silently fires into a cleared screen. - Tests for adaptive cadence + footer summary. * test(tui): drive /loop end-to-end through Update (submit + stale tick) * docs(readme): add /loop to the slash-command table * fix(tui): stop looping on repeated failure + clear loop tag on cancel Addresses CodeRabbit review on #502: - advanceLoop now counts consecutive failed iterations (loopMaxFailures=3) and stops the loop, so a loop whose turns keep erroring no longer churns to the iteration cap (the doom guard only sees identical non-empty answers). - cancelRun clears activeLoopID and re-arms the interrupted loop for its next cadence, so a cancelled iteration is not left running forever and the next unrelated turn is not misattributed as that loop's completion. * fix(tui): finish /loop v1 — real self-paced completion, session scope, leave gate Response to the review. The self-paced path was the real gap: it advertised model-chosen completion but nothing ever wrote the done/next-wake fields, so it was just adaptive polling dressed up. Replaced that with an actual control channel — each self-paced iteration runs a short maintenance playbook and ends its reply with a control line (LOOP: DONE / LOOP: CONTINUE [interval]) that advanceLoop parses to stop the loop or pick the next wake. The two dead model fields are gone; done and next-wake now come from the model's output. Rest of the review: - loops stop on a session switch (/new, /resume) instead of firing the previous conversation's prompt into the new one - validateLoopTarget resolves custom /commands up front and rejects unknowns, so a mistyped `/loop 5m /nope` isn't scheduled and then re-sent as literal text - a one-shot confirm gates /clear and /quit while loops are active - age-based auto-expire (24h) next to the iteration cap - the two CodeRabbit test nits (HasPrefix footer check, bare `/loop stop` on a single loop) Hardening from a self-review pass over the above: - normalize CRLF and allow a trailing note on the control line, so `LOOP: DONE — all clean` and \r\n replies still stop the loop - disarm the leave-confirm before the queue/exit early returns, so an interposed prompt can't leave it falsely armed - a loop no longer fires behind an open picker/modal; a run launched behind the /resume picker would otherwise complete into whatever session you switch to * fix(tui): address /loop review round 2 — ticker re-arm, self-paced command, resume guard - stopLoop re-arms the poll ticker after removing one loop, so stopping one of several no longer strands the rest with no tick scheduled (jatmn). - a bare self-paced /command is rejected with a pointer to the interval form: self-paced drives a free-form goal via the playbook + LOOP: control line, which a custom command's expanded template never carries, so it would silently degrade to adaptive polling (jatmn). - /resume only tears loops down on an actual session-id change; /resume latest or /resume <currentID> can resolve to the already-active session, whose loops are its own, not a "previous" session's (CodeRabbit). - tests: skipped-behind-modal loop stays scheduled, footer carries the next-wake time, the confirmed /clear actually wipes the transcript, plus multi-loop stop keeping the ticker alive and self-paced-/command rejection. * docs(readme): clarify /loop runs a prompt or custom command, not built-ins |
||
|
|
8f3b8f0d13 |
docs: document AGENTS.md/ZERO.md and plugin manifest format (#522)
* docs: document AGENTS.md/ZERO.md and plugin manifest format Add an "Extending Zero" section to the README covering the AGENTS.md/ZERO.md project-instructions lookup chain and the plugin.json manifest fields, neither of which were previously documented outside the source. Closes #521 Co-Authored-By: Claude Sonnet 5 <[email protected]> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <[email protected]> * docs: fix plugin path scope and add per-user ZERO.md guidelines - Correct project-scope plugin discovery to <cwd>/.zero/plugins, not the repo root. - Clarify that user-scope plugin discovery always resolves under ~/.config regardless of OS, separate from config.UserConfigDir() (which is OS-aware and used for the per-user ZERO.md file). - Document the per-user ZERO.md guidelines file added in #475. Co-Authored-By: Claude Sonnet 5 <[email protected]> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <[email protected]> --------- Co-authored-by: Claude Sonnet 5 <[email protected]> Co-authored-by: Copilot Autofix powered by AI <[email protected]> |
||
|
|
068c5ad3ad |
docs(install): recommend bun pm trust for Bun installs (#452)
The Bun instructions told global installers to run the postinstall script manually against the global install path, because trustedDependencies only works in a project package.json. Current Bun has a simpler sanctioned path that works for both project and global installs: `bun pm trust @gitlawb/zero` (with -g for globals) runs the blocked postinstall after the fact. Verified on Bun 1.3.14 on Windows 11: `bun pm -g trust @gitlawb/zero` fetched the binary and `zero --version` runs. Lead with that in README.md, README_ZH.md, and docs/INSTALL.md, keep trustedDependencies and the manual installer as alternatives for project installs and older Bun versions, and teach the wrapper's missing-binary message the same commands for the Bun case. Co-authored-by: Claude Fable 5 <[email protected]> |
||
|
|
b63fe696fd |
modify readme.md and add readme_zh.md (#436)
Co-authored-by: zhangzetao <[email protected]> |
||
|
|
b4275e3504 |
feat(providers): add Meituan LongCat catalog preset (#424)
Add LongCat-2.0 as a first-class OpenAI-compatible provider preset so it can be selected by name in the setup wizard / `zero providers setup longcat` instead of hand-entering a custom endpoint. LongCat-2.0 is Meituan's open, OpenAI- and Anthropic-compatible model. This wires the OpenAI-compatible transport: base URL: https://api.longcat.chat/openai (Zero appends /chat/completions) model: LongCat-2.0 auth: Bearer via LONGCAT_API_KEY Purely additive — one descriptor plus its catalog/onboarding tests and a README env-var line; no shared plumbing is touched, so existing providers are unaffected. Placed with the other OpenAI-compatible third parties (after moonshot), below the first-party providers. Verified: go build / vet / test green (catalog + cli onboarding), gofmt clean, and `zero providers catalog` lists the descriptor with the correct fields. Endpoint is wired by-spec from LongCat's published docs; a live completion still needs a real LONGCAT_API_KEY to confirm. |
||
|
|
b5a78dca6f |
docs(install): document Bun install and improve missing-binary error (#418)
Bun does not run dependency lifecycle scripts by default, so the npm wrapper's postinstall (which fetches the native Zero binary from GitHub Releases) is silently skipped. The first run then fails with an unhelpful "No native binary found" message that points at a source-only build command, leaving Bun users stuck. Add a Bun section to README.md and docs/INSTALL.md with the two working install paths (run postinstall.mjs manually, or add the package to the project's trustedDependencies). Rewrite the wrapper's missing-binary error to give an actionable fix for everyone and a Bun-specific hint when run under Bun, instead of only suggesting a source checkout. |
||
|
|
49bec0274f |
feat(tui): interactive theme picker + color theme catalog (#354)
* feat(tui): interactive theme picker + color theme catalog
Bare `/theme` now opens a popup that live-previews each palette as the cursor
moves and applies on select (Esc reverts), matching /model and /effort. Adds ten
built-in color themes alongside the dark/light built-ins — dracula, nord, gruvbox,
tokyo-night, catppuccin, one-dark, solarized-dark, rose-pine, everforest, and
solarized-light — plus a reworked light palette. Every palette is contrast-audited
to WCAG AA; the contrast tests now loop the whole registry.
Palettes and an ordered registry live in a new internal/tui/theme_palettes.go (now
the sole home of hex literals); selection, the picker, /theme <name>, --theme, and
ZERO_THEME all resolve through it. The whole frame is painted with the active
theme's surface (tea.View.BackgroundColor) so every theme is legible on any
terminal — a light theme's dark text no longer lands invisibly on a dark terminal,
and color themes show their real surface.
The chosen theme persists to user config (preferences.theme) and is reapplied at
startup, below the --theme flag and ZERO_THEME.
* fix(tui): address CodeRabbit review on the theme catalog
- CHANGELOG: reconcile the --theme bullet with the named themes (was still
documenting {auto|dark|light}).
- /theme usage string advertises the list and auto forms (/theme list is still
accepted) so /help matches the command surface.
- Theme-persistence test parses the written config and asserts preferences.theme
directly instead of a raw substring, so a wrong-key regression fails.
|
||
|
|
fb3a0bff44 |
Document source-build sandbox helpers (#342)
Clarify that Linux source builds need the zero-linux-sandbox helper on PATH for native sandboxing, while macOS does not need an extra helper and Windows can self-dispatch from zero.exe. Also document optional release-style helper builds for Linux and Windows. Tested: git diff --check |
||
|
|
726f1512a3 |
Clean up public release docs (#339)
Rewrite the README around the initial public-release user flow, add the logo asset, and refresh install/update documentation. Remove internal planning, audit, design, and work-split documents from the public docs tree. Update stale documentation references and workspace seed expectations to point at public docs. Tested: env GOCACHE=/tmp/zero-go-cache go test ./internal/workspaceseed Tested: git diff --check |
||
|
|
2b6568f82b |
chore: launch-readiness — add MIT LICENSE + reconcile README (#320)
* docs: add MIT LICENSE Adds the canonical MIT License (Copyright (c) 2026 Gitlawb) at the repo root, resolving the launch blocker where the project was marketed as open source with no license file (legally all-rights-reserved). * docs(readme): declare MIT license + badge, link LICENSE README's License section said 'being finalized'; now states MIT and links the LICENSE file. Adds a license badge to the header row. |
||
|
|
29246e5659 |
fix(providers): make the stream idle timeout global, configurable, and less aggressive (#285)
A long generation could die with "provider stream error: idle timeout after 1m30s (upstream stopped sending data)". The 90s idle watchdog was duplicated as defaultStreamIdleTimeout in three providers (anthropic, gemini, openai; codex inherits openai) and wired to no config, so a slow cloud/reasoning backend that goes silent without SSE keep-alives for >90s was killed mid-stream. Centralize it in providerio: - providerio.DefaultStreamIdleTimeout — single source of truth, raised 90s -> 5m so a genuine silent pause has real headroom. The watchdog still resets on SSE keep-alive comments, so a heartbeating-but-slow upstream is never aborted; this only changes how long true silence is tolerated before aborting a hung stream. - providerio.ResolveStreamIdleTimeout(option): precedence is explicit option > ZERO_STREAM_IDLE_TIMEOUT env (a Go duration or bare seconds; 0/off/none disables the watchdog) > default. All providers resolve through it, so one env var tunes every provider at once. Removes the three duplicated consts. Existing idle tests (which set an explicit short StreamIdleTimeout) keep passing because an explicit option still wins. Adds resolver tests (precedence, env parsing, disable, typo-falls-back-to-default) and documents ZERO_STREAM_IDLE_TIMEOUT in the README. |
||
|
|
33da6b6454 |
feat(npm): publish @gitlawb/zero with a postinstall binary installer (#284)
Make `npm install -g @gitlawb/zero` deliver a working CLI. bin/zero.js already
execs an adjacent binary, but nothing fetched it and the name `zero` is taken
on npm. This wires up delivery:
- package.json: rename to @gitlawb/zero; add a postinstall hook, files, engines,
os/cpu, repository, and a license placeholder (the LICENSE file is still
pending and tracked separately).
- scripts/postinstall.mjs: detect platform/arch (mapping mirrors
internal/release/release.go — darwin->macos, amd64->x64), download
zero-v{version}-{os}-{arch}.{tar.gz|zip} from the matching GitHub Release,
verify its SHA-256, and extract into place. Extraction goes to a temp dir and
copies only known binary basenames, so a crafted archive cannot escape the
package (no zip-slip). Idempotent via a version marker; ZERO_SKIP_DOWNLOAD and
unsupported platforms are non-fatal skips.
Security hardening (from an adversarial review):
- require https for the download origin (the same-origin .sha256 makes TLS the
load-bearing integrity control); ZERO_ALLOW_INSECURE_DOWNLOAD=1 opts out for
local testing, and an https->http redirect is refused.
- the checksum parser is anchored per line and validates the filename field.
- cap the download size; warn when an optional sandbox helper is absent.
release-artifacts.yml: on a tag push, assert the git tag equals
v{package.json version} before packaging — zero-release names assets from
package.json while the release publishes under the tag, so any drift would 404
every install (install.sh, install.ps1, and npm alike).
Tests (internal/npmwrapper): package.json now requires exactly a postinstall
hook (no repo build scripts) plus name/license/repository/files; new
node-driven tests cover the per-platform asset plan, the skip env, and the
unsupported-platform path. Verified end-to-end against a local mirror:
download -> verify -> extract places the binary, a tampered checksum is
rejected, and http is refused without the opt-in.
Going live still needs two maintainer steps: create the `gitlawb` npm org +
NPM_TOKEN, and publish the first v0.1.0 release so there is an asset to fetch.
|
||
|
|
9047977895 |
fix: resolve product/UX audit findings for OSS launch (#282)
Resolve product/UX audit findings for the OSS launch: doctor now fails uncredentialed remote providers (no false green), Redact() no longer mangles the common first-run auth error, SSRF-safe loopback exemption unblocks keyless local models, new validated --theme flag, WCAG-AA contrast fixes, NO_COLOR compliance, clearer first-contact errors, and full OSS scaffolding (SECURITY/CoC/CHANGELOG/templates/release workflow). Audit ledger maps every Fixed claim to a real change with zero overclaims. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
4cb1f10952 |
Add sandbox architecture baseline (#240)
* Add sandbox architecture baseline Record target backend names, enforcement metadata, legacy sandbox entrypoints, and policy JSON baseline coverage for the sandbox rewrite. Add a focused sandbox smoke script with cross-platform compile checks and an opt-in real backend smoke path. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestTargetBackendForPlatformBaseline|TestLegacySandboxEntrypointsAreExplicitAndExist|TestBackendPlanCarriesPhase0ManagerFields|TestCommandPlanCarriesSandboxMetadata|TestPolicyOnlyDisabledFailClosedForTargetPlatforms|TestSelectBackendChoosesPlatformAdapterWithFallback|TestBackendBuildPlanDocumentsBestEffortIsolation|TestBackendCapabilitiesReflectDisabledPolicy' Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/cli -run 'TestRunSandboxPolicyInspectTextAndJSON|TestRunSandboxPolicyJSONGoldenIncludesManagerBaselineFields|TestRunSandboxPolicyEffectiveTextAndJSON' Tested: scripts/sandbox-smoke.sh Tested: git diff --cached --check * Introduce sandbox manager and permission profile Add a normalized permission profile and sandbox manager execution request that owns target backend selection, enforcement metadata, and fail-closed validation. Route command planning and sandbox policy output through the manager boundary while keeping legacy adapters explicit as temporary integration points. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestPermissionProfileFromPolicyBuildsWorkspaceWriteProfile|TestPermissionProfileFromDisabledPolicyDoesNotRequirePlatformSandbox|TestSandboxManagerBuildsExecutionRequestFromProfile|TestSandboxManagerFailsClosedWhenNativeRequiredAndPolicyOnlyDisabled|TestTargetBackendForPlatformBaseline|TestBackendPlanCarriesPhase0ManagerFields|TestCommandPlanCarriesSandboxMetadata|TestPolicyOnlyDisabledFailClosedForTargetPlatforms' Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/cli -run 'TestRunSandboxPolicyInspectTextAndJSON|TestRunSandboxPolicyJSONGoldenIncludesManagerBaselineFields|TestRunSandboxPolicyEffectiveTextAndJSON' Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/tools -run 'TestRegistryRunsBashThroughSandboxEngine|TestBashToolReportsDisabledPolicyOnlyRunner|TestBashToolBuildsWrappedSandboxExecCommand' Tested: scripts/sandbox-smoke.sh Tested: git diff --cached --check * Move sandbox backend selection into manager Make SandboxManager own platform backend selection and keep SelectBackend as a compatibility shim for existing CLI and TUI wiring. Add tests proving manager-owned selection and the compatibility wrapper agree. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestSandboxManagerSelectsPlatformBackend|TestSelectBackendDelegatesToSandboxManagerSelection|TestSelectBackendChoosesPlatformAdapterWithFallback|TestLegacySandboxEntrypointsAreExplicitAndExist|TestSandboxManagerBuildsExecutionRequestFromProfile|TestSandboxManagerFailsClosedWhenNativeRequiredAndPolicyOnlyDisabled' Tested: scripts/sandbox-smoke.sh Tested: git diff --cached --check * Route command planning through sandbox manager Make SandboxManager build command plans from sandbox execution requests, with backend-specific wrapping isolated behind a temporary adapter. Move Engine.BuildCommandPlan to a single manager boundary and mark buildLegacyCommandPlan as the explicit delete target for later backend work. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestSandboxManagerBuildsCommandPlanThroughTemporaryAdapter|TestSandboxManagerBuildsDegradedPolicyOnlyCommandPlan|TestBuildCommandPlanWrapsBubblewrap|TestBuildCommandPlanWrapsSandboxExec|TestBuildCommandPlanUsesPolicyOnlyFallback|TestBuildCommandPlanCanRejectPolicyOnlyFallback|TestLegacySandboxEntrypointsAreExplicitAndExist|TestSandboxManagerBuildsExecutionRequestFromProfile|TestSandboxManagerFailsClosedWhenNativeRequiredAndPolicyOnlyDisabled' Tested: scripts/sandbox-smoke.sh Tested: git diff --cached --check * Add Linux sandbox helper command boundary Introduce the zero-linux-sandbox command surface and typed permission-profile argv builder used by the upcoming Linux helper rewrite. Keep the helper fail-closed until enforcement is wired, and add parser tests plus smoke compilation for the new helper package. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestBuildLinuxSandboxCommandArgsSerializesPermissionProfile|TestParseLinuxSandboxHelperArgs|TestRunLinuxSandboxHelperFailsClosedUntilEnforcementIsWired' Tested: scripts/sandbox-smoke.sh Tested: git diff --cached --check * Fix sandbox policy golden path aliases Normalize both original and symlink-resolved temp paths before comparing sandbox policy JSON goldens, so macOS /private/var aliases do not fail the smoke job. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/cli -run 'TestRunSandboxPolicyJSONGoldenIncludesManagerBaselineFields|TestRunSandboxPolicyInspectTextAndJSON|TestRunSandboxPolicyEffectiveTextAndJSON' Tested: scripts/sandbox-smoke.sh Tested: git diff --cached --check * Fix Windows sandbox smoke test assumptions Normalize JSON-escaped Windows paths in the sandbox policy golden test and mark the fake bubblewrap backend as Linux in the command metadata test. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/cli -run 'TestRunSandboxPolicyJSONGoldenIncludesManagerBaselineFields|TestRunSandboxPolicyInspectTextAndJSON|TestRunSandboxPolicyEffectiveTextAndJSON' Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestCommandPlanCarriesSandboxMetadata|TestBackendPlanCarriesPhase0ManagerFields|TestPolicyOnlyDisabledFailClosedForTargetPlatforms' Tested: scripts/sandbox-smoke.sh Tested: git diff --cached --check * Stabilize sandbox policy golden test Compare the Windows sandbox policy golden as decoded JSON after path tokenization so platform-specific byte formatting does not fail smoke tests while preserving the same field-level regression coverage. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/cli -run TestRunSandboxPolicyJSONGoldenIncludesManagerBaselineFields Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/cli -run 'TestRunSandboxPolicyJSONGoldenIncludesManagerBaselineFields|TestRunSandboxPolicyInspectTextAndJSON|TestRunSandboxPolicyEffectiveTextAndJSON' Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox Tested: scripts/sandbox-smoke.sh Tested: git diff --check * Route Linux sandboxing through helper Move Linux command planning to invoke zero-linux-sandbox with the permission profile, command cwd, and command argv instead of exposing bubblewrap construction from the runner boundary. The helper now builds the bubblewrap argv, binds its own executable for the inner stage, applies the seccomp/no-new-privs step before exec, and preserves the original command cwd path. Package Linux releases with zero-linux-sandbox next to zero so release builds use the helper directly, while local development can fall back to go run for the helper. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/cli ./internal/tools ./cmd/zero-linux-sandbox Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/release ./cmd/zero-release Tested: scripts/sandbox-smoke.sh Tested: git diff --check * Normalize Linux helper cwd test paths Compare native paths in the helper cwd regression test so Windows smoke does not fail on path separator formatting while preserving the same helper behavior assertion. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run TestLinuxHelperPlanPreservesRealExtraRootCwd Tested: scripts/sandbox-smoke.sh Tested: git diff --check * Add Linux Landlock helper enforcement Implement the zero-linux-sandbox Landlock helper path with fail-closed profile validation, Landlock filesystem rules, and seccomp network-deny enforcement for the fallback backend. Broaden the Linux real smoke coverage to cover helper write allow/deny, deny-read, temp writes, metadata protection, network deny, and direct Landlock fallback behavior. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox Tested: scripts/sandbox-smoke.sh Tested: GOCACHE=/tmp/zero-go-cache go test ./cmd/zero-linux-sandbox ./internal/cli ./internal/tools ./internal/release ./cmd/zero-release Tested: git diff --check * Route Seatbelt profiles through permission profiles Build macOS sandbox-exec profiles from the shared PermissionProfile so read roots, write roots, temp access, deny-read, deny-write, and protected metadata are expressed by the platform profile instead of legacy write-root-only inputs. Keep the existing sandbox-exec adapter path as a compatibility wrapper while adding target macos-seatbelt capability handling. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox Tested: scripts/sandbox-smoke.sh Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/cli ./internal/tools Tested: git diff --check * Stabilize Seatbelt write carveouts Emit protected metadata and read-only write-root carveouts as explicit Seatbelt deny rules after the broad write allow. This preserves deny precedence while avoiding fragile allow-rule predicates in real sandbox-exec runs. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox Tested: scripts/sandbox-smoke.sh Tested: git diff --check * Expand macOS Seatbelt runtime allowances Restore the platform startup allowances needed by sandbox-exec-wrapped commands under restricted filesystem profiles: executable mapping, system metadata reads, pty/device access, preferences, IPC, and standard library/runtime roots. This keeps project writes constrained to the PermissionProfile roots while preventing macOS shells from aborting before user commands run. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox Tested: scripts/sandbox-smoke.sh Tested: git diff --check * Normalize Seatbelt deny expectations in tests Build Seatbelt deny-rule expectations from normalized profile paths so the metadata ordering test matches production behavior on Windows and Unix hosts. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox Tested: scripts/sandbox-smoke.sh Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go test -c ./internal/sandbox -o /tmp/zero-sandbox.test.exe Tested: git diff --check * Add Windows sandbox runner command planning Select the windows-restricted-token backend only when the Windows sandbox command runner is discoverable, and keep the missing-runner path as an explicit policy-only downgrade. Add the Windows runner argument contract for command cwd, workspace roots, permission profile JSON, child environment JSON, restricted-token level, and payload command parsing. Update sandbox policy output and tests to describe the missing Windows runner instead of an unimplemented adapter. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/cli Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go test -c ./internal/sandbox -o /tmp/zero-sandbox.test.exe Tested: scripts/sandbox-smoke.sh Tested: GOCACHE=/tmp/zero-go-cache go test ./... Tested: git diff --check * Add Windows sandbox command runner Add the zero-windows-command-runner executable and wire windows-restricted-token plans to pass sandbox home, command cwd, workspace roots, permission profile JSON, child env JSON, sandbox level, and command argv. Persist Windows capability SIDs under the resolved sandbox home, select read-only or writable-root capability SIDs from the permission profile, and create a restricted token before launching the payload with CreateProcessAsUserW. Package the Windows runner next to zero.exe and extend sandbox smoke coverage to cross-build it. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./internal/release ./cmd/zero-windows-command-runner ./cmd/zero-release Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go build -o /tmp/zero-windows-command-runner.exe ./cmd/zero-windows-command-runner Tested: scripts/sandbox-smoke.sh Tested: GOCACHE=/tmp/zero-go-cache go test ./... Tested: git diff --check * Plan Windows sandbox ACL operations Derive a deterministic Windows ACL operation list from the restricted permission profile, including writable root allows, protected metadata deny-write entries, explicit deny-write paths, and deny-read materialization targets. The plan maps workspace and extra writable roots onto the persisted capability SIDs so the elevated setup helper can apply the same contract without reinterpreting the profile. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go test -c ./internal/sandbox -o /tmp/zero-sandbox.test.exe Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go build -o /tmp/zero-windows-command-runner.exe ./cmd/zero-windows-command-runner Tested: GOCACHE=/tmp/zero-go-cache go test ./... Tested: scripts/sandbox-smoke.sh Tested: git diff --check * Add Windows sandbox setup boundary Add the zero-windows-sandbox-setup command, setup argument parsing, deterministic ACL plan hashing, and a setup marker contract that can detect profile changes requiring refresh. Make Windows native backend discovery require both the command runner and setup helper, package the setup helper with Windows releases, and include it in sandbox smoke builds. The setup command remains fail-closed until the Windows ACL applier is wired, so it cannot create a setup-complete marker without applying enforcement. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./internal/release ./cmd/zero-windows-sandbox-setup Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go test -c ./internal/sandbox -o /tmp/zero-sandbox.test.exe Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go build -o /tmp/zero-windows-command-runner.exe ./cmd/zero-windows-command-runner Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go build -o /tmp/zero-windows-sandbox-setup.exe ./cmd/zero-windows-sandbox-setup Tested: GOCACHE=/tmp/zero-go-cache go test ./... Tested: scripts/sandbox-smoke.sh Tested: git diff --check * Apply Windows sandbox ACL setup Implement the Windows ACL setup path with grouped native DACL updates, deny-read materialization, existing DACL snapshots, and rollback for partial application failures. Write the setup marker only after ACL setup succeeds, roll back applied ACLs if marker persistence fails, and make the Windows command runner validate the setup marker before creating the restricted token. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./cmd/zero-windows-sandbox-setup ./cmd/zero-windows-command-runner Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go test -c ./internal/sandbox -o /tmp/zero-sandbox.test.exe Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go build -o /tmp/zero-windows-command-runner.exe ./cmd/zero-windows-command-runner Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go build -o /tmp/zero-windows-sandbox-setup.exe ./cmd/zero-windows-sandbox-setup Tested: GOCACHE=/tmp/zero-go-cache go test ./... Tested: scripts/sandbox-smoke.sh Tested: git diff --check * Fail closed for Windows network policies Add Windows network policy validation that allows only explicit network-allow until native Windows network enforcement is implemented. Make the Windows command runner reject deny, scoped, and unset network modes before launching a restricted-token process so default network-deny cannot silently run open. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./cmd/zero-windows-command-runner Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go test -c ./internal/sandbox -o /tmp/zero-sandbox.test.exe Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go build -o /tmp/zero-windows-command-runner.exe ./cmd/zero-windows-command-runner Tested: GOCACHE=/tmp/zero-go-cache go test ./... Tested: scripts/sandbox-smoke.sh Tested: git diff --check * Track Windows network setup state Add a canonical Windows network-policy hash and persist it in the Windows sandbox setup marker so setup refresh can detect network mode, domain, and proxy-requirement changes. Bump the setup marker schema and add tests for domain-order normalization plus network-change marker invalidation. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go test -c ./internal/sandbox -o /tmp/zero-sandbox.test.exe Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go build -o /tmp/zero-windows-command-runner.exe ./cmd/zero-windows-command-runner Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go build -o /tmp/zero-windows-sandbox-setup.exe ./cmd/zero-windows-sandbox-setup Tested: GOCACHE=/tmp/zero-go-cache go test ./... Tested: scripts/sandbox-smoke.sh Tested: git diff --check * Add sandbox setup CLI command Add zero sandbox setup to resolve the active sandbox policy, derive the Windows setup helper next to the command runner, and invoke it with the shared Windows setup argument contract. Return text or JSON setup results, no-op cleanly for non-Windows native backends, and add tests using an injected helper runner. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/cli ./internal/sandbox Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go test -c ./internal/cli -o /tmp/zero-cli.test.exe Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go test -c ./internal/sandbox -o /tmp/zero-sandbox.test.exe Tested: GOOS=windows GOARCH=amd64 GOCACHE=/tmp/zero-go-cache go build -o /tmp/zero-windows-sandbox-setup.exe ./cmd/zero-windows-sandbox-setup Tested: GOCACHE=/tmp/zero-go-cache go test ./... Tested: scripts/sandbox-smoke.sh Tested: git diff --check * Stabilize Windows ACL canonical path test * Add TUI sandbox setup command * Gate TUI sandbox setup command * Clarify sandbox doctor setup status * Add Windows WFP network setup Adds a Windows network enforcement plan with stable WFP provider, sublayer, and filter specs for deny-mode sandbox identities. The setup helper now installs or removes those filters transactionally before writing the setup marker, and the marker tracks the network enforcement plan so stale filters require refresh. Scoped Windows network mode remains fail-closed until the proxy exception path is implemented, so it cannot silently become open network. Diagnostics now report native Windows network enforcement when the restricted-token backend is active. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestValidateWindowsNetworkPolicyAllowsNativeModes|TestValidateWindowsNetworkPolicyFailsClosedForScoped|TestBuildWindowsNetworkPlanForDenyUsesCapabilityIdentity|TestBuildWindowsNetworkPlanFailsClosedForScoped|TestWindowsNetworkPlanHashIsStableAcrossEntryOrder|TestWindowsSandboxSetupMarkerRefreshesWhenNetworkChanges|TestSelectBackendChoosesPlatformAdapterWithFallback' Tested: GOCACHE=/tmp/zero-go-cache GOOS=windows GOARCH=amd64 go build -o /tmp/zero-windows-sandbox-setup.exe ./cmd/zero-windows-sandbox-setup Tested: GOCACHE=/tmp/zero-go-cache GOOS=windows GOARCH=amd64 go build -o /tmp/zero-windows-command-runner.exe ./cmd/zero-windows-command-runner Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./internal/cli ./internal/doctor Tested: GOCACHE=/tmp/zero-go-cache go test ./... Tested: GOCACHE=/tmp/zero-go-cache scripts/sandbox-smoke.sh Tested: git diff --check * Add Windows sandbox network smoke Adds a gated Windows real sandbox smoke test that runs the compiled setup helper and command runner, verifies sandboxed writes still work, and proves network=deny blocks a loopback TCP connection from a restricted-token command. The sandbox smoke script now runs this test on Windows using the helper binaries it builds in its temporary directory. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox -run 'TestValidateWindowsNetworkPolicyAllowsNativeModes|TestBuildWindowsNetworkPlanForDenyUsesCapabilityIdentity' Tested: GOCACHE=/tmp/zero-go-cache GOOS=windows GOARCH=amd64 go test -c -o /tmp/zero-windows-sandbox.test ./internal/sandbox Tested: GOCACHE=/tmp/zero-go-cache GOOS=windows GOARCH=amd64 go build -o /tmp/zero-windows-sandbox-setup.exe ./cmd/zero-windows-sandbox-setup Tested: GOCACHE=/tmp/zero-go-cache GOOS=windows GOARCH=amd64 go build -o /tmp/zero-windows-command-runner.exe ./cmd/zero-windows-command-runner Tested: GOCACHE=/tmp/zero-go-cache scripts/sandbox-smoke.sh Tested: GOCACHE=/tmp/zero-go-cache go test ./... Tested: git diff --check * Remove legacy sandbox runner paths Drop the old direct bubblewrap adapter, the standalone zero-seccomp wrapper, legacy adapter metadata, and the Phase 0 legacy inventory now that command planning goes through the sandbox manager backends. Switch platform selection and tests to target backend names: linux-bwrap, macos-seatbelt, windows-restricted-token, and explicit policy-only degradation. Keep policy as the shared permission/profile and diagnostics surface, but stop exposing legacy adapter fields in CLI policy output and tool metadata. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./internal/cli ./internal/tools ./internal/zerocommands ./internal/doctor Tested: GOCACHE=/tmp/zero-go-cache scripts/sandbox-smoke.sh Tested: GOCACHE=/tmp/zero-go-cache go test ./... Tested: git diff --check * Address sandbox baseline review blockers Restore sandbox.blockUnixSockets as a resolved config and policy field, route it into the Linux helper as --block-unix-sockets, and keep zero-seccomp as a Linux compatibility wrapper packaged with release artifacts. Add the missing legacy-entrypoint test referenced by the smoke script, extend helper/release/config tests, and make CLI startup tests hermetic by stubbing MCP registration where they assert quiet output. Document the compatibility path, release helper scope, and the already-resolved #237 prompt-file overlap in the PR body. Tested: GOCACHE=/tmp/zero-go-cache go test ./internal/sandbox ./internal/cli ./internal/config ./internal/release ./cmd/zero-seccomp -run 'TestLegacySandboxEntrypointsAreExplicitAndExist|TestSandboxManagerBuildsCommandPlanThroughLinuxHelper|TestBuildLinuxSandboxCommandArgsSerializesPermissionProfile|TestParseLinuxSandboxHelperArgs|TestBuildLinuxSandboxBwrapArgsWrapsInnerSeccompStage|TestApplyConfiguredSandboxPolicyDiagnosticsFlags|TestResolveSandboxBlockUnixSocketsFromFile|TestCopyPackageFilesStagesLinuxSandboxHelper' Tested: GOCACHE=/tmp/zero-go-cache go test ./... Tested: GOCACHE=/tmp/zero-go-cache scripts/sandbox-smoke.sh Tested: git diff --cached --check Tested: local leak scan found no matches |
||
|
|
c0fedade8d |
feat: free out-of-the-box web (keyless Firecrawl default) + resilient MCP startup (#239)
* fix(mcp): skip an unreachable MCP server at startup instead of aborting RegisterTools connected to every server eagerly and failed the whole batch if any server could not be reached or list its tools — and the caller aborts startup with exit 1 on that error. So a single unreachable MCP server prevented zero from launching at all and disabled every other (working) server. Make registration best-effort across servers but still atomic within a server: each server's tools are validated as a unit before anything commits, and a server that cannot be connected, returns a nameless tool, or conflicts is SKIPPED — recorded on Runtime.Skipped(), not fatal. The caller surfaces each skip as a warning and keeps launching. The "no dead tools" and no-partial-tool-set guarantees are preserved. * feat(mcp): ship keyless Firecrawl as a default web search/scrape server Seed an enabled-by-default Firecrawl MCP server (keyless: 1,000 free credits/month, no API key, no setup) into ResolveMCP before user/project config is merged, so web search and scraping work out of the box for open-source users. A user can override any field (self-hosted URL, bring-your-own-key header) or disable it entirely; `zero mcp disable firecrawl` writes the override even though the default lives in code rather than the config file. The built-in web_fetch tool stays the always-local floor. Firecrawl is AGPL-3.0, but zero only calls it over the network, so the license never reaches zero's own code. README documents disable / self-host / bring-your-own-key / the privacy note that keyless routes through firecrawl.dev. Depends on the resilient-startup change: on an offline machine the default is skipped with a warning, not fatal. * perf(mcp): connect MCP servers concurrently with a per-server startup timeout RegisterTools connected servers serially with a 30s handshake timeout, so startup waited for the SUM of all servers and one slow/unreachable server (e.g. a hosted endpoint blocked by the local network) blocked every other server AND the first model response — often 10-30s of dead time per launch. Connect all servers CONCURRENTLY, each bounded by a per-server ConnectTimeout (default 8s). A server that doesn't connect+list in time is abandoned (its context cancelled to tear down the half-open connection/subprocess) and skipped, so total startup cost is now the slowest REACHABLE server, not the sum. The concurrent phase does only connect+list (no shared state); validation, conflict detection, and registration stay in a deterministic serial phase, so the outcome is identical regardless of completion order. A kept server's context is released at Close — a stdio subprocess is tied to its context, so it must outlive the connect. Pairs with the resilient skip-on-failure behavior already in this branch. |
||
|
|
474f494b93 |
Deep-audit remediation + sandbox hardening (batches 1–35) (#189)
* audit batch 1: redact nested secrets fully; count image tokens in compaction estimate
- secrets.Redact: replace longest matches first so a containing PRIVATE KEY
block is redacted before a nested shorter match corrupts its exact string
(audit medium internal/secrets/scanner.go:81) + regression test.
- agent compaction estimateTokens: add a flat per-image token cost so an
image-heavy context trends toward compaction instead of reading as ~0
(audit medium internal/agent/compaction.go:58) + test.
- ledger: re-verify against current main; mark scanner.go:35 and
redaction.go:362 already-fixed-on-main, record this batch's fixes.
* audit batch 2: worktrees defaultRunGit separates stdout/stderr
Capture git stdout and stderr into separate buffers instead of CombinedOutput,
so parsed rev-parse output is not polluted by git warnings and CommandResult.Stderr
carries the real error text (audit medium internal/worktrees/worktrees.go:247) + test.
* audit: re-verify all 87 pending findings against current main
Independent agent per file re-checked each open/partial finding against the
actual current code (the 06-11 ledger was verified on a since-diverged branch),
with an adversarial second pass on every already_fixed verdict. Authoritative
result: 78 genuinely pending (66 still_open + 12 partial); 9 rows were already
fixed on main and are reclassified. Full table in 2026-06-13-reverification.md.
* audit batch 3 (agent): cancellation identity, always-allow w/o sandbox, compaction usage, dedup
- loop.go: check ctx.Err() before collected.Error so a cancelled run returns
context.Canceled (errors.Is holds) instead of a plain stringified error.
- loop.go: 'Always allow' with no sandbox engine now allows THIS call instead of
denying it (persistence is skipped when there's nowhere to persist).
- loop.go: remove a duplicate schema["items"] assignment in propertyToRuntimeMap.
- compaction.go: forward OnUsage (token accounting) through the summarizer while
still omitting OnText, so compaction's token cost is counted in usage/budgets.
- tests for all three behavioral fixes (each fails pre-fix).
* audit batch 4 (cli): list-tools -o json, interrupted -o json terminal, surface session-record failures
- exec --list-tools -o json now emits a JSON tool listing instead of falling
through to the human-readable text (audit medium exec.go:182) + test.
- interrupted run with -o json now emits a terminal error+done on stdout instead
of only printing to stderr, so a JSON consumer sees a clean end (medium exec.go:500) + test.
- execSessionRecorder: surface a latched session-append failure to stderr once at
run end (deferred) instead of silently dropping it (medium exec_sessions.go:114/115) + test.
- left app.go trailing-arg ignore as-is: documented-intentional, TUI has no argv prompt.
* audit batch 5 (zerogit): status -z porcelain (renames + non-ASCII paths); document env-drop on plain Runner
- parseStatus: switch `git status` to `--porcelain -z` and parse the
NUL-delimited form (audit medium zerogit.go:212). Previously the default
--short output was split on newlines, so a rename `R old -> new` was kept as
one bogus path and a non-ASCII/whitespace filename arrived C-quoted+escaped
(e.g. `"caf\303\251.txt"`). The -z form never quotes paths and emits a
rename/copy as `XY <dest>\0<src>`, so we now record the destination verbatim
and consume the source field. + TestParseStatusZHandlesRenamesAndSpecialPaths.
- resolveRunners: document that the plain-Runner EnvRunner adapter intentionally
drops env (audit low zerogit.go:417). The branch is reached only when a caller
supplies a custom Runner without a RunGitEnv — in practice fake-Runner tests.
Production leaves both nil and gets defaultRunGit/defaultRunGitEnv, which honor
GIT_INDEX_FILE, so snapshot-diff isolation holds. Routing env calls to real git
instead would break the fake-Runner test pattern and fix nothing in production.
- update Inspect/Commit mock tests to the -z status format and the new command.
* audit batch 6 (config): reject negative maxTurns; drop dead contract-gap helpers
- FileConfig.UnmarshalJSON now rejects a negative maxTurns instead of letting the
`MaxTurns > 0` merge gates silently drop it and fall back to the default,
hiding the misconfiguration (audit medium resolver.go:136). 0 is left as the
"unset" sentinel (omitempty) and still falls back to the default; the CLI
--max-turns flag continues to reject 0 too since there it is distinguishable
from absent. + TestResolveRejectsNegativeMaxTurns / ...ZeroFallsBackToDefault.
- remove dead config/contracts.go (ContractGap, DefaultContractGaps,
FindContractGapsByMilestone, ContractOwnerRuntime) and its test — referenced
only by that test, never by production (audit low contracts.go:12).
- NOT changed: ResolveMCP intentionally does not run the provider command
(TestResolveMCPDoesNotRunProviderCommand guards this), so the "ZERO_PROVIDER_COMMAND
MCP servers dropped" row is by-design, not a defect.
* audit batch 7 (mcp): bound captured MCP server stderr
connectStdio attached a plain bytes.Buffer as cmd.Stderr that is only ever read
on initialize failure, yet it kept growing for the entire process lifetime as a
long-lived server logged to stderr (audit medium client.go:98). Replace it with
a concurrency-safe boundedBuffer capped at 64 KiB that keeps the head (enough for
init-error diagnostics) and discards the rest, reporting full write lengths so
os/exec never sees a short write. + TestBoundedBufferCapsRetainedBytes.
Left as-is (not defects worth the risk in the MCP client):
- client.go writer.write under client.mu: already designed to release the lock
before the unbounded response wait; the residual concern is a server that stops
draining stdin, an involved async rewrite for a theoretical deadlock.
- readLoop not answering server-initiated requests (ping): responding safely
needs re-locking around the shared writer; no configured server relies on it.
* audit batch 8 (tools/providers): read_file truncation marker; drop dead provider branch
- read_file: when a max_lines cut occurs, append an explicit "[truncated: N more
line(s)... set start_line=X to continue]" marker to the output. Previously only
the Result.Truncated flag was set, which is invisible in the rendered text, so
the model could not distinguish a cut read from a complete one (audit low
read_file.go:89). + TestReadFileToolMarksTruncation.
- providers/factory: simplify `if !explicitProvider || isImplicitOpenAI(...)` to
`if !explicitProvider` and delete the now-unused isImplicitOpenAI. The clause
was dead: explicitProvider==true implies ProviderKind or Provider is set, but
isImplicitOpenAI required both empty, so it could never contribute a case
(audit low factory.go:118). Behavior-preserving; providers tests still green.
* audit batch 9 (sessions): Tree builds from a single List() snapshot (no N+1 disk scans)
Store.Tree recursed via ListChildren per node, and each ListChildren ran a full
store.List() that re-read every metadata.json from disk — O(nodes * total-sessions)
reads for one tree (audit medium lineage.go:140). Snapshot all sessions once, index
children by parent in memory, and recurse over that. Child ordering is factored into
sortChildSessions so ListChildren and Tree stay identical; path-scoped cycle
detection (delete(seen,...)) is preserved. Existing tree/lineage tests still pass.
Left as-is: store.go timestamp() second precision (RFC3339) — switching to
RFC3339Nano would break lexical UpdatedAt ordering against existing second-precision
records ("...00.5Z" sorts before "...00Z"), so it is not a safe one-line change.
* audit batch 10 (specialist): serialize accounting dedup-then-append
recordSpecialistStop and appendSpecialistUsageRollup checked for an existing event
(specialistEventExists → ReadEvents) and then appended under SEPARATE store locks,
so two concurrent finishers — a foreground onExit racing a TaskOutput poll, or a
background reaper — could both pass the dedup check and write duplicate stop/usage
events (audit medium accounting.go:72). Guard each check-then-append pair with a
package-level accountingMu (Executor is used by value, so a struct field would not
be shared). + TestRecordSpecialistStopDedupesUnderConcurrency (passes under -race).
* audit batch 11 (zeroruntime/gemini): O(1) text accumulation; gemini cached-token counts
- zeroruntime CollectStreamWithOptions accumulated assistant text with
`collected.Text += event.Content`, reallocating the whole string on every chunk
(O(n^2) over a long streamed response). Accumulate in a strings.Builder on the
collector and materialize it once in flush, the single finalization point hit
before every return (audit low helpers.go:104). Existing collect tests still pass.
- gemini decoded usageMetadata without cachedContentTokenCount, so a cached-prompt
response reported zero cached input tokens while the other providers report them
(audit medium gemini/types.go:88). Decode the field and thread it through
streamState into NormalizeUsage as CachedInputTokens. + extended
TestStreamCompletionEmitsTextUsageAndReasoningTokens.
* audit batch 12 (specialist): parse child stream without a line-length cap; drop dead helper
- ParseStream used a bufio.Scanner capped at 1 MiB, so one large stream-json line
from a child (a big tool result or final answer) hit bufio.ErrTooLong and aborted
the entire specialist run (audit medium streamer.go:43). Switch to a bufio.Reader
with ReadString, which has no per-line limit; the child is our own trusted
subprocess, so its output length is the legitimate bound. Line numbering and
blank-line/EOF handling are preserved. + TestParseStreamHandlesLineLargerThan1MiB.
- remove dead formatTaskOutput (output_tool.go:244): zero callers and zero tests;
it only forwarded to formatTaskOutputSummary, which is the one actually used.
* audit batch 13 (background/specialist): kill background tasks as a process group
Background children were launched in the parent's process group and terminated by
signalling only the single PID, so any process the task forked was orphaned and
outlived a cancel; the signal also risked hitting a recycled PID after the child
was reaped (audit medium process_posix.go:38/50).
- ConfigureChildProcessGroup (Setpgid on POSIX, no-op on Windows where taskkill /T
already tree-kills) is applied at launch so the child leads its own group.
- terminateProcess now SIGTERM/SIGKILLs the negative PID (the whole group), with a
pid>1 guard so a bogus 0/1 can never expand to "our own group" or "everything",
and treats ESRCH as already-gone. Liveness polls the group. Targeting a group
(rarely recycled vs. a bare PID) also shrinks the reaped-then-recycled window.
- specialist launchBackgroundProcess opts every child into its own group; on a
SetPID failure it now kills the child + group, marks the task errored, and records
the stop (deduped) instead of leaking an untracked orphan.
- tests launch as group leaders; new TestTerminateProcessKillsForkedChildren proves
a forked grandchild dies with the group.
* audit batch 14 (sandbox): quote-aware shell segment splitting
splitShellSegments used a quote-unaware strings.Replacer, so a shell operator
inside quotes was treated as a separator: `git commit -m "use top | less"` split
into a `less` segment and `echo "a; vim b"` into a `vim` segment, both falsely
flagged as interactive (audit medium safe_command.go:138).
Replace it with a quote-aware scanner that mirrors the shell: single quotes make
everything literal; double quotes keep $(...)/`...` substitution active but treat
|, ;, && as literal; unquoted text splits on the same operator set as before
(;, |, ||, &&) plus substitution boundaries. Real unquoted operators still split,
so an interactive program behind a separator — or inside a live `"$(...)"`
substitution — is still caught (no new false negatives, the security-relevant
direction). + TestDetectInteractiveQuoteAwareSeparators covering both directions.
* audit batch 15 (cron): collapse DST fall-back repeated hour to one fire
On a fall-back day the clock repeats an hour (e.g. 01:30 EDT then 01:30 EST), so
Next("30 1 * * *", <01:30 EDT>) returned the same-day 01:30 EST — firing a daily
job twice (audit medium schedule.go:171, which previously guarded only the
spring-forward gap).
Next now skips a match whose local wall-clock minute equals `after`'s but whose
absolute time is later (the repeated-hour duplicate) and keeps searching. Normal
forward search always advances the wall-clock, so this only triggers on the
fall-back repeat; when the scheduler passes the last fire time as `after`, the
repeated hour collapses to a single fire. + TestNextDSTFallBackDoesNotRepeatHour
(spring-forward termination test still passes).
Note: Next is stateless, so it dedupes relative to `after`; a caller that passes
an arbitrary mid-hour "now" instead of the last fire time is out of scope here.
* audit batch 16 (mcp): skip SSE notifications before the response; larger SSE buffer
- decodeSSERPCMessage returned the first non-empty `message` event unconditionally,
so a server-initiated notification/request arriving before the response made the
caller see an id mismatch and fail (audit medium network_client.go:621). It now
skips any message carrying a method (notifications/requests have one; a response
does not) and returns the first real response; the caller's rpcIDMatches still
validates the id. + TestDecodeSSERPCMessageSkipsNotifications.
- raise the per-event SSE scanner cap from 1 MiB to a bounded 8 MiB so a large but
legitimate MCP message no longer hits bufio.ErrTooLong and fails the request
(audit medium network_client.go:637); still bounded against an unbounded server.
Out of scope (kept): full reconnect of a persistent SSE stream after a fatal read
error — a larger change than this remediation.
* audit batch 17 (sessions): fsync session metadata before the atomic rename
writeMetadata wrote the temp file with os.WriteFile (no sync) and renamed it into
place, so a crash right after the rename could expose a metadata file whose bytes
were never flushed — a torn or empty file that corrupts the whole session (audit
medium store.go:632). Write+fsync the temp via a new writeFileSync helper before
renaming, closing that window. + TestWriteFileSyncRoundTrips.
Deliberately scoped:
- Event-log appends (appendEventLocked) are NOT fsynced per append: that would add
a disk flush to every event (every tool call), and a hard-crash loss of the log's
unsynced tail is acceptable, whereas torn METADATA is not. Metadata is the
integrity-critical part and is what this syncs.
- Directory fsync is omitted for cross-platform simplicity (it errors on Windows);
the file fsync already prevents the torn-contents failure, the serious case.
Left for follow-up (in this bucket but not safe/clear wins here):
- hooks append sequence still re-reads the file per append; the in-memory cache is
cross-process-unsafe and the safe last-line read is fiddly for a low-volume log.
- Fork event re-append/de-dup is nuanced usage-accounting and needs its own change.
* audit batch 18 (agent): count tool-defs in compaction estimate; don't deny an always-allow on persist failure
- maybeCompact's estimate omitted the tool definitions, which ride on every
request, so the real input could exceed the model limit while the message-only
estimate stayed under threshold and compaction never fired (audit medium
compaction.go:58). partitionTools is now computed before maybeCompact (it does
not depend on the messages) and the exposed tools' estimated tokens are added to
both the threshold check and the post-compaction shrink check. + estimateToolDefTokens
and TestEstimateToolDefTokensCountsDefinitions.
- PermissionDecisionAlwaysAllow denied the call when persistPermissionGrant failed,
overriding what the user explicitly allowed (audit medium loop.go:468). Persisting
is now best-effort: a failure (or no sandbox engine) just means the grant is not
remembered and the user is re-prompted next time, not denied now. This also drops
the dead requestEvent.GrantMatched/Grant writes (audit low loop.go:473) — the
emitted event derives them from the sandbox decision via buildPermissionEvent.
Left intentional (documented, not defects): the reactive-retry path omits OnText to
avoid duplicating mid-stream text (loop.go:183); OnContext/MeasureContext is a
complete, tested context-breakdown scaffold awaiting a consumer (types.go:183).
* audit batch 19 (usage): price escalated usage from the event's own model
exec persists payload["model"] on --allow-escalation runs (the model can change
mid-run), but BuildReport priced every event from the session's Metadata.ModelID
and never read the payload, so usage from an escalated model was mis-priced at the
base model (audit medium report.go:17). usageEventPayload now carries Model and
BuildReport prefers it, falling back to the session model when absent (older
events stay byte-identical and behave as before). + TestBuildReportPricesFromEventModelWhenPresent.
* audit batch 20 (sessions): don't copy usage events into a fork (no double-count)
Fork re-appended every parent event, including provider_usage, into the child, so
a usage report aggregating the parent and the fork double-counted the parent's
usage (audit medium store.go:387). Skip EventUsage when copying — it already
counted against the parent and is not part of the conversation history a fork
replays — and record the actual copied count in the fork marker. + TestStoreForkSkipsUsageEvents.
* audit batch 21 (cli/skills): warn on duplicate skill names instead of silently dropping
skills.Load deduplicates same-named skills (first directory wins) and records the
dropped collisions via skills.Duplicates, but nothing surfaced them, so a shadowed
skill just vanished with no signal — and Duplicates had no production caller (audit
low skills.go:91). `skills list` now reports each collision to stderr (keeping
stdout/--json clean), using Duplicates. + TestRunSkillsListWarnsOnDuplicateNames.
Note: skills.Get is kept — it is a by-name accessor used throughout the package's
tests, not dead code.
* audit batch 22 (cli/cron): capture the run error from stream-json stdout
Cron jobs run exec with --output-format stream-json, so a failure is reported as an
`error` event on STDOUT, but the run record's Error was set only from stderr — so a
failed job often recorded an empty/uninformative error while the real message sat
unread in outBuf (audit low cron_run.go:150). On a non-zero exit, scan the stdout
stream-json for the error event's message and prefer it, falling back to stderr.
+ extractStreamJSONError and TestExtractStreamJSONError.
* audit batch 23 (sandbox): richer macOS seatbelt profile (signal + mach-lookup)
The generated sandbox-exec profile was default-deny with only process/sysctl/file
allowances, so two real gaps remained on macOS (sandbox quality report H1):
- A sandboxed script could not signal the children it spawned — `kill` returned
"Operation not permitted" under seatbelt, breaking common `cmd & ... kill`,
test-runner, and timeout patterns. Add `(allow signal (target self) (target pgrp))`,
which stays scoped to the command's own process group.
- XPC to system daemons was denied, so keychain (securityd/trustd), user/group
lookup (opendirectoryd), preferences (cfprefsd), network config
(SystemConfiguration), launch services, and the pasteboard all failed. Add a
curated `(allow mach-lookup ...)` allowlist (`sandboxMachServices`).
Neither touches the file-write or network rules, so the workspace boundary is
unchanged — verified on a real macOS host: `security`/`scutil`/`pbpaste`/self-kill
now succeed while writes to $HOME and /etc are still denied. iokit-open was tried
and dropped as unnecessary. + unit assertions (TestSandboxExecProfileGrantsSignalAndMachLookup)
and extended the darwin integration test with a self-kill case and an
outside-workspace deny check.
* audit batch 24 (sandbox/tools): remove dead OnSandboxDecision hook
RunOptions.OnSandboxDecision had no setter anywhere, yet registry.RunWithOptions
spawned a goroutine (with panic recovery) for it on every sandboxed tool call —
pure per-call overhead for an unreachable callback (sandbox report M1). The sandbox
decision is already surfaced to observers via the permission event
(buildPermissionEvent reads the decision), so this hook is redundant; delete the
field and its call site.
* audit batch 25 (sandbox/cli): exact-path grants — create and revoke by path
The engine already matched ScopeFile grants and DeriveScope already produced them
from path/file tool args, but the CLI could neither create one explicitly nor
revoke a single one (sandbox report L3 / prompt 7). Add:
- GrantStore.RevokePath(tool, path): revokes only the grant whose scope matches the
given file/dir, leaving tool-wide and other-path grants intact (path canonicalized
to absolute like stored scopes). + TestGrantStoreRevokePathRemovesOnlyMatchingScope.
- `zero sandbox grants allow <tool> --path <p>`: persists an exact-file (ScopeFile)
grant, path resolved to absolute so it matches lookups and revokes.
- `zero sandbox grants revoke <tool> --path <p>`: revokes just that grant (default
still revokes all grants for the tool). + TestRunSandboxGrantsCreateAndRevokeByPath.
The TUI permission-card "Allow always (exact path)" option is intentionally left for
the TUI feature branches (it overlaps #187/#188); the engine + CLI now cover the
full create/match/revoke round-trip for file-scoped grants.
* audit batch 26 (sandbox): Engine.Precheck — report violations before execution
Add Engine.Precheck(ctx, request) []Violation, which reports the sandbox
violations that would block a tool request before it runs (sandbox report / prompt 6).
It reuses Evaluate so policy is not duplicated: an allowed or merely-prompted
request yields no violations; a denied request yields its violation (via the shared
violationsFromDecision helper, with a ViolationPolicyDenied fallback for a deny that
carries no structured violation). A nil engine yields nothing.
+ TestEnginePrecheckReportsViolationsBeforeExecution.
Wiring Precheck into the batch-confirmation prompt is intentionally left to the TUI
branch: it touches prompt rendering, and short-circuiting the agent loop's deny path
would skip the before/after-tool hooks that currently fire around a policy-denied
call. The engine API is in place for that consumer.
* audit batch 27 (sandbox): scoped-egress domain prompt with timeout + caching
The scoped-egress proxy failed closed on any host outside the allowlist. Add an
opt-in DomainPrompt callback (prompt 8): an UNKNOWN host (neither allowed nor
explicitly denied) is authorized at request time via the callback instead of being
refused outright. An explicitly denied host is still refused without prompting; the
callback is bounded by PromptTimeout (default 60s, timeout/error => deny); decisions
are cached for the proxy's lifetime so a host is prompted at most once. With no
callback set the strict fail-closed behavior is unchanged. Wired through both the
CONNECT and HTTP handlers via authorizeTarget.
+ TestEgressProxyAuthorizeDomainPrompt / ...TimeoutDenies / ...NilPromptFailsClosed.
The user-facing prompt that supplies the callback is left to the permission UI (TUI)
branch; the egress mechanism (callback + timeout + cache) is in place and tested.
* audit batch 28 (sandbox): AST-based shell analysis (AnalyzeCommand)
Add AnalyzeCommand(script) AnalysisResult, a static shell analyzer built on the
mvdan.cc/sh/v3 parser (prompt 12). Walking the parsed command tree makes it a more
precise second opinion than the regex detector: a program name only counts when it
is an actual command, so `echo "vim ..."`, `printf 'open with vim'`, and
`git commit -m "rm -rf /"` are clean, while real commands are flagged —
interactive (editors/pagers + REPLs, with the same -e/script suppression as
nonInteractiveREPLFlags), destructive (rm -rf/-fr, dd, mkfs, shred, ...), and
network (curl/wget/ssh/nc/...). An unparseable script yields TooComplex so callers
can treat it as higher-risk. + TestAnalyzeCommand (16 cases) / ...EmptyIsClean.
Dependency: mvdan.cc/sh/v3 pinned to v3.11.0 — compatible with the existing go 1.24
directive (the latest v3.13.1 would have bumped the module to go 1.25). It is kept
as a standalone analyzer for callers to consult; feeding it into Engine.Evaluate is
deliberately not done here, since that would change allow/deny classification.
* audit batch 29 (sandbox): unify proxy-env generation (ProxyEnv)
Export ProxyEnv as the single source of truth for proxy-env injection (prompt 9
req 1), replacing the unexported scopedProxyEnv used by the sandboxed-shell path.
+ TestProxyEnv.
Investigation that bounds the rest of prompt 9 (documented in the ProxyEnv doc):
- web_fetch already honors HTTP_PROXY/HTTPS_PROXY — its transport clones
http.DefaultTransport, whose Proxy is http.ProxyFromEnvironment.
- MCP children already inherit the parent env (mergeProcessEnv appends os.Environ),
so they pick up any proxy vars the agent has.
So the only missing link to scope FetchUrl/MCP is a SESSION-level egress proxy whose
address reaches the agent process. That is deliberately NOT wired here: doing it
safely requires allowlisting the active LLM provider's own domain first, or the
agent's provider calls would route through the scoped proxy and be denied — a
correctness pitfall that also can't be verified end-to-end on this host. Flagged
rather than shipped blind.
* audit batch 30 (sandbox): Linux seccomp Unix-socket block — UNVERIFIED, needs Linux CI
Adds a pure-Go (no cgo, no vendored binary) seccomp BPF filter that denies
socket(AF_UNIX) with EPERM on x86-64/arm64, closing the Unix-socket gap bubblewrap's
filesystem/network isolation leaves open (prompt 11):
- internal/sandbox/seccomp.go: the classic-BPF program as a platform-neutral
[]sockFilter, built and UNIT-TESTED (structure + every jump offset in range — the
classic way a hand-written BPF filter goes silently wrong).
- seccomp_linux.go: ApplyUnixSocketBlock installs it via prctl(NO_NEW_PRIVS) +
prctl(SET_SECCOMP, MODE_FILTER). seccomp_other.go is a no-op everywhere else.
- cmd/zero-seccomp: a tiny linux-only exec wrapper that applies the filter then
execs the real command (the standard way to install seccomp in a child with
os/exec); degrades gracefully (warns + runs) if the kernel lacks seccomp.
⚠️ HONEST CAVEAT: I developed this on macOS and CANNOT run the actual AF_UNIX block
here. darwin build, `GOOS=linux` cross-compile, vet, and the structure test all pass,
but the unit test verifies the program's SHAPE, not that the kernel actually blocks
AF_UNIX — that MUST be verified on Linux CI (create a Unix socket under the wrapper
and assert EPERM). Safe to ship as-is: it is linux-only, no-op on every other
platform, and not wired into the bwrap command yet (opt-in prefixing the sandboxed
command with zero-seccomp is the remaining integration), so it cannot regress any
current path.
* audit batch 31 (sandbox): macOS denial log monitor (opt-in, default off)
Re-adds the macOS seatbelt-denial monitor as an opt-in feature (prompt 10). When
Policy.MonitorDenials is set, the sandbox-exec profile tags its default-deny with a
marker and bash tails `log stream` for that tag during the command, appending any
captured denials as a <sandbox_violations> block to the command's stderr so the
model can see what was blocked.
- DenialMonitor (darwin) tails/parses tagged denials with a capped, deduped ring
buffer and noise filtering; no-op stub on other platforms. parseSandboxDenyLine is
unit-tested. + profile-tag test + appendSandboxViolations test.
- Default OFF: with MonitorTag empty, StartDenialMonitor is a no-op and the command
path is byte-for-byte unchanged, so there is no regression or per-command overhead
unless enabled.
CAVEAT: on at least one macOS host (verified live) seatbelt denials are NOT delivered
to the `log stream`-queryable unified log, so the monitor captures nothing there. It
is the standard mechanism and works on macOS versions that do log denials; shipping
it default-off means it never affects whether a command runs.
* audit batch 32 (sandbox): wire seccomp Unix-socket block into bubblewrap (opt-in)
Completes prompt 11: the zero-seccomp helper (which installs the AF_UNIX seccomp
filter) is now actually invoked by the sandbox instead of only existing as a
standalone binary.
- bubblewrapCommandPlan, when Policy.BlockUnixSockets is set, ro-binds the
discovered zero-seccomp helper into the sandbox at its host path and prefixes the
command with it so the filter is installed before the real argv runs.
- findSeccompHelper discovers the helper next to the running executable first, then
on PATH; it returns "" when absent, so the sandbox degrades gracefully (runs
without the extra filter — bubblewrap's fs/net isolation still applies) rather
than failing the command. Exposed as a package var so the wiring is unit-tested
on any OS.
- Reachable from config: SandboxConfig.BlockUnixSockets and .MonitorDenials now map
onto the policy in applyConfiguredSandboxPolicy (opt-in only — config can turn a
hardening flag on, never off). Both default OFF.
- Tests: helper-prefix present when enabled / absent by default; config flags apply
independently and stay off when omitted.
Default OFF and Linux-only effect; the runtime seccomp enforcement still needs a
Linux host/CI to exercise end-to-end (the wiring and BPF program are unit-tested).
* audit batch 33 (sandbox): enforce scoped/deny network policy in web_fetch & web_search
Completes prompt 9: the built-in network tools now honor the SAME allow/deny/scoped
policy the sandboxed shell egress proxy enforces, instead of bypassing it. Before
this, web_fetch/web_search would reach any public host even when the policy was
NetworkDeny or NetworkScoped — the egress proxy only constrained shell commands.
- New shared gate Engine.NetworkHostAllowed(host) reuses the egress domain matcher
(domainAllowed/normalizeDomains/effectiveNetwork) as the single source of truth:
allow → all hosts; deny → none; scoped → only AllowedDomains minus DeniedDomains
(empty allowlist collapses to deny). Disabled policy / nil engine impose nothing.
- web_fetch is now sandbox-aware: the host is checked before the request AND on every
redirect, so a denied/unlisted host fails closed before any dial. Plumbed via a
networkPolicyGate so the no-sandbox path is byte-for-byte unchanged.
- web_search is sandbox-aware: the configured backend's endpoint host is checked
before the query leaves the machine; blocked under deny, and under scoped unless the
backend host is allowlisted.
- The registry already routes sandbox-aware tools through RunWithSandbox when an
engine is present, so this activates automatically wherever the sandbox is wired.
- Tests: engine gate matrix (deny/allow/scoped/denylist/empty/disabled/nil + host:port);
web_fetch & web_search block denied/unlisted hosts without dialing and allow listed
ones; endpoint-host parsing.
* audit batch 34 (sandbox): prove network-policy wiring end-to-end + document opt-in flags
Verification follow-up to batches 31-33; no behaviour change.
- Adds a registry-level test that exercises the FULL chain for the scoped-network
enforcement: registry.RunWithOptions -> engine.Evaluate (passes a scoped,
egress-capable sandbox-exec backend) -> routes web_fetch through RunWithSandbox ->
per-host allowlist blocks an unlisted host before any dial. This proves the
Tool-interface value actually satisfies sandboxAwareTool at the registry boundary,
not just in a direct method call.
- Documents the two opt-in hardening flags (sandbox.blockUnixSockets,
sandbox.monitorDenials) and the scoped web_fetch/web_search behaviour in the README
so the config keys are discoverable. Both remain off by default.
Confirmed wiring: agent loop forwards options.Sandbox to the registry; the registry
routes sandbox-aware tools (web_fetch, web_search) through RunWithSandbox when an
engine is present; MonitorTag is set only on the macOS sandbox-exec plan; the
seccomp helper prefix is added only on the bubblewrap plan when BlockUnixSockets is
set; both config flags map onto the policy via applyConfiguredSandboxPolicy.
* audit batch 35 (sessions): fix Windows CI failure in TestWriteFileSyncRoundTrips
The exact-permission assertion (perm == 0o600) added in batch 17 fails on
windows-latest because Windows does not honor Unix permission bits: Go reports
0o666 for a writable file regardless of the mode passed to OpenFile. This was the
sole cause of the failing Smoke (windows-latest) CI job; macOS and Ubuntu passed.
Guard the perm check with runtime.GOOS != "windows" so it still verifies the mode
on the platforms that enforce it, while the round-trip (write + read-back) is still
asserted on every platform.
* audit batch 36: address CodeRabbit review (5 findings)
- background/process_posix: after SIGKILL, poll until the process group is
actually gone (SIGKILL is async) and return an error if it survives past a
second deadline, instead of returning nil while descendants still race.
- sandbox/safe_command: only treat ')' as a substitution boundary when it closes
an active $(...). A literal ')' (e.g. echo "a) less") no longer splits into a
fake `less` segment and is no longer misflagged as interactive. Depth-tracked so
nested $(...) still isolate their inner command. Direct splitShellSegments test
(fails pre-fix: yields ["echo \"a", "less\""]).
- cron/schedule: the DST fall-back collapse now only applies when `after` sits
exactly on the minute boundary (its "last fire time" form). With sub-minute
precision (e.g. 01:30:30 EDT) the first 01:30 fire already passed, so the repeated
01:30 EST is the legitimate next fire and is returned — restoring strictly-after
semantics. New test fails pre-fix (skipped to next day).
- config/types: maxTurns error message now reads "must be >= 0" to match the check
(0 is accepted; only < 0 is rejected). Negative-maxTurns test asserts the message.
- docs/audit: header date/branch metadata now matches the body (2026-06-13,
fix/audit-batch) instead of the stale 2026-06-11/fix/audit-critical-high snapshot.
Verified: gofmt clean; go vet clean; host + linux + windows builds; affected suites
green. Each behavioural fix ships with a test confirmed to fail without the change.
* audit batch 37: address CodeRabbit re-review (12 findings)
Findings on the audit code from batches 21-36; each behavioural fix ships with a
regression test (several confirmed to fail without the change).
sandbox/analyzer (AnalyzeCommand):
- Unwrap launcher prefixes (sudo/env/nice/bash -c/...) and recurse into `sh -c`
payloads so `sudo rm -rf`, `env curl …`, `bash -c 'vim x'` classify on the real
command, not the launcher. Mirrors DetectInteractiveCommand.
- Honor rm's `--` end-of-options marker so `rm -- -rf` (delete a file named -rf) is
not flagged destructive.
sandbox/engine: NetworkHostAllowed now shares one backend-aware effective-mode
helper with Evaluate, so scoped collapses to deny when the backend can't enforce
scoped egress — the per-tool gate can no longer permit traffic the engine-level
decision would fail closed.
sandbox/egress: cache domain-prompt decisions by host:port, not host, so one
"allow" can't authorize other ports on the same host.
sandbox/safe_command: the segment splitter is now escape-aware (\| , \" ) so an
escaped operator/quote can't manufacture a fake interactive segment.
sandbox/log_monitor (darwin): Wait() the `log stream` child after draining stdout
so the cancelled process isn't leaked as a zombie.
tools/web_search: confine the search backend to same-host redirects (with a limit)
so a redirect chain can't egress to a host the scoped/deny policy never authorized.
cli/sandbox: an explicit empty --path (`--path=` / `--path ""`) now fails closed
instead of silently widening an allow to tool-wide or a revoke to all-grants.
cli/skills: warn on stderr when duplicate-skill detection fails instead of
swallowing the error.
specialist/exec: don't mask a failed orphan-kill — join the TerminateProcess error
with the SetPID error so a surviving process is visible.
background/process_posix: resolve the PGID and only group-signal when pid leads its
own group; otherwise signal the individual PID. Avoids both a false-success on a
non-leader ESRCH and the hazard of signalling an unrelated (possibly our own) group.
Verified: gofmt + go vet clean; host + linux + windows builds; full go test ./...
green.
* audit batch 38: address CodeRabbit re-review round 2 (3 findings)
Follow-ups to batch 37, each with a regression test:
- safe_command (splitter): a ')' is now only a substitution boundary when it
closes an active, UNQUOTED $(...). A quote-state stack saved at each '$(' lets a
substitution run in a fresh quoting context and restores the outer quoting on
its matching ')'. Fixes `echo $(printf "a) less")` splitting into a fake `less`
segment, while still isolating the inner command of `echo "$(vim x)"`.
- egress: deduplicate in-flight domain prompts per host:port. Concurrent requests
for the same unknown target now wait on the first prompt and reuse its cached
decision instead of each firing a prompt (a prompt storm) and racing to write
the final decision. Race-tested with 25 concurrent authorizers → one prompt.
- analyzer + safe_command: wrapper option-value consumption is now wrapper-specific
(wrapperValueOptionsByProg) instead of a global option set. A valueless flag like
`sudo -n` no longer swallows the real payload, so `sudo -n rm -rf` /
`sudo -n curl …` are correctly classified destructive/network; `sudo -u root vim`
still consumes its value. Shared by the AST analyzer and the regex detector so
they can't diverge.
Verified: gofmt + go vet clean; host + linux + windows builds; go test ./... and
-race on sandbox/background all green.
* audit batch 39: address CodeRabbit re-review round 3 (12 findings)
All verified against current code; each behavioural fix has a regression test.
Security / correctness:
- tools/web_search: redirect policy now also refuses a scheme change, blocking a
same-host https→http downgrade that would send the query + bearer token over
plaintext.
- sandbox/safe_command: a ')' is only a substitution boundary when it closes an
active UNQUOTED $(...), and backticks now get the same quote-state save/restore
as $( — so `echo "`a | less`"` and `echo "$(true | less)"` no longer hide the
inner pager from interactive detection.
- sandbox/analyzer + safe_command: wrapper option-value consumption stays
wrapper-specific, and unwrapping no longer aborts on a dynamic ($x) wrapper arg
once a wrapper is active, so `env "$opts" curl` / `sudo "$x" rm -rf` are still
classified.
- sandbox/egress: in-flight prompts deduped per host:port (carried from batch 38);
the prompt callback is now cancellable (context cancelled on timeout) so a stuck
prompt can abort instead of leaking; signature threaded through.
- sandbox/runner: per-plan unique denial tag (pid+counter) so concurrent monitored
sandbox-exec runs can't ingest each other's denials; helper selection now
requires an executable regular file so a non-exec sibling degrades gracefully.
- mcp/network_client: aggregate SSE event-size cap (not just per-line) so a server
can't grow memory with many unterminated data: lines.
- specialist/accounting: a previously-recorded stop/usage with an empty runId is a
catch-all and now suppresses a later runId-bearing duplicate.
- sessions/lineage: Tree fetches the root via Get() first so a corrupt root
surfaces its real error instead of degrading to "not found".
Docs/tests:
- background/terminate: docstring now states the leader-only group-kill (PID-only
fallback for non-leaders); test child-liveness probe uses ESRCH specifically.
- cli/sandbox: grant usage strings include --path.
Verified: gofmt + go vet clean; host + linux + windows builds; go test ./... and
-race on sandbox/background all green.
* audit batch 40: address CodeRabbit re-review round 4 (8 fixed, 1 skipped)
Verified each against current code.
Fixed:
- sessions/lineage: PANIC FIX — store.Get returns (nil, nil) for a missing
session, so the batch-39 root prefetch could nil-deref. Guard root == nil and
return a not-found error. Regression test added (was a panic).
- agent/compaction: the reactive recover() now records lowWaterMark in the SAME
combined (messages + tool-defs) token domain maybeCompact uses, so the proactive
shrink-guard compares like with like. recover() takes tools; callers pass
request.Tools.
- mcp/network_client: SSE aggregate counter no longer counts a joining newline for
the first data line (only when prior content exists), avoiding premature rejects.
- sessions/store: fsync the parent directory after the metadata rename so the
rename is durable across a crash (no-op on Windows, which can't fsync a dir).
- sandbox/egress: parse the target port with strconv.Atoi instead of
net.LookupPort (no context-unaware resolver use).
- sandbox/runner: escape the per-plan denial tag through sandboxProfileString when
embedding it in the seatbelt profile.
- sandbox/seccomp_linux: guard against an empty filter program before taking
&kernelFilters[0] (defensive; the program is fixed non-empty today).
- cli/cron_run_test: assert last-error-wins with two error events.
- background/terminate, cli/sandbox usage, process_posix_test ESRCH (carried).
Skipped (with reason):
- zerogit worktree-rename (Y='R') test: git porcelain v1 -z only emits the
rename-source field when the INDEX column is R/C; the worktree column never
carries 'R' with a source field in v1, so the fixture would test output git
doesn't produce, and making parseStatus consume on code[1]=='R' would risk
mis-consuming the following entry for a plain ` R` status. Left as-is.
Verified: gofmt + go vet clean; host + linux + windows builds; go test ./... and
-race on sandbox/background all green.
* audit batch 41: address CodeRabbit re-review round 5 (4 findings)
All verified against current code; code fixes have regression tests.
- agent/compaction: summarizeWithFallback's reduce pass no longer swallows ALL
errors. A context-limit error still falls back to the joined partial summaries
(documented "extreme" case), but a non-context failure (auth/network/provider)
now surfaces unchanged, per the function's contract. Two tests (propagate vs.
joined-fallback); the propagate test fails without the fix.
- sandbox/safe_command + analyzer: wrapper value-consuming options now include the
long spellings (sudo --user root, env --unset NAME, timeout --signal …), so a
long flag's value is no longer mis-read as the program (interactive/destructive/
network detection bypass). A `--flag=value` token carries its own value and never
consumes the next token. Tests for space and = forms in both the AST analyzer and
the regex detector.
- docs/audit: the status-ledger summary table's open/partial cells (64/14) were the
lone outlier — the ledger's own narrative and 2026-06-13-reverification.md both
state 66/12. Corrected to 66/12 (66+12+97 = 175); derivation: body 69 open + 18
partial minus the 9 already-fixed-on-main (3 open + 6 partial) = 66/12 = 78
pending, fixed 88+9 = 97. Added a comment documenting the summary-vs-body
relationship. The reverification doc already states 66/12, so both now agree.
Verified: gofmt + go vet clean; host + linux + windows builds; full go test ./...
green.
* audit batch 42: address CodeRabbit re-review round 6 (1 fixed, 1 kept w/ proof)
CodeRabbit re-surfaced two "duplicate" refinements; CI was already green (its 6th
pass was a COMMENT with no new findings).
- cli/sandbox_test (TestRunSandboxGrantsRejectsEmptyPath): now seeds a tool-wide
grant and asserts grant-store immutability after EACH rejected empty-`--path`
call (reflect.DeepEqual before/after), so a mutation in one call can't be masked
by a later one — and a buggy "revoke all" is actually observable (it's a no-op on
an empty store). Stronger than the suggested patch.
- zerogit.parseStatus: KEPT consuming the rename source on the index column only
(code[0]), with an explanatory comment. Verified empirically that git porcelain
v1 -z reports a rename/copy (and emits the extra source field) only in the index
column: `git mv` → "R new\0old\0"; a worktree-only rename → " D old\0?? new\0"
(delete + untracked, never "R" in code[1]). Consuming on code[1]=='R'/'C' would
match no real git output and would only risk mis-consuming the next entry on
malformed input, so the suggestion is declined as incorrect for v1.
Verified: gofmt + go vet clean; host + linux + windows builds; affected suites green.
|
||
|
|
e381d8eab1 |
Additional write roots: --add-dir flag, global config key, /add-dir TUI command (#162)
* docs: spec for additional write roots (--add-dir / /add-dir) Co-Authored-By: Claude Fable 5 <[email protected]> * docs: implementation plan for additional write roots Co-Authored-By: Claude Fable 5 <[email protected]> * sandbox: add Scope type for multi-root write access Co-Authored-By: Claude Fable 5 <[email protected]> * docs: write roots honored from global config only, union merge semantics Co-Authored-By: Claude Fable 5 <[email protected]> * sandbox: fix multi-root violation reporting and pin prefix-alias behavior - validate: prefer ViolationSymlinkTraversal over ViolationOutsideWorkspace when all roots deny; return original requestedPath in violation; append --add-dir hint only on outside_workspace results; extend doc comment to state that a symlink resolving inside any root is allowed. - normalizePrefixForRoot: dedupe insideRoot check via pathWithinRoot (Fix 3); widen EvalSymlinks comment to cover jump-over case; add POSIX-only note. - WorkspaceRoot: add doc comment noting immutability/lock safety (Fix 4). - Tests: pin multi-root traversal-preferred behavior, deterministic alias test (TestValidateResolvesAliasedPathPrefixes), Add symlink normalization test, and tilde-expansion error path test. Co-Authored-By: Claude Fable 5 <[email protected]> * sandbox: make engine path validation and risk scope-aware Thread *Scope through EngineOptions/Engine so Evaluate and Classify use multi-root scope validation instead of single-root validateWorkspacePaths, letting extra write roots (--add-dir / /add-dir) be honoured by the policy gate and risk classifier. Remove now-dead validateWorkspacePaths helper. Co-Authored-By: Claude Fable 5 <[email protected]> * sandbox: pin override-root scope isolation, tidy Evaluate scope handling Co-Authored-By: Claude Fable 5 <[email protected]> * sandbox: widen seatbelt/bubblewrap profiles and command cwd to scope roots Co-Authored-By: Claude Fable 5 <[email protected]> * sandbox: pin extra-root chdir behavior and document bind invariants Co-Authored-By: Claude Fable 5 <[email protected]> * tools: resolve paths against the shared write scope Add PathScope interface and scoped resolver helpers to workspace.go, then thread a scope field through all 8 file tools (read_file, list_directory, glob, grep, write_file, edit_file, apply_patch, bash). Each existing constructor delegates to a new NewScoped* variant; nil scope is byte-identical to the original behavior. Registry gains CoreReadOnlyToolsScoped, CoreWriteToolsScoped, CoreShellToolsScoped, and CoreToolsScoped. Co-Authored-By: Claude Fable 5 <[email protected]> * tools: restore fail-closed write-target symlink checks, share prefix normalization * tools: report absolute paths for extra-root changes, pin scoped traversal denial - resolveScopedPath and resolveScopedTargetPath now return the absolute path as the second value when the matched root is an extra (non-workspace) root, eliminating ambiguity in ChangedFiles, cwd meta, and display summaries downstream - Add TestScopedWriteRefusesSameRootSymlinkTraversal to pin that same-root symlink traversal within an extra root is denied (pre-existing behavior) - Add TestScopedWriteReportsAbsolutePathForExtraRoot to enforce the new absolute-path contract for ChangedFiles on extra-root writes - Extend doc comments on PathScope, ChangedFiles, changedFilesFromPatch, and both scoped resolvers to capture the workspace-vs-absolute invariant Co-Authored-By: Claude Fable 5 <[email protected]> * config: add sandbox.additionalWriteRoots (global config only, union merge) * cli: add repeatable --add-dir flag wiring scope into registry and engine Co-Authored-By: Claude Fable 5 <[email protected]> * cli: forward --add-dir across dispatch paths, pin scope wiring with tests Co-Authored-By: Claude Fable 5 <[email protected]> * cli: pin --add-dir dispatch forwarding end-to-end, close guard gaps Review fixes for the --add-dir dispatch commit: - Add TestRunAddDirDispatchForwardsGrantIntoExecScope: drives runWithDeps through both the "exec" and "-p" dispatch shapes with a provider that calls write_file inside the extra root, asserting the grant reaches the exec sandbox scope (and a negative control without --add-dir is denied fail-closed). Mutation-verified: dropping addDirFlagArgs forwarding from either case fails the test. - Remove help/version from the --add-dir allowlist so any non-TUI/ non-exec leading --add-dir hard-errors, matching the stated requirement; extend the rejection test with those cases. - Fail loud when --add-dir is hidden behind a stray non-flag arg on the --skip-permissions-unsafe path instead of silently dropping the grant. - Reject flag-like values in the inline --add-dir= spelling to match the space form (a directory named -foo stays reachable as ./-foo). - Move TestExecScopeReRegistrationSwapsCoreToolsByName out of the parse test file into the new exec_scope_test.go alongside the e2e test. Co-Authored-By: Claude Fable 5 <[email protected]> * tui: add /add-dir command for session write-root grants Co-Authored-By: Claude Fable 5 <[email protected]> * sandbox: surface write roots in zero sandbox status and plan snapshots Co-Authored-By: Claude Fable 5 <[email protected]> * cli: surface write-roots error in --effective --json, normalize fallback root Review fixes for the Task8 observability range: - runSandboxPolicyEffective JSON payload gains writeRootsError (omitempty) so --json consumers see the same fail-soft signal as the text write_roots_error line instead of a misleading workspace-only writeRoots list. - The fail-soft fallback now derives its write roots from a workspace-only Scope (which cannot fail) so the error path renders the same symlink-resolved root as the success path. - TestRunSandboxPolicyEffectiveWriteRootsFailSoft gains a --json leg asserting writeRootsError names the stale root with a workspace-only fallback; the configured-roots test asserts the key is absent for valid roots. - SandboxPlanSnapshot doc comment reworded to match the converter: no current builder populates WriteRoots. Co-Authored-By: Claude Fable 5 <[email protected]> * sandbox: seatbelt integration test for extra write roots Co-Authored-By: Claude Fable 5 <[email protected]> * docs: document --add-dir and /add-dir write-root grants Co-Authored-By: Claude Fable 5 <[email protected]> * sandbox: pass volume-qualified paths through prefix normalization verbatim NormalizePrefixForRoot's POSIX component walk mangled Windows drive paths (C:\Users -> C:Users), which downstream single-root checks treated as relative and joined under the workspace — failing the policy gate OPEN on Windows. Volume-qualified paths now bypass the walk entirely (the macOS /var alias problem it solves has no Windows equivalent) and windows-gated tests pin both the verbatim pass-through and the engine denial. Co-Authored-By: Claude Fable 5 <[email protected]> * sandbox: make prefix normalization volume-aware for Windows alias resolution The previous commit disabled NormalizePrefixForRoot on Windows, which broke it the other direction: a workspace created under an 8.3 short path (C:\Users\RUNNER~1\...) resolves via EvalSymlinks to its long form (runneradmin), so raw short-form requests escaped the long-form root and legitimate extra-root writes were denied. This is the same alias problem as macOS /var -> /private/var, which the helper already solves. Start the component walk from the volume root (C:\ or //host/share/) instead of "/" so it resolves the prefix on Windows too; POSIX VolumeName is empty so the walk reduces to the original "/"-rooted behavior byte-for-byte. Windows test now pins allow-inside-root + deny-outside both directions. Co-Authored-By: Claude Fable 5 <[email protected]> * sandbox: address PR review findings for additional write roots Resolve the CHANGES_REQUESTED review on PR #162: - engine: derive workspaceRoot from scope.Roots()[0] when NewEngine is given a Scope without an explicit WorkspaceRoot, so Evaluate's EnforceWorkspace/classification guards no longer silently skip (+ regression test). - tools/workspace: scopedRoots now fails closed (returns an error) when a non-nil PathScope exposes empty Roots(), instead of returning success with an empty path; threaded through the scoped resolve/recheck helpers and resolveGrepRoot. - tools/glob: emit absolute matches when cwd resolves into an extra root so results can't be fed back and resolve to a same-named workspace file (+ regression test). - tools/bash,grep,apply_patch: schema descriptions mention granted extra roots (relative -> workspace, absolute -> granted root). - docs/plan: fix markdownlint heading increment, malformed rule+heading, and bare code fence. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * tools/grep, tui: address PR review for additional write roots - grep now emits absolute, symlink-resolved paths for matches in a granted extra (non-workspace) root, mirroring the glob fix. A bare workspace-relative name like "report.go" otherwise resolves back under the workspace when fed to read_file/edit_file and hits the wrong file when the name exists in both roots. Covers content and files_with_matches output. - tui: only reset chatScrollOffset for a real submission, not an empty Enter (a no-op), so the viewport is no longer yanked to the bottom when nothing was submitted. Real submissions still snap back. - glob: schema description now mentions granted extra roots, matching the bash/grep/apply_patch wording so the --add-dir/-add-dir flow is discoverable. Regression tests: TestScopedGrepReturnsAbsoluteMatchesForExtraRoot, TestEmptySubmitKeepsChatScrollOffset. * docs: add /add-dir to the TUI commands quick-reference table The command was documented further down but missing from the scannable commands table, so users browsing the table wouldn't discover the additional-write-roots flow. --------- Co-authored-by: Claude Fable 5 <[email protected]> |
||
|
|
06a49f6557 |
docs: overhaul README to match current product surface (#164)
Rewrite the README around what main actually ships today: - ANSI Shadow wordmark matching the TUI splash, centered header + badges - 25+ provider catalog (frontier, fast-inference, local, compatible endpoints) - new features: spec mode, mid-run model escalation, cron, repo-map/repo-info, workspace index, context report, image input, deferred tool loading, notifications - full command reference (25 subcommands), exec flag summary, TUI slash commands - updated tools table (web_fetch, ask_user, skill, tool_search, escalate_model) - refreshed architecture diagram and project layout for the 47-package tree |
||
|
|
6fb0231df8 |
TUI scrollback re-architecture, Lime chat surface, and deep-audit fixes (#158)
* docs(design): add Lime TUI prototype and rebuild spec
* tui: remove the zeroline skin system
One design remains in the tree ahead of the Lime rebuild: the zeroline
package, its TUI view/skin plumbing (Options.Skin/ThemeVariant/ThemeDark,
model skin branches, theme picker), and the zeroline CLI subcommand are
gone. /theme keeps its shell-only message.
* tui: replace the cyan palette with the Lime token table
theme.go is rebuilt around the Lime design tokens (docs/design/
zero_tui_lime.html): near-black panels, one lime accent, solid tint
stand-ins for the prototype's rgba overlays. Existing render sites map
onto the new role styles (ink/line/userPrompt/toolName/toolArg) without
layout changes; no hex exists outside theme.go.
* tui: build the Lime chat-surface components
Title bar with brand badge + context window, empty state replacing the
deleted splash (block-art 0, tagline, 1-3 starter suggestions), stream
blocks (user prompt gutter, muted interim with cursor, final-answer
accent rail, done line, bordered notes), tool cards with diff/read/
bash/grep bodies and MiniDot run spinner, borderless composer with
run-state hints, and the grouped status line. Final answers are marked
at append time; resolved tool calls collapse into their result cards.
* tui: fix Stage 2 review findings
Adversarial review of the chat-surface commit confirmed 15 issues, all
fixed here:
- wrapPlainText sliced wide runes by rune count: CJK/emoji prose could
panic the TUI or emit lines ~2x the measure; split at width instead
- a cancelled run's streamingText leaked into the next turn's interim
block; cancelRun now clears it
- orphaned tool-call cards (cancelled/errored turns) re-animated with
the spinner on any later run; the glyph is now scoped to the call's
own runID
- starter-suggestion digit capture was active while the chips were off
screen (/clear mid-run); the guard now mirrors the render condition
- prompt submits while a deferred Ctrl+C quit is draining could start a
run the quit would orphan; submission is blocked while exiting
- the running placeholder advertised ctrl+c (which quits) as the
interrupt key; it now says esc
- read-card parser expected 'N: text' but read_file emits 'N | text',
so the gutter and L-range never rendered on real output
- toolCard borders were one column short of the body rows
- diff tint bands now span the full row; NEW FILE renders green on
addBg; preamble/no-newline lines are unnumbered meta and no longer
shift hunk numbering
- card bodies and system notes paint the panel surface; the head gains
the separate faintest arg column (grep target + pattern)
- title-bar overflow now truncates the minimal candidate instead of
rendering the deleted design's 'ZERO … READY' line
- untracked the stray snake-game.html (left on disk, ignored)
* tui: restyle the interactive surfaces for Lime
Permission prompts render as the amber permission card (PERMISSION
badge, risk readout, key chips); resolved prompts collapse to one line
(allowed once/always/denied · tool) and unprompted allows fold into the
tool card's [auto] tag. Ask-user and spec-review prompts share the card
language with line borders. Autocomplete and picker overlays move onto
the panel/selection tints with the accent ❯ marker; model-picker rows
gain the provider dot and ctx · KEY_ENV metadata from the registry and
provider catalog. /resume renders its list as stacked session cards
(id + age, title, meta). Key handling is untouched everywhere.
* tui: adaptive width tiers, doc updates, final polish
Width tiers re-evaluate from the live width: ≥100 renders the full
spec; 80–99 drops the tool-arg column, header ctx, and the status
'interactive' group; 58–79 drops the diff/read gutters, bares the
badge, and keeps provider+tokens+mode; <58 renders a single-segment
header, rail-less cards, and a mode-only status line. Table-driven
tests cover {58,70,80,100,120} plus a frame-wide invariant that no
emitted line exceeds the terminal at any tier. Card bodies expand tabs
so width math holds. Dead splash-chip machinery (startupCommandNames,
startupOrder) is gone; README's TUI section now describes Lime.
* tui: fix final-review findings across Stages 3-4
A second adversarial review confirmed 14 issues, all addressed:
- permission collapse and tool-card pairing are now run-scoped (rcKey):
providers with repeating synthetic ToolCallIDs (Gemini gemini_tool_N)
could attribute a decision or result to a different run's call
- rehydrated always-grants no longer mislabel as 'allowed once'
(GrantMatched fallback)
- the width invariant now actually holds everywhere: empty-state
tagline/hint, ask-user rows, permission detail blobs, pending image
chips, and the sessions trailing hint are wrapped or fitted; the
invariant test renders the empty state and all of those rows at
widths 24-120
- every card (permission, ask-user, spec-review, sessions, notes)
drops side borders on tiny terminals, not just tool cards
- /resume card fields are sanitized against separator bytes from
user-controlled titles
- the picker's selected row paints its gap so the selBg band is solid
- dead splash helpers (normalizedStartupWidth, countLines, nonEmpty,
unreachable rowWelcome branch) and stale /theme comments are gone
- README no longer claims transcript scrolling keys or Tab-accept
behavior that never existed; chatWidth documents its 24-col floor
Tag tui-lime-s4 moves to this commit.
* audit: deep-audit report, TUI scrollback re-architecture, and cross-module fixes
TUI: settled-row flush frontier prints history to native terminal
scrollback (tea.Println, ordered + ack-serialized); View() renders only
the live tail. Full diffs (400-line flush cap) and OSC 8 file:// links
land in history; streamed interim text, cancellation markers, ask_user
exchanges, and composer input history are preserved; run-scoped dedup
keys fix Gemini's repeating tool-call ids; /exit, Ctrl+C-after-Esc, and
cross-session late flushes no longer orphan or contaminate checkpoints.
Cross-module: zero search panic + offset mapping, usage report
robustness, 529 retry, SSE keep-alive watchdog, registry corrections,
update --check on dev builds, MCP ping + mcp list --json secret
redaction, sandbox.network config knob, doctor apiKey presence, atomic
config writes, C1-safe notifications, rune-safe truncation everywhere,
gofmt + vet CI gates, dead glamour dependency dropped.
Full audit report: docs/audit/2026-06-10-deep-audit.md (175 findings).
Co-Authored-By: Claude Fable 5 <[email protected]>
* tui: hide malformed ask_user tool errors
* fix tui malformed tool retries
* fix max-turn finalization
* upgrade tui agent guardrails
* fix coderabbit guardrail findings
---------
Co-authored-by: Claude Fable 5 <[email protected]>
Co-authored-by: Vasanthdev2004 <[email protected]>
|
||
|
|
32d68b8d09 |
Persist background specialist tasks (#118)
* Persist background specialist tasks * Harden background task reload |
||
|
|
699df010e2 |
feat(update): verify release metadata by target (#97)
* Add update check target selection * Reject empty update target flag |
||
|
|
3e4add95ab |
feat(update): expose release check target options (#96)
* Expose update check target options * Reject non-positive update timeouts |
||
|
|
90b3cf1093 | Add sandbox backend capability reporting (#94) | ||
|
|
442f24c7de |
Remove Bun installer test dependency (#91)
* Remove Bun installer test dependency * Harden installer smoke tests * Stabilize npm wrapper subprocess tests * Make npm wrapper fixture explicit ESM |
||
|
|
503b6c5177 |
Move performance benchmark to Go (#87)
* Move performance benchmark to Go * Address perf benchmark review feedback * Fix perf benchmark pipe drain order |
||
|
|
8b9b19b839 | Move build smoke tooling to Go (#86) | ||
|
|
6c41984dbe |
Move release packaging to Go (#85)
* Move release packaging to Go * Handle release archive close errors * Guard release output cleanup paths |
||
|
|
ca4c57fc59 |
Remove legacy TypeScript runtime (#78)
* chore: remove legacy TypeScript runtime * fix: align perf benchmark semantics * fix: clarify harness rss benchmark |
||
|
|
43c256068a |
feat: make Go runtime the app path (#71)
* feat: make go runtime the app path * fix: harden update semver parsing |
||
|
|
2a5b89c095 | feat: expose MCP server CLI |