mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 05:01:28 -10:00
test/exec-image-tests-isolation
784
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f2e7c14637 |
test(cli): isolate the exec image tests from the models.dev refresh
runExec starts a background models.dev download unless ZERO_DISABLE_MODELS_FETCH is set. TestRunExecImageAttachmentHistory moved the user directories into a TempDir by hand but left the fetch on, so a 5.7 MB download landed in the TempDir while its cleanup ran, and the macOS runner failed with "unlinkat ... directory not empty". The two older image tests had no isolation and fetched into the real user cache. Use isolateCLIUserState in all three, which also points the cache path into the fixture and turns the fetch off. |
||
|
|
ef4da1323f |
fix(workspaceindex): import gitsafe under the Twigpine module path (#1153)
#1149 was opened before #1105 moved the module to github.com/Twigpine/zero
and merged right after it, so its two imports of internal/gitsafe still
used the old path. Nothing provides github.com/Gitlawb/zero any more, so
main stopped building at
|
||
|
|
1e848492e5 |
fix(sessions): preserve image attachment summaries on resume (#1152)
* fix(sessions): preserve image attachment summaries on resume Persist TUI-compatible attachment counts for ACP and exec user events. Restore readable markers in live history, session replay and resumed prompts without storing or resending historical image data. Cover image-only and mixed prompts, restart recovery, malformed metadata and the aborted-turn boundary with regression tests. Fixes #1135 * fix(workspaceindex): follow the current module path Use github.com/Twigpine/zero/internal/gitsafe in the ignore scanner and its tests. The upstream module rename preceded the scanner's gitsafe change, leaving these two imports on the old path and preventing the rebased tree from compiling. * fix(sessions): count only images accepted by the model Take ACP and exec attachment counts after the vision capability gate, matching the TUI's persisted attachment semantics. Update the ignored-image regressions to require zero saved images and no historical image marker. Refs #1135 |
||
|
|
2ffe1831b0 |
fix(workspaceindex): run Scan's git calls through gitsafe (#1149)
* fix(workspaceindex): run Scan's git calls through gitsafe Scan asks git for ignored paths on every turn with its own gitCommand, which turned off core.fsmonitor and dropped the repository-location variables but still let a clean filter or diff driver from the repository's own config run during status. It now goes through gitsafe, like the TUI and the scratch-file warning: hooks are off as well, and a repository whose config defines a filter or diff command takes the non-git path. The status call also passes --ignore-submodules=dirty. Scan makes two git calls, so gitsafe gains Check, which reads the repository config once and builds several commands from it. Command is now Check plus one command, and each command gets its own copy of the environment. * test(workspaceindex): pin Scan's fallback when gitsafe refuses the repository A repository whose own config defines a filter or diff command is refused by gitsafe, and Scan then takes the non-git path. Scan reaches gitsafe.Check through a package variable now, so a test can stand in for such a repository without configuring one that names a command, and TestScanFallsBackWhenGitsafeRefusesTheRepository checks both sides: with git consulted, .gitignore decides and the fixed list does not apply; refused, the fixed list applies and .gitignore is not read. |
||
|
|
13e2596b2e |
chore: move repository references to Twigpine/zero (#1105)
Update the Go module path, imports, install scripts, docs, workflows, packaging and action defaults from Gitlawb/zero to Twigpine/zero. The npm scope (already @twigpine/zero on main, with its @gitlawb/zero migration notes), LICENSE, action author, the OpenGateway provider and historical CHANGELOG links are unchanged. The X-AIMLAPI-Integration-Repo header keeps Gitlawb/zero: it is what AIMLAPI sees for attribution, so changing it needs a maintainer's yes. docs/INSTALL.md keeps `go install github.com/Gitlawb/zero/...`, because every tagged release so far declares that module path. The Homebrew formula comment names the Twigpine/homebrew-tap tap. Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
8c4e759798 |
feat(sandbox): support self-exec pattern to eliminate separate helper binary (#946)
* feat(sandbox): support self-exec pattern to eliminate separate helper binary Linux sandboxing previously required distributing a separate 'zero-linux-sandbox' binary and looking it up on PATH. This wires an internal '__sandbox-helper' entrypoint directly in cmd/zero/main.go and configures findLinuxSandboxHelperCommand to fallback to os.Executable(), enabling single-binary zero distributions with fully functional sandboxing. * fix(sandbox): prefer PATH helper before self-exec fallback Colocated zero-linux-sandbox stays first. A standalone helper on PATH is used next. Self-exec of the main zero binary is only the last fallback, so an installed helper is not ignored. * feat(sandbox): clean formatting in main.go and add self-exec unit tests * feat(sandbox): insert __sandbox-helper verb in bwrap plan for self-exec binary * fix(sandbox): drive Linux selection from the self-exec helper resolver Availability and command-plan construction now share the same helper resolution and keep the __sandbox-helper prefix on the backend, so a main-only install with bwrap reaches native sandboxing. * fix(sandbox): self-exec only the zero main binary, not test binaries * fix(sandbox): carry standalone vs self-exec identity into the inner bwrap plan A versioned physical helper (zero-linux-sandbox-v1) must not receive __sandbox-helper. The inner plan uses an explicit SelfExec flag, threaded from argv, instead of guessing from filepath.Base. * docs(install): describe self-exec as the last helper fallback Self-execution runs only when neither a colocated nor a PATH zero-linux-sandbox is available. * docs(install): state colocated, PATH, then self-exec helper order --------- Co-authored-by: cl-ment <[email protected]> |
||
|
|
d597f0e095 |
chore(main): release 0.11.0 (#1146)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>v0.11.0 |
||
|
|
ce2fcffaf1 |
perf(redaction): add fast-path byte checks to eliminate regex allocations on clean strings (#945)
* perf(redaction): add fast-path byte checks to eliminate regex allocations on clean strings Evaluating cascading regular expressions across every log line, tool output, and session event incurred ~23 µs and 51 heap allocations even on strings with zero secrets. This introduces fast-path substring checks before invoking expensive regex substitutions, yielding a 70x speedup and 0 B/op heap allocation on clean strings. * test(redaction): lengthen ghp_ fixtures and cover conditional gates GitHub classic tokens need 36 characters after ghp_. Benchmarks and shape tests now use a matching fixture. A table-driven test checks each fast-path gate, including near-misses and case-insensitive Authorization. * perf(redaction): structure triggered patterns and avoid allocation in header case checks * test(redaction): cover secret-header and URL-password gates Keep the redaction marker literal after url.URL rewrites userinfo, so callers still see [REDACTED] instead of a percent-encoded form. * fix(redaction): unescape only the generated URL password A path or query that already contains the encoded redaction marker must stay encoded. Only the userinfo password is rewritten in the clear [REDACTED] form. * fix(redaction): re-escape URL username after parse Username() is decoded. Re-encoding with url.User keeps control bytes out of the redacted URL so a %0AAuthorization userinfo cannot inject a header after the header pass. * fix(redaction): fail closed on password URLs without a host * test(redaction): isolate query-gate coverage from assign and text secrets * fix(redaction): fold Unicode U+017F and U+212A in containsCaseInsensitive Go (?i) regular expressions apply Unicode simple case folding, where U+017F (ſ) folds to 's' and U+212A (K) folds to 'k'. Teach containsCaseInsensitive to fold these runes to ASCII lowercase with zero allocations so case-insensitive header gates cannot be bypassed by Unicode-folded equivalents. --------- Co-authored-by: cl-ment <[email protected]> |
||
|
|
61b09364f0 |
feat(providers): add Concentrate provider preset (#1147)
* feat(providers): add Concentrate provider preset Register Concentrate (https://api.concentrate.ai/v1) as an OpenAI-compatible catalog provider. Concentrate is already listed in the README Partners section; this adds the provider itself. - models.dev has no Concentrate provider, so the id `concentrate` and the key name CONCENTRATE_API_KEY match OpenClaude's Concentrate integration. `Concentrate AI`, `concentrate.ai` and `concentrate-ai` resolve to it. - Default model deepseek-v4-flash-0731: the dated id behind the deepseek-v4-flash alias that OpenClaude uses. Concentrate's /v1/models keeps the rolling name only in `aliases`, and onboarding keeps the default selected only when it matches a live id, so an alias default made setup land on the first model in the list (claude-fable-5). - Curated models: deepseek-v4-flash-0731 (default), deepseek-v4-pro, kimi-k3 and glm-5.3. Context windows (1,048,576 each), tool calling and thinking support come from Concentrate's public /v1/models. - Regression test: a Concentrate-shaped live list must contain the preset's default model. - README and README_ZH: key line and setup command. * test(providers): prove the Concentrate default check runs on the live list The regression test passed whenever the default model was in the result, and the curated fallback also contains it. Assert that the mock served /v1/models and that the result includes claude-fable-5, a model only the live response lists, so a silent fall back to the curated catalog fails. |
||
|
|
d1a0d048ee |
fix(cli): let providers subcommands run with no active provider (#1143)
* fix(cli): let providers subcommands run with no active provider `zero providers list`, `use`, `remove` and `rename` exited 3 with `no active provider configured: active provider "" not found` as soon as the environment supplied more than one provider and config.json had no active provider: normalizeProvidersWithOptions only elects an active provider by itself when there is exactly one, and resolveCommandCenterConfig treated the resulting error as fatal, so the command stopped before doing anything. A base URL without a key counts as a second provider too (applyProviderEnv builds a profile from a base URL alone). The resolver already returns the successfully normalized list alongside ErrNoActiveProvider, and the interactive TUI already falls back to a usable entry from it in runInteractiveTUIWithSetup. Do the same here instead of failing, so these subcommands can answer for providers that only come from the environment. Also pin the four env-derived provider tests to a fixed environment and a temporary user config. They set OPENAI_API_KEY but resolved through the real resolver against the developer's own config.json and ambient provider variables, so they failed on any machine that also exported another provider variable and whose user config had no active provider. Adds regression coverage for the two-env-provider case: `providers list` must report every detected provider and `use`/`remove`/`rename` must give their usual env-derived answer. Verified against the fix (pass) and with the fix reverted (exit 3, `active provider "" not found`). Fixes #1130 * refactor(cli): report the configured active provider, not an elected one Provider-management subcommands answer for the SAVED configuration, while the TUI's recovery path answers "what can I run with". Adopting firstUsableProvider in resolveCommandCenterConfig conflated the two: with two env-derived providers it made `providers current`/`list` present a provider that `zero exec` would still refuse to run with, and a base-URL-only profile failed `providers list` because the election demands a credential. Return the resolver's partial config untouched instead, so nothing is reported as active when nothing is selected. Also removes the OAuth store read this path picked up with firstUsableProvider. Adds assertions that `current` shows nothing selected, `list` marks nothing active, `use --json` leaves activeProvider empty, plus a base-URL-only case. * fix(cli): scope the no-active-provider exception to provider management Returning the resolver's partial config made `zero config` exit 0 with a partial summary, contradicting the resolver's documented headless contract: `zero config` / `zero exec` still fail with the actionable message and only the interactive TUI recovers from a missing active provider. Split the helper so only issue #1130's commands — `providers list`/`current` and the unpersisted `use`/`remove`/`rename` reporters — tolerate the state; `zero config`, `detect`, `models` and `check` keep failing as before. Also pin the OAuth store in providerEnvSetupDeps: summarizeConfig reads OAuth login state through the default store, so a developer's stored login could flip these assertions. Point ZERO_OAUTH_TOKENS_PATH at a temp file and force ZERO_OAUTH_STORAGE=file so an ambient keyring setting cannot pull the OS keychain in either. |
||
|
|
c88c77bf70 |
fix(workspaceindex): skip git-ignored and build-cache dirs in Scan only (#1117)
* fix(workspaceindex): skip git-ignored and build-cache dirs in Scan only Scan has a fixed file budget, and build output (Cargo target/, Python virtualenvs, caches) could exhaust it before real source was reached: repo-map reported truncation and ranked fingerprint files above code. Inside a git work tree, Scan now asks git for the ignored set (`git status --porcelain=v1 -z --ignored=matching .`), so the repo's own .gitignore, nested ignore files, info/exclude and core.excludesFile all apply, including names no fixed list anticipates (.locust_env/). Outside a work tree, or when git is missing, older than 2.16, fails, or exceeds 5s, a fixed list of common build/cache directory names stands in. ShouldSkipDir is unchanged: glob, grep, list_directory and path autocomplete share it and must keep seeing target/, .venv/ and friends. The lookup runs with GIT_OPTIONAL_LOCKS=0 so a per-turn scan never takes .git/index.lock next to a concurrent git command. Fixes #1108 * fix(workspaceindex): skip a rename's original-path field when parsing status In `git status --porcelain=v1 -z`, a rename or copy record is followed by one more NUL field holding the original path, with no XY prefix. The parser inspected every field, so an original name beginning with "!! " read as an ignored entry: after `git mv "!! a" b`, a tracked file "a" disappeared from Scan. Move the parsing into parseIgnoredStatus, which consumes that field for R or C in either status column, and test it directly and end to end. * fix(workspaceindex): disable repo fsmonitor and force untracked-files=normal for status Scan's git status lookup must not run a repository's core.fsmonitor hook or fail when status.showUntrackedFiles=no would reject --ignored=matching. * fix(workspaceindex): ignore inherited git repository location Git exports GIT_DIR and GIT_WORK_TREE to hooks. With those pointing at another repository, Scan kept .locust_env/lib/x.py, which root's own .gitignore excludes. * fix(workspaceindex): quote the fsmonitor hook path in the regression On Windows, sh treats an unquoted backslash path as escapes, so the hook ran and the marker the test stats was never created. Require a status without the override to create that marker first. * fix(workspaceindex): pass the fsmonitor hook path to git with forward slashes Smoke (windows-latest) failed TestGitIgnoredPathsDisablesRepoFsmonitor because core.fsmonitor was set to a backslash script path. Git for Windows hands that value to sh, which consumes the separators, so the hook never runs and the marker is never created. The test now stores a quoted forward-slash path. The product code is unchanged. |
||
|
|
640d936bfb |
fix(tui,cli): stop a repository's git config from running programs in Zero's own git calls (#1133)
* fix(tui,cli): stop a repository's git config from running programs in Zero's own git calls Zero runs git status and git diff on its own: the TUI's files sweep at startup and after each command, its pull request diffstat, and the scratch-file warning after a run. A repository's own config can name programs those commands run (core.fsmonitor on every status, hooks when the index is rewritten, clean filters and diff drivers assigned through .gitattributes), so a workspace unpacked from an archive ran its own code on the first turn, although Zero refuses the same workspace's project hooks and plugins until the user trusts it. internal/gitsafe builds those commands with core.fsmonitor and core.hooksPath turned off and GIT_OPTIONAL_LOCKS=0, and drops the repository-location variables git exports to hooks, using the same environment for its config check and for the command. A repository whose own config, or per-worktree config when extensions.worktreeConfig is on, defines a filter or diff driver command is refused, and the callers already treat that like a directory that is not a repository. The status and diff calls also pass --ignore-submodules=dirty, because a submodule's own config is not checked. * test(gitsafe): keep the real-git setup off an inherited GIT_DIR The setup helper in TestCommandAgainstRealGit ran git with the inherited environment, so from a hook, or a shell that exports GIT_DIR or GIT_WORK_TREE, git init and git config would act on the repository those name instead of the temporary one. It now uses the same environment as Command, and the test sets both variables to a decoy repository and checks after each setup step that the decoy was not touched. |
||
|
|
8e363fcaa5 |
fix(npm): wait for npm to apply each publish, and drop the one-time @gitlawb/zero steps (#1145)
* fix(npm): wait for npm to apply each publish, and drop the one-time @gitlawb/zero steps npm applies a publish asynchronously: `npm publish` returns once the upload is accepted, and the version appears later. In the 0.10.0 release the @twigpine/zero wrapper appeared 9 seconds after publish-packages.sh stopped checking for it, and the last platform payload about ten minutes after its upload. Two things went wrong: - The wrapper became visible before four of its five platform payloads, so installs in that window resolved aliases that 404 and fell back to the GitHub-release download. Ordering the uploads does not order what npm shows. - The final `latest` check gave up after 25 seconds and failed the job even though every publish landed, which also skipped the @gitlawb/zero steps until a re-run. publish-packages.sh now waits until npm shows every platform version before it publishes the wrapper, then waits until `latest` is the wrapper version. One deadline bounds the waits (ZERO_NPM_PUBLISH_WAIT_TIMEOUT, default 30 minutes). On timeout it fails before the wrapper goes out and says what npm never showed. These lookups treat an error as "not yet", unlike the skip decision, which still stops on anything but E404. The test fake can now apply a publish only after a number of later lookups, and records which aliased versions were missing when the wrapper was published. Against the script as released in v0.10.0, the two new tests fail the way the release did: the job exits 1 although everything landed, and the wrapper goes out while its platform payload is not visible. The two release steps that published 0.10.0 once more as @gitlawb/zero have done their job and are removed; docs/NPM_PACKAGING.md and the comments that pointed at them now describe what happened. The Go and wrapper handling of the old name stays, since only a build installed under it can trigger it. * fix(npm): validate the publish wait settings, and correct the legacy publishing claim ZERO_NPM_PUBLISH_WAIT_TIMEOUT went into Bash arithmetic as typed. A leading zero reads as octal (0600 waited 384 seconds), and 08 is an arithmetic error that, in a script, does not stop it: Bash prints the error, leaves the deadline unset and carries on, so the run only died at its first wait, after the platform payloads were published. A negative timeout ended the wait before it started, and a bad ZERO_NPM_PUBLISH_RETRY_DELAY only failed at the first sleep, also after publishing began. Both settings are now checked before anything is published: whole seconds only, converted with 10# so a leading zero is decimal, bounded before the conversion, and capped (timeout 86400, delay 600). A bad value fails with the variable's name. TestPublishPackagesReadsTheWaitTimeoutAsDecimal makes the script wait, which is where the old code broke: against the reviewed script it publishes the platform payload and then exits 1 on "08: value too great for base". TestPublishPackagesRejectsAnInvalidWaitSetting covers -5, abc, 1e3, an over-range and an overflowing timeout, and a non-numeric delay; the old script published with five of them and crashed on the sixth with "abc: unbound variable". docs/NPM_PACKAGING.md said nothing could publish @gitlawb/zero again. Removing the trusted publisher stops this workflow; a maintainer could still publish by hand, and the doc now says that. |
||
|
|
856919bcaa |
chore(main): release 0.10.0 (#1061)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>v0.10.0 |
||
|
|
883e5bec24 |
fix(sessions): keep a session lease for the life of the process, not of the Store (#1144)
A session's lease file lived in the Store that took it, so once that Store was unreachable the garbage collector closed the file, and closing it drops the lock. That made TestForkAndChildHoldTheParentTheyAreCreatedFrom fail on main whenever a collection ran before its dry-run prune. It can also happen in an exec run: PrepareExec takes the calling or forked session's lease on a Store the run never touches again, so the parent's lease can disappear part way through and a prune in another terminal could remove it. Leases now belong to the process, as Hold documents. Every Store holding a session shares one lease file in a process-wide table, and the file is unlocked and closed only when the last Store holding it releases it. |
||
|
|
530563a0bb |
feat(npm): publish as @twigpine/zero (#1142)
* feat(npm): publish as @twigpine/zero The npm package moves from @gitlawb/zero to @twigpine/zero, and everything that finds Zero's GitHub repository by name now names Twigpine/zero. Package name - package.json, the lockfile and release-please use @twigpine/zero. `repository` names Twigpine/zero: npm checks provenance against it, so a publish from this repo with the old URL fails with E422. The homepage is zero.twigpine.com. - The name lives in package.json only. build-platform-packages.mjs names the wrapper, platform versions, alias names and alias targets from it, and bin/zero.js reads its own package.json for the alias it resolves and for every reinstall hint. The Go constant npmPackageName is held equal to package.json by a test. Repository defaults - The update check (DefaultRepository), install.sh, install.ps1, the npm wrapper's fallback downloader, and the GitHub Action's zero-repo default all used Gitlawb/zero. That name now belongs to an unrelated GitHub org, and these paths download and run what they find there, so they point at Twigpine/zero. Docs, README and issue-template links follow. The Go module path (and so `go install github.com/Gitlawb/zero/...`) is unchanged here. Publishing - The two inline publish steps become scripts/npm/publish-packages.sh, which reads the name from the assembled payloads, refuses to start when the wrapper's aliases do not match the platform payloads, skips versions already on the registry (so re-running a half-finished job is safe), and publishes with --ignore-scripts. The dist-tag rules are unchanged. internal/npmwrapper/publish_test.go runs it against a fake registry. Docs explain the switch for existing installs: uninstall @gitlawb/zero first, because both packages own the `zero` bin and npm refuses the second (EEXIST). * feat(npm): publish 0.10.0 once more as @gitlawb/zero to point users at the new name Deprecating @gitlawb/zero alone would strand every existing npm install. 0.9.0's `zero upgrade` runs `npm install -g @gitlawb/zero@latest`; with nothing newer under that name npm installs nothing, prints no deprecation warning (it only warns when it installs something), and Zero then reports "updated via npm to 0.10.0" while staying on 0.9.0. So this release goes out once more under the old name, as a bridge: - The publish job assembles the same verified artifacts a second time with the new `build-platform-packages.mjs --package-name @gitlawb/zero` and publishes them with publish-packages.sh, after the @twigpine/zero publish. A gate runs it only while @gitlawb/zero's latest is still 0.9.0 (or is this version, on a re-run), so it happens once. - The build knows its name from the package.json beside it. The Go side reports InstallMethodNpmLegacy: `zero upgrade` refuses with the switch steps before any release check (so it works offline too), and `zero update --check` adds them whether or not an update is out. - bin/zero.js prints the same steps after zero exits (the TUI would hide a notice printed first), only when stderr is a terminal, and not for `update`/`upgrade`, which print it themselves. - Updating from 0.9.0 now installs a real version, so npm also shows the deprecation message. The steps say to uninstall @gitlawb/zero before installing @twigpine/zero: both own the `zero` bin, npm refuses the second install with EEXIST, and a forced install loses the bin when the old package is later removed (checked against npm 11.16). docs/NPM_PACKAGING.md has the maintainer runbook: bootstrap and trust the new package, retarget the old one's trusted publisher for the bridge, then deprecate @gitlawb/zero, revoke its publisher, and delete the bridge steps. * fix(npm): only skip a publish when npm says the version is not there publish-packages.sh treated any failed `npm view <name>@<version>` as "not published". A transient network, auth or registry error on the re-run that the skip exists for therefore published a version that was already out, and npm rejected it with EPUBLISHCONFLICT; with the registry unreachable it published blind. npm 11 reports every kind of missing (no such package, no such version, including platform versions) as E404, so only E404 now means "not published". Any other failure is retried three times and then stops the job before it publishes anything, with npm's own error shown. The test fake now answers a missing version with E404 the way npm does (it returned an empty success before), and can fail lookups with a network error. The two new tests fail on the old lookup: one with EPUBLISHCONFLICT after transient failures, the other by publishing while the registry is unreachable. Also renames the unused retry variable in the legacy-name gate step to `_` (shellcheck SC2034), matching the script. |
||
|
|
c56a4c4e6e |
fix(deps): refresh the lockfile to patched shell-quote (#1131)
shell-quote 1.9.0 is inside the range GHSA-pqg4-j6r4-53mv covers (>= 1.8.4, < 1.11.0). It reaches the lock through tuistory, @opentui/react and react-devtools-core, which asks for ^1.6.1, so refreshing the lock moves it to 1.12.0 without touching package.json. Release packaging installs the local-control helpers from this lock, so the old version shipped in the helper tree. |
||
|
|
300cf6275f |
fix(acp): persist token usage events for ACP sessions (#1137)
* fix(acp): persist token usage events for ACP sessions ACP turns never set agent.Options.OnUsage, so provider token counts were dropped on the floor. zero exec and the TUI persist the same counts as session usage events, and `zero usage report` totals those events, which left every editor-driven ACP session at zero requests and zero tokens. runTurn now queues each report through usage.EventUsagePayload, the same writer exec uses for a non-escalating run, and commits it in the turn's existing buffered batch under the session id. A hard run failure still returns before persistTurn, so that turn's usage is dropped with its history. StopCancelled stays a successful outcome and keeps the usage already reported. session/load and session/resume still skip event types other than messages, compaction, and tools, so the restored prompt and the wire replay do not gain a usage channel. ACP has no model escalation, so the payload has no "model" key and no prompt, tool argument, or other raw input. This sits beside the permission-mode field persisted on the session (#1134). It does not change the ACP wire protocol, exec, the TUI, or the report reader. Refs #1118 * test(acp): pin cancel, resume, and payload shape for usage events The persistence test showed that a successful turn records token counts, and the hard-failure test showed that an aborted turn records nothing. Neither one locked the two edges that are easy to get backwards. A client cancel is a StopCancelled outcome, not a hard failure, so the tokens the provider already reported stay in the session log. session/load and session/resume read that same log; the replay and the next prompt must still be the conversation, with no usage payload on the wire and no token counts folded into the resumed prompt. The persisted JSON is also checked to be token-count keys only, so a prompt or tool argument cannot ride along in the usage record. Refs #1118 |
||
|
|
106c0b77f9 |
test(cli): pin sandbox policy golden temp roots (#1139)
* test(cli): pin sandbox policy golden temp roots TestRunSandboxPolicyJSONGoldenIncludesManagerBaselineFields compared the policy snapshot to a fixed golden after stripping only the temp roots NewScope keeps. NewScope drops a candidate that sits inside an earlier one, while the JSON (built with a nil scope) still lists it, so a TMPDIR inside /tmp — or TMP inside TEMP on Windows — survived normalization and failed the golden. Pin the variables that path reads (TMPDIR on Unix, TEMP and TMP on Windows) and cover a nested temp dir that is not inside the earlier candidate, so the normalizer removes it instead of leaving it in the JSON. Refs #1121 * test(cli): run the golden against an ambient nested temp dir The nested golden created its temp dir in the package working directory and failed when that path sat inside /tmp, so a checkout under /tmp could not run it and the test wrote into the source tree. Stage the issue's shape first — TMPDIR inside /tmp, or TMP inside TEMP — then let the pin override those variables. Directories come from t.TempDir. If the nested shape cannot be built, skip. Refs #1121 |
||
|
|
846d30f57f |
test(cli): give the local adoption probe a test-sized timeout (#1136)
* test(cli): give the local adoption probe a test-sized timeout TestLocalAdoptionSavesAndReloadsEligibleModel relied on the 400ms defaultLocalProbeTimeout, which treats a slow probe as "runtime not running" and silently drops the candidate; under -race on a busy runner one slow loopback round trip can exceed it and empty the detection result. Pass a 5-second test deadline instead. The production default is unchanged and stays covered by TestDetectLocalRuntimesAppliesDefaultTimeout. Refs #1132 * test(cli): correct the timeout-coverage note on the adoption probe TestDetectLocalRuntimesAppliesDefaultTimeout pins the probe timeout with an explicit 20ms deadline, so it covers timeout enforcement rather than the 400ms production default. Reword the comment accordingly; no assertion or production behavior changes. Addresses the CodeRabbit finding on #1136. |
||
|
|
bb96206c27 |
fix(acp): persist the session permission mode across restarts (#1134)
ACP kept the permission mode in memory only: both mode doors called setMode, registerSession always started at PermissionModeAuto, and nothing wrote the mode to the session store, so session/load and session/resume returned a plan/ask session as auto after a restart. Save the mode next to the model on both doors, store first so a failed write fails the request instead of leaving disk behind memory, and restore it through registerSession. A session with no saved mode (written before this change) keeps auto; a saved value outside auto/ask/plan never comes back weaker than ask, and unsafe stays unreachable over ACP. Refs #1125 |
||
|
|
e0fb65f003 |
test(sessions): run the session lock test on Windows and add a cross-process one (#1115)
* test(sessions): run the session lock test on Windows and add a cross-process one TestSessionFileLockSerializesAcrossStores was skipped on Windows with "flock semantics differ on windows", so the LockFileEx path in filelock_windows.go was never exercised by a test on the platform that uses it. LockFileEx locks are per handle, which is what the test relies on, so the skip was unnecessary: remove it. Also add a helper-process test (same pattern as lockutil's) proving the session lock blocks a different process and is released by the OS when the holder exits without unlocking. Both fail on Windows when the lock is replaced by a no-op. Fixes #1104 Co-Authored-By: Claude Sonnet 5.5 <[email protected]> * test(sessions): keep the session lock alive in the helper process The helper discarded the unlock closure returned by lockSession, which is the only reference to the open lock file/handle. GC finalization could close it while the helper waits on stdin, releasing the OS lock early and making the cross-process test pass for the wrong reason. Retain the closure and runtime.KeepAlive it after io.Copy, as the lockutil helper does. --------- Co-authored-by: Claude Sonnet 5.5 <[email protected]> |
||
|
|
a3d4edef8f |
test(tui): isolate user home, config and cache dirs for the TUI tests (#1114)
* test(tui): isolate user home, config and cache dirs for the TUI tests newModel reads <user config>/zero/commands/*.md, and the package has 500+ newModel calls with no isolation, so a developer's own slash commands and state leaked into the TUI tests (and tests could write to real user directories), against the AGENTS.md "Hermetic tests" rule. Add testutil.IsolateUserDirs, which points HOME, USERPROFILE, APPDATA, LOCALAPPDATA and the XDG_* roots at a temp dir (covering Linux, macOS and Windows), and call it from a TestMain in internal/tui. Per-test t.Setenv overrides still win. Refs #1103 Co-Authored-By: Claude Sonnet 5.5 <[email protected]> * test(testutil): fail closed when user dir isolation cannot be applied IsolateUserDirs now returns an error when any Setenv fails, rolling back the variables it had already redirected, and its restore func reports restoration failures. The TUI TestMain exits non-zero when isolation, restoration or removal of the temp root fails, and the helper test restores the inherited XDG_DATA_HOME via t.Setenv before clearing it. Co-Authored-By: Claude Opus 5.5 <[email protected]> * test(testutil): cover partial-redirect rollback and compare physical paths Make Setenv injectable so a test can let HOME be redirected and fail the next variable, proving IsolateUserDirs rolls back a variable it really changed (the NUL-root case fails on the first call and cannot). Add testutil.PathWithin, which resolves the existing part of each path with EvalSymlinks before the containment check, and use it in both the helper test and the TUI hermetic test so symlinked temp dirs and Windows short names cannot cause false results. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Sonnet 5.5 <[email protected]> |
||
|
|
a195e8848c |
fix(tui): stop held peer message Allow/Deny from deadlocking Update (#1111)
The peer approval prompt's decide closure called runtimeMessageSink from inside Update. The sink ends in program.Send, which blocks on bubbletea's unbuffered message channel; the only reader is the event loop that is running Update, so answering a held cross-session message hung the TUI. Give pendingPermissionPrompt a decideCmd that returns a tea.Cmd yielding the peerDecisionMsg, and have resolvePermissionWithReason return it instead of calling the sink. Tests now drive the decision through the returned command and a blocking sink, which hangs on the old code. Fixes #1099 Co-authored-by: Claude Sonnet 5.5 <[email protected]> |
||
|
|
a0979fb756 |
test(tools): poll for listening address in foreground server test (#1110)
TestExecCommandForegroundServerReturnsSessionAndServesHTTP parsed the helper's listening address from the first exec_command result only, with a 500 ms yield. On a loaded Windows runner the helper had not printed its address yet, so the test failed even though the session was running. Poll the session with write_stdin until the address appears (20 s deadline), and register the cleanup before the address check so the server is stopped on early failure too. Fixes #1097 Co-authored-by: Claude Sonnet 5.5 <[email protected]> |
||
|
|
4dc603f622 |
refactor(config): drop the unreachable SetNotify wrapper (#1128)
SetNotify was kept in #1001 as a thin replace-form wrapper over UpdateNotify for existing tests, but every product surface (the CLI `zero config notify` and the TUI /notify picker) calls UpdateNotify's merge form directly, so the advisory deadcode job reports it as unreachable. Move the replace helper into writer_test.go as the unexported setNotify, keep every test it served unchanged in behaviour, and point the two CLI doc comments at UpdateNotify. `make deadcode` no longer lists internal/config; the other advisory findings in that job are pre-existing and untouched. |
||
|
|
4d2e9f0d08 |
fix(config): sync atomic config replacements (#1093)
* fix(config): sync atomic config replacements Persist temporary config contents before rename, then sync the parent directory on platforms that support it. Return persistence failures and cover ordering, cleanup, and pre/post-replacement failure states. Fixes Gitlawb/zero#1087 Amp-Thread-ID: https://ampcode.com/threads/T-01a0dcd7-47fc-76eb-82d3-3f6c25384912 Co-authored-by: Pierre Bruno <[email protected]> * fix(config): sync new config directories and relax unopenable dir sync Sync each directory entry created by MkdirAll into its parent so a crash cannot lose the config directory after a successful first write. Treat a config directory that cannot be opened as best-effort durable, matching the sessions store's syncDir; only a failed Sync or Close is reported, since the rename has already replaced the file. Check the temp file is closed with Seek instead of Stat: on Windows, File.Stat on a closed file returns "The handle is invalid" rather than os.ErrClosed. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(config): report directory-open failures except for write-only directories syncConfigDir treated every os.Open failure as success, so a caller could acknowledge a save whose directory barrier was never reached (Copilot review, PR #1093). Keep the maintainer-requested best effort for a directory the user can write but not read, as in the sessions store, and return every other open failure. TestSyncConfigDir now expects os.ErrNotExist for a missing directory on non-Windows hosts; it fails on the previous code with "missing directory: error = <nil>, want file does not exist". * fix(config): create the config directory durably on the lock path and roll back failed barriers Every exported mutator acquires the config lock before it writes, and the lock file is a sibling of the config, so lockConfigFile's MkdirAll was what created the config directory on a first write. writeConfigData then found nothing missing and never synced the new entries into their parents, so a locking mutator could report success without the directory creation being durable (CodeRabbit review, PR #1093). A failed parent sync also left the newly created directories in place: a retry found nothing missing, skipped the barrier, and reported success. Route both creation sites through one helper, ensureConfigDir. It creates each missing component with os.Mkdir so it knows exactly what it created, syncs each new entry into its parent deepest first, and on any barrier failure removes only the directories this call created, deepest first, with os.Remove so a directory another process has populated survives. A failed rollback is joined into the error, never swallowed. A component that appeared between the lookup and the Mkdir is not rolled back but its parent is still synced. Regression coverage: - TestLockingMutatorSyncsCreatedDirectories drives UpdateNotify on a missing nested directory, asserts the parent syncs happen before the lock file exists, that a failed barrier rolls the directories back, and that the retry re-runs the barrier. On the previous code it fails with "error = <nil>, want injected sync failure" because the lock path bypassed the barrier entirely. - TestWriteConfigDataRollsBackOnlyCreatedDirectories asserts a failed barrier removes the created directories but keeps a pre-existing directory and its file, and that a directory populated by another process is kept and the failed rollback reported. On the previous code it fails with "must be removed after a failed parent sync: <nil>". --------- Co-authored-by: Amp <[email protected]> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> Co-authored-by: Kevin Codex <[email protected]> |
||
|
|
1335ca6671 |
feat(providers): add GMI Cloud provider preset and partner listing (#1127)
Register GMI Cloud (https://api.gmi-serving.com/v1) as an OpenAI-compatible catalog provider and add it to the README Partners section. - The catalog id `gmicloud` matches the models.dev provider id, so the remote model catalog resolves without a mapping. `gmi-cloud`, `GMI Cloud` and `gmi` resolve to it. - Reads GMI_API_KEY (the name in GMI's docs) and GMICLOUD_API_KEY (the name models.dev uses). - Defaults to Qwen/Qwen3.8-Max. The curated list is Qwen 3.8 Max and Flash (GMI's partner models), then DeepSeek V4 Pro and Flash. All four are served by GMI's /v1/models. - README and README_ZH: key line, setup command, and the partner logo in light and dark variants under docs/assets/. Verified live with a GMI key: `zero providers check gmicloud --connectivity` passes, and `zero exec` completes a read-then-write task on both Qwen 3.8 models. |
||
|
|
ddc45f2c81 |
feat(providers): opt-in ZERO_RESPONSE_HEADER_TIMEOUT override for slow first-byte deployments (#1106)
* feat(providers): opt-in ZERO_RESPONSE_HEADER_TIMEOUT override for slow first-byte deployments The shared HTTP transport waits at most 120s for a response header. A local model server that has to load a large model can take longer than that to produce the first byte (measured 1-5 minutes on a throttled local Ollama), so an otherwise-alive request is aborted. Add ZERO_RESPONSE_HEADER_TIMEOUT, mirroring ZERO_STREAM_IDLE_TIMEOUT: a Go duration or a bare number of seconds; "0", "off", "none" or "disabled" remove the limit; an unparseable or non-positive value falls back to the default instead of removing the limit. The default stays 120s, so nothing changes for anyone who does not set the variable. The constant and the resolver sit next to the idle-timeout equivalents in providerio. Checked against a throttled local Ollama: ZERO_RESPONSE_HEADER_TIMEOUT=5s fails at ~6.4s, =300s succeeds at ~223s (a request that would have hit the 120s ceiling). Refs #1038 Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_0142QEjA7eEFcdXTpcTmcAZk * fix(providers): keep the 120s header timeout when bare seconds overflow A value such as 36028797018963968 passes strconv.Atoi and wraps to zero when multiplied by time.Second, which would disable the default. Values that do not fit in time.Duration stay on the default. * fix(providers): keep timeout defaults when bare seconds overflow ResolveStreamIdleTimeout multiplied bare seconds by time.Second the same way the header timeout did, so a large count wrapped to zero and disabled the watchdog. Both resolvers now share a bounds-checked conversion. * fix(providers): apply the header timeout when the shared client is taken The stall-hardened transport baked ResponseHeaderTimeout in at package init, so a later ZERO_RESPONSE_HEADER_TIMEOUT never reached the real client and the 120s check depended on the env at init. Each resolved value now keeps its own transport, and a slow header proves that transport enforces it. --------- Co-authored-by: Claude Sonnet 5.5 <[email protected]> |
||
|
|
eebb384676 |
feat(runtime): add process-local per-model RPM admission limits (#1116)
* feat(runtime): add process-local per-model RPM admission limits * fix(config): preserve model RPM through provider fallback * test(config): pin provider-command RPM cap precedence * style(config): gofmt provider-command RPM test |
||
|
|
39d6560137 |
docs(readme): add Trendshift badges and Partners section (#1126)
Add three Trendshift badges under the tagline (#1 Go repository of the day, #3 Go repository of the week, #3 on GitHub Trending) and a Partners section listing Twigpine, Xiaomi MiMo, Novita AI, Concentrate, and Exa, following the layout used in the OpenClaude README. README_ZH.md gets the same badge row and a matching section. The Novita, Concentrate, and Exa logos are vendored under docs/assets with light and dark variants. The Twigpine and Xiaomi MiMo logos are loaded from their own sites, as OpenClaude does; Twigpine switches to its dark variant under a dark color scheme. |
||
|
|
bb3346a828 |
fix(execution): reserve process capacity before launching (#1092)
Reject starts when all slots are live or reserved, prune only completed history, and release reservations on launch failure. Keep live processes tracked even after failed termination. Add concurrent admission and real-process regressions for #1085. Amp-Thread-ID: https://ampcode.com/threads/T-01a0dcd5-eb93-74c9-a1ac-5b140edaf973 Co-authored-by: Amp <[email protected]> |
||
|
|
ac5fe0d218 |
fix(security): harden shell flag parsing, write roots, and formatters (#1066)
* fix(security): harden shell flag parsing, write roots, and formatters - sandbox: recognize POSIX short-option clusters in shellCommandFlag (ZERO-ESC-02) Extract payload commands behind combined short flags (-ec, -lc, -xc) and long --command in dashCPayload and shellDashCPayload, preventing destructive subcommands from bypassing recursive AST inspection and auto-allowing execution. - tools: anchor write_file and edit_file to *os.Root descriptor (ZERO-TRAV-01) Use openScopedWriteRoot and commitRootedFileContents to traverse parents relative to the root descriptor handle, eliminating residual TOCTOU directory swaps. - tools: harden format_on_write against untrusted execution and secret leakage Pass --no-config to prettier to disable dynamic JS config execution, reject formatter binaries resolved inside the workspace (formatterBinaryInWriteRoots), and scrub credential-bearing environment variables before running formatters. * fix(security): stop shell flag parsing at dash-dash and verify root descriptor identity Stop recursing into shell `-c` payloads for tokens that follow the `--` separator, which marks positional operands rather than options, in both shellDashCPayload (text detector) and dashCPayload (AST analyzer). This prevents `bash -- -ec '...'` from being classified on a command the shell never runs as a command string. Verify the identity of the *os.Root descriptor returned by openScopedWriteRoot against a pre-open os.Stat of the resolved root, closing the check-to-use window opened by os.OpenRoot re-resolving the path. A root substituted for a symlink or a different directory is refused and the descriptor closed. Regression tests fail on the unfixed code: - TestShellDashCPayloadStopsAtDashDash, TestDetectInteractiveCommandAllowsNonInteractive - TestAnalyzeCommand (`bash -- -ec ...` must stay non-interactive/non-destructive) - TestOpenScopedWriteRootRejectsSubstitutedRoot Bureau: desk-pr-1066 * fix(tools): relax symlink resolution error handling on host formatters and clean up test directory * fix(security): anchor write preimage on root descriptor and report skipped formatters --------- Co-authored-by: hazyhaar <[email protected]> |
||
|
|
0c6f179a84 |
fix(tools): hint when Windows resolves POSIX paths under root (#1063)
* fix(tools): hint when Windows resolves POSIX paths under the workspace root On Windows a leading-slash POSIX path is not absolute, so read tools joined it under the workspace root and surfaced a raw GetFileAttributesEx error. Append a Windows-only hint naming the host OS and the workspace root when such a path is missing, so the model switches to a workspace-relative path instead of retrying the same shape. Guidance only: which path is opened, confinement errors, and write-path resolution are unchanged. All read tools share the annotated resolver. Refs #972 * test(tools): keep the Windows path hint wiring test hermetic The cross-platform wiring test used a fixed "/home/..." request, but resolveScopedReadPathForGOOS injects only the annotation GOOS: on Unix the request is host-absolute, so root was ignored and the test read state it did not own. Build the request from t.TempDir() instead - joined under root on Windows, anchored in root on Unix - and resolve an existing file under that tree to prove the resolution stayed in test-owned storage. The native Windows end-to-end test is unchanged. Addresses the P2 finding on #1063. * fix(tools): reject mixed-separator UNC paths in the Windows hint Windows treats both separators as valid, so a second leading separator in either position (/\server\share, /\?\C:...) is the same UNC or device-style prefix as //. looksLikePosixAbsolutePath excluded only the literal "//", so a missing mixed-separator target was misreported as a POSIX path resolved under the workspace root. Exclude either separator byte and cover both mixed forms in the shape and annotation tables. Addresses the P3 finding on #1063. |
||
|
|
feb992757c |
fix(tui): bound live dictation regions (#985)
* fix(tui): bound live dictation regions Amp-Thread-ID: https://ampcode.com/threads/T-01a0448f-5860-721c-8a47-5119fc57f685 Co-authored-by: Amp <[email protected]> * fix(tui): preserve user edits across dictation partials * fix(tui): reanchor stale dictation regions Amp-Thread-ID: https://ampcode.com/threads/T-01a04c92-2d1d-7508-91bc-416341b7e8b0 Co-authored-by: Amp <[email protected]> * fix(tui): bound live dictation regions Amp-Thread-ID: https://ampcode.com/threads/T-01a0448f-5860-721c-8a47-5119fc57f685 Co-authored-by: Amp <[email protected]> Co-authored-by: Pierre Bruno <[email protected]> * fix(tui): preserve user edits across dictation partials Co-authored-by: Pierre Bruno <[email protected]> * fix(tui): reanchor stale dictation regions Amp-Thread-ID: https://ampcode.com/threads/T-01a04c92-2d1d-7508-91bc-416341b7e8b0 Co-authored-by: Amp <[email protected]> Co-authored-by: Pierre Bruno <[email protected]> * fix(tui): preserve dictation separators after reanchoring Amp-Thread-ID: https://ampcode.com/threads/T-01a07cfc-4f4e-773c-b42e-6229ef568a01 Co-authored-by: Pierre Bruno <[email protected]> * fix(acp): reject Unicode whitespace in browser title origins Cover encoded whitespace before permission translation and through wire payloads. Pin parent reasoning-effort forwarding at the specialist resume call site. Amp-Thread-ID: https://ampcode.com/threads/T-01a097c8-242d-732f-bf6e-7321d8cf732c Co-authored-by: Pierre Bruno <[email protected]> --------- Co-authored-by: Amp <[email protected]> Co-authored-by: Anandan <[email protected]> |
||
|
|
23c27e0778 |
feat(sessions): add zero sessions prune, which never removes a session another process has open (#1089)
* feat(sessions): prune old sessions, never one another process has open Store.Prune removes sessions last updated before a cutoff of at least a day, and reports what it removed, what it kept and why, and what failed. A dry run makes the same decisions and removes nothing. A session holds no lock between writes, so nothing told a prune in one terminal that another terminal still had the session open. Each session now has a lease file: a process that writes a session, or loads it through the rehydrated reads the TUI, exec --resume and ACP use to continue one, holds a shared lock on it for its lifetime, and prune takes it exclusively and leaves alone any session it cannot get. On Windows the lease file is opened with FILE_SHARE_DELETE so a held lease never blocks removing the directory. Prune also keeps every ancestor of a session that stays, since Lineage and Tree fail on a missing ancestor, removes descendants before their ancestors, re-checks each session under its lease and write lock, and removes the metadata first so a removal that fails part way leaves nothing that looks like a session. Part of #971. * feat(cli): add zero sessions prune, with a retention setting read from user config only `zero sessions prune --older-than 30d` removes sessions last updated before the cutoff, `--dry-run` lists what it would remove and why it keeps the rest, and `--json` prints the report. The cutoff is days or a Go duration and never under a day. Without --older-than, prune uses sessions.retentionDays; with neither it refuses and says how to set one, so nothing is removed unless asked for. sessions.retentionDays is read straight from the user config file. Sessions belong to the user rather than to a repository, so a project's .zero/config.json cannot choose what prune deletes, and no resolver merge copies the setting from anywhere. Closes #971. * test(sessions): a dry run reports a session another process has open as kept * test(sessions): pin the dry run for undated sessions and the lease a new session takes * test(sessions): report every failure when a session is written while pruning * docs(readme): list sessions prune in the Chinese README as well * fix(sessions): a fork or child holds its parent, and is refused while prune holds it A lease is taken best effort, so a fork whose parent prune held at that moment went ahead: the rehydrated read in exec --fork could not hold the parent, Fork read it anyway, and the fork was created under a parent prune was removing. Fork and CreateChild now take the parent's lease themselves before reading it, and refuse when prune holds it, so the parent of a session being created is never removed under it. * docs(sessions): say that nothing calls Release yet, and what it is for * fix(sessions): let go of the lease before removing a pruned session's directory lease.lock is deleted with the rest of the session while prune still holds it. Where a delete only takes effect once the last handle closes (Windows without POSIX delete semantics: older builds, or FAT and exFAT volumes), prune's own handle kept the directory from being empty, so the removal failed and left an empty directory behind. Prune now closes the lease after session.lock is gone and before the directory goes. * test(cli): an empty --older-than is a usage error, not the retention setting * fix(sessions): refuse to continue a session prune holds, rather than read it without the lease A resume that started while prune held the session could not take the lease, read the session anyway, and went on to use one prune was free to remove: a read changes nothing prune checks again. The rehydrated read behind the TUI resume, exec --resume and --fork, and ACP session/load and session/resume now fails with ErrPruning at that moment, and none of the three falls back to a raw read on it. ACP load stays best effort about a history it cannot read, but not about this one. Writes are unchanged: a write lands under session.lock, where prune checks the session again. Prune takes the lease through the new Store.HoldExclusive, which the TUI and ACP tests use to hold a session the way prune does. * test(sessions): a lease that cannot be taken keeps the session, and does not stop a resume Prune fails closed when it cannot take a session's lease at all, so a process whose own hold fails the same way can carry on: prune cannot remove the session either. Refusing there instead would stop resume and fork wherever locking does not work, for people who never prune. * fix(sessions): keep a parent forked after the plan, and refuse sessions removed under a caller Three gaps jatmn found in the prune lease rules. A fork made after prune's plan, by a process that has since exited, was not in the plan, so its old parent was removed and the fork was left under a missing ancestor. Once prune holds a parent exclusively it now looks for sessions the plan did not see that name it as parent, and keeps it if there is one. Store.Create with a ParentSessionID, which exec --calling-session-id and spec implementations use, did not hold the parent at all. It now holds it the way Fork and CreateChild do, and all three refuse a parent whose directory is still there without its metadata: prune removes the metadata first and unlinks lease.lock after it, so a lease taken at that moment lands on a fresh lease file and proves nothing. A resume, exec --resume or --fork, or ACP load that picked a session a moment before prune removed it could take a fresh lease and read the session as empty. HoldToContinue now holds the picked session and requires its metadata, and those callers go through it. The rehydrated read on its own still reads a missing session as empty. * fix(sessions): make a dry run decide at removal time like a real prune A dry run appended every planned session to Removed without calling pruneSession, so none of the checks made once a session is held for removal ran: the second lease check, the re-read of the last update, and the scan for a child created after the plan. On the same disk, a dry run could list a session that a real run keeps. pruneSession now takes a dry-run flag, makes every one of those checks under the same locks, and stops before removing anything. The prune command reports a session a dry run could not check as such, not as one it could not remove. |
||
|
|
2c418fc8ff |
fix: unbreak main CI (cron claim tests, Go 1.26.9) (#1124)
* test(cron): pass the history limit in the claim tests #1094 added a limit argument to cron.Store.Runs. #1090 merged shortly after from a branch that predated it, so its two new tests in cron_claim_test.go still called Runs with only the job id and the internal/cli test package stopped compiling on main: internal/cli/cron_claim_test.go:111:38: not enough arguments in call to stores[0].Runs have (string) want (string, int) That failed vet and every job that runs go test. Pass 0, which selects the default MaxRunHistory cap, as the other callers in this package do. * fix: bump Go to 1.26.9 for stdlib vulnerability fixes govulncheck reports ten reachable standard-library vulnerabilities on Go 1.26.6, all fixed in 1.26.9: GO-2026-6603, 6605, 6610, 6611, 6612, 6613 and 6617 in net/http, GO-2026-6608 in net/textproto, GO-2026-6607 in crypto/tls, and GO-2026-6604 in os (Windows only). CI and release workflows read the toolchain from go.mod, so the go directive is the only version to change. Update the stated source-build minimum in the READMEs and install guide to match. |
||
|
|
571af701bd |
fix(mcp): refresh OAuth tokens for discovered endpoints and registered clients (#1113)
Login discovers the token endpoint (RFC 9728 / 8414) and may obtain a client_id/client_secret through dynamic registration, but persisted none of it. Refresh read only the config-file OAuthConfig, which is empty for the standard `auth: oauth` setup, so every expired access token failed with "no token endpoint configured for refresh" and required a new `zero mcp oauth login`. Store the discovered token endpoint (and whether it was server-advertised) and the dynamically registered client on the token, and fall back to them in Refresh. Explicit config still wins, and values already in config are not copied into the store. A stored endpoint is re-validated before use: the shared https/loopback rule always, and for a server-advertised endpoint the same public-network policy and pinned client Login used. The refreshed token keeps the stored fields so later refreshes keep working. Fixes #1101 Co-authored-by: Claude Sonnet 5.5 <[email protected]> |
||
|
|
2eb9d94fac |
fix(cron): preserve newer claims when overlapping runs complete (#1090)
* fix(cron): preserve newer claims when overlapping runs complete Let claimFire own schedule state and increment the current fire count under the store lock. Abort completion writes on read errors rather than restoring a stale snapshot. Cover adjacent slots in both completion orders using independent stores, including failed executions, duplicate claim rejection, and unreadable completion state. Fixes Gitlawb/zero#962 Amp-Thread-ID: https://ampcode.com/threads/T-01a0dc9c-8bc2-72cc-ad0b-6c64e3578df8 Co-authored-by: Pierre Bruno <[email protected]> * fix(cron): avoid stale next-run summary on persist failure Report the next run as unknown when completion state cannot be persisted, as requested in review. Extend the existing corrupt-state regression to verify the summary. Amp-Thread-ID: https://ampcode.com/threads/T-01a0e862-0c9c-753c-9afc-45f9c1082c17 Co-authored-by: Pierre Bruno <[email protected]> --------- Co-authored-by: Amp <[email protected]> |
||
|
|
eefa978f61 |
fix(daemon): do not retry workers that finished with a final exit code (#1112)
The pool stopped retrying only on exit 0 or ExitPermanent (76), so the other codes `zero exec` uses to say a run is over (2 usage, 3 provider, 4 incomplete, 130 interrupted) were treated as crashes and retried up to MaxAttempts with backoff. Exit 4 means the agent already ran, so each retry repeated its side effects, re-billed the provider and appended a second run to the same session stream. Treat those four codes as final: return the worker's exit code with ErrPermanent. Crash-type exits (1, signals) and launch/read errors still retry. Fixes #1100 Co-authored-by: Claude Sonnet 5.5 <[email protected]> |
||
|
|
bea2230633 | chore: remove unused test exports and dead shell-highlight/theme/upstream helpers (#1096) | ||
|
|
df67edec13 |
fix(redaction): redact adjacent AWS keys glued end to end (#1095)
* fix(redaction): redact adjacent AWS keys glued end to end Two AWS keys with no separator between them (AKIA...AKIA...) only had the first redacted: the leading \b in the secret patterns sees a word character on the left of the second key and misses it. Apply each textSecretPattern via a helper that also retries at every redacted span end with a \b-stripped, start-anchored variant of the pattern, so a credential starting exactly where the previous one ended is still caught. Chained runs (three or more glued credentials) are followed to the end. A mid-word occurrence that does not abut a redacted span still does not match. Extracted from #1075 per Vasanthdev2004's suggestion on #969; #1067 remains the fix for the RedactString leak. * Address CodeRabbit critical: bound redactAdjacent span-chaining loop The outer loop re-read len(spans) while the inner loop appended chained spans to the same slice, so appended spans were re-chained and the span count grew exponentially on long glued-key runs (RedactString handles untrusted content). Bound the loop to the original match count and add a regression test with 64 glued AWS keys asserting 64 markers promptly. |
||
|
|
42eb6ab5fb |
fix(cron): bound run history and read recent records from the tail (#1094)
* fix(cron): bound run history and read recent records from the tail Retain the newest 1,000 outcomes under the job lock and publish complete history snapshots. Add bounded reverse reads and regression coverage for legacy logs, failures, and concurrent stores. Fixes Gitlawb/zero#1086 Amp-Thread-ID: https://ampcode.com/threads/T-01a0dcd6-ad63-74d8-8396-45573c57dcec Co-authored-by: Pierre Bruno <[email protected]> * fix(cron): skip oversized legacy history records Discard entire oversized lines during bounded tail reads so compaction can record new outcomes without weakening atomic publication. Cover exact size boundaries and oversized first, middle, and unterminated final records. Amp-Thread-ID: https://ampcode.com/threads/T-01a0e861-ebee-7764-be1d-e67517cd914c Co-authored-by: Pierre Bruno <[email protected]> * fix(cron): append the outcome when the history log cannot be replaced On Windows the atomic replace of runs.jsonl fails while another process holds the log open, which dropped every outcome for as long as it stayed open. Keep the replace, and only when it fails append the record on a fresh line. The leading newline terminates any unterminated tail; the blank line it may leave is already skipped by readRuns. Zero's own readers take the job lock, so they never observe a partial append. Co-Authored-By: Claude Opus 5.5 <[email protected]> * fix(cron): preserve the history DACL and append only on sharing violations Compacting runs.jsonl renamed a temporary file over it, which on Windows replaced a DACL applied to the log with the job directory's inherited one. Publish through ReplaceWithRetry so the log keeps its descriptor. Fall back to appending only when the replace fails with a Windows sharing or lock violation, the error a reader holding the log open produces. Any other failure, such as a persistent access denial, is returned so a log that can never be replaced cannot grow past the retention limit. fsutil.IsSharingOrLockViolation exposes the existing classifier, gated to Windows because errno 32 and 33 mean EPIPE and EDOM elsewhere. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Amp <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]> |
||
|
|
8a68660161 |
fix(daemon): bound pending sessions and prune on completion (#1091)
Reject excess unfinished sessions before registration and dispatch, independently of retained history. Release admission and prune on every terminal path. Amp-Thread-ID: https://ampcode.com/threads/T-01a0dcbe-87ec-74e6-bdd0-6341b0c26d9a Co-authored-by: Amp <[email protected]> |
||
|
|
ab018b3098 |
fix(daemon): stop worker process groups by their launch-time identity (#1088)
* test(daemon): stop what a worker left running after it was reaped A daemon worker is launched into its own process group, but Kill and the Cancel hook pass its PID to background.TerminateProcess, which looks the group up again with Getpgid when it signals. Once the worker has exited and been waited, that lookup fails, only the dead PID is signalled, and a child the worker left running survives. This pins the case with a shell worker that backgrounds a sleep and exits: after the wait, Kill and the Cancel hook must still stop the sleep. Committed ahead of the fix so CI shows it failing on the current behaviour on Linux and macOS. * fix(daemon): stop worker process groups by their launch-time identity Kill and the Cancel hook now go through background.TerminateCommandGroup, which signals the group the worker was launched into (ConfigureChild- ProcessGroup's Setpgid) instead of rediscovering it from the PID, so a worker that has exited, or on Darwin one that is still a zombie, no longer leaves its children running. It is TerminateCommand without the reap: the pool waits on its workers itself and exec.Cmd calls Cancel from inside Wait, so reaping there would be a second Wait. On Windows it is the same tree kill, with the process handle pinned while it runs. A command that has already been waited cannot be pinned; that now reports os.ErrProcessDone, which exec.Cmd's Cancel contract treats as nothing to cancel, instead of the unexported "already released" error. Fixes #861 |
||
|
|
1697b7a5ce |
fix(tui): show provider ownership in Recent and Favorites (#1083)
* fix(tui): show provider ownership in recent and favorite models Display saved provider names in mixed-provider picker groups and include them in row sizing. Preserve provider-grouped rows and selection values. Fixes Gitlawb/zero#840 Amp-Thread-ID: https://ampcode.com/threads/T-01a0d8e9-d346-71f9-bdac-4ac762d810a5 Co-authored-by: Pierre Bruno <[email protected]> * fix(tui): label mixed-group rows with the provider Enter uses Recent and Favorites rows labeled their owner from recent history, but selection falls back to the active provider when that owner was renamed or removed, so the label could name a provider that would not be used. Derive the label from the same decision selection makes (modelPickerSwitchOwner) and store it on the row as OwnerLabel. Cap the owner prefix at 20 cells so an unbounded profile name cannot consume the model name once the overlay reaches its 76-column maximum. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> * fix(tui): distinguish abbreviated model picker owners Budget owner cues against the rendered row width, preserve model names, and distinguish abbreviated saved profiles with numbered suffix cues. Show the highlighted full owner as a wrapped key before selection. Cover shared-prefix and shared-suffix owners, narrow terminals, Favorites, and Enter routing. Amp-Thread-ID: https://ampcode.com/threads/T-01a0e862-13e1-756e-9608-58d5fed96f05 Co-authored-by: Pierre Bruno <[email protected]> * fix(tui): preserve model labels when owner cues cannot fit Split owner cues onto a second line when the model needs the row width. Check the actually rendered row before suppressing the full owner key, so owner text hidden inside a clipped model label cannot hide that key. Amp-Thread-ID: https://ampcode.com/threads/T-01a0e862-13e1-756e-9608-58d5fed96f05 Co-authored-by: Pierre Bruno <[email protected]> * fix(tui): fit multiline model rows and mouse targets Fit the model picker item window to its rendered viewport height and share that window with mouse hit-testing. Account for group headers and two-line rows so clicking either line selects the correct model and the keyboard-selected item remains visible. Amp-Thread-ID: https://ampcode.com/threads/T-01a0e862-13e1-756e-9608-58d5fed96f05 Co-authored-by: Pierre Bruno <[email protected]> --------- Co-authored-by: Amp <[email protected]> Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]> |
||
|
|
6b5852dc18 |
fix(sandbox): say when a session's sandbox enforcement is degraded (#1082)
* fix(sandbox): say when a session's sandbox enforcement is degraded Only `zero doctor` and `zero sandbox policy` reported a degraded sandbox, so a TUI session or a `zero exec` run went ahead with the native sandbox effectively off and nobody saw it. Both now say so once, before the first prompt or tool call, naming the downgrade reason: exec on stderr next to the grant migration notice, the TUI as a system notice when it opens. The line comes from the session's own engine through Backend.BuildPlan, the plan `zero sandbox policy` prints, so the notice and the command that explains it cannot describe two different sandboxes. Only the degraded level speaks: a disabled sandbox is the user's choice, and the unelevated Windows tier still enforces the write jail. Exec tests that expect a quiet stderr now compare against the notice this host prints under the default policy, since they run on the host's own backend and a Linux runner without the helper is degraded while macOS and Windows are not. Fixes #1041 * fix(sandbox): say so when the nesting markers skip wrapping With ZERO_SANDBOXED and ZERO_SANDBOX_BACKEND set, BuildCommandPlan passes every command through unwrapped on the word of an outer sandbox nothing verifies (#727). The notice came from Backend.BuildPlan, which does not model that path, so a native backend reported full enforcement and a session started with the markers ran every command unwrapped in silence. The engine's notice now mirrors the runner and names the markers and the unverified outer sandbox. A disabled sandbox still says nothing. hostSandboxNotice builds its expectation through the engine too, and the tests that pin the notice clear the markers so a suite run inside a Zero sandbox does not change their answer. * fix(tui): say the startup notices again when a session is resumed /resume rebuilds the transcript from an empty one, so the startup notices went with the old transcript and the session it switched to ran under a degraded sandbox without a word. The model keeps the notices and puts them back first, before the resume summary. Rewind and compaction stay in the same session and keep the notice in the scrollback above their divider. Also pins why the notice can be decided once, at startup: grants made during a session only open the network or add allow and deny paths, never the mode, and every mode but disabled already needs the platform sandbox, so no grant reaches a degraded command plan the startup notice missed, and a deny path the backend cannot enforce is refused instead. * fix(tui): say the startup notices again when /new opens a session /new cleared the transcript and added only its own note, so the new session started under a degraded sandbox without a word, the same gap /resume had. It now puts the notices back first, the way /resume does. /clear stays in the same session and, like rewind and compaction, keeps the notice in the scrollback above its divider. * fix(modelregistry): start no background refresh when the fetch is turned off The TUI and exec each started a goroutine that called RefreshModelsDevCache, which reads ZERO_DISABLE_MODELS_FETCH and the cache path when it runs. A test that turned the fetch off through t.Setenv could finish and have the setting restored before that goroutine ran, and the refresh would then fetch into the developer's real cache. StartModelsDevRefresh reads the setting before starting anything, so with the fetch off no refresh exists to outlive the test. * test(cli): keep the degraded-notice tests out of the developer's user state runExecWithSandbox and the TUI launch test left the sandbox grant store and the models.dev cache at their defaults, so they read the developer's sandbox-grants.json and could migrate it, and could refresh the real models cache. Both now run under isolateCLIUserState, and both join TestCLILaunchHelpersLeaveUserStateAlone so the isolation is pinned for each of them. * fix(cli): stop exec when the degraded-sandbox notice cannot be written The notice goes out before any tool runs, and before MCP servers and plugins start through the same sandbox, but a failed write to stderr was ignored, so the run went ahead with reduced isolation and no warning delivered. It now stops with exitCrash, the way the image and reasoning-effort notices in the same function already do. |
||
|
|
aa52040498 |
fix(deps): refresh the lockfile to patched hono and @hono/node-server (#1080)
The lock pinned hono 4.12.27 and @hono/node-server 1.19.14 through tuistory 0.10.0, and npm audit reports advisories against both. tuistory already declares ^4.11.7 and ^1.19.9, so the patched releases fit the existing ranges: hono 4.13.9 and @hono/node-server 1.19.17. package.json is unchanged. npm also re-syncs the lock's root entry with package.json, which main had already changed without regenerating the lock: license MIT, android in os, and no install script. |
||
|
|
547330197b |
fix(oauth): give every loopback callback server the bounded shutdown (#1079)
* fix(oauth): give every loopback callback server the bounded shutdown The loopback OAuth callback server was written out three times: the shared oauth.LoopbackListener, MCP's Login and OpenRouter's login. Each ended with Shutdown under a one-second deadline, and Shutdown only closes idle connections. A client that has sent part of a request header is not idle, so it survived the deadline, and its goroutine and the Serve goroutine stayed alive after the flow returned (#1028). #1051 fixed that for LoopbackListener (a header read bound, force-close when Shutdown runs out of time, and a join on Serve) and left the other two exactly as they were, because each was its own copy. The lifecycle now lives once, in oauth.CallbackServer, with #1051's behavior unchanged: ReadHeaderTimeout of 5s, Shutdown within 1s, Close() on a missed deadline, and Close returning only after Serve has. LoopbackListener uses it through an unexported constructor that keeps its ConnState test hook, and MCP's Login and OpenRouterLogin use StartCallbackServer, which both packages could already import. Each call site gets a regression test that stalls a raw client mid-header ahead of the real callback and then requires the stalled connection to be closed once the login returns. The ordering does the synchronising: the stalled client is dialled before the real callback, Serve accepts in order, and the login cannot finish until the real callback has been served, so no sleep is involved. Fixes #1028 * test(oauth): pin the callback server's header bound and its join on Serve Two of the three things CallbackServer does had no test that noticed their removal, the same as when #1051 introduced them on LoopbackListener: the five-second header read bound, and Close waiting for the Serve goroutine. The header test stalls a client mid-header and never calls Close, so it can only pass on ReadHeaderTimeout; the bound is a package var so the test can shorten it. It synchronises on StateNew. Go reports StateActive only after a request read returns, and a stalled header read returns only when the header timeout fires, so a helper waiting for StateActive depended on the bound under test and turned its removal into a SETUP INVALID instead of a failure. The join test uses the case where the join matters: with no connections open, Shutdown closes the listener and returns at once, so without the join Close can return before Serve does. Without it the test failed 10 runs out of 10. |
||
|
|
7f11491078 |
fix(acp): publish sub-run sessions loaded over ACP as view-only (#1078)
* fix(acp): publish sub-run sessions loaded over ACP as view-only session/load registered a fully promptable session for any session kind, including the child, side and spec sub-runs that every other resume path refuses through sessions.IsResumableKind: the TUI, exec sessions, the exec CLI, and ACP's own session/resume. So a sub-run could be opened and continued as a standalone conversation through ACP and nowhere else, with whatever its parent run relied on about that sub-run's lifecycle no longer true. Load has a real render-only use, a client rebuilding a transcript it can scroll, so it still loads those sessions and still replays their history. What changes is that the session it publishes for a non-resumable kind is marked read-only, and session/prompt refuses it with an invalid-params error that says why. runTurn is reached only from session/prompt, so that one gate covers every way a turn can run; set_mode and the config options only change settings. The flag is decided at first registration and never changes. Resume refuses a non-resumable kind before it registers anything, and session/new always creates a standalone session, so the only path that can publish one of these ids is load, which now publishes it read-only. TestACPResumeAppliesStandaloneSessionKindPolicy claimed "session/load should retain render-only access", but its prompt check ran before the load, so it only proved the refused resume never registered the session. It now prompts after the load as well. TestACPLoadPublishesSubRunsReadOnly checks every kind in both directions, so a gate that refused everything would fail on the resumable kinds, with a setup guard that the table agrees with IsResumableKind. Fixes #1045 * test(acp): require resumable sessions to be promptable after load, not merely unrefused TestACPLoadPublishesSubRunsReadOnly decided the outcome from the error text: "refused as view-only" was false for any error other than the view-only one, so a regular or fork session whose prompt failed for some other reason still passed. With every writable prompt made to fail with an internal error, the old test stayed green. Raised by CodeRabbit on the PR. Resumable sessions now have to prompt successfully. For the sub-run kinds the decision is the RPC code, and the message is checked only to tell the view-only refusal apart from other invalid-params errors such as an unknown session id. |