v1.91.346.0 fix(sealed): local_path flags a home path inside a shell default (${VAR:-/home/...}) (#3569)

* fix(sealed): local_path flags a home path after a shell default (${VAR:-/home/...})

Fixes #3568

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes (array, indirect and special parameter defaults; docs limit; changelog example)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: bound the array subscript in LOCAL_PATH_RE so a hostile line stays linear

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(changelog): end the two colon sentences the prose lint flags

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-10-31 02:34:18 +00:00
1 parent 21277291c8
commit 514daebf5f
7 files changed
+48 -6

No files matched your search

+10
View File
@@ -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/<user>…}`, as it already flagged the same path after `?? "/home/<user>…"` 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.
+1 -1
View File
@@ -1 +1 @@
1.91.345.0
1.91.346.0
+1 -1
View File
@@ -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,
+4 -2
View File
@@ -805,8 +805,10 @@ gstack-pregate sealed [--range <a..b>] [--remote origin] [--base <branch>] [--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/<user>/`, `/Users/<user>/`, `C:\Users\<user>\`) check each commit's added
lines. Findings print rule, `file[:line]` and commit, never the text.
(`/home/<user>/`, `/Users/<user>/`, `C:\Users\<user>\`, 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
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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",
+30
View File
@@ -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]) });