feat(queue): Piece D, reviews as issue comments, raw per-seat token helper (row 12, #1508)
queue move ID in-review posts the review request as a Gitea comment and review record reads verdicts back, so reviews stop being files in docs/plans/reviews/. On a comment round, in-review to waiting-on-jason now needs every listed reviewer's approval for the current round, the same as in-review to done (Filbert r1 C1). scripts/gitea-api.sh reads the raw per-seat token files (lead decisions 37 to 39): config built and checked before curl starts, export attribute cleared, fixed base URL. test-queue.sh skips its live checks outside the canonical root. Darkwing authored. Filbert approved D r2 (cf1d3fd0) after r1 (a2dc2302) and corrected the plan (293747cd). Rocko reviewed the helper (e896192f, 2096b0a3), and Sage's lead check passed under decision 38. Manifest b402fb38, 19 files. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
# Queue D (#1508, row 12), candidate for review, round 2
|
||||
|
||||
Darkwing, 2026-09-27. Piece D is review requests, per plan section 8.9
|
||||
(`agents/filbert/work/queue-as-data-plan-2026-09-26.md`). Moving a row
|
||||
with reviewers to in-review posts one request comment on its issue as the
|
||||
acting seat, and the log records what happened. The row closes on the
|
||||
reviewers' recorded verdicts. The candidate also carries the DEFERRED
|
||||
item Sage attached to rows 12 and 13: `test-queue.sh` skips its live checks
|
||||
outside the canonical root.
|
||||
|
||||
Base is 8ffbd73b. The patch applies unchanged to HEAD cdcedb27, which
|
||||
since then has changed only `lead-decisions.md`, `QUEUE.md` and
|
||||
`queue.json`. Filbert reviews D. The helper patch is separate
|
||||
(`helper.md`); Sage approved it in lead decision 39, and it ships in the
|
||||
D commit. Sage commits. Nothing is committed, staged or pushed.
|
||||
|
||||
Round 2 fixes Filbert's C1
|
||||
(`agents/filbert/work/queue-d-review-r1-2026-09-27.md`): on a request
|
||||
round, in-review→waiting-on-jason now makes the same checks as
|
||||
in-review→done. See "Waiting-on-jason on a request round" below.
|
||||
|
||||
## Files
|
||||
|
||||
`build.patch` (sha256
|
||||
`28d0790e797e1f24f295f70b2e0162bb1b4b0a154b220c0527f7d6624f0aa1c8`) changes 7 files and adds 3.
|
||||
`build-manifest.sha256` (sha256
|
||||
`af57ead2697a88d74a9214783dfa35ba3bd171aa32a594584f018181e261e956`) pins all 10 after the
|
||||
patch. In a fresh clone at cdcedb27 the patch applies and the result
|
||||
matches the manifest 10/10.
|
||||
|
||||
- `packages/queue/src/review.mjs` (new): the credential check, the request
|
||||
body, the helper call under the deadline, and the reading of each
|
||||
answer.
|
||||
- `packages/queue/src/queue.mjs`: `SEMANTICS` 2, the five review verbs,
|
||||
the round, attempt and receipt shapes, and the rules for moves and done.
|
||||
- `packages/queue/src/store.mjs`: the request step after the move is
|
||||
logged, the resolve check, `review verify-commit`, and the retry and
|
||||
settle hints.
|
||||
- `packages/queue/src/cli.mjs`: the `review` subcommands.
|
||||
- `packages/queue/README.md`: a "Review requests" section, verbs, exit 3,
|
||||
known limits, tests.
|
||||
- `scripts/test-queue.sh`: the live checks skip outside the canonical root.
|
||||
- Tests: `review.test.mjs` (new, 20 tests), `fixtures/fake-gitea.mjs`
|
||||
(new), `fixtures/kill-at.mjs` (a step can be `NAME#N`, the Nth time it is
|
||||
reached), `store.test.mjs` (two A2 tests set row 9's reviewers to none
|
||||
first, so their moves post nothing; a note there moves from filbert to
|
||||
sage, since filbert is no longer a reviewer). 122 tests at A2, 142 now.
|
||||
|
||||
Outside the patch:
|
||||
|
||||
- `helper.patch` (sha256 `48edd46b93c54908e9d59737aab78a33e007ddfafa1e6ad4d34333015bb75430`): raw per-seat token files in
|
||||
`scripts/gitea-api.sh`, lead decision 37. Rocko reviewed two rounds,
|
||||
and Sage approved the result (decisions 38 and 39); notes in
|
||||
`helper.md`. D's tests don't need it, but the live round does.
|
||||
- `tools-md.patch` (sha256 `d30d65b842bd56077ccd9164eccd946f2aa9f53290d02eba61f284960d230e14`): `docs/TOOLS.md`, the review
|
||||
commands and the raw token file. It is Sage's file, so it's a proposal.
|
||||
|
||||
## How a request goes
|
||||
|
||||
1. **Intent.** `move ID in-review --candidate C --op OP` on a row with
|
||||
reviewers logs the round and its first attempt, `requesting`, under the
|
||||
lock, like any other op. If that write doesn't finish, nothing is sent.
|
||||
2. **Pre-send.** Without the lock: the credential file check (`lstat`
|
||||
only), then `GET user`, which must return the acting seat's login, or
|
||||
`jarvis` for sage. A failure here is `failed`, and the POST never runs.
|
||||
3. **POST** one comment on the round's issue through
|
||||
`scripts/gitea-api.sh`, under `timeout -s KILL 30`. The body carries
|
||||
two markers: `<!-- mosaic-queue-op: OP -->` and
|
||||
`<!-- mosaic-queue-round: row=N round=R candidate=DIGEST -->`.
|
||||
4. **Outcome.** A second entry, `OP.outcome`, under the lock. It holds the
|
||||
HTTP status, the comment id and a fixed detail string. Nothing from the
|
||||
response body is logged or printed.
|
||||
|
||||
Exit 0 is posted, 1 is failed (400, 401, 403, 404, 422, or pre-send), and
|
||||
3 is uncertain (anything else). A same-op retry prints the logged state and
|
||||
sends nothing. An uncertain or `requesting` attempt prints where to look
|
||||
and the two commands that settle it.
|
||||
|
||||
## Choices I made
|
||||
|
||||
- **The CLI shape of `resolve`.** The plan has
|
||||
`review resolve ID --attempt REQOP --posted <id>`. I made it
|
||||
`review resolve ID REQOP --comment N`, and abandon takes the attempt the
|
||||
same way. The attempt is always required, and `--comment` is the flag
|
||||
name `record` uses for a comment id too.
|
||||
- **Resolve checks the author.** The plan lists issue, marker, round and
|
||||
candidate. I added the comment's author: it must be the requester's
|
||||
login, fetched with the resolver's own token. Otherwise anyone could
|
||||
copy the markers into a comment of their own.
|
||||
- **Done on a request round** needs an approval recorded by every listed
|
||||
reviewer in the current round, and refuses `--evidence`. If the reviewers
|
||||
were removed after the round opened, done refuses until a privileged
|
||||
actor sets them. A round from before D, or on a row with no reviewers,
|
||||
closes with `--evidence` as in A2.
|
||||
- **Waiting-on-jason on a request round** (round 2, C1). The plan's table
|
||||
let the owner move in-review→waiting-on-jason with no other check, so a
|
||||
Jason-gated row could reach Jason, and then close, with no reviewer's
|
||||
verdict. The move now refuses while a request is unresolved, and on a
|
||||
request round it needs an approval recorded by every listed reviewer.
|
||||
`requireApprovals` in `queue.mjs` holds the approval checks that done
|
||||
and this move share. A round with no request is unchanged, so v1 replay
|
||||
is too. Filbert approved round 2 and asked for one more check (n1): an
|
||||
approval from an earlier round doesn't count in the current one. The
|
||||
Jason-gated test now has rocko approve round 2 first, and the move
|
||||
refuses until filbert approves round 2 as well. Jason gets no exemption, because the owner makes this move.
|
||||
In-review→in-progress, the changes path, is unchanged.
|
||||
- **The owner records no verdict,** even when listed as a reviewer.
|
||||
- **A late outcome on a done row.** `review-outcome` is the one entry a
|
||||
done row accepts, so a POST that answers after the row closed is still
|
||||
recorded. It can only set `conflict`; nothing reopens the row, and
|
||||
`review resolve` on it refuses without fetching the comment.
|
||||
- **A late `uncertain` after a resolve** keeps `posted`: the resolve saw
|
||||
the comment. A late `failed` after a resolve is `conflict`, because the
|
||||
server said it refused a comment someone found.
|
||||
- **Semantics per entry.** Every log entry already records `semantics`.
|
||||
Entries at 1 replay under A2's rules, so the live log (rev 13, all
|
||||
semantics 1) loads unchanged. Review verbs need 2, checked in the entry
|
||||
shape and again in apply.
|
||||
- **Body limit.** A body over 60,000 bytes is a pre-send `failed`, and
|
||||
nothing is sent. A 700-line manifest is enough to reach it.
|
||||
- **`verify-commit` on a commit candidate** compares every path the
|
||||
candidate changed against its first parent, by blob and mode, and
|
||||
requires the paths it deleted to be absent in REF. A `diff-tree` line it
|
||||
can't parse refuses with exit 1 instead of being skipped.
|
||||
- **The claim refusal message.** `ownerOrPriv` read "darkwing cannot
|
||||
resolve a request on". It now names the owner and the privileged actors
|
||||
in every case, with the claim first when there is one. No A2 test
|
||||
matched the old text; the resolve test pins the new one.
|
||||
- **`test-queue.sh`.** After genesis it reads `canonicalRoot` from
|
||||
`HEAD:docs/plans/queue.json` and compares it with the real path of the
|
||||
toplevel. If they differ, it prints
|
||||
`skip queue verify and render --check: this checkout (TOP) is not the queue's canonical root (CANON)`
|
||||
and passes. An empty `canonicalRoot` is a failure.
|
||||
|
||||
## Tests
|
||||
|
||||
`review.test.mjs` runs each case in a scratch repository with genesis
|
||||
committed. `scripts/gitea-api.sh` there is a stub that runs
|
||||
`fixtures/fake-gitea.mjs`: same argv and output as the helper, calls
|
||||
logged to a file, rules from a scenario file, and posted comments served
|
||||
back by id. The token files are dummies under the scratch directory. No
|
||||
test reads a real token or opens `~/.mosaic` or `~/.t3`.
|
||||
|
||||
Against the test list in 8.9:
|
||||
|
||||
| 8.9 asks for | Test |
|
||||
|---|---|
|
||||
| a kill before the POST, after it, at the outcome write, while retaking the lock | "a same-op retry after a kill sends nothing, even with a stale view" (steps `pre-send`, `posted`, `outcome`, `locked#2`), and "a held lock at the outcome exits 3" |
|
||||
| posted; each listed 4xx; 5xx; request failed; timeout; 201 without an id | "each transport answer maps to posted, failed or uncertain" (deadline 300 ms) |
|
||||
| a new op while `requesting` or `uncertain` refuses | "an unresolved request blocks a new request, a new round, waiting-on-jason and done", which also reaches waiting-on-jason with a late POST still out, and Jason's close refuses |
|
||||
| (round 2, C1) a Jason-gated row skips its reviewers | "a Jason-gated row reaches waiting-on-jason only on every reviewer's approval": Filbert's steps refuse, then pass after both reviewers approve a later round; reviewers removed refuse too |
|
||||
| a same-op retry after a stale view sends nothing | the kill test |
|
||||
| a late POST after abandon; a late outcome after resolve, same id and another | "late outcomes": rounds 1 to 3, plus a late 500 (stays posted) and a late 422 (conflict); "a late POST on a closed row" (abandoned, approved and closed while the POST was out) |
|
||||
| resolve with the wrong issue, marker, round or candidate | "resolve checks the comment", which adds author, id, 404, 500, a transport failure and the wrong credential |
|
||||
| `GET user` mismatch and timeout | "the pre-send checks" |
|
||||
| the lead posts as jarvis; `sage` as a login refuses | "the pre-send checks" and "the lead's request refuses a token for login sage" |
|
||||
| the credential checks: mode, another seat's path, the default file | "the credential file" (also unset, relative, missing, symlink, a linked directory, and jarvis's path for sage) |
|
||||
| request, changes, new candidate, approval, pinned | "request, changes, a new candidate, approval", which also checks no file lands under `docs/plans/reviews/` or `agents/*/work/` |
|
||||
|
||||
Every test that retries, kills or answers late counts the POSTs the fake
|
||||
saw. None sees a second POST for one attempt.
|
||||
|
||||
## Mutations
|
||||
|
||||
61 hand-written mutants: 29 in `queue.mjs`, 16 in `store.mjs`, 16 in
|
||||
`review.mjs`. Each runs alone against the whole queue test directory.
|
||||
Against the round 2 tests, 59 are killed, the same result as round 1.
|
||||
|
||||
Round 2 adds six in `queue.mjs` for C1, run against the round 2 tests.
|
||||
All six are killed:
|
||||
|
||||
- the round 1 branch restored (waiting-on-jason checks only the actor);
|
||||
- the approval check dropped from waiting-on-jason;
|
||||
- the unresolved check dropped from waiting-on-jason;
|
||||
- the unresolved check dropped from waiting-on-jason→done;
|
||||
- the no-reviewers check dropped from `requireApprovals`;
|
||||
- the approval check applied to every round, not only request rounds.
|
||||
|
||||
Seven survived earlier runs. Five of them now have a test that kills them:
|
||||
a second outcome for one attempt, the owner recording a verdict, done on a
|
||||
row whose reviewers were removed, a same-op resolve retry that fetched the
|
||||
comment again, and a resolve on a done row that fetched the comment before
|
||||
the log refused it. The last one is the new test "a late POST on a closed
|
||||
row".
|
||||
|
||||
Two survive, and each is equivalent while the other stays:
|
||||
|
||||
- the semantics check in `applyEntry` dropped;
|
||||
- the semantics check in the entry shape dropped.
|
||||
|
||||
Every entry passes the shape check before it is applied, so either check
|
||||
alone refuses a review verb at semantics 1. With both dropped, the
|
||||
semantics test fails.
|
||||
|
||||
## Suites
|
||||
|
||||
In the scratch clone with the patch and the helper patch applied: config
|
||||
24/0, task 90/0, foundation 44/0, conductor 17/0, release 14/0, auth 15/0,
|
||||
discord 64/0, extension-package 18/0, queue 27/0. `node --test
|
||||
packages/queue/tests/` passes 142, and `node --test packages/ledger/tests/`
|
||||
passes 58.
|
||||
|
||||
The scratch clone isn't the canonical root, so the queue suite printed its
|
||||
skip line for the two live checks. In the canonical tree they run.
|
||||
|
||||
In a fresh clone at cdcedb27 with `build.patch` applied: the manifest
|
||||
matches 10/10, the queue tests pass 142, the queue suite 27/0. With
|
||||
`helper.patch` applied on top, the ledger tests pass 58.
|
||||
|
||||
## Known limits
|
||||
|
||||
- **Scope of darkwing's and dewey's tokens.** They hold
|
||||
`write:repository`. If that doesn't cover an issue comment, the live
|
||||
POST gets 403, which is `failed` with nothing posted (8.9 says so). The
|
||||
scope question then goes to Sage.
|
||||
- **Post to outcome.** A kill between the POST and the outcome entry
|
||||
leaves the attempt `requesting` with a comment on the issue. The queue
|
||||
never posts again on its own; a person resolves it.
|
||||
- **Verdict comments aren't fetched.** `review record` takes the
|
||||
reviewer's comment id on trust, as the queue takes `--by`.
|
||||
- **Commit candidates** stay checkable only while a ref keeps the commit.
|
||||
- The helper's own limits are in `helper.md`.
|
||||
|
||||
## After approval (Sage)
|
||||
|
||||
1. Apply `build.patch` to the canonical tree and check it against
|
||||
`build-manifest.sha256`. Run `scripts/test-queue.sh` and
|
||||
`node --test packages/queue/tests/`. Commit the 10 files by path.
|
||||
2. Apply `helper.patch` for the same commit (decision 39). The live
|
||||
round needs it: without it the helper refuses a raw token file.
|
||||
3. `tools-md.patch` if you want it.
|
||||
4. Row 12 lists no reviewers, so moving it to in-review now would open a
|
||||
round that posts nothing. Set them first:
|
||||
`scripts/mosaic queue set 12 reviewers filbert --op OP --by sage`, and
|
||||
add rocko if the helper counts as part of this round.
|
||||
|
||||
The live round, which is mine:
|
||||
|
||||
5. `MOSAIC_GITEA_CREDENTIAL_FILE=~/.mosaic/fleet/agents/darkwing/secrets/gitea-mosaicstack-darkwing.token
|
||||
scripts/mosaic queue move 12 in-review --candidate <D's commit> --op OP --by darkwing`.
|
||||
The queue `lstat`s the file, the helper reads it, and `GET user` must
|
||||
answer `darkwing`. The request goes on #1508.
|
||||
6. Exit 0 means posted. Exit 1 with HTTP 403 is the scope question above.
|
||||
Exit 3 means I look on #1508 for the marker and resolve, or ask you to
|
||||
abandon.
|
||||
7. Filbert posts a verdict on #1508 and runs `review record 12`. Then
|
||||
`move 12 done`, and `queue-commit.sh` for the queue ops.
|
||||
@@ -0,0 +1,184 @@
|
||||
# Raw per-seat token files in `scripts/gitea-api.sh` (row 12, #1508)
|
||||
|
||||
Darkwing, 2026-09-27, after round 2 (lead decision 38). Lead decision 37 approved this patch. It ships next
|
||||
to the Piece D candidate but is a separate patch, and Rocko reviews it
|
||||
because it handles a credential. Filbert reviews D. Sage commits both.
|
||||
Nothing is committed, staged or pushed.
|
||||
|
||||
`helper.patch` (sha256 `48edd46b93c54908e9d59737aab78a33e007ddfafa1e6ad4d34333015bb75430`)
|
||||
changes `scripts/gitea-api.sh` and adds
|
||||
`packages/ledger/tests/gitea-helper-raw.test.mjs`. It applies to HEAD
|
||||
8efc0ff3 by itself. With only this patch applied, `node --test
|
||||
packages/ledger/tests/` passes 58/58. D does not depend on it, and it does
|
||||
not depend on D.
|
||||
|
||||
## Why
|
||||
|
||||
Each seat's Gitea token is at
|
||||
`~/.mosaic/fleet/agents/<seat>/secrets/gitea-mosaicstack-<seat>.token`.
|
||||
These files hold the bare token, not the JSON `mosaic.gitea.json` shape
|
||||
that the helper reads today. Without this change the helper refuses them,
|
||||
so D's live round can't post as the seat.
|
||||
|
||||
## What changed
|
||||
|
||||
Both old node snippets, the one that printed the base URL and the one that
|
||||
wrote the curl config, are now one script, `CRED_JS`, run in two modes:
|
||||
`base` and `cfg`. Each mode runs every check before it prints anything.
|
||||
|
||||
1. `lstat`: a regular file, not a symlink, no group or other bits.
|
||||
Unchanged.
|
||||
2. Read the file. A read error exits 3. It used to be caught by the JSON
|
||||
parse's catch, with the same result.
|
||||
3. `JSON.parse`. If the text parses, the JSON path runs unchanged: `url`
|
||||
must be `https://git.mosaicstack.dev` (trailing slashes stripped), and
|
||||
`api_token` must be a nonempty string. A non-`SyntaxError`, such as
|
||||
`null.mosaicstack`, exits 3.
|
||||
4. The raw path runs only on a `SyntaxError`. It accepts the file only if
|
||||
all of these hold:
|
||||
- `stat` size is 40 or 41;
|
||||
- the byte length of the text equals the `stat` size;
|
||||
- the text matches `^[0-9a-f]{40}\n?$`.
|
||||
|
||||
Then the base URL is the fixed string `https://git.mosaicstack.dev`,
|
||||
and the token is the first 40 characters. Anything else exits 3
|
||||
before any request.
|
||||
5. The second read runs, and must succeed, before curl starts. Round 1
|
||||
ran it inside `curl -K <(…)`, where bash drops its exit status, so a
|
||||
refusal gave curl an empty config and the request still went out.
|
||||
Now `CFG="$(node -e "$CRED_JS" cfg)" || exit 3` and `[ -n "$CFG" ] ||
|
||||
exit 3` run first, and curl reads `-K <(printf '%s\n' "$CFG")`.
|
||||
`printf` is a builtin, so the token is in no argv. Both curl branches,
|
||||
with and without a body, use it.
|
||||
6. `export -n CFG` runs right after the checked assignment, before any
|
||||
child starts. A `CFG` inherited from the caller's environment keeps its
|
||||
export attribute when assigned, and `SHELLOPTS=allexport` in the
|
||||
environment exports every assignment. Either way the config would reach
|
||||
curl's environment, and the body file's `mktemp` and `chmod` too.
|
||||
|
||||
## Decision 37, condition by condition
|
||||
|
||||
1. **The JSON path is unchanged.** The last test runs a good JSON file,
|
||||
four refused ones, and content that parses as JSON but would pass the
|
||||
raw pattern: 40 decimal digits, with and without a newline. Those take
|
||||
the JSON path and refuse.
|
||||
2. **One line of token characters.** The format is 40 lowercase hex
|
||||
characters. I checked by `stat` that the real files are 40 bytes (jarvis)
|
||||
or 41 (the others), all mode 600. I then ran the patched `CRED_JS` in
|
||||
`base` mode against my own file only. It printed
|
||||
`https://git.mosaicstack.dev` and exited 0, so darkwing's file matches
|
||||
the pattern. Nothing printed the token, and I read no other seat's file.
|
||||
The other seats' files are unconfirmed beyond size and mode. If one
|
||||
doesn't match, the helper exits 3 before any request. The test refuses
|
||||
16 bad contents before curl runs: empty, 39 characters, 41 characters,
|
||||
upper case, CRLF, a trailing CR, a trailing space, a trailing tab, two
|
||||
newlines, a leading space, a trailing space at 40, a second line, a
|
||||
quote, non-hex, non-ASCII, and 80 characters.
|
||||
3. **Fixed base URL.** The raw path assigns the literal. The test sets
|
||||
`MOSAIC_GITEA_URL`, `GITEA_URL` and `MOSAIC_GITEA_BASE_URL` to another
|
||||
host, and curl still gets `https://git.mosaicstack.dev/api/v1/user`.
|
||||
4. **File checks and the config stream.** Modes 640, 604, 660, 644, 000
|
||||
and 200, a symlink, a missing file and a directory all refuse before
|
||||
curl runs. 000 and 200 pass the group and other check, and the read
|
||||
refuses them. The stub curl records its argv and its `-K` stream. The
|
||||
token is in the stream only, never in argv, stdout or stderr. Every
|
||||
token in the test is a dummy that the test writes.
|
||||
5. **Only the acting seat's file.** That is D's job, not the helper's. D
|
||||
refuses unless `MOSAIC_GITEA_CREDENTIAL_FILE` resolves to
|
||||
`…/agents/<login>/secrets/gitea-mosaicstack-<login>.token` for the
|
||||
acting seat, or jarvis when Sage acts, and before posting it checks that
|
||||
`GET user` returns that login. See `review.mjs` `credCheck` and
|
||||
`checkUser` in the D candidate.
|
||||
6. This review.
|
||||
|
||||
## Round 1 and what changed for round 2
|
||||
|
||||
Rocko's round 1 (`agents/rocko/work/gitea-helper-raw-r1-review-2026-09-27.md`)
|
||||
found one blocker: a second read that refused still let curl run with an
|
||||
empty config, and a 2xx then exited 0. My round 1 notes called that a 401,
|
||||
which the server doesn't guarantee. Sage agreed it belongs in this patch.
|
||||
Step 5 above is the fix.
|
||||
|
||||
The new test, "a file that changes between the two reads refuses before
|
||||
curl runs", uses the git stub, which runs between the two reads, to
|
||||
change a valid dummy file to invalid text, to `{}`, to mode 644, to a
|
||||
symlink, and to missing. It runs each change for `GET user` and for a POST
|
||||
with a dummy body. Every case exits 3 with no curl call and nothing on
|
||||
stdout. It also changes a valid JSON file to invalid text. As a control,
|
||||
both calls reach curl with the right config when nothing changes. The
|
||||
first test now also checks that the token is not in the environment curl
|
||||
gets.
|
||||
|
||||
Rocko's round 2 (`agents/rocko/work/gitea-helper-raw-r2-review-2026-09-27.md`)
|
||||
closed that blocker and found another: the inherited export attribute in
|
||||
step 6. Lead decision 38 took the fix, `export -n CFG`, with no round 3;
|
||||
Sage checks it with Rocko's reproducer. The test "the token reaches no
|
||||
child environment, even with an inherited CFG or SHELLOPTS=allexport" runs
|
||||
raw and JSON dummy files, GET and POST, with an exported harmless `CFG`,
|
||||
with `SHELLOPTS=allexport`, and with both. Each call must exit 0 with the
|
||||
token in curl's config stream and not in its environment, argv, stdout or
|
||||
stderr. I found the `SHELLOPTS` route while checking the fix; `export -n`
|
||||
covers it too, so I added no second line for it.
|
||||
|
||||
Not in this patch: the predictable response path
|
||||
`/tmp/gitea-api-response.$$`. Sage has it on DEFERRED as "Gitea helper
|
||||
response-file hardening".
|
||||
|
||||
## What I'd like you to look at
|
||||
|
||||
- **The second read now re-validates.** Before, `gen_curl_cfg` parsed the
|
||||
file again without the `lstat` checks. Now both reads run every check.
|
||||
So a file swapped for a symlink or a wider mode between the two reads
|
||||
refuses. On the JSON path this is a tightening.
|
||||
- **The token now sits in a shell variable** for the rest of the script.
|
||||
`export -n` keeps it out of every child's environment, and nothing
|
||||
prints it. Before, it existed only in the pipe.
|
||||
- **`lstat` then `readFile` is still not atomic.** A swap between those two
|
||||
calls inside one read is the pre-existing race Rocko noted. This patch
|
||||
doesn't claim to close it.
|
||||
- **`text.slice(0, 40)`** relies on the pattern having matched. The only
|
||||
accepted texts are the token alone or the token plus `\n`.
|
||||
|
||||
## Mutations
|
||||
|
||||
25 mutants, each run alone against `gitea-helper-raw.test.mjs`: 19 of
|
||||
`CRED_JS` and 6 of the gate and the export. 15 are killed:
|
||||
|
||||
- uppercase allowed, any trailing whitespace allowed, the `^` anchor
|
||||
dropped;
|
||||
- a raw file refused outright (the old behaviour);
|
||||
- an env override of the raw base URL;
|
||||
- the token taken with its newline;
|
||||
- the mode check dropped;
|
||||
- the JSON path's host check or token check dropped, or its `|| {}`;
|
||||
- `cfg` output in `base` mode;
|
||||
- the read's own `try` dropped (killed by the 000 and 200 modes; I added
|
||||
those after this mutant first survived);
|
||||
- both gate checks dropped, and the round 1 code (no gate, curl reading
|
||||
`<(node … cfg)`), killed by the between-reads test;
|
||||
- `export -n CFG` dropped, killed by the inherited-environment test.
|
||||
|
||||
Ten survive, and each is equivalent while the other checks stay:
|
||||
|
||||
- `\n*$` for `\n?$`: the size check caps the file at 41 bytes;
|
||||
- the size check dropped: the pattern caps the length;
|
||||
- the byte-length check dropped: the pattern is ASCII, so a match means
|
||||
bytes equal characters; the check differs only if the file changes
|
||||
between `lstat` and the read;
|
||||
- the `SyntaxError` test dropped: the only non-`SyntaxError` is JSON
|
||||
`null`, which then fails the raw pattern;
|
||||
- `text.trim()` for `text.slice(0, 40)`: same result on every accepted
|
||||
text;
|
||||
- the `isSymbolicLink` test dropped: `lstat` of a symlink is never
|
||||
`isFile`;
|
||||
- `!== "base"` for `=== "cfg"`: the script calls only these two modes;
|
||||
- `|| true` for `|| exit 3` on the `CFG` line: node prints nothing when it
|
||||
refuses, so `[ -n "$CFG" ]` refuses;
|
||||
- `[ -n "$CFG" ]` dropped: `|| exit 3` refuses first. Dropping
|
||||
`|| exit 3` outright is the same, since `set -e` exits on the failed
|
||||
assignment;
|
||||
- `export CFG=…` for `CFG=…`: `export -n` on the next line clears it. In
|
||||
round 2 this one was killed; the fix makes it equivalent.
|
||||
|
||||
I kept the size, byte-length and symlink checks anyway. Decision 37 asks
|
||||
for size plus pattern, and the other two cost nothing.
|
||||
@@ -1467,7 +1467,7 @@ cooperative trust model (J2).
|
||||
| in-progress→briefed (`release`) | claimant, or privileged | clears `claim` |
|
||||
| in-progress→in-review | claimant | once D is built, for a row with `reviewers`, this move is the review request, and it takes `--candidate` (8.9). Before D, or with no reviewers, it opens the round with `request: none` |
|
||||
| in-review→in-progress | owner, or privileged | changes requested (Sage) |
|
||||
| in-review→waiting-on-jason | owner, or privileged | — |
|
||||
| in-review→waiting-on-jason | owner, or privileged | with D built, on a comment round: refused while any attempt is unresolved, and it needs the approving receipts of every listed reviewer for that round, the same checks as in-review→done (8.9). Before D, or on a round with `request: none`: no condition |
|
||||
| waiting-on-jason→done | `jason`; or `sage` with `--evidence` citing Jason's approval | the evidence reference is logged |
|
||||
| in-review→done | the row's `gateOwner`, or privileged | refused when `gateOwner` is `jason` (that path runs through waiting-on-jason). `--evidence` must name the current round and its candidate digest: with D built, the approving receipts of every listed reviewer for that round (8.9); before D, a comment id together with the round's candidate digest, which must match. Logged (J5) |
|
||||
| any non-terminal→blocked | owner, or privileged | reason required; records `previousState`; blocked→blocked refused (update the reason with `note`) |
|
||||
@@ -1688,6 +1688,10 @@ The CLI may print the issue URL and the marker as a hint. It never decides.
|
||||
**Receipts.** `review record` is accepted from a listed reviewer, for the
|
||||
current round, citing the comment id and the candidate digest that was
|
||||
approved. A mismatch refuses. Every round is kept in `review.rounds[]`.
|
||||
On a comment round, the receipts gate both moves out of in-review toward
|
||||
done: in-review→done, and in-review→waiting-on-jason, which is the route
|
||||
for a Jason-gated row. Each needs an approval from every listed reviewer
|
||||
and no unresolved attempt.
|
||||
|
||||
**Tests** use a fake transport:
|
||||
- a kill:
|
||||
@@ -2402,3 +2406,8 @@ The round-2 modifications:
|
||||
- the guard checked as active on every invocation, including a
|
||||
`git hook run` canary (G2), checked in a scratch repository;
|
||||
- the lead's expected login is `jarvis`. 8.19 maps the findings.
|
||||
- 2026-09-27: correction from Filbert's Piece D review round 1 (C1),
|
||||
ruled by Sage. The transition table let in-review→waiting-on-jason pass
|
||||
with no reviewer approvals, so a Jason-gated row could close without its
|
||||
reviewers. That move now carries the in-review→done checks on a comment
|
||||
round; the table and 8.9's receipts paragraph say so.
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# Queue Piece D review, round 1 (#1508, row 12)
|
||||
|
||||
Filbert, 2026-09-27. Plan: `queue-as-data-plan-2026-09-26.md` §8.9.
|
||||
A2 round 1 review: `queue-a2-review-r1-2026-09-27.md` (sha256 f167b85e).
|
||||
|
||||
## Verdict
|
||||
|
||||
**Changes requested, one item (C1).** The review covers `build.patch`
|
||||
sha256 cb7a6c6649f88496887eacab52db77ef1cc62aaf59a48c3cadcd5f8e33ae1243
|
||||
on base 8ffbd73b (it applies cleanly at 8efc0ff3), with
|
||||
`build-manifest.sha256` 5646d938…. Everything else in the candidate is
|
||||
ready. C1 is a gap in my own plan's transition table, and D implements that
|
||||
table as written. Sage can rule it a plan question and approve without it,
|
||||
but I recommend fixing it now: no semantics-2 entry is logged yet, so the
|
||||
fix needs no semantics 3.
|
||||
|
||||
I re-hashed the inputs after the review. `build.patch` and
|
||||
`build-manifest.sha256` match the hashes Darkwing sent. `build.md` changed
|
||||
from b079fbc1… to ced0946b… while I reviewed. The diff is two lines: the
|
||||
`helper.patch` hash now names helper round 2, and a helper test count went
|
||||
from 56 to 57. Both are in the helper, which is Rocko's scope, so the
|
||||
change doesn't touch this review.
|
||||
|
||||
## C1. A Jason-gated row can close without its reviewers' verdicts
|
||||
|
||||
**Where:** `packages/queue/src/queue.mjs`, `applyMove`, the branch
|
||||
`} else if (from === "in-review" && (to === "in-progress" || to === "waiting-on-jason")) {`
|
||||
(lines 766–767 in the patched file). It calls only `ownerOrPriv`.
|
||||
|
||||
**What happens.** On a semantics-2 comment round, in-review→waiting-on-jason
|
||||
checks neither the reviewers' approvals nor the unresolved attempts. The
|
||||
in-review→done path checks both, but a Jason-gated row never takes that
|
||||
path; it goes through waiting-on-jason. After that, waiting-on-jason→done
|
||||
checks only for unresolved attempts. So on a Jason-gated row with
|
||||
reviewers, the listed reviewers are advisory.
|
||||
|
||||
**Reproduced** with a temporary probe test on the `review.test.mjs`
|
||||
harness (since deleted):
|
||||
|
||||
1. `set 9 gate-owner jason` on a row with reviewers.
|
||||
2. The owner's move to in-review posts the request (comment 1000). The
|
||||
round's `receipts` is `[]`.
|
||||
3. darkwing: `move 9 waiting-on-jason` exits 0.
|
||||
4. jason: `move 9 done` exits 0.
|
||||
|
||||
No reviewer recorded a verdict at any point.
|
||||
|
||||
**Why it matters now.** Row 5 (CHAT-03 source) is next through this path.
|
||||
Its gate owner is jason and its reviewers are filbert and rocko. D exists so
|
||||
that the queue, not a seat's memory, holds "reviewed before Jason sees it".
|
||||
|
||||
**Cause.** My plan's table, line 1470, gives
|
||||
in-review→waiting-on-jason as "owner, or privileged | —". I wrote the
|
||||
approval condition only on in-review→done (line 1472). D follows the table.
|
||||
|
||||
**Fix.** When the current round is a comment round, in-review→waiting-on-jason
|
||||
runs `refuseUnresolved` and requires an approving receipt from every listed
|
||||
reviewer, the same checks as in-review→done (lines 779–787). Share them in
|
||||
one helper. In-review→in-progress stays as it is: that's the changes path,
|
||||
and the next round already refuses unresolved attempts.
|
||||
|
||||
- Scope it to `cur?.request === "comment"`. Only semantics-2 entries create
|
||||
such rounds, so v1 replay is unaffected.
|
||||
- Whether `by === "jason"` is exempt is Sage's call. I lean against it:
|
||||
an exemption is one more path to test, and the owner can't use it.
|
||||
- Add a test: the steps above refuse at step 3, then pass once every
|
||||
reviewer has recorded an approval. Add a mutant that drops the check.
|
||||
- Update the plan table line 1470 and the README's review section to match.
|
||||
I'll make the plan edit if Sage wants it in my file.
|
||||
|
||||
## What I checked
|
||||
|
||||
All of this ran in a scratch clone, `/tmp/fqd`, with push disabled, on
|
||||
frozen 0444 copies of the inputs in `/tmp/fqd-in`.
|
||||
|
||||
- **Manifest and suites.** The manifest checks 10/10 after
|
||||
`git apply build.patch`. `node --test packages/queue/tests/` passes
|
||||
141/141, and the ledger tests pass 51/51. `scripts/test-queue.sh` reports
|
||||
27/0 with its "outside the canonical root" skip line, as Sage described.
|
||||
- **The source**, read in full: `review.mjs`, and the diffs to `queue.mjs`,
|
||||
`store.mjs`, `cli.mjs`, `scripts/test-queue.sh`, the README, the tests and
|
||||
`fixtures/fake-gitea.mjs`. `tools-md.patch` matches the code; it's Sage's
|
||||
to apply.
|
||||
- **The deadline.** `timeout -s KILL` kills the helper's grandchild. I
|
||||
checked it with a helper that forks a sleeper: the grandchild is gone
|
||||
after the deadline, and `spawnSync` reports `signal: "SIGKILL"`, which
|
||||
`classifyPost` maps to uncertain.
|
||||
- **Mutations.** I wrote 24 mutants of my own, separate from Darkwing's 61,
|
||||
and the suite kills all 24. They cover:
|
||||
- a same-op retry falling through to a second POST;
|
||||
- an abandoned attempt followed by a late post, which must be a conflict;
|
||||
- resolve accepting a comment with another id, another author, or no
|
||||
round marker;
|
||||
- `credCheck` skipping the mode check or the realpath check;
|
||||
- a 201 with no comment id treated as posted;
|
||||
- other HTTP codes treated as failed instead of uncertain;
|
||||
- a killed call treated as failed;
|
||||
- done without every approval;
|
||||
- a new round, or waiting-on-jason→done, ignoring unresolved attempts;
|
||||
- a new request while one is posted;
|
||||
- resolve without the GET;
|
||||
- the body limit removed;
|
||||
- sage posting as sage instead of jarvis;
|
||||
- `verify-commit` ignoring the mode or a deletion;
|
||||
- the outcome-write failure exiting 1 instead of 3;
|
||||
- a failed pre-send still posting;
|
||||
- `next` offering a round the reviewer already answered;
|
||||
- abandon leaving `duplicateRisk` false;
|
||||
- record accepting a different candidate digest.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
- **n1. Done doesn't require a posted request.** If every attempt failed
|
||||
or was abandoned, reviewers can still record approvals and the row can
|
||||
close. That's acceptable: the receipts are the evidence, and the comment
|
||||
is how reviewers are asked, not what they approve.
|
||||
- **n2. A review issue that is a pull request.** `checkComment` needs
|
||||
`issue_url` to end in `/issues/N`. I haven't checked what Gitea returns
|
||||
for a comment on a pull request. If it's anything else, resolve refuses.
|
||||
That fails closed, and our review issues are issues, so no change now.
|
||||
- **n3. Abandoning a `requesting` attempt sets `duplicateRisk`** even when
|
||||
the kill came before the POST. That's per plan: the queue can't tell
|
||||
which side of the POST the kill landed on.
|
||||
- **n4. The body travels on argv as a JSON argument.** It's capped at
|
||||
60,000 bytes, well under the per-argument limit. An E2BIG would classify
|
||||
as uncertain, which is the safe side.
|
||||
- **n5. For Rocko:** `scripts/gitea-api.sh` writes the response to the
|
||||
predictable path `/tmp/gitea-api-response.$$`. That's the helper, not D.
|
||||
|
||||
What I'd keep as it is:
|
||||
- resolve checking the comment's author against the requester's login.
|
||||
That goes beyond the plan, and it's right;
|
||||
- semantics recorded per entry, with review verbs refused on v1 entries;
|
||||
- the late-outcome rules in `outcomeState`;
|
||||
- fixed detail strings, so nothing from a response reaches the log.
|
||||
|
||||
## For Darkwing and Sage
|
||||
|
||||
Fix C1 with its test and mutant, re-send the patch and manifest, and I'll
|
||||
review round 2 against this list only. If Sage rules C1 out of D's scope,
|
||||
the candidate is approved as it stands, and C1 has to land before row 5
|
||||
moves to in-review.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Queue Piece D review, round 2 (#1508, row 12)
|
||||
|
||||
Filbert, 2026-09-27. Round 1: `queue-d-review-r1-2026-09-27.md`
|
||||
(sha256 a2dc2302…). This round checks C1 only, per Sage's ruling to fix it
|
||||
in D.
|
||||
|
||||
## Verdict
|
||||
|
||||
**Approved.** The review covers `build.patch` sha256
|
||||
8a19f7fa03e2ee6c3bd9ce7b2e296acb21655471c4cc56ba19f25464c8129209 (base
|
||||
8ffbd73b, applied at cdcedb27), with `build-manifest.sha256` 647d0170…
|
||||
and `build.md` e77c8c5e…. The C1 fix is correct. One mutant of mine
|
||||
survives: an approval from an earlier round counted in the current one.
|
||||
The code handles that case correctly; the tests just don't check it. n1
|
||||
gives a four-line test that kills the mutant. I'd add it before the
|
||||
commit, and it doesn't need a round 3. I checked it myself (below).
|
||||
|
||||
## What I checked
|
||||
|
||||
All of this ran in a scratch clone, `/tmp/fqd2`, at cdcedb27 with push
|
||||
disabled, on frozen 0444 copies of the three inputs.
|
||||
|
||||
- **Manifest and suites.** The manifest checks 10/10. `node --test
|
||||
packages/queue/tests/` passes 142/142, and the ledger tests pass 51/51
|
||||
(Darkwing's 58 includes the helper patch, which I didn't apply).
|
||||
`scripts/test-queue.sh` reports 27/0 with the "outside the canonical
|
||||
root" skip line.
|
||||
- **What changed.** I compared all ten manifest files with round 1's tree.
|
||||
Only `queue.mjs`, `README.md` and `review.test.mjs` differ, as Darkwing
|
||||
said.
|
||||
- **The fix in `queue.mjs`.**
|
||||
- `requireApprovals` is the round-1 inline check moved into a helper.
|
||||
in-review→done's messages are unchanged.
|
||||
- in-review→in-progress still checks only `ownerOrPriv`, which is right
|
||||
for the changes path.
|
||||
- in-review→waiting-on-jason runs `ownerOrPriv`, then `refuseUnresolved`,
|
||||
then `requireApprovals` when the last round is a comment round.
|
||||
`unresolvedAttempts` skips rounds that aren't comment rounds, and v1
|
||||
entries can't open one. So v1 replay is unaffected.
|
||||
- There's no exemption for jason or sage, as agreed.
|
||||
- **The tests.** The new test runs my round-1 reproduction and now
|
||||
refuses at the step that used to pass. It covers a reviewer who asked
|
||||
for changes, a second round, and reviewers cleared after the round
|
||||
opened. The rewritten unresolved test keeps waiting-on-jason→done
|
||||
covered through a late post that turns an abandoned attempt into a
|
||||
conflict. That's a better route than the old one.
|
||||
- **The README** matches the code.
|
||||
- **Mutations.** I wrote eight mutants of my own for this round. The
|
||||
suite kills seven:
|
||||
- G1: sage bypasses the approval check on waiting-on-jason;
|
||||
- G2: a `changes` verdict counts as an approval;
|
||||
- G3: the check reads the first round, not the last;
|
||||
- G4: one approval is enough;
|
||||
- G5: the owner-or-privileged check dropped from waiting-on-jason;
|
||||
- G6: the unresolved check dropped from waiting-on-jason→done;
|
||||
- G7: the approval check dropped from in-review→done.
|
||||
|
||||
G8 survives: approvals are collected from every round's receipts
|
||||
instead of the current round's. See n1.
|
||||
- **My plan.** Table line 1470 and 8.9's Receipts paragraph (plan sha256
|
||||
293747cd…) describe the code as built.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
- **n1. Add a test that an earlier round's approval doesn't count (kills
|
||||
G8).** In the new test, filbert approves round 1 and rocko asks for
|
||||
changes. In round 2 both approve before anything checks, so an old
|
||||
approval counting in round 2 goes unnoticed. In
|
||||
`packages/queue/tests/review.test.mjs`, replace the round-2 `for` loop
|
||||
(three lines) with:
|
||||
|
||||
```js
|
||||
ok(s.run(["review", "record", "9", "--verdict", "approve", "--comment", "91", "--candidate", head, "--op", "record-9-rock02"], "rocko"));
|
||||
// Filbert's round 1 approval doesn't carry into round 2.
|
||||
no(s.run(["move", "9", "waiting-on-jason", "--op", "wait-9-000001"], "darkwing"), 2, /row 9 round 2 has no approval recorded by filbert$/m);
|
||||
ok(s.run(["review", "record", "9", "--verdict", "approve", "--comment", "90", "--candidate", head, "--op", "record-9-filb02"], "filbert"));
|
||||
```
|
||||
|
||||
I checked it in a copy of the scratch clone: `review.test.mjs` passes
|
||||
20/20 on the candidate and fails 1 under G8.
|
||||
- **n2. An owner who is also a listed reviewer blocks the row.** `set
|
||||
reviewers` and `assign` don't stop the owner from being on the list.
|
||||
`record` refuses the owner, but `requireApprovals` still waits for the
|
||||
owner's approval. The row then can't reach done or waiting-on-jason
|
||||
until a privileged actor runs `set reviewers`. That fails closed, and it
|
||||
was already true of done in round 1. The refusal names the owner as
|
||||
missing, which could confuse. Don't fix it by dropping the owner from
|
||||
the required list: with reviewers `[owner]`, that list would be empty
|
||||
and the row would pass with no review. A later fix could refuse the
|
||||
owner at `set reviewers` and `assign`. That's a follow-up, not D.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Gitea raw-token helper — R1 security review
|
||||
|
||||
Verdict: **changes requested**, one blocking finding. Round 1 of two.
|
||||
|
||||
Reviewed `helper.patch`, sha256 `c5e3de137ee999620688fab6b9721fe973a9d0ab1ca98ae6bcd8cc742d5620ee`, applied alone to an isolated shared clone at `8efc0ff3`. The patch hash was checked before reading and again after testing. Scope is lead decision 37's credential-handling helper; Filbert reviews Piece D. `scripts/mosaic queue next rocko` returned `nothing`; this explicit review request supplies the assignment.
|
||||
|
||||
## 1. Blocking — second validation failure does not prevent the request
|
||||
|
||||
Candidate `scripts/gitea-api.sh:86` and `:90` invoke curl with `<(gen_curl_cfg)`. Bash does not propagate the producer's exit status to curl. Revalidating in `cfg` mode is useful, but a refusal produces an empty config stream while curl still sends the request. Lines 99–100 can then report success on a 2xx response. A 401 is not guaranteed and is not a substitute for refusing before the request.
|
||||
|
||||
Independent deterministic reproduction used only dummy credentials and stub git/curl:
|
||||
|
||||
1. The first read accepts a valid raw dummy token.
|
||||
2. The git stub, which runs between the two reads, replaces that dummy file's contents with `invalid-token`.
|
||||
3. The second validator exits 3. The curl stub still runs, observes zero config bytes, and returns 200.
|
||||
4. The helper exits 0, prints `{}` and `HTTP 200`.
|
||||
|
||||
This control-flow hole predates the patch, as helper.md correctly discloses. It directly contradicts decision 37's acceptance condition for this new raw path: invalid content must refuse **before any request**. It also defeats the newly added second-read file checks at the request boundary. It needs fixing in this patch rather than deferral.
|
||||
|
||||
**Required change:** finish credential validation/config generation and check its success before starting curl. Keep the successful config in memory and deliver the token only through the config stream, never through argv, exported environment, disk or diagnostics. Preserve the JSON validation contract. Cover both body and no-body curl branches.
|
||||
|
||||
**Required regressions:** change a valid dummy file to invalid content, a symlink and a disallowed mode between the reads (or at the equivalent validation boundary after refactoring). Assert exit 3 and zero curl invocations, including a POST with a dummy body. Do not merely assert an eventual HTTP failure. Remove the success gate as a mutation and require these tests to fail.
|
||||
|
||||
## 2. Nonblocking — predictable response path is a separate existing issue
|
||||
|
||||
Lines 86/90/95/96 use `/tmp/gitea-api-response.$$`. A pre-existing symlink can redirect curl's output and the later `cat`; predictable shared-directory names are not a safe temporary-file allocation. This does not put the raw token in that file, and this patch does not introduce the path. I would keep it out of the narrow credential patch and track a separate fix using an exclusively created private temporary response file and cleanup on all exits. No exploit against a real path was attempted.
|
||||
|
||||
## What is fine
|
||||
|
||||
- The raw branch is reached only after JSON syntax failure. JSON values such as `null` and 40 decimal digits do not become raw credentials. The JSON URL/token validation rules remain unchanged.
|
||||
- The size, byte-count and ASCII pattern checks together accept exactly 40 lowercase hex characters with at most one final LF. Rejected contents emit no token. The fixed raw URL has no environment override.
|
||||
- Both reads perform the existing regular-file, no-symlink and group/other-mode checks. This is an improvement over the old second read, subject to finding 1. It is not an atomic file-descriptor identity guarantee across `lstat` and `readFile`; that separate pre-existing race is not claimed closed here.
|
||||
- On successful validation, the raw token reaches curl through its config descriptor and not through argv or normal stdout/stderr. Tests use dummy files and stub executables.
|
||||
- The helper is intentionally generic; it does not enforce the acting seat's identity. Decision 37's own-file rule remains an integration gate in Piece D (`credCheck` and `GET user` identity verification). This helper review does not certify those separate paths or authorize another seat's credentials.
|
||||
- Mutation results are honestly separated into killed and surviving mutations. The byte-length mutation is equivalent only for a stable file; helper.md already notes the changing-file exception. The missing request-gating test above is material despite the existing suite passing.
|
||||
|
||||
## Validation
|
||||
|
||||
`node --test packages/ledger/tests/` in the isolated candidate: **56/56 passed**, no skips. The separate between-read replacement reproducer demonstrated finding 1 with helper exit 0 and an empty config received by stub curl. No real credential file was opened, printed or tested; no live API request, source edit in the shared checkout, commit or restart was made. Only this review report was added to the checkout.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Gitea raw-token helper — final R2 review
|
||||
|
||||
Verdict: **changes requested**, one blocking finding. This is round 2 of 2; escalate the remaining blocker to Sage under item 27, not an automatic third round.
|
||||
|
||||
Target: `helper.patch`, sha256 `dd9e38bfa95d2e2305ad004a8583646bb90d6a5bf85cadf3140b3b688319770f`, applied alone to an isolated clone at `8efc0ff3`. Hash verified before reading and again after testing. Queue lookup returned `nothing`; the explicit review request supplies this assignment.
|
||||
|
||||
## 1. Blocking — inherited CFG retains its export attribute
|
||||
|
||||
Candidate `scripts/gitea-api.sh:74` assigns the secret configuration to `CFG` without clearing an inherited export attribute. In Bash, assigning a value to a variable imported from the environment does not make it private. The line-72 comment that CFG is not exported is therefore false when the caller already has an exported variable with that name.
|
||||
|
||||
Independent reproduction, with a dummy token and stub git/curl only:
|
||||
|
||||
- Start the helper with environment `CFG=innocent inherited value`.
|
||||
- Both validations pass, and line 74 replaces CFG with the credential config.
|
||||
- Stub curl observes the dummy token in its environment. The helper exits 0.
|
||||
|
||||
No caller needs to know or supply the credential value for this to happen. The body branch also starts external utilities after the assignment, so exposure is not limited to curl. This is newly introduced by storing the secret in a shell variable and violates decision 37's config-stream-only boundary.
|
||||
|
||||
**Required fix:** explicitly remove CFG's export attribute before launching any child with the captured secret, rather than relying on the absence of an `export CFG` statement. For example, a checked assignment followed immediately by Bash's builtin `export -n CFG`, before body-file creation or curl, addresses this inherited-attribute case. A task-specific name reduces collisions but does not replace clearing the attribute.
|
||||
|
||||
**Regression:** seed the helper environment with an exported, harmless CFG value. Exercise GET and POST using dummy raw and JSON credentials; assert that the token appears only in the config stream, never in child environment, argv or diagnostics. The current environment assertion runs without this seed, so it passes despite the bug. Removing the explicit attribute-clearing step must fail the regression.
|
||||
|
||||
## R1 disposition
|
||||
|
||||
The R1 blocker is **closed**. Configuration generation finishes synchronously and its failure prevents curl from starting in both branches. The added tests cover invalid text, invalid JSON shape, disallowed mode, symlink and missing-file changes between reads, plus a JSON-to-invalid transition. They assert exit 3 and zero curl calls rather than depending on an HTTP error. The nonempty check provides an additional guard.
|
||||
|
||||
The raw parser, fixed base URL and JSON validation rules retain the R1 behavior. The predictable response path remains a separate, acknowledged DEFERRED item and is not a blocker for this patch. Acting-seat path/login enforcement remains Piece D's integration responsibility, not certified by this helper-only review.
|
||||
|
||||
## Evidence and scope
|
||||
|
||||
The isolated R2 ledger suite passed **57/57**, no skips. Separately reproduced the inherited-export leak with the actual candidate helper and stubs, reporting only the boolean result, not credential contents. A small shell check confirmed that `export -n CFG` removes the inherited export attribute. No candidate source was changed for this review, and no real token file was opened or printed. No live API call, commit, push or restart occurred. Only this report was written in the shared checkout.
|
||||
Reference in New Issue
Block a user