diff --git a/CHANGELOG.md b/CHANGELOG.md index 488849c80..870935964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.91.346.0] - 2026-10-31 + +**`sealed.local_path` now flags a home path inside a shell default, `${UBI_RUNNER:-/home/…}`, as it already flagged the same path after `?? "/home/…"` in TypeScript.** gstack-evals `quick/laneb/runner/launch.sh:9` passed the scan because the `-` before the path counted as part of a word (#3568). + +### Fixed +- **`sealed.local_path`** (`lib/sealed-guard.ts`): a home path directly after a `${…:-` or `${…-` default now matches, for a name, an array element (`${arr[0]:-`, a subscript of up to 64 characters, so a long line of unclosed `${a[` cannot make the scan quadratic), an indirect name (`${!ref:-`), a positional number or a special parameter (`${@:-`). `:=`, `:+` and `:?` already matched, because `=`, `+` and `?` were never word characters. A hyphen anywhere else still excludes the match, so a path glued to a word (`docs-…`, `$VAR-…`, `${VAR}-…`) stays unflagged; docs/pregate.md lists that as a known limit. + +### Gate integrity +- This is a gate edit that only tightens. The guard now refuses lines it passed before. No existing test, threshold or allowlist changed. A new case in `test/sealed-guard.test.ts` covers all five operators, a positional parameter, `${@`, an array element, an indirect name, a Windows path and the TypeScript `??` form. It also checks six negative controls, which are two hyphenated paths, a hyphenated sentence, `$VAR-`, `${VAR}-` and a relative default. Without the fix it misses every `-` form. + ## [1.91.345.0] - 2026-10-31 **A capped `/ship` on Capy now spends fewer agent turns and counts only its own tokens, while every reviewer still gets exactly the brief 1.91.331.0 gives it.** Two new commands do `/ship`'s fixed shell work, a short driver file replaces reading the whole skill when a parent agent runs `/ship`, and the budget measures each reviewer batch on its own instead of across the parent's work. Replays of gstack-evals #34 and #36 (GSTA-167) spent 12.19M tokens on Steps 12-17 and 0.25-0.4M on each parent turn; these changes cut turns, not reviewer context. diff --git a/VERSION b/VERSION index bb998b909..5572e2159 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.345.0 +1.91.346.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 38ea47900..5f4464e09 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.345.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.346.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/docs/pregate.md b/docs/pregate.md index a55d05e75..03de0f36a 100644 --- a/docs/pregate.md +++ b/docs/pregate.md @@ -805,8 +805,10 @@ gstack-pregate sealed [--range ] [--remote origin] [--base ] [--js - **Rules.** `sealed.path` and `sealed.transcript` check every path a commit adds, renames or copies, whatever its content (binary, empty, deleted again in a later commit); `sealed.answer`, `sealed.seed` and `sealed.local_path` - (`/home//`, `/Users//`, `C:\Users\\`) check each commit's added - lines. Findings print rule, `file[:line]` and commit, never the text. + (`/home//`, `/Users//`, `C:\Users\\`, including after a shell + default such as `${VAR:-…}` or `${VAR-…}`) check each commit's added lines. + Known limit: a path glued to the text before it, by a hyphen (`docs-…`, + `$VAR-…`, `${VAR}-…`) or a letter (`-I…`, `-L…`), is not matched. Findings print rule, `file[:line]` and commit, never the text. - **Globs.** `paths`, `transcripts` and `allow_paths` match the whole repo-relative path from the repository root. `*` and `?` stay inside one path segment; `**/` spans zero or more directories; a `**` not followed by diff --git a/lib/sealed-guard.ts b/lib/sealed-guard.ts index 976a44860..7ba8b261d 100644 --- a/lib/sealed-guard.ts +++ b/lib/sealed-guard.ts @@ -47,7 +47,7 @@ export type Visibility = 'public' | 'private' | 'unknown'; const KEYS = ['paths', 'transcripts', 'answer_markers', 'seed_patterns', 'local_paths', 'allow_paths', 'case_insensitive', 'visibility']; const MAX_ENTRIES = 64; const MAX_REGEX = 256; -const LOCAL_PATH_RE = /(?:^|[^A-Za-z0-9_.~-])(?:\/home\/[^/\s'"`]+\/|\/Users\/[^/\s'"`]+\/|[A-Za-z]:\\+Users\\+[^\\\s'"`]+\\)/; +const LOCAL_PATH_RE = /(?:^|[^A-Za-z0-9_.~-]|\$\{!?(?:[A-Za-z_][A-Za-z0-9_]*(?:\[[^\]\s]{0,64}\])?|[0-9]+|[@*#?$!-]):?-)(?:\/home\/[^/\s'"`]+\/|\/Users\/[^/\s'"`]+\/|[A-Za-z]:\\+Users\\+[^\\\s'"`]+\\)/; const PUBLIC_TTL_MS = 24 * 3600_000; const PRIVATE_TTL_MS = 3600_000; diff --git a/package.json b/package.json index d2f4f3651..880ef7e12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.91.345", + "version": "1.91.346", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/test/sealed-guard.test.ts b/test/sealed-guard.test.ts index 1d159e69e..878f36dc0 100644 --- a/test/sealed-guard.test.ts +++ b/test/sealed-guard.test.ts @@ -102,6 +102,36 @@ describe('pre-push hook: public destination refuses each kind of sealed output', } }); + test('local_path matches a home path after every shell parameter-expansion operator, and still skips hyphenated words', () => { + const positives = [ + 'RUNNER="${UBI_RUNNER:-/home/user/.capy/drive/runner}"', + 'RUNNER="${UBI_RUNNER-/home/user/runner}"', + 'RUNNER="${UBI_RUNNER:=/home/user/runner}"', + 'RUNNER="${UBI_RUNNER:+/Users/bob/runner}"', + 'RUNNER="${UBI_RUNNER:?/home/user/runner}"', + 'ROOT="${1:-/Users/bob/work}"', + 'ROOT="${@:-/home/user/work}"', + 'ROOT="${arr[0]:-/home/user/work}"', + 'ROOT="${!ref:-/home/user/work}"', + 'ROOT="${WIN_ROOT:-C:\\Users\\bob\\work}"', + 'const dir = process.env.UBI_WORK ?? "/home/ubi/work";', + ]; + const negatives = [ + 'see docs-/home/user/x for the old layout', + 'mount at data-/Users/bob/cache', + 'RUNNER="$UBI_RUNNER-/home/user/runner"', + 'RUNNER="${UBI_RUNNER}-/home/user/runner"', + 'RUNNER="${UBI_RUNNER:-./runner}"', + 'a well-known, self-hosted, run-to-completion job', + ]; + const s = scratch({ '.gstack/pregate.json': policy(SEALED) }); + commit(s, { 'runner/launch.sh': [...positives, ...negatives].join('\n') + '\n' }); + const r = hook(s); + expect(r.code).toBe(1); + const flagged = [...r.err.matchAll(/sealed\.local_path\s+runner\/launch\.sh:(\d+)/g)].map(m => Number(m[1])); + expect(flagged).toEqual(positives.map((_, i) => i + 1)); + }); + test('path rules ignore content: a binary file, an empty transcript, an add-then-delete and a rename into a sealed path all refuse', () => { const binary = scratch({ '.gstack/pregate.json': policy(SEALED) }); commit(binary, { 'heldout/questions.bin': Buffer.from([0, 1, 2, 0, 255, 0]) });