Merge pull request #4666 from open-gsd/fix/4652-containment-at-boundaries

This commit is contained in:
Tom Boucher
2026-09-12 11:44:24 -04:00
committed by GitHub
12 changed files with 818 additions and 14 deletions

View File

@@ -1177,6 +1177,20 @@ from `todos/pending/` to `todos/completed/` and upserts `completed:` and
`status: completed` inside the file's frontmatter block. Unknown flags are
rejected loudly.
`<filename>` is a **basename inside the todos root**, not a path. A basename
guard runs first, before `<filename>` is joined onto any directory: a value
containing an embedded separator (either `/` or `\`, e.g. `sub/name.md` or
`sub\name.md`), a value whose own basename differs from itself (e.g.
`a/../../b.md`, `../sibling.md`), a bare `.` or `..`, an absolute path (e.g.
`/etc/passwd`), or a NUL byte is rejected as a usage error **before** any file
is read or moved (#4327, #4652). A traversal that only escapes the `pending`/
`completed` subdirectory without leaving the todos root (`../sibling.md`) is
caught by this same guard, not by containment. Containment against the todos
root still runs afterward as defense-in-depth for the resolved source and
target paths, so neither half of the move can land outside the root. The
check covers both halves of the move, and `--dry-run` is rejected on the same
terms rather than previewing a resolved outside path.
```bash
# UAT audit — scan all phases for unresolved items
node gsd-tools.cjs audit-uat

View File

@@ -81,6 +81,18 @@ the gate context; all others are left untouched for `sh` to interpret:
An undefined placeholder interpolates to the empty string.
**`--phase-dir` is confined to the project.** The value is validated to resolve
inside the project root before any predicate is evaluated; one that escapes is
rejected as a usage error rather than evaluated. This applies to both kinds —
`artifact-frontmatter-equals` resolves its artifact under that directory, and
`command-exit-zero` interpolates it into `${PHASE_DIR}` — so an unconfined value
would let a **blocking** gate return `block: false` on evidence from a directory
the caller chose (#4354). An absolute path inside the project is still accepted;
absolute is not a synonym for escaping. `${PHASE_DIR}` always interpolates the
**resolved absolute path**, even when `--phase-dir` was given as a relative
value — a command relying on `${PHASE_DIR}` staying relative must not assume
that.
**Sandbox.** cwd = project root; env = inherited from the GSD process; killed
(SIGTERM) on timeout. The command runs as the user, on the user's machine —
there is no sandbox boundary vs. the user's own shell. See ADR-2008 "Trust