mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-12 19:13:28 -10:00
test/tty-exec-test-root
793
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a4578dacc0 |
test(tools): stop the tty session before removing its root
TestExecCommandTTYSkipsInteractiveBlock runs ssh to a bogus host under tty with a 250ms yield. When ssh is still resolving the host after the yield, the session keeps running in the test's directory and nothing stops it, so on Windows t.TempDir's cleanup fails with a sharing violation. It failed Smoke (windows-latest) on a README-only PR. Stop the session in a cleanup, and build the tool on execTestRoot, which retries the removal while Windows still holds the directory right after the stop, as the other exec tests that leave a session do. |
||
|
|
164150633a |
feat(skills): install a skill's bundled files and point the skill tool at them (#1167)
* feat(skills): install a skill's bundled files and point the skill tool at them A skill was only its SKILL.md: install copied that one file, and the skill tool returned only the markdown body, so a skill whose instructions run its own scripts was broken by installing it, and a hand-placed skill directory gave the model no way to find the files beside it. Install now copies the whole skill directory, leaving out hidden entries (.git, .env) and symlinks, which it neither follows nor copies but reports. A script stays executable; everything else is installed 0644. A tree over 1,000 files or 32 MiB is refused. skills.lock records a treeHash beside the SKILL.md hash; skill info reports drift in bundled files, and an entry from before this is still checked on SKILL.md alone. A reinstall that changed only a bundled file is reported as an update. Both skill tools end their output with the skill's directory and a list of up to 50 bundled files when there are any; a SKILL.md-only skill is returned unchanged. Closes #584 * fix(skills): compare a reinstall over an old lock entry with SKILL.md alone A lock entry written before tree hashes has no treeHash, so the reinstall compared the new tree hash with an empty string and always counted as an update. Re-adding an unchanged skill that is only a SKILL.md printed "Updated" and "the files bundled with it changed" when there were none. That entry comes from an install that copied SKILL.md alone, so compare with that: it is an update when SKILL.md changed or the reinstall now brings bundled files. InstallResult gains previousTreeHash, and the CLI uses it to say the bundled files are now installed rather than changed on the first reinstall over such an entry. * fix(skills): match the tree hash when recovering an interrupted install lockReconciler matched both trees of an interrupted workspace on the SKILL.md hash alone. A reinstall that changes only a bundled file keeps SKILL.md, so a kill between the tree swap and the lockfile publish left two trees carrying the recorded hash, and recovery kept the new one and retired the backup the lockfile still described. A tree now matches an entry with a tree hash only when both hashes match. An entry from before tree hashes is still matched on SKILL.md alone. A tree hash that cannot be computed is returned as an error, the way an unreadable SKILL.md already is, so recovery decides nothing about a tree it could not read. Also bring the package comment up to date: install copies the skill directory, not SKILL.md alone. |
||
|
|
5d080ef46b |
test(tui): pin the cwd in tests that assert text is absent from the view (#1170)
* test(tui): pin the cwd in tests that assert text is absent from the view Nine tests built their model with an empty Options.Cwd, so newModel took the package directory and the title bar showed the developer's real branch and checkout path. Each then asserted that some text was absent from the whole view, and failed wherever that branch or path happened to contain it: TestSuggestionOverlayRenders on a branch with /model in its name, TestSuggestionOverlayCapsRowsWithoutMoreText on a branch or path containing "more" or "compact", the provider wizard tests on "offline", "chatgpt" or "gpt-4.1", and so on. They now pin Options.Cwd to isolatedCwd, a path that does not exist, so no branch is read and the path is the same on every machine. A guard test checks that isolatedCwd stays outside any git checkout. * test(tui): make isolatedCwd fully qualified and absent by construction The fixed /workspace/zero path could exist on some machine, a dev container for one, and newModel would then read whatever is there. On Windows it was also only drive-relative: \workspace\zero is not a fully qualified path. isolatedCwd is now a child of the home TestMain isolates that nothing creates, so it is fully qualified, absent and outside any checkout on every platform, and the title bar shows ~/not-a-checkout everywhere. A t.TempDir path would have put the user name and the test name into the view. The guard test checks all three properties. * test(tui): clear the branch in view-test models instead of trusting the cwd isolatedCwd is a child of the isolated home, which lives under the temporary directory. With TMPDIR inside a checkout, gitBranch walks up from that absent path to the checkout and puts its branch in the title bar, so the guard failed on an ordinary branch and a colliding branch broke the nine view tests again. newViewTestModel now builds those models: it sets the isolated cwd and clears the branch after construction, since no path can promise there is no checkout above it. The guard keeps only what a path can promise (fully qualified and absent), and a new test puts the home inside a checkout on feat/model-picker and checks that neither that branch nor the real checkout path reaches the view. * test(tui): assert no checkout path reaches the view in the enclosing-checkout test The test checked the branch strings and that the isolated cwd shows, but a view could show both the isolated cwd and a real checkout path without failing it. It now also asserts that neither the enclosing checkout nor the directory the tests run from appears in the view. |
||
|
|
1d233c0a9b |
test(modelregistry): clear ZERO_DISABLE_MODELS_FETCH in the models.dev tests (#1169)
The variable turns off both the models.dev fetch and the cached overlay, and four tests in modelsdev_test.go inherited it from the shell. With it set, TestRefreshModelsDevCacheRejectsBadBodyWithoutClobbering and TestDefaultModelEntriesAppliesFreshCache fail, and TestCachedModelsDevProvidersIgnoresStaleCache and TestCachedModelsDevProvidersRequiresOptIn pass without reaching the check they are named for: the nil they expect comes from the variable. Clear it in each of them, as the two sibling refresh tests already do. |
||
|
|
de1443b2ed |
fix(deps): refresh the lockfile to hono 4.13.13 (#1171)
GHSA-5r4p-p66f-jhc7 (moderate): hono's serveStatic decodes the request path a second time, so middleware on static paths can be bypassed. Versions 4.12.4 to 4.13.10 are affected and main pins 4.13.9 through tuistory, which release packaging installs as a local-control helper. npm update takes hono to 4.13.13, inside every range that asks for it. package.json is unchanged and nothing else in the lock moves. |
||
|
|
84c1b15540 |
fix(oauth): reuse the crash-safe secret store and sync encrypted publications (#1165)
* fix(oauth): use the crash-safe secret store and sync token publications The encrypted-file OAuth backend carried its own AES-GCM secret implementation that had drifted from internal/securefile: a crash during first login left <secret>.lock behind and every later Save timed out instead of reclaiming it, and the secret and token blobs were published without fsync.\n\nUse securefile.Crypter with an opt-in publication directory so the secret still publishes through the sandbox-denied .secret.publish directory, add file and directory barriers to the encrypted token blob, and extend securefile with the publication directory plus destination-directory sync.\n\nMove the concurrent-creation and wrong-sized-secret coverage to securefile, add a stale-lock regression through the OAuth store, and test publication-dir use, sync ordering, sync-failure preservation, and retry after a failed secret sync. The stale-lock test fails on the previous code. * test(oauth): cover post-rename directory-sync failures The existing failure tests inject a file sync error before the rename. Add coverage for a destination-directory barrier failure after the token or secret has been atomically replaced: the write must report the error and the newly published bytes must remain readable. Also cover the corresponding securefile secret path, including its expected size. * fix(securefile): never clobber a newer lock when restoring a reclaimed one reclaimStaleLock could rename a lock that had just been replaced, then restore the quarantined copy with os.Rename, replacing a third holder's new lock. Both holders could then publish different secrets for the same file. Restore with O_CREATE|O_EXCL and discard the quarantined copy when the destination is already held. Extend the directory-sync failure test to prove a later caller actually loads the published secret, not just its size. Add restore tests for the free and already-held cases. |
||
|
|
6393a90ed6 |
fix: bump Go to 1.26.10 for GO-2026-6809 (#1168)
govulncheck now reports GO-2026-6809 (crypto/x509 distrusts compromised intermediate certificates) against Go 1.26.9, reachable through daemon.WriteFrame, so Security & Code Health fails on main and on every open PR. It is fixed in 1.26.10. CI and release builds read the toolchain from go.mod. The stated source-build minimum in README.md, README_ZH.md and docs/INSTALL.md moves with it. |
||
|
|
cd3e0b0a9f |
test(cli): isolate the exec image tests from the models.dev refresh (#1161)
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. |
||
|
|
aa9ba8412e |
fix(tools): grep stops where a file's line breaks stop (#1160)
* fix(tools): grep stops where a file's line breaks stop
scanGrepFile treated EOF after a trailing line break - and an empty file -
as the START of another line, and handed the matcher an empty line one
number past the real content. Every pattern that matches an empty string
then reported a match on a line no reader sees:
a.txt holds "x\n"; grep pattern "^$"
-> a.txt:2: (grep -c '' GNU-style reports no such line)
same file, pattern ".*", count mode
-> "2 matches found" (one real line, one phantom)
an empty file, pattern "^$"
-> a.txt:1: (the file has no lines at all)
read_file and the file tracker's countLines already state the rule the
scan violated: "the trailing newline does not open a line", and an empty
file has no lines. The scan now stops there too, so anchored and
empty-matching patterns can only match the lines the content carries.
Real matches, numbering, count-per-hit accounting for non-empty patterns,
file mode, glob and symlink handling are unchanged.
* test(tools): make the grep line-number parity test catch the phantom
The parity test matched only "needle", a pattern that cannot match the
empty line the old EOF branch invented, so its absence-of-line-4
assertion passed on the code being fixed. The pattern now also matches an
empty line, and the expected output is asserted exactly: on the pre-fix
scanner the same run reported
grep = "a.txt:2: needle two\na.txt:4: ",
want exactly the line read_file numbers 2
so the phantom is visible right beside the real line, and the test is
about the numbering claim it states. Also gofmt-aligns the case table
that made the CI formatting check fail.
|
||
|
|
a97db8832f |
fix(redaction): redact credentials whose trigger a greedy span swallowed (#1158)
Two credentials glued end to end share one recovering property: the second
one's trigger prefix is where redaction can re-anchor. For fixed-length
bodies that anchor is exact (AKIA{16} stops at the next key), which is the
case the anchored end-chaining always covered. For unbounded bodies it
isn't: github's [A-Za-z0-9]{36,} body eats the literal "ghp" of the token
glued behind it, so the first match ends PAST the second trigger, the scan
that re-anchors at the span end finds nothing there, and the second
credential leaves the output as +_bbbb... / .eyJzdWIi... plaintext:
RedactString("ghp_aaa..36a..ghp_bbb..36b..")
-> "[REDACTED]_bbbb......" (second token's body printed in full)
RedactString(jwtA+jwtB)
-> "[REDACTED].eyJzdWIiOiI5ODc2...SA" (second token's payload+sig)
The same holds across shapes (a github key directly feeding "sk-ant-..."
through its body) and for three tokens in one run.
redactAdjacent now follows the run a match starts: at each boundary it
re-binds the match at the EARLIEST trigger occurrence inside the span that
stills validates as its shape (the head before the trigger must match the
anchored pattern exactly, and the run chained from the trigger must reach
past the span, so a mid-body lookalike and a mid-span cut that recovers
nothing leave the span alone), and continues under whichever shape matched
the swallowed trigger. Candidate examination is capped at
maxTriggerOccurrences per span so body spam of trigger lookalikes costs
what the span costs, and the first validating cut wins.
End-chaining for fixed-length bodies, single-credential shapes, custom
replacement templates, and the split path's announced no-op-for-contiguous
text guarantee are all unchanged; the regression tests pin the glued pairs
that previously leaked plus the AWS chaining and lone-token cases.
|
||
|
|
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) |