From d5cf2a1c409d449324f944613c63d752e17b360e Mon Sep 17 00:00:00 2001 From: garrytan Date: Sat, 31 Oct 2026 21:53:09 +0000 Subject: [PATCH] docs(sealed): hex lists count as identifiers; a \u escape always parses (Step 14.5 audit) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- changelog.d/capy-gst620-sealed-identifiers.md | 4 ++-- docs/pregate.md | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/changelog.d/capy-gst620-sealed-identifiers.md b/changelog.d/capy-gst620-sealed-identifiers.md index 588978845..4d429059d 100644 --- a/changelog.d/capy-gst620-sealed-identifiers.md +++ b/changelog.d/capy-gst620-sealed-identifiers.md @@ -1,8 +1,8 @@ **The sealed guard can now refuse a published record that puts a label next to an identifier.** gstack-evals hides which test units are decoys. Its /ship on garrytan/gstack-evals#99 published a panel where each unit's `stratum`, `tags` and `decoy_set` sat next to its `head`, `tree_sha256`, `plan_bytes` and `files`, values a model under test can compute from its own copy of the work. A model could look itself up and learn it was the decoy. No answer leaked, so `gstack-pregate sealed` said `clear` (GSTA-2, GST-620). The new rule is opt-in. A repo that sets `sealed.label_keys` gets it, and every other repo sees the same output as before, byte for byte. ### Added -- **`sealed.label_keys` and `sealed.identifier_keys` in `.gstack/pregate.json`.** With `label_keys` set, `sealed.label_join` checks every .json, .jsonl, .ndjson, .csv, .tsv and Markdown pipe table a published commit adds, modifies, renames or copies. A record that holds a label field and an identifier (a field matching an `identifier_keys` glob, by default `head`, `*_sha256`, `*_bytes`, `files` and similar names, or any 7 to 64 character hex string, as a value or as the key of an object) is a join. The guard fingerprints each join in memory and compares it with the parent's version of the file, so unchanged rows and reformatting stay clear while a replaced, added or widened row refuses even when the row count stays the same. Findings name the file, the commit and the field names (`keys=stratum,tags,head,plan_bytes`), never the values, on stderr, in `--json` and in the pre-push hook. -- **`sealed.label_join_unchecked`.** A file whose text never names a label key, ignoring double quotes, cannot hold a join and is skipped without parsing, so a commented `tsconfig.json` passes. A supported file over 4 MiB, one that names a label key and does not parse, one nested deeper than 64 levels, or a blob git cannot read refuses instead of passing unread. The `SEALED:` line counts these as `unchecked=`. Like every sealed finding, it can be waived through the base branch's `.gstack/sealed-allow.tsv`; `allow_paths` never exempt either rule. +- **`sealed.label_keys` and `sealed.identifier_keys` in `.gstack/pregate.json`.** With `label_keys` set, `sealed.label_join` checks every .json, .jsonl, .ndjson, .csv, .tsv and Markdown pipe table a published commit adds, modifies, renames or copies. A record that holds a label field and an identifier (a field matching an `identifier_keys` glob, by default `head`, `*_sha256`, `*_bytes`, `files` and similar names, or any 7 to 64 character hex string, as a value, in a list, or as the key of an object) is a join. The guard fingerprints each join in memory and compares it with the parent's version of the file, so unchanged rows and reformatting stay clear while a replaced, added or widened row refuses even when the row count stays the same. Findings name the file, the commit and the field names (`keys=stratum,tags,head,plan_bytes`), never the values, on stderr, in `--json` and in the pre-push hook. +- **`sealed.label_join_unchecked`.** A file whose text never names a label key, ignoring double quotes, cannot hold a join and is skipped without parsing, so a commented `tsconfig.json` passes. A file with a `\u` escape is always parsed. A supported file over 4 MiB, one that names a label key and does not parse, one nested deeper than 64 levels, or a blob git cannot read refuses instead of passing unread. The `SEALED:` line counts these as `unchecked=`. Like every sealed finding, it can be waived through the base branch's `.gstack/sealed-allow.tsv`; `allow_paths` never exempt either rule. - **The policy only tightens.** A field can count as both a label and an identifier, so adding identifier names to `label_keys` never hides a join. Both lists are the union of the base branch's and the pushed policy's, each resolved to its default first, so a pushed config can turn the rule on or widen it but cannot turn it off or narrow it. ### Changed diff --git a/docs/pregate.md b/docs/pregate.md index 89989b233..92df222c9 100644 --- a/docs/pregate.md +++ b/docs/pregate.md @@ -1006,8 +1006,9 @@ gstack-pregate sealed [--range ] [--remote origin] [--base ] [--js matches an `identifier_keys` glob (`*` is the only wildcard; the default is `head`, `head_sha`, `sha`, `commit`, `tree`, `tree_sha*`, `*_sha`, `*_sha256`, `sha256`, `digest`, `*_digest`, `hash`, `*_hash`, - `*_bytes`, `bytes`, `size`, `files`, `lines`), or any string value of 7 - to 64 hex characters with at least one digit and one letter a to f. An + `*_bytes`, `bytes`, `size`, `files`, `lines`), or any string value (or a + list holding one) of 7 to 64 hex characters with at least one digit and + one letter a to f. An object key of that shape counts as an identifier of the object it holds (shown as `(key)`), so a map keyed by sha joins too. Numbers count only through the field name. A field can be both a label and an identifier, and @@ -1022,7 +1023,9 @@ gstack-pregate sealed [--range ] [--remote origin] [--base ] [--js and a replaced, added or widened record refuses even when the count stays the same. A file whose text never names a label key, ignoring double quotes (so `"Strat"um` still names `stratum`), cannot hold a join, so - it is skipped without parsing (a commented `tsconfig.json` passes). A + it is skipped without parsing (a commented `tsconfig.json` passes). A file + with a `\u` escape, which could spell a label key, is always parsed, so a + commented one is `sealed.label_join_unchecked`. A supported file over 4 MiB, one that names a label key and does not parse (one bad JSONL line is enough), one nested deeper than 64 levels, or a blob git cannot read is `sealed.label_join_unchecked`. It refuses too, and the