diff --git a/CHANGELOG.md b/CHANGELOG.md index 63444c6d8..d8557a3e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,13 @@ ## [Unreleased] -**`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/..."` 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). +**`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 `${NAME:-` or `${NAME-` (a name or a positional number) now matches. `:=`, `:+` and `:?` already matched, because `=`, `+` and `?` were never word characters. A hyphen anywhere else still excludes the match, so `docs-/home/x/`, `$VAR-/home/...` and `${VAR}-/home/...` stay unflagged. +- **`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]:-`), 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, a Windows path and the TypeScript `??` form, and checks six negative controls (hyphenated words, `$VAR-`, `${VAR}-`, a relative default). It fails without the fix, missing the four `-` forms. +- 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: two hyphenated paths, a hyphenated sentence, `$VAR-`, `${VAR}-` and a relative default. Without the fix it misses every `-` form. ## [1.91.301.0] - 2026-10-29 diff --git a/docs/pregate.md b/docs/pregate.md index b12e6bdf3..824d6b8b2 100644 --- a/docs/pregate.md +++ b/docs/pregate.md @@ -786,8 +786,9 @@ gstack-pregate sealed [--range ] [--remote origin] [--base ] [--js 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\\`, including after a shell - default such as `${VAR:-/home//}` or `${VAR-…}`; a path glued to a word - by a hyphen, `docs-/home/x/`, is not flagged) check each commit's added lines. Findings print rule, `file[:line]` and commit, never the text. + 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. - **Policy.** The union of the destination base branch's `sealed` and the pushed commit's: a push can tighten it, never relax it. A malformed or unreadable policy refuses. Waivers come only from the base branch's diff --git a/lib/sealed-guard.ts b/lib/sealed-guard.ts index 7fd357249..5386f87c2 100644 --- a/lib/sealed-guard.ts +++ b/lib/sealed-guard.ts @@ -43,7 +43,7 @@ export type Visibility = 'public' | 'private' | 'unknown'; const KEYS = ['paths', 'transcripts', 'answer_markers', 'seed_patterns', 'local_paths', 'visibility']; const MAX_ENTRIES = 64; const MAX_REGEX = 256; -const LOCAL_PATH_RE = /(?:^|[^A-Za-z0-9_.~-]|\$\{(?:[A-Za-z_][A-Za-z0-9_]*|[0-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-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/test/sealed-guard.test.ts b/test/sealed-guard.test.ts index bb0369d72..fe82a7fe7 100644 --- a/test/sealed-guard.test.ts +++ b/test/sealed-guard.test.ts @@ -110,6 +110,9 @@ describe('pre-push hook: public destination refuses each kind of sealed output', '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";', ];