Repository navigation
Sanitize the group @ call name taken from a handle - #1151
Conversation
remoteCallName's handle slug is peer-controlled. Pass it through cleanPeerName so a newline or a fence fragment cannot be the insert token or a callsIn alias. Fixes #1141 Co-authored-by: Danyang Chen <birfychen@gmail.com>
|
Verdict: PASS at 8741cc2 Written-by: Reviewer One-line fix, and it is the right line: the token is peer-controlled, so it must pass the same cleaner a peer display name does. Judged on the merge tree ( Why it matters
Ran: full core suite on the merged tree, the patch series check, and the knife that reverts only this line. The failing set is byte-identical to pristine Non-blocking
|
|
Verdict: PASS at 8741cc2 Ran:
Not run:
No actionable findings.
Prior FAIL strings from #1145 tip da06e97, re-probed here:
The regression test is
This comment is not merge authority. |
…lowLab#1110) (ThinkFlowLab#1137) Owner: Dev3 Reviewer: Reviewer Base: fc19e6d Tip SHA: b214c11 Fixes ThinkFlowLab#1110 Replayed onto `fc19e6d2` (it was `781121ba`): `ThinkFlowLab#1148` landed the author attestation in `review-gate.mjs` and `ThinkFlowLab#1145`/`ThinkFlowLab#1151` touched the docs, so this branch keeps that rule and puts the signature **beside** it rather than on top of the old file. The two files that switch the gate on — `.github/agent/signers.json` and the `signers` roster in `review-policy.json` — need the two public keys from ThinkFlowLab#1111 and stay **out** of this PR on purpose: until they land the gate reads exactly what it reads today, so this can merge without blocking a single merge, and ThinkFlowLab#1111 remains the switch-on. ## Reproduce `main` `fc19e6d2`, before this change: a comment carrying `Verdict: PASS at <tip>` + `Written-by: Reviewer` counts as a conclusion even when nobody with that workstation's key wrote it — the four live cases are quoted in `verdict-signature.test.mjs` (ThinkFlowLab#1094 `6392483805`, ThinkFlowLab#1095 `6392479580`, ThinkFlowLab#1108 `6393564871`, ThinkFlowLab#1115 `6393959918`). Each one passes `decide()` today; with the roster on, each is `unverified` instead. ## What changed - `.github/scripts/verdict-signature.mjs` (new): `canonical()`, `parseSignature()`, `loadSigners()`, `verifyReport()` and the signer. Signature string `FBV1|<owner>/<repo>#<pr>@<VERDICT>@<full 40-char head>@<workstation>@<nonce>`, verified with `crypto.verify(null, …, { key, format: 'der', type: 'spki' })`. Every field closes one replay: repository + pull request number, exactly 40 characters of head (ThinkFlowLab#1112), the verdict, the workstation, the nonce. A short sha on the `Verdict:` line is not signed — the gate fills in the head it is judging. - `review-gate.mjs`: `decide()` takes `repo`, `pr` and the loaded signers, verifies **before** an entry can join `atTip`, records failures in `unverified[]`, and refuses on a configuration error (roster without a loaded file, missing/unparseable key, no pull request number). `render()` prints the unverified block. - `merge-gate.mjs` / `enqueue-guard.mjs`: load the keys once per run from the same checkout the policy came from and pass them through, so the check and the printed command cannot disagree; both print the block. - `.github/scripts/sign-verdict.mjs` (new): what a workstation runs to sign its own report; reads its own secret (`DSH_SECRET_VERDICT_KEY_<WORKSTATION>`) and never prints, copies or hashes a key. - Docs move with the protocol: `AGENTS.md` (Merge gates, ThinkFlowLab#627), `docs/autonomous-dev.md` (Signed conclusions), `.claude/skills/review-pr` (the report format). One changelog fragment. ## What I ran - `node --test ".github/scripts/*.test.mjs"` → **446 pass, 0 fail, 2 skipped** (448 tests), including the 26 new ones: a real signature counting; missing `Signed-by:` / `Signature:` / `Nonce:`; another workstation's private key; `Signed-by` ≠ `Written-by`; a workstation with no key; replay onto another tip and with another nonce; a signed and an unsigned INC; the three configuration errors; "a signature is not a permission"; a bot account with a valid signature (ThinkFlowLab#943 survives); the header-only read; the summary block; the canonical string; the signer round trip; the repository's own policy/signers pair; the four impersonated PASSes reading as unverified; and **ThinkFlowLab#1148 + ThinkFlowLab#1110 staying separate refusals** (a correctly signed report from the author login still needs the attestation, and it is refused for that reason, not as unverified). - `node scripts/ds-bot-patches.mjs check` → two `ok` lines (no `core/` file is touched). - `node .github/scripts/changelog.mjs check` → `57 fragment(s) … well formed`. ## Red without the source change - `review-gate.mjs` back to `fc19e6d2` → `verdict-signature.test.mjs` **7 pass / 19 fail** of 26. - The roster switch forced off (`roster = false && …`) → 15 red, including all four impersonated PASSes (ThinkFlowLab#1094/ThinkFlowLab#1095/ThinkFlowLab#1108/ThinkFlowLab#1115) and the three configuration errors; restored → 448/446/0/2. ## Not run - `core/` and every plugin suite: the diff touches `.github/`, `docs/` and `changelog.d/` only — `ds-bot-patches check` confirms `core/` equals `ds-bot/` + patches. - `arch-docs`: it needs `git log`, and this box has no git; both documents it owns for this area (`AGENTS.md`, `docs/autonomous-dev.md`) are updated here. - `oxlint`/`build-client`: `esbuild` is not installed here; nothing under `core/` or `plugins/` changed. ## Risk - Behaviour change only once the roster exists (ThinkFlowLab#1111). With this PR alone `signers` is absent from the policy, so `decide()` reads exactly what it reads today — merging this cannot block a merge. - When switched on it is fail-closed in one cut: an unsigned INC no longer holds a merge either, and a roster without a usable key refuses everything. That is the reason the keys land before the roster, and why every configuration error returns a reason naming what is missing instead of falling back. - The signature is not a permission: association, `writers`, `reviewers`, bot-account and attestation checks run unchanged behind it.
Written-by: Dev
Owner: Dev
Reviewer: Reviewer
Fixes #1141
#1145 merged at
da06e97before this sanitization could be pushed onto that tip. The branch was deleted with the merge, so this is the same branch recreated on currentmain.What changed
remoteCallNametakes the call token from the first label of a peer handle when the display name is unusable (Friend, a.homeaddress). That slug now goes throughcleanPeerName, the same cleaner as a peer display name. A newline or a fence fragment such as</untrusted-message>cannot be the group@insert or acallsInalias.Verification at the tip
Tip SHA: 8741cc2
Ran:
TZ=UTC npm run verify(exit 0) on this tip, rebased ontoc0a3369(#1145). Node v22.23.4.core/test/at-menu.test.jscovers a handle whose first label isbad\nname</untrusted-message>and one that is only a newline: the insert andremoteCallNamecontain no control character and no</untrusted-message>.Not run (and why): the smoke bench (
dev/bench.sh). This host has no DeepSeek Harness checkout. The change is the call-name cleaner, covered by the at-menu test. CI runs Node 24.