Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c8c38b698 | ||
|
|
4531834677 | ||
|
|
048d907b20 | ||
|
|
947d9ce69c | ||
|
|
f922cc51dd | ||
|
|
88402c25ee | ||
|
|
088ea2f960 | ||
|
|
10672772e4 | ||
|
|
00ba0cc5c3 | ||
|
|
bf52628f9f | ||
|
|
c8049e6d2d | ||
|
|
07fddf56f4 | ||
|
|
ffca8e4fc7 | ||
|
|
e52305c5d8 | ||
|
|
556c67fb0d | ||
|
|
4e7092478f | ||
|
|
a4691affdc | ||
|
|
634eef5605 | ||
|
|
1c58506006 | ||
|
|
db8d41a480 | ||
|
|
ee01f6f9cd | ||
|
|
8ea792256a | ||
|
|
c3dc58f307 | ||
|
|
78bdae693d | ||
|
|
9dceb21486 | ||
|
|
d20f28ea9f | ||
|
|
7490801154 | ||
|
|
d025ef492e | ||
|
|
23f5520b5a | ||
|
|
62200362b5 | ||
|
|
364c5b000b | ||
|
|
9bffb2b2a9 | ||
|
|
6711d55be7 | ||
|
|
9f4d5556ba | ||
|
|
9ba8385a9a | ||
|
|
182ff49b15 | ||
|
|
317bb05e93 | ||
|
|
56d59c4c7e | ||
|
|
4c052837e3 | ||
|
|
38cc4f114e | ||
|
|
808aa41a30 | ||
|
|
52b5fee634 | ||
|
|
8140b7f393 | ||
|
|
2a1d17da7d | ||
|
|
a76767e877 | ||
|
|
f1809238d8 | ||
|
|
606df83ba2 | ||
|
|
da03f5bf53 | ||
|
|
d273963e5b | ||
|
|
c15c70f367 | ||
|
|
c2a9fb8404 | ||
|
|
640011518b | ||
|
|
3cb6888e1b | ||
|
|
523a6bddf3 | ||
|
|
d2e305db49 | ||
|
|
8a22d08976 | ||
|
|
0b1f0336ed | ||
|
|
4065ed59f9 | ||
|
|
6ab9cd9271 | ||
|
|
912b1d325a | ||
|
|
4cf3ce2327 | ||
|
|
cb6d840df1 | ||
|
|
aa7f082bf4 | ||
|
|
b14f111362 | ||
|
|
86be181a2a | ||
|
|
4966136d40 | ||
|
|
45cbe58966 | ||
|
|
03a663cd72 | ||
|
|
b402197ff3 |
@@ -0,0 +1,59 @@
|
||||
# Review: Gizmoduck federation design r2 and r2.1 (duckworth-0002)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md at head 5ad67f671 (r2.1, parent 60b9b112a, r2 parent 6925f78f0).
|
||||
DESIGN.md line numbers below are at 5ad67f671. The r2.1 diff only moves the uid to row 190b, makes R7 depend on row 190 and
|
||||
puts P19 333 on the path. I checked it and found no issue. Code at origin/refactor
|
||||
9ae8c73ac. Against duckworth-0001 (REVIEW.md, b402197ff).
|
||||
|
||||
## Verdict: changes, B6 only
|
||||
|
||||
B1 to B5 are resolved. r2 adds one new blocker of its own. Fixing B6 and C1 as line edits plus their
|
||||
test lines would earn approval.
|
||||
|
||||
## r1 codes
|
||||
|
||||
- B1 resolved (3.6). The walk is the human's (human.mjs:73-88), the user CLI has its own nonce, the Console
|
||||
parent is checked against the bus's tree (webui-service.sh:3 confirms `cli.mjs serve`), and the user id
|
||||
binds only with a live grant. R3 has both mutants. Condition C1 below.
|
||||
- B2 resolved (3.6 list, 4). `#owner`, the `#threadFor` refusal plus `message.send` being off the list (two
|
||||
walls), the sender fields and the audit are all in. The 48 `s.human` and 41 `s.tester` line counts match.
|
||||
- B3 resolved (3.5). There is one level with seven verbs. Each read has a filter, and everything else is
|
||||
`access-refused`. The cited lines match (broker.mjs:2143-2146, :2288-2295, :2318-2322, :2331-2334,
|
||||
:2351-2420, :1203-1207, :2398).
|
||||
- B4 resolved (3.7). There are three triggers. `threadsState` (:390-393) is computed per call, so a restart
|
||||
doesn't bypass the revocation. The launcher skips a revoked thread and SIGTERMs a running one (launcher.mjs:510-553,
|
||||
:527, tick :133). R4 gets mutants and the new order.
|
||||
- B5 resolved (R8, 5, 8, 9). Separate uids are a hard entry gate (row 190b in r2.1) with checks, and N9 is
|
||||
folded in.
|
||||
- N1 to N9 are handled where the "Changes from r1" section says.
|
||||
|
||||
## Blocker
|
||||
|
||||
B6. With r2's sender change, a user's thread never gets an answer. 3.5 sets `thread.send` to write
|
||||
`from_role` `user` (DESIGN.md:207). The runner answers every message with `message.send { to: m.from_role }`
|
||||
(packages/harness/src/runner.mjs:450). The broker accepts only `human`, `tester` or a role as `to`
|
||||
(broker.mjs:1862), so each reply fails `unknown-role`, and the user sees no answer. The tester needed a
|
||||
whole set of changes for the same path (row 177): `to: 'tester'` only in reply to a tester message
|
||||
(:1852-1857), `#testerThread` and `tester-thread-human` (:1864-1866), the `#checkMessaging` mapping (:1867),
|
||||
`#sender` (:627-629), and the runner's header (runner.mjs:75-78). The design names none of these for users.
|
||||
|
||||
Fix: state the reply path. Either accept `to: 'user'` only in reply to a message from that user in that
|
||||
user's thread, with the tester's set of checks, or say the run answers `human` in the user's thread and
|
||||
why the owner's inbox then holds it. Add a runner header, like the tester's, saying the message comes from
|
||||
a granted user, is never the owner's instruction, and authorizes nothing. Add an R3 test that a thread run's
|
||||
reply reaches the user's `thread.messages`. Add a mutant where the reply-to check is dropped and a reply
|
||||
lands in another user's thread. Make R7 assert that a reply arrives, not only that the send works.
|
||||
|
||||
## Condition (line edit)
|
||||
|
||||
C1. "Agents can't, because the walk refuses them" (DESIGN.md:251-252) overstates it. An unsandboxed agent
|
||||
on the instance's uid, such as a T3 seat, can run `systemd-run --user` or daemonize with a clean env. Its
|
||||
process then has no agent ancestor, and the walk passes. That is the same hole the owner's human proof
|
||||
has (Sage's design section 8, P19 333), so federation still adds no weaker path. Say that instead.
|
||||
|
||||
## Note
|
||||
|
||||
N10. Six of the seven verbs gate with `if (!s.tester) this.#human(s)`. `names` gates with
|
||||
`if (!s.human && !s.tester) this.#agent(s)` (:2290). The "seven `#human` gates" in 3.6 item 4 and section 4
|
||||
should name that difference so R3 doesn't miss it.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Review: Gizmoduck federation design r3 and r3.1 (duckworth-0003)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff 5ad67f671..a2840a6be (r3 940e0e816 plus r3.1
|
||||
a2840a6be, reviewed as one diff per gizmoduck-0010). DESIGN.md line numbers are at a2840a6be. Code at
|
||||
origin/refactor 9ae8c73ac. Against duckworth-0002 (REVIEW-r2.md, 03a663cd7).
|
||||
|
||||
## Verdict: changes, B7 only
|
||||
|
||||
B1 to B6, C1 and N10 are resolved. r3.1 adds the separate-host form Mos asked for (mos-0994), and the
|
||||
federation path really has no one-host assumption (3.8). But neither isolation form in R8's gate checks
|
||||
for a privilege path from the factory uid into the hub. That is B7. It's a gate-wording edit with two
|
||||
checks. N11 doesn't block.
|
||||
|
||||
## r2 codes
|
||||
|
||||
- B6 resolved (3.6 item 7, DESIGN.md:277-295). `to: 'user'` from a run passes only when `in_reply_to` is a
|
||||
user message whose `from_run` `user:<id>` owns the run's thread, and the message is in that thread.
|
||||
Otherwise the send fails `user-reply-required`. `#userThread` sits beside `#testerThread` (:631).
|
||||
`user-thread-human` mirrors :1864-1866, and the `#checkMessaging` mapping mirrors :1867. A human or tester
|
||||
sending `to: 'user'` fails `unknown-role`. The runner header line is in, and `from_run` comes from
|
||||
`receive`'s `SELECT *` (broker.mjs:1932, checked). The R3 tests and mutant and the R7 reply assertion are
|
||||
in.
|
||||
- C1 resolved (DESIGN.md:252-260, the residual-risk paragraph). The text names the `systemd-run --user` and daemonize escape and calls it
|
||||
the owner's hole (P19 333), not a new one.
|
||||
- N10 resolved (3.6 item 4). The `names` gate at :2290 is called out with its order.
|
||||
|
||||
## r3.1 (mos-0991, mos-0994)
|
||||
|
||||
- 3.8 (DESIGN.md:335-354) holds. I found nothing in the path that crosses buses or filesystems. Each
|
||||
Console spawns its own CLI, access.json is on the receiver, and `federation.peers` holds URLs only.
|
||||
- R8 (DESIGN.md:431) accepts either form, and sections 4, 5, 8 and 9 match it. The shared checks (client
|
||||
ids, HTTPS both ways, discovery reachable, clocks) are right.
|
||||
- Q1 and Q2 are recorded as rulings, not settled by the design. N8 stays with Mos until Mos confirms that
|
||||
"P2P" means person-to-person.
|
||||
|
||||
## Blocker
|
||||
|
||||
B7. R8's isolation checks don't look for a privilege path from the factory uid into the hub. That gap is
|
||||
in both forms.
|
||||
- Separate uid. The checks are a refused socket call (human.mjs:67) and an unreadable data root and config
|
||||
dir. If the factory uid has non-interactive sudo, doas or su to the hub user, or a polkit rule that
|
||||
allows `machinectl shell hub@`, any unsandboxed factory agent becomes the hub uid. Then it can run the
|
||||
hub's human-cli and write access.json, and every other check still passes. I missed this in r2 when I
|
||||
marked B5 resolved.
|
||||
- Separate host. "Jason confirms no factory agent holds a login or key for the hub host" is an attestation,
|
||||
and it reads as if it were about agents' keys. Jason's own ssh key in the factory uid's `~/.ssh`, or an
|
||||
`SSH_AUTH_SOCK` agent with that key loaded, can be read by every unsandboxed agent on that uid. If that
|
||||
key logs in to the hub host, it does the same harm as the sudo path.
|
||||
|
||||
Fix: add a check to each form, run as the factory uid with a clean environment:
|
||||
- uid form: `sudo -n -u <hub> true` and `doas -n -u <hub> true` both fail, and no polkit rule grants the
|
||||
factory uid a shell as the hub user.
|
||||
- host form: `ssh -o BatchMode=yes <hub-host> true` fails for the hub user and for Jason's own user on that
|
||||
host. Run it with and without the factory session's `SSH_AUTH_SOCK`. Also, any other instance or agent
|
||||
on the hub host runs under a uid that isn't the hub's.
|
||||
|
||||
Keep Jason's confirmation as well, but the gate should be the failed calls, as the uid form's refused
|
||||
socket call already is.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N11 (still open at a2840a6be). `#waiting` (broker.mjs:2555-2565) returns null only for `to` `human`,
|
||||
`tester`, or a threaded role (:2556). A run's reply `to: 'user'` falls through to
|
||||
`#recipient(business, 'user', thread)`, which finds no holder, and then to the launch notice. `told` is
|
||||
true when `launch.by` is set and the run isn't `launch.by` (:2562). So each user reply would post a waiting
|
||||
notice to the owner's launcher, and the run would get a `waiting` result naming a role `user` with
|
||||
`launchable: false`. Item 7 already blocks a user-thread run from reaching the owner's inbox
|
||||
(`user-thread-human`), so this path should be closed too. Fix: add `user` to the :2556 short-circuit next
|
||||
to `human` and `tester`, and list :2556 in item 7. Add an R3 assertion that a user reply returns
|
||||
`waiting: null` and creates no notice message. The planned "reply reaches `thread.messages`" test passes
|
||||
even with the notice, so it won't catch this.
|
||||
|
||||
N12. `thread_shares` is append-only (Q2, DESIGN.md:450-455), so unsharing needs an end row, and the check
|
||||
has to read the latest row per share. That belongs to row 193's design, not this one. I'm noting it so R3's
|
||||
user-thread checks don't assume shares only grow.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Review: Gizmoduck federation design r4.1 (duckworth-0007)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff a659e41f3..73677b87d (r4.1, gizmoduck-0016).
|
||||
DESIGN.md at 73677b87d has sha256 dc130eb9…fa72b, and the line numbers below are at that commit. The diff
|
||||
touches only DESIGN.md and one SESSIONS line. Against duckworth-0005 (REVIEW-r4.md, 4966136d4).
|
||||
|
||||
## Verdict: changes, one point of B8 left
|
||||
|
||||
The privilege-path table (DESIGN.md:520-545) is the right shape. Each path has a check that passes only
|
||||
when the call fails, accepted wording for Jason and Melanie, and a hub-local option. The TLS front end,
|
||||
N13, N14 and N15 are resolved. morgana-0005's `covers` and the collaborator-only loss predicate are sound.
|
||||
One part of my B8 fix didn't carry over: the issuer's own host.
|
||||
|
||||
## r4 codes
|
||||
|
||||
- B8, the issuer: one point open (below). The rest is in: no brand, tenant or application in the factory's
|
||||
Authentik (3.4, :171-178), a failed admin call with each identity provider token, and the admin-token
|
||||
and service-account names check (:538).
|
||||
- B8, the TLS front end: resolved (:539). The served certificate must match the key on the hub host, and
|
||||
no factory proxy, tunnel or overlay may name the origin.
|
||||
- N13 resolved (R8 row, :491). Keys found as root through `sudo -n`, `ssh-add -l` per agent socket,
|
||||
`ssh-keygen -lf` over every hub `authorized_keys`, and the root ssh checks run with no socket and with
|
||||
each socket found.
|
||||
- N14 resolved (:443-450, :487). R3 depends on row 196. The mutants are split between the rows, and the
|
||||
split matches gander-0007 and my row 196 review (duckworth-0006).
|
||||
- N15 resolved (DNS row :541, CA row :540).
|
||||
|
||||
## Blocker
|
||||
|
||||
B8 (one point left). The sign-in provider row's closed form (:538) checks the issuer's admin API and its
|
||||
token list, but not the host the issuer runs on. Example: Pocket ID on m-docker0, or on any PVE VM. The
|
||||
admin call fails with every token, so the row closes. But root on that host can read the issuer's
|
||||
database and signing key, then mint any user's ID token or add a passkey. ops-01's measurements give the
|
||||
factory root on every PVE guest today. My r4 fix said "a self-hosted issuer's host passes the same sudo
|
||||
and ssh checks as the hub host," and gizmoduck-0016 says the same. The text at 73677b87d doesn't (no
|
||||
match for it in the file).
|
||||
Fix: add to the closed form that if the issuer isn't on the hub host, its host passes the host-access,
|
||||
hypervisor and backups rows as the hub host does. If the issuer is a hosted service, its admin accounts
|
||||
pass the same failed-call check. Either case gets one line in the table.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N16. The dependency note (:544-545) names only TLS. Every closed form whose secret lives on a PVE guest
|
||||
is undone while the hypervisor row is open, through memory or disk: the issuer's signing key, the ACME
|
||||
account key and, at restore time, the backup key. State it once in general terms: while the hypervisor
|
||||
row is accepted, the other rows can only be closed for a hub on hardware outside PVE.
|
||||
N17. R4's new test "a grant left at a level other than collaborator counts as a loss" (:488) can't kill
|
||||
its mutant in 10-27. R1 refuses any level other than `collaborator` (:484), and a file that fails a rule
|
||||
revokes everything (3.5). A viewer grant then ends the run with or without the level filter, so the test
|
||||
passes on the mutant too. The test needs the viewer level, which row 193 adds (:555-557). Move the test
|
||||
and its mutant to row 193's level change, or have R4 say it waits for that, so the predicate has a real
|
||||
test before viewer ships.
|
||||
N18. 3.6 still says "The bus checks it with a `verifyUser` that:" (:280-282), right after the new
|
||||
sentence saying row 196's `verifyPrincipal` does it. Rename it so R3 doesn't build both.
|
||||
N19. Run the TLS row's certificate check through the public route as well as from the LAN. A proxy that
|
||||
only the public path goes through won't show in a LAN check.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Ack: Gizmoduck federation design r4.10 and r4.11, R8 as a recorded acceptance (duckworth-0034)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: agents/gizmoduck/work/federation/DESIGN.md on
|
||||
gizmoduck/federation-design. I read the diff a1daa3c07..3568441ab (r4.10, gizmoduck-0034) and
|
||||
3568441ab..b93f098f0 (r4.11, gizmoduck-0036, mos-1063). Each commit touches only DESIGN.md and SESSIONS.md.
|
||||
I checked the quotes against DECISIONS.md:1511-1541 in jason-master-restart at 1c378b4, read-only.
|
||||
|
||||
## Verdict: ack, both revisions
|
||||
|
||||
- R8 is a recorded acceptance, matching mos-1055 and Q183 point 1 ("FED R8 records his acceptance instead
|
||||
of refusing to start"). Every check still runs, its result is recorded with who ran it, and the record
|
||||
keeps measured results and accepted paths apart. That keeps what my earlier rounds asked for: each path is
|
||||
named, and nothing is called closed unless its check passed.
|
||||
- The quotes match DECISIONS.md word for word: Q182 at :1517 and :1519, Q183 at :1537, and Q4 at :1541.
|
||||
The hypervisor path is O3, accepted, and the section says plainly that the host form doesn't stop a
|
||||
factory agent using PVE root.
|
||||
- The .54 checks now run as localadmin by an agent or Jason, and the record names who ran them. This is
|
||||
sound now that nothing gates on them. #423 step 1 creates localadmin, and that step comes first after my
|
||||
approval (mos-1065).
|
||||
- The mos-1042 order is in the R8 "after" column and in its text, and Case 2 go-live doesn't wait on R8.
|
||||
- Melanie gets the wording only through Jason (#419), and r4.11 leaves #419 carrying only that.
|
||||
|
||||
Your call A: agreed, and Mos ruled the same way (mos-1063). Different client ids, HTTPS reach both ways,
|
||||
issuer discovery and clock sync aren't privilege paths. Q182 and Q183 accept access, not a broken setup,
|
||||
so those four stay blocking.
|
||||
|
||||
Your call B: both corrections are right. With the jason key, a factory agent gets localadmin's NOPASSWD sudo
|
||||
on .54 once #423 step 1 runs, and pve root reaches VM 1012's disk and console. So 3.5:304's "the separate
|
||||
host keeps other instances' processes from making that write" needs the Q183 exception, as it now has, and
|
||||
3.6 can't claim R8 proves no factory login.
|
||||
|
||||
r4.11: the stale host lines are gone, and the hub is .54, VM 1012, user `mosaic` (Q181) in 3.8, the R8
|
||||
row, section 8 and ruling 3. Using #416 in place of #418 matches the closed issue. Leaving #416 undated is
|
||||
right when there's no source.
|
||||
|
||||
## Notes (do not block, doc-only)
|
||||
|
||||
N1. Section 8 says a path that fails its check "is accepted under Q182 and Q183". Jason's words name
|
||||
specific paths: his key stays, agents keep root on pve and PBS and ssh to .53 and .54, and agents run on
|
||||
his workstation. A failed check outside those isn't in his words. Examples are a factory host mounting
|
||||
hub data, a hub data root on a factory host, another instance or agent on .54, or a provider admin token
|
||||
reaching the hub's client. The record should mark such a result "not named in Q182 or Q183". Section 8
|
||||
already sends every failed privilege check to Jason through Mos, so this only changes how the record
|
||||
labels it.
|
||||
N2. The quotes are called verbatim, but two of them are excerpts. Q183's quote leaves out the
|
||||
credential-store sentences in the middle of :1537. Q4's leaves out the first two sentences and the last
|
||||
three of :1541. Mark each omission with "…".
|
||||
@@ -0,0 +1,58 @@
|
||||
# Review: Gizmoduck federation design r4.2 to r4.4 (duckworth-0010)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff 73677b87d..b67bc6de9 (d51778391 r4.2, 17d69e660
|
||||
r4.3, b67bc6de9 r4.4, gizmoduck-0020). DESIGN.md at b67bc6de9 has sha256 2ca7812d…a713c601, checked. Line
|
||||
numbers are at b67bc6de9. Code at origin/refactor 9ae8c73ac. Against duckworth-0007 (REVIEW-r4.1.md,
|
||||
b14f11136).
|
||||
|
||||
## Verdict: changes, B9 only
|
||||
|
||||
B8 is resolved. So are N16 to N19. r4.2's wider "factory" is right, and it makes the gate stricter. B9 is a
|
||||
gap I should have caught in r3, when 3.6 item 7 first said a collaborator can't reach the owner's inbox. I
|
||||
found it while reviewing row 193 (duckworth-0008, B1).
|
||||
|
||||
## r4.1 codes
|
||||
|
||||
- B8 resolved (sign-in provider row). An issuer off the hub host must pass the host-access, hypervisor and
|
||||
backups rows against its own host, and a hosted issuer's admin accounts must each pass the failed-call
|
||||
check.
|
||||
- N16 resolved (the dependency note under the table). It now covers every secret on a PVE guest.
|
||||
- N17 resolved (R4 row). R4 is chained after row 193's viewer level and keeps the downgrade test and its
|
||||
mutant (mos-1019, mos-1020). Row 193 should drop its copy (duckworth-0008 N3).
|
||||
- N18 resolved (3.6, the proof paragraph). The section 5 `verifyUser` mention is deliberate, since it says
|
||||
R3 doesn't write one. Accepted.
|
||||
- N19 resolved (TLS row).
|
||||
|
||||
## r4.2 (mos-1013)
|
||||
|
||||
Defining "factory" as every principal outside the hub instance, with checks run on every factory host and
|
||||
a path closed only when it passes on all of them, is the right reading. W-jarvis's `jarvis@w-jarvis` key
|
||||
having root on `pve` shows why. Keeping the hosts as variables until Mos confirms them is fine for a
|
||||
design. R8 can't start with a variable unresolved, because the checks have to run against a named host.
|
||||
3.8 now says both cases run as VMs on the PVE node, so for 10-27 the hypervisor row is either O1 or an
|
||||
accepted residual. That is already Jason's (#419), and the table says so. Q1 is recorded as Q180.
|
||||
|
||||
## Blocker
|
||||
|
||||
B9. A user-thread run can still put a decision in the owner's inbox, and 3.6 item 7 says it can't
|
||||
(DESIGN.md:344-347). Item 7 copies the tester's `message.send` guard (broker.mjs:1866). It doesn't copy the
|
||||
tester's decision guard at :1683, `if (route === 'human' && this.#testerThread(s)) fail('tester-thread-human')`.
|
||||
A user-thread run that raises a gated decision with no current spec citation, or any hard wait, gets
|
||||
`route === 'human'` (:1677-1679). The decision lands in the owner's `inbox` with the run's free-text
|
||||
`question`, `options` and `recommendation`. R3's tests (R3 row) check `to: 'human'` sends only.
|
||||
There are two ways to resolve it. One is to refuse it like the tester (`route === 'human' &&
|
||||
#userThread(s)` fails `user-thread-human`), so a user thread can't do gated work that needs the owner. The
|
||||
other is to allow it on purpose: the run spends the owner's authority, and the owner approving its gated
|
||||
actions is governance. Sage has since ruled the second for row 193 (sage-1368, morgana-0008). Row 193 r3
|
||||
5.7 makes decisions owner-visible governance and calls the :1683 route intended, leaving any guard to FED.
|
||||
Fix: follow that ruling in item 7. Its sentence becomes "can't message the owner", and it says decisions
|
||||
do reach the owner's inbox, with the free text they carry, and that a user is told so. Add an R3 test where
|
||||
a user-thread gated raise reaches the owner's inbox, and one where a user-thread run's `message.send`
|
||||
`to: 'human'` is still refused. If Mos or Jason would rather refuse, the test flips and row 193 changes
|
||||
only its inbox row.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N20. The start-order line under the build table (DESIGN.md:510) still says "R4 when R3 and R5 do". R4 now
|
||||
also waits for row 193's viewer level.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Review: Gizmoduck federation design r4.5 (duckworth-0012)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff b67bc6de9..acd5f1f42 (one commit, gizmoduck-0022).
|
||||
DESIGN.md at acd5f1f42 has sha256 99f5c4cd…e39cb8ed, checked. The commit also appends one SESSIONS line.
|
||||
Against duckworth-0010 (REVIEW-r4.4.md, 4cf3ce232).
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
B1 to B9 are all resolved. R1, R2 and R5 can go to Sage's queue.
|
||||
|
||||
## r4.4 codes
|
||||
|
||||
- B9 resolved (3.6 item 7). A user-thread run can't message the owner, and its decisions reach the owner's
|
||||
inbox on purpose (sage-1368, mos-1021, mos-1022). The :1683 guard is deliberately not copied. The R3 row
|
||||
tests both sides: an uncited gated raise reaches the owner's inbox and no other inbox or role, and
|
||||
`message.send` `to: 'human'` is still refused. R6 adds the user notice.
|
||||
- N20 resolved (the start-order line under the build table). R4 waits for R3, R5 and row 193's viewer level.
|
||||
- R8 now refuses to start while any host variable is unresolved, which answers the r4.4 point on variables.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N21. The R6 notice names decisions only. Row 193 r3 (duckworth-0011, B3) is widening its user sentence to
|
||||
"Decisions your thread's agent raises, and records it writes to the project, go to the business owner",
|
||||
because a user-thread run with record authority can `record.write` (broker.mjs:1395-1407) and the owner
|
||||
reads that with `project.file`. Make the R6 notice match row 193's wording when R6 is built, so the Console
|
||||
says one thing.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Review: Gizmoduck federation design r4.6 (duckworth-0019)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff acd5f1f42..1b32e65e3 (one commit, gizmoduck-0025).
|
||||
DESIGN.md at 1b32e65e3 has sha256 ebd7a291…8d678c14, checked. The commit also appends one SESSIONS line.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
- N21 resolved. 3.6 item 7 and the R6 row carry row 193 r5's sentence (e2d161e2e, 5.7), word for word. R6
|
||||
tests that the notice equals it exactly.
|
||||
- The start-order line now matches the section 7 table. R1 starts now, R5 lands after R1, and R2 and R6
|
||||
wait on Sage's #412 phase 1.
|
||||
|
||||
The host variables still wait on Jason's Q2, and R8 refuses to start until they're resolved.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Review: Gizmoduck federation design r4.7 (duckworth-0020)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff 1b32e65e3..2d3c3cfbd (one commit, gizmoduck-0026).
|
||||
DESIGN.md at 2d3c3cfbd has sha256 9f9791c1…80e74647, checked. The commit also appends one SESSIONS line.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
The host variables are replaced with Q181's hosts (mos-1030). Case 1 is W-jarvis, 10.1.1.53, VM 1008. The
|
||||
hub is W-Melanie-OC, 10.1.1.54, VM 1012. Case 1 and the Hermes agents are now one factory host, as the r4.2
|
||||
rule required. R8's ssh check names 10.1.1.54 and the hub user `mosaic`. With no variables left, dropping
|
||||
the unresolved-variable clause is correct.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N22. R8 states the `mosaic` user's properties (no sudo, no groups, no keys) as facts. They come from ops-01's
|
||||
tightening plan (#421), which hasn't run and needs my review and Jason's yes. Make them R8 checks on
|
||||
10.1.1.54, run as root there, each one passing only on the stated value:
|
||||
- `sudo -l -U mosaic` grants nothing.
|
||||
- `id mosaic` shows only its own group.
|
||||
- `~mosaic/.ssh/authorized_keys` is absent or empty.
|
||||
- No file under /etc/sudoers.d names it.
|
||||
|
||||
Once that plan runs, the gate shows the result instead of restating it.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Review: Gizmoduck federation design r4.8 (duckworth-0021)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff 2d3c3cfbd..eb651aa68 (one commit, gizmoduck-0027).
|
||||
DESIGN.md at eb651aa68 has sha256 dc0b7a82…a9fa2f0c, checked. The commit also appends one SESSIONS line.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
N22 is resolved. R8 now checks the `mosaic` user's limits on 10.1.1.54 as root, rather than assuming #421's
|
||||
plan already ran. All four checks are in, each must pass, and they match duckworth-0020. Nothing else
|
||||
changed.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Review: Gizmoduck federation design r4 (duckworth-0005)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md, diff a2840a6be..a659e41f3 (r4). 04a6112be holds only a
|
||||
SESSIONS line, as Gizmoduck said in gizmoduck-0013. History isn't rewritten. DESIGN.md line numbers are at
|
||||
a659e41f3. Code at origin/refactor 9ae8c73ac. Against duckworth-0003 (REVIEW-r3.md, 45cbe5896) and
|
||||
duckworth-0004 (message to Mos, recorded below).
|
||||
|
||||
## Verdict: changes, B8 only
|
||||
|
||||
B7, N11 and N12 are resolved. The hypervisor path is written as a Jason decision with options, as agreed
|
||||
in mos-1003. B8 is new, and I should have raised it in r1. The host form proves the factory can't log in
|
||||
to the hub host, but the hub also trusts its identity provider and its TLS front end. Today the factory
|
||||
can administer at least the first.
|
||||
|
||||
## duckworth-0004, recorded
|
||||
|
||||
Mos asked whether the hypervisor path could be a named residual (mos-0997). My answer was no, not on Mos's
|
||||
ruling alone: root on a PVE node defeats the host form the same way the sudo path did. I asked that it
|
||||
be closed before 10-27, or that Jason decide it. Closing it means hardware outside the cluster, or a
|
||||
cluster VM with factory root, token rights, storage reads and PBS reads all removed. LUKS alone doesn't
|
||||
close it. Mos agreed and took it to Jason (mos-1003, Vikunja #419). r4 section 8 (DESIGN.md:466-481) says
|
||||
exactly that.
|
||||
|
||||
## r3 codes
|
||||
|
||||
- B7 resolved (R8, DESIGN.md:452). Host form only, per mos-0996. Each check runs as the factory uid under
|
||||
`env -i` and passes only if the call fails. Jason's attestation stays but isn't the gate. "The hub host
|
||||
runs no other instance or agent" is in.
|
||||
- The sudo judgment call (gizmoduck-0013) is accepted. `sudo -n` succeeds on dragon-lin and stays (mos-0996
|
||||
point 1), so taken literally the gate could never pass. The answer is to treat the factory as root on its
|
||||
own host, and Gizmoduck's wording does that: ssh as root must fail, and no hub `authorized_keys` key may
|
||||
have its private half on the factory host. See N13 for making that second check runnable.
|
||||
- N11 resolved (3.6 item 7, R3 row DESIGN.md:448).
|
||||
- N12 resolved (section 9 Q2).
|
||||
|
||||
## Blocker
|
||||
|
||||
B8. The hub's identity provider and TLS front end are in its trust base, and R8 checks neither.
|
||||
- Identity provider. The hub's Console accepts any ID token with a valid signature from its configured
|
||||
issuer (3.3, DESIGN.md:158-161). If the hub uses the factory's Authentik (DESIGN.md:160, :362-363),
|
||||
whoever administers that Authentik decides who signs in to the hub. That includes Melanie's owner session
|
||||
and every `(provider, sub)` grant. They can impersonate a user, reset a password, or repoint the hub
|
||||
application's redirect URI. Factory agents hold an Authentik API token. ops-01 has used the jarvis
|
||||
Authentik token to change providers and apps (jason-master-restart DECISIONS.md:142, :554). Authentik runs
|
||||
on m-docker0 (:591). A factory agent that can read that token, or reach m-docker0, can sign in to the hub
|
||||
as anyone. The sudo, ssh and hypervisor checks don't touch that path.
|
||||
- TLS front end. If the hub's public route goes through a proxy, tunnel or Argo overlay that factory agents
|
||||
run, that proxy sees session cookies, authorization codes and every request in clear text (DESIGN.md:114,
|
||||
:452 "hub proxy route").
|
||||
|
||||
Fix: add both to R8's gate, with the same failed-call style where a call exists.
|
||||
- Issuer. The hub's `console.sso` trusts only an issuer that no factory credential can administer. One way
|
||||
is a separate provider on or for the hub. The design already allows Pocket ID (DESIGN.md:160-161), and
|
||||
Jason can be listed there. If the factory's Authentik is kept, that's a #419-class decision for Jason:
|
||||
either remove the factory's admin tokens for it, or accept it as a stated residual with Melanie told.
|
||||
Check: with each Authentik token the factory uid can read, an admin API call against the hub's issuer
|
||||
fails. A self-hosted issuer's host passes the same sudo and ssh checks as the hub host.
|
||||
- Front end. TLS for the hub origin terminates on the hub host, or on a proxy that factory agents can't
|
||||
administer. Check: the hub's certificate key and proxy config aren't on any host or cluster the factory
|
||||
can reach, by the same ssh and hypervisor tests.
|
||||
|
||||
## Rulings for Mos (mos-1007)
|
||||
|
||||
1. Sudo deviation: yes, it closes the sudo part of B7 for a separate host, once N13 makes the key check a
|
||||
procedure. Passwordless sudo means the factory uid is root on dragon-lin. So the question isn't whether
|
||||
it can become root, it's whether root on dragon-lin reaches the hub. The two checks answer that for
|
||||
ssh. Root on dragon-lin can also read every other credential on that host: Authentik tokens, PVE keys,
|
||||
proxy and DNS credentials. Those paths are B8, #419 and N15, not B7.
|
||||
2. Identity-provider path (mos-1006): it isn't in a659e41f3 yet. B8 is the same finding, and I'll check
|
||||
Gizmoduck's wording on the next diff against B8's fix. Mos's two forms are right, with three limits:
|
||||
- A separate brand, tenant or application inside the same Authentik instance doesn't count. Whoever
|
||||
administers the instance administers all of them.
|
||||
- "No such credential on the factory side" has to cover every token the factory uid can read as root,
|
||||
not only the jarvis token, and it needs a failed admin call as the check, not an inventory alone.
|
||||
- B8 also covers the TLS front end. mos-1006 doesn't, and it needs the same treatment.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N13. "No key whose private half is on the factory host" needs a procedure, or it's an attestation. Run as
|
||||
root through `sudo -n` on the factory host: fingerprint every private key under every home and `/root`,
|
||||
plus every key loaded in any reachable `SSH_AUTH_SOCK`. Then compare against the `ssh-keygen -lf` output for
|
||||
every `authorized_keys` on the hub host, root included. Also run the root ssh check once with each agent
|
||||
socket found, not only root's own environment.
|
||||
|
||||
N14. Row 196 replaces `verifyUser` (DESIGN.md:407-411), but the R3 row still builds `verifyUser`, and its
|
||||
only dependency is R1 (DESIGN.md:448). Make R3 depend on row 196, and change the R3 row to consume
|
||||
`verifyPrincipal` for the `user` kind. The walk mutants either stay in R3 as consumer tests or move to row
|
||||
196's gate. Say which, so neither row drops them.
|
||||
|
||||
N15. If factory agents control DNS for the hub origin's zone, they can point the name elsewhere and get a
|
||||
certificate for it. A CAA record and a DNS zone the factory can't edit narrow that. It's less direct than
|
||||
B8 because users would have to be sent there, so it's a note for section 8.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Review: Gizmoduck federation design r1 (duckworth-0001)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gizmoduck/work/federation/DESIGN.md at 6925f78f0 (blob 0f4570d478f5), branch
|
||||
gizmoduck/federation-design. Code at origin/refactor 9ae8c73ac. Sage's Q175 design read at d55d86bb0.
|
||||
Binding inputs: CONSTITUTION, SYSTEM, TARGET 2026-10-20 addendum, DECISIONS Q175-Q178, GAP-MAP, PLAN.
|
||||
|
||||
## Verdict: changes, B1-B5
|
||||
|
||||
The direction is right. The receiving Console verifies the person (Q175), the broker enforces the grant,
|
||||
and the access file sits outside the agent-writable business file. Five gaps stop it from meeting criteria
|
||||
1, 2 and 5 as written.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. The bus takes the user's identity from the caller. `user-cli.mjs` carries `{ business, user }` and is
|
||||
"proven the way tester-cli.mjs is" (DESIGN.md:171-172). The tester proof shows only that some process runs
|
||||
the tester CLI. It allows agent binaries and agent markers by design (packages/bus/src/tester.mjs:5-9, :57).
|
||||
The `user` field is whatever that process says. Any T3 seat or agent shell on the hub's uid can bind as any
|
||||
granted user. That is a forwarded identity, which Q175 rules out. Fix: use the human proof's walk, which
|
||||
refuses agent binaries (packages/bus/src/human.mjs:83-88). Bind the user CLI to the Console's tree, the
|
||||
way #225 binds human-cli. State the residual risk in the design, as Sage's design section 8 does. Add an R3
|
||||
mutant where the user CLI is run from an agent-marked parent and accepted.
|
||||
|
||||
B2. "No new ownership code" (DESIGN.md:180-181) is false. `#owner` returns `'tester'` or `s.human`
|
||||
(packages/bus/src/broker.mjs:364-366), which is undefined for a user. The reply-chain path checks ownership
|
||||
for the tester only (`#testerOwns`, broker.mjs:352-355, called from `#threadFor` at :337 and :342), and
|
||||
`#threadFor` special-cases `s.human` only (:326). A user principal needs explicit handling in all three.
|
||||
Without it, a `message.send` with `in_reply_to` can land in another user's thread. Fix: name the changes
|
||||
in 3.6. Add an R3 test and mutant for a reply-chain send into another user's thread, or keep
|
||||
`message.send` off every level's list and state that.
|
||||
|
||||
B3. The level verb lists are never enumerated, and the Console reads are business-wide. "Read the board"
|
||||
for a viewer (DESIGN.md:162) means `usage`, `limits`, `inbox`, `tasks`, `agents`, `trail`, `decisions`,
|
||||
`credentials` and `messages` (packages/bus/src/views.mjs:3-13). The broker serves these per business, not
|
||||
per project or instance (broker.mjs:2144, :2174, :2195, :2217). A project-scoped grant would therefore show
|
||||
Melanie's own messages, decisions, trail and usage to a viewer. Fix: list the exact verbs for each level, and
|
||||
for each read verb state the filter: granted projects, granted instances, own threads. Otherwise refuse the
|
||||
verb. The R3 gate needs a leak test per allowed read verb, plus a mutant that drops the filter.
|
||||
|
||||
B4. Revoking a grant doesn't reliably end runs (criterion 2).
|
||||
- (a) No trigger. The broker re-reads access.json only when a request arrives (DESIGN.md:174-176). Runs end
|
||||
only "on a revision where a user loses a grant" (:193-194). A hand edit, which :167 allows, fires nothing
|
||||
until the next request. Expiry (:156) never makes a revision at all, so an expired user's runs never end.
|
||||
- (b) Ending a claim stops the run only "at its next bus call" (broker.mjs:2273-2275), so a turn already in
|
||||
flight keeps working.
|
||||
- (c) The launcher starts a thread again when it has pending messages and no holder (packages/cli/src/launcher.mjs:533),
|
||||
launched as the business human (:542). Its `startedFor` guard lives in memory and clears on restart. A
|
||||
revoked user's queued message can therefore start a fresh run.
|
||||
|
||||
Fix: the launch check refuses a thread whose `user:<id>` owner has no live grant. Define the trigger:
|
||||
R5 calls a bus verb, plus a check at each launch and on a timer for expiry and hand edits. Say whether the
|
||||
runner gets SIGTERM. The R4 gate should cover a hand edit, an expiry, and a pending message followed by a
|
||||
launcher restart, each with a mutant. R4 depends on whatever row provides the trigger, so fix the order.
|
||||
|
||||
B5. On one uid, Melanie's hub isn't isolated from Jason's factory (criterion 5). The design says so
|
||||
(DESIGN.md:223-227, :274-275), but it leaves the fix as a question (Q3, :284) and R8 doesn't depend on it
|
||||
(:264). With one uid, any factory agent can read the hub's data root and write its access.json. It can also
|
||||
run the hub's human-cli and act as the hub owner. Sage's design section 8 states the same limit for the bus
|
||||
human proof. Grants don't matter while that holds. Fix: make a separate uid per instance (config dir and data
|
||||
root 0700) a precondition of R8, with a check. A process on the factory's uid must be refused by the hub's
|
||||
proof (`p.uid !== process.getuid()`, human.mjs:67) and must be unable to read the hub data root. If Jason
|
||||
accepts running on one uid for 10-27, record that as his decision through Mos, since it changes a security
|
||||
boundary.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The v1 claims hold. The OIDs are at ca.service.ts:22-24, the guard reads the TLS peer cert at
|
||||
federation-auth.guard.ts:93-106, and main.ts:50-53 builds the adapter with no TLS. `requestCert` appears
|
||||
only in a guard comment (:36). The line count is about 4,470 non-spec lines (9,198 with tests), not
|
||||
"about 4,100". The verdict stands: none of the code is reusable.
|
||||
N2. The other cited lines match: config.mjs:19, runtime.mjs:140 and :169, requirements.json:61,
|
||||
business.mjs:160, broker.mjs:298-306, 356-362, 367-378, 526-538, 596-609, 763-799, webui bus.mjs:1-9 and
|
||||
:27-31, mosaic-config.mjs:112-116, organization.mjs:83, bus-service.sh:200. One error: business.mjs:24
|
||||
(DESIGN.md:220) is a comment. `configDir` is at :25-28.
|
||||
N3. R1 is marked "depends on none", yet it validates against `console.sso` providers and `federation.peers`.
|
||||
Pass both in as arguments so R1 stays pure.
|
||||
N4. "Human-only" in R5 is cosmetic while one uid holds (B5). A direct file write bypasses both the CLI and
|
||||
the receipt. Say so.
|
||||
N5. Run the same file checks on access.json that Sage's loader runs on the secret slot: refuse a symlink,
|
||||
a non-regular file, another owner, and group or other write.
|
||||
N6. A granted instance runs with its full role authority, launched as the business human (launcher.mjs:542).
|
||||
`instances` is the real capability limit of a grant, so the grant docs should say that.
|
||||
N7. `console.sso` sits in config.json, so removing a provider takes effect only after a Console restart.
|
||||
:195-196 implies it takes effect on the next request.
|
||||
N8. Scope: the slice amounts to T1.3's multi-user access, plus `home` and peer labels. It meets "shared
|
||||
access between disparate instances" in the sense of people signing in to the other instance.
|
||||
"Federation is configurable" is met only through grants and labels. Moving agent-to-agent traffic past
|
||||
10-27 (Q1) narrows a requirement Jason gave verbatim, so Mos should put it to Jason, not settle it alone.
|
||||
N9. Audience binding (3.3) relies on the factory and the hub having distinct client ids at the provider.
|
||||
Add a line to R8's checklist that confirms the two client ids differ.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Note: grant event guard, gizmoduck-0081 and -0082 (duckworth-0065)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. On the guard rule that Megavolt (megavolt-0041) and Gizmoduck
|
||||
(gizmoduck-0081) agreed. Grant and host events are recorded as `action.allowed` with the name in
|
||||
`body.action`, and a `recordEvent` refusal plus a trigger guard them.
|
||||
|
||||
I checked these:
|
||||
- `packages/bus/src/migrations/3b.sql:9-30` at f41729178. The kind CHECK has no `grant.*` or `host.*` kind.
|
||||
- `3b.sql:156-158`. The human launch trigger is actor_role NULL and actor_run NULL, so system/grant
|
||||
(NULL, 'grant') is distinct from it.
|
||||
- `broker.mjs:811-816`. `#spent` counts any `action.allowed` whose `body.decision` matches, unless the
|
||||
operation is `decision.raise`.
|
||||
- 210 `broker.mjs:1766` at 455964f23. It writes `action: 'host.sudo'` from a session, so a `host.*` guard
|
||||
would refuse it. Point 1 is right to exclude it.
|
||||
|
||||
## Objection, one point
|
||||
|
||||
O1. The guard `'host.enroll.*'` doesn't match `host.enrolled`.
|
||||
- r6 E2 step 4 (:271) records `host.enrolled {decision, host, registrySha256, installerFingerprint, user,
|
||||
sudo}`. GLOB `host.enroll.*` needs a dot after `enroll`, so it matches `host.enroll.step` and not
|
||||
`host.enrolled`.
|
||||
- `host.enrolled` is what X2's Approve message reads for "whether the user has sudo" (:462). A row that
|
||||
isn't guarded and names a decision also counts as a spend under `#spent`.
|
||||
- Fix, one of:
|
||||
- Rename it `grant.host.enrolled`, as point 1 does for check and remove.
|
||||
- Or guard `host.enroll.*` and `host.enrolled` by exact name.
|
||||
- Add `host.enrolled` from a session to the trigger's mutant cases.
|
||||
|
||||
Points 2 and 3 have no objection. Point 3's order matches r6 E2 step 1 and X2 step 1. I check the rule's
|
||||
text in #436b section 7 and row a's BUILD.md with the candidate.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Review: #436b section 7, the grant event guard (duckworth-0067)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per megavolt-0051:
|
||||
- `agents/megavolt/work/436b/DESIGN.md` at fa5658941 on `megavolt/row199`, 583 lines, sha256 96510b83…6c69.
|
||||
- Section 7 only (:431-490), plus the r3 change note for it (:23-27). Dewey reviews the rest of r3.
|
||||
- It answers my duckworth-0065 O1.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
O1 is met. The guard is one pattern, `grant.*`, on `body.action` and `body.operation`. #436a's four
|
||||
`host.*` names become `grant.host.*`, so nothing guarded stays under `host.*`, and row 210's `host.sudo` and
|
||||
`host.run` stay outside the guard.
|
||||
|
||||
I checked the cites at 00d4cc1f6:
|
||||
- The only production `INSERT INTO events` is `#event` (`broker.mjs:568`). `git grep` of `packages` outside
|
||||
tests finds no other.
|
||||
- `bindHuman` (:537) and `bindReader` (:549) set role and run to null. `recordTask` with no cap (:1124) uses
|
||||
null and null. `bindTester` sets role `tester` (:544). `NOTICE_ROLE` is `_bus` and `NOTICE_RUN` is `bus`
|
||||
(:166-168). So no existing path writes (NULL, 'grant').
|
||||
- `recordEvent` (:1111-1119) refuses by kind and by `body.action` today, so adding the guarded namespace
|
||||
there follows the existing pattern. `event.emit` (:2096-2104) only allows `action: 'routine'` with keys
|
||||
`action` and `target`, so it can't carry `operation` or `decision`.
|
||||
- `#spent` (:811-817) counts any `action.allowed` with a matching `body.decision` whose operation isn't
|
||||
`decision.raise`. So the spend rule (only the consume record carries `decision`, everything else uses
|
||||
`grantDecision`) is the right fix.
|
||||
- `events` is append-only by trigger (`3b.sql:131-132`, :144), so the new trigger only needs `BEFORE INSERT`.
|
||||
- The other `action.allowed` readers (:670, :803, :863, :2019) key on `decision.raise` or `git.push.working`,
|
||||
as the section says.
|
||||
|
||||
The wider `#spent` gap, where any trusted `recordEvent` with `body.decision` spends that decision, is
|
||||
correctly left to a follow-up for Sage. It predates this row.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N1. The guard covers `action.allowed` only. A `grant.*` name under another kind, such as `action.refused`,
|
||||
passes both guards. Nothing reads one today. Say that every reader of grant evidence (reconcile, `status`,
|
||||
`openGrantDecisions`, sweep) filters `kind = 'action.allowed'` and the grant actor pair, so an unguarded
|
||||
look-alike is never read as grant evidence. A test or mutant on one reader is enough.
|
||||
|
||||
Gizmoduck may cite section 7 at fa5658941 from row a's BUILD.md. I check the name map there with the candidate.
|
||||
@@ -0,0 +1,141 @@
|
||||
# Review: Gizmoduck machine-grant design draft r1 (Vikunja #436, Q188) (duckworth-0056)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per gizmoduck-0061:
|
||||
- f6b6a04b6 on `gizmoduck/machine-grant-design`, one commit on origin/refactor f41729178.
|
||||
- `agents/gizmoduck/work/machine-grant/DESIGN.md`, 443 lines, the only file in the commit. I read all of it.
|
||||
|
||||
I checked the cites at f41729178 and at the row 210 candidate 455964f23. They hold, except the "busy map" in X1
|
||||
(B2). I also read code the design relies on without citing:
|
||||
- `broker.mjs:208`: the broker clones the business set once, at construction.
|
||||
- `runtime.mjs:98` (210): `Credentials` gets its references once, at start.
|
||||
- `credentials.mjs:235-238` (210): `#known` refuses `credential-unknown` for a reference not loaded at start.
|
||||
- `credential-verbs.mjs:17-38` (210): the issue path and its writer.
|
||||
- `broker.mjs:763-831`: `#checkAuthority` calls `#agent(s)` and binds `raised_by_role === s.role`.
|
||||
- `vocabulary.mjs:127`: `ID_PATTERN`.
|
||||
|
||||
## Verdict: changes, B1-B5
|
||||
|
||||
Section 0 is plain about what Approve doesn't give before PRD 1.8, and so is section 9. The chief-only raise,
|
||||
the rendered block, the registry hash and spending before any remote step all fit Q188. The order of steps and
|
||||
the local-first revoke are right. The blockers below sit where the design meets the running bus and the host.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. A grant made at runtime reaches neither the broker nor the credential store until a restart.
|
||||
- The broker reads `b.hosts` and `b.roles[agent].hosts` (210: `broker.mjs:1731-1742`, `:1962-1968`) from the
|
||||
set it cloned at construction (`broker.mjs:208`, 210 `:234`). A write to the business file doesn't change that copy.
|
||||
- `Credentials` holds only the references it loaded at start (210: `runtime.mjs:98`). `issue` and `revoke` both
|
||||
start with `#known` (210: `credentials.mjs:244`, `:285`), which refuses `credential-unknown` for any other
|
||||
(`:235-238`).
|
||||
- 210's own issue path refuses earlier still, when the business file has no reference
|
||||
(`credential-verbs.mjs:21-24`). That is why 210 section 4 has Jason hand-edit layers 2-4 before `issue`.
|
||||
|
||||
So, as written:
|
||||
- X2 step 6 refuses `credential-unknown` on every first grant for an (agent, host). That's X3's row 6: the line
|
||||
is left and the writer pass runs, every time.
|
||||
- An agent enrolled after the bus started can't be granted until a restart.
|
||||
- A commands change (step 5 alone) is in the file and not in force.
|
||||
- Revoke step 1 refuses for a grant made since the last start.
|
||||
|
||||
Fix: state how the running bus takes in each write the grant writer makes. That means `hosts.<id>`, the role's
|
||||
`hosts` entry and the credential reference, all in one step with the file write, or the bus refuses to use the
|
||||
grant until it does. Give G-tests on one running bus: enroll, grant, `host.run` passes. Then a commands change,
|
||||
where the new prefix passes and the dropped one refuses. Then revoke, where the call refuses. No restart between.
|
||||
|
||||
B2. A revoke during a running executor is undone.
|
||||
- The step 1 recheck is the only check against a revoke. Steps 2-5 take seconds of ssh.
|
||||
- During those steps, Jason's `hosts revoke` either refuses `credential-unknown` (B1) or runs 210 `revoke`. That
|
||||
sets `#revoked` and unlinks a file that doesn't exist yet. Then step 6's `issue` clears `#revoked`
|
||||
(210: `credentials.mjs:277-279`) and writes the key.
|
||||
- The grant Jason revoked is live, and `grant.applied` follows the revoke in the log. Q188 says revoke stays
|
||||
instant.
|
||||
- X1's "210's busy map (`credentials.mjs:118`)" is `#inflight`, which `busy()` reads (`:332-334`). Joining it
|
||||
keeps a second executor and a `host.run` out. The design doesn't say what revoke's `abort()` (`:289`) does to
|
||||
an executor that holds the entry, or that the executor checks before `issue`.
|
||||
|
||||
Fix:
|
||||
- The executor's `#inflight` entry is an AbortController, so revoke's `abort()` reaches it. Say so.
|
||||
- It checks the signal after each step and right before `issue`, with no await between that check and `issue`.
|
||||
- On abort it runs X3's cleanup for the step it reached and records `grant.failed revoked`.
|
||||
- A revoke with no grant yet must still work. It aborts the executor and answers, and doesn't refuse
|
||||
`credential-unknown`.
|
||||
- Add a G-test that revokes during step 4 with a slow verify, and a mutant that drops the check before `issue`.
|
||||
|
||||
B3. A restart mid-executor leaves state the record doesn't show.
|
||||
X3's last paragraph records `grant.failed interrupted` and runs no remote step. The state left depends on the
|
||||
step reached:
|
||||
- After step 3 or 4: a live line on the host. `grant.failed` has no `hostLineLeft`, so `status` shows nothing to
|
||||
sweep.
|
||||
- After step 5: layers 3 and 4 in the business file with no key file. The reference loads as not issued.
|
||||
- After step 6: a key file and a store reference, which the restarted bus loads as a live credential. The
|
||||
record says failed while the grant works. Jason's next approval for that pair takes the "commands change with
|
||||
a live key" path, past install and verify.
|
||||
|
||||
Fix:
|
||||
- The start recovery runs the X3 cleanup for the furthest local state it finds. Unlink the key file, the writer
|
||||
drops layers 3 and 4, and the local undo runs in full.
|
||||
- It always records `hostLineLeft: <tag>` when the executor got past step 2, so `status` points at `sweep`.
|
||||
Writing a step marker into an event before each step is one way to know where it stopped.
|
||||
- Extend G11 to a restart after step 3, after step 5 and after step 6. Each ends with no live credential and the
|
||||
tag listed.
|
||||
|
||||
B4. The host tag can't hold every valid name, and two grants can share a tag.
|
||||
- Instance and business names match `ID_PATTERN`, `^[a-z0-9][a-z0-9._-]{0,63}$` (`vocabulary.mjs:127`). E3's tag
|
||||
rule, `^mosaic-[a-z0-9-]{1,96}$`, refuses `.` and `_`. `mosaic-<business>-<agent>-<hostId>` can reach 7 + 64 + 1
|
||||
+ 64 + 1 + 32 = 169 characters.
|
||||
- Joining with `-` isn't one to one. Business `a`, agent `b-c` and business `a-b`, agent `c` give the same tag
|
||||
on one host.
|
||||
- Step 3 removes an existing line under the tag first, so the second grant deletes the first one's live line.
|
||||
- `sweep` removes every `mosaic-` line whose tag has no live grant. One business's sweep deletes another
|
||||
business's live lines on a shared user. The same goes for another stack's lines, if ops-01's `from` file lists
|
||||
more than one address.
|
||||
|
||||
Fix:
|
||||
- Make the tag one to one and bounded. One way is `mosaic-` plus a fixed-length hash of
|
||||
(business, agent, hostId), stored in the grant entry.
|
||||
- Scope `list` and `sweep` to tags this business recorded, and never act on the bare prefix.
|
||||
- Add a G-test with two businesses on one user, and a sweep that leaves the other's line.
|
||||
|
||||
B5. The part of `mosaic-keys` that holds the privilege is outside the design and outside review.
|
||||
- The forced command runs as the install user `I`. To write another user's `authorized_keys` it calls a root
|
||||
helper, `mosaic-keys-write`, which "is ops-01's choice".
|
||||
- The rules E3 lists only hold where the privilege is: the user list, the prefix rule, `from=` from the host file
|
||||
and the tag pattern. If the reviewed front checks them and an unreviewed helper writes, the helper's sudoers
|
||||
argv is the real boundary, and G16 tests the wrong layer.
|
||||
- The helper also writes, as root, into a directory the target user owns (`~/.ssh`), with tmp and rename,
|
||||
"keeping owner and mode". Without stated care, that is the classic symlink and hard-link race. The target
|
||||
user's own processes can redirect a root write.
|
||||
- `AuthorizedKeysFile` may not be `~/.ssh/authorized_keys` on every host.
|
||||
|
||||
Fix:
|
||||
- Put the root part in `tools/host` too, reviewed, with the rules enforced there. Refuse the install user and
|
||||
root by name in code, not only by the list.
|
||||
- State the write: no symlinks followed (`O_NOFOLLOW`, check owner and mode of the directory and file), and the
|
||||
tmp file made with `O_EXCL` in the same directory. Or have ops-01 use a root-owned
|
||||
`AuthorizedKeysFile /etc/ssh/authorized_keys/%u`, which removes the user-owned path altogether.
|
||||
- Q-O1 then asks ops-01 to accept a stated mechanism, not an effect.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The executor can't call `#consumeAuthority`. Through `#checkAuthority` it needs an agent session (`#agent(s)`,
|
||||
`broker.mjs:764`) whose role is the raiser (`:786`), and the resolve session is the human's. X1 says "the same
|
||||
`action.allowed` event", which is fine. Name the new consume path (`#spent` and `#raised`, `:801-818`), and the
|
||||
session its events carry after the resolve has returned.
|
||||
|
||||
N2. The raise body keeps `commandsSha256` and not the commands. Say where the executor reads the decided list,
|
||||
for example the full list in the raise body, hashed again at step 1. The rendered block counts against the
|
||||
4096-character question cap (`broker.mjs:126`). Name the refusal when the chief's text plus the block exceeds it.
|
||||
|
||||
N3. `hosts remove` refuses while a grant names the host. It should also refuse while a `hostLineLeft` names it,
|
||||
or the installer key is gone before `sweep` can clear the line.
|
||||
|
||||
N4. On Q-S1, starting without `grant.ask` is fine with me. The reviewed definition list gates who can be
|
||||
granted, and Jason sees the rendered block either way.
|
||||
|
||||
The next round is a full read. The blockers touch sections 3, 5 and 6 and the test list.
|
||||
|
||||
## Correction (after d025ef492)
|
||||
|
||||
B2 first said 210 has no busy map. That was wrong. `#inflight` is the busy map (`credentials.mjs:332-334`,
|
||||
`host-run.mjs:249`). The blocker holds: the design must say that revoke aborts the executor's entry, and that the
|
||||
executor checks again right before `issue`.
|
||||
@@ -0,0 +1,151 @@
|
||||
# Review: Gizmoduck machine-grant design r3 and r4 (Vikunja #436, Q188) (duckworth-0060)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per gizmoduck-0068 and gizmoduck-0074:
|
||||
- 1c5705d6d (r4) on `gizmoduck/machine-grant-design`, `agents/gizmoduck/work/machine-grant/DESIGN.md`,
|
||||
791 lines, sha256 99ee35ee…9839 (checked).
|
||||
- r4 is one commit on r3 d8bcf8066 (776 lines, sha256 6d4fbccb…886b). I read r2 in full (705 lines), the
|
||||
whole r2..r3 diff (+188 -117), the r3 sections that diff leaves as they were (E2, X1, X2 and its claim,
|
||||
X5 and section 6), and the whole r3..r4 diff (+20 -5). Line cites below are r4's.
|
||||
- r4 changes only S1's migration lines and the Q-S4 target line (mos-1133). I have nothing against them:
|
||||
a Q173 agent-run `--migrate` window with a backup first and a STATE report after, and .53 and .54
|
||||
starting at 3j if row a lands before the 10-26 installs. I haven't read mos-1133 itself and take it
|
||||
from the r4 changes section.
|
||||
|
||||
I drafted a review of r2 that I never sent. gizmoduck-0068 replaced r2 with r3 before I sent it. This note
|
||||
replaces that draft. Each of its findings was checked again against r3 and r4, and is carried over,
|
||||
changed or dropped below.
|
||||
|
||||
I checked these again:
|
||||
- 210's `credentials.mjs:243-298` (`issue`, `revoke`), `:332-360` (`busy`, `use`) and
|
||||
`credential-verbs.mjs:1-60`
|
||||
- `bus/src/store.mjs:1`. The store is `node:sqlite` `DatabaseSync`, so a store write is synchronous.
|
||||
- `vocabulary.mjs:127` at f41729178, which is `:143` at 455964f23
|
||||
- `instance-settings.mjs`, `vocabulary.mjs` and `broker.mjs` at f41729178, searched for a dev-instance
|
||||
marker. There isn't one.
|
||||
|
||||
## Verdict: changes, B1-B2
|
||||
|
||||
The r1 blockers are still met:
|
||||
- B1 by X4 and G22. r3 needs less here: `host.run` reads the store records at each call, and only
|
||||
`register` and `unregister` stay in `Credentials`.
|
||||
- B2 by X5 and G23
|
||||
- B3 by X3's restart table and G11
|
||||
- B4 by X2 step 2, the sweep and G24
|
||||
- B5 by E3 and G25
|
||||
|
||||
What r3 adds holds up:
|
||||
- S1 and X4 carry out mos-1131. Only the executor, revoke, `hosts.check` and `hosts.remove` write the
|
||||
records. After row a, `host.run` uses a grant only when its decision was spent by `#consumeGrant`, and
|
||||
the loader refuses `hosts` in the business file. That settles my r2 draft's B3 (W1's hand edit). The
|
||||
draft is dropped.
|
||||
- E1's keyscan carries out mos-1132. Two independent reads have to agree, and the stack, not a message,
|
||||
supplies the key lines that get pinned. A mismatch stores nothing (G17).
|
||||
- E3 has ops-01's points a to d: the user pattern before any path use, `.lock`, `SSH_CONNECTION` logged
|
||||
before sudo, and the `use_pty` check in the #433 proof. Section 9 adds the "rights gained later" limit.
|
||||
- Section 6 follows mos-1124 d and PRD r3's REQ-GRANT-4. G26 covers each revoker.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. An enrollment still has no recheck at the time it runs (E2, unchanged in r3).
|
||||
- E1's raise refuses a `hostId` that is already enrolled. E2 step 1 then writes the `hosts` record. "The
|
||||
same start rules as a grant" (X1) are the start conditions only. They aren't X2 step 1's recheck, and
|
||||
E2 calls no `#consumeGrant`.
|
||||
- So two enrollment raises for one `hostId`, both approved, can both run. Section 3 itself describes two
|
||||
hosts at one address raised close together.
|
||||
- The second run replaces the host record and the installer key. The pin, the user or the sudo mark then
|
||||
changes under grants that name the host. That gets around `hosts.remove`'s `host-in-use` refusal and
|
||||
section 11's "remove and enroll again".
|
||||
- S1 gives no key for the `hosts` table, so an insert can't be relied on to refuse. r4 changes none of this.
|
||||
- Fix:
|
||||
- E2 step 1 becomes one transaction: `#consumeGrant`, then a check that the `hostId` is still not
|
||||
enrolled, then the insert. Otherwise refuse `host-enrolled` and write nothing.
|
||||
- Give `hosts` a primary key of (business, host id) in `3i-3j.sql` so the store refuses a second insert
|
||||
as well. With that and the single transaction, enrollment needs no `#inflight` claim.
|
||||
- Add the case to G17, with a mutant that drops the check.
|
||||
|
||||
B2. An approved grant can still end with nothing recorded and nothing run (X2 and X5, unchanged in r3).
|
||||
- The resolve answers `{grant: "started"}` (X1). X2 claims "before step 1" (:434), so before the decision
|
||||
is spent, and a claim that finds an entry refuses `host-busy`.
|
||||
- For a commands change, that entry is often a running `host.run` from the same agent. Then Jason's
|
||||
Approve gets no `grant.failed`, no message and no `grant.step`, though his inbox said "started".
|
||||
- The same thing happens if the bus child dies between the resolve commit and the claim. X3's reconcile
|
||||
only looks at spent decisions.
|
||||
- X5's first bullet (:530, "a revoke before the executor starts writes `grant.revoked`") can't happen for a
|
||||
first grant. With no reference and no claim, revoke refuses `credential-unknown`, as G23's last sentence
|
||||
and X5's second bullet both say.
|
||||
- Fix, one of:
|
||||
- Consume first and claim second. A busy claim ends `grant.failed host-busy` with a message, and the
|
||||
chief raises again.
|
||||
- Or wait for the running call to end. 210 bounds that by its timeout. Then claim, and say how long the
|
||||
wait can be.
|
||||
- Either way:
|
||||
- At start, a resolved-yes grant decision with no `action.allowed` ends `grant.failed interrupted` with
|
||||
a message. It never runs late.
|
||||
- Reword X5's first bullet to say what the code does. One way is to have the revoke verb record
|
||||
`grant.revoked` for the (agent, host) even when `Credentials` has no reference, so step 1's "no revoke
|
||||
newer than the raise" check has something to find.
|
||||
- Add both cases to G11 and G23.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. X2 step 2 (:443) cites `ID_PATTERN` at `vocabulary.mjs:143`. That line number is 210's (455964f23). At
|
||||
f41729178, the base the header names, it's `:127`. Mark it `210:`. (Carried from the r2 draft.)
|
||||
|
||||
N2. X5's answer to B2 depends on `issue` (210 `credentials.mjs:243-280`) and the `commit` it calls staying
|
||||
synchronous. Under S1 the commit is a store write, and the store is `DatabaseSync` (`store.mjs:1`), so it
|
||||
is synchronous today. Write that down as a build rule in X5, and add a mutant that makes `commit` async
|
||||
next to the "add an `await` between the abort check and `issue`" mutant.
|
||||
|
||||
N3. The `hosts revoke` verb must not go through 210's `credential-verbs.mjs:21-24`. That path refuses
|
||||
`credential-unknown` when the business file has no reference, and after row a no `host.<id>` reference is
|
||||
in the business file at all. Name the verb's path. (Carried, and more needed now that S1 moves the
|
||||
reference.)
|
||||
|
||||
N4. A revoke has no step marker.
|
||||
- A restart after step 1 and before step 2 leaves the key file gone and the `host_grants` record in
|
||||
place. The restarted bus registers a reference for it (X4) and `status` shows a grant.
|
||||
- A restart after step 2 and before step 3 leaves the line on the host with no event naming the tag, so
|
||||
`status` shows nothing to sweep.
|
||||
- Write `grant.revoked {tag, hostLine: 'pending'}` before step 1, delete the record in that same
|
||||
transaction or reconcile it at start, and close the marker when `remove` succeeds or turn it into
|
||||
`hostLineLeft`.
|
||||
|
||||
N5. The writer's refused groups (E3) still miss root-equivalent groups: `disk` (raw block devices), and
|
||||
`libvirt` or `incus-admin` where they exist. The `users` list is the main bound. The group check is there
|
||||
to catch a user ops-01 lists by mistake, so it should be complete. Add them or say why not.
|
||||
|
||||
N6. Writer details. r3 settles the lock name with `.lock`. Two points are left:
|
||||
- The first `add` for a user has no file yet. Allow `ENOENT` there and create the file by tmp and
|
||||
`renameat`.
|
||||
- In a root-only dir, a leftover tmp can only come from a writer that crashed. Under the `flock`, unlink
|
||||
it instead of refusing every later request. G25 should still refuse a tmp that is a symlink or not a
|
||||
regular file.
|
||||
|
||||
N7. State the installer's own line exactly, for example
|
||||
`command="/usr/local/sbin/mosaic-keys",restrict,from="<stack>" ssh-ed25519 ... mosaic-installer-<tag>`.
|
||||
Put it in the run sheet too, so ops-01's hand-written line can't drop `restrict` or `command=`.
|
||||
|
||||
N8. "A dev instance" (section 6 :593, G26) has no source. Nothing in `instance-settings.mjs`, `vocabulary.mjs`
|
||||
or `broker.mjs` marks an instance as dev. Name the field, who sets it, and that a missing field counts as
|
||||
not dev, so `tester-dev-only` fails closed.
|
||||
|
||||
N9. At row a's landing the loader refuses a business file with `hosts` (S1, G14). A business still
|
||||
carrying row 210's hand-edited test grants then fails to load. That's fail closed, which is right. The
|
||||
row a run sheet should still remove those keys before the `--migrate` re-pin, and say that any 210 grant
|
||||
in use stops there and needs an Approve.
|
||||
|
||||
N10. Small consistency points:
|
||||
- The r3 changes list (:25-26) names the record writers as "the executor, revoke and `hosts.remove`".
|
||||
S1 (:556) adds `hosts.check`, which sets `ready`. Make them match.
|
||||
- E1 (:175) reads every `/etc/ssh/ssh_host_*_key.pub`, and the keyscan asks only for `ed25519,ecdsa,rsa`. A host
|
||||
with another key type would always refuse `host-key-mismatch`. That fails closed, but ops-01 should
|
||||
send only those three types.
|
||||
|
||||
N11. My PRD 1.8 review duckworth-0058 N2 said a revoke never rewrites the user's file. I withdraw it (PRD
|
||||
REVIEW-r3, duckworth-0059). Section 6 step 2 deleting the record is right, since a restart can't bring the
|
||||
grant back.
|
||||
|
||||
Dropped from the r2 draft: its B3 (met by S1 and X4) and its N8 on Q-S5 (settled by mos-1124 d, and
|
||||
section 6 follows it).
|
||||
|
||||
The next round gets a diff check if it changes only the B1 and B2 lines, the notes and their tests.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Diff check: Gizmoduck machine-grant design r5, for duckworth-0060 (duckworth-0063)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per gizmoduck-0075:
|
||||
- 7686fd783 on `gizmoduck/machine-grant-design`, one commit on r4 1c5705d6d, one file,
|
||||
`agents/gizmoduck/work/machine-grant/DESIGN.md`, 897 lines, sha256 bb5f8159…6b60 (checked). The diff is
|
||||
+149 -43, and I read all of it.
|
||||
- Cites checked at f41729178: `vars.mjs:125` (`bus.tester`, business layer, default `false`) and
|
||||
`broker.mjs:544` (`bindTester` refuses `tester-off` unless the var is exactly `true`). Both hold.
|
||||
|
||||
## Verdict: approve, with C1
|
||||
|
||||
B1 is met (E2 step 1, S1, G17, two mutants):
|
||||
- One transaction runs `#consumeGrant`, the "not enrolled" check, and the step marker and insert.
|
||||
- A second enrollment spends its decision and ends `grant.failed host-enrolled` with a message, and
|
||||
nothing else is written.
|
||||
- `hosts` has the primary key (business, host id), and `host_grants` has (business, agent, host id).
|
||||
- Dropping the claim for enrollment is right, because the transaction stops a second run before
|
||||
anything is written.
|
||||
|
||||
B2 is met (X2 step 1, X3, X5, G11, G23):
|
||||
- The decision is consumed first and claimed right after the commit. `DatabaseSync` keeps the two in one
|
||||
turn.
|
||||
- A busy claim ends `grant.failed host-busy` with the decision spent and a message. Failing instead of
|
||||
waiting is a fair choice, and X2 gives the reason.
|
||||
- At start, a decision resolved yes with no `action.allowed` is spent in the same transaction as
|
||||
`grant.failed interrupted`. That applies to enrollments too.
|
||||
- `hosts.revoke` records `grant.revoked` with no reference and no claim, so X5's first bullet now
|
||||
matches the code. `abortClaim` avoids 210's `credential-unknown`.
|
||||
|
||||
The notes are taken:
|
||||
- N1: the `210:` cite
|
||||
- N2: the build rule in X5 and a mutant
|
||||
- N3: `hosts.revoke` lives in `host-grants.mjs` (G27)
|
||||
- N4: the store transaction and marker come first, then `revoke` or `abortClaim` with no await, then
|
||||
`remove` and `grant.revoke.closed`, with the reconcile (G12, G27)
|
||||
- N5: the groups added, and a group the host doesn't have counts as no membership
|
||||
- N6: `ENOENT`, and `.<user>.tmp` unlinked only if regular (G25)
|
||||
- N7: the exact installer line
|
||||
- N8: `bus.tester` exactly `true` per mos-1139, with missing meaning not dev (G26)
|
||||
- N9: the run-sheet bullet
|
||||
- N10: the writer list and the three key types
|
||||
|
||||
`remove` of an absent tag exiting 0 is a sensible addition. Revoke and an executor undo both need it.
|
||||
|
||||
## Condition
|
||||
|
||||
C1. A revoke must not be undone by a commands-change undo.
|
||||
- r5's revoke order deletes the `host_grants` record in step 1.
|
||||
- Two existing paths set the record "back to `previous`": X3's failure row "5 after `commit`" (:541) and
|
||||
the restart reconcile row "`commit`, commands change" (:557).
|
||||
- A commands change that commits at step 5, and is then revoked before `grant.applied`, can hit either
|
||||
path:
|
||||
- the executor sees the abort and undoes by X3, or
|
||||
- the bus restarts and reconciles.
|
||||
- If either path writes the record back, by an insert or upsert, the revoked grant comes back with its
|
||||
old commands. The key file is gone, but the next restart registers a reference for the record (X4).
|
||||
- Say:
|
||||
- After step 5 commits, the executor ignores the abort. :613 "A revoke after step 5 is an ordinary
|
||||
revoke" implies this. State it as a rule.
|
||||
- Both "back to `previous`" paths only update an existing record, and never insert one. A `grant.revoked`
|
||||
newer than the decision makes them a no-op.
|
||||
- Add a G-test: a commands change, a revoke after its commit, then a restart before `grant.applied`. It
|
||||
ends with no record. Add a mutant that makes the reconcile an upsert.
|
||||
|
||||
C1 gets a diff check on those lines when row b's BUILD.md is written, or in an r6 if Gizmoduck prefers.
|
||||
The design is approved for code once row 210 lands (sage-1527).
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Section 6 step 2: if the unlink fails, "the start reconcile unlinks the file". The reconcile acts on
|
||||
an open marker, and step 3 closes the marker whether or not the unlink worked. Either keep the marker
|
||||
open until the unlink is done, or have the start sweep unlink any key file with no `host_grants` record.
|
||||
The file is unused either way, because no reference loads without a record. It's only clean-up.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Diff check: Gizmoduck machine-grant design r6, for duckworth-0063 C1 (duckworth-0064)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per gizmoduck-0078:
|
||||
- f9039682e on `gizmoduck/machine-grant-design`, one commit on r5 7686fd783, one file,
|
||||
`agents/gizmoduck/work/machine-grant/DESIGN.md`, 926 lines, sha256 56b01fab…5d39 (checked). The diff is
|
||||
+36 -7, and I read all of it.
|
||||
|
||||
## Verdict: approve, C1 met
|
||||
|
||||
C1 from duckworth-0063 is met:
|
||||
- X5 (:631-632): once step 5 commits, the executor ignores an abort and goes on to step 6. Its undo can't
|
||||
run against a revoke that came after the commit.
|
||||
- X3 (:556-559): a set-back to `previous` updates only an existing record, never inserts one, and does
|
||||
nothing when a `grant.revoked` for the (agent, host) is newer than the decision. The restart table's
|
||||
"`commit`, commands change" row (:575) points to that rule. The rule covers both paths I named, :554
|
||||
and :575.
|
||||
- G28 (:836) covers the restart case and the failure-after-step-5 case. Each ends with no record, no
|
||||
reference and no key file.
|
||||
- The four mutants (:879-882) are the upsert, the undo past a newer revoke, acting on a late abort, and
|
||||
skipping the sweep.
|
||||
|
||||
N1 from duckworth-0063 is taken (section 6 :724-727, G27 :835):
|
||||
- At start, after the grant reconcile and before `Credentials` loads, the sweep unlinks each `host.<id>`
|
||||
key file in the agent store that has no `host_grants` record.
|
||||
- Running it after the reconcile is the right order. An interrupted executor's key is settled by X3 first.
|
||||
- The sweep is scoped to `secrets/agents/<business>/<agent>/host.<id>` (F7 :162). The installer key lives
|
||||
under `secrets/hosts/<business>/<hostId>/installer` (E2 step 3, :268), so the sweep can't touch it.
|
||||
- The sweep is in row b, after row a (:905-908). Any key file still left from a row 210 hand grant then
|
||||
has no record and gets swept. That's the intended end of those grants (mos-1129).
|
||||
|
||||
No new findings. The design is approved for code once row 210 lands (sage-1527). Further rounds that touch
|
||||
only the build's numbering and mutant list need no check from me. Row b's BUILD.md is reviewed with its
|
||||
candidate.
|
||||
@@ -0,0 +1,94 @@
|
||||
# Review: Honker PRD 1.8 draft (Vikunja #434, Q188) (duckworth-0057)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per sage-1529:
|
||||
- 3c30f1036 on `honker/row194-b2`, the only file `agents/honker/work/queue-194/PRD-1.8-DRAFT.md`, 228 lines,
|
||||
sha256 e7d33708…904f (checked). I read all of it.
|
||||
- Compared against: PRD 1.6 at f41729178 (REQ-WAIT-1 to -3, :345-372), the 1.7 draft r2 at 9ba31b5b3, row 210
|
||||
DESIGN section 5 on `gizmoduck/row210-on-199-r2` a2fd30390 (:376-396), and Gizmoduck's machine-grant draft
|
||||
f6b6a04b6 with my review of it (duckworth-0056).
|
||||
- Code read at f41729178: `broker.mjs:1677-1678` (a cited gated raise goes to the arbiter unless it is a hard
|
||||
wait), `roles/pm.md:55` and `roles/chief-of-staff.md:75` (both list `credential.mint` as a hard wait they
|
||||
raise). At 455964f23: `broker.mjs:1731-1735` (`host.run` checks the grant and the allowlist before any decision).
|
||||
|
||||
## Sage's two checks
|
||||
|
||||
REQ-CRED-4 keeps every row 210 section 5 condition:
|
||||
- it is routed, and runs without a decision only when the definition lists it within-role
|
||||
- the definition declares the credential kind
|
||||
- a grant names the agent and host. That was "the business file grants" in 210. B2 covers the change.
|
||||
- the command matches the argv allowlist, with exact prefixes and a wildcard refused
|
||||
- the key is issued. The draft adds "and not revoked", which is right.
|
||||
- a role without it within-role still needs a decision for each use
|
||||
- only a reviewed role change adds it to a definition
|
||||
|
||||
It adds "a command off the allowlist is refused". That matches `broker.mjs:1733-1735` (210), which refuses before
|
||||
any decision. Nothing from section 5 is lost.
|
||||
|
||||
Is "a grant request is a `credential.mint` decision" enough binding for the machine-grant design? With B1 and
|
||||
B2 fixed, yes. The binding sits in REQ-GRANT-1 and -2's fields, not in the action name:
|
||||
- the agent, method, target, rights and date are named
|
||||
- the approval binds what the user saw, and a change voids it
|
||||
- it's used once, with no automatic retry
|
||||
- a resolve by an arbiter, the tester or an agent makes nothing
|
||||
|
||||
Gizmoduck's draft uses a new action `host.grant` (its R2). Under this text it uses `credential.mint`, with the
|
||||
method in the grant body. That's a change for their r2, not here. Its `registrySha256` is stricter than the PRD
|
||||
(N1), and that's allowed.
|
||||
|
||||
## Verdict: changes, B1-B2
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. REQ-GRANT-2's failure clause can't be met when the host or service is unreachable. "A grant the code
|
||||
couldn't finish undoes what it made" can't hold for a key line already installed on a host that then drops off
|
||||
the network. Gizmoduck's draft leaves it as `hostLineLeft` until a sweep (its X3), and REQ-GRANT-4 already
|
||||
allows that for revoke ("Until that removal is done it stays visible as pending"). Say the same here: the code
|
||||
undoes what it made on the stack's side at once, and what it couldn't remove at the host or service stays
|
||||
visible as pending until removed.
|
||||
|
||||
B2. "Approve is the only thing that creates a grant" (REQ-GRANT-2) conflicts with the hand-edited grants row 210
|
||||
lands with (its section 4) and with Gizmoduck's W1 ("A hand edit of the business file still works"). Read
|
||||
plainly, the user's own edit of the business file breaks the requirement, and 1.8 doesn't say whether the code
|
||||
must refuse such grants. Pick one and state it:
|
||||
- "No agent creates a grant. The user creates one by Approve, or by editing their own business file." The
|
||||
hand-edit path then stays as row 210 ships it.
|
||||
- Or say grants that exist when 1.8 lands stay, as REQ-CRED-1's new line says for tokens, and the code then
|
||||
accepts a grant only with an Approve record. That is a bigger change for row 210 and #436.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. "A change to the agent, its definition, the target or the rights after the raise voids it." A host re-pinned
|
||||
between raise and Approve keeps its id, so "the target" may read as unchanged while the address or host keys
|
||||
Jason saw have changed. Say "the target or its enrolled identity" so the binding covers what Gizmoduck's
|
||||
`registrySha256` covers.
|
||||
|
||||
N2. REQ-GRANT-4 says the next use is refused. Add that a revoke while a grant is being made stops it, and nothing
|
||||
it made stays usable. That's my B2 on Gizmoduck's draft, and it is how Q188's "instant" reads.
|
||||
|
||||
N3. `credential.mint` is raisable today by any role, and `roles/pm.md:55` and `roles/chief-of-staff.md:75` say so.
|
||||
Under REQ-GRANT-1 only the chief raises it. "When this version lands" should list the role contract updates. It
|
||||
should also say that a `credential.mint` decision raised before 1.8, or without the grant fields, makes nothing
|
||||
when approved.
|
||||
|
||||
N4. "A request that makes an account is also `account.create`." A decision has one action. Say how one request
|
||||
is both, for example the method implies `account.create`, or two decisions. This only matters for the Gitea
|
||||
and GitHub methods.
|
||||
|
||||
N5. Once `host.sudo` leaves the hard waits, a raise from a role that doesn't list it within-role routes by
|
||||
REQ-DEC-2. A cited gated raise then goes to an arbiter (`broker.mjs:1677-1678`), not the user. The grant and the
|
||||
allowlist still bound it, because `host.run` refuses before any decision (210 `:1733-1735`). It's the same
|
||||
pattern 1.7 set for `git.push.remote`. It is still a change in who decides a root command on another host, so
|
||||
the notes for Mos should name it.
|
||||
|
||||
N6. The chain of command (worker through its PM, PM and CTO direct) has no data in the code (Gizmoduck F1). The
|
||||
broker can enforce only "the chief raises". Say in the notes that the chain beyond that is messaging lists and
|
||||
role contracts, not a broker check.
|
||||
|
||||
Honker's flagged choices are noted and are Mos's to rule on:
|
||||
- manual mode as a list in the organization file
|
||||
- REQ-CRED-5 folded into REQ-GRANT-1 to -4
|
||||
- the chain of command and who may ask
|
||||
- "the assistant" for revoke
|
||||
- the Gitea unit, with GitHub left to a later design
|
||||
|
||||
A round that changes only B1's and B2's lines and the notes gets a diff check.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Review: Honker PRD 1.8 draft r2 (Vikunja #434, Q188) (duckworth-0058)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per honker-0032:
|
||||
- 60f5ca8fa on `honker/row194-b2`, one file, `agents/honker/work/queue-194/PRD-1.8-DRAFT.md`, 276 lines,
|
||||
sha256 c3923e07…1e81 (checked). Diff 3c30f1036..60f5ca8fa, +99 -51.
|
||||
- r2 carries Mos's edits a to f (mos-1124) as well as my items, so I read the whole file again, not only
|
||||
the diff.
|
||||
- Cites checked on origin/refactor f41729178: `broker.mjs:1674-1680`, `roles/pm.md:55`,
|
||||
`roles/chief-of-staff.md:75`, `vocabulary.mjs:38`, `:46`, PRD :354. Row 210 DESIGN section 5 at a2fd30390
|
||||
(:376-392 holds the proposed text).
|
||||
|
||||
## Verdict: approve, with C1
|
||||
|
||||
My r1 items:
|
||||
- B1 is met (:167-169). The stack side is undone at once, and what's left at the host or service stays
|
||||
visible as pending, as under GRANT-4.
|
||||
- B2 is met (:151-153, :188-190). The user creates a grant by Approve or by writing it in their own
|
||||
business file. The note at :228-232 gives the reason, and it holds: with the other choice, nothing could
|
||||
make a new grant between row 210 landing and #435/#436 landing.
|
||||
- N1 (:165-166), N2 (:183-184), N3 (:63-66, :171-172), N4 (:144-148, :233-235) and N6 (:148-150,
|
||||
:240-242) are taken. N5 is put to Mos (:236-239), which is right.
|
||||
|
||||
Mos's edits a, d, e and f read as mos-1124 describes them. The paragraph for Jason (:13-16) states the
|
||||
chief-only reading as Mos's and says how Jason changes it. I have nothing against them.
|
||||
|
||||
## Condition
|
||||
|
||||
C1. `account.create` covers more than grants. Today it is the hard wait for any new external account
|
||||
(PRD :354, `vocabulary.mjs:43-46`), and `roles/pm.md:55` and `roles/chief-of-staff.md:75` both raise it.
|
||||
r2 now says two things about it without saying they apply only to grant requests:
|
||||
- landing step 2 (:54-56): "The broker refuses the raise from any other role", for `credential.mint` or
|
||||
`account.create`
|
||||
- REQ-GRANT-2 (:171-172): an `account.create` decision without the REQ-GRANT-1 fields makes nothing
|
||||
|
||||
Read plainly, a PM can no longer raise `account.create` for an account that isn't a grant, such as a
|
||||
service sign-up, and if the chief does, Approve makes nothing. That is a broker change the notes don't
|
||||
name, and step 6 still has the PM keep `account.create` in its hard-waits line.
|
||||
|
||||
Fix, one of:
|
||||
- Scope both lines to grant requests: the broker refuses an `account.create` raise that carries a grant
|
||||
method from any role but the chief, and other `account.create` raises stay as 1.7 has them.
|
||||
- Or say that every `account.create` is now a grant request raised by the chief, and add that to the
|
||||
notes for Mos and to step 6's role contract update.
|
||||
|
||||
`credential.mint` doesn't need this. Every credential is a grant under 1.8, so chief-only for it is the
|
||||
intent.
|
||||
|
||||
C1 gets a diff check on those lines before Sage opens the `prd.approve` decision.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. A grant the user writes in their own business file isn't a request, so REQ-GRANT-1's bounds (a
|
||||
rotate-by or expiry date, no shell, no empty command) don't reach it as written. REQ-CRED-4 refuses a
|
||||
wildcard either way. Say whether the business file loader enforces the same bounds on a hand-written grant.
|
||||
I'd say it should, since the same REQ-CRED-4 path runs it.
|
||||
|
||||
N2. The revoke paths (:179-180) include grants the user wrote by hand. The assistant's and the tester's
|
||||
revoke then changes what the user wrote in their business file. That's fine, since revoke only narrows.
|
||||
The grant code's design (#436) should say that revoke marks the grant as revoked and doesn't rewrite the
|
||||
user's file.
|
||||
|
||||
A round that changes only C1's lines and these notes gets a diff check.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Diff check: Honker PRD 1.8 draft r3, for duckworth-0058 C1 and mos-1129 (duckworth-0059)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per honker-0034:
|
||||
- d16ccdb91 on `honker/row194-b2`, one commit on r2 60f5ca8fa, one file, `PRD-1.8-DRAFT.md`, 300 lines,
|
||||
sha256 2fdc13a6…7d23 (checked). The diff is +30 -6 in five hunks, as the header says. I read all of it.
|
||||
|
||||
sage-1541 relays Mos's ruling mos-1129, made after r2 went up. B2 takes my second option:
|
||||
- only an Approve creates a grant
|
||||
- a hand-written grant in the business file makes nothing
|
||||
- row 210's hand-edited grants stay a test path until #436a
|
||||
|
||||
I haven't read mos-1129 itself. This check takes those three points from sage-1541.
|
||||
|
||||
## Verdict: changes, B1
|
||||
|
||||
What r3 gets right:
|
||||
- C1 is met. Landing step 2 (:58-60) and REQ-GRANT-2 (:176-179) apply the chief-only raise and "makes
|
||||
nothing" only to `credential.mint` and to an `account.create` that carries a grant method. A PM's
|
||||
sign-up for a service routes as in 1.7. That matches Gizmoduck's machine-grant r2 R2, where a
|
||||
`credential.mint` with no `grant` keeps today's behaviour.
|
||||
- The enrollment wording (:162-163) matches the machine-grant r2 E1 and mos-1120 and mos-1125.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. r3 still carries the B2 reading Mos didn't choose. These lines say a hand-written grant is allowed:
|
||||
- REQ-GRANT-2 :155-157, "or by writing it in their own business file on the human's path, as row 210
|
||||
does"
|
||||
- REQ-GRANT-2 :179-180, a hand-written grant "is held to the same bounds when it is loaded"
|
||||
- REQ-CRED-1's appended line (:196-198), "A grant the user writes in their own business file, with its key made by
|
||||
hand, stays allowed"
|
||||
- the B2 note (:236-240) and the N1 and N2 notes (:245-248, :254-256)
|
||||
|
||||
Fix, under mos-1129:
|
||||
- Only an Approve creates a grant. A grant entry the user writes by hand makes nothing: the bus doesn't
|
||||
use it.
|
||||
- Say what happens to row 210's hand-edited grants. They are a test path until #436a lands. After that
|
||||
the loader refuses or ignores a grant with no Approve record, and the refusal is logged.
|
||||
- Drop the hand-written-grant bounds sentence and the N1 and N2 notes. Rewrite the B2 note to the
|
||||
ruling.
|
||||
|
||||
## My r2 notes under mos-1129
|
||||
|
||||
Sage asked whether anything is left of N1 and N2:
|
||||
- N1 goes away. A hand-written grant makes nothing, so no bounds need to reach it.
|
||||
- N2 goes too. I also withdraw it on its own terms. It said a revoke never rewrites the user's file. That
|
||||
was wrong: the machine-grant r2 section 6 step 2 drops the grant through the grant writer, and that's
|
||||
safer, because a restart can't bring the command list back.
|
||||
- One point is left. The text should say how the change from row 210's hand-edited grants to Approve-only
|
||||
happens, as in the second fix bullet. Without it, the row 210 grants that exist when #436a lands
|
||||
either keep working, against mos-1129, or stop with no record.
|
||||
|
||||
A round that changes only these lines gets a diff check.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Diff check: Honker PRD 1.8 draft r4, for mos-1129 and duckworth-0059 B1 (duckworth-0061)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per honker-0036:
|
||||
- 781d91624 on `honker/row194-b2`, one commit on r3 d16ccdb91, one file, `PRD-1.8-DRAFT.md`, 318 lines,
|
||||
sha256 a08ffc95…9833 (checked). The diff is +89 -71. I read all of it.
|
||||
|
||||
My r3 diff check (duckworth-0059, changes B1, committed at 9dceb2148) crossed with r4 and wasn't sent on
|
||||
its own. r4 answers its B1, so this note replaces it.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
B1 from duckworth-0059 is met:
|
||||
- REQ-GRANT-2 (:170-174): only an Approve creates a grant, the code uses a grant only with a matching
|
||||
Approve record, and a grant written by hand makes nothing. Row 210's hand grants stay a test path until
|
||||
#436a moves `host.run` to Approve-made grants.
|
||||
- The hand path is out: the bounds sentence on hand grants is gone from REQ-GRANT-2, REQ-CRED-1's append
|
||||
(:209-210) says "not by hand", and the B2 note (:250-258) states the ruling with Q188's reason.
|
||||
- My 0058 N1 and N2 notes are removed, as 0059 asked.
|
||||
- The top of the draft (:22-30) names the B7 residual and the `host.sudo` arbiter route for Jason. That
|
||||
matches the machine-grant design's section 9 and its S1 "same-user limit".
|
||||
|
||||
The other r4 changes read as mos-1129 rules:
|
||||
- N4 and C1 (:58-62, :69-73, :163-169): a grant that makes an account is one `credential.mint` decision
|
||||
that shows the account name. `account.create` stays as 1.7 has it, and only `credential.mint` is
|
||||
chief-only. That keeps C1's point: a PM's service sign-up routes as before.
|
||||
- N1 "the target or its enrolled identity" (:186), N2 (:204), N3 (:192-193), N5 landing step 5 (:78-80)
|
||||
and N6 (:84, :169) are in.
|
||||
|
||||
The transition after #436a (what the loader does with a leftover hand grant) is a design matter, and the
|
||||
machine-grant design r4 S1 covers it: the loader refuses `hosts` in the business file at load. "Makes
|
||||
nothing" in the PRD is enough as an outcome.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Two notes are stale. Neither is in the requirement text.
|
||||
- :246-247: "Duckworth hasn't reviewed their draft yet". I've reviewed the machine-grant design r1 to r4
|
||||
(duckworth-0056, -0060).
|
||||
- :263-267: "Their design gives revoke to the user and the tester only, and their Q-S5 asks Jason…". The
|
||||
design at r3 and r4 (1c5705d6d section 6) follows mos-1124 d, and its Q-S5 is answered. Point the
|
||||
bullet at 1c5705d6d and drop the Q-S5 clause.
|
||||
|
||||
Doc-only, so they can be fixed before the text goes into the PRD, without another check.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Review: Honker PRD 1.7 draft, REQ-CRED-3 host access through the credential store (duckworth-0038)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: agents/honker/work/queue-194/PRD-1.7-DRAFT.md at
|
||||
0bb1a3bd0 on honker/row194, sha256 0236dee8…40fd (checked), diff +32 -4 over 69544f342, per sage-1428
|
||||
item 2. I checked the quotes against DECISIONS.md:1533-1571 in jason-master-restart at 1c368ff (read only),
|
||||
and the cited requirements against docs/prd/mosaic-stack.md at 0bb1a3bd0.
|
||||
|
||||
## Verdict: changes, B1 and B2
|
||||
|
||||
What holds:
|
||||
- The Q183 quote (draft :47-53) matches DECISIONS.md:1537 word for word, from "I will still want" to "I am a
|
||||
happy man." It leaves out the first and last sentences, and "in part" says so.
|
||||
- The Q184 quote "Q5: by 10.27" matches :1558. The due date 2026-10-27 is Mos's reading at :1568, and the
|
||||
draft uses it only as the #424 due date.
|
||||
- "Not every agent gets a grant" and "An agent without a grant for that host is refused" rest on "not all
|
||||
agents will need to have that access" and "The system will enforce access to the credentials".
|
||||
- "The agent never sees the key or any other secret value" is already PRD law. REQ-CRED-1 keeps tokens out
|
||||
of an agent's environment and files, and REQ-VAR-2 lets only the broker read a secret's contents. Citing
|
||||
both is right.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. REQ-CRED-3 (draft :113-114) says the stack "then does the ssh and the sudo for the agent", and landing
|
||||
item 5 (:30) says "the stack's ssh and sudo for a granted agent". Jason didn't say that. He said the
|
||||
credential "works as some sort of variable for the agent to call" and that "the system will enforce access
|
||||
to the credentials". Running the ssh on the agent's behalf is one design. Lending the agent a signing socket
|
||||
that never exposes the key is another, and both meet REQ-CRED-1. Which one it is belongs to the #424 design
|
||||
(row 210, Gizmoduck), which I review. Fix: state the outcome and the enforcement only. For example: "The
|
||||
agent asks for the host credential by name. The stack checks that a grant exists for that agent and that
|
||||
host before the credential is used. The agent never sees the key or any other secret value." Change item 5
|
||||
to "adds the host credential grant check and the host access it allows (REQ-CRED-3, Vikunja #424)".
|
||||
|
||||
B2. The last sentence of REQ-CRED-3 (:116), "Jason grants one through a reviewed role or config change",
|
||||
answers the open point that the note at :151-153 says Q183 leaves open. The note says both "Q183 doesn't
|
||||
say who grants" and "The text leaves it as Jason through a reviewed role or config change", so the
|
||||
requirement invents the answer the note calls open. Fix: in the requirement, say it is open. For example:
|
||||
"Who grants a host credential, and how, is set by the #424 design, which Jason approves." Reword the note to
|
||||
match. If Sage wants a floor that already exists in the PRD, "a grant changes only through a reviewed
|
||||
commit (REQ-ROLE-1)" is sourced, but "Jason grants" is not.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Jason's words name "infra agent(s) on my .53 instance". REQ-CRED-3 is general, which is fine because
|
||||
the grant gates it. The source bullet carries the scope, so nothing is lost.
|
||||
N2. Jason's outcome is NOPASSWD sudo. REQ-CRED-3 says only "with sudo". That's fine if the design keeps
|
||||
sudo passwords out of the agent's hands too, which REQ-CRED-1 already requires of any secret. The design
|
||||
review checks it.
|
||||
N3. "Jason through Mos (mos-1067), DECISIONS Q183" (:47): Jason said it in Mos's thread 38bc4c81, and Mos
|
||||
recorded it. "Recorded by Mos" would be more exact than "through Mos". Doc-only.
|
||||
|
||||
A round with only B1 and B2 changed gets a diff check, not a full re-read. Sage's cutoff is 23:00Z.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Review: Honker PRD 1.7 draft r2, REQ-CRED-3, diff check (duckworth-0039)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: agents/honker/work/queue-194/PRD-1.7-DRAFT.md at
|
||||
11cb0423b on honker/row194, sha256 e896746d…8647 (checked), per honker-0019. 11cb0423b is a fast-forward
|
||||
of 0bb1a3bd0, and the diff touches that one file only, +12 -11. I read the diff only, as REVIEW-r1 said I
|
||||
would.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
- B1 resolved. REQ-CRED-3 states the check, "before the credential is used", and no longer says how the
|
||||
stack reaches the host. Item 5 reads "the host access it allows". The new note says the #424 design
|
||||
picks the path.
|
||||
- B2 resolved. The requirement now says who grants, and how, is set by the #424 design, which Jason
|
||||
approves. The open-point note matches it and no longer names a granter.
|
||||
- The build line (row 210, #1707, Gizmoduck, on top of row 199) is Sage's routing (sage-1430). It is a
|
||||
reviewer note, not PRD text.
|
||||
- Nothing outside B1, B2 and those notes changed. The quotes I checked in r1 are untouched.
|
||||
|
||||
N1 to N3 from r1 stay as notes.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Review: PRD 1.7 and 1.8 landing (duckworth-0075)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in sage-1636 and sage-1641. Subject:
|
||||
`honker/prd-17-18` 46c8adaeb, parent 011d41ba6, 4 files. Checked:
|
||||
- `docs/prd/mosaic-stack.md`: 1196 lines, sha256 a5bba06c…4fc6209f3.
|
||||
- Sources: 1.7 is `PRD-1.7-DRAFT.md` at 9ba31b5b3, sha256 e896746d…1d328647. 1.8 is r7 at 82df17347,
|
||||
sha256 de079281…727440cd.
|
||||
- refactor 816d48b03 hasn't touched the four paths since 011d41ba6.
|
||||
|
||||
## Verdict: approve, no change to what any requirement allows or forbids
|
||||
|
||||
1. Every word of each requirement matches its source. A script took each quoted block of both drafts,
|
||||
collapsed the whitespace and checked each item for one contiguous match in the landed file. All 42
|
||||
items match, including every sub-bullet, the CRED-1 and WAIT-3 appends and the risks bullet. The only
|
||||
two misses are the status sentences, where the drafts had a `<prd.approve decision id>` placeholder.
|
||||
I read the whole diff (8 hunks), and no added line comes from anywhere but the drafts or the five
|
||||
deviations below.
|
||||
2. The deviations are layout and approval record only:
|
||||
- The status field goes from 1.6 to 1.8. The 1.6 landing bumped this same field in place
|
||||
(63c82f1c4).
|
||||
- The 1.7 status cites decision d52febb5 (prd.approve, yes). DECISIONS:1611 confirms decision 21
|
||||
was resolved yes by jason at 2026-10-20T20:36:47Z on the 9ba31b5b3 file.
|
||||
- The 1.8 status says Jason approved it in T3 through Mos (Q192). Its source bullet quotes
|
||||
DECISIONS:1765 exactly, and the Q191 source bullet quotes :1745 ("Q13: that looks better")
|
||||
exactly.
|
||||
- COS-3's 1.7 block sits after ASSIST-3, the last item of the 1.2 list, so the 1.2 items keep
|
||||
their heading.
|
||||
- The CRED-1 append is a paragraph inside CRED-1 after its Vikunja sub-bullet. The two WAIT-3
|
||||
appends are paragraphs inside WAIT-3, the last item before `### Messages`.
|
||||
- TRUST sits after DEC-6, the last Decisions item. CRED-4 and GRANT-1 to 4 sit after CRED-3.
|
||||
LAND-2 sits after LAND-1. The risks bullet ends that list.
|
||||
3. The ten ids are COS-3, CRED-3, CRED-4, GRANT-1 to 4, TRUST-1, TRUST-2 and LAND-2. Each appears in
|
||||
the PRD as a `- **REQ-X-N.**` bullet and in `requirements.json` with the same id and in the same
|
||||
order. `tasks/tests/requirements.test.mjs` asserts the id list equals the PRD's in order, that no
|
||||
bold id is missed, the count of 68 and version 1.8, and that the status line names that version.
|
||||
4. The titles don't change what any requirement allows or forbids. COS-3, CRED-3, GRANT-1, GRANT-3,
|
||||
GRANT-4, TRUST-1, TRUST-2 and LAND-2 are fair summaries. GRANT-2 drops "and the target", but it
|
||||
leaves nothing allowed that the text forbids. On CRED-4 see N1.
|
||||
5. Pins. The tasks test moves 58 to 68, '1.6' to '1.8', and the `twice` index to [68]. Bus
|
||||
`requirements.test.mjs:48` moves 58 to 68. No other test pins the count or the version. No test or
|
||||
script pins a hash of the PRD or `requirements.json` (0 hits, control 11).
|
||||
`requirements.json` is in `scripts/stack-copy.sh:30` and `cli stack-copy.test.mjs:37` as a path
|
||||
only.
|
||||
|
||||
My runs, in a detached worktree of 46c8adaeb:
|
||||
- tasks: requirements 3/0, verbs 31/0, and the whole tasks package 97/0.
|
||||
- bus: requirements 3/0.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The CRED-4 title, "...only with its kind declared, a grant, an allowlisted command and a live key",
|
||||
is stricter than the requirement in two places:
|
||||
- The allowlist applies only "for a command", but the title reads as if every grant action needs an
|
||||
allowlisted command. A Gitea token use has none.
|
||||
- "live key" drops "or token".
|
||||
The PRD text governs, and a stricter label permits nothing extra, so this isn't an allow/forbid
|
||||
change. Agents do read the titles through the bus `requirements` verb, though. A possible wording:
|
||||
"A within-role grant action needs no decision only with its kind declared, a grant, a live key or
|
||||
token and, for a command, an allowlisted command". Sage's call.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Delta check: REQ-CRED-4 title (duckworth-0076)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in honker-0064 (sage-1644, my duckworth-0075 N1).
|
||||
Subject: `honker/prd-17-18` 46c8adaeb..c03d660e7. One line of `packages/tasks/requirements.json`
|
||||
changes, and nothing else.
|
||||
|
||||
## Result: approve, one note
|
||||
|
||||
The new title is "Within-role grant actions need no decision with the kind declared, a grant, a live key
|
||||
or token, any command allowlisted". It's 120 characters, exactly the schema's limit
|
||||
(`packages/tasks/src/requirements.mjs:33`).
|
||||
- "live key or token" now matches the text.
|
||||
- "any command allowlisted" applies only when there is a command, which also matches.
|
||||
- Nothing else in the file changed.
|
||||
|
||||
N1 (doesn't block). The old title said "only with". The new one drops "only", so it lists the
|
||||
conditions as enough to skip a decision but no longer says they're required. REQ-CRED-4 says "only when
|
||||
all of these hold". The title states nothing false and permits nothing the text forbids, and the PRD
|
||||
governs. Still, the one word that carries the requirement's restriction is gone.
|
||||
|
||||
This wording keeps "only" in 117 characters: "A within-role grant action needs no decision only if kind
|
||||
declared, grant, live key or token, any command allowlisted". Sage's call.
|
||||
|
||||
## Follow-up: 2884ee496 (duckworth-0077)
|
||||
|
||||
Asked in honker-0066 (sage-1648). 2884ee496 on c03d660e7 changes one line of
|
||||
`packages/tasks/requirements.json` and nothing else. The REQ-CRED-4 title is now my 117-character
|
||||
wording, byte for byte. That resolves N1. Approve, no notes.
|
||||
|
||||
My runs, in a detached worktree of 2884ee496: tasks requirements 3/0, bus requirements 3/0.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Scoped check: PRD 1.8 r7 with follow-up, 762a6711f..82df17347 (duckworth-0073)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in sage-1624, sage-1627, sage-1632 and sage-1633 (Jason
|
||||
Q191, mos-1149, mos-1152). Subject: `honker/prd18-r7` at 82df17347, two commits on r5 762a6711f: 47c2cb33c
|
||||
(r7) and 82df17347 (Mos's r6 rulings). One file, `agents/honker/work/queue-194/PRD-1.8-DRAFT.md`, 342
|
||||
lines, sha256 de079281…727440cd (checked), 8+/4- against 47c2cb33c (checked). This is the one verdict
|
||||
for both commits. It includes duckworth-0072 (`REVIEW-r7-scoped.md`, the r7 commit and the enforcement
|
||||
note at 011d41ba6).
|
||||
|
||||
## Result: passes the scope, no findings
|
||||
|
||||
r7 (47c2cb33c), from duckworth-0072:
|
||||
1. The only changes are the revision paragraph, the top note, one sentence added to REQ-GRANT-1, and the
|
||||
risks notes. No other requirement text changes.
|
||||
2. The top note and the REQ-GRANT-1 sentence match sage-1624 word for word.
|
||||
3. "one role under two names" is gone from the file.
|
||||
4. Every file:line in the enforcement note is true at refactor 011d41ba6 (list in `REVIEW-r7-scoped.md`).
|
||||
|
||||
Follow-up (82df17347 against 47c2cb33c). There are four hunks, all in scope:
|
||||
- `:18-20` revision clause: it names Mos's rulings on r6 items 1, 3 and 4 (mos-1152) and describes the
|
||||
three edits correctly. Item 2 isn't claimed.
|
||||
- `:25` (b): "Also for Jason at approval:" is now "For Jason at approval:".
|
||||
- `:27-28` (1): the first bullet under the heading is Mos's sentence, word for word as sage-1632 quotes it:
|
||||
"Q188's 'you, Mos or the tester' is read as the user, the assistant for grants in its own organization,
|
||||
and the tester only on a dev instance (Mos's reading, mos-1124 d)."
|
||||
- `:253` (3): the citation is now "design r1 to r4 (duckworth-0056 and duckworth-0060)". Both labels are
|
||||
right. 0056 is my machine-grant r1 review (f6b6a04b6). 0060 is my r3+r4 review (1c5705d6d), and my r2
|
||||
draft was dropped unsent, as SESSIONS records.
|
||||
- `:273` (4): ", and their Q-S5 is answered" is removed. The sentence now ends at "(mos-1124 d)." Nothing
|
||||
else in that bullet changed.
|
||||
|
||||
No other line changed, and the file no longer contains "Also for Jason" or "Q-S5".
|
||||
@@ -0,0 +1,46 @@
|
||||
# Scoped check: PRD 1.8 draft r7 (duckworth-0072)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in sage-1624 and sage-1627 (Jason Q191, mos-1149).
|
||||
Subject: `honker/prd18-r7` at 47c2cb33c, one commit on r5 762a6711f, one file:
|
||||
`agents/honker/work/queue-194/PRD-1.8-DRAFT.md`, 338 lines, sha256 a50ce730…b0551e (checked), 29+/10-.
|
||||
Sage's scope only. The dangling "Also for Jason at approval:" is known (sage-1627) and not reviewed
|
||||
here.
|
||||
|
||||
## Result: passes the scope, no findings
|
||||
|
||||
1. No other requirement text changed. The diff has four hunks:
|
||||
- the revision paragraph (`:16-18`, convention)
|
||||
- the top note (`:20-21`)
|
||||
- one added sentence at the end of REQ-GRANT-1 (`:170-171`)
|
||||
- the two "Risks and open questions" notes (`:293-314`)
|
||||
REQ-GRANT-2 onward and every other requirement are unchanged.
|
||||
2. The quoted lines are exact.
|
||||
- Top note: "Only the chief of staff raises a grant request. The assistant, the user's main agent,
|
||||
never raises one. It passes the user's request to the chief and can revoke a grant in its own
|
||||
organization."
|
||||
- REQ-GRANT-1: "The assistant and the chief of staff are separate roles. No agent holds both, and the
|
||||
broker refuses a claim that would."
|
||||
Both match sage-1624 word for word.
|
||||
3. "Which roles may request" no longer reads "one role under two names". That phrase appears nowhere in
|
||||
the file. The note now says the assistant is a separate role, passes the request to the chief
|
||||
(REQ-ASSIST-1) and can revoke in its own organization (REQ-GRANT-4).
|
||||
4. Every file:line in the enforcement note is true at refactor 011d41ba6:
|
||||
- `business/src/business.mjs:118-120`: the instance's single `definition` is required and loaded.
|
||||
- `business/src/vocabulary.mjs:134`: `THREAD_DEFINITION = "assistant"`.
|
||||
- `bus/src/broker.mjs:164` (`STAFF_TO = 'chief-of-staff'`) and `:2137` (the `no-chief-of-staff`
|
||||
check against `STAFF_TO`).
|
||||
- `broker.mjs:451-456`: `bindLaunch` takes one `role` (keys at `:454-455`). `:478` sets the run key
|
||||
as `business:run`. `:479` gives `duplicate-run`, and `:499` gives `launch-record-mismatch` on a
|
||||
rebind under another role.
|
||||
- `cli/src/launcher.mjs:438-449`: one `spawnSession` with `role: instance`, then one `bindLaunch`
|
||||
for that instance.
|
||||
- `broker.mjs:1689-1734`: `role.claim` writes only `s.role`. `:1693-1709` is the thread branch,
|
||||
which writes `thread_claims` and returns before `role_claims`. A threaded definition must bind a
|
||||
thread (`:474`, `thread-required`), so an assistant run always takes that branch.
|
||||
- `bus/src/migrations/3b.sql:117-119`: `role_one_holder`, unchanged in `schema.sql:117`.
|
||||
- `broker.mjs:34`: `credential.mint` is in ACTIONS (`:9-43`) and not in ROUTED (`:46-69`), so it's
|
||||
in HARD_WAITS (`:71`). There's no other use in bus or business src, so no check limits which role
|
||||
raises it, as the note says.
|
||||
|
||||
The "not enforced" claims also hold. The run key is per business, and nothing in the broker ties two
|
||||
runs to distinct processes or users beyond the launcher's record.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Review: ops-01 Q180 tightening plan r2 (duckworth-0023)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review of the document. I ran nothing on pve, PBS, .53 or
|
||||
.54. The PVE and PBS permission rules below come from ops-01's reading of the local schemas and source, and
|
||||
I didn't re-check them.
|
||||
Subject: /mnt/storage/scratch/tmp/mosvk/q180-tightening-plan.md, written 19:31:42Z, sha256 1084aa15…585f
|
||||
(checked), Vikunja #421. Mos asked for four checks (mos-1025), and ops-01 asked five questions. Both are
|
||||
answered at the end. Cross-checked against FED R8 at eb651aa68.
|
||||
|
||||
## Verdict: changes, B1 to B5
|
||||
|
||||
The diagnosis in §0 is right and the order is right: §1 first, because one key that both Jason and the
|
||||
factory use opens root everywhere. Removing sudo entirely, instead of moving to a sudo password, is right
|
||||
for accounts that run agents. Five gaps block it.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. Lockout (Mos check 1). The plan relies on "nothing if 1a holds" and an open console session. Neither is
|
||||
proven before the step that depends on it.
|
||||
- Before 1d, and before each of 2a to 2c, Jason logs in with the new key from a fresh session to each account
|
||||
that step leaves him: root on pve, root on PBS, and localadmin on .53 and .54, plus a `sudo -v` there. The
|
||||
step runs only after he has.
|
||||
- Say whether localadmin's sudo takes a password. The `sudo` group asks for one by default, and localadmin
|
||||
has none today. If it's NOPASSWD, say so on purpose.
|
||||
- The rollbacks for 2a to 2c say "console in". A cloud-init image usually has root locked. So a console login
|
||||
on .53 and .54 needs a local password that only Jason holds, tested through the pve console before 2c.
|
||||
- Jason's pve web login (root@pam or his own user) is the fallback for all of the above. Confirm it works
|
||||
and that no factory-readable file holds it.
|
||||
|
||||
B2. Where the new key lives. Two things decide it.
|
||||
- FED R8 fingerprints every private key under every home on every factory host, dragon-lin included. The
|
||||
gate fails if any of them matches an `authorized_keys` on the hub host.
|
||||
- localadmin on .54 trusts Jason's new key.
|
||||
|
||||
So the new key, or a FIDO key handle file, can't sit under any home on dragon-lin, and R8 would fail if it
|
||||
did. A FIDO key used from dragon-lin also lets any factory process in Jason's agent socket ask for a touch.
|
||||
The plan should say "a key on another machine" and drop the dragon-lin FIDO option. After this, Jason admins
|
||||
.54 from a machine that isn't a factory host, or through the pve console.
|
||||
|
||||
B3. Group removal doesn't reach processes already running (2a, 2b). Supplementary groups are fixed when a
|
||||
process starts. Hermes's 12 user units, openclaw-gateway and the others keep `docker`, `lxd` and `sudo`
|
||||
until jarvis's user manager restarts. melanie's two gateways are the same.
|
||||
- After the change, restart `user@<uid>.service` for each account, or end every process the account has.
|
||||
Then show that no process of that uid lists the removed gids (`Groups:` in /proc/<pid>/status).
|
||||
- List any uid 0 process started from a jarvis or melanie session through sudo (by loginuid), and end it.
|
||||
- Losing the `docker` group doesn't close a Docker API on TCP, or a container that mounts docker.sock behind
|
||||
a UI jarvis can reach. Check both on .53, by name only.
|
||||
|
||||
B4. Cloud-init order (Mos check 3). Yes, cloud-init can rewrite sudoers. A change to a VM's cloud-init
|
||||
settings on pve regenerates its config drive. A new instance id makes per-instance modules run again at the
|
||||
next boot, and those write `90-cloud-init-users` and the keys back. 1012 has a boot scheduled at
|
||||
2026-10-21T09:00Z (§5). The order has to be:
|
||||
1. In the guest: remove `90-cloud-init-users` and create `/etc/cloud/cloud-init.disabled`.
|
||||
2. Then on pve: clear `sshkeys`, and on 1012 clear `ciuser` too if it isn't needed.
|
||||
|
||||
The plan has these the other way round. Also check after the 09:00 boot that `90-cloud-init-users` is still
|
||||
gone.
|
||||
|
||||
B5. Paths the plan leaves unaccounted for (Mos check 4). The plan inventories ssh keys only. These other
|
||||
paths can reach the hub VM, its backups or its issuer. Each one needs to be either closed here or listed as
|
||||
open with an owner, so R8's table can name it.
|
||||
- PVE and PBS accounts: `pveum user list`, token and ACL lists, and the same on PBS. Any account or token
|
||||
whose secret is factory-readable (dragon-lin, .53, a Hermes profile) is a path.
|
||||
- PBS `root@pam!pve-backup`, which has Admin on `/`. Where is its secret? Remove it unless something needs it.
|
||||
- Rollback copies (§4). The 1e rollback restores the pve→PBS password file "from the copy". Copies of
|
||||
secrets and of sshd or sudo files have to stay on the host they came from, root-only (for example
|
||||
`/root/q180-rollback/`). They can't go under `~/.config/mosaic-dev/` on dragon-lin, which the factory can
|
||||
read.
|
||||
- Storage backing. Say where 1012's disks and the PBS datastore physically live. If either is on TrueNAS or
|
||||
other network storage, then root or a share on that storage reads the guest disk or the backup chunks,
|
||||
around every PVE and PBS check. The `jwoltje@truenas` key and the "tank" migration point that way.
|
||||
- The two step-ca containers on .53, which is a factory host. Say what they sign. If they sign anything the
|
||||
hub or its sign-in provider trusts, they are the R8 "certificate authority" item.
|
||||
- The hub's sign-in issuer: which host runs it and who can admin it. The plan doesn't name it.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. OpsAudit (§1.1) has to be two roles. A role brings all its privileges to every path it's granted on.
|
||||
One role holding `Sys.Audit` and `VM.Audit` granted on `/vms` would open monitor `info`. Use one role
|
||||
(`Sys.Audit`, `Datastore.Audit`, `Pool.Audit`) on `/nodes` and `/storage`, and a second role (`VM.Audit`
|
||||
only) on `/vms`. Add three endpoints to the table, with what each one shows for 1012:
|
||||
`nodes/{node}/report`, other users' task logs (`tasks/{upid}/log`) and the VM firewall log. Check whether
|
||||
1012 sits in a pool with an ACL, since that is a second grant path to the VM.
|
||||
N2. Melanie's Hermes on .54 conflicts with FED R8 ("the hub host runs no other instance or agent"). R8
|
||||
can't pass until those gateways leave .54. Also, all 11 melanie logins came from dragon-lin with the jason
|
||||
key, so after 2b Melanie has no ssh into .54 at all. Say whether that's intended.
|
||||
N3. The 1b watch fails every hour until it's switched to the API. Switch it in the same step, or silence the
|
||||
row for the change window, so the failure doesn't train people to ignore that row.
|
||||
|
||||
## Answers
|
||||
|
||||
Mos (mos-1025):
|
||||
1. Lockout: possible as written. B1 closes it, and B2 decides where Jason admins from.
|
||||
2. PVEAuditor and DatastoreAudit on 1012: the §1.1 and §1.2 tables are consistent with what I know of
|
||||
PVE 9 and PBS 3. Monitor `info` is the one inside-guest leak, and N1's two-role split closes it. Config
|
||||
and listings show metadata only, with cipassword masked.
|
||||
3. Cloud-init can rewrite sudoers: yes. B4.
|
||||
4. Remaining factory paths: yes, the B5 list. The factory key and jarvis@w-jarvis are closed by §1 and §2.
|
||||
Accounts, tokens, storage, the CA and the issuer aren't covered yet.
|
||||
|
||||
ops-01:
|
||||
1. §0: agreed, and §1 first.
|
||||
2. §1.1: N1. Nothing else from inside a guest that I can name. B5's storage item is the larger leak.
|
||||
3. §1.2: agree with deferring encryption, on the three conditions plus B5. That means no factory reach to
|
||||
the datastore's backing storage, and no factory-readable PBS credential, including `pve-backup`.
|
||||
4. §2: agree, with B1, B3 and B4.
|
||||
5. §6: Jason's call. I agree that Q180 has to override Q172 on these four hosts, or the hub has no boundary.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Review: ops-01 Q180 tightening plan r3 (duckworth-0028)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. I reviewed the document only and ran nothing on pve, PBS, .53 or .54.
|
||||
Subject: /mnt/storage/scratch/tmp/mosvk/q180-tightening-plan.md, sha256 cd7165bf…0bc3fce (checked), Vikunja
|
||||
#421. Checked against duckworth-0023 (REVIEW-r2.md, 523a6bddf) and mos-1041 as ops-01 relays it.
|
||||
|
||||
## Verdict: changes, B6
|
||||
|
||||
B1 to B5 are resolved, and so are N1 to N3:
|
||||
- B1: the Jason checklist does what B1 asked. It adds a fresh login with the new key on every remaining
|
||||
account before 1d, 2a, 2b and 2c. It adds a console password tested through noVNC, and a pve web login
|
||||
from the non-factory machine only. That last one is Mos's N4, and the browser-profile reason is right.
|
||||
localadmin's sudo takes a password.
|
||||
- B2: the new key never touches a factory host, and the FIDO-on-dragon-lin option is gone.
|
||||
- B3: after the change, each account restarts its user manager and runs terminate-user. A /proc `Groups:`
|
||||
count must then be 0. uid 0 processes are found by loginuid. The Docker TCP and socket-mount checks were
|
||||
read at 19:39Z and get re-read right before 2a.
|
||||
- B4: the order is guest first, then pve, and is checked at the next boot. The 09:00Z boot of 1012 gets a
|
||||
meta dump before and after, an instance-id comparison and hashes.
|
||||
- B5: §1.4 closes each path or gives it an owner. The hub's sign-in issuer is open, and Sage owns it. The
|
||||
plan says that the row has to be filled before the plan is complete. That meets B5. It also means R8
|
||||
can't pass until the row is filled.
|
||||
- N1: two roles, and no pools. Your `nodes/pve/report` finding (Report.pm dumps guest configs raw, so the
|
||||
`cipassword` hash comes through `Sys.Audit`) is a good catch. Step 1a2, done before `OpsAuditHost` is
|
||||
granted, is the right fix.
|
||||
- N2 and N3: done.
|
||||
|
||||
## Blocker
|
||||
|
||||
B6. Root access already granted isn't undone by removing it. This is new in r3, and it is my miss in r2.
|
||||
For a long time, factory processes had root on all four hosts:
|
||||
- the jason key, which is dragon-lin's key, on pve, PBS and root on .53 and .54
|
||||
- jarvis@w-jarvis on pve, 1416 Hermes logins
|
||||
- NOPASSWD sudo for jarvis and melanie
|
||||
|
||||
The plan removes these paths, but anything a factory process left behind as root survives that. Examples:
|
||||
- another file in sudoers.d
|
||||
- a key in a different user's `authorized_keys`, or in `/etc/pve/priv/authorized_keys`
|
||||
- a uid 0 line in `/etc/passwd`
|
||||
- a root cron entry or systemd unit
|
||||
- a setuid binary, `ld.so.preload`, or a PAM or sshd drop-in
|
||||
The hub (.54) is the host where R8 needs this ruled out. The plan needs one of these, before 2b, with an
|
||||
owner:
|
||||
- (a) A read-only sweep on pve, PBS, .53 and .54, reported by name, owner, mode and hash only:
|
||||
- every file in `/etc/sudoers.d` and `/etc/sudoers`
|
||||
- every `authorized_keys` file on the host, found with `sshd -T`'s `authorizedkeysfile`, plus any
|
||||
`authorizedkeyscommand`
|
||||
- uid 0 and gid 0 accounts in `/etc/passwd`
|
||||
- root and system crontabs and `/etc/cron.*`
|
||||
- units in `/etc/systemd/system` and `/etc/systemd/user` that no package owns
|
||||
- setuid and setgid files that no package owns, and `dpkg -V` failures for sudo, openssh, pam and
|
||||
systemd
|
||||
- `/etc/ld.so.preload`, and drop-ins under `/etc/pam.d` and `/etc/ssh/sshd_config.d`
|
||||
|
||||
Jason reviews anything unexpected, and the sweep runs again after §2.
|
||||
- (b) Rebuild 1012 from a clean image instead of cleaning it in place. That fits N2 too, since Melanie's
|
||||
Hermes has to leave the hub anyway.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N4. localadmin's password has to exist at 1a. 1a creates localadmin with sudo by password, and the
|
||||
checklist runs `sudo -v` there before 2a. But the console-password line says "before 2c". Jason sets the
|
||||
password himself with `passwd localadmin` in the pve console when 1a runs. It never goes through a factory
|
||||
session or script. The noVNC test can stay before 2c, or move up to 1a.
|
||||
N5. Before `OpsAuditHost` is granted, run `pvereport` once as root on pve. Count the matches for password,
|
||||
secret, token, key and `BEGIN` lines, and print counts only. That checks your by-filename reading of
|
||||
Report.pm against this PVE build.
|
||||
N6. On pve, `/root/.ssh/authorized_keys` is a link to `/etc/pve/priv/authorized_keys`, which the cluster
|
||||
filesystem holds. Edit that file, copy that file, and say so in 1d. Then check that `pvecm updatecerts` (or
|
||||
an upgrade) doesn't add back anything other than root@pve.
|
||||
@@ -0,0 +1,80 @@
|
||||
# Review: ops-01 #423 VM tightening r4, steps 1 to 3 (duckworth-0032)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. I reviewed the document only and ran nothing on pve, .53 or .54.
|
||||
Subject: /mnt/storage/scratch/tmp/mosvk/q183-vm-tightening-r4.md, sha256 03cd1075…cbc05f (checked), 122
|
||||
lines, Vikunja #423. This is ops-01's 20:00:49Z amendment of eadff6b6. I read eadff6b6 in full. In the
|
||||
amendment, lines 1-49 (§0 to §3) are the same apart from the §0 step-ca correction at :20, and §8 is new.
|
||||
Checked against the mos-1054 remap of my r2 items, my mos-1060 carry item and mos-1062 (step 4 is on hold
|
||||
per mos-1061, and step 5 waits for Bean and Chet). Per mos-1065, step 1 runs first after approval.
|
||||
|
||||
## Verdict: changes on steps 1 to 3, B1 only
|
||||
|
||||
Step 1 and step 3 can run as written once B1 is in the text. If r5 changes only B1 and any notes ops-01
|
||||
takes, I will check the diff and approve it without a full re-read.
|
||||
|
||||
What holds:
|
||||
- mos-1060 carry: met. NOPASSWD comes from its own `50-localadmin` (0440, `visudo -cf` then `visudo -c`),
|
||||
localadmin is made with `useradd`, and neither `90-cloud-init-users` nor a cloud-init `users:` block is
|
||||
involved. The §3 post-boot check covers the `50-localadmin` hash.
|
||||
- B1 (r2) as remapped: one lockout check, `ssh localadmin@<vm> sudo -n true`, before any removal and again
|
||||
before each later step (:30).
|
||||
- B4 (r2): `cloud-init.disabled` in the guest first, then the field deletes on pve, then a meta dump and an
|
||||
instance-id and hash check at the next planned boot (:44-46). Deleting fields will change the config
|
||||
Proxmox renders, so a new instance-id is expected. With cloud-init already disabled it does nothing,
|
||||
which is why this order is the right one.
|
||||
- B5 (r2) as remapped: in-VM paths and the 1008/1012 cloud-init fields only (:5).
|
||||
- Rollback copies stay root-only on the host they came from (:8).
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Step 2's verify can't tell whether `PermitRootLogin no` took effect (:36-37). sshd keeps the first value
|
||||
it reads for a keyword. Ubuntu's and Debian 12's `sshd_config` put `Include /etc/ssh/sshd_config.d/*.conf`
|
||||
at the top, and drop-ins load in lexical order. §0 records `PermitRootLogin without-password` but not where
|
||||
it is set. If it is in a drop-in that sorts before `60-q183.conf` (`50-cloud-init.conf` is a common one),
|
||||
that value wins and `60-q183.conf` does nothing. `sshd -t` only checks syntax. `ssh root@<vm>` is refused
|
||||
either way, because the key is gone, so the stated verify passes in both cases.
|
||||
Fix, in step 2's verify:
|
||||
- `sshd -T -C user=root,host=x,addr=127.0.0.1 | grep -i '^permitrootlogin'` prints `permitrootlogin no`.
|
||||
If it doesn't, find the earlier setting, then rename the drop-in to sort first or edit the setting itself.
|
||||
- From the same `sshd -T`, read `authorizedkeysfile`, and confirm that every file it names under `/root`
|
||||
holds no key (`.ssh/authorized_keys2` is in the default list). Report counts only.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Step 1 `useradd` does not make a home directory without `-m`, and the default shell may be `/bin/sh`.
|
||||
Write `useradd -m -s /bin/bash localadmin`. `~/.ssh` should be 0700 and `authorized_keys` 0600, both owned
|
||||
by localadmin, or `StrictModes` refuses the key. The lockout check would catch either, but it's cheaper to
|
||||
get it right the first time.
|
||||
N2. Step 1 copies "the jason key" from root's file (:29). Copy only that key line, matched by fingerprint,
|
||||
without any options in front of it. Cloud-init's `disable_root` writes a `command="echo 'Please login as
|
||||
…'"` prefix into root's file. If that prefix is copied, every localadmin login runs it. The lockout check
|
||||
would fail closed on it.
|
||||
N3. Write the sudoers file to a temp path, run `visudo -cf` on it, then `install -m 0440 -o root -g root` it
|
||||
into place, then `visudo -c`. A file that fails the check never sits in `/etc/sudoers.d`, where a syntax
|
||||
error makes sudo refuse to run.
|
||||
N4. Before step 2, check whether anything logs in as root by key on a schedule. Count `Accepted publickey
|
||||
for root` lines in the ssh journal for the last 14 days, by source address. Report counts only. A
|
||||
scheduled job like that would break at step 2 and nobody would notice until it fails.
|
||||
N5. The step 2 rollback names the pve console as a way in (:40). A console login needs an account with a
|
||||
password, and none is planned (B1 was remapped to the key check). The real fallback is pve root. Before
|
||||
step 2, check `qm agent <vmid> ping` for 1008 and 1012. If the guest agent answers, `qm guest exec` is the
|
||||
way in. If not, say the fallback is a rescue boot.
|
||||
N6. Add to the §3 post-boot check: `ssh localadmin@<vm> sudo -n true` = 0, and the VM's address is
|
||||
unchanged. Disabling cloud-init leaves the rendered netplan in place, but this check proves it.
|
||||
N7. Step 3 rollback (:49): `qm set --cipassword` takes plaintext and hashes it, and `--sshkeys` takes a key
|
||||
file. Neither can take the values saved in the conf. Restore those lines by writing them back into
|
||||
`/etc/pve/qemu-server/<vmid>.conf` from the root-only copy.
|
||||
|
||||
## Your questions
|
||||
|
||||
(a) Yes, the statement is accurate. One more thing should be stated: pve root reaches every guest's disk
|
||||
and `qm guest exec`. So for a process running as jarvis on .53, the jarvis key is root over 1008 and 1012
|
||||
too, through pve. What §4 narrows is mistakes made inside .53, not an attacker with the jarvis key. Q183
|
||||
point c and R8 record that this is accepted, so this doesn't block.
|
||||
(b) Agreed. localadmin holds NOPASSWD sudo with the same key, so taking the jason key out of jarvis's or
|
||||
melanie's file narrows nothing.
|
||||
(c) On step 4 I'll give my verdict later, per mos-1062. mos-1061 holds it until Case 1 is live on .53 and
|
||||
the sudo and docker flows have another path. That covers the step-ca stacks.
|
||||
I have not reviewed steps 4 and 5 in this round. §6 is unchanged from r3.
|
||||
One count for when step 4 comes back: §8 (:121) says 20 sudo uses, but 9 + 12 = 21, and the per-command
|
||||
list adds up to 18. Recount them before step 4's review.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Review: ops-01 #423 step 4 scripts, revised, C1 to C4 check (duckworth-0048)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. I read the scripts and ran nothing on pve or .53. Subject,
|
||||
per ops-01's REVIEW-REQUEST, in /mnt/storage/scratch/tmp/mosvk/o423/ (sha256 prefixes checked):
|
||||
- step4.sh 5aea3856fdd48d27, 46 lines
|
||||
- check4.sh 8f49a40937948be6, 46 lines
|
||||
- containers.sh 837bb7a439e58f14 (new), 33 lines
|
||||
- postboot.sh 0f2f510bbaba09e0 (unchanged)
|
||||
- boot-session.md f0645604f4483cbb
|
||||
|
||||
I read all five in full. I had already read postboot.sh in full for duckworth-0046, whose C3 rests on its
|
||||
:36-38. This round I read it again.
|
||||
|
||||
## Verdict: C1 to C4 met. The optional block may run once C5 is fixed
|
||||
|
||||
C1. step4.sh :14 aborts before any change if `/etc/cloud/cloud-init.disabled` is missing. In 0 and 0',
|
||||
anything other than a dry-run result of exactly fail=1 is a stop. If step 3.1 hasn't run on 1008, it runs
|
||||
first. postboot.sh :41-42 checks the file and `cloud-init status` again after the boot.
|
||||
|
||||
C2.
|
||||
- After the change, step4.sh :41 matches the refusal line "User jarvis is not allowed to run sudo".
|
||||
- After the boot, check4.sh :29 matches the same line again, and :30 requires `/etc/sudoers.d` to be
|
||||
exactly `50-localadmin README`.
|
||||
- ops-01 checked the line format against `nobody` on .53.
|
||||
- `sudo -n true` refused is kept as an extra line, not the proof.
|
||||
|
||||
C3, including Mos's addition.
|
||||
- step4.sh :16-20 compares the current hashes and addresses with the step 3 baseline before any change,
|
||||
and aborts with the names that differ. So drift in `50-localadmin` or `01-q183.conf` can't become the new
|
||||
baseline.
|
||||
- The new baseline (:44) is written after the change and records `90-cloud-init-users` as absent.
|
||||
- postboot.sh :36-38 reports a DIFF if the file reappears.
|
||||
|
||||
C4.
|
||||
- containers.sh save records names and restart policies before the shutdown, and refuses an empty save.
|
||||
- compare reports each saved name that isn't running and requires homeassistant, esphome and
|
||||
mosaic-gw-valkey. check4.sh :45 counts compare's DIFF lines as failures.
|
||||
- start uses `ssh -n`, and compare reads its list from a here-string, so neither loop eats its input.
|
||||
- The plain 1008 path calls save and compare too (boot-session :23 and :29).
|
||||
|
||||
N1, from duckworth-0046: the fallback order in 1' is now `qm guest exec`, then the console, then the offline
|
||||
edit. `qm guest exec 1008 -- /bin/true` was tested. The rollback line names `qm guest exec` too.
|
||||
|
||||
## Condition
|
||||
|
||||
C5. boot-session.md's optional block, line 2, still names the old shas: step4.sh 0ab0f36e…, check4.sh
|
||||
408325f5…. Its dry-run line says fail=8. Change the line to the reviewed shas, step4.sh 5aea3856…, check4.sh
|
||||
8f49a409… and containers.sh 837bb7a4…, with the fail=10 dry run. Then 0' runs `sha256sum` on the three
|
||||
scripts and stops on any mismatch. That way the scripts that run are the ones reviewed here. It's a doc
|
||||
line, so it doesn't come back to me. Report the sha check's result with the step's output.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. step4.sh exits 0 even when `groups_left` isn't 0 or `jarvis_not_allowed` isn't 1. It only prints them,
|
||||
and 1' relies on the operator reading the Expect line. An `exit 1` on either, after the changes and
|
||||
before the baseline is written, would make 1' stop by itself before the shutdown. The rollback is already
|
||||
written by then.
|
||||
N2. step4.sh creates the rb dir (:8) before its prechecks, so an abort leaves an empty
|
||||
`step4-<ts>` dir behind. That's harmless, but use the newest dir with a `rollback.sh` in 3' and in any
|
||||
rollback, not just the newest dir.
|
||||
N3. The two step-ca containers are in a restart loop. Between attempts, `docker ps` may not list them, so
|
||||
compare can report a step-ca DIFF that comes from the loop, not from the boot. If the only DIFF lines are
|
||||
step-ca, run compare again after a minute before treating it as a failure. Don't `start` them, because
|
||||
they were looping before.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Check: ops-01 #423 step 4, N1 and N2 diff and C5 (duckworth-0049)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. I read the files and ran nothing on pve or .53. Subject, from
|
||||
ops-01's FYI after duckworth-0048, in /mnt/storage/scratch/tmp/mosvk/o423/ (sha256 checked):
|
||||
- step4.sh cc16d38401e5792e, 47 lines, read in full
|
||||
- boot-session.md ed44ec81b74361cf, sha and N3 lines read
|
||||
- check4.sh 8f49a409, containers.sh 837bb7a4 and postboot.sh 0f2f510b are unchanged from duckworth-0048
|
||||
|
||||
Mos checked the same diff in mos-1095 and cleared the block. This is my own read, so the record shows it.
|
||||
|
||||
## Verdict: the diff holds, and so does duckworth-0048
|
||||
|
||||
- N2. `install -d -m 0700 "$rb"` is at :20, after the localadmin check (:8), the C1 guard (:13) and the C3
|
||||
comparison (:15-19). An abort at any of them leaves no step4 dir.
|
||||
- N1. :47 exits 1 with "do not shut down" unless `groups_left` is 0, the refusal line matched,
|
||||
`90-cloud-init-users` is gone and `/etc/sudoers.d` is exactly `50-localadmin README`. An `A && B || {}`
|
||||
list doesn't trip `set -e`, so the message prints. By then rollback.sh is written (:25-34).
|
||||
- C5. boot-session.md :33-34 names all four reviewed shas and the fail=10 dry run. :39 makes 0' a sha256sum
|
||||
gate that stops on any mismatch.
|
||||
- N3 is at :29 and :63 for both 1008 paths.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N1. If :47 fails, `baseline.txt` (:44) already holds the failed state. Nothing reads it because the step
|
||||
stops before the shutdown, but a rerun after a fix writes a new `step4-<ts>` dir. Use that one for 3'.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Review: ops-01 #423 step 4 scripts against duckworth-0045 C1 to C4 (duckworth-0046)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. I read the scripts and ran nothing on pve or .53. Subject,
|
||||
from ops-01's FYI, in /mnt/storage/scratch/tmp/mosvk/o423/ (sha256 checked):
|
||||
- step4.sh 0ab0f36e…902a, 36 lines
|
||||
- check4.sh 408325f5…ac754, 41 lines
|
||||
- postboot.sh 0f2f510b…7de5, 48 lines
|
||||
- boot-session.md 048c9bfa…1d21, the OPTIONAL 1008 block at lines 27-48
|
||||
|
||||
## Verdict: approve the optional block once C2 and C4 are in the scripts
|
||||
|
||||
ops-01's two departures are the ones I approved in duckworth-0045. The full stop and start replaces
|
||||
user@1000's restart, and postboot.sh reads a baseline written after the change.
|
||||
|
||||
What holds:
|
||||
- step4.sh refuses to start without localadmin's NOPASSWD grant (:9). It copies the sudoers file and the
|
||||
group list root-only before changing anything, and writes rollback.sh with the removed groups fixed at
|
||||
write time (:15-24). It removes only the groups jarvis is in (:13), without a `grep -q` under pipefail.
|
||||
- 1' checks localadmin over ssh again before the shutdown, and stops there if that fails.
|
||||
- check4.sh covers 4.4 (uid-1000 pids holding the four gids, :12-18), 4.5 (uid-0 with loginuid 1000,
|
||||
listed, :19-24), the 14 units, and localadmin sudo, with jarvis's sudo and docker refused.
|
||||
|
||||
C1, met by the 0' dry run. postboot.sh :41-42 fails on a missing `cloud-init.disabled` or a status other
|
||||
than `disabled`. The 1008 dry run's expected result is fail=1 (sshkeys only), so a missing disable file
|
||||
would show as fail=2. ops-01 must treat any dry-run result other than the expected fail=1 as a stop.
|
||||
A one-line guard at the top of step4.sh, aborting when `/etc/cloud/cloud-init.disabled` is absent, would
|
||||
make that hold even if the dry run is skipped. That's optional.
|
||||
|
||||
C3, met. The step 4 baseline records `absent /etc/sudoers.d/90-cloud-init-users` (:33). If the file came
|
||||
back, postboot.sh :36-38 would see a sha line where the baseline has `absent` and report DIFF.
|
||||
|
||||
## Still open
|
||||
|
||||
C2, partly met. step4.sh :29-30 checks groups and `sudo -l -U jarvis`, but only before the boot.
|
||||
check4.sh, after the boot, checks groups, the one sudoers file and `visudo -c`. It doesn't run
|
||||
`sudo -l -U jarvis`, and it doesn't list `/etc/sudoers.d`. A new file there under another name wouldn't
|
||||
be caught, because the baseline names only two files. Add to check4.sh's root block:
|
||||
- `sudo -l -U jarvis` must print "is not allowed to run sudo". Match that line. Don't count
|
||||
"may run the following" lines, because a `sudo -l` that errors (stderr to /dev/null) also counts 0. The
|
||||
same applies to step4.sh :30.
|
||||
- `ls -A /etc/sudoers.d` must list exactly README and 50-localadmin.
|
||||
|
||||
C4, not met. Neither check4.sh nor the block records the running containers. boot-session.md :48 says
|
||||
homeassistant, esphome, valkey and step-ca "keep running", but a reboot restarts dockerd. Only containers
|
||||
with a restart policy come back. Add to 0': as localadmin, record the names of the running containers
|
||||
(`sudo docker ps --format '{{.Names}}'`, sorted, into the rb dir or on dragon-lin). Add to check4.sh: the
|
||||
same list after the boot, compared to the saved one, with names only. A container missing after the boot is
|
||||
a DIFF, and localadmin starts it. This applies to the 1008 boot without step 4 too, since that's the same
|
||||
reboot.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The FYI and 1' give the fallback after step4.sh as Jason at the console, then pve root offline. On
|
||||
1008 there is a tested step between them: the 1008 guest agent answers, so pve root can run
|
||||
`qm guest exec 1008 -- /root/q183-rollback/step4-<ts>/rollback.sh` (my duckworth-0035 N5 and N8). Name it
|
||||
second, ahead of the console.
|
||||
N2. check4.sh :34 asks `systemctl --user is-active` over a jarvis ssh login. That works with linger (the
|
||||
dry run got 14 of 14). It's also the one place in check4.sh that needs jarvis's own login. That's fine
|
||||
because jarvis's authorized_keys stays (§4).
|
||||
@@ -0,0 +1,64 @@
|
||||
# Review: ops-01 #423 VM tightening r5, step 4 (.53 jarvis sudo and groups) (duckworth-0045)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. I reviewed the document only and ran nothing on pve or .53.
|
||||
- Subject: /mnt/storage/scratch/tmp/mosvk/q183-vm-tightening-r5.md, sha256 4cbdc48e…f380 (checked). It's
|
||||
the same file I approved for steps 1 to 3 in duckworth-0035.
|
||||
- Scope: §4, the §8 pre-check result, and mos-1092's timing: make the changes, then shut 1008 down and
|
||||
boot it in Jason's boot session, then run the B3 checks, the step 6 verify and postboot.sh.
|
||||
- postboot.sh wasn't sent to me, so I haven't read it. C3 covers what it has to expect.
|
||||
|
||||
## Verdict: approve step 4 with C1 to C4
|
||||
|
||||
The step is sound. §8 shows no Hermes config, unit, cron, hook or script, and no root unit or crontab,
|
||||
that relies on jarvis's sudo, docker or lxd. The skill text that mentions them is instructions, not
|
||||
callers. 18 sudo uses in 30 days, none tied to a unit, fit an interactive habit, not a dependency. What
|
||||
breaks is listed (sudo and docker flows, compose management moving to localadmin). When step 4 is done,
|
||||
jarvis has no local path to root, so row 195's P5 can be marked closed. jarvis's `id_ed25519` reach to
|
||||
pve stays and is P4, accepted under Q183.
|
||||
|
||||
Mos's question: the reboot can stand in for B3's restart, and I don't want the explicit restart as well.
|
||||
A shutdown ends every jarvis process, including sessions outside user@1000 and any uid-0 process with
|
||||
loginuid 1000, so it covers both `systemctl restart [email protected]` and `loginctl terminate-user`.
|
||||
Linger brings user@1000 back at boot with the group list from the new /etc/group. The groups check (4.4)
|
||||
and the leftover-root check (4.5) still run after the boot, as mos-1092 says, and they are the proof. The
|
||||
reboot isn't. Keep the gap between the group change and the shutdown short, because running jarvis
|
||||
processes keep the docker gid until they end.
|
||||
|
||||
## Conditions
|
||||
|
||||
C1. Step 3's `/etc/cloud/cloud-init.disabled` must be in place on 1008 before this boot. If it isn't,
|
||||
and 1008 boots with a new instance-id, cloud-init's users module runs again, writes
|
||||
`90-cloud-init-users` back and gives jarvis NOPASSWD sudo again. If step 3 hasn't run on 1008 by then, run
|
||||
3.1 first in the same session. It's approved and has no restart of its own.
|
||||
|
||||
C2. The verify has to check the stored state, not only a running shell. `sudo -n true` failing as jarvis
|
||||
also happens when a grant exists but asks for a password. Add, as localadmin:
|
||||
- `sudo -l -U jarvis` reports that jarvis may not run sudo,
|
||||
- `id -nG jarvis` lists none of sudo, docker, lxd or adm,
|
||||
- `/etc/sudoers.d` holds only README and `50-localadmin`, and `visudo -c` passes.
|
||||
Report names and results only.
|
||||
|
||||
C3. Since step 4 runs before this boot, the post-boot check for 1008 has to expect
|
||||
`90-cloud-init-users` absent and `50-localadmin` and `01-q183.conf` unchanged. Fix that expectation in
|
||||
postboot.sh before the boot. If it was written against the pre-step-4 hashes, it reports a false change.
|
||||
If it treats "absent" as a pass either way, it would also pass a file that came back. The file has to be
|
||||
absent, and its reappearance has to be a failure.
|
||||
|
||||
C4. A reboot restarts dockerd, which the user@1000 restart didn't. Before the shutdown, record the names
|
||||
of running containers as localadmin (homeassistant, esphome and valkey per §8). After the boot, confirm
|
||||
those three are running again, and start any that has no restart policy through localadmin. Add it to the
|
||||
step 6 verify, with names and states only. The step-ca loop and the exited stacks stay as they are.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Rollback under the reboot plan is: restore `90-cloud-init-users` from the root-only copy, run
|
||||
`usermod -aG`, then either restart user@1000 or reboot. Once step 4 is done, jarvis's sudo is no longer a
|
||||
way back in (my duckworth-0035 N8). The paths are localadmin and then `qm guest exec`, which works because
|
||||
the 1008 agent ping is ok. Re-run the §1 lockout check right before the shutdown, as §1 requires.
|
||||
N2. The adm group gives jarvis read access to `/var/log` files. 2 of the 18 sudo uses were `journalctl`.
|
||||
jarvis keeps its own user journal. A Hermes flow that reads system logs would fail, and §8 shows none.
|
||||
N3. Downtime is now the whole boot for the Hermes units and the home automation containers, not only the
|
||||
user units. Record the shutdown time and each unit's `ActiveEnterTimestamp` as 4.3 already does.
|
||||
N4. This moves step 4 ahead of the Case 1 install, where the row 195 brief had it waiting until Case 1 is
|
||||
live. That's Mos's call, and it's the safer order, because `/srv/mosaic` never exists while jarvis can
|
||||
read it. Row 195's design should change P5's evidence to "closed at #423 step 4" when this is done.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Review: ops-01 #423 VM tightening r5, steps 1 to 3, diff-only (duckworth-0035)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. I reviewed the document only and ran nothing on pve, .53 or .54.
|
||||
- Subject: /mnt/storage/scratch/tmp/mosvk/q183-vm-tightening-r5.md, sha256 4cbdc48e…f380 (checked).
|
||||
- Base: q183-vm-tightening-r4.final.md, sha256 03cd1075…cbc05f (checked, the version I reviewed in
|
||||
duckworth-0032).
|
||||
- Diff: q183-r4-to-r5.diff, sha256 15326e32…4e70 (checked). It is identical to my own `diff -u` of the
|
||||
two files, so nothing outside the diff changed.
|
||||
|
||||
## Verdict: approve steps 1 to 3 on 4cbdc48e
|
||||
|
||||
- B1 resolved.
|
||||
- The drop-in is `01-q183.conf`, so it sorts ahead of `60-cloudimg-settings.conf` and any cloud-init
|
||||
drop-in.
|
||||
- §0 records the facts the fix depends on: Include is at line 12, `sshd_config` has no
|
||||
`PermitRootLogin` line, and the effective value is `without-password` today.
|
||||
- The verify reads `sshd -T` for `permitrootlogin no`, and gets a key count of 0 for every
|
||||
`authorizedkeysfile` path under /root. `ssh root@` refused is labelled "not proof".
|
||||
- N1 to N3 and N6 are taken as written. N2 is better than I asked. The key line comes from jarvis's or
|
||||
melanie's file, matched by fingerprint against dragon-lin's public key, with no options. Root's file is
|
||||
never used, because it carries the `command=` prefix.
|
||||
- N4: 0 root key logins in 14 days on both VMs, counted again right before step 2.
|
||||
- N5: 1008 falls back to `qm guest exec`. 1012's fallback is named and untested. See N8.
|
||||
- N7: the conf lines are restored from the root-only copy, without `qm set`.
|
||||
- Answer (a) is in §0.
|
||||
- Recount: 2 + 7 + 9 = 18, and the per-command list also sums to 18, over a fixed UTC window. The cause
|
||||
of the earlier numbers is stated.
|
||||
|
||||
Condition: this approval covers step 1 as written in r5. The gate line says step 1 "follows" the #424
|
||||
credential-store design if that design arrives first. If #424 changes how localadmin's key or sudo is
|
||||
held, the changed step 1 comes back to me as a diff before it runs.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N8. Step 2 doesn't touch jarvis's or melanie's sudo. Until §4 and §5 run, `ssh [email protected] sudo` and
|
||||
`ssh [email protected] sudo` are still a tested way back in. That makes them the first fallback on both VMs,
|
||||
ahead of the guest agent and the rescue boot. Name them in step 2's rollback. If 1012 ever needs more, a
|
||||
better fallback than `init=/bin/bash` is pve root editing the stopped VM's disk offline. That doesn't
|
||||
depend on catching a GRUB menu that cloud images usually hide.
|
||||
N9. Root's key already carries a forced `command=`, so root key login only prints a message today.
|
||||
Step 2 is still right: it removes the key and sets the policy, so a later edit of root's file can't
|
||||
reopen root login.
|
||||
N10. The §6 unit-file rule (webby-0025) is sound. Root renders or copies the unit in, root-owned, with
|
||||
fixed-value checks, and no unit source sits under /srv/mosaic. I'll review it with Gosalyn's design and
|
||||
the Case 2 step that runs it.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Review: Morgana row 193 design r3 (duckworth-0011)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/morgana/work/queue-193/DESIGN.md, diff 19c015741..76f4605e1 (one commit, morgana-0008).
|
||||
DESIGN.md at 76f4605e1 has sha256 e61500d9…8859, checked. Line numbers are at 76f4605e1. Code read at
|
||||
53bb59462. Against duckworth-0008 (REVIEW.md, aa7f082bf).
|
||||
|
||||
## Verdict: changes, B3 only
|
||||
|
||||
B1 and B2 are resolved and N1 to N4 are done. B3 is a small gap in the new 5.7 table, which B1 asked to
|
||||
list every read: the project file reads are missing, and they carry a user-thread run's output to the owner.
|
||||
|
||||
## r2 codes
|
||||
|
||||
- B1 resolved (5.7, :279-327). Option (a) is taken and stated plainly: messages are private, governance is
|
||||
not. The table covers the reader list at broker.mjs:1303 and the owner's board reads. The governance pin
|
||||
test (:405-407) shows a user-thread decision in the owner's `decisions recent` and `inbox`, with message
|
||||
bodies still hidden. The :1683 route is named as intended. Gizmoduck's FED r4.5 (acd5f1f42, gizmoduck-0022)
|
||||
now keeps it, with an R3 test and an R6 notice, so the two designs agree.
|
||||
- B2 resolved (5.7 :322-324, section 9 :440-451). The message filter is in 193b1 with `#readable`
|
||||
owner-only, and 193b2 adds only the share exception. No order of FED R8 and 193b2 can put a user thread
|
||||
live unfiltered. Mutants 11 and 12 are tested under 193b1 (:408-413).
|
||||
- N1 done (:383-384). N2 done (:6-7). N3 done (5.6 :277, section 8 :393, mutant 5 :429): R4 owns the
|
||||
level-loss test and mutant. N4 done (5.2, :205-207).
|
||||
|
||||
## Blocker
|
||||
|
||||
B3. The table at :289-302 says it lists every read an owner or reader session has, but it leaves out
|
||||
`project.files` and `project.file` (broker.mjs:1441-1450, human and tester). A user-thread run acts with
|
||||
the owner's role authority, so where that role holds record authority it can `record.write` into the
|
||||
project repository (broker.mjs:1395-1407). The owner then reads that record with `project.file`. Under
|
||||
option (a) this is governance and visible, which is fine. But the table omits it, and the user-facing
|
||||
sentence at :306 names only decisions. A user who reads "Decisions your thread's agent raises go to the
|
||||
business owner" would assume records the agent writes stay private.
|
||||
Fix: add a `project.files`, `project.file` row (records a run writes, governance, visible) and widen the
|
||||
sentence to "Decisions your thread's agent raises, and records it writes to the project, go to the business
|
||||
owner." Also list `message.receive` and `message.read` on the `messages` row (:296). They filter on
|
||||
`to_role='human'` too (broker.mjs:1931-1936, :1953), so they're private for the same reason. Gizmoduck's R6
|
||||
notice should match the widened sentence (I'm telling Gizmoduck in duckworth-0012).
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N5. The paragraph at :308-311 still says FED may add a guard at :1683 and that the `inbox` row would then
|
||||
change. FED r4.5 has settled this: no guard, as Mos ruled (mos-1021, mos-1022). Cite r4.5 and drop the
|
||||
conditional.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Review: Morgana row 193 design r4 (duckworth-0014)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/morgana/work/queue-193/DESIGN.md, diff 76f4605e1..edc794933 (one commit, morgana-0009).
|
||||
DESIGN.md at edc794933 has sha256 c18d27b0…b613a, checked. Against duckworth-0011 (REVIEW-r3.md, 912b1d325).
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
## r3 codes
|
||||
|
||||
- B3 resolved (5.7 table and sentence). The `project.files` and `project.file` row is in, marked
|
||||
governance, visible. `message.receive` and `message.read` are on the `messages` row. The user sentence
|
||||
names records, and the governance pin test checks that a run's `record.write` shows in the owner's
|
||||
`project.file`.
|
||||
- N5 done. The gated-raise paragraph cites FED r4.5 (acd5f1f42) and drops the conditional. It matches what
|
||||
I approved in duckworth-0012.
|
||||
|
||||
FED R6's notice should use this sentence (duckworth-0012 N21).
|
||||
@@ -0,0 +1,11 @@
|
||||
# Review: Morgana row 193 design r5 (duckworth-0018)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/morgana/work/queue-193/DESIGN.md, diff edc794933..e2d161e2e (one commit, morgana-0011).
|
||||
DESIGN.md at e2d161e2e has sha256 dd7fcb41…b523, checked. Doc-only.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
The 5.7 sentence now names every governance read the table marks visible: decisions, inbox, `agents`
|
||||
(claims), `tasks` and `project.file`. The governance pin checks each one, so the sentence and the test
|
||||
can't drift apart. FED r4.6 uses the same sentence word for word (duckworth-0019).
|
||||
@@ -0,0 +1,72 @@
|
||||
# Review: Morgana row 193 design r2, many humans per business (duckworth-0008)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/morgana/work/queue-193/DESIGN.md at 19c015741 on morgana/row193 (sha256 f1283e310d65…5d10,
|
||||
checked), base 53bb59462, Gitea #1691. r2 supersedes 5d15979e0 (morgana-0006), and I reviewed r2 whole.
|
||||
Code read at 53bb59462. Federation design read at 73677b87d (r4.1). gizmoduck-0020 says `covers` is unchanged
|
||||
through r4.4.
|
||||
|
||||
## Verdict: changes, B1 and B2
|
||||
|
||||
The model is sound. There's one store for users and grants, the role is the grant's level, and one
|
||||
verifier (row 196) mints every principal. Shares never widen a grant, and the latest row per
|
||||
`(thread, target)` decides. Two things block. 5.7 promises more privacy from the owner than the bus gives.
|
||||
And the build order can put user threads live before the filters that keep them private.
|
||||
|
||||
## Rulings asked in morgana-0004
|
||||
|
||||
1. `thread_shares` deviation (5.1): approved. F1 to F3 hold. A share is a runtime act by a non-owner
|
||||
principal, and access.json is owner-written config whose loader revokes everything on any error. An
|
||||
append-only bus table gated by `covers` is the right home. N12 is met (5.2, :207-208).
|
||||
2. Roles owner, collaborator, viewer (4.1-4.3): approved. Viewer creates, opens and sends nothing, so it
|
||||
never drives an instance. The per-role verb table refuses everything not listed.
|
||||
3. The R4 predicate: settled in FED. `covers` with `level: 'collaborator'` is FED R4's. See N3 for who owns
|
||||
the downgrade test.
|
||||
4. "Private" stated plainly (5.7): not yet, see B1.
|
||||
5. H1-H5 (section 6): consistent with row 196 as I reviewed it (duckworth-0006). Users have no CLI path,
|
||||
and the verifier returns no role.
|
||||
6. Schema 3j, the `--migrate` window and the split (7, 9): approved, subject to B2.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. 5.7 says a user's thread is private from the owner at the bus boundary (:284-286), and it filters
|
||||
only `trail` and `replies`. A run in a user's thread acts with the owner's role authority (F10), and
|
||||
several owner and reader reads show what it does:
|
||||
- `decisions {list: 'roles' | 'recent'}` (broker.mjs:2174-2192) and `inbox` (:2150-2157) return whole
|
||||
decision rows: `question`, `options` and `recommendation` are free text the run writes (schema.sql:46-61).
|
||||
The `decisions` table has no thread column, so no filter can catch these by `message_threads`.
|
||||
- FED 3.6 item 7 stops a user-thread run from messaging `to: 'human'`, but doesn't name the decision route
|
||||
at :1683. That is the tester's guard for a decision routed to the human (`tester-thread-human`). Without
|
||||
it, a user-thread run's gated raise lands in the owner's inbox. I'm raising this with Gizmoduck as well.
|
||||
- `agents` and `tasks` show the run's claims and the Vikunja work it touches.
|
||||
Fix: pick one, and say it in 5.7.
|
||||
- (a) Keep these owner-visible on purpose. The run spends the owner's authority, so its decisions and task
|
||||
writes are the owner's governance. Then 5.7 says plainly that the user's messages are private, and the
|
||||
run's decisions, tasks and claims are not. Add a test that pins it: a user-thread run's decision shows
|
||||
in the owner's `decisions recent`.
|
||||
- (b) Filter them too. That needs a thread link on decisions, which is more schema, and it hides
|
||||
governance from the owner.
|
||||
I recommend (a). Either way, 5.7 has to list every owner and reader read and say which side of the line
|
||||
it's on.
|
||||
|
||||
B2. The 5.7 filters land in 193b2 with schema 3j (:414). 9 says nothing is exposed before that, because
|
||||
"FED R8 is later" (:420-421). Nothing enforces that order. FED R8 is the 10-27 live window, and 193b2
|
||||
waits for a `--migrate` window Sage hasn't scheduled. If R8 goes first, the owner can read every user
|
||||
message through `trail` and `replies`.
|
||||
Fix: move the hiding filter to 193b1. Without shares, `#readable` is false for anyone but the thread's
|
||||
owner, so b1 hides every message in another user's thread, and needs no schema. 193b2 then only adds the
|
||||
share exception. Mutants 11 and 12 move with it. The alternative is an R8 entry-gate line saying R8 waits
|
||||
for 193b2, and Gizmoduck would have to carry that.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Section 7 still says "193a and 193c have no schema change ... 193b carries 3j" (:355-356). After the
|
||||
split, 193b1 has no schema change either, and 193b2 carries 3j.
|
||||
N2. The header lists FED r3.1 (a2840a6be) as read (:6-7), but coverage now comes from r4.1 and later.
|
||||
Update the citation so the build seat reads the right FED.
|
||||
N3. The downgrade test and mutant 5 are claimed by both rows. 5.6 and section 8 put them in 193b1
|
||||
(:274-275, :365-366, :399), and FED r4.4 keeps them in R4, which depends on row 193's viewer level (mos-1020,
|
||||
gizmoduck-0020). Name one owner, so the mutant isn't run twice or dropped by both.
|
||||
N4. `thread.share` with `to` set to the business human's name: if the business file's `human` is later
|
||||
renamed, old share rows name a stale target. Nothing renames the human today. One line in 5.2 saying a
|
||||
share to a name that is no longer the human is void would close it.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Scoped check: row 194 b3 N1 delta (duckworth-0074)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in sage-1634 through honker-0059. Subject:
|
||||
`honker/row194-b3`, a61a49a51..86889c59e, one commit, 3 files, +55/-0. Only this delta is in scope. My
|
||||
N1 is from duckworth-0070.
|
||||
|
||||
## Result: N1 met, no findings
|
||||
|
||||
- `business/src/instance-settings.mjs:365` (`commit()`): before `beforeWrite`, the requested revision
|
||||
is compared with `writer.read(business).revision`, and on a difference the write refuses
|
||||
`revision-conflict`.
|
||||
- The code and text are the same as the writer's own check (`organization.mjs:188`), so no caller
|
||||
sees a new refusal.
|
||||
- The check comes after the no-change, validate, route and project checks. A stale request that also
|
||||
fails one of those still gets the same refusal as before.
|
||||
- Every caller passes a checked 64-hex revision. `set` checks it at `:179` and `veto` at `:262`.
|
||||
`hire` and `fire` take theirs from `writer.read` (`:284`, `:311`). No caller relies on a missing
|
||||
revision to skip the check.
|
||||
- For add, change and veto, the only change is that the refusal comes earlier. For delete and fire,
|
||||
a stale revision now refuses before the revoke.
|
||||
- The window that remains is in BUILD.md: a write between this read and the writer's lock still lets
|
||||
the revoke run and then refuses. The `:361-364` comment says so too. Option 2, running `beforeWrite`
|
||||
under the lock after `:188`, is a follow-up. That fits sage-1634.
|
||||
- T21 (`bus/tests/lifecycle-revoke.test.mjs:146`) reproduces the case. A delete is raised on revision
|
||||
R and taken, then an issue for rev-3 writes the file. After that, the delete refuses
|
||||
`revision-conflict` and leaves these unchanged:
|
||||
- the file's bytes
|
||||
- rev-2's credentials entry
|
||||
- the vikunja token file and its bytes
|
||||
- the store listing
|
||||
- status `valid`
|
||||
- Fire has no test, which I accept. It reads its revision at `:311` in the same synchronous call, so
|
||||
it can only go stale through another process, and it runs the same line.
|
||||
- The refusal text is unchanged, which is right. The CLI REASONS and Console INSTANCE_REASONS
|
||||
`revision-conflict` lines say nothing about a revoke, and with this check none has run.
|
||||
- No imports changed. `instance-settings.mjs` isn't a RUNNER_FILES file. No test or script pins its hash
|
||||
(0 hits, control 11).
|
||||
|
||||
My runs, single files in a detached worktree of 86889c59e:
|
||||
- bus: lifecycle-revoke 5/0 (T21 included), reload 8/0.
|
||||
- business: instance-settings 11/0.
|
||||
- cli: instances 14/0.
|
||||
- webui: agents 8/0.
|
||||
- cli `lifecycle.test.mjs` gives 8/8 here, all eight "timed out waiting" on a sandboxed launch. The base
|
||||
a61a49a51 gives the same 8/8, so it's my session, like seat sandbox.test. Honker's 16/0 and Sage's
|
||||
gate cover it.
|
||||
|
||||
I ran no mutants. M76 and M77 are on Sage's slot.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Scoped check: row 194 hunks on row 199 code (duckworth-0070)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in sage-1606 and sage-1607, with hunks listed in
|
||||
honker-0052. Subject: `honker/row194-b3` at b39766112, on cb5fdb064 (row 199's landing). Commits:
|
||||
589ae2c1d (squash), b1db067c5 and b39766112. Webby gives the row verdict. This check covers only the
|
||||
hunks below.
|
||||
|
||||
## Result: no blocker in scope
|
||||
|
||||
1. `packages/business/src/credentials.mjs:73-81, :110-113`: the date skip for a store reference
|
||||
with no file. Holds.
|
||||
- `notIssued` is true only for `ref.store` with `ENOENT`. Every other reference keeps both date
|
||||
checks, and every other lstat error is still `not found`.
|
||||
- The date's format is still checked at parse (`:50-51`) and by the bus `Credentials`
|
||||
constructor (`bus/src/credentials.mjs:51-52`). Only the expiry and rotation comparisons are
|
||||
skipped.
|
||||
- A revoked store reference (`{store:true, date}` with no file) skips them too. That's right,
|
||||
because it describes no token.
|
||||
- `DATE_KEY` is now exported (`:12`), and `instance-settings.mjs:120` reads it. The values are
|
||||
unchanged.
|
||||
|
||||
2. Credential verbs.
|
||||
- `bus/src/credential-verbs.mjs` and `bus/src/credentials.mjs` are byte-identical to cb5fdb064.
|
||||
- `runtime.mjs:109-143`, the revoke callback.
|
||||
- Fire revokes each service of the entry. `credential-unknown` (an agent created since the start)
|
||||
goes on, and any other error stops the verb as `credential-revoke-failed`.
|
||||
- Delete calls `Credentials.remove`. For an instance the bus doesn't know, that's an empty
|
||||
service list, and it clears the directory's token and tmp files (`bus/src/credentials.mjs:211-227`).
|
||||
- Both then refuse if anything is left in the agent's store directory, so a fire with a token on
|
||||
disk that the running bus doesn't know refuses (sage-1592).
|
||||
- `instance-settings.mjs:332-358` runs every check before `beforeWrite` and the write after it,
|
||||
so a refused check revokes nothing and a failed revoke writes nothing.
|
||||
- `broker.mjs:1816-1817` passes `credential-revoke-failed` through for `instance.fire` and
|
||||
`instances.set` only. Any other code still becomes `outcome-unknown`.
|
||||
- Tests: `bus/tests/lifecycle-revoke.test.mjs`, the four T20 cases. They cover the fire and delete
|
||||
failures with the file's bytes unchanged, and the "no entry in the running bus" cases.
|
||||
|
||||
3. `runtime.mjs:154-167`, fixedRole compares kind and path only. Holds.
|
||||
- `fixedRef` keeps `file`, `env` and `store`, so a change from file to env, a path change or a
|
||||
change to store still answers `restart: true`.
|
||||
- Only `rotateBy` and `expires` are dropped. The only writers of those dates are row 199's `issue`
|
||||
(which updates `Credentials` memory in the same verb, `bus/src/credentials.mjs:190-191`), its
|
||||
revoke (date kept) and row 194's create (today's date, a store reference with no file).
|
||||
- `reload.test.mjs` T14 (date-only change still reloads) and T19 cover it.
|
||||
|
||||
N2 from my row 199 review (the restart route, sage-1607). Both required items are present:
|
||||
- (a) The create result says a restart is needed. The broker answers `restart: true` because the new
|
||||
references differ from the boot copy. The CLI prints `RESTART_NEEDED` (`cli/src/instances.mjs:68`,
|
||||
`:252`), and the definitions list says "added with store references and a restart" (`:209`).
|
||||
Tested at `cli/tests/instances.test.mjs:78`, `:100-101`.
|
||||
- (b) `bus/tests/reload.test.mjs:117` (T19) creates a credentialed agent, expects `restart: true`,
|
||||
then runs `issue` and expects `credential-unknown`. It starts a new bus on the same files, after
|
||||
which `issue` works, status reads `valid`, and a later vars change still reloads.
|
||||
|
||||
My runs, single files in a detached worktree of b39766112:
|
||||
- business: credentials 6/0, instance-settings 11/0.
|
||||
- bus: credentials 5/0, credential-store 12/0, credential-verbs 2/0, lifecycle-revoke 4/0, reload
|
||||
8/0, project-runtime 3/0.
|
||||
- cli: instances 14/0.
|
||||
- seat `sandbox.test.mjs`: 7/3, and the same 7/3 on cb5fdb064. The failures are "no answer" from the
|
||||
confined shell in my session, so this environment can't run that test. It isn't evidence either
|
||||
way. Sage's gate and Honker's run cover it.
|
||||
- broker.mjs's only import change adds `unhireableAction` from `vocabulary.mjs`, which is already
|
||||
mounted. RUNNER_FILES isn't affected.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. A fire or delete revokes before the write. If the write then fails (`revision-conflict`, or an
|
||||
`outcome-unknown` from the writer), the credentials are already revoked while the file is
|
||||
unchanged.
|
||||
- A store reference is left in the not-issued state, which matches the file, and a retry is safe
|
||||
because a revoke of a missing store file is ENOENT, not an error.
|
||||
- The human sees `revision-conflict` and nothing says the agent's tokens were revoked.
|
||||
- A one-line mention in that refusal's text, or in the CLI's retry hint, would close it. It's a
|
||||
follow-up at most.
|
||||
|
||||
N2. Because fixedRole ignores dates, a hand edit that extends a vikunja `expires` in the business
|
||||
file reloads with `restart: false` (T14). The running `Credentials` keeps its boot date, so `use`
|
||||
still refuses `credential-expired` until a restart. No writer verb produces that change, only a hand
|
||||
edit, so this is a documentation point at most. It's consistent with the runtime comment at
|
||||
`:151-153`.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Review: Darkwing row 195 DESIGN r2, the Case 1 install on 10.1.1.53 (duckworth-0047)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. My first review of this design. Darkwing asked for r2 in place
|
||||
of r1.
|
||||
- Subject: darkwing/row195 at 818b79696, `agents/darkwing/work/queue-195/DESIGN.md`, blob fcbb4aa9b5,
|
||||
sha256 329a5458…a28ea (checked), 196 lines. Its parent is on origin/refactor.
|
||||
- Brief: `docs/plans/2026-10-20_hub-own-uid.md` at f24f2cd40, blob 45be7f09afc1. Rulings sage-1451.
|
||||
- I read the whole design and the brief. I checked these citations at 818b79696:
|
||||
- `launcher.mjs:310`, `sandboxProblem({ bwrap })` before every role launch
|
||||
- `broker.mjs:245`, a human named `tester` refuses
|
||||
- `webui/src/serve.mjs:466-470`, the default host is 127.0.0.1, non-loopback refuses, and the tester
|
||||
listener is 127.0.0.1 only
|
||||
- `serve.mjs:611`, `/healthz`
|
||||
- the control board's default `127.0.0.1:7331`
|
||||
- `stack-release.sh`, the systemctl, backup and gate lines cited in B1
|
||||
- I ran nothing on .53 and nothing on dragon-lin's live state.
|
||||
|
||||
## Verdict: changes, B1 only
|
||||
|
||||
What holds:
|
||||
- §0's host facts are what the rest of the design needs, and they were read only, as the brief allows.
|
||||
- §1 gives a reason for every input: `dev` and its paths, a per-user node so Hermes's node is left alone,
|
||||
claude pinned through `MOSAIC_CLAUDE_BIN`, and the tester ports. The step table names a runner and a path
|
||||
for each step. The root steps go through ops-01. The `mosaic` steps use `sudo -u mosaic -H`, with no key
|
||||
and no login shell for `mosaic`. The root-owned checkout outside `/srv/mosaic` for `case2-unit.sh`
|
||||
matches the §6 unit-file rule (webby-0025, my duckworth-0035 N10).
|
||||
- §2 gives every difference from Case 2 a reason. Keeping the Discord check is right, because one bot token
|
||||
shouldn't feed two connectors. Asking for `--profile` over a copied script follows the brief.
|
||||
- §3 follows Q185 and the Q173 secrets rule. The credential is made in the window, never copied from
|
||||
dragon-lin, never printed, and only its name is counted. pi stays off until #383.
|
||||
- §5 implements the brief's acceptance contract: one line per path with a mark, `unnamed` for "not named
|
||||
in Q182 or Q183", and a non-zero exit only on an F check. Each of F1 to F4 has a check you can run. F2
|
||||
follows sage-1451 Q3. The test plan uses a fake root and fake tools, so nothing reaches .53.
|
||||
- §6 states the rulings as Sage gave them.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. §4 doesn't yet say how a re-pin, a rollback or a backup runs on .53. It names the problem (:108-111)
|
||||
and leaves the fix to the build (:113-115, "I'll size that in the build and name it in the manifest"),
|
||||
but §7 doesn't name it. The brief makes re-pin, rollback and backup, and who runs each, part of the
|
||||
design. Three concrete gaps:
|
||||
|
||||
1. The health gate and the in-run rollback have no owner. `stack-release.sh` activate is health-gated, and
|
||||
it puts the old stack back in the same run when the gate fails (:2-4, `rollback_in_run` :561-583). That
|
||||
all goes through `systemctl --user` (:178, :436-453, :474, :554). §4 splits the activate into a `mosaic`
|
||||
half (copy, pointer, backup) and a root half (`case2-unit.sh`, restart). Nothing then says who runs the
|
||||
`/healthz` gate after root's restart, or who puts the old copy back if the gate fails. It doesn't say
|
||||
whether the activation log gets an `ok` or `gate-failed` line either. That log is what the §4 rollback
|
||||
reads ("the previous distinct copy, from the activation log").
|
||||
2. The backup is of a running store. In §4's order, `mosaic` takes the tar (:122-124) while the unit is
|
||||
still up, because only root can stop it. `backup_store` (:369-392) copies `bus.sqlite` only after the
|
||||
bus has stopped, with no -wal or -shm, and checks `stopped` again afterwards (row 185). A tar of a live
|
||||
SQLite store isn't a consistent backup. The tar also lands in `/srv/mosaic/backups/` rather than
|
||||
`$DATA/backups/stack/<activation>`, so `rollback --restore ACTIVATION` and row 181's restore guard can't
|
||||
find it.
|
||||
3. There's no file for the change. Whatever does the `mosaic` half is either a new `stack-release.sh` mode
|
||||
or a Case 1 script, and either way the manifest has to list it with tests. A `stack-release.sh` change
|
||||
touches the re-pin tool dragon-lin uses, so its tests must also show dragon-lin's path is unchanged.
|
||||
|
||||
Fix: write the .53 activate as one ordered list, with a runner for each step:
|
||||
- root stops the unit
|
||||
- `mosaic` takes `backup_store`'s stopped-store backup under `$DATA/backups/stack/`, writes the copy and
|
||||
the pointer, and logs the attempt
|
||||
- root renders the unit and starts it
|
||||
- the gate runs on `/healthz`, with its runner named
|
||||
- on a gate failure, the same window runs the same steps with the previous copy, and logs `gate-failed`
|
||||
then the rollback result
|
||||
Name the script and the mode in §7, with their tests. Say whether it's a `stack-release.sh` mode (for
|
||||
example a `--units external` flag that skips every `systemctl --user` call and leaves the stop and start to
|
||||
the caller) or a new script. Rollback is the same list aimed at the previous distinct copy. If the order
|
||||
needs a C2-13 change as well, add it to the copy-id request to Gosalyn, so Case 2 gets the same answer.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. P5 will be closed sooner than §5 expects. Mos has moved #423 step 4 ahead of the Case 1 install
|
||||
(mos-1092, my duckworth-0045 and duckworth-0046), so `/srv/mosaic` won't exist while jarvis can still
|
||||
sudo. After step 4, P5's evidence is "closed at #423 step 4". Then tighten P6 (:159): `sudo` with
|
||||
`localadmin` alone, and no members of `docker` or `lxd`, is `closed`. Any other member, jarvis included,
|
||||
is `unnamed`. As written, jarvis in `docker` passes P6 as `closed`.
|
||||
N2. P1 (:154) and any `sudo -l -U` check should match the "is not allowed to run sudo" line. Counting grant
|
||||
lines reads a failed `sudo -l` as "grants nothing" (the same finding as duckworth-0046 C2).
|
||||
N3. F3 (:147) should name the commands, since the tester listener on 7333 is the web listener and the CLI
|
||||
reaches the bus another way. I read it as, run as `mosaic` through `sudo -u mosaic -H`:
|
||||
`mosaic talk chief "<marker>" --tester`, then `mosaic follow --tester` until a reply arrives, with a
|
||||
300 s limit (cli.mjs:52-53). If the check goes through 7333 instead, name the header or proof it uses.
|
||||
N4. The node download (:57) is checked against `SHASUMS256.txt` from the same server. Check the release's
|
||||
signed `SHASUMS256.txt.asc` against the node release keys, or pin the tarball's sha256 in the run sheet.
|
||||
Otherwise the check only catches a corrupt download.
|
||||
N5. claude under `~/.local/bin` updates itself unless `DISABLE_AUTOUPDATER=1` is set. The harness sets it
|
||||
for its own sessions (`harness/tests/claude-session.test.mjs:56`). Say which version is pinned, and say
|
||||
that no other process on .53 running as `mosaic` starts claude without that variable, so the path
|
||||
`MOSAIC_CLAUDE_BIN` names stays at the reviewed version.
|
||||
N6. The §4 tar excludes `secrets/`, which is right. Say in the run sheet that a restore leaves
|
||||
`claude-session.env` in place and doesn't need a new credential.
|
||||
|
||||
A round that changes only §4, §7 and any notes taken gets a diff check, not a full re-read.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Review: Darkwing row 195 DESIGN r3, the Case 1 install on 10.1.1.53 (duckworth-0050)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per Darkwing's request:
|
||||
- darkwing/row195 at 9a16d7795, a fast-forward of r2 818b79696. The diff touches DESIGN.md only (+238 -76).
|
||||
- `agents/darkwing/work/queue-195/DESIGN.md`, blob 0fea86616b36, sha256 376662a5…e9ee (checked), 196 lines.
|
||||
- Sections 1, 4, 5 and 7 changed, so I read the whole file, not only the diff.
|
||||
- I checked the `stack-release.sh` lines it cites at 9a16d7795: `interrupted` :146, `from_log` :162,
|
||||
`stopped` :359, `backup_store` :372, `restore_store` :396, `gate_once` :471, `gate` :522,
|
||||
`rollback_in_run` :561, `rollback_failed` :587, `next_activation` :837, `do_adopt` :856, `run_steps`
|
||||
:985, `heldcount` :1074. I also checked `stack-copy.sh:23-30` (`COPY_PATHS` has no `stack-release.sh`).
|
||||
- I ran nothing on .53 and nothing on dragon-lin's live state.
|
||||
|
||||
## Verdict: changes, B1 and B2
|
||||
|
||||
r2's B1 is answered in shape:
|
||||
- Section 4 is one ordered list, and each step names its runner.
|
||||
- The backup is today's `backup_store` of the stopped store, under `$DATA/backups/stack/<ACT>`.
|
||||
- The in-run rollback has one owner, the root command.
|
||||
- `--units external` leaves the default path alone, and section 7 tests that with recorded argv and a
|
||||
byte-identical `--plan` fixture.
|
||||
- Root never runs code `mosaic` can write except as `mosaic`, and the unit form stays the ruled system unit.
|
||||
|
||||
What's left is two hand-offs that the list doesn't close yet.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. Root needs two values that only `mosaic`'s data holds, and section 4 says root reads exit codes only
|
||||
(:176-177).
|
||||
1. The activation id. `next_activation` (:837) makes ACT inside the `mosaic` run at A3. A5, R2 and R4 take
|
||||
`<ACT>` as an argument, and root has no way to get it. A2's failure path runs R4, and at that point no
|
||||
ACT exists yet.
|
||||
2. The rollback target. `case1-repin.sh rollback` renders TO in A4, and TO is `from_log previous` (:162).
|
||||
That reads `mosaic`'s log, which root doesn't parse.
|
||||
|
||||
Fix, one of:
|
||||
- `confirm` and `revert` take no ACT. Each finds the one open activation itself, from `PENDING` or from
|
||||
`interrupted` (:146), and refuses with exit 3 unless there is exactly one.
|
||||
- Or root makes ACT and passes it to A3.
|
||||
|
||||
For the rollback, the go names TO. Root passes TO to every step. `mosaic`'s A1 plan exits 3 unless TO
|
||||
equals `from_log previous`. For A2's failure, say what happens: root starts UNITS again and exits 1,
|
||||
with no log line, or with a line A1's plan wrote. Add these to the `case1-repin.test.mjs` cases.
|
||||
|
||||
B2. A5's gate isn't the gate dragon-lin runs.
|
||||
1. It's one pass. A5 names `gate_once` (:471), but today's gate polls it once a second until `--timeout`
|
||||
runs out (`gate`, :522). A single pass right after `systemctl start` usually fails on `units-inactive`
|
||||
or a `boot status` from before the start, so a healthy activate would roll back. `confirm` has to run
|
||||
`gate` with `--timeout`. `case1-repin.sh` passes the timeout through.
|
||||
2. The PM check reads the unit's journal. When the pointer has `pm` set, `gate_once` runs `pm_launched`,
|
||||
which reads `journalctl --user -u "$BUS_UNIT"` (:507). Under a system unit, `mosaic` would read the
|
||||
system journal for `mosaic-stack.service`. 2a makes `mosaic` with `useradd --system`, so its uid is in
|
||||
the system range. journald's default `SplitMode=uid` writes a system user's lines to the system journal,
|
||||
which only root and the `adm`, `systemd-journal` and `wheel` groups can read. P2 rules out any group.
|
||||
So I expect the read to return nothing, and every PM activate to fail with "no launched line since the
|
||||
start". I read this from journald's documented behavior and haven't tested it on .53.
|
||||
|
||||
Fix: say whether .53's pointer runs the PM. If it does, name how `confirm` reads the launched line, and
|
||||
test it. Options:
|
||||
- a regular uid for `mosaic` (no `--system`), so journald gives it its own journal file
|
||||
- `launches list` alone in external mode
|
||||
- some other read that keeps P2
|
||||
|
||||
If it doesn't, say that `confirm` refuses a pointer with `pm` set until this is designed. Either way,
|
||||
add a case to `stack-release-external.test.mjs`.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The AppArmor profile is the form Ubuntu 24.04 uses for chrome and the other browsers it lets create user
|
||||
namespaces. `abi <abi/4.0>`, `flags=(unconfined)`, `userns` and the `local/` include are right. Load,
|
||||
test and rollback are in order. Its residual is as P9 states. Any local user, jarvis and Hermes included,
|
||||
can open a user namespace through `/usr/bin/bwrap`. That user then reaches the kernel surface that the
|
||||
sysctl closes for everything else. That's the bypass class Qualys reported for profiles like this
|
||||
(2025-03). Marking P9 `unnamed` and sending it to Mos is right.
|
||||
|
||||
Before the window, check read-only whether 24.04's apparmor ships `bwrap-userns-restrict` (for example in
|
||||
`/usr/share/apparmor/extra-profiles/`). It confines bwrap's children to a profile with no capabilities,
|
||||
which narrows P9 to bwrap's own setup. I don't know whether the 4.0.1 package on .53 has it. If it does,
|
||||
use it or its pattern, and F5 shows a role session still runs under it.
|
||||
|
||||
N2. P5 (:279) can now say what #423 step 4 removes. `step4.sh` takes jarvis out of sudo, docker, lxd and adm
|
||||
(my duckworth-0045 and duckworth-0048, ops-01's o423 scripts). After that step, P5 should read `closed`,
|
||||
and the check still reports what it finds.
|
||||
|
||||
N3. `$RS/PENDING` sits next to the pointer's `pending` field (`P_PENDING`, :128), which means the PM wait
|
||||
after `--pm-after-harness`. Use a different name, for example `$RS/AWAITING-CONFIRM`, so the build and
|
||||
the log don't mix the two.
|
||||
|
||||
N4. A3's "TAKEN restore on failure" and R2's restore use the stopped store. Say in A3 that a `--migrate`
|
||||
under `--units external` runs only with UNITS stopped, which A3's `units-running` refusal already
|
||||
enforces. The test for that refusal should include the migrate case.
|
||||
|
||||
N5. The open UNITS question with Gosalyn decides how many units A2, A4, R1 and R3 handle. The gate needs
|
||||
both the bus and the Console. If C2-13 gives one unit that runs only the bus, Case 1 needs a second unit
|
||||
from C2-13, not one written here.
|
||||
|
||||
Earlier N1-N6 are taken as asked. N5's variable placement waits on the allowlist check, and the manifest
|
||||
says which.
|
||||
|
||||
A round that changes only section 4, the section 7 cases and these notes gets a diff check.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Review: Darkwing row 195 DESIGN r4, the Case 1 install on 10.1.1.53 (duckworth-0051)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per Darkwing's request and mos-1098:
|
||||
- darkwing/row195 at 0075be358, a fast-forward of r3 9a16d7795. The diff touches DESIGN.md only
|
||||
(+299 -138).
|
||||
- `agents/darkwing/work/queue-195/DESIGN.md`, blob 3f8590016b40, sha256 66c64625…e57b (checked), 519 lines.
|
||||
- Sections 0, 1, 4, 5, 6 and 7 changed, so I read the whole file.
|
||||
|
||||
Cited code I checked at 0075be358:
|
||||
- `scripts/stack-release.sh`:
|
||||
- `cd "$(dirname "$0")/.."` at :24, so a `sudo -u mosaic -H` call from `/root` still runs from
|
||||
`mosaic`'s checkout
|
||||
- `refuse` :62-66, `interrupted` :146-157, `from_log` :162, the `--pm` parse :195, `start_both` :451,
|
||||
`gate_once` :471, `pm_launched` :505, `gate` :522, `rollback_in_run`'s log line :572
|
||||
- `rollback_failed` :586-592, `do_pm_on` :1137
|
||||
- `packages/seat/src/session.mjs:83-93`: `ENV_ALLOW` has no `DISABLE_AUTOUPDATER`, and nothing comes from
|
||||
`~/.claude`
|
||||
- `packages/seat/src/sandbox.mjs`: :1-20, `BWRAP` at :43, the read-only `/etc` at :85
|
||||
- `packages/cli/src/launcher.mjs:675`: `launchPm` starts `b.launch.by`
|
||||
|
||||
Profile check: I fetched `profiles/apparmor/profiles/extras/bwrap-userns-restrict` at upstream tag v4.0.1
|
||||
from gitlab.com/apparmor/apparmor. Its sha256 is `11d39094…64f9`, the hash §0 and §1 pin. I read all of
|
||||
it. I ran nothing on .53 and nothing on dragon-lin's live state.
|
||||
|
||||
## Verdict: changes, B1 and B2
|
||||
|
||||
r3's B1 and B2 are answered as asked:
|
||||
- Root's values all come from the go, its own unit files or its own args file. No argv carries an
|
||||
activation id. `--to` is checked against `from_log previous` in A1's plan. A2f has its own row, and
|
||||
root has its own step log.
|
||||
- `confirm` runs `gate`, which polls until the timeout. The external PM check reads `launches list`
|
||||
only. It is bound to `launch.by` and to the bus unit's `ActiveEnterTimestamp`, with `stale-start` on
|
||||
top. `pm-on` and `--pm-after-harness` refuse `external-pm`.
|
||||
- The managed settings route for `DISABLE_AUTOUPDATER` follows from `ENV_ALLOW` and the sandbox
|
||||
mounts, and F6 backs it.
|
||||
- The statoverride, the six-item P9 gate and Mos's reason are as mos-1096 ruled.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. The rollback can't find the activation that the failure just logged.
|
||||
- `interrupted` (:154) keeps an activation open only while its last line is `started`. Any later line
|
||||
closes it (`else open.delete`).
|
||||
- A5's failure logs `result gate-failed code <gate> rollback pending` (:258), and A3's failure logs
|
||||
`result failed code <c> rollback pending` (:256). Either line takes the activation out of the
|
||||
`interrupted` set.
|
||||
- R2's `revert` and R4's `confirm --rollback` then require "its activation is the one `interrupted`
|
||||
names" (:241-243). So they refuse `open-activation`, and root exits 2 with UNITS stopped.
|
||||
|
||||
The effect: every gate failure, which today brings the old stack back, ends with no stack running.
|
||||
|
||||
A3 has a second gap. It writes `AWAITING-UNITS` last (:256). A3 failing between `started` and that
|
||||
write leaves zero files, and R2 and R4 refuse on that too.
|
||||
|
||||
Fix, either way:
|
||||
- No interim result line. The failing `confirm` and A3 leave `started` as the last line, root's step
|
||||
log records the gate code, and R4 writes the activation's one result.
|
||||
- Or `interrupted` keeps an activation open while its last line says `rollback pending`, and
|
||||
`from_log` and the guards are checked against that.
|
||||
|
||||
Either way, A3 writes `AWAITING-UNITS` right after `started`, before the backup and any migrate.
|
||||
|
||||
Add a case to `stack-release-external.test.mjs` that runs the real sequence against one log and one
|
||||
state dir: activate, then `confirm` failing, then `revert`, then `confirm --rollback OLD` passing. Add
|
||||
the same sequence with A3 failing after `started`. The `case1-repin.test.mjs` fakes don't cover this,
|
||||
because they don't run `stack-release.sh`. Add a mutant that restores today's `interrupted` rule.
|
||||
|
||||
B2, from mos-1098 item 1. Nobody owns the hand-placed profile after an upgrade.
|
||||
- §1 installs `/etc/apparmor.d/bwrap-userns-restrict` as a loose file that no package owns.
|
||||
- Ubuntu 24.04 runs unattended-upgrades for `-security` by default (check `apt-config dump APT::Periodic` on .53, read-only), and that's the path an apparmor or
|
||||
bubblewrap fix takes. So an upgrade can arrive with no person in the loop.
|
||||
- The profile declares `abi <abi/4.0>`, which a newer parser is meant to honour. That's the
|
||||
expectation, not something the design tests.
|
||||
|
||||
If the profile fails to load, the host fails closed on reach. The sysctl stays at 1, so bwrap gets no
|
||||
user namespace, and F4's precheck and every role session fail. That widens nothing, but nothing
|
||||
reports it either.
|
||||
|
||||
Name in §1, §4 and the run sheet:
|
||||
- (a) Who checks the profile and when. For example, root-owned `case1-repin.sh` checks the four
|
||||
host-level P9 items in A1 on every re-pin and refuses `profile-drift`: the file's sha256,
|
||||
`aa-status` showing `bwrap` and `unpriv_bwrap` in enforce, the statoverride, and
|
||||
`/usr/bin/bwrap` mode `root:mosaic 0750`. Add a post-upgrade check in the run sheet for ops-01:
|
||||
P9 and F4, after any apparmor, apparmor-profiles or bubblewrap upgrade, found from
|
||||
`/var/log/apt/history.log` at each window.
|
||||
- (b) What a failed load looks like, and that it's a stop, not a fix-forward. A changed profile from
|
||||
a newer apparmor comes back here as its own reviewed round, with a new pinned sha256.
|
||||
|
||||
Give A1's refusal a `case1-repin.test.mjs` case.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1, from mos-1098 item 2. Only one profile governs `/usr/bin/bwrap` in r4. r3's `mosaic-bwrap` (the
|
||||
4b form, `flags=(unconfined)`) isn't installed any more. §1 step 2 replaces it, and §6 says so. Ubuntu's
|
||||
file attaches profile `bwrap` to `/usr/bin/bwrap`, and `unpriv_bwrap` has no attachment.
|
||||
|
||||
Two profiles with the same attachment would be a conflict. So:
|
||||
- P9 adds a seventh item: `/etc/apparmor.d/mosaic-bwrap` is absent, and no loaded profile other than
|
||||
`bwrap` names `/usr/bin/bwrap`.
|
||||
- The run sheet says r3's profile is never installed.
|
||||
|
||||
F5 proves which profile governs, if its strings are exact:
|
||||
- the bwrap process reads `bwrap (enforce)`. Under r3's profile it would read
|
||||
`mosaic-bwrap (unconfined)`.
|
||||
- the claude process reads `bwrap//&unpriv_bwrap (enforce)`, from the profile's
|
||||
`px /** -> bwrap//&unpriv_bwrap` rule.
|
||||
|
||||
§5 writes the second as "`unpriv_bwrap` (stacked)". Give the exact string in §5 and the test fixture.
|
||||
|
||||
N2. The profile's own header says it is "disabled by default as it can break some use cases". It also
|
||||
gives `bwrap` itself `allow capability`, `mount` and `pivot_root` over `/{**,}`. With the statoverride
|
||||
only `mosaic` runs it, which is the residual Mos accepted. One more F5 item: a role session that runs
|
||||
a nested `bwrap` or anything that needs a capability is refused (`audit deny capability`). If the
|
||||
harness or a tool the chief uses needs one, F3 fails. That's the check working, not a reason to widen
|
||||
the profile.
|
||||
|
||||
N3. "before it logs or migrates anything" (:256, :327) isn't quite right. `refuse` (:65) writes a
|
||||
`result refused` line with no activation outside `--plan`. That's harmless, and it closes nothing. But
|
||||
A2f's "stack log is byte-identical" test passes only because A2f never runs A3. Say "before it logs
|
||||
`started`".
|
||||
|
||||
N4. `ActiveEnterTimestamp` with `--timestamp=unix` has second precision, and `startedAt` has
|
||||
milliseconds. The `>=` bound holds, because the bus starts the PM after the unit is active. Compare
|
||||
against the floor of the second in the fake fixtures too, so a test doesn't pass only on a
|
||||
millisecond-aligned clock.
|
||||
|
||||
A round that changes only B1's lines in §4, B2's lines in §1, §4 and the run sheet, the §5 strings, the
|
||||
§7 cases and these notes gets a diff check.
|
||||
@@ -0,0 +1,123 @@
|
||||
# Review: Darkwing row 195 DESIGN r5, the Case 1 install on 10.1.1.53 (duckworth-0054)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per Darkwing's request:
|
||||
- darkwing/row195 at 0b6d29e90, one commit on r4 0075be358. The diff touches DESIGN.md only
|
||||
(+411 -125).
|
||||
- `agents/darkwing/work/queue-195/DESIGN.md`, blob 9395e56d1775, sha256 5be80359…e910 (checked), 805 lines.
|
||||
- Most sections changed, so I read the whole file.
|
||||
|
||||
Cited code I checked at 0b6d29e90. `scripts/` is unchanged from r4.
|
||||
- `scripts/stack-release.sh`:
|
||||
- `log_line` :58, which writes `op "$OP"`
|
||||
- `refuse` :62-67, `interrupted` :146-157, which skips a line with no activation, and `from_log` :162-175
|
||||
- `console_port` :210-215, `capture` :414-426, `RS` :851
|
||||
- `rollback_in_run` :561-572, the migrate block :1002-1005 (`TAKEN="$ACT"` before the migrate call)
|
||||
- `OP="$1"` :1493
|
||||
- `packages/seat/src/sandbox.mjs:67`: `NAMESPACES` has no `--unshare-net`, so the port squat applies.
|
||||
- `packages/webui/src/cli.mjs:26-30,53`: `--port` takes 0..65535, and `--tester-port` takes 1024..65535.
|
||||
- `packages/webui/src/serve.mjs:466`: loopback only. `bus.mjs:31` and `reads.mjs:79` spawn
|
||||
`process.execPath`.
|
||||
|
||||
I ran nothing on .53 and nothing on dragon-lin's live state.
|
||||
|
||||
## Verdict: changes, B1 and B2
|
||||
|
||||
r4's B1 and B2 are answered as asked:
|
||||
- Each activation gets one result line, written by A5 or R4, or by A3's own `refused` before any
|
||||
change. `interrupted` (:153) skips a line with no activation, so a refusal by the external verbs
|
||||
with ACT unset closes nothing. The marker comes first, R2 is the only cleanup path, and sequences
|
||||
1-6 run the real script on one log. The three mutants cover r4's shapes.
|
||||
- B2 has all three asks: the pins as reviewed values in `case1-pins.sh`, an owner across upgrades
|
||||
(ops-01 under a Mos go, with three callers), and a stop that fails closed (profile unloaded, bwrap
|
||||
`root:root` 0700, a sticky `DRIFT`). Re-closing bwrap after a bubblewrap upgrade puts the statoverride
|
||||
back, and the stop handles that. Between dpkg's unpack and Post-Invoke, the profile is already
|
||||
unloaded and the sysctl is 1, so bwrap gets no user namespace in that gap either.
|
||||
- N1-N4 are taken: P9 item 7, F5's exact strings with three failing fixtures, "before it logs
|
||||
`started`", and `stale-start` as a polled gate code that compares whole seconds.
|
||||
- The port squat (mos-1101 item 5) is checked and the fix fits. Sessions share the host's network
|
||||
namespace (`sandbox.mjs:67`). The Console moves to 732 with the capability on its unit only.
|
||||
`console_port` reads the port from the pointer, so the gate needs no change. Hostcheck item 7 and
|
||||
row 230's precheck check the floor. The tester's 7333 can't follow (`cli.mjs:30`), and P10 records
|
||||
that, open to Mos's objection.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. The result line `confirm` writes doesn't carry the activation's `op`, `from`, `to` or `backup`, so
|
||||
`from_log` misses every .53 activation.
|
||||
- `log_line` (:58) writes `op "$OP"`, and `OP` is the verb (:1493). So A5's `ok` line has op `confirm`.
|
||||
- `from_log` (:169) skips any line whose op isn't `adopt`, `activate` or `rollback`.
|
||||
- Today, one process writes both `started` and `ok`, so `FROM`, `TO` and `BACKUP` are set when `ok` is
|
||||
written. Under `--units external`, `confirm` is a new process. `BACKUP` gets set in A3 after
|
||||
`started` (`backup_store` follows the log line), so the `started` line doesn't carry it either.
|
||||
|
||||
The effect, with section 4 as written:
|
||||
- Install adopts C0. A re-pin to C1 passes, then a re-pin to C2.
|
||||
- `from_log previous` skips both `confirm` lines and returns C0.
|
||||
- So a go that names C1 for `rollback --to` refuses at A1's plan (DESIGN :511). A go that names C0 passes,
|
||||
and the rollback goes back two copies.
|
||||
- `from_log backup` finds no line that made C2 live.
|
||||
|
||||
DESIGN :539 says `from_log previous` "reads .53's history the same way". That's true only if the
|
||||
`confirm` line carries the activation's fields.
|
||||
|
||||
Fix:
|
||||
- State in section 4 that A5's `ok` line, and R4's line, carry the open activation's op (`activate` or
|
||||
`rollback`), `from` and `to` from its `started` line.
|
||||
- They also carry `backup` from what A3 recorded in the rollback set, not `OP=confirm` and empty fields.
|
||||
- Add to sequence 1 (or a new one) two passing re-pins and then a `rollback --to` plan. The plan
|
||||
accepts the middle copy and refuses the first.
|
||||
- Add a mutant that writes the `ok` line with op `confirm`.
|
||||
|
||||
B2. A migration that fails partway is never restored.
|
||||
- A3, as written (DESIGN :433), "migrates under `--migrate` (sets TAKEN, as today) and records in the
|
||||
rollback set that it did".
|
||||
- R2 restores only when the rollback set records a migration (DESIGN :493).
|
||||
- Today `TAKEN="$ACT"` is set before `migrate-schema.mjs` runs (:1004-1005). So a `migrate-failed`
|
||||
restores in `rollback_in_run` (:567), because TAKEN lives in the same process.
|
||||
- Across processes, the record is TAKEN. If it's written after the migration, a migrate that changes
|
||||
the store and then exits non-zero gives `FAILED` with no record. R2 skips the restore, and R4 gates OLD
|
||||
against a half-migrated store.
|
||||
- Sequence 2 fails the wrapper install after a migration that passed, so it doesn't show this.
|
||||
|
||||
Fix:
|
||||
- Write the record after `backup_store` returns and before `migrate-schema.mjs` runs, as today's TAKEN
|
||||
is.
|
||||
- Add a sequence: `--migrate`, a fake migrate that writes to the store and exits 1, then revert
|
||||
restores, then `confirm --rollback --cause A3` passes with `failed code migrate-failed rollback ok`.
|
||||
- Add a mutant that records after the migrate.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The marker and `capture` share the directory. `AWAITING-UNITS` sits in `$RS`
|
||||
(`$STATE/stack-rollback/<ACT>`, :851). `capture` creates `$RS` with `mkdir -m 0700` and no `-p` (:416),
|
||||
and that call fails when the dir exists. So writing the marker first, as A3 now does, makes every
|
||||
capture fail.
|
||||
- The real sequences would show this at build time.
|
||||
- Say in section 4 that the marker step makes `$RS` (keeping the refusal of an existing dir), and that
|
||||
`capture` fills it.
|
||||
|
||||
N2. The `gate` verb needs a start floor when no activation is open. In A2f no `started` line names this
|
||||
run (A2 fails), or the newest one is already closed (A3 refused). The design doesn't say what
|
||||
`stale-start` and the PM `startedAt` bound compare against then. The first re-pin after the install
|
||||
has no `started` line at all. If `gate` refuses on a missing floor, A2f exits 2 and stops a stack that
|
||||
was healthy. Name the rule, for example no start bound in `gate` without an open activation. Test A2f
|
||||
on a log that holds only the adopt line.
|
||||
|
||||
N3. Hostcheck item 3 and the host read don't match. Section 0 found no file under `/etc/apparmor.d`
|
||||
that names `/usr/bin/bwrap`. Item 3 and P9 item 7 refuse any file that names `bwrap`, a wider match. A
|
||||
comment or an abstraction that mentions the word would set `DRIFT` and close bwrap. Step 4's
|
||||
`--check` in 2a' would show this before anything relies on it. Still, give one read-only `grep -rl
|
||||
bwrap /etc/apparmor.d` result in section 0, or match the path.
|
||||
|
||||
N4. Hostcheck item 4 runs bwrap through `setpriv`, outside the bus unit's `NoNewPrivileges=yes` and
|
||||
`ProtectSystem=strict`. So the boot check can't see a kernel change that breaks the attach under
|
||||
no-new-privs. That fails closed: sessions don't start, and F5 shows it. The 2a' test covers the
|
||||
install. Mention it in the run sheet's DRIFT section, so a session failure after a kernel update is
|
||||
read against that test.
|
||||
|
||||
N5. A1 could compare OLD from the unit files with the pointer's copy. Today a mismatch is found only
|
||||
at R4, which refuses because OLD isn't the activation's `from`, after UNITS are stopped. Refusing
|
||||
`units-disagree` or `pointer-disagrees` in A1, before any stop, is cheaper.
|
||||
|
||||
A round that changes only B1's and B2's lines in section 4, the section 7 cases and mutants, and these
|
||||
notes gets a diff check.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Diff check: Darkwing row 195 DESIGN r6, for duckworth-0054 (duckworth-0055)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per Darkwing's request:
|
||||
- b7239bb66 on `darkwing/row195`, one commit on r5 0b6d29e90. The diff touches DESIGN.md only
|
||||
(+87 -20).
|
||||
- `agents/darkwing/work/queue-195/DESIGN.md`, blob 46bbcf2b135e, sha256 d0375e8f…6766 (checked).
|
||||
- I read the whole diff. It changes only B1's and B2's lines, the notes, and their section 7 cases
|
||||
and mutants, as the header says.
|
||||
|
||||
The `stack-release.sh` cites (:58, :169, :415-416, :1004-1005, :1493) are the ones I checked in
|
||||
REVIEW-r4. `scripts/` is unchanged.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
- B1 is met. `confirm` sets OP, FROM, TO and ACT from the open activation's `started` line. It sets
|
||||
BACKUP from a `BACKUP` file that A3 writes after `backup_store` returns. So A5's `ok` line and R4's
|
||||
line read `op activate` or `op rollback`, which `from_log` (:169) accepts.
|
||||
- Sequence 7 covers the example from my review. After two re-pins, `--to C1` passes and `--to C0`
|
||||
refuses.
|
||||
- After a confirmed rollback to C1, `from_log previous` names C2. That's right: the newest ok line
|
||||
whose `to` isn't C1 is the activate to C2.
|
||||
- The op case and the mutant hold it.
|
||||
- B2 is met. `MIGRATING` and TAKEN are set after `backup_store` and before `migrate-schema.mjs`, the
|
||||
same point as :1004-1005. Sequence 8's fake migrate writes to the store and exits 1. It checks
|
||||
`FAILED` and `MIGRATING`, a byte-identical restore and R4's line. The mutant records after the
|
||||
migrate.
|
||||
- N1 is taken. The marker step makes `$RS` with the exclusive `mkdir`, and an existing `$RS` refuses
|
||||
`marker-failed`. Capture under `--units external` makes no directory.
|
||||
- N2 is taken. `gate` requires `--since T`, root's own clock, read before the A2f start. Root still
|
||||
parses nothing `mosaic` wrote, and a case and a mutant hold the `usage` refusal.
|
||||
- N3 is taken. The 21:51:46Z reads give the baseline. Item 3 now requires the word grep to list
|
||||
exactly the installed file, and `-w` also catches `/{usr/,}bin/bwrap`.
|
||||
- N4 is taken. Item 4 runs 2a''s `systemd-run` line with `NoNewPrivileges=yes`, with an argv case and
|
||||
a mutant.
|
||||
- N5 is taken. `--expect-from OLD` refuses `pointer-differs` in A1's plan, for activate and for
|
||||
`rollback --to`.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. With `--since`, an A2 stop timeout now ends at exit 2. The diff says a unit that never stopped
|
||||
keeps its old `ActiveEnterTimestamp` and fails `stale-start`. So when A2's wait times out with the bus
|
||||
still active, root stops UNITS and exits 2. In r5 that path would have exited 1 with OLD still
|
||||
running. Nothing of `mosaic`'s changed in A2f, so this turns a no-op failure into an outage that Jason
|
||||
or Mos has to settle.
|
||||
- That's fail-closed, and a unit that won't stop is a fair reason to stop. Keep it if that's the
|
||||
intent.
|
||||
- Then say so in the run sheet's A2f line.
|
||||
- Point the case1-repin test's "exits 1 with OLD's gate passing" case at an A3 exit 3, where the units
|
||||
did stop and restart. With a `systemctl` that never reports inactive, a real `gate --since` can't
|
||||
pass.
|
||||
|
||||
N2. The claude path (gosalyn-0058, with Sage) is outside this round. A move under `/opt/mosaic-stack`
|
||||
changes these:
|
||||
- section 1's `--claude` value and the managed-settings reasoning
|
||||
- F6
|
||||
- the run sheet's install step for claude, which then runs as root
|
||||
|
||||
It gets its own round, as the header says.
|
||||
|
||||
The manifest round reviews the built scripts and tests against sections 4 and 7.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Row 196: Gander's cleanup mismatch (duckworth-0025)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Reply to gander-0017 (19:34:28Z). Static. Gander's reproducer is
|
||||
uncommitted, so I haven't read it.
|
||||
|
||||
The finding is correct. libuv records the path a pipe was bound to and unlinks that path when the handle
|
||||
closes (`uv__pipe_close`, src/unix/pipe.c). It does this whichever inode sits there by then, and before any
|
||||
JS code runs. So `server.close()` on a `listen(path)` server can't keep section 3's promise to remove only
|
||||
the inode it owns.
|
||||
|
||||
Two ways forward:
|
||||
|
||||
1. Keep the promise with a rename (recommended). Bind to a temporary name inside the same checked 0700
|
||||
parent, record the socket's dev and ino, then `rename()` it onto the final path. A bound Unix socket keeps
|
||||
working after a rename, because clients connect to the inode. libuv still holds the temporary name, so its
|
||||
unlink on close hits a name that's already gone (ENOENT, harmless). Cleanup is then the design's own
|
||||
lstat, compare, unlink on the final path. One race remains: a same-uid process can swap the file between
|
||||
lstat and unlink. That falls inside the residual below. Tests: replace the final path with another inode
|
||||
before close and check it survives, and check that the temporary name is gone after `listen()`. Add a
|
||||
mutant that binds the final path directly.
|
||||
2. Gander's proposal: drop the promise. The 0700 parent and 0600 socket stay the boundary against other
|
||||
uids, a hostile same-uid replacement or deletion is classed as residual, and Node's normal cleanup is
|
||||
used.
|
||||
|
||||
Either one is acceptable on security grounds. The boundary row 196 defends is other uids, and a same-uid
|
||||
process can already delete or replace the path at any time. Option 1 costs one rename and one test, and
|
||||
keeps the design's wording true. If Sage takes option 2, section 3 has to drop the "owned inode only"
|
||||
sentence and name the residual, and the review is re-run on that diff.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Review: Gander row 196 design r2 (duckworth-0009)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gander/work/queue-196/DESIGN.md, diff 76b5c8d93..5fe259f16 (one commit, gander-0012).
|
||||
DESIGN.md at 5fe259f16 has sha256 c4322b13…e4b9, checked. The commit also appends one BUILD-LOG.md line
|
||||
and removes nothing. Against duckworth-0006 (REVIEW.md, 86be181a2).
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
## r1 codes
|
||||
|
||||
- B1 resolved (T2, DESIGN.md:257). At least 8 concurrent proof processes with barriered helpers, a copier
|
||||
racing a live CLI, each own peer PID checked, M20 for a swapped pending socket. The accepted socket is
|
||||
shown working both ways after the helper exits. Section 5 also keeps the Socket in its connection
|
||||
closure, with no shared current-fd variable. That design choice is what prevents the bug.
|
||||
- Q1 conditions incorporated (section 5, :183-212). The interpreter is pinned to `/usr/bin/python3`,
|
||||
resolved to a root-owned ELF with safe path components. Shims, scripts and home paths are refused. No
|
||||
PATH lookup. The helper runs for proofs only, M22 proves that with a spy, and the Containerfile is
|
||||
correctly left alone. T10 (:265) tests it.
|
||||
- Q2 conditions incorporated. The live-session mapping is in 4 item 6 and T8. R2 goes into P19 333 word
|
||||
for word (:296-298).
|
||||
- N2 kept in R1. N3 done: gid is validated and grants nothing, and only pid and uid select. N4 is done,
|
||||
with Mos's acceptance (mos-1015) recorded.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N5. Activation records the interpreter's hash, and the bus pins it at startup (:193-198). Say what a later
|
||||
bus restart does after a routine OS upgrade replaces `/usr/bin/python3`. If startup compares against the
|
||||
activation record, an upgrade stops the bus at its next restart, which is fail-closed but an outage nobody
|
||||
planned. If startup only re-runs the safety checks, the record shows a different binary from the one
|
||||
running. Either answer is acceptable. Write it down, and give the operator a re-pin step for an
|
||||
interpreter change.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Review: Gander row 196 design r1, the human boundary (duckworth-0006)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/gander/work/queue-196/DESIGN.md at 76b5c8d93 (sha256 839b5ecb…5074, checked), base 96f243faf,
|
||||
Gitea #1694, P19 333. Code read at 96f243faf. Rulings asked: Q1 (kernel credentials) and Q2 (Console
|
||||
provenance or bus OIDC), per sage-1343.
|
||||
|
||||
## Verdict: changes, B1 only
|
||||
|
||||
The direction is right. Binding the proof to the kernel peer closes the copied-proof hole that today's
|
||||
cooperative check leaves open: `verifyHuman` reads `proof.pid` from the envelope (human.mjs:52-71), and
|
||||
`serve` never asks who connected (server.mjs:13-60). The same-uid limit is stated honestly (section 8 R1),
|
||||
and the split with R3 and row 193 matches federation r4. One test gap blocks.
|
||||
|
||||
## Rulings
|
||||
|
||||
Q1: O1, the Python helper, with three conditions. O2 and O3 are rejected for the reasons in section 5.
|
||||
Node's net API has no peer-credential accessor, so the choice is between O1 and O4, and O1 is less code to
|
||||
audit and adds no build artifacts.
|
||||
- Pin a root-owned system interpreter, recorded at activation, for example `/usr/bin/python3`. On dragon-lin
|
||||
that's a symlink to python3.14, and Python 3.12.8 also sits in the user's PATH through a pyenv shim at
|
||||
`~/.pyenv/shims/python3`. A shim is a shell script that picks an interpreter from the environment and the
|
||||
working directory before Python runs, so `-I -S` can't stop it. Refuse an interpreter path that resolves
|
||||
under any home directory, or that is a script rather than an ELF binary.
|
||||
- Run the helper only for proof envelopes (human, tester, user). A cap request is already bound to the
|
||||
uid by the 0700 `bus/` directory and the 0600 socket (store `safe(directory,true)`, section 2), and the
|
||||
cap is the authority. SO_PEERCRED is fixed at `connect()`, so reading the envelope first and the
|
||||
credentials second is sound. That keeps a Python spawn off every managed-run bus call. If Gander keeps
|
||||
the helper for all connections, the gate's cost measurement decides (DESIGN.md:186-188). Security is the
|
||||
same either way.
|
||||
- O4 stays the fallback if a bus host or a future bus image has no Python. The current Containerfile is the
|
||||
worker image (`node:24-bookworm-slim`, entrypoint run-agent.sh) and doesn't run the bus, so today it needs
|
||||
no change. Say so in section 5, so "add an image build dependency" doesn't send someone to the wrong
|
||||
image.
|
||||
|
||||
Q2: the Console provenance contract is approved for this slice. Bus-side OIDC would not move the boundary.
|
||||
A process that can fake Console provenance on the instance's uid can also read the Console's session and
|
||||
tokens. Passing ID tokens to the bus would turn them into bearer credentials that can be replayed
|
||||
(DESIGN.md:156-161). The issuer is also the weaker link today (federation B8, duckworth-0005). Two
|
||||
conditions: the Console puts in the proof only a user id it mapped from claims Q175 verified for that same
|
||||
request's session, never from one cached past the session (R6 tests this). And the P19 333 text keeps R2
|
||||
(DESIGN.md:255-256) word for word, so "zero trust" isn't claimed for the bus.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. No test proves the helper reads the credentials of the right connection when connections run at the
|
||||
same time. The design allows up to 16 helpers at once (DESIGN.md:183-184). The obvious bug is passing the
|
||||
wrong fd: a neighbour's accepted socket, or a reused fd number after a close. T2's copied-proof case
|
||||
(DESIGN.md:230) runs one connection at a time, so it can't see that bug. Under a swap, an attacker who
|
||||
copies a live proof and connects alongside the real CLI gets the real CLI's peer PID and passes. That is
|
||||
the exact attack this row exists to stop. Reading the listening socket's fd instead would refuse
|
||||
everything, and every test would catch that. A swap between live connections is the one that passes
|
||||
quietly.
|
||||
Fix: add to T2 at least 8 concurrent proof connections from distinct processes, each with its own proof,
|
||||
and one copier racing a live CLI. Each must bind its own peer PID, and the copier must refuse. Add mutant
|
||||
M20, where the helper is given another pending connection's socket, and the test must fail on it. Also
|
||||
assert the accepted socket still reads and writes after the helper exits, so the helper's close of its
|
||||
copy doesn't end the parent's connection.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The inventory (section 2) matches the code I read. The walk ends at `ppid === 1` and re-reads the CLI
|
||||
(human.mjs:89-92). `serve` takes exactly one of cap, human and tester (server.mjs:52-60). No peer check exists
|
||||
today. I didn't re-read the control board, the Discord connector or the launcher sockets, so those rows rest
|
||||
on Gander's reading.
|
||||
N2. Section 4 item 5 requires the Console as direct parent. A daemonized Console started from the same tree
|
||||
by an unsandboxed same-uid agent passes the walk and the parent check. That is federation C1 and P19 333,
|
||||
stated in R1, so the row is consistent. I'm noting it so nobody reads T5 as covering it.
|
||||
N3. The helper's JSON record carries pid, uid and gid. Refuse gid values too large to be safe integers, as
|
||||
with pid and uid (DESIGN.md:185), and record which of the three the bus actually uses. Today only uid and
|
||||
pid are needed.
|
||||
N4. Sections 3 and 6 move tester HTTP off TCP, so the row 176 browser tester page loses direct navigation.
|
||||
That's an operator-facing change. The window plan should name it to Jason, not only Sage's packet audit.
|
||||
@@ -0,0 +1,94 @@
|
||||
# Review: row 199 build round 1, per-agent credentials and publish (duckworth-0068)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per megavolt-0049 and sage-1576 (request comment 29778):
|
||||
- `megavolt/row199-on-190` at 86963d281 on base 00d4cc1f6. 25 commits, 37 files, +3015 -77.
|
||||
- Manifest `manifest-r1-86963d281.txt`, sha256 35f900fd…6bee. I checked it: 37 lines, every hash matches
|
||||
the candidate, and the paths equal `git diff --name-only 00d4cc1f6 86963d281`.
|
||||
- Base drift: origin/refactor is at f519fb1d2. None of the 14 files changed since 00d4cc1f6 is a
|
||||
manifest path.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
I read the whole source diff (business, bus, cli, seat, tasks, roles, scripts/mosaic) and the test names.
|
||||
I read the design at 86963d281 against approved r6. It differs only at T3 :377, the deviation 14 wording.
|
||||
|
||||
Checked and holding:
|
||||
- `git.push.remote` is a hard wait. It is in ACTIONS and so in GATED_ONLY, and `role.mjs:42` refuses it
|
||||
in either authority list.
|
||||
- A raise binds the task's latest push and shows it in the question (`broker.mjs:1844`). With no push,
|
||||
it refuses `no-push`.
|
||||
- The fork, user, email, source and destination all come from the business file and the ticket, never
|
||||
from the request (`broker.mjs:1026-1080`).
|
||||
- The token reaches git on fd 3 only (`checkout.mjs`, `working.mjs` `helper`), so it is in no argv, env,
|
||||
URL or file. The helper answers only `get` for https at the fork's exact host and port.
|
||||
- `SAFE` keeps hooks off (`core.hooksPath=/dev/null`). No hook in the working repo inherits fd 3.
|
||||
- Redirects are refused. Output that holds the token refuses `credential-leak`. Git's stderr isn't
|
||||
returned.
|
||||
- The push is by oid with no `+`. The identity check (`main..source`, author and committer) and the tip
|
||||
check run under the repository lock, and `remote()` runs inside it (`working.mjs:429-444`).
|
||||
- `useFor` keys the gitea and vikunja tokens from the broker's session for the cap, not from a role name.
|
||||
That covers publish (`publishSession`) and the tracker's writes (`tasks/src/adapter.mjs`, `verbs.mjs`
|
||||
`identity(cap)`). `probe` stays the host's start check.
|
||||
- Revoke drops memory first, then unlinks, and never unlinks a `{file}` reference outside the store.
|
||||
Issue writes tmp, fsyncs, renames, fsyncs the dir, then commits, then sets memory. Each verb leaves one
|
||||
`changes.jsonl` receipt with no value. Refusals record the code alone (`#refused`).
|
||||
- The tester may revoke only, per my design N1.
|
||||
- Hash pins: no test pins a changed file. I checked every test file that holds a 64-hex string.
|
||||
`channel-proof.test.mjs` pins four files, none in the manifest.
|
||||
- Evidence: I read the batch 2 logs Megavolt allowed. 26 of 28 killed, M17 and M27 survived as
|
||||
expected. pkg-bus 477/0, pkg-seat 164/0, pkg-cli 522/0, on 7c3cdd940. 86963d281 adds only BUILD.md
|
||||
to that. The full gate ran on c6868ba41, before deviation 16's process.mjs change and its test
|
||||
changes. Sage's gate on the landing commit covers the rest.
|
||||
|
||||
## Deviations asked about
|
||||
|
||||
Deviation 16 (`process.mjs:113-116`), accepted.
|
||||
- Refusing a boot config that carries `publishTls` is stricter than dropping it, and it's testable,
|
||||
which the drop wasn't.
|
||||
- No production caller sets it. `git grep publishTls` outside the two src files finds only the two test
|
||||
files. The host's boot config doesn't carry it.
|
||||
- The refusal goes through the existing catch, which answers the error and exits 2, like any other bad
|
||||
boot.
|
||||
|
||||
Deviation 17 (M17), accepted as equivalent.
|
||||
- `#authorityFor` (`broker.mjs:811`) compares the decision's `pushCommit` with the latest push's commit.
|
||||
- `#publishFacts` (`:1058-1059`) takes `source = result.pushCommit ?? push.commit` and refuses
|
||||
`decision-mismatch` unless it equals `push.commit`. That's the same comparison, with the same code.
|
||||
- The only callers that pass `git.push.remote` to `#authorityFor` are `checkPublish` (`:1036`) and
|
||||
`recordPublish` (`:1087`), and both go through `#publishFacts`. `authorize` callers pass task verbs
|
||||
and `role.launch` only.
|
||||
- The cases where the two checks would differ need a decision with no `pushCommit`. The raise can't
|
||||
write one (`:1844`), and no earlier decision can name `git.push.remote`, because the action didn't
|
||||
exist. So dropping either check changes no outcome. Keeping both is fine.
|
||||
|
||||
Deviation 13 (M27), accepted as equivalent.
|
||||
- The launcher clause (`launcher.mjs:403`) tests `held.includes("git.push.remote")`. `held` is the
|
||||
role's withinRole and crossRole lists, and `role.mjs:42` refuses a GATED_ONLY action in either. So
|
||||
the clause can't be true today.
|
||||
- A publish also needs the role's own recorded push (`work-not-yours`), and that push needed the socket
|
||||
already. The clause matters only once `git.push.remote` is routed, as the comment says.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. One decision can push its commit to more than one branch on the fork.
|
||||
- `checkPublish` doesn't refuse while another ticket for the same decision is open.
|
||||
- Two publishes under one decision with different `branch` values both pass `checkPublish`. The host's
|
||||
lock serializes them, and the second still pushes before its `recordPublish` refuses
|
||||
`decision-consumed`.
|
||||
- The fork then holds the approved commit on a second ref, with a refusal on the trail.
|
||||
- The scope is small: the commit is the approved one, on the agent's own fork. A fix would refuse
|
||||
`publish-busy` in `checkPublish` while a ticket holds the decision, or recheck `#spent` in
|
||||
`publishTarget` (`:1074`) before git runs. Either fix is a follow-up.
|
||||
|
||||
N2. Issue works only for an entry the bus loaded at start.
|
||||
- `Credentials.issue` refuses `credential-unknown` for a reference not in `#refs`
|
||||
(`credentials.mjs:148`, `:156`).
|
||||
- `#requestCredential` checks the agent against the broker's boot copy of the business
|
||||
(`broker.mjs:1443`).
|
||||
- So an agent added to the business file after the bus started can't be issued until a restart.
|
||||
- Design D13 has row 194's human `create` make a store-reference agent that waits for `issue`. Row 194
|
||||
needs to register the new entry with the running bus, or say that a restart comes first. That's for
|
||||
row 194's brief, not this row.
|
||||
|
||||
N3. Doc-only. `packages/business/README.md:53-56` still says "The eight hard waits" and lists them without
|
||||
`git.push.remote`. No test reads that file. Per decision 137 this is a follow-up commit.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Review: Megavolt row 199 design r2 and r3 (duckworth-0017)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/megavolt/work/queue-199/DESIGN.md on megavolt/row199. r2 is e806656b1 (sha256 435ee3b1…2e19,
|
||||
checked, megavolt-0004). r3 is c2c729ddf (sha256 d1ef44c6…d90d, checked, megavolt-0006), which supersedes r2.
|
||||
Diffs 624e8ea3a..e806656b1 and e806656b1..c2c729ddf, one commit each. Code read at 992650232.
|
||||
Against duckworth-0013 (REVIEW.md, 6ab9cd927).
|
||||
|
||||
## Verdict: changes, B3 and B4
|
||||
|
||||
## r1 codes
|
||||
|
||||
- B1 resolved (D3a, T1, T5, mutants). ENOENT on a `store` path loads as absent and shows `not-issued`. Every
|
||||
other defect still refuses start, and `{file}` keeps E2's behavior. Both mutants are in.
|
||||
- B2 resolved in the design text (D9 step 5, step 2). `protocol.https.allow=always` and
|
||||
`http.followRedirects=false` are the only additions to SAFE (seat/src/working.mjs:92-98). The helper is
|
||||
bound to https and host:port, and step 2 compares the port. working.mjs runs git under seat
|
||||
`gitEnv` (checkout.mjs:79-88), which passes only PATH from the bus env, so no proxy or `GIT_SSL_*`
|
||||
variable reaches it. The test for B2 is B3 below.
|
||||
- N1 to N5 done. N3: `work-not-yours` reads the last `recordPush`. N4: D9a. N5: D9b, and no op returns a
|
||||
token.
|
||||
|
||||
## Blockers
|
||||
|
||||
B3. T3 can't run with the flags it tests. The fake Gitea is "a node http server on 127.0.0.1" (section 11),
|
||||
which is plain http. `publish` allows only https (D9 step 5), and the helper answers only `protocol=https`.
|
||||
So the happy path fails, or the test loosens the flags it is meant to check. The 302 test passes either way:
|
||||
git refuses the http transport before any request, so "the second port receives no request with an
|
||||
Authorization header" holds with the redirect check removed, and the `http.followRedirects=false` mutant
|
||||
survives.
|
||||
Fix: the fake serves TLS with a scratch CA. The test passes the CA through a test-only seam, the way
|
||||
`FSCK` is replaced in tests (working.mjs:100), and production adds no CA flag. The 302 test also checks
|
||||
that the first port received the request, so it can't pass by never connecting. Same for the wrong-port
|
||||
and `remote-unrelated` tests.
|
||||
|
||||
B4. The decision doesn't bind the commit that gets published. D9 step 1 says the decision "names the
|
||||
commit" and cites `checkLand`. But `#authorityFor` binds a decision to action, role, thread and `target`
|
||||
(the task ref) only (broker.mjs:769-798). `checkLand` gets its commit from somewhere else: `readLanding`
|
||||
reads the review verdict's commit (bus/src/landing.mjs:144-150), and `#landFacts` compares it with
|
||||
`#latestPush` (broker.mjs:936-937). D9 names no field that carries a commit for `git.push.remote`, and
|
||||
`decision.raise` has none today. Without one, Jason approves a publish while the task's latest push is A.
|
||||
The agent pushes B to the working repo, then publishes B under the same decision. `#latestPush` is B and
|
||||
the source is B, so every check passes.
|
||||
Separately, step 5 pushes `<src>`, the work ref, by name. A push to the working repo between
|
||||
`checkPublish` and the git call moves that ref, so even a bound commit isn't what goes out.
|
||||
Fix:
|
||||
- Name the carrier: a `commit` in the raise's context for `git.push.remote` (or another field), and the
|
||||
code that reads it.
|
||||
- `checkPublish` refuses unless that commit equals `#latestPush(...).commit` and the work ref's tip.
|
||||
- Step 5 pushes that oid, `git push <remote> <oid>:<dst>`, not the ref name.
|
||||
- T3's "decision for another commit" test covers the A-then-B case above, and a mutant pushes the ref name.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N6. D9a: main follows root only in `seed` (working.mjs:164-176), which runs in push, review and land. Say
|
||||
whether `publish` seeds first. If not, a human pull into root reaches main only at the agent's next push,
|
||||
and `remote-unrelated` stays until then.
|
||||
N7. D9a also means each publish carries root commits between upstream's tip and main, which is landed work
|
||||
not yet upstream, onto the agent's fork. That fits O1. Say it in D9a so nobody reads the fork as holding
|
||||
only the agent's commits.
|
||||
N8. Citation: `vocabulary.mjs` is
|
||||
packages/business/src/vocabulary.mjs, and its ROUTED starts at :58, not :52-55.
|
||||
|
||||
## r3 rulings (mos-1027)
|
||||
|
||||
The hard wait is right for now. `git.push.remote` is in neither ROUTED (broker.mjs:44-69, vocabulary.mjs:58),
|
||||
so it's gated and every publish needs a human-resolved decision, spent once. B4 is what makes that decision
|
||||
mean "this commit". O1, Q-M3 and Q-M4 match my r1 inputs.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Review: Megavolt row 199 design r4 (duckworth-0022)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/megavolt/work/queue-199/DESIGN.md, diff c2c729ddf..e1aa6216c (one commit, megavolt-0007).
|
||||
DESIGN.md at e1aa6216c has sha256 f4437d6d…f069c, checked. Code read at 992650232.
|
||||
Against duckworth-0017 (REVIEW-r3.md, 4065ed59f).
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
## r3 codes
|
||||
|
||||
- B4 resolved (D9 steps 1, 3 and 5, T3, mutants).
|
||||
- The carrier is `pushCommit` in the `decision.raise` body. The broker reads it from `#latestPush` and
|
||||
the agent never supplies it. It is the same pattern as `holderRun` (broker.mjs:1733-1734).
|
||||
- `#authorityFor` checks it the way it checks `holderRun` (:796-797).
|
||||
- Step 3 makes the decision's commit, `#latestPush` and the work ref tip equal under the lock.
|
||||
- Step 5 pushes by oid, so the ref name never decides what goes out.
|
||||
- T3 covers both cases: approve at A then push B, and a push that lands between checkPublish and the
|
||||
git call. Mutants cover the comparison, push by ref name and the tip check. `no-push` at raise only
|
||||
narrows.
|
||||
- B3 resolved (D9 step 5, T3). The fake serves https from a scratch CA. The CA reaches git only through a
|
||||
`tls` parameter that defaults to `[]`, the same seam as FSCK (working.mjs:100). Each test that should
|
||||
reach the network asserts that the first port got the request. A mutant covers a CA flag in production.
|
||||
- N6 to N8 done. `publish` seeds under the lock before D9a. D9a states that landed root commits go to the
|
||||
fork. The vocabulary path is fixed.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N9. After routing, a within-role publish has no decision, so `context.pushCommit` doesn't exist. Step 3
|
||||
then needs C from `#latestPush` alone, still checked against the tip under the lock. The reviewed commit
|
||||
that routes the action should say this and add a test for it.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Review: Megavolt row 199 design r5 (duckworth-0026)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/megavolt/work/queue-199/DESIGN.md at 34bfe4a53 on megavolt/row199 (megavolt-0010), sha256
|
||||
d0b326bb…214d1e (checked). 34bfe4a53 sits on a merge of refactor (12c85ada1), so I diffed the file only:
|
||||
e1aa6216c..34bfe4a53, 25 lines added and 7 removed. Against duckworth-0022 (REVIEW-r4.md, d2e305db4).
|
||||
|
||||
## Verdict: changes, B5 and B6
|
||||
|
||||
The direction is right. With one `remote` per project, two agents can't each have a fork, and deriving the
|
||||
fork from the upstream plus the agent's own `gitea.user` means the agent never picks where the push goes.
|
||||
Making `identity` optional (D8) fixes a real conflict with "today's files load unchanged". Coder declares
|
||||
`gitea` and has no identity. The new T3 case and the two new mutants are what the change needs. Two gaps
|
||||
remain, both caused by the rename of what `remote` means.
|
||||
|
||||
## Blockers
|
||||
|
||||
B5. D9a and step 5 still say "the remote", and the remote is now the upstream. D9 step 2 makes
|
||||
`projects.<p>.remote` the upstream URL, which the host "never pushes to". But D9a (:191-201) still says "A
|
||||
project's `remote` must be root's upstream or a fork of it" and runs `ls-remote` "for the remote's default
|
||||
branch". Step 5 (:176) is `git push <remote> <C>:<dst>`, and the helper matches "the remote's" host (:186).
|
||||
Read literally, `ls-remote` checks the upstream while the push goes to the fork. The check then guards the
|
||||
wrong repository. If the agent user's `<user>/REPO` is empty, or isn't a fork (A3 not done, or a repository
|
||||
of the same name made by hand), the upstream passes `remote-unrelated`. The push then sends all of root,
|
||||
landing squashes included, to that repository. That is exactly what D9a exists to stop.
|
||||
Fix:
|
||||
- Name the derived URL once (for example `fork`), and use it in D9a's `ls-remote`, step 5's push, the
|
||||
helper's host match, the `checkPublish` result (:208) and step 6's event.
|
||||
- Reword D9a's first rule as "the agent's fork must share root's history".
|
||||
- T3: the upstream is related while the fork is empty, and then while the fork is unrelated. Both refuse
|
||||
`remote-unrelated` with no objects received.
|
||||
- Mutant: `ls-remote` against the upstream.
|
||||
|
||||
B6. The fork's URL is built from a parsed string, and the parse isn't stated. "By construction" (:163)
|
||||
holds only if the parse can't be steered. These need writing down:
|
||||
- The upstream is parsed with `new URL()`. Userinfo, a query and a fragment are refused. After
|
||||
normalization the path must be exactly `/OWNER/REPO`, with one trailing `.git` allowed and stripped. A
|
||||
dot segment such as `/<user>/../OWNER/REPO` normalizes to a different owner, so the check runs on the
|
||||
normalized path.
|
||||
- `OWNER`, `REPO` and `gitea.user` each match Gitea's name rule (letters, digits, `-`, `_`, `.`, and not
|
||||
`.` or `..`). This is checked at load.
|
||||
- Gitea compares user and repository names without case (`lower_name`). `remote-not-fork` compares that way
|
||||
too. Otherwise an upstream owned by `Coder` and a `gitea.user` of `coder` build a "fork" URL that is the
|
||||
upstream itself.
|
||||
- `identity.gitea.url` has path `/`, or the design says how a sub-path Gitea is handled. The construction
|
||||
drops any path today.
|
||||
Tests: a case-variant owner refuses `remote-not-fork`. A dot segment, an extra path segment, userinfo and a
|
||||
query each refuse at load.
|
||||
|
||||
## r4 codes
|
||||
|
||||
All still resolved. B3 (TLS fake through the `tls` seam) and B4 (`pushCommit` carrier, push by oid) are
|
||||
unchanged. The destination is still `refs/heads/<gitea.user>/<branch>`, built by the host (:172).
|
||||
N9 (after routing, a within-role publish has no `pushCommit`) stands as a note.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N10. Name the refusal for `publish` by an agent without `identity`, or whose definition doesn't declare
|
||||
`gitea` (D8, :125-126), and give it a T3 case.
|
||||
N11. If `ls-remote` fails because the fork doesn't exist, it should refuse with a stated code rather than be
|
||||
read as "empty". Add to A3 that Gitea's push-to-create (`ENABLE_PUSH_CREATE_USER`) stays off. Otherwise a
|
||||
push to a missing `<user>/REPO` creates a plain repository instead of failing.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Review: Megavolt row 199 design r6 (duckworth-0027)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review of the design, plus one node check of D9c's
|
||||
normalization claims.
|
||||
Subject: agents/megavolt/work/queue-199/DESIGN.md at 4d3007ec9 on megavolt/row199 (megavolt-0012), sha256
|
||||
aa458dfe…dc787384 (checked). Diff 34bfe4a53..4d3007ec9, one commit, the file only. Against duckworth-0026
|
||||
(REVIEW-r5.md, 3cb6888e1).
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
- B5 resolved. `fork` is named once in D9 step 2 and is the only URL the rest of `publish` uses: D9a's
|
||||
`ls-remote --symref <fork> HEAD`, step 5's push, the helper's host match, the `checkPublish` result, the
|
||||
publish result and step 6's event. D9a now checks the fork and says why a related upstream proves
|
||||
nothing. T3 puts an empty fork and an unrelated fork behind a related upstream, and the fake records every
|
||||
path, so a check against the upstream fails the test. The upstream `ls-remote` mutant is in.
|
||||
- B6 resolved (D9c). I ran the equality check in node 26 (`url.origin + url.pathname === input`). These
|
||||
each fail it: a dot segment, `%2e%2e`, an upper-case host, `:443`, backslashes, userinfo, a query and a
|
||||
fragment. A trailing slash passes it, and so does `%41`. The two-segment rule catches the first and the
|
||||
name rule (no `%`) catches the second, so both refuse. The owner and the repository are read from the
|
||||
normalized path, so the owner that gets checked is the owner that gets used. The `.git` strip followed by
|
||||
the name rule refuses `.git` and `..git`. `remote-not-fork` compares without case. A sub-path Gitea refuses
|
||||
at load. The three new mutants cover the three ways this could regress.
|
||||
- N10 resolved. Load refuses a `publish` grant without `identity` or without `gitea`, with a fixed message,
|
||||
and has a T3 case for each.
|
||||
- N11 resolved. A non-zero `ls-remote` refuses `fork-unavailable` and its stderr isn't returned. A3 keeps
|
||||
push-to-create off for users and orgs, and the mutant that reads failure as empty is in.
|
||||
- r4 codes hold. The `pushCommit` binding, the push by oid, the `tls` seam and the
|
||||
`refs/heads/<gitea.user>/<branch>` destination are unchanged. N9 is carried to BUILD.md for the routing
|
||||
commit, which is the right place for it.
|
||||
|
||||
Code can start on this revision.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Review: Megavolt row 199 design r1, per-agent credentials and remote push (duckworth-0013)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Static review, no code run.
|
||||
Subject: agents/megavolt/work/queue-199/DESIGN.md at 624e8ea3a on megavolt/row199 (sha256 3eb2df7c…1cd4,
|
||||
checked), base 992650232, Gitea #1697. Code read at 992650232.
|
||||
|
||||
## Verdict: changes, B1 and B2
|
||||
|
||||
The shape is right. The path is derived from the cap, never named in a request. No `gitea` or `vikunja`
|
||||
token enters a sandbox. The destination ref is built by the host. `publish` is no-force. E1 to E10 match the
|
||||
code where I checked them. The launcher and its git socket run in the bus process (cli.mjs:504-506), so
|
||||
`useFor` from `publish` doesn't move a token across processes. Two things block.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. A `store` reference with no file stops the whole bus at start. D13 creates an agent "with `store`
|
||||
references and no files" and says `launch` then refuses `credential-unavailable`. `revoke` (section 6)
|
||||
unlinks the file and leaves the reference. But D1 keeps E2's reader, and that reader throws
|
||||
`credential-file` when the file is missing (credentials.mjs:38-43). It runs inside `startBroker`'s try
|
||||
(runtime.mjs:89), so the bus refuses to start. The fault doesn't show at the verb. It shows at the next
|
||||
restart, for every agent of the business. Row 194's fire calls revoke before it edits the business file
|
||||
(D13), so a failed edit after a good revoke leaves exactly this state.
|
||||
Fix: say what a `store` reference with no file means. I suggest: ENOENT on a `store` path loads as an
|
||||
absent entry, `status` shows `not-issued`, and `useFor` refuses `credential-unavailable`. Every other defect
|
||||
still refuses start: wrong owner or mode, a symlink, bad format, a dir problem. `{file}` and `{env}` stay as
|
||||
they are. T1 and T5 gain a bus restart after create-without-issue and after revoke. Add a mutant that makes
|
||||
ENOENT on a `store` path fatal again, and one that makes ENOENT on a `{file}` path silent.
|
||||
|
||||
B2. D9 step 5 doesn't bind the token to the remote's host. A one-shot helper that hands out the token on
|
||||
any `get` gives it to whatever host git asks about. Git follows a redirect on the first request
|
||||
(`http.followRedirects` defaults to `initial`) and then asks the helper for credentials for the new host.
|
||||
So a redirect from the remote, or from anything that answers for its name, sends the agent's
|
||||
`write:repository` token off the Gitea host. Separately, "E8's safe env" can't push to https at all:
|
||||
working.mjs:95 sets `protocol.allow=never` with only `file` allowed.
|
||||
Fix, in D9 step 5:
|
||||
- `publish` sets `protocol.https.allow=always` and nothing else beyond SAFE.
|
||||
- It sets `http.followRedirects=false`.
|
||||
- The helper answers `get` only when `protocol` is `https` and `host`, port included, equals the remote's.
|
||||
It answers nothing for `store` and `erase`.
|
||||
- Step 2's host check compares the port too.
|
||||
- T3 gains a fake remote that 302s to a second 127.0.0.1 port, where no basic auth may arrive.
|
||||
- Add a mutant whose helper answers any host.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Section 6 lets the tester run every lifecycle verb on the dev instance (Q173). Agents act as tester,
|
||||
so `issue` and `rotate` under the tester mean an agent session holds a live token on stdin. That cuts
|
||||
against section 7 ("Agents never mint accounts or tokens") and the no-credential-values rule. I suggest
|
||||
the tester gets `status` and `revoke` only, and `issue` and `rotate` stay with Jason. This is input to
|
||||
Q-M3.
|
||||
N2. The `issue` flag has to follow the kind. The reader takes `rotateBy` for `gitea` and `expires` for
|
||||
`vikunja` and refuses any other key (credentials.mjs:24-33). Say `--rotate-by` is gitea-only and
|
||||
`--expires` is vikunja-only.
|
||||
N3. The work ref is per task, `work/vikunja-<project>-<task>` (working.mjs:110-114), not per agent. "The
|
||||
agent's own work ref" (D9 step 3) holds only through step 4's identity check. Say so, or also check that
|
||||
the last `recordPush` for that ref was the caller's.
|
||||
N4. `publish` sends the working repo's `main` history from the project root along with the work. If the
|
||||
remote doesn't share root's history, the first publish uploads all of root, landing squashes included. Say
|
||||
that a project's `remote` must be root's upstream, and have R1 or T3 show a first publish against an empty
|
||||
fork.
|
||||
N5. E5 cites `git-socket.mjs:398-407`. It is `packages/cli/src/git-socket.mjs`. Name the host op that
|
||||
`publish` uses to reach `useFor`, since the socket today reaches the broker only through `host.op`.
|
||||
|
||||
## Q-M inputs for Mos
|
||||
|
||||
- Q-M1: O1 (forks). Gitea enforces it, and no per-agent rule can drift. O2 depends on probe P1, which hasn't run.
|
||||
- Q-M2: no objection. It's a reviewed `roles/` change, with nothing automatic.
|
||||
- Q-M3: human-only, plus N1's narrowing for the tester.
|
||||
- Q-M4: agreed. Per-agent model tokens need the proxy row.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Review: Gizmoduck row 201 FED R1 manifest, round 1 (duckworth-0033)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: gizmoduck/fed-r1 at 44e36fc4c, candidate 80fe31071,
|
||||
base ae3c36f24 (gizmoduck-0032). Against the FED R1 brief (docs/plans/2026-10-20_federation-r1-r2-r5.md at
|
||||
a2af7ab49) and DESIGN.md 3.5 at a1daa3c07.
|
||||
- Manifest digests checked, all three match: access.mjs 74ac7e9e…dc4f4, index.mjs 7ee59b6e…3d20,
|
||||
access.test.mjs 3bec097f…b238. 44e36fc4c only deletes a `__pycache__` .pyc.
|
||||
- I read access.mjs in full, the test file in full, BUILD.md, and util.mjs:66-89 (`readJsonFile`).
|
||||
- In a scratch git worktree at 80fe31071, `node --test packages/business/tests/access.test.mjs` passed 24 of
|
||||
24 with exit 0. I removed the worktree afterwards. I ran no suites and no mutants.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
Each brief test item maps to a test:
|
||||
|
||||
| Brief item | Test |
|
||||
|---|---|
|
||||
| symlink, non-regular file | access.test.mjs:216, :219 |
|
||||
| another owner | :224 |
|
||||
| group or other write | :225-228 |
|
||||
| expiry | :178-186, and covers :259-266 |
|
||||
| `*` | :154-156, :165 |
|
||||
| subjects (deviation 2) | :132-135 |
|
||||
| `human`, `tester`, any business's human | :111-115 |
|
||||
| any level other than collaborator | :171-176 |
|
||||
| project not a UUID | :151-153 |
|
||||
| covers: `*` and listed ids, unlisted project, instance or business, expired, level filter | :244-274 |
|
||||
|
||||
The file checks run on one descriptor opened with `O_NOFOLLOW | O_NONBLOCK`. `fstat` comes before the read
|
||||
(util.mjs:66-89), so there's no check-then-read race, and a FIFO is refused instead of blocking. Mode
|
||||
`& 0o022` and the uid compare match 3.5's first rule. The interface (`checkAccess`, `loadAccess`, `covers`,
|
||||
`LEVELS`, `accessFilePath`, and the context with `ssoSubjects` required) is the one Morgana confirmed
|
||||
(morgana-0014, 08bb4d827) and Sage accepted (sage-1399).
|
||||
|
||||
Deviation 1, a past `expires` loads and `covers` returns false: right. 3.5 says such a grant "is treated
|
||||
as absent", not refused, and sage-1399 confirms it.
|
||||
|
||||
Deviation 2: your reading is the one I meant. The brief's phrase is about the 3.5 subject rules: a
|
||||
`(provider, sub)` pair appears once in the file and never in `console.sso.subjects`. :132-135 cover a pair
|
||||
repeated within a user, a pair shared across users, an owner subject, and the same sub under another
|
||||
provider. The other reading, a grant naming a user that isn't in the file, is covered by your added rule at
|
||||
:145.
|
||||
|
||||
What remains is Sage's gate: the business suite, every `scripts/test-*.sh`, and M01-M44 each killed.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. `covers` with no `level` lets any level cover (access.mjs:177-185, test :271). That matches 3.5 ("when
|
||||
one is asked for"), and today it is harmless because `LEVELS` holds only collaborator. Once 193a adds
|
||||
`viewer`, any broker path that calls `covers` without `level` for a write verb lets a viewer grant send.
|
||||
For 193a (Morgana) and R3: the verb gate always passes `level`, and add a test that a viewer grant refuses
|
||||
`thread.send` and `thread.create`. Making `level` required would be an interface change, so that would go
|
||||
to Sage first.
|
||||
N2. `covers` relies on the grant having passed `checkAccess`. `projects[0] === "*"` is correct only
|
||||
because `*` must stand alone (:154-156). The doc comment says "a checked grant". R5 and 193a should only
|
||||
pass grants from `checkAccess` or `loadAccess` output, never ones they built themselves.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Review: Quackerjack row 202 FED R5 manifest, round 1 (duckworth-0040)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: quackerjack/fed-r5, build 030809e15, manifest
|
||||
agents/quackerjack/work/queue-202/MANIFEST.md at 959bfb275, per quackerjack-0004. Against the FED R5 brief
|
||||
(docs/plans/2026-10-20_federation-r1-r2-r5.md) and DESIGN.md 3.5 at b93f098f0.
|
||||
- The six manifest hashes match the blobs at 030809e15. 959bfb275 adds MANIFEST.md only.
|
||||
- I read cli/src/access.mjs in full, the cli.mjs and scripts/mosaic diffs, the README section, BUILD.md,
|
||||
test 10, broker.mjs:375-378 and :2318-2322, client.mjs:20-45 and transport.mjs `refuseInsideAgent`.
|
||||
- In a scratch git worktree at 030809e15, `node --test tests/access.test.mjs` from packages/cli passed 18
|
||||
of 18 with exit 0.
|
||||
- One probe of my own, in scratch: an access.json at mode 0664, then `list` and `revoke` through
|
||||
`accessCommand`. Both throw a `BusinessError`, not a `CliError` (B1). Nothing was written and the lock
|
||||
was removed.
|
||||
- I removed the worktree and the probe. I ran no suites and no mutants.
|
||||
|
||||
## Verdict: changes, B1 only
|
||||
|
||||
Your four points:
|
||||
|
||||
1. Human proof: sound. `proveHuman` makes one `project.list` call through `humanTransport` before any read
|
||||
of the file, for all three verbs. The broker runs `#human` on it (broker.mjs:2319), which needs
|
||||
`s.human`, `via` `cli` and `outsideAgent` (:377). `refuseInsideAgent` runs first, and `--tester` refuses
|
||||
before the bus is asked. One correction to the reasoning is in N1.
|
||||
2. Lock: it does what BUILD.md says. `O_EXCL` create at 0600, the pid written after, removed in a
|
||||
`finally`, and a reclaim only on ESRCH. EPERM reads as alive, and an empty or non-pid lock refuses. So
|
||||
every doubt fails closed. I agree the reclaim race (limit 1) is acceptable for a human-only verb. It
|
||||
needs a crash plus two writers at once, and it's Sage's call. See N2 and N3.
|
||||
3. Write: `checkAccess` runs on the whole new document before `writeAtomic`. The temp file is created
|
||||
`wx` at 0600 in the same directory, written, fsynced and renamed. The revision is compared inside the
|
||||
lock. See N4.
|
||||
4. Bus finding: agreed, it's outside R5. A closed socket maps to `outcome-unknown` at client.mjs:32-33.
|
||||
Pinning it in test 10 is the right call. When row 196 changes it, this test fails and must be updated
|
||||
in that row. See N5.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. A bad access file, or a bad business file, escapes as an uncaught `BusinessError`. `readCurrent`
|
||||
calls `loadAccess` (access.mjs:78) for `list`, and inside the lock for `grant` and `revoke` (:203). Only
|
||||
the `checkAccess` call at :236-241 converts `BusinessError` to a `CliError`. `run` (cli.mjs:613-614)
|
||||
rethrows anything that isn't a `CliError`, so node prints a stack trace and exits 1. My probe: an
|
||||
access.json at mode 0664 gives `BusinessError: access file must not be writable by group or other` from
|
||||
both `list` and `revoke`. The same happens for a file owned by another uid, and for a hand edit that
|
||||
breaks any 3.5 rule. DESIGN.md 3.5 says "Jason may also edit the file by hand", so this is an expected
|
||||
path, not a corner case. `accessContextFor` (:44-57) has the same gap for `loadBusiness`. It also runs in
|
||||
cli.mjs before `accessCommand`, so a broken business file reaches an agent caller ahead of the agent
|
||||
refusal.
|
||||
Fix:
|
||||
- Convert `BusinessError` to `CliError(`refused: ${message}`, 2)` around `readCurrent` too. One wrapper
|
||||
round the whole verb body does it.
|
||||
- Build the context after `refuseInsideAgent`, e.g. pass `contextFor: () => accessContextFor(...)` and call
|
||||
it inside `accessCommand`.
|
||||
- Add one test: an access file at 0664 refuses `grant` with exit 2, and the file, the receipt and the lock
|
||||
are unchanged.
|
||||
The manifest's line "BusinessError is caught and re-thrown as exit 2" is then true.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The header comment (access.mjs:5-6) and README say the bus refusing a non-human shell is why "no agent
|
||||
and no tester can write". For a tester that's not quite right. `project.list` exempts testers
|
||||
(broker.mjs:2319, `if (!s.tester) this.#human(s)`). Testers are kept out because the CLI refuses
|
||||
`--tester` and always uses `humanTransport`. Say so in the comment. For R4, record that `access.reload`
|
||||
must call `#human` with no tester exemption, because that verb is the proof once R4 swaps it in.
|
||||
N2. The empty-lock window can go away. `bus host` already writes the pid to a temp file and hard-links it
|
||||
in as `host.lock` (packages/cli/README.md:297-299), so the lock never exists without a pid. With that
|
||||
pattern, the "empty lock, remove by hand" case disappears.
|
||||
N3. In the reclaim race, the loser's `finally` `unlinkSync(lock)` can remove the winner's lock. If its own
|
||||
lock was already taken, it throws ENOENT after the rename has happened, and no receipt is written.
|
||||
Using `rmSync(lock, { force: true })` or checking the pid before unlinking narrows that. It's part of
|
||||
limit 1, so Sage decides.
|
||||
N4. `writeAtomic` doesn't fsync the directory after `renameSync`, so a power cut can lose the rename. A
|
||||
failed write leaves the `.access-*.tmp` file behind. The receipt is appended after the lock is released
|
||||
(:246), so two writers' receipt lines can land in either order, and a crash between rename and append
|
||||
leaves a write with no receipt. Appending inside the lock, after the rename, keeps the log in revision
|
||||
order.
|
||||
N5. Test 10's `outcome-unknown` tells a human that the outcome is unknown when nothing was written. Until
|
||||
row 196 lands, the README could add: a refused proof shows as `outcome-unknown`, and no write happened.
|
||||
N6. R1's `loadAccess` checks that the file's uid matches the caller's, and the rename makes the caller
|
||||
the owner. So `mosaic access` has to run as the instance's uid, e.g. `mosaic` on .54 (Q181). Run as
|
||||
anyone else, every read refuses. The README should say which user runs it.
|
||||
N7. No verb adds or removes a `users` entry. `grant` needs the user already in the file (R1's rule), so a
|
||||
new user is a hand edit. "To cut a user off, revoke" removes that user's grants at one `--business` only.
|
||||
Say both in the help text. DESIGN.md 3.7 says "Removing the users from access.json acts at once", and the
|
||||
help should not read as if `revoke` does that.
|
||||
N8. When row 204 (193a) lands, `--level viewer` is accepted through `LEVELS` with no R5 change. The usage
|
||||
line, the help and the README name only `collaborator`. The row that lands second updates the text and
|
||||
adds a viewer grant test (my duckworth-0037 N1, carried in Morgana's BUILD.md).
|
||||
N9. `accessContextFor` calls `p.subjects.map` with no guard, so a provider with no `subjects` throws a
|
||||
`TypeError`. That goes away once R5 rebases onto row 192 and reads `console` through `checkConsole`, as
|
||||
BUILD.md says.
|
||||
|
||||
A round that changes only B1 and any notes taken gets a diff check, not a full re-read.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Review: Quackerjack row 202 FED R5, round 2 (duckworth-0041)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: candidate 48a3fb96e on quackerjack/fed-r5, manifest
|
||||
agents/quackerjack/work/queue-202/MANIFEST.md at 7fd265033, per quackerjack-0005. Round 1 was
|
||||
REVIEW-r1.md (duckworth-0040, changes B1, on 030809e15).
|
||||
- The six manifest hashes match the blobs at 48a3fb96e. scripts/mosaic is unchanged since round 1.
|
||||
- I read the full diff 030809e15..48a3fb96e for packages/cli (access.mjs, cli.mjs, the test file and the
|
||||
README) and BUILD.md.
|
||||
- In a scratch git worktree at 48a3fb96e, from packages/cli, `tests/access.test.mjs` alone passed 21 of
|
||||
21 with exit 0. I removed the worktree. I ran no suites and no mutants.
|
||||
- No test pins a hash of `packages/cli/src/access.mjs` or reads it by path.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
- B1 resolved. `accessCommand` runs `refuseInsideAgent`, then wraps the whole verb body, so a
|
||||
`BusinessError` from `loadAccess`, `loadBusiness` or `checkAccess` becomes `CliError("refused: …", 3)`.
|
||||
The exit code is 3 under Sage's ruling (sage-1437). The new test sets a 0664 file and asserts exit 3,
|
||||
the path in the message, and the file, receipt and lock unchanged.
|
||||
- The context is now `contextFor()`, called after the `--tester` refusal and the human proof. The new
|
||||
test makes `contextFor` throw and the file 0664, and both the agent and the `--tester` caller get their
|
||||
own refusal first.
|
||||
- The lock no longer reclaims. A dead-pid, empty or non-pid lock refuses and names the path, and the
|
||||
dead-pid case names the pid. With no reclaim, the `finally` unlink only ever removes the caller's own
|
||||
lock, so my N3 is gone too. That trades the race for a manual clear after a crash, which is the
|
||||
fail-closed choice, and Sage made it (sage-1429).
|
||||
- `writeAtomic` removes its temp file on any failure up to the rename. The test injects a throwing
|
||||
`renameSync` through `deps.fs` and checks the old bytes, no `.access-*.tmp`, no lock and no receipt.
|
||||
M11, the direct-write mutant, is now killable.
|
||||
- `deps.fs` undefined falls back to the real functions through the default parameter, in both
|
||||
`writeAtomic` and `withLock`.
|
||||
- N6 taken in the help text and the README.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Exit codes are now mixed. Load and check refusals are 3. `--level takes …`, `project-not-found` and
|
||||
"no grant … to revoke" stay 2. If that's intended, say so in the README. If not, it's a small follow-up.
|
||||
Sage's call.
|
||||
N2. The receipt is still appended after the lock is released (round 1 N4, second part). Receipt lines
|
||||
can land out of revision order when two writes run back to back. A crash between the rename and the
|
||||
append leaves a write with no receipt. Not required by the brief.
|
||||
N3. `writeAtomic`'s catch unlinks `tmp` even when `openSync(tmp, "wx")` failed with EEXIST, i.e. a temp
|
||||
name that belongs to someone else. With 8 random bytes this won't happen in practice.
|
||||
N4. Round 1 N1, N5, N7, N8 and N9 carry as you said. N1 is already in the R4 brief line of the
|
||||
manifest.
|
||||
|
||||
What remains is Sage's gate: the cli suite, every `scripts/test-*.sh`, and M1-M13 each killed.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Review: Morgana row 204 (193a) manifest, round 1 (duckworth-0037)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: morgana/row193a at 0caf6e2f6, base 44e36fc4c (row 201
|
||||
FED R1 candidate tree), per morgana-0018. Against design `agents/morgana/work/queue-193/DESIGN.md` r5 4.2,
|
||||
my R1 N1 (duckworth-0033) and sage-1420.
|
||||
- Manifest digest d38de8c9…f75a checked, and its three file hashes match the tree: access.mjs 0c1d9a12…b2ae,
|
||||
access.test.mjs 11dc4dd3…698c, access-levels.test.mjs a0656806…d52a.
|
||||
- I read the full diff 44e36fc4c..0caf6e2f6, BUILD.md, mutants.py and mutants.log.
|
||||
- In a scratch git worktree at 0caf6e2f6, from packages/business, each file alone with the tap reporter:
|
||||
access-levels.test.mjs 5 of 5, access.test.mjs 26 of 26, both exit 0. I removed the worktree. I ran no
|
||||
suites and no mutants.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
- `LEVELS` is `["collaborator", "viewer"]`, most to least, and the comment at access.mjs:13-15 says the
|
||||
order matters.
|
||||
- `covers` (access.mjs:180-188) throws a `TypeError` for a missing or unknown `level` after the `now`
|
||||
check. That resolves my R1 N1 the way sage-1420 item 2 ruled.
|
||||
- The least-level compare is right. A grant covers when its index is at or below the asked index, so a
|
||||
viewer grant (1) fails a collaborator ask (0), and any grant passes a viewer ask.
|
||||
- The `!LEVELS.includes(grant.level)` guard at :188 is needed. Without it an unknown grant level gives
|
||||
`indexOf` -1, which would pass every ask. Mutant A6 drops it and access.test.mjs test 25 catches that.
|
||||
- `checkAccess`'s refusal path is unchanged apart from the list in its message. "Viewer", "viewer " and
|
||||
`["viewer"]` refuse.
|
||||
- The tests carry the design's matrix, the missing-level throw, an unknown grant level, and a viewer grant
|
||||
that still needs business, project, instance and expiry.
|
||||
- No other package calls `covers` (grep over packages at 0caf6e2f6), and no test pins a hash of either
|
||||
source or test path.
|
||||
|
||||
What remains is Sage's gate: the business suite, every `scripts/test-*.sh`, and A1-A6 each killed on the
|
||||
rebased tree.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. R5 (row 202, quackerjack/fed-r5 at 959bfb275) imports `LEVELS` for `access grant --level`. Once both
|
||||
land, `--level viewer` is accepted with no R5 change. R5's usage line and README still name only
|
||||
`collaborator`. Whichever row lands second should update that text and add one grant test with `viewer`.
|
||||
R5 doesn't call `covers`.
|
||||
N2. On the rebased tree, R1's M43 anchor no longer exists, as BUILD.md says. If Sage's gate for 193a
|
||||
reruns R1's mutants, M43 needs the new anchor or it aborts on an anchor count of 0.
|
||||
N3. BUILD.md's mutant table labels A5b "no level means viewer", while mutants.py and the log say "means
|
||||
any". The two are the same mutant, since every grant covers a viewer ask. Doc-only.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Review: Gizmoduck row 210 round 1 code, host.run with sudo through the credential store (duckworth-0052)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per gizmoduck-0052, which moved it from gizmoduck-0049:
|
||||
- 12ffe0b56 on `gizmoduck/row210-on-199-r2`, base 07141966e (row 199's tip, megavolt-0027).
|
||||
- `git range-diff addee664a..8ec923585 07141966e..12ffe0b56`: the DESIGN commits and the code commit
|
||||
c7958740a are `=`. 12ffe0b56 adds only BUILD.md and manifest lines.
|
||||
- `candidate-manifest.sha256` (sha256 82b701e7…): 20 files, all OK in my worktree. It lists exactly the
|
||||
non-packet files in `07141966e..12ffe0b56`.
|
||||
|
||||
What I read in full: `bus/src/host-run.mjs`, `business/src/hosts.mjs`, and the diffs of `broker.mjs`,
|
||||
`bus/src/credentials.mjs`, `credential-verbs.mjs`, `runtime.mjs`, `server.mjs`, `bus/src/business.mjs`,
|
||||
`business/src/business.mjs`, `credentials.mjs`, `resolve.mjs`, `role.mjs` and `vocabulary.mjs`. I read
|
||||
T9 and the test titles against DESIGN r3 D3-D9 and section 9.
|
||||
|
||||
## Checks I ran
|
||||
|
||||
- The 17 touched test files, each alone, in a scratch worktree at 12ffe0b56 with TMPDIR under my own scratch
|
||||
dir: 134 pass, 0 fail, from an awk sum over the TAP log. This matches gizmoduck-0052.
|
||||
- `host-run.test.mjs` alone again: 18 tests, 0 skipped. The scratch sshd ran.
|
||||
- After the runs, no sshd, ssh or ssh-agent of mine was left, and no `h-` dir was left.
|
||||
- Hash pins: no test line pairs a changed file's name with a 64-hex value.
|
||||
- I didn't run the suites or the 47 mutants. They need Sage's slot.
|
||||
|
||||
## Verdict: changes, B1
|
||||
|
||||
The design holds in the code:
|
||||
- D4 binding. `hostRun` is `{host, argvSha256, stdinSha256}` at the raise (broker.mjs `hostRun`). It's
|
||||
compared whole in `#authorityFor`. The broker appends the question text, and the 4096 cap applies to all
|
||||
of it. T9 covers each mismatch, consumed, unapproved, and the full stdin in the question.
|
||||
- D5 order. Busy is checked before `#consumeAuthority`, so a busy refusal leaves the decision usable
|
||||
(deviation 5). From the broker's `busy` check to `#inflight.set` in `use()` there's no await, so two
|
||||
calls can't both pass.
|
||||
- D6. The quoting is POSIX single-quote, with `sudo -n --` first. T10's argv list covers `$( )`, a
|
||||
backtick, `;`, a newline and glob characters. The prefix match is literal. `refusedCommand` takes the
|
||||
basename. The digits-and-dots and hex-label refusals follow my r3 note.
|
||||
- D7. ssh and ssh-agent each lead a group under `setpriv --pdeathsig KILL`. The options are all on argv
|
||||
with `-F /dev/null`, and known_hosts is pinned per call. Field 22 of `/proc/<pid>/stat` is counted after
|
||||
the last `)`. The sweep keeps a dir whose bus pid and start time are alive, kills only a live recorded
|
||||
pid whose cmdline names the dir, and refuses start on any stray entry.
|
||||
- D8 and D9. The needles cover the seed at the three base64 offsets and in hex. Revoke aborts the
|
||||
controller, and `use()` answers `credential-unavailable` on an aborted signal.
|
||||
- The kinds are closed through `credentialKind`. `resolveInstance` drops `host.*` from the session, and
|
||||
the bus puts it back on the bus side only (deviation 1).
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. The ssh tests skip and pass when sshd is missing, but their skip text says the gate fails.
|
||||
- `host-run.test.mjs:22`: `skip = existsSync(SSHD) ? false : 'no /usr/bin/sshd: the gate fails on this skip'`.
|
||||
14 of the 18 tests carry `{ skip }`. That's T4 to T22 and three of the four T27 cases.
|
||||
- `node --test` exits 0 on skipped tests, and no `scripts/test-*.sh` counts skips. I grepped for `skip`. The
|
||||
only hits are each script's own docker-unavailable skip lines.
|
||||
- So on a gate machine without `/usr/bin/sshd`, the bus suite reports green and runs none of the D4-D9
|
||||
executor tests. That's the false-pass shape, and the text claims the opposite.
|
||||
|
||||
Fix, either way:
|
||||
- Make a missing sshd a failure, for example a first test that asserts `existsSync(SSHD)`, with no skip on
|
||||
the rest.
|
||||
- Or keep the skip, change the text so it doesn't claim a failure, and have the gate check `# skipped 0`
|
||||
for this file.
|
||||
|
||||
Add a mutant or a one-off check with `SSHD` pointed at a missing path, so the gate's answer is shown.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. A `host.sudo` raise checks only that the host is in the registry. It doesn't check that the raising
|
||||
role has a grant for that host, or that the argv matches a prefix. T9 raises and approves for `web2`
|
||||
(`host-run.test.mjs:235`), which that role isn't granted (`:209-211` refuses `host-not-granted`). Jason can
|
||||
be asked to approve a call that can never run. That widens nothing, because the call checks the grant.
|
||||
Checking `roles[s.role].hosts[target]` and `commandAllowed` at the raise would keep those questions out
|
||||
of his queue. Refuse with the same codes as the call.
|
||||
|
||||
N2. `addKey` (`host-run.mjs:84-92`) has no time limit and doesn't listen to the abort signal. If ssh-add
|
||||
never exits, `runHost` never returns. `#inflight` keeps the entry, because its delete is in `use()`'s
|
||||
`finally`. Every later call for that agent and host then refuses `host-busy` until the bus restarts.
|
||||
Revoke aborts the controller but can't clear the entry. ssh-add talks to a local agent on a private
|
||||
socket, so this is unlikely. Give it the `agentWaitMs` limit, or kill its group on abort.
|
||||
|
||||
N3. `host-key-mismatch` (`:177`) is chosen from exit 255 plus the stderr text. A remote command that exits
|
||||
255 and prints that line gets the same answer, and its stdout is lost. Only a command Jason approved can
|
||||
do that, so this is a reporting matter. Say so in the D6 exit-255 residual, or match the text only at the
|
||||
start of stderr.
|
||||
|
||||
N4. Deviation 3's wider runner list and deviation 11's T3 at the broker are fine. M25's CRLF mutant is
|
||||
equivalent, as BUILD.md says. With `\r` refused at :27, the line regex (:29) refuses it anyway.
|
||||
|
||||
N5. `business/tests/helpers.mjs:14` makes `mosaic-business-*` dirs that no test removes. One run of the
|
||||
17 files left 377 under TMPDIR, and `hosts.test.mjs` alone left 111. The helper is older than this row.
|
||||
It's a follow-up, not this row's to fix.
|
||||
|
||||
A round that changes only `host-run.test.mjs`'s skip, the mutant or check for B1, and these notes gets a
|
||||
diff check.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Diff check: Gizmoduck row 210 round 2 code, for duckworth-0052 (duckworth-0053)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject, per gizmoduck-0054:
|
||||
- 455964f23 on `gizmoduck/row210-on-199-r2`, one commit on 12ffe0b56. The base is still 07141966e.
|
||||
- The range is `12ffe0b56..455964f23`: 7 files, +163 -40. I read the whole code and test diff.
|
||||
- `candidate-manifest.sha256`: all 20 files OK in my worktree, and it lists exactly the non-packet files in
|
||||
`07141966e..455964f23`.
|
||||
|
||||
## Checks I ran
|
||||
|
||||
- The 17 touched test files, each alone, in a scratch worktree at 455964f23: 137 pass, 0 fail, 0 skipped,
|
||||
from an awk sum over the TAP log. `host-run.test.mjs` ran 21 tests, all passing, with none skipped.
|
||||
- B1 by hand. In my copy I set `SSHD` to `/nonexistent/sshd`, with a backup, and checked the sha on restore.
|
||||
The file exits 1 with 21 tests: 8 pass, 13 fail, 0 skipped. That matches gizmoduck-0054.
|
||||
- After the runs, no sshd or ssh-agent of mine was left.
|
||||
- I didn't run the suites or the 53 mutants. They need Sage's slot.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
- B1 is met. The `{ skip }` guard is gone. A first test asserts each binary, and `sshd()` throws when
|
||||
`SSHD` is missing. A missing sshd now fails the file, and the gate sees it.
|
||||
- N1 is taken. The raise runs host.run's grant and prefix check (`broker.mjs:1964-1967`). T5-T8 count 0
|
||||
`host.sudo` decisions after both refusals. T9 grants `web2` through `coderWeb2`. The `hard-waits.test.mjs`
|
||||
fixture change follows from N1 and only adds a grant.
|
||||
- N2 is taken. `addKey` kills ssh-add's group at `limitMs` (`host-failed`) or on abort
|
||||
(`credential-unavailable`). `runHost` passes `agentWaitMs` and the call's signal. The test's silent socket
|
||||
sees both connections close.
|
||||
- N3 is taken. `-E <dir>/ssh.log` keeps ssh's own messages apart from the remote command's stderr, and the pin
|
||||
refusal reads only that log on exit 255. The new test covers both a remote `fail255` and a closed port.
|
||||
|
||||
## Note (does not block)
|
||||
|
||||
N1. On an exit 255, `out.stderr += log` can take stderr up to 4096 characters past `outputMax`, and
|
||||
`truncated.stderr` stays false. That's bounded and only ssh's own text. Leave it or cap the sum.
|
||||
|
||||
The suites and the mutant batch, M36-M38 included, are Sage's gate.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Row 210 queue round 3: rebase check (duckworth-0078)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in sage-1658 and sage-1661 (request rev 2612/2613,
|
||||
Gitea comment 29849) and in gizmoduck-0094. Candidate: `gizmoduck/row210-r5`, tip 0485384e9 on
|
||||
ad95c888c, manifest sha256 688d38b96b3de80f7bbbca857130ef36f68ec65009a3df51c5084aef7cb96788. Scope is a
|
||||
rebase only (sage-1657). My round 2 approve (rev 2592) was on c43008749, on cb5fdb064.
|
||||
|
||||
## Verdict: approve, no findings
|
||||
|
||||
1. broker.mjs is a clean 3-way merge. I ran `git merge-file` myself with ours c43008749, base cb5fdb064
|
||||
and theirs ad95c888c. It gave 0 conflicts and sha256 610af0c4207bbd805a2a8826daa32b8cdb423937f793c32fd4241f6addfabfda,
|
||||
which equals r5's broker.mjs.
|
||||
- `git diff c43008749 0485384e9 -- broker.mjs` is exactly row 237's 3-line hunk, after :1055.
|
||||
- `git diff d7d943199 0485384e9` on broker.mjs equals `git diff cb5fdb064 c43008749`, ignoring
|
||||
index and hunk-header lines. So 210's broker change carries over unchanged.
|
||||
2. Across the 23 manifest paths, cb5fdb064..ad95c888c changes only broker.mjs. Every one of the 23
|
||||
hashes in the r5 manifest matches the r5 tree. The manifest differs from r4 only in the broker line,
|
||||
and its sha256 is 688d38b9…6788, which matches the request.
|
||||
3. The rebase is the nine r4 commits, all `=` in range-diff, plus one commit that touches only
|
||||
BUILD.md and the manifest. c43008749..0485384e9 adds no 210 path beyond the broker merge.
|
||||
DESIGN.md and mutants.py are unchanged.
|
||||
4. No interaction. 210's broker diff has no line with checkPublish, #publishes, publish-busy,
|
||||
recordPublish, #publishFacts, #checkAuthority or git.push. 237's check sits in checkPublish and adds
|
||||
no imports.
|
||||
|
||||
My runs, single files in a detached worktree of 0485384e9:
|
||||
- bus: publish 13/0, host-run 21/0, hard-waits 13/0, project-runtime 3/0.
|
||||
- cli: publish-socket 8/0, credentials 5/0.
|
||||
- business: hard-waits 5/0, hosts 7/0.
|
||||
- seat sandbox gave 7/3, and the base ad95c888c gives the same 7/3 in my session. It's environmental,
|
||||
as seen before. Gizmoduck reports 10/0.
|
||||
|
||||
Sage's mutants and full gate on this tree are still to come.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Review: Gizmoduck row 210 (#1707) DESIGN.md r1, agent host access with sudo (duckworth-0042)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: agents/gizmoduck/work/queue-210/DESIGN.md at 7b10bc0ff
|
||||
on gizmoduck/row210, blob 1514a36d96 (checked), base refactor 9ba31b5b3, per gizmoduck-0041. Against the
|
||||
brief docs/plans/2026-10-20_host-credentials.md (9db578ff5), REQ-CRED-3 in Honker's 1.7 draft r2 (approved
|
||||
duckworth-0039), row 199 design r6 (4d3007ec9) and row 199's code on origin/megavolt/row199 at addee664a.
|
||||
- I checked E1-E11 at 9ba31b5b3: credentials.mjs:22, :84-87, :95-103, :104-117, host.mjs:31 and :270,
|
||||
broker.mjs:145-147, :790-797 and :1490-1532, server.mjs:87-95, business.mjs:89, vocabulary.mjs:101-103,
|
||||
sandbox.mjs:29-31, instance-settings.mjs:27. They hold. E12 is stale, see N1.
|
||||
- 199's `useFor` at addee664a (credentials.mjs:228-256) passes a BusError through and maps anything else to
|
||||
`service-failed`, so D5's own codes (`host-key-mismatch`, `credential-leak`) reach the caller as designed.
|
||||
- I ran nothing. The probe in section 2 is the author's, and I didn't repeat it.
|
||||
|
||||
## Verdict: changes, B1-B4
|
||||
|
||||
What holds:
|
||||
- The four grant layers, checked on every call, meet REQ-CRED-3's "a grant exists for that agent and that
|
||||
host before the credential is used". The agent never names another agent: the key comes from the cap
|
||||
through `useFor`.
|
||||
- No agent and not the chief can write layers 2-4. `hosts` is outside ORGANIZATION_KEYS and INSTANCE_KEYS,
|
||||
and `instances.set` refuses credentialed definitions.
|
||||
- `host.sudo` lands as a hard wait under REQ-WAIT-3, and T25 enforces REQ-WAIT-2.
|
||||
- The key handoff keeps the key off disk, argv and env (T19). `-F /dev/null`, a bus-written known_hosts
|
||||
under `HostKeyAlias` and `StrictHostKeyChecking=yes` give no trust on first use. Forwarding is off and
|
||||
`restrict` is on the host side.
|
||||
- The command is built from argv with POSIX single quotes and `sudo -n --`, never taken as a string.
|
||||
- The executor runs in the bus child as a broker.sock verb. The 10 s serial IPC (E5) is a sound reason not
|
||||
to use a host op. It matches where 199 put the push (199 BUILD.md deviation 11), see N1.
|
||||
- The brief's two named mutants are there: pin skip (M12, M13) and key leak (M15, M16).
|
||||
- Section 6 states the limits plainly, including that `"any"` is root and can outlive the grant.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. The event log doesn't say what ran as root. D5 step 6 records `action.allowed` with "host, argv hash,
|
||||
stdin size and hash", and T22 checks that the bodies carry hashes. After routing, a within-role call has no
|
||||
decision, so no record anywhere in the stack names the command, only its sha256. The existing verbs log the
|
||||
thing done in clear: `requestWeb` records the url or query (broker.mjs:1521), and 1.7's push line says
|
||||
"Every push is logged with the remote, the ref, the commit and, when one was given, the decision" (draft
|
||||
:105). argv has already passed `#secretCheck` (step 1), so it holds no known secret, and it's capped at
|
||||
32 KiB. The design gives no reason to hash it. Fix: record `host`, `user` and `argv` in clear in the
|
||||
`requested` event, and keep output as sizes and hashes. Change T22 to assert argv is present and stdout,
|
||||
stderr and the key are not, and M24's partner: a mutant that drops argv from the event.
|
||||
|
||||
B2. A pre-routing decision binds stdin that Jason never sees. D4 binds `stdinSha256`, and the question
|
||||
shows only "the host id and the argv". `stdin` can be 64 KiB (D6). So `argv: ["bash"]` with a script on
|
||||
stdin shows Jason the question "… bash", and he approves a script he can't read. Binding a hash he can't
|
||||
check doesn't give him a decision about it. Fix, either:
|
||||
- the question shows stdin in full when present (it passed `#secretCheck`), or
|
||||
- a gated call refuses non-empty stdin, and stdin is allowed only for a routed within-role call.
|
||||
Add a T9 case for the chosen rule and a mutant that drops it.
|
||||
|
||||
B3. If the bus child dies, the key stays loaded and the call can't be revoked. D7 step 5 cleans up in a
|
||||
`finally`, and T18 covers success, refusal, timeout and a thrown error. A bus child that is SIGKILLed or
|
||||
crashes never runs the `finally`. `ssh-agent -D` has no lifetime of its own, so it keeps the key loaded on
|
||||
its socket until the machine reboots. A detached ssh keeps running the remote command. After the restart
|
||||
the new bus's kill map is empty, so D9's "revoke also kills a `host.run` in flight" no longer holds for
|
||||
that call. 199's push leans on git's own time limit for the same case (deviation 11, "git's own time limit
|
||||
ends it"). ssh-agent has no such limit unless one is given. Fix:
|
||||
- `ssh-add -t <timeout + grace>`, so the agent drops the key on its own.
|
||||
- At bus start, sweep `<configDir>/secrets/run/`: kill what each leftover dir's recorded pids name (check
|
||||
they are still that ssh-agent or ssh first) and remove the dir.
|
||||
- Consider `setpriv --pdeathsig SIGKILL` around ssh-agent and ssh, which the sandbox already uses
|
||||
(sandbox.mjs:25-27). Probe it from node first.
|
||||
Add a test that SIGKILLs the bus child mid-call and then asserts the key is gone from the agent by the
|
||||
bound and the next start leaves no run dir. Add a mutant that drops `-t`.
|
||||
|
||||
B4. Nothing routes section 4 to Jason. REQ-CRED-3 says "Who grants a host credential, and how, is set by
|
||||
the #424 design, which Jason approves", and section 4 ends "The table above is that answer". Section 10's
|
||||
only Jason question, Q-J1, is about the first grants at the live window. My approval of the design doesn't
|
||||
stand in for his. Fix: add a Q-J0 to section 10, sent through Mos before the build lands: approve the
|
||||
section 4 table. Name where his answer is recorded.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. 199 is on origin now: origin/megavolt/row199 at addee664a carries code and BUILD.md. The header's "199
|
||||
has no code on origin yet" and E12 are stale. The bus child already spawns git there: runtime.mjs:245 calls
|
||||
`publishRemote`, which runs `runGit` (seat/src/checkout.mjs:95-138) as a detached group leader with
|
||||
SIGTERM, a grace period and SIGKILL to the group. Reuse that shape, or the helper, for ssh rather than
|
||||
writing a second one. Q-S2: deviation 11 keeps the token and the push in the bus child, and the host side
|
||||
only polls. D5's assumption holds, and since the agent reaches broker.sock directly, 210 needs no poll.
|
||||
Sage confirms.
|
||||
N2. Section 6 should say that a registry entry for the stack's own machine, or any host where the stack's
|
||||
`<configDir>` lives, gives the agent with `"any"` every agent's keys and tokens, not just root there.
|
||||
Consider refusing a loopback `address` at load.
|
||||
N3. D5 says every spawn is `detached: true`, so each gets its own group. D7 step 3 says ssh-agent runs "in
|
||||
the same process group". Say which group ssh-agent is in, and make sure the timeout and revoke kill reach
|
||||
both the agent and ssh.
|
||||
N4. Prefix lists plus stdin: `[["tee"]]` or `[["dd"]]` writes any file as root from stdin. D6 already says
|
||||
a prefix list limits mistakes, not a determined agent. Say in the doc that stdin goes to the command, so
|
||||
some prefixes are root on their own.
|
||||
N5. Pass the user with `-l <user>` and the address as its own argument rather than `<user>@<address>`.
|
||||
Then nothing depends on how ssh splits on `@`, or on an IPv6 literal.
|
||||
N6. D8 is defense in depth. The remote never holds the private key, and forwarding is off, so output can
|
||||
carry the key only if the agent already had it. It's still right to keep. Also note that a `host.*` entry
|
||||
in the shared token map makes today's `assertClean` loop over a multi-line value that never matches
|
||||
(E3), which is harmless.
|
||||
N7. My #423 r5 condition (duckworth-0035) applies. If ops-01 changes step 1 to fit this design, for
|
||||
example per-agent `from=…,restrict` lines for localadmin, that changed step 1 comes to me as a diff before
|
||||
it runs. That's the live part, not this row.
|
||||
N8. Q-S1 (route `host.sudo` in 1.7) is Sage's and Jason's call. Without it, B2's rule matters on every
|
||||
call.
|
||||
|
||||
A round with only B1-B4 and any notes taken gets a diff check, not a full re-read.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Review: Gizmoduck row 210 (#1707) DESIGN.md r2, diff check (duckworth-0043)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: DESIGN.md at b726e10f4 on gizmoduck/row210, blob
|
||||
8c6c6a8c5e (checked), per gizmoduck-0044. Diff 7b10bc0ff..b726e10f4, +153 -60, one file. I read the
|
||||
whole diff, including sage-1450's D6 change, which goes past my B items. Round 1 was REVIEW-r1.md
|
||||
(duckworth-0042). I checked the new cites at 9ba31b5b3:
|
||||
- broker.mjs:126 `text()` caps a string at 4096, and :1654 runs `string(a.question)` on the raise.
|
||||
- sandbox.mjs:44 is `KILL_WITH_PARENT = ["/usr/bin/setpriv", "--pdeathsig", "KILL", "--"]`.
|
||||
- E12 and 199's cites at addee664a match what I read in round 1.
|
||||
I ran nothing.
|
||||
|
||||
## Verdict: approve on b726e10f4, with one condition (C1)
|
||||
|
||||
- B1 resolved. `requested` and `completed` carry host, address, user and argv in clear, after
|
||||
`#secretCheck`. stdin and output stay as size and sha256. T22 asserts both sides, and M30 drops argv.
|
||||
- B2 resolved. The broker appends host, user, JSON-quoted argv and the full stdin to the question itself,
|
||||
inside the existing 4096 cap. A raise that doesn't fit refuses `invalid-request`. T9 checks the stored
|
||||
question and the cap, and M31 omits stdin. With sage-1455 (every call a Jason decision under 1.7, per
|
||||
mos-1086), this was the rule that mattered most, and it now holds.
|
||||
- B3 resolved.
|
||||
- `setpriv --pdeathsig KILL --`, the sandbox's own prefix, wraps every spawn.
|
||||
- `ssh-add -t <timeout+30>` bounds the key even if the agent survives.
|
||||
- A pids file and a sweep before `broker.sock` listens. The sweep kills only a pid whose cmdline names
|
||||
the dir, and leaves alone a dir whose bus pid is alive, which also covers two buses sharing
|
||||
`<configDir>`.
|
||||
- T27 covers all three, and M32-M34 drop each one.
|
||||
- B4 resolved. Q-J0 is in section 10 in standalone form, routed through Mos before landing, and recorded in
|
||||
DECISIONS.md. See C1.
|
||||
- sage-1450's D6 holds together. There's no wildcard grant. Load refuses a string, empty, duplicate and
|
||||
glob-character `commands`, and a shell or runner as `argv[0]` by basename (so `/bin/bash` too). T26 and
|
||||
M6, M28 and M29 cover each one. Section 6's known limit names what closes it (`bwrap --unshare-net`) and
|
||||
doesn't claim the allowlist closes it.
|
||||
- Notes N1-N7 were taken as asked.
|
||||
|
||||
## Condition
|
||||
|
||||
C1. Q-J0 overstates revoke. Its line "You, Mos or the tester can revoke at once. The stack drops the key
|
||||
and stops a running command" goes to Jason word for word. Section 6 says "Revoke stops the stack's path,
|
||||
not remote processes. Killing ssh may leave a remote process running." He should approve on the accurate
|
||||
version. Replace the line with something like: "You, Mos or the tester can revoke at once. The stack drops
|
||||
the key and cuts its connection. A command already started on the machine may keep running." That's a
|
||||
one-line change that needs only my diff check, and it must be in before Mos sends Q-J0.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. The runner list is open by design (D6 says so), and it's Sage's ruling. Commands that run another
|
||||
command as root as directly as `env` or `nohup` do: `systemd-run`, `runuser`, `chroot`, `nsenter`,
|
||||
`unshare`, `setpriv`, `capsh`, `doas`, `pkexec`, `flock`, `stdbuf`, `ionice`, `chrt`, `taskset`, `strace`,
|
||||
`script`, `watch`, and the interpreters (`python3`, `perl`, `ruby`, `node`, `awk`). Adding them costs one
|
||||
array line each. `systemd-run` is the one I'd add first.
|
||||
N2. The loopback check names `127.0.0.0/8`, `::1` and `localhost`. getaddrinfo also takes these to the
|
||||
local machine: `0.0.0.0` (and `0`), `::`, `::ffff:127.0.0.1`, short or numeric IPv4 forms such as
|
||||
`127.1` or `2130706433`, which pass the DNS-name regex, and `*.localhost`, which systemd-resolved maps to
|
||||
loopback. Refuse a name made only of digits and dots unless it's a full four-part IPv4 literal, and run
|
||||
the address check on `net.isIP` output. Section 6 already says load can't know every address of the
|
||||
stack's machine, so this narrows the gap without closing it.
|
||||
N3. The sweep trusts that a live pid is still the bus that wrote the file. A reused pid leaves that dir in
|
||||
place until the next start. The cmdline check already stops a wrong kill. Recording each pid's start time
|
||||
(`/proc/<pid>/stat` field 22) beside it removes the reuse case. Low stakes, because `-t` and pdeathsig
|
||||
have already dropped the key.
|
||||
N4. T26 is listed after T27, and M28/M29 after M35. Cosmetic.
|
||||
N5. On a gated call the 4096 cap counts the agent's own question, the JSON-quoted argv and the stdin, so
|
||||
stdin's real room is smaller than 4 KiB. D4 says this. The tool help should say it too.
|
||||
|
||||
Q-S3 (the host_run harness tool) is Sage's call. What remains: C1, then the build, manifest review and
|
||||
Sage's gate. The row lands after 199 and after the PRD version holding REQ-CRED-3.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Review: Gizmoduck row 210 DESIGN r3, C1 diff check (duckworth-0044)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-20. Subject: gizmoduck/row210 at e021dfa26, DESIGN.md blob
|
||||
ece332e306 (checked), per gizmoduck-0047. e021dfa26 is a fast-forward of b726e10f4, and the diff touches
|
||||
DESIGN.md only (+51 -26). I read the whole diff.
|
||||
|
||||
## Verdict: C1 met, approve stands
|
||||
|
||||
C1. Q-J0 now says "The stack drops the key and cuts its connection. A command already started on the machine
|
||||
may keep running." That matches section 6 and D9's revoke line. The quote is still 10 lines, and the yes/no
|
||||
question is last. Dropping "on the machine" from the ops-01 sentence loses nothing. Mos can send Q-J0 as it
|
||||
stands at e021dfa26.
|
||||
|
||||
The rest of r3 matches the rulings and my r2 notes:
|
||||
- Q-S3 (sage-1468): the `host_run` row is gone from D10, and the scope line says no `packages/harness` file
|
||||
changes. My r2 N5 goes to Sage's follow-up row.
|
||||
- N1 (sage-1473): D6 carries the whole runner list, five interpreters, and the line that the list can't be
|
||||
complete and Jason's per-call decision is the control. T26 has one case per name.
|
||||
- N2: I checked `net.isIP` on node here. It returns 0 for `127.1`, `2130706433`, `0177.0.0.1`,
|
||||
`127.000.000.001` and `127.0.0.01`, and 4 for `10.1.1.53`. glibc resolves `0177.0.0.1` and `127.0.0.01`
|
||||
to 127.0.0.1. The rule "a name of only digits and dots refuses unless `net.isIP` reads it as IPv4"
|
||||
refuses all of them, octal included.
|
||||
- N3: the pids file records the start time (field 22), the sweep treats a mismatch as dead, and T27 and M34
|
||||
test it.
|
||||
- N4: M28 and M29 are in order.
|
||||
- The 1.7 approval line and the Q-S1 line agree with sage-1468.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. D6's interpreter list has `python3` but not `python`, `python3.12` or other versioned names, and has
|
||||
`awk` but not `gawk`, `mawk` or `nawk`. `sed` (GNU `e` command), `vi`/`vim`, `less` and `php` also start a
|
||||
shell. D6 already says the list isn't the control, so these are cheap additions to T26 if Gizmoduck wants
|
||||
them, not a requirement. A basename match on `python3*` and `*awk` would cover the versioned forms.
|
||||
|
||||
The build can start on 199's candidate.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Review: row 210 round 4 diff, the B1 import fix (duckworth-0071)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Subject, per gizmoduck-0089 and sage-1619:
|
||||
`gizmoduck/row210-r3`, diff 3cf63efb4..c43008749, one commit, 9 files. Base cb5fdb064.
|
||||
|
||||
I approved rounds 1 and 2 without catching B1 (broker.mjs:7 importing the unmounted `hosts.mjs`). This
|
||||
round checks Sage's four points and the second fix.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
Sage's checks:
|
||||
1. broker.mjs's imports are all import-free mounted files. The only import change is `:6`.
|
||||
`validArgv` and `commandAllowed` now come from `business/src/vocabulary.mjs`, and the `hosts.mjs`
|
||||
line is gone. `vocabulary.mjs` has 0 import statements at c43008749 and is in RUNNER_FILES
|
||||
(`seat/src/sandbox.mjs`). broker.mjs's other imports are unchanged from cb5fdb064.
|
||||
2. The pins.
|
||||
- `bus/tests/project-runtime.test.mjs:15-16, :27-28` pins the `createHash` line and the new
|
||||
vocabulary line, each with a Row 210 comment. The `createHash` line was also missing from the
|
||||
pin before this round.
|
||||
- The seat pin needs no change, because `packages/seat` is byte-identical to 3cf63efb4 and
|
||||
RUNNER_FILES is unchanged.
|
||||
3. No `errors.mjs` or `util.mjs` in the mounts: RUNNER_FILES is unchanged.
|
||||
4. The whole suites are on Sage's slot. Gizmoduck reports business 198/0, seat 164/0 and bus 498/0,
|
||||
with 0 skipped. I didn't run suites.
|
||||
|
||||
Also checked:
|
||||
- The move is byte-identical. A `diff` of `ARGV_MAX`..`ARGV_BYTES`, `validArgv` and `commandAllowed`
|
||||
between 3cf63efb4 `hosts.mjs` and c43008749 `vocabulary.mjs` shows no difference except a trailing
|
||||
blank line. They use only the `Buffer` global.
|
||||
- `hosts.mjs:7` imports `ARGV_MAX` back for `checkCommands` (`:100`), and `:11` re-exports all five.
|
||||
The only other reader is `business/tests/hosts.test.mjs:9`.
|
||||
- The roles fix. `roles/pm.md:54-57` and `roles/chief-of-staff.md:74-77` now name `host.sudo` among
|
||||
the hard waits. I confirmed it was needed: with the two role files from 3cf63efb4,
|
||||
`workflow-roles.test.mjs` fails 6/1, and with c43008749's it passes 7/0. The test asserts that the
|
||||
listed waits are exactly GATED_ONLY, so this writes down routing the broker already applies. No
|
||||
grant or authority list changes.
|
||||
- The manifest has 23 lines, every hash matches c43008749, and its paths are the diff from cb5fdb064
|
||||
less the four queue-210 packet files.
|
||||
- Base drift: none of the 23 paths changed on origin/refactor since cb5fdb064 (c8bc5cf4c, 390ea5261,
|
||||
011d41ba6).
|
||||
- Hash pins: no test or script pins any changed file. 0 hits, against a control of 354 files that
|
||||
hold a 64-hex string.
|
||||
|
||||
My runs, single files in a detached worktree of c43008749:
|
||||
- business: hosts 7/0, workflow-roles 7/0.
|
||||
- bus: project-runtime 3/0, hard-waits 13/0, host-run 21/0.
|
||||
- seat `sandbox.test.mjs` can't run in my session. It gives 7/3 on cb5fdb064 too ("no answer" from
|
||||
the confined shell), so that one rests on Sage's gate.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Row 234 early check: split and name map (duckworth-0079)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in gizmoduck-0095. Subject: `gizmoduck/row234` 2aa54a67a,
|
||||
`agents/gizmoduck/work/queue-234/BUILD.md` (71 lines). Checked against r6 f9039682e
|
||||
(`agents/gizmoduck/work/machine-grant/DESIGN.md`, "r6:" below) and #436b section 7 at fa5658941. No code
|
||||
yet, so this is a design-to-build check, not a verdict on the row.
|
||||
|
||||
## Result: the split and the name map stand, with four items for the candidate
|
||||
|
||||
The name map is complete. Every `grant.`/`host.` name in r6 is one of these:
|
||||
- The four renamed in the table (r6:257, :271, :394, :404).
|
||||
- Already under `grant.`: step, applied, failed, revoked, revoke.closed.
|
||||
- Not an event: `host.run`, `host.sudo`, and `host.grant` (r1's dropped action) and `grant.ask` (Q-S1, not
|
||||
built).
|
||||
|
||||
`decision` becoming `grantDecision` in every guarded body except the consume record follows section 7's
|
||||
spend rule (fa5658941 DESIGN.md:472-476). `grant.host.checked` and `grant.host.removed` carry no decision.
|
||||
|
||||
The split matches r6 Q-S4 (r6:904-911) and the X/E sections. Row a has no path that makes a `host_grants`
|
||||
record outside a fixture, because the `host` method refuses `invalid-request` until row b. Items 1 to 8 match
|
||||
r6 S1, X1, E1 to E4 and X4's first and third bullets.
|
||||
|
||||
## G21 fixture (question 1): acceptable, on these terms
|
||||
|
||||
Row a can't produce `hostLineLeft`. An enrollment has no remote step, so its failures leave no line. A fixture
|
||||
is therefore the only way to test the check in row a.
|
||||
- F1. Write the fixture through row a's private grant writer, in the real shape: `action.allowed`, actor
|
||||
(NULL, 'grant'), `body.action` 'grant.failed', `body.host`, `body.hostLineLeft`. Don't write it with raw
|
||||
SQL around the trigger.
|
||||
- F2. Read `body.hostLineLeft` from any guarded body, not only `grant.failed`. Row b's
|
||||
`grant.revoke.closed` carries it too (r6:715-717).
|
||||
- F3. r6's `grant.revoke.closed` body is `{tag, hostLineLeft}` (r6:716). It names no host. Either row b
|
||||
adds `host` to that body, or the check maps the tag to a host through the recorded `grant.step` tags.
|
||||
Fix which one in BUILD.md now, so row a's reader and row b's writer agree.
|
||||
- F4. "No later `sweep` cleared it" (r6:403) needs `grant.sweep`, which row b writes. Say in BUILD.md
|
||||
that row b adds the sweep clear to `hosts.remove` with a G21 case. Row b's G8 or G13 should also show that
|
||||
`hosts.remove` refuses after a real write, not only after the fixture.
|
||||
|
||||
## Moves and gaps (question 2)
|
||||
|
||||
- M1. G4 for the `host-enroll` block goes in row a. Row a renders that block (item 3, r6:240-242): the
|
||||
fingerprints, the key lines and the sudo line. Row a should test that a question can't forge it and that
|
||||
the 4096-character limit holds with it appended. G4 for the `host` block stays in row b.
|
||||
- M2. The tag function lands in row a, because E2 step 2 uses it for the installer tag (r6:265-266). Move
|
||||
G24's first sentence (64-character names with `.` and `_` give distinct tags of the fixed length) and the
|
||||
mutant "build the tag without the business" to row a. The `sweep` half of G24 stays in row b.
|
||||
- M3. The Approve record check (X4 first bullet, r6:593-596) is row a code, but no row a G-test covers it.
|
||||
G22 is row b. Add a row a test on store fixtures that `host.run` refuses `host-not-granted` for each of:
|
||||
- no record
|
||||
- a record whose decision is unresolved
|
||||
- a decision resolved by an arbiter, the tester or an agent, or not via cli
|
||||
- a consume record with `operation: 'grant.host-enroll'` instead of `'grant.host'`
|
||||
- an unspent decision
|
||||
|
||||
Also add a restart test that a `host_grants` fixture yields a `Credentials` reference (X4 third bullet).
|
||||
The mutant "`host.run` reads the business file instead of the grant record" goes in row a.
|
||||
- M4. Item 8 lists status, the start reconcile and `openGrantDecisions`. Add `hosts.remove`'s
|
||||
`hostLineLeft` reader (F2). The look-alike test should cover it: an `action.refused` or session-written
|
||||
`hostLineLeft` doesn't block a remove.
|
||||
|
||||
N1 (doesn't block). BUILD.md splits the G-tests but not r6's mutant list (r6:838-883), and r6 leaves the
|
||||
numbering to BUILD.md. A per-row list before the candidate saves a round. By my reading these are row a's:
|
||||
- drop the chief check
|
||||
- start on a mint with no `grant`
|
||||
- start on an arbiter resolve, or on `no`
|
||||
- skip the keyscan compare, or compare one key
|
||||
- the private key in an event body
|
||||
- drop the not-enrolled check
|
||||
- drop the `hosts` primary key
|
||||
- skip the start check for a resolved, unspent enrollment
|
||||
- `host.run` reads the business file
|
||||
- the tag without the business
|
||||
- the writer mutants (`from=`, a line outside the tag pattern, `O_NOFOLLOW`, a dropped group, the user
|
||||
pattern, `privileged` without `users`, a sudo user not in `privileged`)
|
||||
- section 7's trigger mutants
|
||||
- one reader-filter mutant
|
||||
@@ -0,0 +1,59 @@
|
||||
# Row 234 early code read, follow-up (duckworth-0083)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in gizmoduck-0102. This checks ad7373c1b on
|
||||
`gizmoduck/row234` (one commit on f4de24d39, 6 files, +143/-29) against duckworth-0081. It's still not the
|
||||
manifest verdict.
|
||||
|
||||
## Result: B1 to B3 resolved
|
||||
|
||||
- B1 (G10): `grant-enroll.test.mjs:269`.
|
||||
- A second start on a spent decision returns `decision-consumed`, and the events, the host row and the
|
||||
message count don't change.
|
||||
- An open raise returns `not-authorized`. So does one resolved yes by the chief.
|
||||
- After that there's no web2 row and no key file, and `reconcileGrants` returns [].
|
||||
- The new public `runEnrollment` (`broker.mjs:745`) only wraps `#runEnrollment`. No verb, server
|
||||
route or runtime op reaches it. The only other call is the resolve hook at :2445. It's the same kind
|
||||
of trusted entry as `reconcileGrants`, and step 1 checks everything again. I accept it.
|
||||
- The return codes don't change anything the hook depends on, because `setImmediate` ignores them.
|
||||
- B2 (G18):
|
||||
- The host half is at `host-run.test.mjs:297`. `hg.check` with a wrong pin refuses
|
||||
`host-key-mismatch`, and the stub `list` never runs.
|
||||
- The broker half is at `grant-enroll.test.mjs:401`. The host stays `pending-install` with
|
||||
`checked_at` null, and there's no `grant.host.checked`.
|
||||
- B3:
|
||||
- G6 is at :251. A withdraw by the chief and by the human, then an arbiter resolve yes, write nothing,
|
||||
and `runEnrollment` returns `not-authorized`.
|
||||
- G20 is at :390. The reader is refused `read-only`, and the human's check reaches `ready` with `by`
|
||||
`['jason']`.
|
||||
- N2 is fixed. A timeout gives '', which is not the expected line, so the writer refuses `user-sudo`,
|
||||
logs it and exits 1.
|
||||
- N3 is fixed in the comment.
|
||||
- N4 stands as Gizmoduck says. r6:578 lists the tag only for the start reconcile's `interrupted`.
|
||||
- N5 is in the run sheet.
|
||||
|
||||
## Notes (no block)
|
||||
|
||||
- N8. G10's agent-resolved row goes in with `via 'broker'` (:280), so `#approvedGrant` refuses at the
|
||||
via check and never reaches the `by` check. A second row with `via 'cli'` and `by 'chief'` would pin
|
||||
`by == human` too. The A1 to A23 run will show whether a mutant on the `by` check survives. If one
|
||||
does, this is the test to add.
|
||||
- N9. G18's control (:310) calls `runHost` with the fixture's entry, not `hg.check` with the right pin.
|
||||
So it shows the stub works, but not that `hg.check` passes the record's keys through when they're
|
||||
right. E4's broker tests use a fake check, so nothing runs `hg.check` end to end with a good pin. That
|
||||
could be a small follow-up, or the #433 VM covers it.
|
||||
|
||||
## My runs
|
||||
|
||||
In a detached worktree of ad7373c1b, pass/fail:
|
||||
|
||||
| file | result |
|
||||
| --- | --- |
|
||||
| bus grant-enroll | 24/0 |
|
||||
| bus host-run | 24/0 |
|
||||
| bus host-tools | 6/0 |
|
||||
| bus grant-migration | 5/0 |
|
||||
| bus hard-waits | 13/0 |
|
||||
| bus project-runtime | 3/0 |
|
||||
| bus credentials | 5/0 |
|
||||
|
||||
`mosaic-keys-write` parses under python3 -I. I ran no suite and no mutant.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Row 234 early code read (duckworth-0081)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in gizmoduck-0101, before the formal request. Candidate
|
||||
code `gizmoduck/row234` f4de24d39 on 8fede5fc6 (39 files, +2953/-187). Brief:
|
||||
`docs/plans/2026-10-20_host-credentials.md` "Machine grant row a", whose gate needs "the design's section
|
||||
10 tests and mutants for row a". Design r6 f9039682e, section 7 fa5658941. This is not the manifest
|
||||
verdict. That one comes after Sage's A1 to A23 run.
|
||||
|
||||
## Verdict: changes, 3 blockers
|
||||
|
||||
BUILD.md:68-69 lists G6, G10, G18 and G20 among row a's tests. The brief's gate requires them, so each
|
||||
missing case blocks.
|
||||
|
||||
- B1. G10 has no test (r6:818). No row a test starts a second enrollment for a spent decision and gets
|
||||
`decision-consumed`. None shows `#consumeGrant` (`broker.mjs:678`, `#spent` at :681) refusing an
|
||||
unresolved or an agent-resolved decision. A8 (`grant-enroll.test.mjs:250`) uses two decisions, so it
|
||||
tests `host-enrolled`, not this. The only `decision-consumed` assert near hosts is `host-run.test.mjs:250`,
|
||||
which covers row 210's host.run. The direct way is to call the executor or `reconcileGrants` twice for
|
||||
one resolved decision, and once each for an open decision and an agent-resolved fixture.
|
||||
- B2. G18 has no test (r6:826). The code path exists: `host-grant.mjs:207-211` calls `runHost` with the
|
||||
pinned keys, and `host-run.mjs:197` refuses `host-key-mismatch` on ssh exit 255. But
|
||||
`grant-enroll.test.mjs:333-356` uses a fake check that never throws that code, and
|
||||
`host-tools.test.mjs` tests only the writer, mosaic-keys, sudoers and sshd. A test needs to show that a
|
||||
wrong pin refuses `host-key-mismatch`, runs nothing, and leaves the record `pending-install` with no
|
||||
`grant.host.checked`. The fake check can throw the BusError for the broker half. The runHost half can
|
||||
reuse the host-run.test.mjs ssh stand-in that gives :290 its `host-key-mismatch`.
|
||||
- B3. G6 and G20 are partly tested.
|
||||
- G6 (r6:814): the no is tested (A4, :231), and so are the tester, pm and chief resolves (A3, :237).
|
||||
The withdraw and the arbiter resolve have no case. A withdraw of an open `host-enroll` decision must
|
||||
start nothing.
|
||||
- G20 (r6:828): the chief reaches `ready` (:341) and the tester gets past the role check to
|
||||
`host-changed` (:355). The human's `hosts.check` is never called. It must reach `ready` through
|
||||
`#hostsCaller`'s `#human` leg.
|
||||
|
||||
## Notes (no block)
|
||||
|
||||
- N1. A3 (`grant-enroll.test.mjs:237`) can't reach `#approvedGrant`'s via and by checks, because every
|
||||
resolve it tries is refused first. The M3 fixtures in `host-run.test.mjs` cover those checks, so
|
||||
nothing is untested. The test name reads as more than it shows.
|
||||
- N2. `mosaic-keys-write:255-266`: `main` catches `Refused` and `OSError`. A `sudo -l` timeout raises
|
||||
`subprocess.TimeoutExpired`, which escapes, so the writer exits with a traceback and logs nothing.
|
||||
Catching it as `refused user-sudo` keeps every refusal in syslog.
|
||||
- N3. `host-grant.mjs:86,106`: a keyscan that exits nonzero, such as an unreachable host, resolves with
|
||||
no lines, so the raise refuses `host-key-mismatch`. The comment says a scan that can't run refuses
|
||||
`host-failed`. That holds only for a spawn error. G17 doesn't decide which code is right. Worth one line
|
||||
in BUILD.md or the comment.
|
||||
- N4. In the run, `#enrollFailed` writes `grant.failed` with no tag. `reconcileGrants` does tag it. If
|
||||
section 7 leaves the tag optional this is fine. Otherwise the two should match.
|
||||
- N5. `read_conf(host, 'privileged')` refuses when the file is missing (`mosaic-keys-write:136-138`). The
|
||||
#433 run sheet should say to create an empty, root-owned `/etc/mosaic-keys/privileged`.
|
||||
- N6. `#hostAccess` binding the grant's agent and host (`broker.mjs:692`) goes beyond X4. I agree with
|
||||
it. It narrows and refuses nothing X4 allows.
|
||||
- N7. `isHostKind` (`business/src/resolve.mjs:92`) can't be reached and stays, as BUILD.md records. No
|
||||
objection.
|
||||
|
||||
## Checked, no finding
|
||||
|
||||
- `#approvedGrant`, `#consumeGrant`, `#runEnrollment`'s two transactions and their catch, and
|
||||
`reconcileGrants` for both the spent-unfinished and the open decision. `#openGrantDecisions` uses the
|
||||
unfiltered `#spent`. That is sound while no verb consumes `credential.mint`.
|
||||
- `#raiseGrant` order, with the enroll block appended before the 4096 cap (M1, :165).
|
||||
- `hosts.remove` order (host-unknown, host-in-use, host-line-left, then delete, event, unenroll last), and
|
||||
`hosts.check` with its host-changed recheck in the transaction.
|
||||
- 3i-3j.sql: hosts and host_grants keys, CHECKs, the FK, and the `events_grant_actor` trigger. The trigger
|
||||
cases are in `grant-migration.test.mjs:134`.
|
||||
- The writer's dir, lock, user-file and tmp handling (G25, `host-tools.test.mjs:225`).
|
||||
- Hash pins: there is no 64-hex pin in `packages/*/tests/*` or `scripts/test-*.sh` on a changed path. The
|
||||
one basename hit, `channel-proof.test.mjs:12` for `bus/src/human-cli.mjs`, is a file this row doesn't
|
||||
change. The control found pins in account-, comment-migration and channel-proof.
|
||||
- RUNNER_FILES rule (sage-1619): the broker adds the `HOST_ID` import. I ran seat sandbox.test.mjs and
|
||||
bus project-runtime.test.mjs (below).
|
||||
|
||||
## My runs
|
||||
|
||||
Single files in a detached worktree of f4de24d39, pass/fail:
|
||||
|
||||
| file | result |
|
||||
| --- | --- |
|
||||
| bus grant-enroll | 20/0 |
|
||||
| bus host-run | 23/0 |
|
||||
| bus host-tools | 6/0 |
|
||||
| bus grant-migration | 5/0 |
|
||||
| bus hard-waits | 13/0 |
|
||||
| bus project-runtime | 3/0 |
|
||||
| bus credentials | 5/0 |
|
||||
| bus chief-of-staff | 18/0 |
|
||||
| bus account-, comment-, name-, spec-, thread-, usage-migration | 3/0, 5/0, 1/0, 1/0, 3/0, 1/0 |
|
||||
| business hosts | 4/0 |
|
||||
| cli hosts | 5/0 |
|
||||
| seat sandbox | 7/3, the same environmental failure I get on base commits in this session |
|
||||
|
||||
I ran no suite and no mutant. Those are slot work.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Row 234 round 1, content (duckworth-0084)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in sage-1711. Request #1740 comment 29903, revs 2645-2646.
|
||||
Candidate `gizmoduck/row234` 861182fa6 on 8fede5fc6, manifest sha256
|
||||
2114a09d5a1963279f3cb37bd46bbad4d092b48499d867740b64129a928daf71 (39 lines, Gizmoduck's in-review move,
|
||||
rev 2642). Brief: `docs/plans/2026-10-20_host-credentials.md` "Machine grant row a" (blob 64451be5). Design
|
||||
r6 f9039682e, section 7 fa5658941.
|
||||
|
||||
I rebuilt the manifest from the queue text and it has that digest. Each of the 39 digests matches its blob
|
||||
at 861182fa6, and the path set equals `git diff --name-only 8fede5fc6 861182fa6`.
|
||||
|
||||
Per sage-1711, this note reviews content on 861182fa6. Two manifest paths have drifted on refactor since
|
||||
the base: `packages/bus/src/runtime.mjs` (row 242) and `packages/cli/README.md` (row 232). Gizmoduck
|
||||
rebases as `gizmoduck/row234-r2`. I'll check those two merges on round 2 and record the verdict there.
|
||||
|
||||
## Verdict on content: approve
|
||||
|
||||
My reading of the code is in duckworth-0081 (REVIEW-code-early.md, 88402c25e) and duckworth-0083
|
||||
(REVIEW-code-early-2.md, 947d9ce69). Since then:
|
||||
|
||||
1. ad7373c1b..861182fa6 changes three files: BUILD.md, the G10 web3 row (A3b's killer, at
|
||||
`grant-enroll.test.mjs:282-285`), and the "A22b, sage-1703" test in `grant-migration.test.mjs`.
|
||||
- The A22b test writes two session rows at 3i, then migrates them: a `grant.host.removed` with
|
||||
`hostLineLeft`, and a `grant.host-enroll` consume look-alike for decision d1. After the step both
|
||||
rows are still there. `reconcileGrants` ends nothing, and `hosts.remove` still removes web1.
|
||||
- With the actor filter gone from `GRANT_EVIDENCE`, the first row would refuse `host-line-left`, and
|
||||
the second would make d1 look spent and unfinished. So the test pins the filter that the trigger
|
||||
can't stand in for on pre-3j rows.
|
||||
2. The section 10 row a tests are all present. G1, G5, G6, G10, G11 (enroll), G14 to G18, G20, G21 and
|
||||
G25 are there, along with the migration, trigger and reader-filter tests and M1 to M4. None is
|
||||
missing.
|
||||
3. The business and cli diffs, which I hadn't read before:
|
||||
- `business.mjs` refuses `hosts` at the top level and in a role with one message that says where the
|
||||
key went (S1). It no longer builds `host.<id>` kinds from the file (X4).
|
||||
- `hosts.mjs` keeps `checkAddress`, `USER`, `HOST_KEY` and `checkCommands` as exports, which the
|
||||
enrollment raise and row b use, and drops the registry and grant parsers.
|
||||
- `cli/src/hosts.mjs` exits 4 on usage and 2 on a bus refusal, and reads no key.
|
||||
- `scripts/mosaic` sends `hosts` to packages/cli. That's covered at `cli/tests/hosts.test.mjs:67`.
|
||||
4. Mutants, 25/25 on Sage's slot. I read the table and the results (BUILD.md:102-131, 246-262). I didn't
|
||||
run them.
|
||||
- On A9, I don't want a behaviour-level mutant. Without the `hosts` primary key, `host_grants`' FK has
|
||||
no parent key, so a store like that can't open. The behaviour the key backs is the second-enrollment
|
||||
refusal, and A8 (`#hostRecord` in step 1) kills that by assertion in the S1 test. The A9 test
|
||||
(`grant-enroll.test.mjs:314-323`) asserts the constraint by message, so it would also kill a variant
|
||||
that drops the FK too. A9 is a backstop behind A8.
|
||||
5. Hash pins: `packages/*/tests/*` and `scripts/test-*.sh` have no 64-hex pin on any of the new or
|
||||
changed grant, host or tools paths. The positive control from duckworth-0081 still holds.
|
||||
|
||||
## Notes (no block)
|
||||
|
||||
- N10. `cli/src/hosts.mjs` checks the verb with `shape[verb]` on a plain object. So
|
||||
`mosaic hosts constructor web1` passes the usage check and calls `hosts.constructor`. The bus refuses an
|
||||
unknown verb, so the result is exit 2 with the bus's code instead of exit 4 with the usage. Nothing is
|
||||
reached, so `Object.hasOwn(shape, verb)` can wait for a later cli row.
|
||||
- N9 (from duckworth-0083) stays as BUILD.md's follow-up for the #433 VM run.
|
||||
|
||||
## My runs
|
||||
|
||||
At 861182fa6, singly, pass/fail: bus grant-enroll 24/0, grant-migration 6/0, host-run 24/0. My earlier
|
||||
singles at ad7373c1b (0083) and f4de24d39 (0081) cover the files that haven't changed since. I ran no
|
||||
suite and no mutant.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Row 234 round 2, rebase check (duckworth-0085)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in gizmoduck-0113 and sage-1725. Request #1740 comment 29923,
|
||||
revs 2660-2661. Candidate `gizmoduck/row234-r2` b0320d395 on refactor 9b5362018, manifest
|
||||
`/mnt/storage/scratch/gizmo-234-out/manifest-r2.txt`, sha256
|
||||
08306afe4f2d8b01a6edd30b4e40f275d92912ca5d330bcf771e7d14b5beaddd (39 lines). Content approved on 861182fa6 in
|
||||
duckworth-0084 (REVIEW-r1.md, 048d907b2, comment 29919).
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
1. The manifest holds. All 39 digests match their blobs at b0320d395. The path set equals
|
||||
`git diff --name-only 9b5362018 b0320d395` and r1's set. Only `packages/bus/src/runtime.mjs` and
|
||||
`packages/cli/README.md` differ from r1.
|
||||
2. `git range-diff 8fede5fc6..861182fa6 9b5362018..b0320d395` gives 13 `=`.
|
||||
3. The two merges are clean.
|
||||
- For each file, `git merge-file` of 861182fa6 (ours), 8fede5fc6 (base) and 9b5362018 (theirs) exits 0,
|
||||
and its output equals the r2 blob byte for byte. Row 234's +/- lines are identical to r1's.
|
||||
- Since the base, `runtime.mjs` moved only with 5fb736b92 (row 242), and `cli/README.md` moved only with
|
||||
e65a3ab4d (row 232).
|
||||
4. The merged code doesn't interact.
|
||||
- In `runtime.mjs`, row 234's hunks are at :17-200: the host grant store references, `hostGrant`, moving
|
||||
`runRoot` above the Broker, and `reconcileGrants` at start. Row 242's are at :260-332, in the publish
|
||||
socket's `publishRefused` and `settle`. Row 242 adds no use of `runRoot`, `credentials`, `references`,
|
||||
`storeRoot`, `hostGrant` or `reconcileGrants`.
|
||||
- In `cli/README.md`, row 232's units paragraph is at :777 and row 234's hosts section is at :1049.
|
||||
|
||||
## Notes (no block)
|
||||
|
||||
- N11. In my session, `packages/cli/tests/thread-launch.test.mjs` gives 4 pass, 7 fail at b0320d395. Every
|
||||
failure is in the launched run: at :340 the run's claim exits 1, not 21, and the rest time out waiting
|
||||
for a session reply. That matches my session's seat sandbox failure (sandbox.test.mjs 7/3 on base
|
||||
commits). Per sage-1732, this is left to Sage's landing gate, which runs the whole cli suite on this tree
|
||||
from a session where the sandbox works. Gizmoduck's singles (387/0) include this file.
|
||||
- N10 (from 0084) goes to row 235's carry list or a cli follow-up (sage-1719).
|
||||
|
||||
## My runs
|
||||
|
||||
At b0320d395, 30 test files: the ones that import `runtime.mjs` or read `cli/README.md`, plus the row 234
|
||||
and row 242 tests. 29 files pass with 0 failures, 277 tests in all (summed by awk over the logs), including bus grant-enroll 24/0,
|
||||
grant-migration 6/0, host-run 24/0, publish 13/0 and project-runtime 3/0, cli publish-socket 9/0, host 42/0
|
||||
and hosts 5/0. They ran in parallel, not one at a time, and in Researcher's slot without a grant (sage-1721).
|
||||
I reran thread-launch alone afterwards. No suite and no mutant.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Row 234 round 3, schema count tests (duckworth-0086)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in gizmoduck-0120. Scope from sage-1742, sage-1746 and
|
||||
sage-1747. Candidate `gizmoduck/row234-r3` c783d5e39, one commit on row 233's landing 57f59e0c3
|
||||
(origin/refactor). Manifest `/mnt/storage/scratch/gizmo-234-out/manifest-r3.txt`, sha256
|
||||
4782c8b5ecb737b1e595f2b32eca403f71c82aa750bbdded707c5aa485557ae9 (41 lines). Revs 2679 and 2680. Round 2 was
|
||||
approved on b0320d395 (duckworth-0085, rev 2670).
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
1. The manifest holds. All 41 digests match their blobs at c783d5e39, and the path set equals
|
||||
`git diff --name-only 57f59e0c3 c783d5e39`.
|
||||
2. The r2 paths are unchanged.
|
||||
- 38 of the 41 paths have the same blob ids as b0320d395. The three that differ are BUILD.md,
|
||||
`schema.test.mjs` and `stack-release.test.mjs`. Modes match.
|
||||
- 9b5362018..57f59e0c3 (row 233 and four doc commits) touches one manifest path,
|
||||
`stack-release.test.mjs`. So keeping r2's blobs for the other 38 reverts nothing.
|
||||
3. `schema.test.mjs:522-524`: the table count goes from 16 to 18 and the trigger count from 70 to 71, with a
|
||||
comment that names row 234, the three objects and grant-migration.test.mjs. These match 3i-3j.sql's two
|
||||
tables and its one trigger.
|
||||
4. `git diff 57f59e0c3:packages/cli/tests/stack-release.test.mjs c783d5e39:...` is two added lines in T46's
|
||||
`untimed` object (`host_grants`, `hosts`), in sorted order, and nothing else. That follows Sage's ruling
|
||||
(sage-1745).
|
||||
5. BUILD.md's diff from b0320d395 adds lines and removes none. The "Round 3" section holds against the
|
||||
code.
|
||||
- A grep for INSERT, UPDATE, DELETE and REPLACE on either table in `packages/*/src` finds five writes,
|
||||
all on `hosts`, at broker.mjs:737, :763, :783, :932 and :950. Each sits in the transaction and beside
|
||||
the `grant.*` event that BUILD.md names.
|
||||
- `#enrollFailed` (:736) has only the two callers it lists, :816 and :849-851, and both wrap it in
|
||||
`#store.transaction`.
|
||||
- `#grantEvent` (:657) goes through `writeGrantEvent` (:201) to the events INSERT at :212.
|
||||
- Nothing writes `host_grants`. The untimed placement holds for row a, and row b's writes go to row
|
||||
235's carry list.
|
||||
6. The count-pin grep (`sqlite_master|schema\.sql` over `packages/*/tests/*` and `scripts/test-*.sh`)
|
||||
gives 14 files. The positive control, schema.test.mjs, is among them. I checked the hits BUILD.md
|
||||
doesn't name. `stack-release-fixture.mjs:214` and `scripts/test-land-work.sh:254` each look up one
|
||||
named object, and `state.test.mjs:106` lists tables without pinning them. So no other test pins the
|
||||
table list or counts.
|
||||
|
||||
## Notes (no block)
|
||||
|
||||
- N12. BUILD.md's round 3 list of other hits leaves out `stack-release-fixture.mjs:214` and
|
||||
`scripts/test-land-work.sh:254`. Both are fine, as item 6 says. This is doc-only.
|
||||
- Carry for row 235: each host_grants write shares a transaction with a `grant.*` event, so the untimed
|
||||
placement stays true. Also N10 from 0084.
|
||||
|
||||
## My runs
|
||||
|
||||
None this round. Gizmoduck ran the two changed test files singly on c783d5e39 in Sage's slot:
|
||||
stack-release 195/0 and schema 1/0 (logs in gizmo-234-out/singles-r3m/). I didn't run them myself. Sage's
|
||||
landing gate runs the suites.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Review: row 237 round 1, one decision publishes once (duckworth-0069)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Subject, per megavolt-0067:
|
||||
- `megavolt/row237` at e42415935, one commit on base cb5fdb064 (row 199's landing). 4 files, +123.
|
||||
- Manifest `manifest-r1-e42415935.txt`, sha256 bb777cb0…8b7e. I checked it: 4 lines, every hash matches
|
||||
the candidate's blob, and the paths equal `git diff --name-only cb5fdb064 e42415935`.
|
||||
- Base drift: origin/refactor changed only `packages/business/README.md` since cb5fdb064. That isn't a
|
||||
manifest path.
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
This closes my row 199 note N1 (duckworth-0068).
|
||||
|
||||
Checked and holding:
|
||||
- `checkPublish` refuses `publish-busy` (`broker.mjs:1040`) when an open ticket holds the same business
|
||||
and decision. The check and the `#publishes.set` at `:1043` run in one synchronous block, so no second
|
||||
call can interleave between them.
|
||||
- The comparison is exact. `p.decision` is the request's id. `result.decision` is `d.id` from
|
||||
`#authorityFor` (`:821-822`), which `#decision` (`:663-664`) looks up with `id=?`. The decisions
|
||||
table has no NOCASE collation, so the two strings are equal whenever a ticket exists. No alias or
|
||||
case form slips past the check.
|
||||
- Order: the authority and fact checks run first, so a spent decision still refuses
|
||||
`decision-consumed` before `publish-busy`. A within-role publish has no decision and skips the check.
|
||||
- Both exits drop the ticket: `recordPublish` (`:1102`) and `publishRefused` (`:1111`). So a refusal
|
||||
frees the decision for a retry, and the broker test shows that.
|
||||
- Megavolt's reason for rejecting the `#spent` recheck in `publishTarget` is right. Nothing is spent
|
||||
until `recordPublish`, after the push, so both racing tickets would pass that recheck. My N1 offered
|
||||
it as an option, and it wouldn't have closed the race.
|
||||
- Hash pins: no test pins a manifest file. `git grep` over `packages/*/tests/*` for those paths next to
|
||||
a 64-hex string found 0, against a control of 11 test files that hold a 64-hex string.
|
||||
|
||||
My runs, in a detached worktree of e42415935:
|
||||
- `packages/bus/tests/publish.test.mjs`: 13/13.
|
||||
- `packages/cli/tests/publish-socket.test.mjs`: 8/8.
|
||||
- M1 (delete the `publish-busy` line): bus 12/1, cli 7/1, so it's killed in both. `git diff` was
|
||||
clean after the restore.
|
||||
|
||||
## Notes (do not block)
|
||||
|
||||
N1. Residual 2 can still put the commit on a second fork ref.
|
||||
- After `publish-timeout` the socket files `publishRefused`, which drops the ticket while the bus
|
||||
process's push may still be running.
|
||||
- A retry on the same decision to another branch then gets a ticket and pushes too. That's the N1
|
||||
shape again, but only after a timeout.
|
||||
- It's the same scope as before: the approved commit on the agent's own fork. Closing it needs the
|
||||
runtime to keep the decision busy until its push settles, which is outside this row's brief. I'd row
|
||||
it as a follow-up.
|
||||
|
||||
N2. Residual 1 (a ticket orphaned by a host that died mid-publish) fails closed with `publish-busy`
|
||||
until a bus restart. That's acceptable as is.
|
||||
|
||||
N3. Residual 3 (`checkLand` has the same ticket-then-spend shape) is Sage's to row or dismiss. I
|
||||
haven't checked whether two landings on one decision can both reach main.
|
||||
|
||||
Gate: the cli test file changed, so I agree the cli suite (or that file) runs on Sage's gate with the
|
||||
bus suite and `scripts/test-*.sh`.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Row 241 round 1 (duckworth-0080)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in megavolt-0073. Brief: `docs/plans/2026-10-20_agent-credentials.md`,
|
||||
"Brief: row 237 follow-up, one decision lands once, with tests at the broker" (c25785d3d). Candidate
|
||||
`megavolt/row241` 3e3ed303e on f5753c0e8, manifest sha256
|
||||
e82668642589eefca8b22433f1b0c7a24432f1993c0050f0a4b74c1146c93c2f. I rebuilt it from the two blobs at 3e3ed303e,
|
||||
and it matches. The diff is the two manifest paths only (+93), tests and BUILD.md, with no source.
|
||||
|
||||
## Verdict: approve, no findings
|
||||
|
||||
1. The test meets the brief. `land.test.mjs:312` opens two `checkLand` tickets on one decision. The first
|
||||
`recordLand` lands. The second refuses `decision-consumed` at :319. Before `landRefused` it leaves one
|
||||
merge `action.allowed` (commit B), one RESULT and no `action.refused`.
|
||||
- `recordLand` runs inside `#store.transaction` (`broker.mjs:1001`), so a refusal at `#landFacts` rolls
|
||||
back everything.
|
||||
- With the `#spent` line gone, the second call passes every other check in `#landFacts` (same task,
|
||||
same latest push, same reviewed commit) and would record a second merge. So :319 is the assert that
|
||||
pins the recheck, as BUILD.md says.
|
||||
- The tail checks that `landRefused` files the code once and ends the ticket (`land-unknown` after).
|
||||
2. The three guard cites are true at f5753c0e8:
|
||||
- `land-busy` at `git-socket.mjs:351`. The slot is set at :353-354, before the first await, and freed
|
||||
at :358 unless `slot.done`.
|
||||
- The CAS `update-ref main land.commit land.old` at `working.mjs:391`, refusing `base-moved` at :392.
|
||||
The move back is at :396, and `rollback-failed` at :397.
|
||||
- `#spent` at `broker.mjs:963` in `#landFacts`. That function is called from `checkLand` (:950) and
|
||||
`recordLand` (:1006). The brief's :945 predates row 210. BUILD.md records the move.
|
||||
3. The mutant table names real tests and asserts: `land.test.mjs:94`/:144, `working.test.mjs:555`/:567
|
||||
(base-moved) and `git-socket.test.mjs:812`/:825 (land-busy). M2 is killed by the existing test, so
|
||||
working.test.mjs is left alone per the brief. I didn't run the mutants. They are slot work, on Sage's
|
||||
gate.
|
||||
4. No test or script pins a hash of either path. The four hits for `land.test.mjs` in the tests are
|
||||
comments.
|
||||
|
||||
My runs, in a detached worktree of 3e3ed303e: `land.test.mjs` 18/0, and the new test alone 1/0.
|
||||
|
||||
I didn't check BUILD.md's aside that a replayed patch fails `base-conflict` in `prepareLand`. No guard or
|
||||
test here depends on it.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Row 242 round 1 (duckworth-0082)
|
||||
|
||||
Reviewer Duckworth (Opus 5.5), 2026-10-21. Asked in megavolt-0078. Brief:
|
||||
`docs/plans/2026-10-20_agent-credentials.md`, "Brief: row 237 follow-up N1, a retry after publish-timeout
|
||||
pushes once" (9d165be8c). Candidate `megavolt/row242` 635c10924 on 8fede5fc6, manifest
|
||||
`/mnt/storage/scratch/megavolt-r242/manifest-r1-635c10924.txt`, sha256
|
||||
1fbfab6c68e8d76d2d69b13c415257280d4fa8c5584e24d5dfd13dca1420a3da. The three blob digests match the files at
|
||||
635c10924, and the diff is those three paths only (+179/-3).
|
||||
|
||||
## Verdict: approve
|
||||
|
||||
1. The fix holds the decision. `runtime.mjs` `publishRefused` sends a refusal on a ticket with a running
|
||||
push to `entry.held`, so `broker.publishRefused` (`broker.mjs:1125`) doesn't run and the ticket stays in
|
||||
`#publishes`. `checkPublish`'s row 237 check (`broker.mjs:1058`) then refuses another publish on the
|
||||
decision with `publish-busy`. When the push ends, the chained `.then` calls `settle`.
|
||||
2. `settle` closes every way.
|
||||
- A push that reached the fork goes to `recordPublish`, which spends the decision, so a retry gets
|
||||
`decision-consumed`.
|
||||
- A failed push files the socket's code.
|
||||
- A `recordPublish` BusError files that code.
|
||||
- Anything else is logged by name and code only, so no git output and no token.
|
||||
`remotes.delete` runs first, and the `.then` can't reject, so no unhandled rejection is possible.
|
||||
3. The other paths are unchanged or improved.
|
||||
- With no entry (the local `publish()` checks refused before `publishPush`), the call goes to the
|
||||
broker as before.
|
||||
- An entry that has ended but wasn't collected settles at once. Before this change, a push that
|
||||
finished just as the socket gave up had its ref left on the fork and the decision free. Now it's
|
||||
recorded.
|
||||
- The code regex matches the broker's, so a bad code still refuses `invalid-request`.
|
||||
4. The hold is bounded. Each git run in `publishRemote` (`seat/src/working.mjs:460`) has GIT_LIMITS, as
|
||||
BUILD.md:35-36 says.
|
||||
5. The test meets the brief (`publish-socket.test.mjs:302`). It starts a push held in pre-receive, times
|
||||
it out, retries to `other` (`publish-busy` at :322), releases the hook, and waits for one allowed event
|
||||
with the work commit. Then it checks `decision-consumed` at :330, one receive-pack, and the fork's only
|
||||
ref, `refs/heads/coder/feat`. The try/finally releases the hook on a failed assert.
|
||||
6. Both mutants match what they claim. M1 puts back the old export, which is the fix removed, as the
|
||||
brief asks. M2 is hold-only. Both kills are on Sage's slot (sage-1686). I didn't run them.
|
||||
7. The `publish-timeout` text is unchanged (`git-socket.mjs:433-434`). It isn't in the diff.
|
||||
8. No test or script pins a 64-hex hash of `runtime.mjs` or `publish-socket.test.mjs`. The control found
|
||||
pins in account-migration and channel-proof.
|
||||
|
||||
## Notes (no block)
|
||||
|
||||
- N1. Two comments in `packages/cli/src/git-socket.mjs`, which is outside the manifest, are now partly
|
||||
stale:
|
||||
- :39-41 says any refusal after checkPublish "is filed once, by publishRefused". A late recorded push
|
||||
now files no `publish-timeout` at all.
|
||||
- :427-428 says the fork "could then hold the commit with no record". After this row that happens only
|
||||
when `recordPublish` refuses.
|
||||
These are comments only, so they're a follow-up or ride with a later cli row.
|
||||
- N2. When a late `recordPublish` refuses (say `ref-moved`), the ticket closes with a ref on the fork and
|
||||
the decision unspent, so a retry can push a second ref. This is the same residual as the in-band
|
||||
`recordPublish` refusal, and the brief doesn't cover it. Worth a line under BUILD.md's residuals.
|
||||
- N3. I agree with BUILD.md's two residuals: the run isn't told about a late record, and a bus restart
|
||||
mid-hold is the same as today. The RESULT question is Sage's to rule on.
|
||||
- N4. BUILD.md:83-84 says M2 fails at ":327 (the bounded wait at :311 ...)". The wait call is at :327,
|
||||
and :311 is the `assert.fail` inside `until`. That reading is right, the wording is just loose.
|
||||
|
||||
## My runs
|
||||
|
||||
In a detached worktree of 635c10924: `publish-socket.test.mjs` 9/0, the row 242 test alone 1/0, and bus
|
||||
`publish.test.mjs` 13/0. No suite and no mutant. Those are on Sage's gate.
|
||||
@@ -1361,3 +1361,82 @@ are never rewritten or removed; corrections are new entries.
|
||||
2026-10-20T18:34:53Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design review r2 (DESIGN.md blob 3eb718b5e131 at 1a268249e), static | changes: C1 config.json console key fails old loaders (mosaic-config.mjs:113 rejects unknown keys; key add inside phase 2 window, rollback removes it, loader test), C2 app.js signs out only on opaqueredirect so 401 never shows sign-in; notes on tests 15/7/9a, Q-B 200-HTML classification, scratch Console flags; Q-E keep iss/sub escaped and capped; verdict sent to Sage, no Gitea/queue record
|
||||
2026-10-20T18:40:20Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design review r3 (DESIGN.md blob a152fd58e18e at 817e8f51b), static | approve on condition D1 (slot file checks stay out of shared loader, shape-only there) as a line edit; D2 notes: slot root from configDir, scratch dataRoot, name discovery GETs for Mos; C1/C2 and notes verified; Mos C1 sequence confirm still open; no Gitea/queue record
|
||||
2026-10-20T18:41:20Z | Filbert (Opus 5.5, 048f3c63) | Q175 SSO design D1/D2 check (DESIGN.md blob eba861f5d2bb at d55d86bb0), static git diff 817e8f51b..d55d86bb0 | ack: D1 and D2 match; design approved; Mos C1 sequence confirm still open; no Gitea/queue record
|
||||
2026-10-20T18:51:09Z | Duckworth (Opus 5.5) | Gizmoduck federation design r1 review (DESIGN.md blob 0f4570d478f5 at 6925f78f0, code at 9ae8c73ac), static | changes B1-B5 (forwarded user identity, ownership code, business-wide reads, revocation ends no runs reliably, one-uid isolation); notes N1-N9; agents/duckworth/work/federation-r1/REVIEW.md
|
||||
2026-10-20T18:58:59Z | Duckworth (Opus 5.5) | Gizmoduck federation design r2/r2.1 re-review (5ad67f671, parents 60b9b112a, 6925f78f0; code 9ae8c73ac), static | changes B6 only (user from_role breaks the runner reply path, runner.mjs:450 vs broker.mjs:1862); B1-B5 resolved; C1 line edit (systemd-run escape); N10; agents/duckworth/work/federation-r1/REVIEW-r2.md
|
||||
2026-10-20T19:03Z | Duckworth (Opus 5.5) | federation design r3+r3.1 re-review (Gizmoduck 5ad67f671..a2840a6be), duckworth-0003 | changes B7 (R8 gate lacks sudo/ssh privilege-path checks in both isolation forms), B1-B6 resolved, N11 #waiting, N12 thread_shares
|
||||
2026-10-20T19:10Z | Duckworth (Opus 5.5) | federation design r4 re-review (Gizmoduck a2840a6be..a659e41f3), duckworth-0005, plus mos-1007 rulings | changes B8 (hub issuer and TLS front end not in R8 gate), B7/N11/N12 resolved, sudo deviation accepted, N13-N15
|
||||
2026-10-20T19:14Z | Duckworth (Opus 5.5) | row 196 design r1 review (duckworth-0006), Gander 76b5c8d93 | changes B1 (concurrent peer-binding test), Q1 O1 with interpreter pin, Q2 Console provenance approved
|
||||
2026-10-20T19:16Z | Duckworth (Opus 5.5) | federation design r4.1 re-review (duckworth-0007), Gizmoduck a659e41f3..73677b87d | changes, B8 issuer-host point left; TLS, N13-N15 resolved; notes N16-N19
|
||||
2026-10-20T19:18Z | Duckworth (Opus 5.5) | row 193 design r2 review (duckworth-0008), Morgana 19c015741 | changes B1 (5.7 owner-visible reads), B2 (5.7 filters before user threads go live); rulings 1-6
|
||||
2026-10-20T19:19Z | Duckworth (Opus 5.5) | row 196 design r2 re-review (duckworth-0009), Gander 76b5c8d93..5fe259f16 | approve; note N5 interpreter change on restart
|
||||
2026-10-20T19:20Z | Duckworth (Opus 5.5) | federation design r4.2-r4.4 re-review (duckworth-0010), Gizmoduck 73677b87d..b67bc6de9 | changes B9 (user-thread decision route :1683); B8, N16-N19 resolved; note N20
|
||||
2026-10-20T19:22Z | Duckworth (Opus 5.5) | row 193 design r3 re-review (duckworth-0011), Morgana 19c015741..76f4605e1 | changes B3 (5.7 table omits project.file reads), B1 B2 N1-N4 resolved
|
||||
2026-10-20T19:22Z | Duckworth (Opus 5.5) | federation design r4.5 re-review (duckworth-0012), Gizmoduck b67bc6de9..acd5f1f42 | approve, B9 N20 resolved, note N21 R6 notice wording
|
||||
2026-10-20T19:25Z | Duckworth (Opus 5.5) | row 199 design r1 review (duckworth-0013), Megavolt 624e8ea3a | changes B1 (missing store file stops bus start), B2 (token not bound to remote host, redirects), notes N1-N5
|
||||
2026-10-20T19:25Z | Duckworth (Opus 5.5) | row 193 design r4 re-review (duckworth-0014), Morgana 76f4605e1..edc794933 | approve, B3 and N5 resolved
|
||||
2026-10-20T19:29Z | Duckworth (Opus 5.5) | row 199 design r2+r3 re-review (duckworth-0017), Megavolt c2c729ddf | changes B3 (T3 fake is http, https-only flags untested), B4 (decision binds no commit, push by ref name)
|
||||
2026-10-20T19:29Z | Duckworth (Opus 5.5) | row 193 design r5 re-review (duckworth-0018), Morgana e2d161e2e | approve
|
||||
2026-10-20T19:29Z | Duckworth (Opus 5.5) | federation design r4.6 re-review (duckworth-0019), Gizmoduck 1b32e65e3 | approve
|
||||
2026-10-20T19:30Z | Duckworth (Opus 5.5) | federation design r4.7 re-review (duckworth-0020), Gizmoduck 2d3c3cfbd | approve, N22 (R8 checks the mosaic user's properties)
|
||||
2026-10-20T19:31Z | Duckworth (Opus 5.5) | federation design r4.8 re-review (duckworth-0021), Gizmoduck eb651aa68 | approve, N22 resolved
|
||||
2026-10-20T19:33Z | Duckworth (Opus 5.5) | row 199 design r4 re-review (duckworth-0022), Megavolt e1aa6216c | approve, N9 (post-routing source commit)
|
||||
2026-10-20T19:36Z | Duckworth (Opus 5.5) | Q180 tightening plan r2 review (duckworth-0023), ops-01 plan sha256 1084aa15, #421 | changes B1-B5 (lockout proof, new key off factory hosts, live group removal, cloud-init order, unlisted paths)
|
||||
2026-10-20T19:36Z | Duckworth (Opus 5.5) | federation r4.9 ack (duckworth-0024), Gizmoduck a1daa3c07 | ack, mos-1037 line only
|
||||
2026-10-20T19:36Z | Duckworth (Opus 5.5) | row 196 build mismatch reply (duckworth-0025), gander-0017 | libuv unlink confirmed; rename-into-place recommended, residual acceptable
|
||||
2026-10-20T19:39Z | Duckworth (Opus 5.5) | row 199 design r5 review (duckworth-0026), Megavolt 34bfe4a53 DESIGN.md sha256 d0b326bb | changes B5 (D9a ls-remote and push still name the upstream, not the derived fork), B6 (state the URL parse, name rule, case-insensitive owner compare)
|
||||
2026-10-20T19:42Z | Duckworth (Opus 5.5) | row 199 design r6 review (duckworth-0027), Megavolt 4d3007ec9 DESIGN.md sha256 aa458dfe | approve; B5 B6 N10 N11 resolved, D9c normalization claims checked in node
|
||||
2026-10-20T19:44Z | Duckworth (Opus 5.5) | Q180 tightening plan r3 review (duckworth-0028), ops-01 plan sha256 cd7165bf, #421 | changes B6 (root persistence sweep on all four hosts or rebuild 1012); B1-B5 N1-N3 resolved
|
||||
2026-10-20T20:02Z | Duckworth (Opus 5.5) | #423 VM tightening r4 steps 1-3 review (duckworth-0032), ops-01 plan sha256 03cd1075 (amends eadff6b6) | changes B1 (step 2 verify must read sshd -T permitrootlogin and root's authorizedkeysfile); N1-N7; steps 4-5 not reviewed (held, mos-1061/1062)
|
||||
2026-10-20T20:02Z | Duckworth (Opus 5.5) | row 201 FED R1 manifest review r1 (duckworth-0033), Gizmoduck 80fe31071 | approve; digests match, access.test.mjs 24/24 in scratch worktree; deviation 2 reading confirmed; N1 covers without level, N2 checked grants only
|
||||
2026-10-20T20:02Z | Duckworth (Opus 5.5) | federation design r4.10 + r4.11 ack (duckworth-0034), Gizmoduck 3568441ab and b93f098f0 | ack; R8 recorded acceptance, quotes checked vs DECISIONS Q182/Q183, call A blocking agreed, call B correct; N1 N2 doc-only
|
||||
2026-10-20T20:05Z | Duckworth (Opus 5.5) | #423 VM tightening r5 steps 1-3 diff review (duckworth-0035), ops-01 r5 sha256 4cbdc48e on base 03cd1075 | approve steps 1-3; B1 N1-N7 resolved, recount 18 checked; a #424-changed step 1 returns as a diff; N8-N10
|
||||
2026-10-20T20:11Z | Duckworth (Opus 5.5) | row 204 (193a) round 1 manifest review, morgana/row193a 0caf6e2f6, digest d38de8c9 | approve (duckworth-0037), N1-N3 notes, 5/5 and 26/26 single-file
|
||||
2026-10-20T20:14Z | Duckworth (Opus 5.5) | PRD 1.7 draft REQ-CRED-3 review, honker/row194 0bb1a3bd0 sha256 0236dee8; row 204 queue record rev 2335 | changes B1-B2 (duckworth-0038), quotes match DECISIONS 1c368ff
|
||||
2026-10-20T20:16Z | Duckworth (Opus 5.5) | PRD 1.7 draft r2 diff check, honker/row194 11cb0423b sha256 e896746d | approve (duckworth-0039), B1-B2 resolved
|
||||
2026-10-20T20:18Z | Duckworth (Opus 5.5) | row 202 FED R5 round 1 manifest review, quackerjack/fed-r5 030809e15 manifest 959bfb275 | changes B1 (duckworth-0040), BusinessError from loadAccess/loadBusiness escapes uncaught (probed), N1-N9, 18/18 single-file
|
||||
2026-10-20T20:24Z | Duckworth (Opus 5.5) | row 202 FED R5 round 2, Quackerjack candidate 48a3fb96e manifest 7fd265033 | approve (duckworth-0041), B1 resolved, 21/21 single-file, N1-N4
|
||||
2026-10-20T20:34Z | Duckworth (Opus 5.5) | row 210 (#1707) DESIGN.md r1, Gizmoduck 7b10bc0ff | changes B1-B4 (duckworth-0042): argv not in event log, decision stdin unseen, orphan ssh-agent after bus death, section 4 not routed to Jason; N1-N8
|
||||
2026-10-20T20:39Z | Duckworth (Opus 5.5) | row 210 DESIGN r2 diff check, Gizmoduck b726e10f4 | approve with C1 (duckworth-0043): B1-B4 resolved, Q-J0 revoke line must match section 6 before Mos sends it; N1-N5
|
||||
2026-10-20T20:42Z | Duckworth (Opus 5.5) | row 210 DESIGN r3 C1 diff check (duckworth-0044) on Gizmoduck e021dfa26 | C1 met, approve stands, N1
|
||||
2026-10-20T20:43Z | Duckworth (Opus 5.5) | #423 r5 step 4 .53 jarvis sudo and groups (duckworth-0045), reboot timing per mos-1092 | approve with C1-C4, reboot stands in for B3 restart
|
||||
2026-10-20T20:45Z | Duckworth (Opus 5.5) | #423 step 4 scripts (step4.sh, check4.sh, postboot.sh, boot-session optional block) vs duckworth-0045 (duckworth-0046) | C1 C3 met, C2 C4 open, N1-N2
|
||||
2026-10-20T20:47Z | Duckworth (Opus 5.5) | row 195 DESIGN r2 review (duckworth-0047) on Darkwing 818b79696 | changes B1 (.53 activate, gate, stopped-store backup), N1-N6
|
||||
2026-10-20T20:49Z | Duckworth (Opus 5.5) | #423 step 4 revised scripts (step4.sh 5aea3856, check4.sh 8f49a409, containers.sh 837bb7a4, postboot.sh, boot-session f0645604) (duckworth-0048) | C1-C4 met, C5 stale shas in boot-session block, N1-N3
|
||||
2026-10-20T20:51Z | Duckworth (Opus 5.5) | #423 step 4 N1/N2 diff and C5 check, step4.sh cc16d384, boot-session ed44ec81 (duckworth-0049) | holds, no new condition
|
||||
2026-10-20T20:59Z | Duckworth (Opus 5.5) | row 195 DESIGN r3 review, Darkwing 9a16d7795 blob 0fea86616b36 (duckworth-0050) | changes, B1 ACT/TO hand-off, B2 gate poll and PM journal read
|
||||
2026-10-20T21:11Z | Duckworth (Opus 5.5) | row 195 DESIGN r4 review, Darkwing 0075be358 blob 3f8590016b40, with mos-1098 items (duckworth-0051) | changes, B1 rollback open-activation, B2 profile upgrade owner
|
||||
2026-10-20T21:22Z | Duckworth (Opus 5.5) | row 210 round 1 code review (duckworth-0052) on Gizmoduck 12ffe0b56 | changes B1 (sshd skip passes the gate), N1-N5; 17 singles 134/0
|
||||
2026-10-20T21:33Z | Duckworth (Opus 5.5) | row 210 round 2 diff check (duckworth-0053) on Gizmoduck 455964f23 | approve, B1 met (missing sshd fails 13, skipped 0), N1-N3 taken; 17 singles 137/0/0 skipped
|
||||
2026-10-20T21:45Z | Duckworth (Opus 5.5) | row 210 queue record on comment 29702, candidate 455964f23 | approve recorded, rev 2466 (duckworth-r210-record-1); rev left uncommitted for Sage's queue-commit
|
||||
2026-10-20T21:50Z | Duckworth (Opus 5.5) | row 195 DESIGN r5 review (duckworth-0054), Darkwing 0b6d29e90 blob 9395e56d1775 | changes B1 (confirm result line lacks op/from/to/backup, from_log misses .53 activations) B2 (migration record after migrate, partial migrate never restored); r4 B1 B2 N1-N4 met; port squat checked, 732 fix fits
|
||||
2026-10-20T21:55Z | Duckworth (Opus 5.5) | row 195 DESIGN r6 diff check (duckworth-0055), Darkwing b7239bb66 blob 46bbcf2b135e | approve; B1 B2 N1-N5 met; N1 A2f stop timeout now exits 2 (state in run sheet, fix test wording), N2 claude path round separate
|
||||
2026-10-20T22:18Z | Duckworth (Opus 5.5) | machine-grant design r1 review (duckworth-0056, Vikunja #436) on Gizmoduck f6b6a04b6 | changes B1-B5: runtime grant not loaded, revoke race, restart reconcile, tag collision, host root helper unreviewed
|
||||
2026-10-20T22:21Z | Duckworth (Opus 5.5) | machine-grant r1 review correction (duckworth-0056 B2 busy map) | B2 reworded, blocker stands
|
||||
2026-10-20T22:21Z | Duckworth (Opus 5.5) | PRD 1.8 draft review (duckworth-0057, #434) on Honker 3c30f1036 | changes B1-B2: failure clause vs unreachable host, hand-edit grants vs Approve-only; REQ-CRED-4 keeps 210 section 5
|
||||
2026-10-20T22:25Z | Duckworth (Opus 5.5) | PRD 1.8 draft r2 review (duckworth-0058), Honker 60f5ca8fa | approve with C1 (account.create scope of chief-only refusal), N1-N2 |
|
||||
2026-10-20T22:32Z | Duckworth (Opus 5.5) | PRD 1.8 draft r3 diff check (duckworth-0059) on Honker d16ccdb91 | changes B1: r3 still allows hand-written grants against mos-1129; C1 met; my r2 N2 withdrawn |
|
||||
2026-10-20T22:32Z | Duckworth (Opus 5.5) | machine-grant design r3+r4 review (duckworth-0060) on Gizmoduck 1c5705d6d | changes B1-B2 (enrollment recheck, claim before consume); r1 B1-B5 still met; r4 migration lines fine; unsent r2 draft dropped |
|
||||
2026-10-20T22:33Z | Duckworth (Opus 5.5) | PRD 1.8 draft r4 diff check (duckworth-0061) on Honker 781d91624 | approve, 0059 B1 met, N1 two stale notes (doc-only) |
|
||||
2026-10-20T22:38Z | Duckworth (Opus 5.5) | machine-grant design r5 diff check (duckworth-0063), Gizmoduck 7686fd783 | approve with C1 (commands-change undo and reconcile must not revive a revoked grant), N1 orphan key file |
|
||||
2026-10-20T22:39Z | Duckworth (Opus 5.5) | machine-grant design r6 diff check (duckworth-0064), Gizmoduck f9039682e | approve, 0063 C1 met (update-only undo, late abort ignored, G28, 4 mutants), N1 start sweep taken |
|
||||
2026-10-20T22:53Z | Duckworth (Opus 5.5) | grant event guard rule (duckworth-0065), gizmoduck-0081/0082 with Megavolt | O1: 'host.enroll.*' misses host.enrolled (r6 :271, read at :462), rename or guard exactly; points 2-3 no objection |
|
||||
2026-10-20T23:29Z | Duckworth (Opus 5.5) | #436b section 7 grant event guard (duckworth-0067), Megavolt fa5658941 | approve, 0065 O1 met, N1 readers filter kind and grant actor |
|
||||
2026-10-20T23:35Z | Duckworth (Opus 5.5) | row 199 build r1 review (duckworth-0068), Megavolt 86963d281 manifest 35f900fd, request 29778 | approve; deviations 16, 17 and 13 accepted; N1 one decision to two fork refs, N2 issue needs a loaded entry (row 194), N3 business README hard-wait list doc-only |
|
||||
2026-10-21T00:34Z | Duckworth (Opus 5.5) | row 237 round 1 review (duckworth-0069), Megavolt e42415935, manifest bb777cb0 | approve, M1 killed in both files, N1-N3 non-blocking |
|
||||
2026-10-21T01:11Z | Duckworth (Opus 5.5) | row 194 scoped check of row 199 hunks (duckworth-0070), Honker b39766112 | no blocker in scope, N2 restart route met, N1-N2 notes |
|
||||
2026-10-21T01:12Z | Duckworth (Opus 5.5) | row 210 round 4 diff (duckworth-0071), Gizmoduck 3cf63efb4..c43008749, B1 import fix | approve, Sage's four checks met, roles fix confirmed needed |
|
||||
2026-10-21T01:14Z | Duckworth (Opus 5.5) | PRD 1.8 r7 scoped check (duckworth-0072) on honker/prd18-r7 47c2cb33c, sage-1624/1627 | passes the scope, no findings, citations true at 011d41ba6 |
|
||||
2026-10-21T01:18Z | Duckworth (Opus 5.5) | PRD 1.8 r7 + follow-up scoped check (duckworth-0073), Honker 762a6711f..82df17347, sage-1632/1633 | passes the scope, no findings, four mos-1152 edits exact, includes 0072 |
|
||||
2026-10-21T01:26Z | Duckworth (Opus 5.5) | row 194 b3 N1 delta scoped check (duckworth-0074), Honker a61a49a51..86889c59e, sage-1634 | N1 met, no findings; row 210 r2 approve recorded rev 2592 |
|
||||
2026-10-21T01:29Z | Duckworth (Opus 5.5) | PRD 1.7+1.8 landing review (duckworth-0075), Honker 46c8adaeb on 011d41ba6, sage-1636/1641 | approve, 42 items word-exact, no allow/forbid change, N1 CRED-4 title |
|
||||
2026-10-21T01:32Z | Duckworth (Opus 5.5) | REQ-CRED-4 title delta (duckworth-0076), Honker 46c8adaeb..c03d660e7 | approve, N1 title drops "only", 117-char wording offered |
|
||||
2026-10-21T01:34Z | Duckworth (Opus 5.5) | REQ-CRED-4 title follow-up 2884ee496 (duckworth-0077) | approve, N1 resolved, no notes |
|
||||
2026-10-21T01:59Z | Duckworth (Opus 5.5) | row 210 queue round 3 rebase check on Gizmoduck 0485384e9 (duckworth-0078) | approve, no findings |
|
||||
2026-10-21T03:01Z | Duckworth (Opus 5.5) | row 234 early check of BUILD.md split and name map on Gizmoduck 2aa54a67a (duckworth-0079) | split and map stand, G21 fixture ok on F1-F4, moves M1-M4, N1 mutant split |
|
||||
2026-10-21T03:03Z | Duckworth (Opus 5.5) | row 241 round 1 on Megavolt 3e3ed303e (duckworth-0080) | approve, no findings |
|
||||
2026-10-21T03:44Z | Duckworth (Opus 5.5) | row 234 early code read on Gizmoduck f4de24d39 (duckworth-0081) | changes: B1 G10, B2 G18, B3 G6 withdraw/arbiter and G20 human leg untested, N1-N7 |
|
||||
2026-10-21T03:46Z | Duckworth (Opus 5.5) | row 242 round 1 on Megavolt 635c10924 (duckworth-0082) | approve, N1-N4 |
|
||||
2026-10-21T03:51Z | Duckworth (Opus 5.5) | row 234 early read follow-up on Gizmoduck ad7373c1b (duckworth-0083) | B1-B3 resolved, N8-N9 |
|
||||
2026-10-21T04:53Z | Duckworth (Opus 5.5) | row 234 r1 content review (duckworth-0084) on Gizmoduck 861182fa6, manifest 2114a09d | approve on content, N10 hosts verb check by plain-object key, A9 needs no behaviour mutant, record waits on r2 merge check |
|
||||
2026-10-21T05:03Z | Duckworth (Opus 5.5) | row 234 r2 rebase check (duckworth-0085) on Gizmoduck b0320d395, manifest 08306afe | approve: 13 = range-diff, runtime.mjs and cli/README.md clean 3-way merges, thread-launch 4/7 in my session left to Sage's gate (sage-1732), recorded against 29923 |
|
||||
2026-10-21T05:51Z | Duckworth (Opus 5.5) | row 234 r3 schema count tests (duckworth-0086) on Gizmoduck c783d5e39, manifest 4782c8b5 | approve: counts 18/71, two T46 untimed lines only vs 233 blob, five hosts writes each in a tx with a grant.* event, 38 r2 blobs unchanged; no runs of mine; record waits on Sage's comment id |
|
||||
|
||||
Reference in New Issue
Block a user