A branch that carries the five open config.rs and shim changes at once, so the refactor above them has one tree to stand on - #130
A branch that carries the five open config.rs and shim changes at once, so the refactor above them has one tree to stand on#130HackingGate wants to merge 12 commits into
Conversation
… token `prevent-author-mismatch` was declared by hand in 8 policy files across one consuming superproject and its submodules, byte for byte in every one: same id, same built-in, one `pre-commit` stage, no parameters. The guard is already compiled in, so what was missing was a set to carry the declaration. `mismatched-author` is that set, and it is its own rather than a rule inside an existing one. `unowned-push` asks where a ref is going; `host-identity` reads the running machine and then searches committed content for what it read, which is why it ships `stages = []`; `commit-message-residue` is about the text a commit records. This one asks who is writing, read off `git var` before a message exists. Its ceiling is the single `pre-commit` stage, so it cannot grow a second one without a diff on the line that says so. The header writes down what it costs -- a deliberate `--author` is refused as readily as an accidental one -- and the one thing it cannot do: with no global `user.email` there is no expectation to compare against, so it says so and declines. That is the container case verbatim, and a guard that quietly does not run in its own scenario reads like one that ran and found nothing. `no-hardcoded-home-paths` takes the leading `(?:^|[^A-Za-z0-9_.-])` boundary the same fleet had added to its shadow. Without it the pattern matches `/home` wherever those five characters land inside a longer token, and each such finding is a report on a file holding no home path at all. The corpus records both halves: the two ways a real path still opens a match, and the two shapes the boundary now lets through. Not taken: the same shadow's carve-out for a `u` placeholder user. That is a house convention tied to one repository's own record, and a bundled rule exempting a one-letter username would be exempting a real one. `policy/base/sets.lock.json` regenerated from the binary, not edited. Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
`uphold hook` consulted the literal rules and the text-capable guards and never the prose rules, so every `prose_regexp` rule was dark at the MCP seam -- the whole bundled `prose-shapes` set among them, and those sets declare `command.before = ["gh", "git push"]` precisely because the text they judge is what an agent posts through `mcp__github__*`. `text::failures_in` excludes the prose rules on purpose, and each of the other whole-text seams carried its own compensating call to `prose::over_text` beside it. Four seams assembled the same ingredients by hand, so a rule kind added to three of them was silent in the fourth with nothing anywhere to say so. `text` now owns the answer. `Judged` names every kind of rule a piece of published text is judged by, `Seam::consults_every` says which kinds each of the four seams asks, and `text::judged` is the one assembly the three whole-text seams call. The shim keeps its own loop, because it holds a subject and a per-rule scope that function is not given, but it classifies each rule through `Judged::of` and asks the same table -- which also closes a second hole of the same shape: its editor pass dispatched `regexp` and `require_regexp` to the pattern checker and left `prose_regexp` to fall through to the `exec` consultation, where it was refused as a dispatch hole. Exit codes and report text are unchanged everywhere they already existed. The kinds run in one order, literals then guards then prose, which is the order each seam already printed the kinds it ran. Closes #111 Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
Five ways past this seam, reproduced against the built binary standing on PATH. Four of them ended in exit 0 with nothing printed, which is the shape of failure this tool exists to refuse. A renamed verb was one of them. A `match` list names verbs literally and every command here lets a person rename one, so `git -c alias.p=push p origin HEAD:refs/heads/x` and a persisted `[alias] p = push` both presented the verb `p`, matched nothing, and exec'd a push to a public forge unexamined. Where nothing matches, and only there, the word is resolved once against the command's own alias mechanism and matched again against the expansion. Asked of the real command rather than of whatever PATH resolves, because PATH resolves to this shim. A shell alias and a lookup that failed are could-not-looks and not absences: exit 2, since what a shell runs is not an invocation any table can read and it may well be a push. The refspec collector read one grammar where the matcher reads two. The matcher tries both arities for an option it cannot classify and matches under either; the collector took the bare reading and skipped two words, so `git --attr-source HEAD push origin` checked the remote's nickname while the branch went out unread. Both readings now, and where they disagree about which names are being published the answer is exit 2 rather than a guess. That needed git's own grammar filling in: the value-taking globals it was missing, and the `push` vocabulary, without which the disagreement fires on `git push -f origin topic`. A scope that could not be evaluated stood every checker down. `public-target` is the one predicate that asks somebody else, and a forge that cannot be asked answered `false` -- the same word as "the policy decided these checks do not apply here". So `prevent-unowned-target`, whose own contract is exit 2 on a destination it could not resolve, was skipped exactly where the lookup failed. Three answers now, and the third refuses before the command runs. `unresolved = "run"` on a `[[shim]]` opts back into the old behaviour and is refused at load beside a scope that cannot produce that answer. `--all`, `--mirror` and `--tags` name no refspec and publish many, and the fallback read HEAD, so a mirror push of forty branches was checked as one. They are enumerated with `for-each-ref` and each name is read. A leading `+` is grammar and no part of a name, and leaving it on made the force-push the way past a rule standing in front of the branch it names. `send-pack:*` joins `push:*` in the shipped table and in the checkers that name it: the plumbing publishes the same names from the same positions. The two stand-downs that remain -- an unresolvable subcommand, and `UPHOLD_ALLOW=all` -- keep their behaviour and are now stated as what they are: exit 0, the command runs, and a stderr line saying no checker did. A contract nobody wrote down is one a reader can only learn by reading the source. Also: `git.hooks` takes five names and one of them is `manual`, which is not git's. ADR 0001 and the type's own doc comment said githooks(5). Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
… not a pass Three seams were answering a two-valued question that has three answers, and each one spent the third answer on a green tick. WHAT A BLOB TURNED OUT TO BE. `guard::unicode` had the three-answer reader: byte-order mark first, git's NUL test for binary, and anything else that is not UTF-8 is unreadable and exit 2. Every other reader of the same blobs decoded with `String::from_utf8_lossy` or tested for a NUL and skipped. So a UTF-16 file naming a private repository was replacement characters with NULs between them in the tracked scan, a skipped image in the staged scan, and a decoded document in the codepoint guard, all in one run over one blob. The reader moves to `guard::scope`, beside `read`, because which bytes an operation introduces and what those bytes turned out to be are two halves of one question. The message readers go through it too: a message is text by construction, so bytes that will not decode are a message nobody read, which is exit 2 rather than a skip. What cannot be decoded joins the guard's could-not-look accounting, beside a name the forge would not answer for and outranked by a name that was found. A HOST NOBODY CAN ASK. `gh` answers for github.com and for nothing else, and the guard listed six other forges by hand and dropped every other host in silence: `github.acme.com/acme/secret`, a self-hosted forge, anything nobody had thought of. That is a silent third answer in the guard whose subject is that there is no such thing. Any `host.tld/owner/repo` this tool cannot ask about is unavailable now, which is exit 2, and what quiets one is a declaration rather than a list compiled in: `foreign_hosts`, host globs, at the top of the policy file for the reason `private_owners_from` is there and on a rule where one corner of the tree wants its own. Which hosts a repository cites is policy, which is `parameterize-do-not-enumerate` and what the hand list stood against. An owner whose escaped name would not compile into a pattern was dropped mid-search with nothing printed, so the operator's list said one thing and the guard looked for another. That is a config error at load now, as is a host glob that will not parse. EVERY CHECK READS THE SAME TEXT. `allowed_scripts` decoded under a covering `encoding` rule and stopped the run where nothing declared one. `regexp`, `forbidden_literals` and `require_regexp` reached the tree through a searcher with binary detection off and a lossy sink, so the same UTF-16 file was exit 2 for one check and clean for the other three. One reader in `scan` decides now -- UTF-8 where it is UTF-8, the declared charset where a rule declares one, the byte-order mark otherwise -- and hands text to the engine, which no longer opens files at all. A file nothing decodes goes into the unreadable list, which is reported beside the findings rather than instead of them; binary is the one skip. `require_regexp` gains the most: a file it could not read was a violation about a marker that may well have been in it. Beside the fixes: `redact_matches` was the one field a policy file may write that no document named, so REFERENCE.md documents it and a structural test now holds every public field of the serde-facing structs to being named there in code. ROADMAP said the Python script ships the reconcile step, which `uphold check` has done since it moved into the binary; README says what the script still does. DESIGN's account of which bytes a guard reads was missing the commit messages a push publishes. Closes #112 Closes #113 Closes #114 Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
…y no longer does Twenty-eight host globs at the top of the policy file, and every one of them was being answered by a list compiled into the binary that nobody could see or edit. Without them `no-private-repo-names-in-files` reports 55 names on hosts `gh` cannot be asked about, which is exit 2 at `pre-push` and at `manual`: a licence URL, a bibliography, a standards reference and a fixture publishing to `example.com` are all `host.tld/a/b`, which is the shape a repository name has and is not one. Four of the entries are documentation rather than citation and are noted as such in the file: `host.tld`, `github.acme.com` and `gitlab.com` are the worked examples the reference and this guard's own tests need to be able to write, and the `*.example.*` family is what every fixture publishes to. A working tree with an older uphold installed will find its `git` and `gh` shims failing closed on this file until the binary carrying `foreign_hosts` is installed, because a shim loads the policy with the uphold that is on PATH and an unknown field means the policy does not load. That is the same trade a new bundled set makes, and the cure is the same: install the build from this branch. Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
…gration-2026-09-02
…nt of
`gh pr edit` was unavailable to an agent whose token lacked read:org, so it
ran `gh api -X PATCH repos/OWNER/REPO/pulls/N -F body=@file` -- the same body,
the same account, the same public tracker -- and no `match` list named `api`,
so the shim exec'd it with nothing printed and exit 0. The same hole stood in
front of `gh api -X POST .../issues`, `.../comments`, `.../releases` and every
`glab api`.
The verb is not one a `match` list can simply name, and that is why it was
missing rather than forgotten. One word is both the read side and the write
side of a forge, so standing in front of `api` outright would stand in front
of every GET on the machine -- including the `gh api repos/OWNER/REPO --jq
.visibility` this shim itself runs to answer `public-target`, which is a shim
standing in front of its own lookup. Which half an invocation is, is a
question about its argv.
So `api:*` is matched, and a second question is asked of the one verb that
has to answer it: does this call carry a body at all. It does when
`-X`/`--method` names something other than GET, or when any field or `--input`
is present, which is when gh switches to POST anyway. A GET with no fields is
left exactly where it was -- unmatched, unexamined, exec'd.
The call is read with the verb's own grammar rather than through the table's
flag lists, because they disagree: `-F` is `--body-file` on `gh pr create` and
`--field` on `gh api`, so one vocabulary answering for both reads a `key=@file`
pair as a body file or a body file as a field. Every field value becomes a
text subject, with `@file` read from the file and `@-` from stdin, replayed to
the command on the way through the way a `--body-file -` already is. An
`--input` file is judged one string value at a time where it is JSON, because
the encoding otherwise hides the sentence a prose rule stands in front of
behind its escapes, and whole where it is not -- and JSON here means a
document that starts with `{` or `[` and parses as one, not YAML's superset
reading, under which an ordinary Markdown body with a `Note:` line parses as a
mapping and most of it reaches no checker.
The endpoint path is the destination. `repos/OWNER/REPO/...`, and GitLab's
`projects/OWNER%2FREPO`, resolve to the same `owner/repo` a `--repo` resolves
to on every other verb, so `prevent-unowned-target` and a `public-target`
scope read a `gh api` call exactly as they read a `gh pr create` one. A path
naming no repository -- `gh api graphql`, `gh api user` -- falls through to
the table's own resolver, which is the honest bound: the destination of a
GraphQL mutation is inside its query and nothing in a path can say.
Five tests: a PATCH whose `-F body=@file` trips a text rule refuses at exit 1,
a GET with no fields runs, an `--input` JSON is refused for one string value
and runs when that value is ordinary, a body bound for an owner this
workspace does not have is refused by the destination guard while the same
call under its own owner runs, and a GitLab project id is the same
destination with its separator escaped.
Closes #125
Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
An unaskable host was one answer for every name written against it, and exit 2 for all of them made the cure "enumerate every host you cite" in every consuming repository. That is the enumeration ADR 0001 refuses, moved out of the binary and into eighty policy files, which is worse than where it started: a consumer publishing a pull-request body was refused over `claude.ai/code/<id>`, and this tree needed twenty-eight host globs to say its bibliography is a bibliography. The split is by owner, because the owner is what the rule is about. A name under a declared `private_owners` owner, or under the `owner` this policy says the workspace is, on a host `gh` cannot be asked about, is could-not-look and exit 2: that is the name this rule exists to keep off a public target, and silence about it is not a pass. Every other `host.tld/a/b` goes back to the unresolved bucket it was in -- reported on the way past, refused only where the repository has set `refuse_unknown` -- because a DOI, a licence URL and an encyclopaedia article all have the shape of a repository name and none of them is one. `foreign_hosts` keeps its job and gains no new one: it says a host is not a forge at all, and it quiets both rows. Nothing is quieted by default. The twenty-eight globs come back out of this repository's own policy. They were load-bearing only under the wider default, the guard passes at `manual` without them, and a policy field carrying a list nobody needs is the enumeration again wearing a smaller number. Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
…gration-2026-09-02
|
Warning Review limit reachedNext included review available in 31 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (31)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
❌ Your patch status has failed because the patch coverage (82.64%) is below the target coverage (100.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #130 +/- ##
==========================================
- Coverage 94.12% 93.39% -0.74%
==========================================
Files 37 37
Lines 13493 14136 +643
==========================================
+ Hits 12700 13202 +502
- Misses 793 934 +141 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
A branch that carries the five open config.rs and shim changes at once, so the refactor above them has one tree to stand on.
Merges, in PR-number order, from origin/main:
promote-fleet-rules-- themismatched-authorset, theno-hardcoded-home-pathsboundary, the regeneratedsets.lock.json.one-text-judge--text::Judged,Seam::consults_every,text::judged, and the three whole-text seams rewritten onto them.shim-hardening-- alias expansion, two readings of a push refspec,the three-answer scope predicate and
unresolved = "run", ref enumeration.unknown-is-not-a-pass-- one decoder for a blob,foreign_hosts,and one decode per file at the top of the scan. Merged twice: once at
1a8c6bb, and again at 54b4260 after that branch narrowed the foreign-host
refusal to declared private owners.
gh-api-body-- theapi:*verb in the shippedghandglabshimtables and the
ApiCallreader that stands in front of it. It is based onshim-hardening, so it arrives on top of The shim reads the command line the command will read #123 here as it would on main.Conflicts resolved
None. Every merge applied without a conflict marker:
docs/REFERENCE.md; it is the only file the two branchesshare, and they write different sections of it.
docs/REFERENCE.md,src/config.rsandsrc/shim.rs.src/shim.rsis the one worth naming, because One table of what published text is judged by, reached from every seam #117 and The shim reads the command line the command will read #123 both rewriteit substantially and git took both hunks: One table of what published text is judged by, reached from every seam #117's two
Judged::ofclassification passes survive inside The shim reads the command line the command will read #123's reworked matcher, which is
checked below rather than assumed.
ROADMAP.md,docs/REFERENCE.md,policy/principles.tomland
src/config.rs. Insrc/config.rsThe shim reads the command line the command will read #123 adds the per-shimunresolvedfield and What a file's bytes say is decided once, and a host nobody can ask is not a pass #124 adds
foreign_hostsat policy and rule scope; the two landin adjacent regions of the same serde structs and both survive, which the
load-time refusal tests on each side confirm. The re-merge at 54b4260
auto-merges
docs/REFERENCE.mdandpolicy/principles.toml.docs/REFERENCE.mdandsrc/shim.rs. BothJudged::ofsites are still there after it, now at
src/shim.rs:2395andsrc/shim.rs:2769, beside the sevenApiCallsites the branch adds.Nothing was dropped and nothing was rewritten by hand, so each branch's own
intent is on this tree as its author wrote it. In particular this branch edits
no policy allowlist of its own: the
claude.aiquestion that the firstforeign-host reading raised is answered in #124 at 54b4260, not here.
What was checked, not assumed
cargo buildandcargo testafter every merge, not only at the end:725 tests after Who is writing a commit is a question of its own, and a path is not a token #116, 731 after One table of what published text is judged by, reached from every seam #117, 740 after The shim reads the command line the command will read #123, 750 after What a file's bytes say is decided once, and a host nobody can ask is not a pass #124,
753 after the What a file's bytes say is decided once, and a host nobody can ask is not a pass #124 re-merge, 758 after A forge API call that carries a body is a verb the shim stands in front of #126. 25 suites, all passing.
cargo clippy --all-targetswith warnings denied,cargo fmt --check,and
prek run --all-files: clean on the merged tree.src/shim.rsstill classifies throughcrate::text::Judged::ofin bothpasses, which is the seam One table of what published text is judged by, reached from every seam #117 opened and both The shim reads the command line the command will read #123 and A forge API call that carries a body is a verb the shim stands in front of #126 rewrote around.
cargo run -- rules --sets --jsonbyte-matchespolicy/base/sets.lock.jsonon this tree, so Who is writing a commit is a question of its own, and a path is not a token #116's regenerated lockfile is still the true one after the
other four land on top of it.
Note on the installed binary
#124 puts a
foreign_hostsline inpolicy/principles.toml, and an upholdpredating that field refuses the whole file rather than skipping the line.
A
gitorghshim resolving to an older binary therefore fails closed inthis tree until
cargo install --path .is run from this branch. That is thetrade #124 documents; it is named here because it bites anyone who checks this
branch out with a released binary on PATH.
This is a merge-order aid
It exists so a refactor of
src/config.rshas one tree carrying all fivechanges, not to be the thing that lands them. The five pull requests are the
reviewable units and each should be merged on its own terms. Once they are all
on main this branch has nothing left to say, and the maintainer should close it
unmerged.
Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd