Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f488b4ea7 | ||
|
|
660d25e656 | ||
|
|
15c09fd7b5 | ||
|
|
ebb3d83bf6 | ||
|
|
a11a9926cd | ||
|
|
5d8f0f4b54 | ||
|
|
ef2098c967 | ||
|
|
bb7c028852 | ||
|
|
de135fc029 | ||
|
|
ef63393ee4 | ||
|
|
bbca2935ba | ||
|
|
191048fbec | ||
|
|
4a3dc25763 | ||
|
|
bef9a62f4c | ||
|
|
a0a00630ea | ||
|
|
efd6c1f028 | ||
|
|
02bb0afb3a | ||
|
|
387f04dfda | ||
|
|
7a542c0ea2 | ||
|
|
858f773625 | ||
|
|
e10a6f1166 | ||
|
|
bba870ee9c | ||
|
|
2fb6815963 | ||
|
|
57da6fda51 | ||
|
|
361bd753fa | ||
|
|
69d5b39600 | ||
|
|
4e931c4f1f | ||
|
|
d2344ff8bf | ||
|
|
3ab3c010b1 | ||
|
|
deda9f48e8 | ||
|
|
433a95292e | ||
|
|
7a9a90a7a2 | ||
|
|
2f00563249 | ||
|
|
2a216fd169 | ||
|
|
928405649c | ||
|
|
2dfc1b9ad6 | ||
|
|
5bd27e804b | ||
|
|
449c3458ff | ||
|
|
82f50d1273 | ||
|
|
2beb49daeb | ||
|
|
d37be6ff29 | ||
|
|
a5643f9bd7 | ||
|
|
9705a6aaca | ||
|
|
1f404c973b | ||
|
|
b2793f2aa1 | ||
|
|
f942bd7d43 | ||
|
|
425900b036 | ||
|
|
85a72842c3 | ||
|
|
19bb3ae364 | ||
|
|
216302d65f | ||
|
|
0d0df1ecee | ||
|
|
dba33aaf60 | ||
|
|
0597cc1988 | ||
|
|
c7a672d622 | ||
|
|
04e78354a6 | ||
|
|
fcac66f7bf | ||
|
|
ca6b32d668 | ||
|
|
5a02ee3954 | ||
|
|
e1e64ff011 | ||
|
|
6d2e502728 | ||
|
|
c9d65664af | ||
|
|
18f5daaaef | ||
|
|
822bf1aa04 | ||
|
|
162ce46dbc | ||
|
|
dada066753 | ||
|
|
9afb947c73 | ||
|
|
8d53b93c78 | ||
|
|
365a763122 | ||
|
|
f9d218ab1e |
@@ -16,15 +16,6 @@ not fleet seats. Keep changes in `/mnt/storage/src/mosaic-stack`; do not modify
|
||||
| Rocko | General development, investigation, testing, and review | T3 session (Codex, gpt-6.1-sol, thread d99326cc, lead decision 122; Claude thread b84bb264 retired 2026-10-12); launcher Claude Code, Sonnet model | `agents/rocko/launch.sh` |
|
||||
| Filbert | General development, investigation, testing, and review | T3 session (Claude, Opus 5.5, thread 048f3c63, non-sandbox reviews only, lead decisions 130 and 134; Codex threads f712d8af and 50342f6a retired 2026-10-13, Claude thread 9cb9731e retired 2026-10-12); launcher Pi, `openai-codex/gpt-6-astra:low`, not to be used under R26 until it changes (lead decision 69) | `agents/filbert/launch.sh` |
|
||||
| Researcher | Source-grounded technical research and evidence | T3 session (Claude, Sonnet 5.5, thread 9bac0794, rows 97 and 98, lead decision 124); Haiku rows (71, 80, 84, 88, later 96) run as subagents Sage dispatches with model haiku, effort high and a worktree (lead decision 129); Haiku thread c98aaf4a retired 2026-10-13 unused; launcher Pi, configured Mosaic model | `agents/researcher/launch.sh` |
|
||||
| Launchpad | Builder, MVP T1 row 192 (Q175 SSO phase 1) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| Morgana | Builder, MVP T1 row 193 (human users and RBAC) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| Honker | Builder, MVP T1 row 194 (agent lifecycle) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| Gander | Builder, the human boundary across uids (P19 333, #1694) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
|
||||
| Herb | Builder, a second gate lane (#1695) | T3 session (Codex, gpt-6.1-sol), started by Sage under mos-0977 | none, T3 only |
|
||||
| Bushroot | Builder, retire the control board writer on 7331 (#1696) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1008 | none, T3 only |
|
||||
| Megavolt | Builder, MVP T1 item 6 (agent credentials and git identity, #1697) | T3 session (Claude, Opus 5.5), started by Sage under mos-0974/0977 | none, T3 only |
|
||||
| Webby | Reviewer: Case 2 designs, rows 192 and 194 | T3 session (Claude, Opus 5.5, thread 03086006), started by Mos (mos-0981) | none, T3 only |
|
||||
| Duckworth | Reviewer: federation designs, row 193 | T3 session (Claude, Opus 5.5, thread ffceafec), started by Mos (mos-0977) | none, T3 only |
|
||||
| Sage | Project lead: coordination, review, integration; earlier DYOR strategy records retained | T3 session (Claude Code); Pi launcher `zai/glm-5.3:high` retained | `agents/sage/launch.sh` |
|
||||
|
||||
Each script supports `--check` and `--fresh`. Normal launches resume the agent's
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Bushroot
|
||||
|
||||
Builder seat, Claude Haiku 5.5, started by Sage under mos-1008 item 2 (a low-level model under Q177) for the row
|
||||
that retires the control board writer on 7331 (#1696).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `bushroot/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Duckworth
|
||||
|
||||
Reviewer seat, Claude Opus 5.5, started by Mos (mos-0977, thread ffceafec). Reviews federation designs and MVP T1 row 193.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `duckworth/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,8 +0,0 @@
|
||||
# Gander
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under mos-0977 item 5 (more Codex Sol rows) for the human
|
||||
boundary row (P19 333, #1694, mos-0989).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `gander/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,8 +0,0 @@
|
||||
# Herb
|
||||
|
||||
Builder seat, Codex gpt-6.1-sol, started by Sage under mos-0977 item 5 (more Codex Sol rows) for a second gate lane
|
||||
design row (#1695, mos-0977 item 6).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `herb/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Honker
|
||||
|
||||
Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 194 (agent lifecycle, #1692) under mos-0974 and mos-0977.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `honker/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Launchpad
|
||||
|
||||
Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 192 (Q175 SSO phase 1, #1690) under mos-0974 and mos-0977.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `launchpad/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,8 +0,0 @@
|
||||
# Megavolt
|
||||
|
||||
Builder seat, Claude Opus 5.5, started by Sage under mos-0974 and mos-0977 item 5 for MVP T1 item 6, the
|
||||
credential store per agent with its own git identity (#1697).
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `megavolt/*`
|
||||
branches through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,10 +0,0 @@
|
||||
# #436b build carry items
|
||||
|
||||
The design is approved at r5: 1be535aca, DESIGN.md sha256 68df02cdf890564c6ed0a5426204114389ba7eb0eea1cf04f1bf5938b6de45f4. Dewey gave the approval in dewey-0763 (verdict sha256 de52be1b…6b68). Section 7 is approved at fa5658941 by duckworth-0067 and is unchanged since. DESIGN.md stays as approved. The row's build takes the items below, and its BUILD.md cites this file.
|
||||
|
||||
- N9 (dewey-0763). Revoke writes `grant.revoked`, not a `grant.step`. A restart after a revoke on an open rotation therefore still sees a `commit` with no settle after it, and settles again: no file, `grant-lost`, a wrong message to Jason, and repeated deletes that come back 404 and go to `serviceLeft`. Two fixes:
|
||||
- The section 5 restart row "`commit`, rotation, settle-pending" takes the held row's clause "with no `grant.revoked` after it".
|
||||
- A 404 on a delete by id counts as done, not as a `serviceLeft` entry. That applies to revoke, sweep and the settle deletes.
|
||||
- Add a test: revoke on an open rotation, then a restart. No settle runs, no Gitea call is made, and the restart sends no message about it.
|
||||
- N10 (dewey-0763). `grant.revoked {serviceLeft}` can now hold two ids, so it is a list of ids everywhere. `sweep` reads it as one.
|
||||
- duckworth-0067 N1 is in r4 (section 5, X-G15). Nothing more is carried for it.
|
||||
@@ -1,640 +0,0 @@
|
||||
# Gitea grant path, design draft r5 (Vikunja #436b, Gitea #1697 follow-up)
|
||||
|
||||
Author: Megavolt. Reviewer: Dewey. GitHub half: Bushroot. Base: Gizmoduck's #436a r6 (f9039682e, final,
|
||||
`agents/gizmoduck/work/machine-grant/DESIGN.md` on origin/gizmoduck/machine-grant-design) and row 199's
|
||||
DESIGN.md section 7a (e10e03f6b). Mos rulings used here: mos-1130 and mos-1136.
|
||||
|
||||
This draft builds nothing. It says how an Approve in Jason's inbox makes an agent's Gitea user, its token and
|
||||
its forks with no manual step, and it answers mos-1130 item 3 (a to e) on the executor's admin credential.
|
||||
|
||||
## r5 changes (dewey-0761 on r4 3e66f5acf)
|
||||
|
||||
Section 7 is unchanged from fa5658941.
|
||||
|
||||
- Item 1 (blocks). A revoke on an open rotation deletes both tokens. When the last `grant.step` is a
|
||||
rotation's `commit` with no settle after it (settle-pending or `grant-token-unknown`), section 6 step 3
|
||||
deletes `previousTokenId` and the minted `tokenId`, each failure to `serviceLeft`. Section 5's
|
||||
`grant-token-unknown` text now says both (X-G5).
|
||||
- N8. Step 0 refuses `grant-token-unknown` while the agent is held, so a revoke is the only way out.
|
||||
|
||||
## r4 changes (dewey-0759 on r3 fa5658941, sage-1578, duckworth-0067)
|
||||
|
||||
Section 7 is unchanged from fa5658941, the text Duckworth approved and Gizmoduck cites.
|
||||
|
||||
- Item 1 (blocks), option (b), Sage's ruling in sage-1578. A step 0 refusal leaves the decision unspent only
|
||||
until the next start. #436a r6 X3's start reconcile spends every resolved-yes grant decision with no
|
||||
`action.allowed` as `interrupted`, and this draft no longer claims otherwise. `grants run` works while the
|
||||
bus that refused is still up; after a restart the chief raises again and Jason approves again. `status`
|
||||
says so on each open decision (D5, section 4 step 0, section 5, X-G11). (a) would change #436a r6, which is
|
||||
final. The cost is in D5.
|
||||
- Item 2 (blocks). A settle that gets 200 with an `id` that is neither token no longer leaves the file in use.
|
||||
Memory drops the credential (199 `revoke`'s memory step), the file and both recorded tokens stay, nothing
|
||||
is deleted at Gitea, and the step ends `grant.failed {code: 'grant-token-unknown'}` for Jason. A restart
|
||||
keeps the credential out of memory (`Credentials` `held`, built in #436b's row like `reload`). Revoke clears
|
||||
it (section 5, X-G6).
|
||||
- N7. 3d says the private session-less writer runs `#secretCheck` on every guarded body, as `#event` does
|
||||
(`broker.mjs:559`). The sentence is in 3d, so section 7 doesn't change.
|
||||
- duckworth-0067 N1. Every reader of grant evidence filters on `kind = 'action.allowed'` and the grant actor
|
||||
pair, so a `grant.*` name under another kind is not evidence (section 5, X-G15).
|
||||
|
||||
## r3 changes (dewey-0756 on r2 6db7cc45d, gizmoduck-0081 and -0084, duckworth-0065, mos-1144)
|
||||
|
||||
- B1 (blocks). Only `#consumeGrant`'s own record carries `decision`. Every other `grant.*` body names the
|
||||
decision `grantDecision`, so `#spent` sees exactly one spend. A step 0 or step 1 refusal leaves the
|
||||
decision unspent (section 7, X-G11, a mutant).
|
||||
- B2 (blocks). Settle with the seat token is proven on scratch (F13): `GET /api/v1/token` answers 200 with the
|
||||
minted id and 401 after the token is deleted. Only a 401 ends `grant-lost`. 403, 404, 5xx, a timeout, an
|
||||
unreachable Gitea or a failed C1 to C4 leave the step open as `settle-pending`, delete nothing, and retry
|
||||
on the next start and in `grants sweep` (section 5, X-G6). `Credentials.reload` is built by #436b's own
|
||||
row in `packages/bus/src/credentials.mjs`; row 199's candidate doesn't change.
|
||||
- B3 (blocks). Step 5 compares against the user's forks of every upstream in the agent's `publish` list, not
|
||||
only `target.projects`. The rendered block names those other upstreams as reach the token may have. A fork
|
||||
whose upstream has left `publish` refuses `gitea-reach-wider` with its name (section 2, step 5, X-G12).
|
||||
- Section 7, the guard (gizmoduck-0081, -0084, duckworth-0065): one pattern, `grant.*`, on `body.action` and
|
||||
on `body.operation` (Dewey N2: the consume record's `operation: 'grant.<method>'`). #436a renames its
|
||||
`host.enroll.step`, `host.enrolled`, `host.checked` and `host.removed` into `grant.host.*`, so row 210's
|
||||
`host.sudo` and `host.run` stay outside. The grant actor is `actor_role NULL, actor_run 'grant'`, which no
|
||||
session or human event can produce (answers Dewey N1, same safety as `_bus`).
|
||||
- D12 / Q-B4 closed by mos-1146 (section 8).
|
||||
- N3. Step 5 reads every page of `/user/repos` and checks the total. N4. One nonce format, and the adopt
|
||||
check compares the lowercased login. N5. `grants run` takes the `#inflight` claim. N6. One open-decision
|
||||
query, `openGrantDecisions`, for `status`, rotate's void count and the start reconcile (section 7).
|
||||
|
||||
## r2 changes (from r1 714c07304, Dewey's dewey-0754, verdict sha256 20d63dfb…)
|
||||
|
||||
- D1 (blocks). `gitea_users` records the Gitea user `id`, and every later grant, rotation and fire compares it
|
||||
(step 2). A nonce goes in `grant.step user` before the create and in the user's `full_name`, so a restart
|
||||
adopts a user it made and refuses anything else (section 5, X-G6). Both are proven on scratch (F8, F9).
|
||||
- D2 (blocks). The audit records are `action.allowed` bodies with reserved `grant.*` actions, plus a trigger
|
||||
in the store migration that only `system/grant` writes them (section 7). A call and its result are two
|
||||
records paired by `callId`, because `events` is append-only (3d). To settle once with Gizmoduck for #436a.
|
||||
- D3 (blocks). Seat-token rotation gets its own rows. After `issue` has renamed the file, the executor settles
|
||||
by asking Gitea which token the file holds (`GET /api/v1/token`, F7), and goes forward or back on that
|
||||
answer, never by guessing (section 5).
|
||||
- D4 (blocks). 3c and section 8 now state the real gap. Reading the admin file gives instance admin, which is
|
||||
more than a forged Approve gives. `write:admin` resets any user's password, email and admin flag, proven on
|
||||
scratch (F10). The separate uid is deferred on cost, and the draft says so.
|
||||
- D5. The admin checks run before `#consumeGrant`, so a predictable refusal leaves Jason's Approve unspent. A
|
||||
human `mosaic grants run <decision>` starts it again (step 0). The raise refuses when the admin slot is past
|
||||
its rotate-by date. r4 (sage-1578): unspent only until the next start, when #436a r6 X3's reconcile spends
|
||||
it as `interrupted`. The cost: if a step 0 refusal is followed by a restart before Jason rotates the admin
|
||||
token or upgrades Gitea, the chief raises again and Jason approves again.
|
||||
- D6. One list of start checks (3a, 3e). The "no organization" claim is now part of the bootstrap promise,
|
||||
and the check is that `GET /user/repos` is empty, which catches org and collaborator repositories (F11, and
|
||||
F12 for a site-admin token).
|
||||
- D7. Admin rotation: a timestamped name (F9: names are unique per user), `--rotate-by` required, and `setAt`
|
||||
changes, so open approvals are voided and `status` says so.
|
||||
- D8. The restart's remote undo runs the same admin checks and writes the same call records.
|
||||
- D9 (Q-B1). On fire: `prohibit_login` (F10) and delete every token the user has, not only the recorded one.
|
||||
- D10 (Q-B2). Step 5 checks the seat token's real reach with `GET /user/repos` (F11). Section 8 says reach can
|
||||
grow later.
|
||||
- D11. One Gitea per business, stated, and checked at `set` and at raise.
|
||||
- D12. Section 8 asks for the jason.woltje comparison. I can't check it myself.
|
||||
|
||||
## 0. What this adds to #436a
|
||||
|
||||
#436a section 11 left `method: "gitea"` for a later draft, because the executor needs an admin credential and
|
||||
roles may never hold the admin scope (`packages/business/src/vocabulary.mjs:108-112`). That rule stays. The
|
||||
admin credential here is not a role service: no `useFor` reaches it, no definition can list it, and only the
|
||||
executor reads it.
|
||||
|
||||
Everything in #436a's sections 4 and 5 holds unless this draft says otherwise: the `credential.mint` raise
|
||||
with a `grant` body, chief only (R3), the rendered block (R4), Jason's Approve via cli (R5, X1),
|
||||
`#consumeGrant` with `operation: 'grant.gitea'`, the `#inflight` claim and abort (X5), `grant.step` records and
|
||||
restart reconcile (X3), and revoke local first (section 6).
|
||||
|
||||
What row 199 already gives: the agent's `identity = {name, email, gitea: {url, user}}`, its
|
||||
`publish: [project]` grant and each project's `remote` (199 D8, D9), the store and `issue(instance, service,
|
||||
token, date, commit)` (`packages/bus/src/credentials.mjs:156`), and section 7a's rule that `issue` accepts a
|
||||
resolved `credential.mint` decision as the human path. `issue` writes the file (tmp, fsync, rename), then calls
|
||||
`commit`, then fills memory (`credentials.mjs:156-193`). Section 5 depends on that order.
|
||||
|
||||
## 1. Facts, proven on a scratch Gitea
|
||||
|
||||
Run on gitea/gitea:1.27.1 (sha256:fe4e4b1c8b93) on 127.0.0.1, throwaway data, container removed after each
|
||||
run. Scripts and logs: `/mnt/storage/scratch/megavolt-r199/g436b/`. No log holds a token or password value.
|
||||
F1 to F6 were reported in megavolt-0034, and Mos accepted them in mos-1136.
|
||||
|
||||
- F1 (`run4.log`). `POST /api/v1/users/{u}/tokens` with `Authorization: Basic <admin>:<admin token>` creates a
|
||||
token for another user: 201. The same token in a `token` or `Bearer` header gets 401 on that route, while the
|
||||
`token` header works on `GET /api/v1/user` (200). So the executor sends Basic on the token routes and
|
||||
`token` everywhere else.
|
||||
- F2 (`run4.log`). The child-scope cap: a child scope broader than the parent's gets 403 ("cannot create an
|
||||
access token with a broader scope than the authenticating token"), for `write:organization` and for `all`.
|
||||
A narrower one gets 201.
|
||||
- F3 (`run4.log`). TOTP 2FA on the admin account blocks password Basic (401) and doesn't block token Basic
|
||||
(201).
|
||||
- F4. The route's check is `reqSelfOrAdmin`: whether the account is a site admin, not the token's scope. Any
|
||||
`write:user` token on a site-admin account can mint a token for any user.
|
||||
- F5. `write:X` implies `read:X` under Gitea's `HasScope`.
|
||||
- F6. The cap is Gitea #38406, backported in #38426 (de4b8277e9 on release/v1.27, 2026-07-12). It first ships
|
||||
in v1.27.0. v1.27.0-rc0 lacks it, and so does every 1.24.x, 1.25.x and 1.26.x through 1.26.4.
|
||||
- F7 (`introspect.log`). `GET /api/v1/token` with the `token` header gives 200 with `id`, `name`, `scopes`,
|
||||
`user`, `created_at` and `last_used_at`, and no token value. With Basic it gives 404. Scopes come back
|
||||
normalized (3b).
|
||||
- F8 (`r2probe.log` 1a-1d). `POST /admin/users` with a `full_name` gives 201 with the user's `id` and that
|
||||
`full_name`. `GET /users/SEAT-ONE` finds `seat-one` with the same `id`: names match case-insensitively. A
|
||||
create of `Seat-One` next to `seat-one` gets 422.
|
||||
- F9 (`r2probe.log` 2a-2c). A second token with a name already used by that user gets 400. `GET
|
||||
/users/{u}/tokens` with Basic lists the user's tokens.
|
||||
- F10 (`r2probe.log` 4a-4f, 5a). With `write:admin`, `PATCH /admin/users/{u}` on another site admin sets a new
|
||||
password (200; the new password then signs in, the old one gets 401) and a new email. On a plain user it sets
|
||||
`admin: true`, then `admin: false` with `prohibit_login: true`. Without `write:admin` the same PATCH gets 403.
|
||||
- F11 (`reach.log`). With the seat token (`write:repository`, `read:user`), `GET /user/repos` lists the user's
|
||||
own fork, then also a repository it was made collaborator on, then also an org repository after it joined an
|
||||
org team. `GET /user/orgs` and `GET /users/{u}/orgs` need `read:organization`, which neither token holds.
|
||||
- F12 (`c4.log`). With a site-admin account's three-scope token, `GET /user/repos` is empty on an instance
|
||||
where other users and an org it isn't in hold public and private repositories: being site admin doesn't add
|
||||
them. It lists a repository once the account is a collaborator, and an org repository once it joins that
|
||||
org's team. Joining an org with no repositories leaves it empty, and `GET /user/orgs` gets 403 (no
|
||||
`read:organization`).
|
||||
- F13 (`b2.log` b1-b7). Two seat tokens minted (201, ids 2 and 3). `GET /api/v1/token` with each token in the
|
||||
`token` header gives 200, and the `id` is the minted one for both. After `DELETE` of seat-a's token (204), the
|
||||
same call with it gives 401. After `prohibit_login` on seat-b (200), the call with seat-b's token gives 403
|
||||
("This account is prohibited from signing in"), not 401. A value never issued gives 401. So 401 is the only
|
||||
answer that means the token is gone; 403 means the account is shut, and section 5 leaves it `settle-pending`.
|
||||
|
||||
## 2. The binding
|
||||
|
||||
The shared part (section 10) is proposed to Bushroot in megavolt-0036. This section is the Gitea half.
|
||||
|
||||
```
|
||||
grant: { "method": "gitea", "agent": "<instance>", "rotateBy": "YYYY-MM-DD",
|
||||
"target": { "projects": ["<project>", ...] } }
|
||||
```
|
||||
|
||||
Code fixes the scopes of the agent's token: `write:repository` and `read:user`, row 199 A2's set. The raise
|
||||
can't name scopes, so the chief can't widen them.
|
||||
|
||||
Checks at raise, inside the raise transaction. Each refusal stores nothing:
|
||||
|
||||
- `agent` is a role instance of this business, its definition declares `{"service": "gitea"}`, and it has
|
||||
`identity` and a `publish` grant (199 D8). Otherwise `gitea-not-declared`.
|
||||
- Each project in `target.projects` is in the agent's `publish` list (`remote-not-granted`), and its
|
||||
`remote`'s origin equals `identity.gitea.url`'s (`remote-host-mismatch`), the same checks as 199 D9 step 2.
|
||||
- The business has an admin slot for `gitea` whose recorded `url` has the same origin
|
||||
(`grant-admin-missing`), and the slot isn't past its rotate-by date (`grant-admin-rotation-due`).
|
||||
- `rotateBy` is inside 199's rotation window.
|
||||
|
||||
The rendered block, appended by the broker as #436a R4 does: agent, Gitea user, email, Gitea URL, each fork
|
||||
URL it will make (`<url>/<user>/<repo>`), the two fixed scopes, the rotate-by date, one of "new Gitea user",
|
||||
"existing stack-made user, new token" or "token rotation" (step 2), and the line "The token can write every
|
||||
repository this user can write. Step 5 checks that this is only its forks." Then, when the agent's `publish`
|
||||
list has projects outside `target.projects`, the line "It may also reach this user's existing forks of:" and
|
||||
each such upstream. The raise makes no network call, so the block names every such upstream, not only those
|
||||
with a fork (B3). The 4096-character cap applies as in R4.
|
||||
|
||||
The raise event body gets `grant = {method, agent, target, rotateBy, definitionSha256, identitySha256,
|
||||
adminSha256}`:
|
||||
|
||||
- `definitionSha256` as in #436a.
|
||||
- `identitySha256` over the agent's `identity`, its `publish` list and the `remote` of every project in
|
||||
`publish` (step 5 checks against all of them).
|
||||
- `adminSha256` over the admin slot's metadata (`url`, `account`, `setAt`), never over the token.
|
||||
|
||||
At step 1 any of the three that differs refuses `decision-mismatch`. So a changed identity, publish list,
|
||||
upstream or admin credential between raise and Approve voids the Approve (REQ-GRANT-2).
|
||||
|
||||
## 3. The executor's admin credential (mos-1130 item 3)
|
||||
|
||||
### 3a. A dedicated account
|
||||
|
||||
One Gitea account per Gitea instance, named `mosaic-grant` by default. It is a site admin and has TOTP 2FA on
|
||||
(F3: the token path still works). Its password and TOTP secret stay with Jason and never reach the stack. It
|
||||
is never jason.woltje or any other human's account. It owns no repositories, belongs to no organization and is
|
||||
no repository's collaborator.
|
||||
|
||||
The admin checks, one list, run at `set` (3e), before every executor start (step 0) and before every remote
|
||||
undo (section 5):
|
||||
|
||||
- C1. `GET /api/v1/version` is at least 1.27.0 (F6), compared as numbers, or `gitea-too-old`.
|
||||
- C2. `GET /api/v1/user` gives `login == account` as recorded at `set`, and `is_admin` true, or
|
||||
`grant-admin-account`.
|
||||
- C3. `GET /api/v1/token` gives exactly the three scopes (3b), or `grant-admin-scope`.
|
||||
- C4. `GET /api/v1/user/repos` is empty, or `grant-admin-not-dedicated`. By F11 and F12 this list covers
|
||||
owned, collaborator and org-team repositories, and site admin alone doesn't fill it.
|
||||
|
||||
What code can't check: that nobody else uses the account, and org membership with no repositories (F11: the
|
||||
orgs routes need `read:organization`, which the admin token doesn't hold and shouldn't). Both are the bootstrap
|
||||
step's promise and go in the approval notes (section 8).
|
||||
|
||||
### 3b. The scopes, each shown needed
|
||||
|
||||
The admin token holds exactly `write:admin`, `write:user` and `write:repository` (mos-1136 item 1). From
|
||||
`matrix.log`, with each scope dropped in turn:
|
||||
|
||||
| Parent token | A1 create user | A2 mint seat token | A3 fork (seat token) | A5 delete seat token |
|
||||
|---|---|---|---|---|
|
||||
| all four | 201 | 201 | 202 | 204 |
|
||||
| without `write:admin` | **403** | 201 | 202 | 204 |
|
||||
| without `write:user` | 201 | **403** | 202 | **403** |
|
||||
| without `write:repository` | 201 | **403** (cap, F2) | 202 | 204 |
|
||||
| without `read:user` | 201 | 201 | 202 | 204 |
|
||||
|
||||
- `write:admin` is needed for A1, and for `prohibit_login` on fire (F10).
|
||||
- `write:user` is needed for A2 and A5.
|
||||
- `write:repository` is needed only because of the cap. The seat's token holds `write:repository`, and a
|
||||
parent must hold at least what it mints. The executor itself never writes a repository with it.
|
||||
- `read:user` is covered by `write:user` (F5), so it is dropped.
|
||||
|
||||
C3 reads the scopes with the `token` header (F7). Gitea reports normalized scopes, so a token made with the
|
||||
three plus `read:user` reads as the three. That is harmless, because `read:user` adds nothing to `write:user`.
|
||||
A token made with `all`, or with another category, refuses.
|
||||
|
||||
### 3c. Where it lives, who can read it
|
||||
|
||||
- The token: `<configDir>/secrets/grants/<business>/gitea/admin`, 0600, in 0700 dirs, owned by the stack's
|
||||
OS user. Written and checked by row 199's store code (D1: tmp, fsync, rename, the mode and owner checks).
|
||||
- The metadata: `admin.json` next to it, 0600, holding `url`, `account`, `rotateBy` and `setAt`. No secret.
|
||||
- A business grants on one Gitea. `set` refuses a second `url` while a slot exists (`grant-admin-exists`;
|
||||
`remove` first), and the raise checks the agent's Gitea against the slot's `url` (section 2). Two businesses
|
||||
on one Gitea each hold their own token for the same `mosaic-grant` account.
|
||||
|
||||
Who can read the file: the stack's OS user, and root.
|
||||
|
||||
Whether an agent process runs as that uid:
|
||||
|
||||
- On dragon-lin, yes. The stack runs as jwoltje, and so does every agent process on the machine: 13 claude, 4
|
||||
codex and 2 pi processes, the bwrap sandboxes and node, counted 2026-10-20. That includes the T3 dev seats,
|
||||
which are not sandboxed and can read any jwoltje file.
|
||||
- Stack workers run in the seat sandbox, which has a private HOME and no `~/.config`, `~/.mosaic` or `~/.ssh`
|
||||
(`packages/seat/src/sandbox.mjs`). A sandboxed worker can't reach the file by path. The sandbox doesn't
|
||||
confine the network or the host uid (`sandbox.mjs:29-31`). So the file is safe from a sandboxed worker and
|
||||
not from an unsandboxed process running as the same uid.
|
||||
- On the .53 and .54 installs (#432) the stack runs as its own OS user. Workers run sandboxed as that user.
|
||||
The T3 seats don't run there.
|
||||
|
||||
What a separate uid would buy. A same-uid process has two paths. It can forge a human proof (`verifyHuman` is
|
||||
a cooperative same-UID check, `packages/bus/src/human.mjs:50-52`), and an approved grant then gives it a
|
||||
two-scope token for a stack-made, non-admin user, because step 2 refuses foreign and admin users. Or it can
|
||||
read the admin file, which gives instance admin (section 8). A separate uid for the file and the executor would
|
||||
close the second path and leave the first. That is a real step up, from "a seat token for a stack user" to "no
|
||||
admin".
|
||||
|
||||
I defer it on cost, not because it wouldn't help. It means the executor runs as its own process under its own
|
||||
uid, with an IPC path from the bus that carries the decision id and nothing the bus could forge. That is a row
|
||||
of its own, and it is cleaner after the human proof stops being same-uid (#435 behind Q175 sign-in). Jason
|
||||
approves the gap as section 8 states it.
|
||||
|
||||
### 3d. Every use logged, and rotation
|
||||
|
||||
Every Gitea call made with the admin token, and every call the executor makes with a seat token, writes two
|
||||
records (section 7 has where they live):
|
||||
|
||||
```
|
||||
grant.call { callId, reason, method: "gitea", op } written before the call
|
||||
grant.result { callId, status } written after it
|
||||
```
|
||||
|
||||
- `op` is one of `version`, `self`, `token-scopes`, `repos-check`, `user-get`, `user-create`, `user-patch`,
|
||||
`token-list`, `token-create`, `token-delete`, `admin-rotate`, `fork-get`, `fork`, `seat-self`,
|
||||
`seat-repos`, `token-whoami`.
|
||||
- `reason` is the decision id for a grant or its undo, the `grant.revoked` record id for a revoke, the
|
||||
`grant.sweep` record id for a sweep, or `admin:<verb>` with the human actor for `set`, `rotate` and `remove`.
|
||||
- No body, no header, no token, no URL query. `Credentials.assertClean` (`packages/bus/src/credentials.mjs:233`,
|
||||
the runtime's `secretCheck`) runs on both. The private session-less writer of section 7 runs `#secretCheck`
|
||||
on every guarded body it takes, `grant.step`, `grant.applied`, `grant.failed`, `grant.revoked`,
|
||||
`grant.sweep` and the consume record included, as `#event` does (`packages/bus/src/broker.mjs:559`) (N7).
|
||||
- `events` is append-only (`events_no_update`, `packages/bus/src/migrations/3b.sql:131`), so the result is its
|
||||
own record, written in the transaction of the step's next `grant.step` where there is one. A `grant.call`
|
||||
with no `grant.result` means the process died during the call. Reconcile lists each one in its message.
|
||||
|
||||
Rotation of the admin token: a human-only verb, `mosaic grants admin rotate gitea --rotate-by <date>`. It never
|
||||
runs from an agent, the chief or the tester, and it doesn't need a decision because only `verifyHuman` can
|
||||
start it:
|
||||
|
||||
1. C1 to C4 with the current token.
|
||||
2. Mint a successor for the admin account itself: `POST /users/<account>/tokens` with Basic and the same three
|
||||
scopes, named `mosaic-grant-<UTC yyyymmddThhmmssZ>`, so a second rotation on one day doesn't collide (F9).
|
||||
F2's cap holds it to the current token's scopes.
|
||||
3. C2 and C3 with the successor.
|
||||
4. Write it through 199's tmp, fsync and rename. Update `admin.json` with the new `rotateBy` and a new `setAt`.
|
||||
5. Delete the old token at Gitea by name, with the new token. If that fails, `status` shows
|
||||
`adminTokenLeft: <name>` until `rotate --finish` succeeds.
|
||||
|
||||
A new `setAt` changes `adminSha256`, so every grant decision raised before the rotation and not yet run
|
||||
refuses `decision-mismatch` (fail closed). `rotate` prints how many open grant decisions that voids, and
|
||||
`status` lists them.
|
||||
|
||||
`status` shows the rotate-by date. Past it, the raise and the executor refuse `grant-admin-rotation-due` until
|
||||
Jason rotates. A forgotten rotation stops grants rather than letting an old token run on.
|
||||
|
||||
If the admin token leaks, Jason deletes it in Gitea (Settings, Applications, as `mosaic-grant`) or disables
|
||||
the account, then runs `mosaic grants admin remove gitea`, which unlinks the slot. Live agent tokens keep
|
||||
working. Each one is revoked on its own (section 6).
|
||||
|
||||
### 3e. The one bootstrap step
|
||||
|
||||
Once per Gitea instance per stack, at #436b deploy. This is the only manual credential step left, and
|
||||
Jason's approval names it:
|
||||
|
||||
1. In Gitea, Jason creates `mosaic-grant` as a site admin with no repositories and no organizations, turns on
|
||||
TOTP, and as that user creates a token with `write:admin`, `write:user` and `write:repository`.
|
||||
2. On the stack host, as the stack's OS user:
|
||||
`mosaic grants admin set gitea --url <url> --account mosaic-grant --rotate-by <date>`, with the token on
|
||||
stdin (never argv).
|
||||
|
||||
`set` is human-only (`verifyHuman`). It runs C1 to C4 (3a) before it writes anything, and any failure writes
|
||||
nothing.
|
||||
|
||||
## 4. The executor's steps
|
||||
|
||||
0. **Start and pre-checks.** X1 starts the executor on Jason's yes. It claims `(agent, "gitea")` in
|
||||
`#inflight` (#436a X5), then runs C1 to C4 and the admin rotate-by check. A refusal here writes
|
||||
`grant.failed {code, grantDecision}` and leaves the decision unspent (section 7's spend rule). After the
|
||||
fix (a rotation, an upgrade), Jason runs `mosaic grants run <decision>` (human-only, `verifyHuman`), which
|
||||
starts step 0 again, claim included. That holds only while the bus that refused is up: at the next start,
|
||||
#436a r6 X3's reconcile spends every resolved-yes grant decision with no `action.allowed` as
|
||||
`interrupted`, a step 0 refusal included, and the chief raises again (D5, sage-1578). `status` lists each
|
||||
such open decision with `spentAtNextStart: true`. While the agent is held (`grant-token-unknown`, section
|
||||
5), step 0 refuses `grant-token-unknown`, so a revoke is the only way out (N8). While a start or another `grants run` holds the claim it refuses
|
||||
`grant-busy` before any call, as #436a X2's `host-busy` does. It refuses `decision-consumed` for a spent
|
||||
decision. No other path starts it. The tag is #436a X2 step 2's, over business, agent and `"gitea"`.
|
||||
1. **Recheck and consume**, in one transaction: `#consumeGrant` with `operation: 'grant.gitea'`, the
|
||||
definition still declares `gitea`, the three hashes match (section 2), no revoke of `(agent, gitea)` newer
|
||||
than the raise. A failure rolls the transaction back, so the decision is not spent, and then writes
|
||||
`grant.failed {code, grantDecision}`. Nothing remote has changed. `grants run` may retry it, but these
|
||||
refusals (`decision-mismatch`, a newer revoke, the definition) recur until the chief raises again.
|
||||
2. **User.** `GET /users/{user}` (F8: case-insensitive).
|
||||
- 404: write `grant.step {step: 'user', nonce}`, where `nonce` is 16 random lowercase hex digits. Then
|
||||
`POST /admin/users` with the agent's `gitea.user` and `email`, `full_name` = `<identity.name> [mosaic
|
||||
<nonce>]`, a random password that is never stored, and `must_change_password: false`. Then, in one
|
||||
transaction, record `gitea_users {user, id, nonce}` with the `id` from the response (F8) and
|
||||
`grant.result`.
|
||||
- 200, and `gitea_users` has this business and agent with the same `id`: go on. That is a second grant or a
|
||||
rotation. A user deleted at Gitea and remade by a human under the same name has a new `id`, and refuses.
|
||||
- 200 with any other `id`, or no `gitea_users` record: refuse `gitea-user-foreign`. The executor never mints
|
||||
a token for a user this stack didn't create. Without this check an `identity` naming a human's account,
|
||||
jason.woltje included, would get a token minted for that account on Approve.
|
||||
- The user is a site admin, or has `prohibit_login`: refuse `gitea-user-admin` or `gitea-user-disabled`.
|
||||
3. **Token.** Write `grant.step {step: 'mint', tokenName}`, with the name `mosaic-<tag>-<decision>`. Then
|
||||
`POST /users/{user}/tokens` with Basic and scopes `write:repository` and `read:user`. The value is held in
|
||||
memory only. Write `grant.step {step: 'minted', tokenId}`.
|
||||
4. **Forks.** With the new token, for each project: `GET /repos/{user}/{repo}`. If it exists, its `parent`
|
||||
must be the project's upstream, or `fork-foreign`. If it's 404, `POST /repos/{owner}/{repo}/forks`.
|
||||
Push-to-create stays off on the Gitea side (199 N11).
|
||||
5. **Verify.** With the new token: `GET /api/v1/user` gives `login == user` and the recorded `id`. Then
|
||||
`GET /api/v1/user/repos?limit=50&page=N` from page 1 until a page holds fewer than 50, with the count
|
||||
equal to `X-Total-Count`. More than 20 pages, or a count that disagrees, refuses `gitea-reach-unknown`.
|
||||
Every listed repository must be owned by this user and have as `parent` the upstream of a project in the
|
||||
agent's `publish` list (F11). Anything else refuses `gitea-reach-wider` and names it: a repository from a
|
||||
collaborator or org-team add, or the user's old fork of an upstream that has left `publish`. So the token
|
||||
never reaches past the rendered block (B3).
|
||||
6. **Go live.** Write `grant.step {step: 'commit', previousTokenId}` (`null` for a first grant). Check the
|
||||
abort signal, and with no `await` between, call 199 `issue(agent, "gitea", token, rotateBy, commit)`.
|
||||
`commit` writes the `gitea_grants` record (tokenId, tokenName, projects, rotateBy, decision). The reference
|
||||
is registered first (#436a X4).
|
||||
7. **Old token.** For a rotation, `DELETE /users/{user}/tokens/{previousTokenId}` after the new one is live.
|
||||
A failure adds `serviceLeft: <id>`, which `sweep` clears.
|
||||
8. **Applied.** `grant.applied {grantDecision, agent, user, userId, tokenId, forks}` and a message to the human and
|
||||
the chief. The token appears in no record, message or log.
|
||||
|
||||
The claim is released in a `finally`.
|
||||
|
||||
## 5. Failure and restart
|
||||
|
||||
Ordered so a failure leaves nothing an agent can use. "Delete the token" is `DELETE /users/{user}/tokens/{id
|
||||
or name}` with the admin token, after C1 to C4, with `grant.call` and `grant.result` whose reason is the
|
||||
decision (D8). Best effort; a failure adds `serviceLeft`.
|
||||
|
||||
**Settle.** Once `issue` has renamed the file, the file may hold the new token or the previous one, and the
|
||||
record may name either. The executor and the restart never guess. They read the store file and ask Gitea which
|
||||
token it is: `GET /api/v1/token` with that value in the `token` header (F7 for admin tokens, F13 for the
|
||||
two-scope seat token; op `token-whoami`). The answer is an `id`, never a value.
|
||||
|
||||
- 200, `id` is the new token: forward. The `gitea_grants` record gets the new token (if `commit` hadn't
|
||||
run), memory loads the file (`Credentials.reload(instance, service)`, a new method with load's checks,
|
||||
built in #436b's row; row 199 doesn't change), and the previous token is deleted.
|
||||
- 200, `id` is the previous token: back. The new token is deleted. The record keeps or gets the previous
|
||||
token.
|
||||
- 401: the file's token no longer exists at Gitea (F13 b4). The previous token can't be in the file, so the
|
||||
agent holds nothing usable. Delete both by id, unregister, delete the record, unlink the file, and end
|
||||
`grant.failed {code: 'grant-lost'}`. A new decision is needed. No file at all is the same case.
|
||||
- 200, `id` is neither token: the file holds a live token this grant didn't mint and step 5 never checked.
|
||||
Memory drops it (199 `revoke`'s memory step) and the agent gets `credential-unavailable`. The file, the
|
||||
record and both recorded tokens stay, and nothing is deleted at Gitea. The step ends `grant.failed {code:
|
||||
'grant-token-unknown', grantDecision, tokenId}`, where `tokenId` is the id Gitea gave, and Jason gets a
|
||||
message. `status` shows `tokenUnknown`. At each start, before `Credentials` loads, the reconcile passes such
|
||||
`(agent, "gitea")` pairs as `held` (a `Credentials` option built in #436b's row, like `reload`), so memory
|
||||
never loads that file. A revoke (section 6) clears it: it unlinks the file and deletes both recorded tokens.
|
||||
The unknown token is Jason's to look at.
|
||||
- Anything else is `settle-pending`, and nothing is deleted: 403, 404, 5xx, a timeout, Gitea unreachable, or
|
||||
C1 to C4 failing first. The last `grant.step` stays `commit`, the
|
||||
`grant.call` without a `grant.result` stays listed, and `status` shows `settlePending`. The file stays as
|
||||
it is, and the agent can use it: it holds one of the two tokens, and both passed step 5. The next start
|
||||
and `mosaic grants sweep gitea` settle again. Only a 401 deletes.
|
||||
|
||||
| Fails at | State left | Cleanup |
|
||||
|---|---|---|
|
||||
| 0 | nothing, the decision unspent until the next start | none. `grants run` retries while this bus is up; after a restart the reconcile spends it (`interrupted`) |
|
||||
| 1 | nothing | none |
|
||||
| 2, before the create | nothing | none |
|
||||
| 2, after the create, before the record | a user with the nonce | none here. Restart adopts it by the nonce (below) |
|
||||
| 3, before `minted` | maybe a token nobody holds | delete the token by its recorded name |
|
||||
| 4 or 5 | a token, maybe some forks | delete the token. Forks stay; they hold only upstream content |
|
||||
| 6 before the rename, first grant | a token | unregister, delete the token |
|
||||
| 6 after the rename, first grant | a store file with the new token, maybe the record | unlink the file, unregister, delete the record if written, delete the token |
|
||||
| 6 before the rename, rotation | the previous grant intact | delete the new token |
|
||||
| 6 after the rename, rotation | the file holds the new token, memory the previous | settle (forward: the new token passed step 5) |
|
||||
| 7 | both tokens live, the new one in use | `serviceLeft: <previousTokenId>` |
|
||||
|
||||
**Restart**, before `Credentials` loads, from the last `grant.step`, as #436a X3. The local part runs first;
|
||||
the remote part runs after the bus is up, with C1 to C4.
|
||||
|
||||
| Last step | Local | Remote |
|
||||
|---|---|---|
|
||||
| `user` | none | `GET /users/{user}`. 200 with `full_name` ending `[mosaic <nonce>]`, not admin: adopt, record `gitea_users` with its `id`. 404: none. Anything else: `gitea-user-orphan` with the name, for Jason to look at; nothing is deleted |
|
||||
| `mint` or `minted` | none | delete the token by name |
|
||||
| `commit`, first grant | unlink the file if present, delete the record if present | delete the new token |
|
||||
| `commit`, rotation | none | settle |
|
||||
| `commit`, rotation, settle-pending | none; memory loads the file as usual | none. The step stays open, and the next start or `grants sweep` settles again |
|
||||
| `grant.failed {code: 'grant-token-unknown'}` with no `grant.revoked` after it | pass `(agent, "gitea")` as `held`; memory doesn't load the file | none. Jason's message repeats until a revoke |
|
||||
|
||||
Every reader of grant evidence (this reconcile, `status`, `openGrantDecisions`'s callers, `grants sweep`)
|
||||
reads a `grant.*` record only where `kind = 'action.allowed'` and `actor_role IS NULL AND actor_run IS
|
||||
'grant'`, so a `grant.*` name under another kind or actor is never evidence (duckworth-0067 N1).
|
||||
|
||||
Then `grant.failed {code: 'interrupted'}` (or `grant.applied` if a rotation settled forward) and a message to
|
||||
Jason. A `grant.call` with no `grant.result` is listed in that message. After `interrupted`, the chief raises
|
||||
again and Jason approves again.
|
||||
|
||||
## 6. Revoke
|
||||
|
||||
As #436a section 6, with the same revokers (the human, the assistant in its own organization, the tester on
|
||||
dev only):
|
||||
|
||||
1. 199 `revoke`: memory, abort a use or the executor in flight, unlink the file.
|
||||
2. One store transaction deletes the `gitea_grants` record and records `grant.revoked`.
|
||||
3. After C1 to C4, `DELETE /users/{user}/tokens/{tokenId}` with the admin token. If Gitea can't be reached,
|
||||
`grant.revoked {serviceLeft: <tokenId>}`, and `mosaic grants sweep gitea` clears it later. When the last
|
||||
`grant.step` is a rotation's `commit` with no settle after it (settle-pending, or `grant-token-unknown`),
|
||||
two tokens are live and the record may name either (section 5), so the revoke deletes both
|
||||
`previousTokenId` and the minted `tokenId`, each failure listed in `serviceLeft` (dewey-0761). The id
|
||||
`grant-token-unknown` named is not one this stack minted; it stays for Jason.
|
||||
|
||||
That closes 199 D12's gap: the token is gone at Gitea without Jason's A5 step.
|
||||
|
||||
The Gitea user and its forks stay after a revoke, so a new grant can reuse them.
|
||||
|
||||
On `credentials delete` (a fire), after the steps above and with the recorded `id` checked first:
|
||||
|
||||
- `GET /users/{user}/tokens` with Basic (F9), and delete every token it lists, not only the recorded one. A
|
||||
token the record doesn't know about doesn't outlive a fire.
|
||||
- `PATCH /admin/users/{user}` with `prohibit_login: true` (F10).
|
||||
- Each step that fails is listed in `serviceLeft`, and `sweep` retries it.
|
||||
|
||||
## 7. Store records
|
||||
|
||||
Two tables, in #436a S1's migration if #436a row b and this row land together, otherwise a `3j-3k.sql`:
|
||||
|
||||
- `gitea_users`: business, agent, user, id, nonce, the decision that created it.
|
||||
- `gitea_grants`: business, agent, user, tokenId, tokenName, projects, rotate-by date, decision.
|
||||
|
||||
The executor, revoke and `sweep` are the only writers, each in the transaction of its record. The admin slot
|
||||
is a file, not a table, so a store backup holds no admin token.
|
||||
|
||||
The audit records (`grant.step`, `grant.call`, `grant.result`, `grant.applied`, `grant.failed`,
|
||||
`grant.revoked`, `grant.sweep`) are `action.allowed` events with that name in `body.action`. They are not new
|
||||
event kinds, so `events` and its CHECK (`3b.sql:9-30`) aren't rebuilt. This section is the rule's only home
|
||||
(mos-1144). #436a r6 (f9039682e) uses it too: #436a row a's BUILD.md cites this section at the commit Duckworth
|
||||
approves and maps r6's names to `body.action` values, with these renames (gizmoduck-0084):
|
||||
`host.enroll.step` to `grant.host.enroll.step`, `host.enrolled` to `grant.host.enrolled`, `host.checked` to
|
||||
`grant.host.checked`, `host.removed` to `grant.host.removed`. Cites are origin/refactor 00d4cc1f6.
|
||||
|
||||
**The guarded namespace** is one pattern: an `action.allowed` whose `body.action` or `body.operation` GLOBs
|
||||
`'grant.*'`. `operation` is included because `#consumeGrant`'s consume record is
|
||||
`{operation: 'grant.<method>', decision}` (#436a X1), and a forged one would spend a decision (Dewey N2). Row
|
||||
210's `host.sudo` and `host.run`, written from agent sessions, stay outside it, and no grant-path name is left
|
||||
under `host.*`.
|
||||
|
||||
**The grant actor** is `actor_role IS NULL AND actor_run IS 'grant'`. The only production insert into `events`
|
||||
is `#event` (`packages/bus/src/broker.mjs:568`), which writes the session's role and run. Every session with a
|
||||
NULL role also has a NULL run: the human (`:537`), the reader (`:549`) and a session-less record (`:1124`). The
|
||||
tester's role is `tester`. So neither a session nor the human path can produce that pair, as no instance can
|
||||
be named `_bus` (`:166-168`). A `system` role would not be safe, because `system` matches `ID_PATTERN`
|
||||
(Dewey N1). The broker writes guarded events, the consume record included, through a private session-less
|
||||
writer added in #436a row a; `#event` is unchanged. Revoke, sweep and check run for the human, the assistant,
|
||||
the tester or the chief, and their events still go out under the grant actor, with `body.by` naming the caller
|
||||
the broker checked.
|
||||
|
||||
**Two guards** stop a forged record:
|
||||
|
||||
- `recordEvent`, the trusted adapters' path (`broker.mjs:1111`), refuses `reserved-event` for any
|
||||
`action.allowed` in the guarded namespace, as it does today for `git.push.working` and the merge action.
|
||||
- The migration adds a trigger: an `action.allowed` in the guarded namespace aborts unless
|
||||
`actor_role IS NULL AND actor_run IS 'grant'`. That covers any path that misses the first guard.
|
||||
|
||||
**Spend rule (B1).** `#spent` (`broker.mjs:811-817`) counts any `action.allowed` that names the decision in
|
||||
`body.decision` and whose `operation` isn't `decision.raise`; a record with no `operation` counts. So the
|
||||
consume record is the only guarded body with a `decision` key. Every other `grant.*` body names the decision
|
||||
`grantDecision`. A step 0 or step 1 refusal therefore leaves the decision unspent, and the start reconcile
|
||||
for "resolved yes, unspent" keeps reading `#spent` unchanged.
|
||||
|
||||
A wider gap sits under this one and isn't this row's: today any `recordEvent` `action.allowed` that carries
|
||||
`body.decision` spends that decision. Follow-up for Sage to file.
|
||||
|
||||
**Open decisions (N6).** One query, `openGrantDecisions(business, method)`: `credential.mint` decisions
|
||||
resolved yes by the human whose raise body has `grant.method = method`, and for which `#spent` is false.
|
||||
`status` lists them, `grants admin rotate` counts the ones it voids (all of them, since `adminSha256`
|
||||
changes), and the start reconcile reads the same query.
|
||||
|
||||
**No task refs.** Guarded bodies never carry `target` or `task_ref` holding a task ref, so they stay clear of
|
||||
`task-subject-required` (`broker.mjs:561-565`) on any path. No other `action.allowed` reader sees these
|
||||
events: the rest key on `decision.raise` (`:670`, `:803`), `git.push.working` (`:863`, `:2019`) or
|
||||
routine (`:2100-2102`).
|
||||
|
||||
## 8. Approval notes for Jason
|
||||
|
||||
Stated once, here, for the #436b approval:
|
||||
|
||||
- **Site admin plus `write:user`, and `write:admin`.** The admin token can mint a token for any Gitea user,
|
||||
with any scope up to its own three, including users the stack didn't create (F4). With `write:admin` it can
|
||||
also set any user's password, email and admin flag, and disable any user (F10, proven on scratch against a
|
||||
second site admin). Whoever holds the token can take over any account on that Gitea, jason.woltje's
|
||||
included, make any user a site admin, and read or write any repository. The executor uses none of that
|
||||
beyond its steps, but the token itself can.
|
||||
- **Same uid on dragon-lin.** The token is a file readable by jwoltje, and every agent process on dragon-lin,
|
||||
T3 dev seats included, runs as jwoltje (3c). The seat sandbox keeps sandboxed workers away from it. It does
|
||||
not keep an unsandboxed same-uid process away. Such a process gets instance admin by reading the file, which
|
||||
is more than it could get by forging an Approve. A separate uid would close that path. It is deferred on cost
|
||||
(3c), not because it wouldn't help.
|
||||
- **For comparison, today (D12).** Every dev seat on dragon-lin already pushes as jason.woltje through
|
||||
`scripts/git-credential-mosaic.mjs` (mos-1106). jason.woltje is a site admin on git.mosaicstack.dev, with
|
||||
restricted false (mos-1146: Mos read `/api/v1/users/jason.woltje` with the mos token at 23:05:29Z; Jason's
|
||||
token wasn't used). So a same-uid process on dragon-lin can already act as a site admin there through that
|
||||
credential, and the admin file adds little exposure on that Gitea beyond what the seats already have. It
|
||||
still matters on any other Gitea a business grants on, and once seats stop pushing as jason.woltje (#431).
|
||||
- **A seat token's reach can grow.** Step 5 checks, at the grant, that the token reaches only its forks. If
|
||||
someone later adds the user to an org or as a collaborator, the token reaches that too, until the next grant
|
||||
or rotation checks again.
|
||||
- **The admin account is dedicated by promise.** Code checks that it is a site admin with these three scopes
|
||||
and no reachable repositories (C1 to C4). It can't check that nobody else uses it or that it is in no org.
|
||||
- **The bootstrap step** (3e) is the one manual credential step: Jason creates `mosaic-grant`, its TOTP and
|
||||
its token, and enters the token once on stdin.
|
||||
|
||||
## 9. Tests and mutants (for the row, not built here)
|
||||
|
||||
Unit tests use a fake Gitea over TLS, as row 199's publish tests do. Section 1's facts get a scratch
|
||||
gitea/gitea:1.27.1 test on 127.0.0.1 with throwaway data, removed after. No test contacts git.mosaicstack.dev
|
||||
or any other live service. No token value is printed.
|
||||
|
||||
| Test | Claim |
|
||||
|---|---|
|
||||
| X-G1 | a gitea raise for an agent without `gitea`, `identity` or `publish`, or a project outside `publish`, a remote on another host, no admin slot, or a slot past rotate-by refuses and stores nothing |
|
||||
| X-G2 | `set` refuses a version below 1.27.0 (fake reports 1.26.4, 1.27.0-rc0, and 1.9.0 against 1.27.0 as a string trap), a non-admin, a login mismatch, an account that reaches a repository, a token with another scope or `all`, and a second `url`. It writes nothing on any refusal |
|
||||
| X-G3 | on scratch 1.27.1, end to end: user made with the nonce and its `id` recorded, token minted with the two scopes, forks made, step 5 passes, `issue` writes the 0600 file, `grant.applied`, then revoke deletes the token at Gitea (gone from its list) |
|
||||
| X-G4 | an existing user with no record, a recorded name whose `id` differs (deleted and remade, and a case variant of the name), a site-admin user and a disabled user each refuse before any token call |
|
||||
| X-G5 | each failure row of section 5 leaves the stated state, rotation rows included. A failed delete leaves `serviceLeft`. A revoke while a rotation's `commit` is settle-pending, and one after `grant-token-unknown`, deletes both `previousTokenId` and the minted `tokenId` at the fake, and a failure of either lands in `serviceLeft`. A new grant for a held agent refuses `grant-token-unknown` at step 0, before any Gitea call |
|
||||
| X-G6 | a restart after `user` adopts a user whose `full_name` ends with ` [mosaic <nonce>]` (16 lowercase hex), whose lowercased login equals `gitea.user` lowercased and who isn't an admin; does nothing on 404; and reports `gitea-user-orphan` for a different `full_name`, a different login or an admin. A restart after `mint`, `minted` and `commit` of a first grant leaves no token at Gitea, no file and no record. A restart after `commit` of a rotation settles forward when the file holds the new token and back when it holds the previous one, ends `grant-lost` only on a 401, and stays `settle-pending` with nothing deleted on 403, 404, 500, a timeout and an unreachable fake. On 200 with an unknown `id` it drops the credential from memory, deletes nothing at Gitea, keeps the file, ends `grant-token-unknown`, and a second restart still doesn't load the file; a revoke then clears it |
|
||||
| X-G7 | an identity, publish, upstream or admin slot change between raise and Approve refuses `decision-mismatch` before any Gitea call. An admin rotate voids open grant decisions and `status` lists them |
|
||||
| X-G8 | every admin-token and seat-token call writes `grant.call` before it and `grant.result` after it, paired by `callId`. A crash between them is listed by reconcile. No record body holds a token |
|
||||
| X-G9 | admin rotate: C1 to C4, the successor named by time, checked, written, the old token deleted. Two rotations in one second's name refuse cleanly, not half done. Past rotate-by, the raise and the executor refuse |
|
||||
| X-G10 | seat-token rotation: the new token is live before the old one is deleted |
|
||||
| X-G11 | a step 0 refusal (rotation due, too old) and a step 1 refusal leave `#spent` false, and `grants run` by the human gets past `#consumeGrant`. `grants run` from an agent, the chief or the tester refuses. A `grants run` while a start holds the claim refuses `grant-busy` before any Gitea call. No `grant.*` body other than the consume record has a `decision` key. A step 0 refusal, then a restart, then `grants run`: the reconcile has spent the decision as `interrupted` and `grants run` refuses `decision-consumed`; before the restart `status` shows `spentAtNextStart` |
|
||||
| X-G12 | step 5 refuses `gitea-reach-wider` when the user was made collaborator on another repository, when it joined an org team, and when it owns a fork of an upstream that has left `publish`, and deletes the token. It passes with an old fork of a `publish` project outside `target.projects`, which the rendered block named. A fake returning 51 forks over two pages is read in full; a total that disagrees refuses `gitea-reach-unknown` |
|
||||
| X-G13 | `recordEvent` refuses an `action.allowed` with `body.action` `grant.call` (and each `grant.*`) and one with `body.operation` `grant.gitea`. A direct store insert aborts on the trigger for `grant.step` and `grant.host.enrolled` from a session, and for `grant.step` under the human's NULL pair. `host.sudo` from an agent session still writes |
|
||||
| X-G14 | fire: every token the user has is deleted, the user gets `prohibit_login`, and a failure of either lands in `serviceLeft` |
|
||||
| X-G15 | a `grant.step {step: 'commit'}` stored under `action.refused`, and one under the human's NULL pair, are ignored by the restart reconcile: no settle runs and no Gitea call is made |
|
||||
|
||||
Mutants, a first list: drop the version floor; compare the version as a string; accept another admin scope;
|
||||
skip `gitea-user-foreign`; compare the user by name instead of `id`; skip the admin-user refusal; adopt a user
|
||||
at restart without the nonce; create the user before writing `grant.step user`; mint before writing
|
||||
`grant.step mint`; delete the old token before `issue`; settle a rotation by the record instead of
|
||||
`token-whoami`; run C1 to C4 after `#consumeGrant` instead of before; drop the `identitySha256` compare; drop
|
||||
`adminSha256`; skip the `grant.call` write; write `grant.result` before the call; drop the `grant.` reservation
|
||||
in `recordEvent`; drop the trigger; widen the trigger to `host.*` (breaks `host.sudo`); guard `body.action` only, not
|
||||
`operation`; key the trigger on `actor_run` alone; put `decision` in step 0's `grant.failed`; delete both
|
||||
tokens on a settle 403 or 5xx; compare step 5 against `target.projects` instead of `publish`; read one page
|
||||
of `/user/repos`; let `grants run` skip the claim; send the admin token in a `token` header on the mint route; revoke the
|
||||
remote token before the local steps; skip the fork `parent` check; skip step 5's reach check; delete only the
|
||||
recorded token on fire; let `grants run` accept an agent; keep the file in use on a settle 200 with an
|
||||
unknown `id`; load a `held` pair at start; revoke only `tokenId` on an open rotation; let step 0 pass a held agent; drop the `kind` filter from the restart reconcile.
|
||||
|
||||
## 10. Shared with Bushroot's GitHub half (proposed in megavolt-0036, answer pending)
|
||||
|
||||
- S1. The `grant` body: `{method, agent, rotateBy, target}`, scopes fixed by code per method.
|
||||
- S2. The admin slot: `<configDir>/secrets/grants/<business>/<method>/admin`, set once by
|
||||
`mosaic grants admin set <method>`, never an agent service. One service instance per method per business.
|
||||
- S3. `grant.call` and `grant.result` (r2: two records, paired by `callId`) for every service call, as reserved
|
||||
`action.allowed` bodies (section 7).
|
||||
- S4. The executor shape: pre-checks before consume (step 0), claim, consume, mint, verify, `issue`, applied,
|
||||
with `grant.step` before and after the mint, and settle by asking the service which credential the file
|
||||
holds. GitHub may not fit "mint once and store" (App installation tokens expire within an hour). If so, S4
|
||||
becomes per method and the rest stays shared.
|
||||
- S5. Revoke local first, then the record, then the service, with `serviceLeft` and `grants sweep`.
|
||||
- S6. A capability floor per method, checked at `set`, before every start and before every remote undo.
|
||||
|
||||
Dewey notes items 2, 3 and 5 of dewey-0754 apply to S3, S4 and S6. The r2 text above carries them.
|
||||
|
||||
## 11. Questions
|
||||
|
||||
- Q-B1. Answered (dewey-0754 item 9): on fire, `prohibit_login` and delete every token (section 6). The PATCH
|
||||
route is proven (F10).
|
||||
- Q-B2. Answered (dewey-0754 item 10): no organizations. The token's real reach is checked at step 5 and
|
||||
stated in section 8.
|
||||
- Q-B3. Bushroot's answer on S1 to S6.
|
||||
- Q-B4. Answered (mos-1146): jason.woltje is a site admin on git.mosaicstack.dev, restricted false. Section 8
|
||||
says so.
|
||||
- Q-B5. Answered (gizmoduck-0081, -0084, mos-1144): one rule for #436a and #436b, section 7. #436a row a's
|
||||
BUILD.md cites section 7 at the commit Duckworth approves, or carries its text in full if row a's candidate
|
||||
comes first. Duckworth checks it with that candidate.
|
||||
@@ -1,167 +0,0 @@
|
||||
# Row 199 (#1697): per-agent credential store, each agent's own git identity, publish to its fork
|
||||
|
||||
Builder: Megavolt. Brief: `docs/plans/2026-10-20_agent-credentials.md`. Design: `DESIGN.md` r6 at
|
||||
4d3007ec9, approved by Duckworth in duckworth-0027 (REVIEW-r6.md at 640011518). Authority model ruled
|
||||
by Mos in mos-1027. Branch: megavolt/row199. Reviewer: Duckworth.
|
||||
|
||||
Status: in progress. The full gate has not run; it is Sage's. Only single test files have run on
|
||||
this branch, apart from one slip noted under "Runs".
|
||||
|
||||
## Commits so far
|
||||
|
||||
- 286bf598c business: agent identity, the publish grant, the project upstream with the D9c parse, store
|
||||
credential references; git.push.remote as a hard wait.
|
||||
- a2c387dba bus: broker publish authority (checkPublish, recordPublish, publishRefused, pushCommit on
|
||||
the raise); the D9c parse moves to `business/src/vocabulary.mjs`; pm.md and chief-of-staff.md list
|
||||
git.push.remote among the hard waits.
|
||||
- 609dc9364 bus: the per-agent credential store in `credentials.mjs`.
|
||||
- 504336797 bus: runtime wiring, the store root and the four publish entries in the op table.
|
||||
- a18fc6cf8 tasks: request calls use `useFor` keyed from the session; the start probe keeps `use`. The
|
||||
D5 test in `credential-store.test.mjs` pins the `use(` call sites to `credentials.mjs` (useFor's own
|
||||
call) and `tasks/src/adapter.mjs` (the host's @sync and start probe).
|
||||
- aa740cc71 bus: the lifecycle verbs `credentials.issue`, `credentials.revoke` and `credentials.delete`
|
||||
in `credential-verbs.mjs`, the broker branch, the runtime wiring; `organizationWriter` gets a
|
||||
`detail` on `write` and a receipt-only `record`.
|
||||
- 0cdace415 cli: `mosaic credentials status|issue|rotate|revoke|delete`, the scripts/mosaic route, docs.
|
||||
|
||||
## Deviations from DESIGN.md r6
|
||||
|
||||
1. **Identity is optional on an agent.** D8 reads as identity on every agent. Today's business files
|
||||
carry none, so identity stays optional and `publish` requires it (N10's load refusal). An agent
|
||||
without identity commits as E7 (T6).
|
||||
2. **Revoke of a `{file}` or `{env}` reference.** Section 6 describes revoke for store references.
|
||||
For a reference outside the store, revoke forgets the token in memory and never unlinks the file;
|
||||
the lifecycle verb then turns the business file's reference into `store`, so a restart finds
|
||||
nothing. The test: `credential-store.test.mjs`, "revoke of a {file} reference".
|
||||
3. **Issue order.** Issue writes the token file (tmp, fsync, rename, dir fsync), then runs the
|
||||
caller's commit (the business file's reference becomes `store`), then sets memory. A failed commit
|
||||
leaves the file on disk and the token out of memory, so the agent stays `not-issued` until the
|
||||
issue is run again. The test: "a failed commit leaves the token on disk but not in memory".
|
||||
4. **The D9c parse lives in `business/src/vocabulary.mjs`, not `business.mjs`.** The broker reads
|
||||
`forkProblem` and `forkUrl`, and `project-runtime.test.mjs` pins the broker's import closure to
|
||||
files that import nothing. `business.mjs` wraps `parseGiteaUrl` with the loader's refusal
|
||||
messages. The pin test names the two new imports.
|
||||
5. **`withGitea` takes the publish ticket, not a cap (D9b).** The key is the session checkPublish
|
||||
checked with `#agent` (a run holding its claim; never a tester, reader or human), read through
|
||||
`broker.publishSession(ticket)`. The token is usable only between checkPublish and recordPublish
|
||||
or publishRefused. Its refusals are filed once, by publishRefused on the task's trail, and not
|
||||
also by `refusing()`. The test: `publish.test.mjs`, "runtime: withGitea keys from the open ticket
|
||||
only".
|
||||
6. **Role contracts.** `workflow-roles.test.mjs` requires the hard waits listed in `roles/pm.md` and
|
||||
`roles/chief-of-staff.md` to equal GATED_ONLY. Adding git.push.remote to the vocabulary made it
|
||||
fail, so both lists now name git.push.remote. Prose only; no hash pin reads either file. Duckworth
|
||||
should review this hunk because `roles/` is reviewed authority.
|
||||
|
||||
7. **Delete takes a reason.** Section 6 lists `delete AGENT` with no reason. The broker requires
|
||||
`reason` on delete as on revoke, so every receipt that ends a credential says why.
|
||||
8. **Rotate is `credentials.issue`.** There are three bus verbs, not four. An issue over an entry the
|
||||
bus holds is recorded as `rotate` in the receipt and the answer. The CLI's `rotate` reads the
|
||||
status first and refuses before reading stdin when nothing is issued.
|
||||
9. **Issue covers entries the bus loaded.** The business file must already name the kind for the
|
||||
agent, and the bus must have loaded that reference at start; otherwise `credential-unknown`. An
|
||||
agent added after the bus started needs the restart row 165 already asks for. Row 194's human
|
||||
`create` is where a new agent's `store` references come from.
|
||||
10. **No Console page yet.** Section 6 puts the verbs in the Console's agent page too. This branch has
|
||||
the bus verbs and the CLI. The Console page is a separate piece (packages/webui, its `BUS_VERBS`
|
||||
and `STATUS` pins); I raise the split with Sage before the manifest. Ruled in sage-1421: the
|
||||
Console page is out of row 199 and becomes a follow-up row with Webby as reviewer. The manifest
|
||||
holds bus, business, tasks, cli and seat.
|
||||
11. **The push runs in the bus child, and the git socket polls for it (D9b).** The git socket runs in
|
||||
the host process. The broker, the runtime and the token are in the bus child that
|
||||
`packages/cli/src/host.mjs` forks, and host ops cross that IPC one at a time with a 10 s answer
|
||||
limit. ls-remote and the push can run longer than that, so `withGitea(ticket, fn)` can't wrap a
|
||||
host-side git. The split: the socket runs D9 steps 3 and 4 (tip and identity under the lock,
|
||||
`working.publish`), then op `publishPush`, which starts `publishRemote` in the bus child under
|
||||
`withGitea` and answers `{state: 'running'}`. The socket polls op `publishResult` every 200 ms
|
||||
until it reads `pushed` or `refused`. Fork, user, source and destination come from the ticket
|
||||
(`broker.publishTarget`), never from the host's message. `recordPublish` also refuses `ref-moved`
|
||||
when the pushed ref isn't the ticket's destination. A second `publishPush` on a running ticket
|
||||
refuses `publish-busy`. The token reaches git on fd 3 only (`runGit` option `pass`), read once
|
||||
by a one-shot credential helper that answers `get` for https on the fork's exact host and port.
|
||||
On close, a running publish's git isn't awaited; git's own time limit ends it, and the ticket
|
||||
goes with the process. The 10 s IPC limit stays as it is (sage-1440). The poll has a bound
|
||||
(sage-1440): `publishDeadlineMs`, by default four times git's limit and grace (publishRemote runs
|
||||
at most four git commands) plus 30 s. Past it the socket refuses `publish-timeout`, filed once by
|
||||
publishRefused, and logs that the bus process's push may still reach the fork. A failure that is not a refusal is filed as `git-failed` and logged on
|
||||
the bus's stderr by error name and code only. Tests: `seat/tests/publish.test.mjs` (11),
|
||||
`bus/tests/publish.test.mjs` (the process ops), `cli/tests/publish-socket.test.mjs` (7, end to
|
||||
end through the socket, the in-process runtime, the store and a TLS fake Gitea; one is the
|
||||
publish-timeout outcome).
|
||||
12. **No harness tool for publish.** `packages/harness/src/git-tools.mjs` has push, fetch and land
|
||||
clients. A runner tool that calls the socket's `publish` op is in packages/harness, outside the
|
||||
manifest scope sage-1421 set. Today a publish needs a socket client; the tool is a follow-up.
|
||||
13. **The bind rule's `git.push.remote` clause has no test of its own.** D11 adds it and the
|
||||
launcher has it. A role can publish only a push its own role made (`work-not-yours`), and
|
||||
`recordPush` needs `git.push.working`, which already binds the socket. No role reaches the
|
||||
clause alone, so dropping it changes no outcome today. It matters once a role can publish work
|
||||
it didn't push. Ruled in sage-1440: keep it; its mutant goes in the section 11 list as
|
||||
equivalent, with this reasoning, and Duckworth judges it.
|
||||
14. **A push between checkPublish and the git push refuses; it never lands a different commit.**
|
||||
T3 says such a push "doesn't change the commit that lands". Under the hard wait it refuses:
|
||||
`working.publish` checks the work ref's tip against the ticket's source under the lock
|
||||
(`ref-moved`, `seat/tests/publish.test.mjs`), and `recordPublish` checks `pushCommit` again
|
||||
(`decision-mismatch`, `bus/tests/publish.test.mjs` "r4 B4"). The fork gets only the ticket's
|
||||
oid either way. N9's routing-commit test still applies. Ruled in sage-1440: T3's wording in
|
||||
DESIGN.md now reads "refuses with `ref-moved` or `decision-mismatch`", and an end-to-end test
|
||||
asserts each code mid-flight in `cli/tests/publish-socket.test.mjs`: a push after checkPublish
|
||||
refuses `ref-moved` with no request to the fork; a push recorded while the bus process pushes
|
||||
refuses `decision-mismatch`, filed once, and the fork holds only the ticket's oid.
|
||||
|
||||
15. **Fix: rotate on a due gitea key (gizmoduck-0050, sage-1494).** The CLI's `ISSUED` set
|
||||
(`packages/cli/src/credentials.mjs`) listed valid, expiring and expired, but the bus's `status()`
|
||||
gives a gitea key past its rotate-by date `rotation-due`. So `credentials rotate` refused the one
|
||||
case it exists for. `ISSUED` now names every state status() gives a held token. The test:
|
||||
`cli/tests/credentials.test.mjs`, "rotate goes ahead on each held state". Its mutant (the old set)
|
||||
fails that test and goes in the section 11 list.
|
||||
|
||||
## N9, for the routing commit (sage-1388)
|
||||
|
||||
Today git.push.remote is a hard wait, so every publish carries a decision, and the decision's
|
||||
`pushCommit` fixes the commit. If a later commit routes git.push.remote within role, a publish has
|
||||
no decision and no `pushCommit`. `#publishFacts` then takes the commit from `#latestPush`, and the
|
||||
git socket's tip check under the lock is the only thing that holds it. That commit needs a test
|
||||
that a push between checkPublish and the git push doesn't change what lands.
|
||||
|
||||
## R1, the backup set
|
||||
|
||||
Checked at 504336797. Row 185's `backup_store` (`scripts/stack-release.sh:372-392`) copies only
|
||||
`<dataRoot>/bus/bus.sqlite`, plus its sums, modes, listing and stamps, into
|
||||
`<dataRoot>/backups/stack/<activation>/`. Nothing under `<configDir>` is copied, so
|
||||
`<configDir>/secrets/agents` isn't in the backup set. `rehearse` copies the live business file
|
||||
through `R_BUSINESS_JS` (`:1268-1288`). Its `swap` replaces every credential reference, a `store`
|
||||
one included, with a placeholder file in the rehearsal's own dir. The guard then refuses
|
||||
`credential-ref-live` unless every reference is such a placeholder (`:1329-1337`), so a rehearsal
|
||||
never reads a store token. A backup tool outside this repository that copies `<configDir>` would
|
||||
copy tokens. That is R1's open half, and it is Jason's to check on each VM.
|
||||
|
||||
## T1 limit
|
||||
|
||||
A wrong owner on a store dir or file can't be set up without root, so T1 covers modes, symlinks and
|
||||
dirs. The owner check (`st.uid !== process.getuid()`) is in `privateDir` and `#read`, and is untested.
|
||||
|
||||
## Runs
|
||||
|
||||
Single files, each green on its commit: bus broker, decision-close, decision-route, decisions,
|
||||
hard-waits, land, project-runtime, launch-notice, review, publish, credential-store, credentials,
|
||||
credential-status, limits, tester, plugins-verb, routes-verb, limits-runtime, instances-verb, web,
|
||||
usage-runtime, limit-checks, daily-quota-runtime, chief-of-staff, tasks, tester-writes; every
|
||||
business test file. Since: credential-verbs (2/2), credential-store (11/11, with D5), each tasks file,
|
||||
tester, credential-status, plugins-verb, routes-verb, instances-verb; business launch-settings,
|
||||
plugin-settings, instance-settings, token-limit-settings; cli credentials (3/3), cli, app,
|
||||
tester-reads, tester, other-tree, transport, instances, limits, verbs. Hash pins: none on a changed
|
||||
file (`thread-migration.test.mjs:46` pins DDL only).
|
||||
|
||||
Publish, single files, each green before its commit: seat
|
||||
checkout (77), working (26), publish (11), landing (8); bus process (8), land (17), publish (12),
|
||||
credential-verbs (2); cli publish-socket (4), git-socket (24), launcher (29); harness tools (22).
|
||||
Hash pins: none on a changed file.
|
||||
|
||||
sage-1440 changes (the poll bound, the mid-flight tests), single files, green before the commit: cli
|
||||
publish-socket (7), git-socket (24), launcher (29). Hash pins: none on a changed file.
|
||||
|
||||
The rotation-due fix, single files: cli credentials (4), green; the old-set mutant fails it 3/1.
|
||||
Hash pins: none on a changed file.
|
||||
|
||||
Slip, 2026-10-20 about 19:51Z: I ran `node --test packages/tasks/tests/*.test.mjs` in one command,
|
||||
which is a package suite, without asking Sage for the slot. It passed, 97 pass and 0 fail, in under a
|
||||
minute. Reported to Sage in megavolt-0017.
|
||||
@@ -1,430 +0,0 @@
|
||||
# Row 199 design: a credential store per agent, own git identity, pushes to a remote
|
||||
|
||||
Megavolt, 2026-10-20. Gitea #1697, MVP T1 item 6. Brief: `docs/plans/2026-10-20_agent-credentials.md`.
|
||||
Reviewer Duckworth. Mos rules on the authority model (section 9) before any code. Base: refactor at 992650232.
|
||||
No credential value was read to write this. Evidence is code and docs only.
|
||||
|
||||
r2 answers duckworth-0013 (review at 6ab9cd927 on duckworth/reviews): B1 in D3a, T1 and T5, B2 in D9 step 5
|
||||
and T3, N1 in section 6 and Q-M3, N2 in section 6, N3 in D9 step 3, N4 in D9a and T3, N5 in E5 and D9b.
|
||||
|
||||
r3 records Mos's rulings (mos-1027) in section 9. The Q-M2 hard wait changes D9 step 1, D9b and T3. Q-M1
|
||||
drops O2 (D10, A3, R2).
|
||||
|
||||
r4 answers duckworth-0017 (REVIEW-r3.md at 4065ed59f): B3 in T3 and D9 step 5, B4 in D9 steps 1, 3 and 5, N6
|
||||
and N7 in D9a, N8 in D9 step 1.
|
||||
|
||||
r5 takes webby-0011 N1 (row 194 review, through honker-0009): the host checks at each push that the
|
||||
remote's owner is the agent's own Gitea user. r4 named one `remote` per project, which can't be a fork per
|
||||
agent (O1) once two agents publish the same project. So `projects.<p>.remote` now names the upstream
|
||||
repository, and the host builds each agent's fork URL from it (D9 step 2, D10, T3). It also fixes a conflict
|
||||
found while building: D8 made `identity` required for every agent whose definition declares `gitea`, while D9
|
||||
step 2 says today's files load unchanged. Coder declares `gitea`, so r5 makes `identity` optional and
|
||||
`publish` require it (D8).
|
||||
|
||||
r6 answers duckworth-0026 (REVIEW-r5.md at 3cb6888e1), with Mos's acceptance (mos-1043). B5: the derived URL
|
||||
has one name, `fork`, and D9a's `ls-remote`, step 5's push, the helper's host match, `checkPublish`'s result
|
||||
and step 6's event all use it (D9, D9a, D9b, T3). B6: the URL parse is written down (D9c) and tested (T3).
|
||||
N10 in D8 and T3. N11 in D9a and A3. N9 is carried to BUILD.md for the routing commit.
|
||||
|
||||
## 0. Terms
|
||||
|
||||
- **Stack instance**: one Mosaic Stack deployment (row 190). It has one OS user, one config dir
|
||||
`~/.config/mosaic-NAME` (here `<configDir>`), one data root and one bus. Case 1 and Case 2 are two stack
|
||||
instances on two VMs (DECISIONS Q180).
|
||||
- **Agent**: a role instance in a business file (`roles.<agent>`). Today's code calls it "instance". This
|
||||
design says agent.
|
||||
- **Kind**: a credential type: `gitea`, `vikunja`, `model`.
|
||||
- **Host**: the bus process and its git socket, running as the stack instance's OS user, outside any sandbox.
|
||||
|
||||
## 1. What exists (measured at 992650232)
|
||||
|
||||
| Code | Fact | Where |
|
||||
|---|---|---|
|
||||
| E1 | The business file references credentials per agent: `roles.<agent>.credentials.<kind> = {file or env, rotateBy or expires}`. The kinds an agent may hold are the ones its role definition lists | `business/src/business.mjs:89-111`, `credentials.mjs:18-38`, `role.mjs:105-118` |
|
||||
| E2 | The bus reads each token once at start (O_NOFOLLOW, own uid, 0600, at most 16 KiB, outside the repos and data root) into a private map keyed `<business>/<agent>:<kind>` | `bus/src/credentials.mjs:18-89`, `bus/src/runtime.mjs:78-89` |
|
||||
| E3 | Only Vikunja uses a token: `credentials.use("<business>/<agent>", "vikunja", ...)`. Gitea tokens are loaded and never used. `assertClean` refuses any event that contains a token | `tasks/src/adapter.mjs:70`, `runtime.mjs:134`, `broker.mjs:560` |
|
||||
| E4 | Agents run in bwrap as the bus's own uid. `~/.config`, `~/.mosaic` and `~/.ssh` are not bound. The git socket is bound only for `git.push.working`, `review.verdict` or a landing | `seat/src/sandbox.mjs:21-30, 104-124`, `cli/src/launcher.mjs:401` |
|
||||
| E5 | A session proves who it is with a 64-hex cap the broker mints and maps to a frozen session record. The git socket resolves the cap to (business, role) before anything else | `broker.mjs:280, 379-383`, `packages/cli/src/git-socket.mjs:398-407` |
|
||||
| E6 | The model credential is one env file per host (`EnvironmentFile=-%h/.config/mosaic-dev/secrets/claude-session.env`). `ENV_ALLOW` passes it into every sandbox, so every agent holds the same model token | `cli/systemd/mosaic-bus.service.in:24`, `seat/src/session.mjs:83-94` |
|
||||
| E7 | Commits in a session's clone use `<agent>@<business>.mosaic.invalid`. Landed squashes use "Mosaic bus host" | `sandbox.mjs:151-157`, `working.mjs:280-281` |
|
||||
| E8 | No code pushes to a remote. The git socket pushes bundles into a local bare working repo with no remote and no credential | `working.mjs:1-14, 198-238` |
|
||||
| E9 | Adding or changing an agent refuses a definition that declares credentials (`organization-credentials-out-of-scope`). Binding them is a hand edit. Row 194's design keeps that refusal for create and hire "until the credentials row" | `business/src/instance-settings.mjs:20-29`, row 194 DESIGN.md D1 |
|
||||
| E10 | The development repository's helper pushes as one identity from one JSON file | `scripts/git-credential-mosaic.mjs` |
|
||||
|
||||
So per-agent references and a host-only reader already exist for two kinds. What's missing: a fixed place for
|
||||
the files, an agent identity, any remote push, lifecycle verbs, and a model credential that agents don't
|
||||
share.
|
||||
|
||||
## 2. The store (brief item 1)
|
||||
|
||||
D1. **Layout.** Each stack instance keeps its agents' secrets under its own config dir:
|
||||
|
||||
```
|
||||
<configDir>/secrets/agents/ 0700, owner = the stack instance's OS user
|
||||
<configDir>/secrets/agents/<business>/<agent>/ 0700
|
||||
<configDir>/secrets/agents/<business>/<agent>/<kind> 0600, the token only, one line
|
||||
```
|
||||
|
||||
This is row 190's secrets root (`<configDir>/secrets/...`, as Q175's slot uses). The token file keeps E2's
|
||||
format, so the bus reader doesn't change. Nothing in a token file is a name, an email or a URL. Those are not
|
||||
secret and go in the business file (D6), where review sees them.
|
||||
|
||||
D2. **The business file reference.** A new reference form, `{"store": true, "rotateBy" | "expires": DATE}`,
|
||||
means "the file at the layout path for this business, agent and kind". The path is derived, never written,
|
||||
so an agent's entry can't name another agent's file. The old `{file}` and `{env}` forms stay valid for
|
||||
migration (section 7). A `{file}` reference that points inside `<configDir>/secrets/agents/` but not at its
|
||||
own agent's path refuses `credential-not-yours` when the business file loads.
|
||||
|
||||
D3. **Read path.** Unchanged in shape: the bus reads every entry once at start (E2) and on a store change
|
||||
(section 6), and holds the values in memory. No sandbox binds `<configDir>` (E4 already holds). No token goes
|
||||
into a sandbox for `gitea` or `vikunja`. The agent asks the bus to act, and the bus uses the token.
|
||||
|
||||
D3a. **A `store` reference with no file.** ENOENT on a `store` path loads as an absent entry. `status` shows
|
||||
`not-issued`, and `useFor` refuses `credential-unavailable`. The bus still starts. This is the state after a
|
||||
create without `issue` (D13) and after `revoke` (section 6). Every other defect still refuses start: wrong
|
||||
owner or mode on the file or a dir, a symlink, a non-regular file, bad format, a missing or wrong-mode
|
||||
`secrets/agents/` dir. `{file}` and `{env}` keep E2's behavior, where a missing file refuses start.
|
||||
|
||||
## 3. The "RLS equivalent" (brief item 1)
|
||||
|
||||
Three layers. Each one is enforced by code or the OS, not by prose.
|
||||
|
||||
D4. **OS, between stack instances.** Files are 0600 and dirs 0700, owned by the stack instance's own OS user.
|
||||
Case 1's user can't read Case 2's store (Q180: each instance under its own user without sudo). The bus refuses
|
||||
to start when any file or dir under `secrets/agents/` has the wrong owner or mode (`credential-file`, as E2
|
||||
does for files today, extended to the dirs).
|
||||
|
||||
D5. **Bus, between agents of one stack instance.** The principal for every credential use comes only from the
|
||||
verified cap (E5). The agent-facing API is `credentials.useFor(session, kind, op)`: `session` is the broker's
|
||||
frozen record, and the key is built from `session.business` and `session.role`. It has no agent argument, so
|
||||
there is nothing in a request to point at another agent. The existing `use(name, kind, op)` stays for the
|
||||
host's own `@sync` entry and is not reachable from a request. The tasks adapter's per-role calls (`ctx.as`,
|
||||
`verbs.mjs:126`) move to `useFor`. Any request field that names an agent other than the caller (`as`,
|
||||
`agent`, `instance`, `role` on a credential path) is refused `credential-not-yours` and recorded through
|
||||
`broker.refuse()` as `action.refused`, with the caller's cap and the task ref. A build test greps every
|
||||
`credentials.use(` call site and fails on a new one outside the allowlist.
|
||||
|
||||
D6. **What stays open.** A process of the same OS user outside the sandbox can read every agent's file. That
|
||||
is the same limit as the human proof (P19 333). Q180's per-user VMs move the boundary to "one stack instance
|
||||
per OS user". Agents of one instance are kept apart by the sandbox and D5, not by the OS.
|
||||
|
||||
## 4. Kinds (brief item 2)
|
||||
|
||||
| Kind | 10-27 | Use |
|
||||
|---|---|---|
|
||||
| `gitea` | yes | Git push to a granted remote (section 5). Gitea API verbs (issues, pull requests) wait. The factory's review flow runs in the working repo today, and no 10-27 case needs an agent to open a pull request |
|
||||
| `vikunja` | yes, exists | Moves to `store` references and `useFor`. No behavior change |
|
||||
| `model` | per stack instance only | See D7 |
|
||||
|
||||
D7. **Model provider.** A model token has to sit inside the sandbox today, because the harness calls the
|
||||
provider itself (E6). Per-agent model tokens need a proxy that holds the real token and gives each run its own
|
||||
short-lived one, which is the CLIProxyAPI line in TARGET.md. That waits for its own row. For 10-27 the model
|
||||
credential is per stack instance: row 190's `<configDir>/secrets/claude-session.env`, owned by that
|
||||
instance's OS user. It is never a copy of jwoltje's file. Jason signs in on each VM user himself (A6). Whether
|
||||
Case 1 and Case 2 run on Jason's existing subscription or need a new account is Jason's call (Q-J1). This row
|
||||
doesn't pick. A pi model credential also waits for P19 #383.
|
||||
|
||||
## 5. Git identity and push (brief item 3)
|
||||
|
||||
D8. **Identity.** A new business-file key, `roles.<agent>.identity = {name, email, gitea: {url, user}}`. It is
|
||||
optional, because today's business files with a `gitea` agent (coder) must load unchanged. `publish` (D9
|
||||
step 2) requires it, and requires that the definition declare `gitea`. A business file with a `publish`
|
||||
grant on an agent that lacks either refuses at load, and the error names `roles.<agent>.publish` and says
|
||||
"needs identity and a gitea definition" (N10). Load is the only place this is checked: a business file that
|
||||
fails it never reaches the bus. The sandbox sets `GIT_AUTHOR_*` and `GIT_COMMITTER_*` from
|
||||
it in place of E7's invalid domain. The email is the one Jason gives the agent's Gitea user, so Gitea links the
|
||||
commits to that user. Agents without `gitea` keep E7.
|
||||
|
||||
D9. **Push to a remote: a new git socket op, `publish`.** No new socket, no bundle, and no token in the sandbox.
|
||||
|
||||
```
|
||||
publish {"cap", "op": "publish", "task_ref", "project", "branch", "decision"}
|
||||
-> {"ok": true, "result": {"fork", "ref", "commit", "previous"}} or a refusal
|
||||
```
|
||||
|
||||
The host checks, in order, and each refusal goes through `broker.refuse()`:
|
||||
|
||||
1. Identity from the cap (E5), then the broker's `#checkAuthority(s, "git.push.remote", {decision, target:
|
||||
task_ref})`, as `checkLand` does for a merge (`broker.mjs:907-913`). `git.push.remote` is a new action in
|
||||
ACTIONS and is not in ROUTED (REQ-WAIT-3, `packages/business/src/vocabulary.mjs:52-58`,
|
||||
`broker.mjs:42-69`), so it is a hard wait and classifies `gated` for every role. Every publish therefore
|
||||
needs a decision the human resolved for this task ref. A decision is spent once (`decision-consumed`).
|
||||
|
||||
**The decision binds one commit.** `#authorityFor` binds action, role, thread and target only
|
||||
(`broker.mjs:769-798`), so the commit needs its own carrier. The raise records it the same way it records
|
||||
`holderRun` for `role.revoke` (`broker.mjs:1733`). When the action is `git.push.remote`, the
|
||||
`decision.raise` event body gets `pushCommit`. That is the commit of `#latestPush(business, target)` at
|
||||
raise time, read by the broker and never taken from the agent. A raise with no recorded push for the task
|
||||
refuses `no-push`. The decision's question shown to the human ends with that commit. `#authorityFor` then
|
||||
refuses `decision-mismatch` when `context.pushCommit` differs from the commit of `#latestPush` now, which
|
||||
is the same check `holderRun` gets at `broker.mjs:796`. So the sequence approve at A, push B, publish B
|
||||
refuses.
|
||||
No role definition grants the action yet. When Jason's PRD version routes it (Sage, Vikunja #420), a
|
||||
reviewed commit adds it to both ROUTED lists and to coder's `withinRole`. From then on a coder publishes
|
||||
without a decision, and `decision` is optional for a within-role class only.
|
||||
2. The project is in the agent's grant, `roles.<agent>.publish: [project]`, and the business file names the
|
||||
project's upstream repository, `projects.<project>.remote: "https://HOST/OWNER/REPO"`
|
||||
(`remote-not-granted`). Both URLs pass D9c's parse at load. The upstream's host and port equal
|
||||
`identity.gitea.url`'s (`remote-host-mismatch`). The host never contacts that URL. It builds the agent's
|
||||
fork, `fork = https://HOST[:PORT]/<gitea.user>/REPO`, from the upstream's origin and repository name and
|
||||
the agent's own `gitea.user`. `fork` is the only URL the rest of `publish` uses: D9a's `ls-remote`, step
|
||||
5's push, the helper's host match, `checkPublish`'s result and step 6's event. So the owner of the
|
||||
repository that receives a push is the agent's own Gitea user on every push, by construction, and the
|
||||
agent can't name it (webby-0011 N1). D9c is what makes "by construction" hold. Gitea's refusal of a
|
||||
push to a repository the user doesn't own or collaborate on is the second layer. An upstream whose owner
|
||||
equals the agent's `gitea.user`, compared without case, refuses `remote-not-fork`, at load and at push. `remote` is a new key
|
||||
beside `root` and `repo` (`business.mjs:186`), and `identity` and `publish` are new keys on a role entry
|
||||
(`business.mjs:89`). All three are optional, so today's business files load unchanged.
|
||||
3. The source is one commit, `C = context.pushCommit`. It is checked three ways, so all three are equal:
|
||||
the decision's commit, the commit of `#latestPush`, and the work ref's tip in the working repo,
|
||||
`work/vikunja-<project>-<task>` (`working.mjs:110-114`). The tip is read under the repository's lock, and
|
||||
if they differ the push refuses `ref-moved`. The work ref is per task, not per agent. Two checks make it the
|
||||
caller's: `#latestPush` was recorded by the caller's role (`work-not-yours`), and step 4. The destination is always `refs/heads/<gitea.user>/<branch>`, built by the host.
|
||||
`validBranch(branch)` holds (`invalid-request`). The agent never names the destination ref.
|
||||
4. Every commit in `main..<work ref>` has author and committer email equal to `identity.email`
|
||||
(`identity-mismatch`).
|
||||
5. The push runs through `credentials.useFor(session, "gitea", ...)`: `git push <fork> <C>:<dst>`, by oid,
|
||||
with no `+` and no `--force`. It never pushes by ref name, so a push to the working repo after
|
||||
`checkPublish` can't change what goes out (`non-fast-forward` from git stays a refusal). Git runs with E8's SAFE flags
|
||||
(`working.mjs:92-98`), which set `protocol.allow=never`. `publish` adds exactly two flags,
|
||||
`protocol.https.allow=always` and `http.followRedirects=false`, plus `GIT_TERMINAL_PROMPT=0` and no global
|
||||
or system config. The token reaches git through a one-shot credential helper that reads it from an
|
||||
inherited pipe, never through argv, env, the URL or a file. Seat `gitEnv` (`checkout.mjs:79-89`) takes only
|
||||
PATH from the host and sets the rest itself, so no proxy or `GIT_SSL_*` variable reaches git. `publish` takes a `tls` parameter that
|
||||
defaults to `[]`, the same way `push` takes `fsck = FSCK` (`working.mjs:100`). Tests pass
|
||||
`["-c", "http.sslCAInfo=<scratch CA>"]`. The host never sets it, and nothing in config or env reaches it. The helper answers `get` only when `protocol`
|
||||
is `https` and `host`, port included, equals `fork`'s. It writes nothing for any other `get` and for
|
||||
`store` and `erase`. `assertClean` checks git's output, so a fork that echoes the token gets
|
||||
`credential-leak`.
|
||||
6. The bus records `action.allowed` for `git.push.remote` with fork, ref and commit.
|
||||
|
||||
D9a. **The agent's fork must share root's history.** A push sends every object the fork lacks. The working
|
||||
repo's `main` is the project root's history (`working.mjs`, seed), so a fork that doesn't share it would
|
||||
receive all of root, landing squashes included. The check runs on `fork`, never on the upstream: a related
|
||||
upstream says nothing about what the agent's `<user>/REPO` holds.
|
||||
`publish` runs `seed` first (`working.mjs:164-176`), under the repository's lock as `push` does, so `main`
|
||||
catches up with root before the check below. That means a fork also receives any landed root commits that
|
||||
upstream doesn't have yet, because they sit under the agent's work. That is expected: the agent's branch is
|
||||
built on them.
|
||||
Before the push, the host runs `git ls-remote --symref <fork> HEAD` (same flags and helper). It refuses
|
||||
`remote-unrelated` when the fork is empty or when that tip isn't a commit the working repo already has and an
|
||||
ancestor of `main`. When `ls-remote` exits non-zero, for a fork that doesn't exist, a refused token or a
|
||||
network failure, `publish` refuses `fork-unavailable` and the push doesn't run (N11). Git's stderr isn't
|
||||
parsed to tell those apart and isn't returned, since it can carry the URL. Only `ls-remote` crosses the
|
||||
network for this, so nothing is fetched from the fork. If the fork has moved past root, the refusal stands
|
||||
until root catches up. That is a human pull into root, not an agent's.
|
||||
|
||||
D9c. **The URL parse (B6).** `projects.<p>.remote` and `identity.gitea.url` are parsed with `new URL()` at load,
|
||||
and the business file refuses to load unless all of these hold:
|
||||
|
||||
- The scheme is `https:`. There is no username, password, query or fragment.
|
||||
- The string equals its own normalized form: for the upstream, `url.origin + url.pathname`; for
|
||||
`gitea.url`, `url.origin + "/"` or `url.origin`. So a dot segment (`/<user>/../OWNER/REPO`), a
|
||||
percent-encoded dot, an upper-case host, an explicit `:443`, a backslash, userinfo, a query and a fragment
|
||||
each refuse, because `new URL()` would change them (checked with node: each differs from its normalized
|
||||
form). A trailing slash survives normalization and is refused by the two-segment rule below. The checks below run on that path, so the owner that is checked is
|
||||
the owner that is used.
|
||||
- The upstream's path is exactly `/OWNER/REPO`: two non-empty segments, no more and no fewer. One trailing `.git` on
|
||||
REPO is allowed and stripped. `fork` is built from the stripped REPO.
|
||||
- `OWNER`, `REPO` and `gitea.user` each match Gitea's name rule, `^[A-Za-z0-9_.-]{1,100}$` and not `.` or
|
||||
`..`. REPO after stripping must still match.
|
||||
- `gitea.url`'s path is `/`. A Gitea served under a sub-path isn't supported by this row and refuses at load.
|
||||
- Host and port are compared after `new URL()` normalized them, and an omitted port counts as 443 on both
|
||||
sides.
|
||||
- Gitea compares user and repository names without case (`lower_name`), so `remote-not-fork` compares the
|
||||
upstream's OWNER with `gitea.user` without case. An upstream owned by `Coder` and a `gitea.user` of `coder`
|
||||
would otherwise build a "fork" that is the upstream itself.
|
||||
|
||||
D9b. **How `publish` reaches the broker and the store.** The git socket runs in the bus process
|
||||
(`cli.mjs:504-506`) and reaches the broker only through the host's op table (`bus/src/runtime.mjs:222-246`).
|
||||
`publish` adds three entries there, each wrapped in `refusing()`:
|
||||
|
||||
- `checkPublish(cap, args)`: steps 1 to 3, including the decision check. It returns `{ticket, fork, user,
|
||||
email, source}` and no token. The ticket holds the session and the decision until `recordPublish`, the
|
||||
same way `checkLand` holds a landing.
|
||||
- `withGitea(cap, fn)`: `credentials.useFor(broker.session(cap), "gitea", fn)`. `fn` gets the token and runs
|
||||
step 5. The token never leaves that call.
|
||||
- `recordPublish(ticket, result)`: step 6, and it spends the decision.
|
||||
|
||||
The op table gets no entry that returns a token.
|
||||
|
||||
D10. **Which refs.** Our side: only `refs/heads/<gitea.user>/*`, set by the host (D9 step 3). The default
|
||||
branch and other agents' prefixes can't be named. Gitea side, two options (Q-M1):
|
||||
|
||||
- **O1. A fork per agent. Ruled by Mos (mos-1027).** The agent's Gitea user has read on the upstream repo and owns a fork
|
||||
named like it. The host derives the fork's URL from the upstream and the agent's user (D9 step 2).
|
||||
Its token can write only repos the user owns or collaborates on, so Gitea itself stops any push to upstream
|
||||
or to another agent's fork. No branch rules are needed per agent, so none can drift. The cost: work reaches
|
||||
upstream as a pull request from the fork, opened by a human or a later API verb.
|
||||
- **O2. One shared repo. Not chosen.** The agent is a Write collaborator. Upstream's default and release branches get
|
||||
branch protection with no agent on the push list, plus a rule `<user>/*` per agent. Gitea doesn't stop agent
|
||||
A from creating an unprotected name outside `A/*` unless a catch-all rule outranks the per-agent ones. That
|
||||
depends on Gitea's rule priority, which I haven't verified on our version (probe P1). Every hire adds a rule.
|
||||
|
||||
Token scopes for either: `write:repository` and `read:user`, nothing else, per vocabulary.mjs rules (no
|
||||
`all`, no admin).
|
||||
|
||||
D11. **The host git socket.** `publish` is added to the socket. `push`, `fetch` and `land` don't change. The
|
||||
socket's bind rule (E4) adds `git.push.remote`. The working repo stays credential-free and remote-free. The
|
||||
`fork` URL is passed per call and never written into its config. A landing still squashes into the project
|
||||
root as "Mosaic bus host". Landings don't push anywhere.
|
||||
|
||||
## 6. Lifecycle (brief item 4)
|
||||
|
||||
All verbs are on the bus, in the CLI (`mosaic credentials ...`) and in the Console's agent page. Every verb is
|
||||
human-only. Under Q173 the tester may run `status` and `revoke` on the dev stack instance. `issue` and
|
||||
`rotate` stay with Jason, because the tester is an agent and the token would pass through its session
|
||||
(duckworth N1). Each one appends to the organization
|
||||
writer's `changes.jsonl` (actor, agent, kind, verb, date; never a value). No schema migration, no new event
|
||||
kind. A refusal uses `action.refused`.
|
||||
|
||||
| Verb | What it does |
|
||||
|---|---|
|
||||
| `issue AGENT KIND --rotate-by DATE` (gitea) or `--expires DATE` (vikunja) | Reads the token on stdin (never argv), checks E2's format, writes `<kind>.tmp` 0600, fsyncs, renames over `<kind>`, and sets the business file reference to `{"store": true, date}`. The bus reloads that one entry, with no restart |
|
||||
| `rotate` | Same as `issue` over an existing entry. The old value is gone from disk and memory when the call returns. Jason then deletes the old token in Gitea (A5) |
|
||||
| `revoke AGENT [KIND] --reason` | Drops the entry from the bus's memory first, then unlinks the file. A use in flight finishes. Every later use refuses `credential-unavailable` |
|
||||
| `delete AGENT` | `revoke` for every kind, then removes the agent's dir. Refused while the agent has a running claim |
|
||||
| `status [AGENT]` | Kind, date and state per entry (E3's `status()`), no values |
|
||||
|
||||
D12. **"Stops working at once."** On our side this holds without a timing window. A fired agent never holds a
|
||||
`gitea` or `vikunja` token (D3), so revoking means the bus no longer has it. On the Gitea and Vikunja side the
|
||||
token still exists until Jason deletes it (A5). The status line for a revoked entry says so until Jason
|
||||
confirms. The model token is the exception: it sits in the running sandbox (D7), so a fired agent's runs must
|
||||
end. Row 194's fire ends them.
|
||||
|
||||
D13. **What row 194 calls.**
|
||||
|
||||
- `credentials.issue` is never called by the chief. Row 194's hire stays uncredentialed (its D1).
|
||||
- A human `create` of a credentialed definition: row 194 lifts E9's refusal for the human path only. The agent
|
||||
is created with `store` references and no files. `launch` refuses `credential-unavailable` until Jason runs
|
||||
`issue`.
|
||||
- `fire` and `delete`: row 194 calls `credentials.revoke(business, agent, {reason, actor})` before it changes
|
||||
the business file. A failed revoke stops the fire (`credential-revoke-failed`), so an agent can't be fired
|
||||
with its credentials still live on our side.
|
||||
- `regenerate`: no credential change.
|
||||
|
||||
## 7. Who does what (brief item 5)
|
||||
|
||||
Agents never mint accounts or tokens. Jason's steps, per agent that needs `gitea`:
|
||||
|
||||
- A1. Create the Gitea user: Site Administration, User Accounts, Create User Account. The name is the agent's
|
||||
`gitea.user`. The email is the agent's `identity.email`.
|
||||
- A2. Create its token: sign in as that user, Settings, Applications, Generate New Token, scopes
|
||||
`write:repository` and `read:user`, nothing else.
|
||||
- A3. Create the fork of each granted repo as that user (O1). Gitea's push-to-create
|
||||
(`ENABLE_PUSH_CREATE_USER`, `ENABLE_PUSH_CREATE_ORG`) stays off, so a push to a missing `<user>/REPO`
|
||||
fails rather than creating a plain repository (N11). `publish` refuses `fork-unavailable` before that.
|
||||
- A4. On the stack instance's VM, as its OS user: `mosaic credentials issue AGENT gitea --rotate-by DATE`, with
|
||||
the token pasted into stdin.
|
||||
- A5. On fire or rotate: delete the token in Gitea (Settings, Applications), or disable the user.
|
||||
- A6. Model: sign in to the provider on each VM user to write `<configDir>/secrets/claude-session.env`.
|
||||
|
||||
Vikunja bot accounts and tokens are already Jason's (tracker bots). Only A4 changes for them.
|
||||
|
||||
## 7a. The grant path (mos-1124, Q188, Q190)
|
||||
|
||||
Q188: no agent creates a grant. The chief raises the request, Jason approves it in the inbox, and protected
|
||||
stack code creates the grant. Jason called manual grant steps a no-fly, so A1 to A4 above are the fallback only.
|
||||
|
||||
- a. `credentials.issue`, and so `rotate`, accepts a resolved `credential.mint` decision, Jason's Approve, as
|
||||
the human path. #436b's executor calls it with that decision and nothing pasted. #436b adds that path and
|
||||
the decision kind. Row 199 ships the verbs with today's human check only.
|
||||
- b. Under Q190 ("Q12: O1"), each agent seat gets its own Gitea account. On the Approve, #436b's executor does
|
||||
A1 to A3: the seat's Gitea user, its token and its forks. Until #436b lands, pushes stay as jason.woltje.
|
||||
- c. Q185 settles Q-J1 and Q190 settles Q-J2.
|
||||
|
||||
No code in 07141966e changes for this section. The executor and the decision route are #436b's.
|
||||
|
||||
## 8. Migration (brief item 6)
|
||||
|
||||
- M1. Existing `{file}` and `{env}` references keep working. A business moves one agent at a time with
|
||||
`issue`, which rewrites that agent's reference to `store`.
|
||||
- M2. Nothing here touches the dev repository's helper, its `.git/config`, `~/.mosaic` or `~/.mosaic-dev`.
|
||||
Dev seats (jarvis, seat tokens) keep pushing as today. The mos-0981 rule ends only when Mos says so.
|
||||
- M3. The live dev stack instance needs no change at re-pin: no reference uses `store` until someone issues
|
||||
one.
|
||||
|
||||
## 9. Questions
|
||||
|
||||
Mos ruled on the authority model in mos-1027 (2026-10-20T19:27Z):
|
||||
|
||||
- Q-M1. O1, one fork per agent.
|
||||
- Q-M2. `git.push.remote` and its bounds in D9 to D11 are approved. It lands as a hard wait: out of ROUTED,
|
||||
and no role grants it within-role (D9 step 1). Sage puts its routing in the PRD version draft going to
|
||||
Jason with `role.hire` and `role.fire` (Vikunja #420). After his approval, a reviewed commit routes it and
|
||||
grants it to coder.
|
||||
- Q-M3. Issue, rotate and delete are human-only, and the chief gets none of them. Under Q173 the dev
|
||||
instance's tester gets `status` and `revoke` only.
|
||||
- Q-M4. The model credential stays per stack instance for 10-27. Per-agent model tokens wait for a proxy
|
||||
row.
|
||||
|
||||
Open for Jason. Mos takes these to him one at a time:
|
||||
|
||||
- Q-J1. The model account for Case 1 and Case 2: existing subscription or a new account. Settled by Q185 (7a).
|
||||
- Q-J2. Gitea users and tokens for the agents that need them (A1 to A3). Settled by Q190 (7a).
|
||||
|
||||
## 10. What this closes (brief item 7)
|
||||
|
||||
- G11: closes the store (D1 to D6) and lifecycle (section 6) for `gitea` and `vikunja`. Still open: per-agent
|
||||
model credentials (D7), and the same-uid limit inside one stack instance (D6).
|
||||
- G13: closes "own credentials for git" (D8 to D10). Still open: Gitea API verbs (section 4).
|
||||
- C2.5 (git remote): Case 2's books repo can use `publish` once its project names a remote.
|
||||
|
||||
## 11. Tests and mutants
|
||||
|
||||
All tests use scratch roots, fake tokens and a fake Gitea: `git http-backend` behind a node http server on
|
||||
127.0.0.1 that checks basic auth against a fake token and records each ref update. No request leaves the host.
|
||||
|
||||
- T1. Layout: wrong mode or owner on a dir or file refuses start. A `{file}` reference into another agent's
|
||||
path refuses `credential-not-yours`. A bus restart after a create without `issue` starts, and the agent's
|
||||
status is `not-issued`. A missing `{file}` still refuses start.
|
||||
- T2. `useFor` keys from the cap only. A request naming another agent is refused and leaves one
|
||||
`action.refused`.
|
||||
- T3. The fake Gitea serves https on 127.0.0.1 with a cert from a scratch CA made by the test. It reaches git
|
||||
only through the `tls` parameter (D9 step 5). Each refusal test that should reach the network asserts that
|
||||
the first port received the request, so no test passes because git refused http first. `publish`: the
|
||||
happy path lands `refs/heads/<user>/<branch>` on the agent's fork in the fake, which shares root's history, at
|
||||
exactly the decision's commit. Refusals for each step in D9, one test each, including `work-not-yours`. Until routing, a publish with
|
||||
no decision refuses, one with a decision for another task or commit refuses, and a second publish on a
|
||||
spent decision refuses `decision-consumed`. Approve while the latest push is A, push B, publish: refuses
|
||||
`decision-mismatch`, and the fork receives nothing. A push to the working repo between `checkPublish` and
|
||||
the git push refuses with `ref-moved` or `decision-mismatch` (sage-1440, BUILD.md deviation 14). A raise with no recorded push refuses `no-push`. A test pins that `git.push.remote` is in neither ROUTED list
|
||||
and that no `roles/*.json` grants it. A force-push attempt is
|
||||
refused by git's non-fast-forward. A fake fork that answers 302 to a second 127.0.0.1 port: the push
|
||||
fails, and the second port receives no request with an Authorization header. A remote on the right host
|
||||
and a different port is refused at step 2. Two agents granted one project each land on their own fork:
|
||||
agent A's push reaches `/<A's user>/REPO` only and B's reaches `/<B's user>/REPO` only, and the fake's
|
||||
upstream repository receives nothing. An upstream owned by the agent's own user refuses `remote-not-fork`,
|
||||
and so does one whose owner differs from `gitea.user` only in case. With an upstream that shares root's
|
||||
history, a first publish to an empty fork and to an unrelated fork each refuse `remote-unrelated`, and
|
||||
neither fork nor upstream receives objects. A fork that doesn't exist refuses `fork-unavailable`, and the
|
||||
fake records no push. `ls-remote` and the push each reach the fork's path only (the fake records every
|
||||
path). Load refusals (D9c), one test each: a dot segment, an extra path segment, a missing segment,
|
||||
a trailing slash, userinfo, a query, a fragment, an explicit `:443`, a name outside Gitea's rule, and a `gitea.url` with a
|
||||
sub-path. A `.git` suffix loads and builds the fork without it. A `publish` grant on an agent without
|
||||
`identity`, and on one whose definition lacks `gitea`, each refuse at load with the N10 message.
|
||||
- T4. Token handoff: the token never appears in argv, env, the fork URL, the working repo config,
|
||||
`changes.jsonl`, events or logs (scan every file under the scratch roots).
|
||||
- T5. Lifecycle: issue, rotate, revoke and delete, each live with no restart. After revoke, `publish` and the
|
||||
Vikunja path refuse `credential-unavailable`, and so they do after a bus restart that follows the revoke.
|
||||
The tester can run `status` and `revoke`, and `issue` and `rotate` refuse it.
|
||||
- T6. Default: an agent without `identity` commits as E7. Existing `{file}` references load unchanged.
|
||||
|
||||
Mutants to kill: drop the cap-derived key in `useFor`, let the request name the destination ref, add `+` to
|
||||
the refspec, skip the identity check, put the token in the URL, revoke file before memory, skip the dir mode
|
||||
check, allow a `{file}` reference into another agent's dir, make ENOENT on a `store` path fatal again, make
|
||||
ENOENT on a `{file}` path silent, a helper that answers any host, drop `http.followRedirects=false`, drop the
|
||||
`work-not-yours` check, drop the `remote-unrelated` check, add `git.push.remote` to ROUTED, skip the spent
|
||||
check on the decision, drop the `pushCommit` comparison in `#authorityFor`, push by ref name in place of the
|
||||
oid, push to the upstream URL in place of the derived fork, run `ls-remote` against the upstream in place of
|
||||
the fork, take the fork's owner from anywhere but the agent's `gitea.user`, compare the owner with case, run the
|
||||
D9c checks on the raw path in place of the normalized one, read a failed `ls-remote` as empty, skip the tip
|
||||
check under the lock, and pass a CA flag in production.
|
||||
|
||||
## 12. Risks
|
||||
|
||||
- R1. Backups: if a backup tool copies `<configDir>`, it copies tokens. The build checks row 185's backup set
|
||||
and says what it includes.
|
||||
- R2. Withdrawn. O1 was ruled, so probe P1 isn't needed.
|
||||
- R3. Outbound HTTPS from the bus host to Gitea is new network reach for the host process. On the VMs this is
|
||||
Jason's network to confirm.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Morgana
|
||||
|
||||
Builder seat, Claude Opus 5.5, started by Sage for MVP T1 row 193 (human users and RBAC, #1691) under mos-0974 and mos-0977.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `morgana/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
n=0
|
||||
for p in /proc/[0-9]*; do
|
||||
c=$(tr '\0' ' ' < $p/cmdline 2>/dev/null) || continue
|
||||
case "$c" in *shell-snapshots*|*chrome-devtools-mcp*|*sage-busy-now*) continue;; esac
|
||||
if echo "$c" | grep -qE 'node --test|gate\.sh|mutant|scripts/test-[a-z-]+\.sh|docker compose'; then echo "BUSY ${p#/proc/}: ${c:0:160}"; n=$((n+1)); fi
|
||||
done
|
||||
[ $n -eq 0 ] && echo CLEAR
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -o pipefail
|
||||
# Landing gate template (2026-10-14): sed the G= and OUT= lines and this comment.
|
||||
# Sequential suites, each teed to its own log; stops at the first failure.
|
||||
G=/mnt/storage/scratch/SET-ME-wt
|
||||
OUT=/mnt/storage/scratch/SET-ME-out
|
||||
cd "$G" || exit 9
|
||||
mkdir -p "$OUT" || exit 9 # a missing OUT failed the row 142 gate at step 1 (2026-10-15)
|
||||
# test-task.sh live cases run `docker compose` from $G, so without a name the project is "wt"
|
||||
# and leaves wt_default behind (row 142 gate, 2026-10-15). Name it per gate dir and remove it on exit.
|
||||
export COMPOSE_PROJECT_NAME="$(basename "$(dirname "$G")")"
|
||||
trap 'docker network rm "${COMPOSE_PROJECT_NAME}_default" >/dev/null 2>&1 && echo "removed network ${COMPOSE_PROJECT_NAME}_default"' EXIT
|
||||
# A gate tree without the node_modules link fails cli tests with ENOENT (2026-10-13).
|
||||
[ -d "$G/node_modules" ] || { echo "STOP no node_modules in $G"; exit 8; }
|
||||
echo "base $(git rev-parse --short HEAD) dirty $(git status --short | grep -vc node_modules) DOCKER_HOST=${DOCKER_HOST:-unset}"
|
||||
run() { local name="$1" dir="$2" cmd="$3"
|
||||
(cd "$G/$dir" && bash -c "$cmd") > "$OUT/$name.log" 2>&1; local rc=$?
|
||||
echo "$name rc=$rc $(grep -E '^ℹ (pass|fail) [0-9]+' "$OUT/$name.log" | tr '\n' ' ') | $(tail -n1 "$OUT/$name.log")"
|
||||
[ $rc -eq 0 ] || { echo "STOP $name"; exit $rc; }; }
|
||||
# No pkg-queue step: scripts/test-queue.sh runs packages/queue/tests/ itself (same 156 tests; CI debate item 2a, Vikunja #274).
|
||||
for d in packages/*/; do p=$(basename "$d"); [ -d "$d/tests" ] || continue; [ "$p" = queue ] && continue
|
||||
run "pkg-$p" "packages/$p" 'node --test tests/*.test.mjs'; done
|
||||
for s in scripts/test-*.sh; do n=$(basename "$s" .sh); run "script-${n#test-}" . "bash $s"; done
|
||||
echo GATE_PASSED
|
||||
@@ -1,36 +0,0 @@
|
||||
# Row 189 gate timing (for row 197, Herb)
|
||||
|
||||
Sage, 2026-10-20T19:25:04Z. Gate on 0c177e9ef (base 0217b71c8), one lane. The slot was mine. Seats may have run single test files at the same time, which the slot rules allow, and I did not record that.
|
||||
Start 2026-10-20T19:09:37Z, end 2026-10-20T19:20:36Z. Total wall time: 659 s.
|
||||
|
||||
Wall time per suite is the gap between one suite's log mtime and the one before it (the first suite starts at start.txt), so it includes node start-up. node's duration_ms is listed where the suite prints it. Scripts print no duration.
|
||||
|
||||
| Suite | Wall s | duration_ms s | Ended (UTC) |
|
||||
|---|---|---|---|
|
||||
| pkg-bus | 3 | 3.8 | 19:09:40 |
|
||||
| pkg-business | 3 | 2.2 | 19:09:43 |
|
||||
| pkg-cli | 388 | 388.5 | 19:16:11 |
|
||||
| pkg-control-board | 1 | 0.8 | 19:16:12 |
|
||||
| pkg-conversation | 35 | 35.4 | 19:16:47 |
|
||||
| pkg-discord | 3 | 2.6 | 19:16:50 |
|
||||
| pkg-harness | 12 | 11.6 | 19:17:02 |
|
||||
| pkg-ledger | 13 | 13.5 | 19:17:15 |
|
||||
| pkg-mosaic | 3 | 2.3 | 19:17:18 |
|
||||
| pkg-plugins | 0 | 0.3 | 19:17:18 |
|
||||
| pkg-project | 1 | 1.2 | 19:17:19 |
|
||||
| pkg-runs | 1 | 0.8 | 19:17:20 |
|
||||
| pkg-seat | 6 | 6.2 | 19:17:26 |
|
||||
| pkg-tasks | 1 | 1.2 | 19:17:27 |
|
||||
| pkg-webui | 58 | 57.9 | 19:18:25 |
|
||||
| script-auth | 1 | - | 19:18:26 |
|
||||
| script-conductor | 5 | - | 19:18:31 |
|
||||
| script-config | 2 | - | 19:18:33 |
|
||||
| script-discord | 8 | - | 19:18:41 |
|
||||
| script-extension-package | 1 | - | 19:18:42 |
|
||||
| script-foundation | 40 | - | 19:19:22 |
|
||||
| script-land-work | 11 | - | 19:19:33 |
|
||||
| script-queue | 21 | - | 19:19:54 |
|
||||
| script-release | 11 | - | 19:20:05 |
|
||||
| script-task | 31 | - | 19:20:36 |
|
||||
|
||||
Source: /mnt/storage/scratch/sage-g189/out/*.log mtimes and gate.log. The cli suite is about 59% of the run, and webui plus conversation add about 14%.
|
||||
@@ -1,86 +0,0 @@
|
||||
# Row 189: `stack-release.sh activate --no-send`
|
||||
|
||||
Gitea #1687. Vikunja P19 #413 (Q175 interim, #412). Brief: docs/plans/2026-10-20_activate-no-send.md.
|
||||
Rulings: mos-0972 (the Console runs without `--send` until Q175 phase 2), mos-0973 (go; Sage builds,
|
||||
Dewey reviews; gate is the cli suite plus every scripts/test-*.sh). Base: def2e7932 (refactor, the
|
||||
brief commit on 9ae8c73ac).
|
||||
|
||||
## What it does
|
||||
|
||||
`activate ID BUSINESS --no-send` drops `--send` from `RUN_CONSOLE` in a new `send_for_target`, which
|
||||
`console_for_target` calls after `proof_for_target`. `install_stack` renders the unit from
|
||||
`RUN_CONSOLE` and `write_pointer` writes the pointer from it, so both lose the flag and `flags_drift`
|
||||
stays quiet afterwards. Only `activate` takes the flag; the parser sends every other op to usage
|
||||
(exit 4).
|
||||
|
||||
`check_target` lets a `--no-send` activate name the pointer's copy only when the pointer's Console
|
||||
flags hold `--send`. That run is a live-copy re-pin, the same path row 187 uses for the proof pair:
|
||||
the plan prints the live-copy note, the run stops both units, backs up the stopped store and starts
|
||||
them again. A plain same-copy activate, or a same-copy `--no-send` once `--send` is gone, still
|
||||
refuses `already-active`.
|
||||
|
||||
`--plan` prints one `send:` line after the `proof:` line, for activate and rollback:
|
||||
|
||||
- `send: dropped; --no-send was given`
|
||||
- `send: off; --no-send was given and the pointer holds no --send`
|
||||
- `send: carried` (the pointer's `--send` goes on unchanged)
|
||||
- `send: off`
|
||||
|
||||
The steps log gets `drop --send; --no-send was given`, or `--no-send was given and the pointer holds no
|
||||
--send, so there is nothing to drop`.
|
||||
|
||||
Rollback and pm-on carry the pointer's flags as they are, so once `--no-send` has run, nothing adds
|
||||
`--send` back. There is no re-enable flag (mos-0973). Q175 phase 2 changes the Console flags in its
|
||||
own row.
|
||||
|
||||
docs/TOOLS.md: `[--no-send]` in the usage block and one paragraph after the proof paragraph.
|
||||
|
||||
## Tests (packages/cli/tests/stack-release.test.mjs, T55)
|
||||
|
||||
1. Seeded preview pointer with `--send --tester-port 7333`: a same-copy `activate B acme --no-send`
|
||||
passes. The pointer and the unit ExecStart lack `--send` and keep the other flags in order, status
|
||||
says the unit flags match, the steps log has the drop line, the log line has from = to = B and the
|
||||
backup sum equals the stopped store's, and the units stop and start in order.
|
||||
2. Plain same-copy activate, with `--tester-port` and with `--no-proof`, refuses `already-active`. So does
|
||||
a same-copy `--no-send` once `--send` is gone, and on a pointer that never held it.
|
||||
3. The four `send:` lines, the live-copy note on the same-copy plan only, and the Console line right
|
||||
after `send:` without `--send`. The run on a pointer with no `--send` logs the nothing-to-drop line
|
||||
and no drop line.
|
||||
4. After `--no-send` on C1, a rollback to B leaves `--send` out of the pointer and the unit, the steps
|
||||
log never names `--send`, and status matches. pm-on after `activate --pm-after-harness --no-send`
|
||||
keeps it off.
|
||||
5. `--no-send` on rollback (with and without `--plan`), pm-on, status, adopt and rehearse is a usage
|
||||
error with the usage text, nothing written, started or logged. The usage text shows `[--no-send]`.
|
||||
6. A pointer holding both the proof pair and `--send`: the plans for `--no-proof`, `--no-send` and both
|
||||
name what each drops and carries. `--no-send` keeps the pair, `--no-send` with a new pair replaces it,
|
||||
`--no-proof` keeps `--send`, and both together leave neither. Status matches after each.
|
||||
|
||||
`planTail` gains `'send: off'` after `'proof: off'`, because every plan now prints the line. T36 takes
|
||||
`out.slice(-n)` around `planTail`, so its four slice counts grow by one.
|
||||
|
||||
## Mutants
|
||||
|
||||
mutants.sh in this packet, run on the candidate. Each mutates scripts/stack-release.sh and runs T55
|
||||
plus T1 and T2.
|
||||
|
||||
- M1: the same-copy exception ignores `--no-send`.
|
||||
- M2: the same-copy exception allows `--no-send` without `--send` in the pointer.
|
||||
- M3: with `--no-send`, the pointer is written from `P_CONSOLE`, so it keeps `--send` while the unit
|
||||
drops it.
|
||||
- M4: rollback re-adds `--send`.
|
||||
- M5: the parser accepts `--no-send` for rollback.
|
||||
|
||||
Results: mutants-run.log, all five killed (M1 5 failures, M2 2, M3 7, M4 2, M5 2).
|
||||
|
||||
## Deviations from the brief
|
||||
|
||||
- The brief's line numbers were taken before row 163 landed. The candidate corrects them to the
|
||||
9ae8c73ac lines (check_target :249-256, flags_drift :646-657, console_for_target :664-693, adopt
|
||||
:828).
|
||||
- Test 5 also covers rehearse and `rollback --plan`, which the brief did not list.
|
||||
|
||||
## Outbound calls
|
||||
|
||||
None. The change edits an array before the existing unit render and pointer write. Every test uses
|
||||
the stand-in systemctl, units, stores and data root the file already builds; no test touches
|
||||
`~/.mosaic-dev`, the live units or a slot file.
|
||||
@@ -1,5 +0,0 @@
|
||||
M1 KILLED fail=5
|
||||
M2 KILLED fail=2
|
||||
M3 KILLED fail=7
|
||||
M4 KILLED fail=2
|
||||
M5 KILLED fail=2
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Row 189 mutants M1-M5: each mutates scripts/stack-release.sh in the sage-b189 worktree, runs T55 and
|
||||
# the plan-tail tests, and restores the file from a checked backup.
|
||||
set -uo pipefail
|
||||
WT=/mnt/storage/scratch/sage-b189
|
||||
OUT=/mnt/storage/scratch/sage-b189-mut/out
|
||||
S=$WT/scripts/stack-release.sh
|
||||
mkdir -p "$OUT" || exit 1
|
||||
cp -p "$S" "$OUT/stack-release.sh.orig" || { echo "backup failed"; exit 1; }
|
||||
ORIG=$(sha256sum "$S" | cut -d' ' -f1)
|
||||
[ "$(sha256sum "$OUT/stack-release.sh.orig" | cut -d' ' -f1)" = "$ORIG" ] || { echo "backup differs"; exit 1; }
|
||||
restore() { cp -p "$OUT/stack-release.sh.orig" "$S"; [ "$(sha256sum "$S" | cut -d' ' -f1)" = "$ORIG" ] || { echo "RESTORE FAILED"; exit 1; }; }
|
||||
trap restore EXIT
|
||||
|
||||
mutate() {
|
||||
case "$1" in
|
||||
M1) sed -i '/{ \[ "\$NO_SEND" = 1 \] && \[\[ " \${P_CONSOLE\[\*\]} " == \*" --send "\* \]\]; } ||/d' "$S" ;;
|
||||
M2) sed -i 's/{ \[ "\$NO_SEND" = 1 \] && \[\[ " \${P_CONSOLE\[\*\]} " == \*" --send "\* \]\]; } ||/{ [ "$NO_SEND" = 1 ]; } ||/' "$S" ;;
|
||||
M3) sed -i 's/^ write_pointer "\$TO" "\$RUN_PM" "\$RUN_PENDING" "\$P_CLAUDE" "\$T_SCHEMA" "\${RUN_CONSOLE\[@\]}"$/ if [ "$NO_SEND" = 1 ]; then write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${P_CONSOLE[@]}"; else write_pointer "$TO" "$RUN_PM" "$RUN_PENDING" "$P_CLAUDE" "$T_SCHEMA" "${RUN_CONSOLE[@]}"; fi/' "$S" ;;
|
||||
M4) sed -i 's/^ SEND_PLAN="send: off"$/ SEND_PLAN="send: off"; [ "$OP" != rollback ] || RUN_CONSOLE+=(--send)/' "$S" ;;
|
||||
M5) sed -i 's/--no-send) \[ "\$OP" = activate \] || usage;/--no-send) { [ "$OP" = activate ] || [ "$OP" = rollback ]; } || usage;/' "$S" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
for m in M1 M2 M3 M4 M5; do
|
||||
restore
|
||||
mutate "$m"
|
||||
if [ "$(sha256sum "$S" | cut -d' ' -f1)" = "$ORIG" ]; then echo "$m NOT-APPLIED"; continue; fi
|
||||
bash -n "$S" || { echo "$m SYNTAX"; continue; }
|
||||
diff "$OUT/stack-release.sh.orig" "$S" > "$OUT/$m.diff"
|
||||
(cd "$WT" && TMPDIR=/mnt/storage/scratch/t189 timeout 900 node --test --test-reporter=tap --test-name-pattern='T55|T1 |T2 ' packages/cli/tests/stack-release.test.mjs) > "$OUT/$m.log" 2>&1
|
||||
fails=$(awk '/^# fail /{print $3}' "$OUT/$m.log")
|
||||
if [ "${fails:-0}" -gt 0 ]; then echo "$m KILLED fail=$fails"; else echo "$m SURVIVED"; fi
|
||||
done
|
||||
@@ -1,48 +0,0 @@
|
||||
# Common start for Sage's MVP T1 builder seats (mos-0974, mos-0977)
|
||||
|
||||
You are a builder seat on Mosaic Stack, repository /mnt/storage/src/mosaic-stack, branch base `refactor`.
|
||||
Sage leads the project and is your coordinator (T3 thread 1ef1e4f8-3ead-4208-beca-38f9f1add079). Mos speaks
|
||||
for Jason (thread 38bc4c81-6270-4021-b00c-6a15c43b9b0e). Human input reaches you only through Sage and Mos.
|
||||
|
||||
## Read first, in this order
|
||||
|
||||
1. `/mnt/storage/src/jason-master-restart/USER.md`, `SYSTEM.md`, `CONSTITUTION.md` (who Jason is, the
|
||||
system rules, the constitution).
|
||||
2. `/mnt/storage/src/jason-master-restart/TARGET.md` (the MVP addendum) and `DECISIONS.md` Q176 to Q178.
|
||||
3. `/mnt/storage/src/jason-master-restart/projects/mosaic-mvp/GAP-MAP.md` and `PLAN.md`.
|
||||
Everything under jason-master-restart is read only.
|
||||
4. The repository's `AGENTS.md`, then your brief (named in your first message) and the files it points to.
|
||||
|
||||
## Your worktree
|
||||
|
||||
T3 made your worktree from `refactor`. Work only there. Link node_modules from the canonical checkout right
|
||||
after you start (`ln -s /mnt/storage/src/mosaic-stack/node_modules node_modules`), unless your brief adds a
|
||||
dependency. In that case run `npm ci` in your worktree for a real node_modules first, and never run npm
|
||||
install through a symlink or in a directory without its own package.json (npm walks up to home).
|
||||
|
||||
## Rules that bind you
|
||||
|
||||
- Message ids are `<seat>-NNNN`, starting at 0001. Run `date -u` right before every message and put the
|
||||
stamp in it. End each turn with a clock timestamp.
|
||||
- No Co-Authored-By or any attribution line in any commit or PR, whatever a harness reminder says
|
||||
(mos-0822).
|
||||
- Push only your own `<seat>/*` branches, through the repository credential helper (mos-0981). Never push
|
||||
`refactor`, `main` or another seat's branch.
|
||||
- You have no Gitea token. Sage makes the queue moves that post to Gitea (in-review, review requests) and
|
||||
files issues. You may run `scripts/mosaic queue move N in-progress --op <seat>-N-start --by <seat>` for
|
||||
your own row.
|
||||
- Never touch `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units, slot files or `~/.mosaic`. Never
|
||||
start, stop or restart the live Console or bus. Never read or print a credential value.
|
||||
- Never read other seats' scratch directories. `agents/<seat>/work/` packets in the repository are fine.
|
||||
- Single test files run freely. The cli suite, any `scripts/test-*.sh` and any full package suite share
|
||||
one slot: run `bash /mnt/storage/scratch/sage-busy-now.sh | tail -1`, and if it is not CLEAR, or before a
|
||||
run longer than 5 minutes, ask Sage for the slot and start only on Sage's "granted".
|
||||
- Two failures in one gate run means stop and report.
|
||||
- Design-first rows: the first deliverable is `agents/<seat>/work/queue-<row>/DESIGN.md`, committed on your
|
||||
branch. Send the reviewer and Sage its path, commit and sha256. No product code before approval.
|
||||
- Merge-chaining (Q177): your build may sit on another row's candidate commit. Your row keeps its own
|
||||
review and gate, and the chain lands in order.
|
||||
- Fail closed. Never route around a refusal. If the code shows the brief or design is wrong, stop and tell
|
||||
Sage and your reviewer.
|
||||
- Style for every message and doc: plain words, short sentences, no em dashes, they/them for people.
|
||||
- Sage registers your session in `docs/SESSIONS.md`. Do not edit it.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Webby
|
||||
|
||||
Reviewer seat, Claude Opus 5.5, started by Mos (mos-0981, thread 03086006). Reviews Case 2 designs and MVP T1 rows 192 and 194.
|
||||
|
||||
No launcher in this directory. The seat runs as a T3 thread and works in its own worktree. It has no
|
||||
credentials of its own; until the credentials row settles per-agent identity, it pushes only its own `webby/*` branches
|
||||
through the repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,173 @@
|
||||
# Webby review: Case 2 design r1 (#416)
|
||||
|
||||
Webby, 2026-10-20 (from `date -u`). Independent reviewer, replacing Dewey (Mos, mos-0980).
|
||||
Subject: `agents/gosalyn/work/case2/DESIGN.md`, blob fba567f71dc1, at e997f3b76 and on
|
||||
origin/gosalyn/case2-design 88222f3f3 (same blob). Author: Gosalyn. Static review only. I ran no
|
||||
tests and changed no product code.
|
||||
|
||||
Code base: `git archive 5d019d876`. `git diff --stat 5d019d876 9ae8c73ac -- packages adapters roles scripts`
|
||||
is empty, so every line below also holds at origin/refactor 9ae8c73ac. Paths are relative to `packages/`
|
||||
unless they start with `adapters/` or `roles/`.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
Twelve blockers. Section 6's no-schema claim holds. Section 5's write-side guardrails hold. The
|
||||
message-side guardrails have two channels the design misses (B3, B4), and the smallest slice can't
|
||||
run as written (B1, B2).
|
||||
|
||||
## Citations checked
|
||||
|
||||
These hold as cited: git.mjs:8 (`protocol.allow=never` in SAFE), git.mjs:44 (any `remote.*` key
|
||||
refuses), git.mjs:55-56; project index.mjs:12, :31-84, :97, :195-234, :209, :248-261, :256-258;
|
||||
paths.mjs:7, :14-15, :34-48 (`absolute(project.root)` at :41 throws on a missing root);
|
||||
business.mjs:186-187; gate.mjs:130-167 (typed by name at :134, built-ins at :137-138);
|
||||
claude-gate.mjs:13; tools.mjs:6-7, :95-102, :186, :211-220, :271-280; vocabulary.mjs:92-97, :131;
|
||||
role.mjs:19-28, :55-66; broker.mjs:290-291, :642-650, :1378-1430, :1434-1435, :1867, :2331-2347,
|
||||
:2374-2399; schema.sql:240-250, :296-306, :310; runner.mjs:446-456; seat session.mjs:59-64;
|
||||
adapters/claude/adapter.sh:47-53; launcher.mjs:135, :236-238, :353; webui bus.js:885, :919.
|
||||
|
||||
One miss: the `ls-tree` and `cat-file` reads that 1.3 cites at read.mjs:67-86 are in `blob()` at
|
||||
read.mjs:19-26 (N1).
|
||||
|
||||
## Section 6: "no schema or trigger change"
|
||||
|
||||
It holds. `threads` has no definition column or check (schema.sql:240-250).
|
||||
`message_threads_match` (:296-299) and `delivery_same_thread` (:300-306) check one thread's
|
||||
instance, and a room is one thread per instance, so each insert in `room.send` passes as written.
|
||||
`thread_one_per_run` allows one held thread per run, which matches one session per agent. The only
|
||||
constraint the design must name is `UNIQUE (project, name)` (:249). `room.open` names each thread
|
||||
after its instance, so an existing thread with that name refuses (N9).
|
||||
|
||||
The single-instance rule is code, not schema: broker.mjs:291, :2332-2334, launcher.mjs:135 and
|
||||
:236-237. Changing it is C2-09's job, which is why B1 matters.
|
||||
|
||||
## Jason's three guardrails
|
||||
|
||||
| Guardrail | Enforced in code? |
|
||||
|---|---|
|
||||
| Agents never write prose for the user | Into the library: yes. Gate (gate.mjs:137-138), typed tools (no `write_record` without `record.write`, tools.mjs:211) and human-only verbs make an agent write impossible. In messages: only the form is bounded (M5), and two channels skip M5 (B3, B4). The 400-character limit is stated in 5.1, but the stated residual is too small (B5) |
|
||||
| Agents always refer to the source material | Structural once B6 and B7 are fixed. The quote-in-lines check at a reachable commit is real verification |
|
||||
| Tool use is guarded | Yes: gate, typed tool list, broker re-check (tools.mjs:6-7) and `#checkMessaging` (broker.mjs:1867). N6 closes a role-file gap |
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. The smallest slice gives Melanie no way to chat with a creative agent. `#threaded` is true only
|
||||
for definition `assistant` (broker.mjs:291). `thread.create` refuses `thread-instance-ambiguous`
|
||||
with more than one (:2334). The launcher refuses `thread-not-allowed` for any other definition
|
||||
(launcher.mjs:135, :237). C2-08 adds the `THREAD_DEFINITIONS` constant, but C2-09 is the row that
|
||||
makes the broker and launcher read it and adds `instance` to `thread.create`, and C2-09 is outside
|
||||
the slice (DESIGN 9). The Console's thread creation must pass `instance` too. Fix: put C2-09's
|
||||
broker, launcher and `thread.create` parts in the slice. The Console chat must pick the instance.
|
||||
`room.*` can stay out.
|
||||
|
||||
B2. In the slice, Melanie reads raw JSON and can't see sync failures. Replies are JSON bodies (5.2),
|
||||
and only C2-10 renders them. 7.2 step 2 says "The Console shows them with file and line", but the
|
||||
library pages are C2-11b, outside the slice. A bad Obsidian push silently stops agents seeing new
|
||||
work. Fix: the slice needs a minimal cited-item renderer in the existing chat view and a sync status
|
||||
line (last valid commit, problems, `unpushed`).
|
||||
|
||||
B3. `raise_decision` is a second prose channel to the human. Every agent gets it with free-text
|
||||
`question` and option `text` (tools.mjs:221, comment at :5-6). An uncited raise goes to the human.
|
||||
M3's "no write tool exists" is true, but M5 covers only `message.send`. Fix: either refuse
|
||||
`decision.raise` from creative definitions in the broker and drop the tool from their typed set, or
|
||||
put the raise text through the same guard. Test both paths.
|
||||
|
||||
B4. The Console conversation view shows the model's raw text, including text M5 refused.
|
||||
webui serve.mjs:559-569 proxies `/api/conversation` to the control board, and the reader emits
|
||||
assistant text blocks (conversation/src/events.mjs:58, :82). A refused prose answer stays in the
|
||||
session file and is readable. Fix: leave creative sessions out of the catalogue on a library
|
||||
instance, or show stored messages only. Test that a creative session isn't listed.
|
||||
|
||||
B5. The stated limit understates what M5 lets through. A reply holds 20 items of 400 characters,
|
||||
which is 8,000 characters. An `idea` item's text is free, and any real quote of any line satisfies
|
||||
the cite. That is a full scene per reply, not "a one-sentence suggestion" (5.1). Jason wrote
|
||||
"NEVER", and Q176 says the system enforces it. Fix: add a total-characters cap per reply and a lower
|
||||
cap for `idea` items (for example 200 characters and at most 5 items). State the residual with
|
||||
those numbers. Mos takes the residual to Jason or Melanie for acceptance, because it is their rule.
|
||||
|
||||
B6. A `correction` isn't bound to the source. M5 limits the edit distance between `from` and `to`,
|
||||
but nothing requires `from` to be text in the cited lines. An agent can invent both. Fix: `from`
|
||||
must equal one cite's `quote`, after the same whitespace normalization, inside that cite's lines.
|
||||
Test that an invented `from` is refused.
|
||||
|
||||
B7. `citation-unfetched` has nothing to check against. The web trail stores the URL
|
||||
(broker.mjs:1521) and a summary of status, bytes and redirects (bus/src/web.mjs:46), not the page
|
||||
text. "The quote occurs in the fetched text" needs that text. Also, Claude's built-in WebSearch and
|
||||
WebFetch are on when `policy.webBuiltin` is set (gate.mjs:28-32), and those fetches never reach the
|
||||
broker. Fix: say where fetched text is kept (bounded, per run, or a new table, and if a table then
|
||||
say so). M1 must refuse the built-in web source for creative definitions, so `research` uses only
|
||||
the broker's tools.
|
||||
|
||||
B8. Remote content is a new untrusted input, and the S-rules don't check tree entries. Today only
|
||||
the bus writes record repos. Under 7.2, Obsidian or anyone with push to the Gitea repo writes too.
|
||||
A fast-forward checks out whatever the commit holds. No rule refuses symlinks (120000), gitlinks
|
||||
(160000), executables or nested `.gitattributes` and `.gitmodules`. Separately, S1 refuses the
|
||||
`.gitignore` that C2-12 commits, and Obsidian puts new notes and `.trash/` at the vault root by
|
||||
default. Fix: add a rule that every entry is a 100644 blob or a tree, and refuse those names at any
|
||||
depth. Allow `.gitignore` at the top. Commit Obsidian's new-file location and trash settings in
|
||||
`app.json`. Test each case.
|
||||
|
||||
B9. The fail-closed state in 7.2 strands Melanie's writes. If remote main is ahead and invalid, the
|
||||
bus stays on the old commit (step 2). The next bus write commits on that base. The push can't
|
||||
fast-forward, so the commit is `unpushed`, then `library-diverged`, with no merge until C2-17.
|
||||
S5 makes this likely: Obsidian users create `[[stub]]` links to notes that don't exist yet, so one
|
||||
dangling link in the playground blocks the whole sync. Fix: refuse library writes while remote main
|
||||
is ahead and invalid (`library-remote-invalid`). Apply S5 to canon only, or allow unresolved links
|
||||
in `playground/`. Test both.
|
||||
|
||||
B10. The 7.2 gate can't catch the obvious mutant. Tests use the file protocol through a test-only
|
||||
argument, so the https path, `GIT_ASKPASS` and the protocol override are never run. A mutant that
|
||||
sets `protocol.allow=always` passes them all. `git()` passes only `gitEnv()` plus the identity
|
||||
argument (git.mjs:10-12, seat checkout.mjs:79-89), so sync needs its own env entry for the helper.
|
||||
`GIT_TERMINAL_PROMPT=0` (checkout.mjs:85) doesn't stop askpass, which is fine. Fix: one function
|
||||
builds the production fetch and push argv and env. A test asserts that the argv holds SAFE, then
|
||||
exactly `-c protocol.https.allow=always`, and no other `protocol.*` key. Another asserts that
|
||||
`GIT_ASKPASS` names a helper outside the repo that reads the slot file. Keep the file-protocol
|
||||
switch out of any path that a business file or a bus verb can reach.
|
||||
|
||||
B11. An optional `root` breaks more than the overlap loop. `ensureCheckouts` clones every entry's
|
||||
root (seat checkout.mjs:310, :319). With no root it calls `clone(project, undefined, ...)`, so "a
|
||||
project with no root gives agents no clone" (2.1) is false today. `projectFilePath(entry.root)`
|
||||
(business/src/project.mjs:9-10) throws on undefined, and it is called at business/src/cli.mjs:72,
|
||||
:78, :118, resolve.mjs:44 and instance-settings.mjs:142. Fix: C2-04 lists these consumers, each
|
||||
skips a library project explicitly, and each has a test.
|
||||
|
||||
B12. Scope and crossovers can also come through front-matter fields, and in the slice a hybrid
|
||||
rests on convention. S2 and S3 speak of links only. `extends:`, `sources`, and `exclude` and `pin`
|
||||
paths are references too, and a canon book entity with `extends: playground/...` would bring
|
||||
playground text into canon lookups labelled canon. `library_manuscript` has no `scope`, so it must
|
||||
label or refuse a playground book. In the slice there is no crossover dialog (C2-11b), so a hybrid
|
||||
is a hand-written `universe.json` pushed from Obsidian, and S6 doesn't require `sanctioned`. Fix:
|
||||
put every path field under S2 and S3. Make S6 require `sanctioned`. State that in the slice a hybrid
|
||||
is Melanie's hand edit, and that only her account and the bus account can write to the Gitea repo.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Cite read.mjs:19-26 for the blob reads, not :67-86.
|
||||
N2. `END_PROTECTED` is `[THREAD_DEFINITION]` (vocabulary.mjs:135). Say whether creative
|
||||
definitions become end-protected when `THREAD_DEFINITIONS` replaces it.
|
||||
N3. The runner sends `(the session gave no answer)` and `The turn failed: ...` (runner.mjs:449).
|
||||
The guard will refuse both, so Melanie never learns a turn failed. Add a fixed turn-failed notice,
|
||||
and test that any notice other than the fixed bodies is refused.
|
||||
N4. Promotion moves several files. `commitFile` commits one path (index.mjs:219). C2-02 needs a
|
||||
multi-path commit.
|
||||
N5. Ids allow capitals (paths.mjs:7). Two universes that differ only by case collide on a
|
||||
case-insensitive file system. Enforce lowercase in `checkLibrary`.
|
||||
N6. A role with no `messaging` key may message anyone (role.mjs:50). M1 must require `messaging`
|
||||
for creative definitions.
|
||||
N7. C2-05b's sync-before-read needs C2-03, which its row doesn't list. C2-03 reads the remote URL
|
||||
that C2-04 adds to the business file, so either pass the URL in or order C2-04 first.
|
||||
N8. C2-06 must run the guard through both the runner's RESULT path and `send_message`, with a
|
||||
non-creative sender in a library business. Otherwise a mutant keyed on `CREATIVE_DEFINITIONS`
|
||||
passes, against 5.2's "role choice doesn't matter".
|
||||
N9. Name the refusal when `room.open` meets an existing thread with an instance's name
|
||||
(schema.sql:249).
|
||||
|
||||
## Rows and the date
|
||||
|
||||
The rows are small and most gates name the right refusal. B6, B10 and N8 are the gates that miss
|
||||
their obvious mutant. The listed slice (C2-01 to C2-08, C2-12, C2-13) does not let Melanie start.
|
||||
She can write in Obsidian, but she can't reach an agent (B1) or read its answers (B2). With
|
||||
C2-09's broker part and a minimal renderer added, it can be done by 10-27 only if T1 row 1 (row
|
||||
190) lands by about 10-24, as section 9 says. Reaching the Console from her machine also needs SSO
|
||||
(Q175).
|
||||
@@ -0,0 +1,74 @@
|
||||
# Webby review: Case 2 design r10 (#416), two system units
|
||||
|
||||
Webby, 2026-10-20T21:01Z (from `date -u`). Author: Gosalyn (gosalyn-0045). Commit 4281b0064 on
|
||||
gosalyn/case2-design, DESIGN blob 4a1860e698e0. Diff `git diff 51006710a 4281b0064`, one commit. I
|
||||
read both unit templates, scripts/webui-service.sh, webui `cli.mjs serve`, scripts/mosaic and decisions 62,
|
||||
94 and 102 on origin/refactor. Static review.
|
||||
|
||||
## Verdict: CHANGES (B1, B2, B3)
|
||||
|
||||
The fix is the right one. Two units from the existing templates match how the stack runs today, and C2-10b
|
||||
needs the Console. One copy for both units keeps the human proof working, since the bus accepts only its
|
||||
own human-cli.mjs path (webui-service.sh:266-273). Making `%h` literal is correct for a system unit. The new
|
||||
refusal cases are good.
|
||||
|
||||
B1. The Console's boot start is Jason's call, not mine or Mos's, and the design should default to the strict
|
||||
reading until he rules. Decision 94 (lead-decisions:2016-2023) says the process that starts the Console
|
||||
holds human authority, and that a start through systemd gives a clean ancestry the proof accepts, "the
|
||||
detachment gap decision 62 names". Decision 102 lets one named seat, Mos, start it. It calls that a
|
||||
tradeoff Jason accepted, because "the human proof now rests on Jason's grant to one named seat" (:2253). A
|
||||
boot-enabled system unit hands that start to PID 1 on every reboot, with no named starter. That extends a
|
||||
human-authority boundary, so under AGENTS.md it needs an owner decision. Decision 94 also accepts the
|
||||
local-forgery limit only "for this PoC on dragon-lin under Jason's user ... this decision doesn't carry the
|
||||
limit there" (:2058-2062). On .54 any process that reaches the Console port can send as Melanie, and
|
||||
`melanie`'s Hermes processes and sudo are on that host until #423 step 5. Nobody has accepted that limit
|
||||
for .54.
|
||||
|
||||
Fix: render the Console unit with no `[Install]` section, so a human or Mos starts it at step 9 and after
|
||||
each boot. That fails closed and is cheap to reverse, because adding `[Install]` later is one re-render.
|
||||
Put two questions to Jason through Mos: may a boot start stand in for the human's start on .54, and does he
|
||||
accept the local-forgery limit there? The bus unit can stay boot-enabled, since it holds no human
|
||||
capability.
|
||||
|
||||
B2. `@PATH@`, `@NODE@` and `@FLAGS@` have no render rule. Both templates set
|
||||
`Environment=PATH=@PATH@` (mosaic-bus.service.in:15, mosaic-webui.service.in:25). The bus `ExecStart` is
|
||||
scripts/mosaic, a bash script that runs `exec node` from PATH (scripts/mosaic:22-40). So the bus's node is
|
||||
whatever PATH the render writes, not `--node`. Row 195 found v22 on .53, and the precheck checks
|
||||
`node --version` on `<user>`'s shell PATH, not the unit's. Fix: say `@PATH@` is `dirname(--node)` followed
|
||||
by `/usr/local/bin:/usr/bin:/bin`, as webui-service.sh:246-248 does, and `@NODE@` is `--node`. Say
|
||||
`@FLAGS@` is the checked serve flags, and refuse any `@[A-Z_]+@` left in either render, not only `%h`. Add
|
||||
a test that the bus unit's PATH starts with the `--node` directory.
|
||||
|
||||
B3. The hardening lines are never listed. In r9 they lived in the new template that C2-13 owned. r10 drops
|
||||
that template, and the render "adds ... the hardening lines", but neither the design nor the brief says
|
||||
which ones. The test "a template with a hardening line removed" has nothing to remove: the templates carry
|
||||
only `NoNewPrivileges=yes`. Some common lines break sessions. `RestrictNamespaces=`, `PrivateUsers=` or a
|
||||
`SystemCallFilter=` without the namespace calls stops bwrap, and then no role starts. The bwrap precheck
|
||||
runs in `<user>`'s shell, outside the unit's sandbox, so it can't catch that. Fix: list the exact lines in
|
||||
the brief (for example `ProtectHome=yes`, `ProtectSystem=strict` with `ReadWritePaths=<home>`,
|
||||
`PrivateTmp=yes`, `NoNewPrivileges=yes`), say none of them restricts user namespaces, and add a step 9
|
||||
check that one session starts under the bus unit.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Both templates already end with `[Install]` `WantedBy=default.target` (bus :41-42, webui :39-40). The
|
||||
render should replace that line with `multi-user.target`, not add a second one. Under B1 it removes the
|
||||
Console's section.
|
||||
|
||||
N2. The render adds no `After=` or `Wants=`, so at boot the two units start in parallel. That is harmless
|
||||
today, because serve calls the bus per request (webui cli.mjs, `busCaller`) and starts without it. But the
|
||||
stated order applies only to a manual step 9. Either add `After=mosaic-bus@<business>.service` to the
|
||||
Console unit, or say that boot order doesn't matter.
|
||||
|
||||
N3. "The serve flags pass scripts/webui-service.sh's own flag checks" can't be done by calling that
|
||||
script. `serve_flags` (:146-207) is internal. Its proof-file check compares the owner to `id -u` (:201),
|
||||
which is root under case2-unit.sh. `render` finds node with `command -v` (:237), not `--node`. And
|
||||
webui-service.sh isn't in the files owned. Say whether case2-unit.sh copies the checks, with tests, or
|
||||
whether the row may move `serve_flags` into a sourced file. Also state which serve flags Case 2 uses.
|
||||
|
||||
N4. With a copy, webui-service.sh passes `--checkout <canonical>` (:251). Her host has no canonical
|
||||
checkout, so the queue read runs from the copy, and the queue store refuses that. Say that the Console unit
|
||||
gets no `--checkout`, and that the queue panel isn't part of Case 2.
|
||||
|
||||
N5. A re-pin must move both units to the same copy, or the human proof fails (webui-service.sh:266-273).
|
||||
Have case2-unit.sh refuse a render whose two units name different copy ids, and test it.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Webby review: Case 2 design r11 (#416), Console without [Install], render rules, 9.3
|
||||
|
||||
Webby, 2026-10-20T21:14Z (from `date -u`). Author: Gosalyn (gosalyn-0048, gosalyn-0051). Two commits on
|
||||
gosalyn/case2-design, reviewed together as r11:
|
||||
- fb20a61b4: DESIGN blob 49eafb5a5666, rows blob edbc493fb092.
|
||||
- eac92d956: rows only, rows blob 3a86d5f048dc. This is the one to re-pin.
|
||||
|
||||
Diffs: `git diff 4281b0064 fb20a61b4` and `git diff fb20a61b4 eac92d956`. I read B1 against mos-1097 and
|
||||
the port squat against mos-1100. I read the cited code at fb20a61b4. Its packages and scripts differ
|
||||
from origin/refactor only in scripts/stack-release.sh, which none of these citations use. This is a
|
||||
static review. I ran nothing on any host.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed, per mos-1097. The Console unit renders with no `[Install]`, the script enables only the bus
|
||||
unit, and step 9 makes Jason the one who starts it, after each boot too. The tradeoff after an unplanned
|
||||
reboot is written down. Q185 (jason-master-restart DECISIONS.md:1573) does put him in the homelab for
|
||||
1012 reboots. Mos asked for two facts, and 9.3 gives both with citations:
|
||||
- Who shares the Console's uid: the bus host and its children, the Console and its `human-cli.mjs`
|
||||
children (webui bus.mjs:1-3), every session inside bwrap, and the admin steps.
|
||||
- What a bwrap session can reach. The port, yes. sandbox.mjs:67 has no `--unshare-net`, and :29-31 says
|
||||
the network isn't confined. The proof file, no. The binds at :101-116 are all under the run directory,
|
||||
the workspace, the system paths or the sockets, and :21-22 says there's no `~/.config`. The one
|
||||
harness mount, claude-code's `mounts()` (plugins/harness/claude-code/index.mjs:73-77), is only the
|
||||
pinned binary.
|
||||
|
||||
The 401/403 reading of serve.mjs holds. The proof check at :495-503 comes before any auth-header read, a
|
||||
proof header with a loopback Host gets 403 at :497, and every write route needs `named` (:526, :532,
|
||||
:537, :542, :546).
|
||||
|
||||
B2 is fixed. I listed every token in both templates. The bus has `@REPO@`, `@PATH@`, `@CLAUDE_ENV@`,
|
||||
`@PM_FLAGS@`, `%h` and `%i`. The Console has `@REPO@`, `@PATH@`, `@NODE@` and `@FLAGS@`. Every `@` token
|
||||
and `%h` has a rule, and `%i` is left alone, which is right for a system unit. The leftover refusal is
|
||||
`@[A-Z_]+@` or `%h`, so it doesn't catch `%i`. The `@PATH@` rule fixes the case I missed in r10. With
|
||||
`--node /usr/bin/node`, webui-service.sh:246-248 would let a node in /usr/local/bin win, and there's a
|
||||
test for that case.
|
||||
|
||||
B3 is fixed. I checked the four lines against the sandbox:
|
||||
- Every read-write bind (workspace, run directory, thread store) and every socket is under the data
|
||||
root, inside `<home>`: config.mjs:19, git-socket.mjs:1, launcher.mjs:5. So `ReadWritePaths=<home>`
|
||||
covers them.
|
||||
- Nothing uses `/run/user`, so `ProtectHome=yes` takes away nothing the bus needs.
|
||||
- No line restricts namespaces, and the refusal for `RestrictNamespaces=`, `PrivateUsers=` and
|
||||
`SystemCallFilter=` has a test.
|
||||
- The step 9 session start is the real check, and it waits on #383 as stated.
|
||||
|
||||
N1-N5 from r10 are all in the C2-13a brief, with tests.
|
||||
|
||||
The port squat and its fix are right. A session inside bwrap holds capabilities only in its own user
|
||||
namespace, which doesn't own the host's network namespace, so it can't bind below the port floor. cli.mjs
|
||||
:27 takes ports 0..65535, so serve needs no change. eac92d956 adds both mos-1100 conditions:
|
||||
- The precheck refuses a floor at or below the port.
|
||||
- The bind test builds a real session argv with `binds()` (sandbox.mjs:101) and `sandboxArgv()` (:162).
|
||||
It expects `EACCES` below the floor, runs a control that binds successfully, and fails rather than
|
||||
skips on a host with a low floor.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. A correction to my own r10 B1. I cited the dragon-lin forgery acceptance as decision 94. It is
|
||||
decision 95 (lead-decisions.md:2048, the text at :2058-2062). r11 9.3 has it right. Decision 94
|
||||
(:2003) is the one about Jason's start, which r11's "Changes from r10" also has right.
|
||||
|
||||
N2. The bind test's control binds "a free port of 1024 or above". On a host whose floor is above 1024,
|
||||
that control fails for the wrong reason. Pick the control port at or above the floor the test just
|
||||
read, and the low port below it. It's one line in the brief.
|
||||
|
||||
N3. The precheck reads the Console's `--port` from the installed unit, so it can only pass after step 2c.
|
||||
Say what it does when no Console unit is installed, presumably refuse with a named error, add a case for
|
||||
it, and say in section 10 that the precheck runs after 2c.
|
||||
|
||||
N4. `AmbientCapabilities=` carries across exec, so each `human-cli.mjs` child the Console starts also
|
||||
holds `CAP_NET_BIND_SERVICE`. That's harmless today, because they run reviewed code and start no
|
||||
session. Add one sentence to 9.3 so that a later Console child that starts something doesn't inherit the
|
||||
capability unnoticed.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Webby review: Case 2 design r2 (#416, creative writing hub)
|
||||
|
||||
Webby, 2026-10-20T19:28Z (from `date -u`). Independent reviewer. Author: Gosalyn.
|
||||
Subject: `agents/gosalyn/work/case2/DESIGN.md` at c1913fb07 on gosalyn/case2-design, blob
|
||||
05a79cbca624, sha256 bda7a717...4055 (verified). Code read at 9ae8c73ac, where the citations point.
|
||||
Static review. I ran nothing. I read the whole file. The r1 items I don't name below read as fixed.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
Four blockers. Three are in the persona path, which is new in r2. The data model rewrite onto her
|
||||
real repo is the right call, and the 1.3 claim mostly holds (see "The three questions").
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. A top-level playground has no universe. 1.1 (:169-170) suggests "one at the top", but 1.2
|
||||
(:191) gives a file the universe whose `books` list holds its book folder. A file under a top-level
|
||||
prefix has no book folder, so `library_find` (universe required) can't return it, and the cite
|
||||
check's one-universe rule (:464) has nothing to compare. Fix: put `playground` inside each universe
|
||||
entry, or refuse a prefix that isn't inside a book folder. Also give playground the `.md` condition
|
||||
canon has (:187). As written, a `.canvas` under a playground prefix is playground and citable.
|
||||
|
||||
B2. Where `persona.text` lives, and who can read it. Instance entries take inline `vars` only
|
||||
(business.mjs:89 closes the keys). Nothing lets an entry point at a file, so "a 0600 file that the
|
||||
business file entry takes" (:665, :722-723) has no mechanism. Inline, the text is read by more than
|
||||
the design says:
|
||||
- The tester's `instances` read is allowed (broker.mjs:103) and returns every instance's vars
|
||||
whole (instance-settings.mjs:58-63). That breaks "No agent sees the persona text" (:724) on any
|
||||
host with `bus.tester` on (vars.mjs, `bus.tester`).
|
||||
- Every organization write logs old and new values of each changed path in
|
||||
`businesses/.history/<id>/changes.jsonl` (organization.mjs:70-80, :201-204).
|
||||
Fix: say the text sits inline in the business file, or add a file-reference var with its own
|
||||
checks. Then either refuse persona keys in the tester's reads, or make C2-13's precheck refuse
|
||||
`bus.tester` on `<host>`. State the mode of the business file and `.history`. Test it in C2-08.
|
||||
|
||||
B3. Name the places `THREAD_DEFINITION` must stay `assistant`. Section 4 (:404-406) replaces it
|
||||
"wherever it means may hold a thread" and keeps only tools.mjs:186. launch-settings.mjs:140, :186
|
||||
and :191 also use it. They let `organization.set`'s `assistants` key set an assistant's whole vars,
|
||||
which the chief of staff holds (`ORGANIZATION_KEYS`, broker.mjs:145, :1266-1271, Q169). Read as "may hold a thread", that
|
||||
lets the chief of staff rewrite Bean's `persona.text` and model. Fix: list those three lines as
|
||||
staying `assistant`, and add a C2-08 test that `assistants` with `bean` refuses
|
||||
`organization-not-assistant`.
|
||||
The same change has a second effect the design doesn't state. With `END_PROTECTED` set to
|
||||
`THREAD_DEFINITIONS` (:407-408), `instances.set` refuses to add or change a creative instance
|
||||
(instance-settings.mjs:23, :109), and `definitions()` marks all six not accepted (:52). So her model
|
||||
choice for Bean, a persona edit and adding a specialist later are all hand edits of the business
|
||||
file. 4 (:394) and 10 step 7 read as if she can pick specialists later. Say how that happens.
|
||||
|
||||
B4. Name the persona block's exact place. `prompt()` is the contract (bundle.mjs:177), then
|
||||
`## This session` (:179) with the session lines. The last of those says "end each turn with the
|
||||
answer itself" (:188), then the end_run and review-loop lines follow. "After the contract" can
|
||||
mean before `## This session` or at the very end, and the C2-08 snapshot depends on which. I'd put
|
||||
it between the contract and `## This session`, so the broker's facts come after the persona. Fence
|
||||
it the way skills are (`SKILL_END`, :107, :170), and refuse persona text that holds the end marker
|
||||
or a line starting `## `. Hermes text is 8,000 characters that nobody here has read, so it can't be
|
||||
allowed to forge a section. The snapshot then pins the order: contract, persona, session.
|
||||
|
||||
## The three questions
|
||||
|
||||
1.3, reporting is enough. For the agent-side guarantee, yes. Scope comes from the path alone, and
|
||||
the cite check (:463) refuses a playground cite on `fact`, `finding` and `correction` whatever her
|
||||
files contain. None of R1-R7 changes a scope. Holding back her head would add nothing to that
|
||||
check. Two limits belong in the text (N1, N2).
|
||||
|
||||
M9. It holds today. Every organization setter builds its document from the file and changes only
|
||||
its own keys (broker.mjs:1261-1269, instance-settings.mjs:93-121, launch-settings.mjs:182-191), so
|
||||
no verb can change `projects` now. M9 guards against a future setter. Place it where that setter
|
||||
can't skip it (N3).
|
||||
|
||||
Persona order. See B4.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. `library_get` takes only `path` (:346), so its `links_out` and `links_in` are the one place a
|
||||
lookup can name a file in another universe or an unmapped file. Say both lists drop those targets
|
||||
(R6, R2), and add a C2-05a test.
|
||||
N2. Scope is the current mapping applied to the path. A note she moves into a book folder is canon
|
||||
on her next push, and a mapping edit re-scopes old commits for the cite check. Both are right, and
|
||||
both are hers. Put the first in the setup sheet (C2-12).
|
||||
N3. Put M9 in `organizationWriter.write` (organization.mjs:177), comparing `canonicalJson` of old
|
||||
and new `projects`, hybrid entries included. Since no verb can build the change, the C2-04 test
|
||||
calls `writer.write` directly with a changed playground prefix, and one with a changed hybrid.
|
||||
N4. Obsidian resolves a shortest-path link across the whole vault, which is `~/src` with her other
|
||||
repos in it. The reader sees one repo. A base name unique in the books repo but not in the vault
|
||||
can resolve to a different file than Obsidian shows her. Count those in the survey's link-form
|
||||
counts if the survey can see it, or note it in the setup sheet.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Webby review: Case 2 design r3 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T19:43Z (from `date -u`). Author: Gosalyn. Subject: `agents/gosalyn/work/case2/DESIGN.md` at
|
||||
2958acb81 on gosalyn/case2-design, blob 60bc7d682e0d, sha256 f368c27d...c325 (verified). Read as the diff from
|
||||
c1913fb07 (r2, my REVIEW at 162ce46db), with the code at 9ae8c73ac. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
My r2 B1, B2 and B4 are fixed, and so are N1 to N4. B3 is fixed for the three paths it names, but it misses a
|
||||
fourth (B1 below).
|
||||
|
||||
- r2 B1. Playground prefixes now sit inside a universe and must start with one of its book folders, or the
|
||||
file refuses with `library-playground-outside-book`. A `.canvas` under a prefix is unmapped. 1.2's universe
|
||||
rule then holds for playground files.
|
||||
- r2 B2. The text moves to `<configDir>/personas/<business>/<instance>.md`, and the vars hold only the name and
|
||||
the digest. I checked the modes against the code. `durable` (organization.mjs:59-61) and `log` (:138-141)
|
||||
open with 0600 and O_NOFOLLOW. `log` refuses a group or other bit. `directory` (:39, :45) makes 0700 and
|
||||
refuses a loose one. The `instances` read (instance-settings.mjs:61-63) copies vars whole, so it now carries
|
||||
only the digest. `previous.json` (organization.mjs:197-198) holds the old business file, which has no text
|
||||
either.
|
||||
- r2 B3. launch-settings.mjs:140, :186 and :191 stay `THREAD_DEFINITION`. `organization.set`'s `instances` key
|
||||
refuses a creative instance for every actor. `instances.set` gets the human-only creative branch.
|
||||
- r2 B4. The fence follows `SKILL_END` (bundle.mjs:107, :170). The block sits between `contract.trimEnd()`
|
||||
(:177) and "## This session" (:179). The snapshot pins that order.
|
||||
- The correction is right. broker.mjs:147-149 says the chief of staff or a human-resolved decision ends an
|
||||
`END_PROTECTED` instance.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. `routes.set` lets the tester change a creative instance's harness, model and thinking. It is in
|
||||
`TESTER_ORGANIZATION_VERBS` (broker.mjs:103), and the gate at :1259 lets a tester through. `setRoutes`
|
||||
(launch-settings.mjs:240-272) has no protected-definition check. It replaces the route vars of any named
|
||||
instance, then writes at :277. So the design's 4 is wrong in two places:
|
||||
- DESIGN.md:474-493 lists every path to a creative instance and leaves this one out.
|
||||
- DESIGN.md:493 says that with `bus.tester` on, the tester "could change no creative instance".
|
||||
|
||||
M10 (DESIGN.md:563) has no test for it. `bus.tester` is off on her business and C2-13 refuses it, so this is
|
||||
not reachable there today. But the design claims the rule holds even if testing were on, and on any library
|
||||
business that turns testing on, it doesn't.
|
||||
|
||||
Fix: in C2-08, have `setRoutes` refuse a `CREATIVE_DEFINITIONS` instance from actor `tester` with
|
||||
`organization-creative-human-only`, as the `instances.set` branch does. The human keeps `routes.set`, which is
|
||||
the Console's routing table. Add the path to the list at 474, fix 493, and add the case to M10's tests: the
|
||||
tester's `routes.set` on `bean` refuses, the file is unchanged, and the human's still works.
|
||||
|
||||
## Your two questions
|
||||
|
||||
Keying the creative branch on actor `"tester"` is enough. broker.mjs:1272 sets the actor to `"tester"` only
|
||||
for a tester session. The business load check refuses a human id of `tester` (broker.mjs:245) and an instance
|
||||
of that name (:248). instance-settings.mjs:86 refuses one too. So the string can't name the human, and the
|
||||
writer is the only place that knows the definition, because it reads the document. A broker-side check would
|
||||
have to read the file a second time to learn the definition. The one thing to add: make C2-08's tester case go
|
||||
through the broker with a bound tester session, not just `set(..., "tester")`, so a change to the actor line at
|
||||
:1272 fails the test. The same goes for B1's case.
|
||||
|
||||
The `# `/`## ` refusal is safe, because it fails closed, but it may refuse her real personas. The fence alone
|
||||
stops a section escape, because nothing after `</persona-file>` belongs to the persona. The heading rule
|
||||
guards against a model reading a `## ` line inside the fence as a new section, which is reasonable. I don't
|
||||
know what her Hermes persona files look like. If they use Markdown headings, C2-20 stops at the fence check
|
||||
(DESIGN.md row C2-20) and the design names no next step. See N2.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The chief of staff can change which model a creative instance runs without touching the instance.
|
||||
`organization.set` takes `models` (broker.mjs:145, ORGANIZATION_KEYS), and launch-settings.mjs:169 replaces
|
||||
the whole `launch.models` table. If `bean`'s `model` var is a key the chief of staff rewrites, Bean runs the new
|
||||
model. That matches what 4 promises, since the instance and the persona stay unchanged, but mos-1033's "only
|
||||
the human" reads wider. Either say in 4 that the models table is shared and the chief of staff may retune it,
|
||||
or refuse a `models` change to a key a creative instance names. Mos's call, so I'd put it to him.
|
||||
N2. For C2-20, count the lines starting `# ` or `## ` in each persona and print the count with the byte count,
|
||||
before Jason's go. Then state the remedy in 9.1. That could be Melanie or Jason editing the text, or a recorded
|
||||
rewrite to `### `. Any rewrite needs her say, because the text is hers.
|
||||
N3. "Only Bean's or Chet's own session sees it" (DESIGN.md:866) needs a qualifier. `buildBundle` writes
|
||||
prompt.md, persona included, once per run into the run directory at 0600 (bundle.mjs:241-244, :265). Run
|
||||
directories are write-once and stay until `prune`. The claude adapter also passes prompt.md as one argument
|
||||
(bundle.mjs:104-105), and that argument is visible in `/proc/<pid>/cmdline` to other local users. No verb reads
|
||||
either copy, and on her VM the only other user is `melanie`, whose personas they are. So this doesn't change the
|
||||
design's safety. The text should name both copies, and the C2-13 precheck should check the run root's mode
|
||||
with the rest.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Webby review: Case 2 design r4 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T19:58Z (from `date -u`). Author: Gosalyn. Subject: `agents/gosalyn/work/case2/DESIGN.md` at
|
||||
5fc296e70 on gosalyn/case2-design, blob 7c62c40cbbb5, sha256 d06d3053...ab0a (verified). Read as the diff from
|
||||
r3 (2958acb81, my REVIEW at fcac66f7b), with the code at 9ae8c73ac. Static review. I ran nothing, apart from
|
||||
reading pi 0.85.1's resource-loader.js in the canonical checkout's node_modules.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
My r3 B1 is fixed, and so are N1, N2 and N3.
|
||||
|
||||
- r3 B1. `routes.set` is now on the list (DESIGN.md:549-552), and :493 is fixed (now :565). The one rule keys
|
||||
on the business file's `human`. I checked the actor lines: broker.mjs:1272 sets `tester` or the human id,
|
||||
and :1278 sets `<definition>.<instance>` for a role. M10 has the tester's `routes.set` on `bean` refusing
|
||||
and the human's working, and the tester cases go through a bound tester session (:567-570).
|
||||
- N3. The adapter lines are right (claude adapter.sh:79, :82; pi :106, :121). I read pi 0.85.1's
|
||||
`resolvePromptInput` (dist/core/resource-loader.js:17-31). It reads the value as a file when
|
||||
`existsSync` is true, and on a read error it warns and returns the path itself. See N1 below for the case
|
||||
where the file doesn't exist.
|
||||
- N2. The dry run prints counts only, and the rule changes before the import if a count is nonzero. That's
|
||||
what I asked for.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. The human does reach the `models` write, through `limits.set`. DESIGN.md:29-30 and :545-548 say that
|
||||
`organization.set` takes only an agent session, "so no verb writes `launch.models` for the human today", and
|
||||
that Jason edits it by hand. The first half is true. The conclusion isn't:
|
||||
|
||||
- `limits.set` is a human verb (broker.mjs:1248). It takes the same `ORGANIZATION_KEYS`, `models` included
|
||||
(:145, launch-settings.mjs:16).
|
||||
- `organization.set` is `limits.set` for a role (broker.mjs:1249-1251). Both reach the same `set()`
|
||||
(:1284, launch-settings.mjs:150-169), and the `models` replace is at :169.
|
||||
- So the human's `limits.set { models }` replaces `launch.models` today, with actor = her id. The tester
|
||||
can't, because `limits.set` isn't in `TESTER_ORGANIZATION_VERBS` (broker.mjs:103).
|
||||
|
||||
The one actor rule still gives the right behaviour here, since the human passes and the chief of staff
|
||||
refuses. But section 4 is the map of who can change a creative instance, and it states this path wrongly.
|
||||
M10 also has no test showing the `models` check keys on the actor rather than on the verb. If someone builds
|
||||
it as "refuse for every actor", the way the `instances` key at :539-541 works, her own model retune refuses,
|
||||
and no test catches it.
|
||||
|
||||
Fix:
|
||||
- In both places, say that the human writes `launch.models` with `limits.set` and the chief of staff with
|
||||
`organization.set`, both through `set()`, and that the check refuses unless the actor is the human.
|
||||
- Add to M10 and C2-08: the human's `limits.set` change to Bean's `launch.models` key works, next to the chief
|
||||
of staff's change that refuses.
|
||||
- :541 already says the human's `instances: {bean: ...}` refuses. That case also goes through `limits.set`.
|
||||
Name the verb there too, so the test is written against it.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Pi also uses the path as the prompt when the file doesn't exist. Then `existsSync` is false and
|
||||
`resolvePromptInput` returns the input with no warning (resource-loader.js:21, :30). So the adapter's check
|
||||
has to cover existence as well as readability. `[ -r "$f" ]` does both. Add a missing file to C2-08's pi
|
||||
adapter test, next to the unreadable one.
|
||||
|
||||
N2. The claude adapter test can only show what the adapter puts in argv. Whether claude 2.1.304 reads
|
||||
`--system-prompt-file` as a full replacement, the way `--system-prompt` works today, rests on its `--help`
|
||||
until C2-13. Have the C2-13 precheck run one prompt through the host's claude with a marker system prompt and
|
||||
check the reply, not only that the flag is listed. If it acts as an append, the contract and gate text would
|
||||
sit on top of claude's default prompt, which changes what the session is told.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Webby review: Case 2 design r5 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T19:54Z (from `date -u`). Author: Gosalyn. Subject: `agents/gosalyn/work/case2/DESIGN.md` at
|
||||
e1635308b on gosalyn/case2-design (parent 5fc296e70), blob fd5df75ce635, sha256 9ba4f965...2d22d (verified).
|
||||
Read as the diff from r4 (my REVIEW at 0597cc198), with the code at 9ae8c73ac. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- r4 B1 is fixed. Section 4 (DESIGN.md:555-557) says `limits.set` and `organization.set` take the same
|
||||
keys and reach the same `set()`, and that the check keys on the actor. I re-read
|
||||
packages/bus/src/broker.mjs:1248-1251 and :1284 at 9ae8c73ac, and they match. The `instances` case
|
||||
(:565-566) names both verbs. The `models` case (:568-573) now refuses unless the actor is the human, and
|
||||
its test has her `limits.set` change to Bean's key working next to the chief's refusal. M10 (:665) and
|
||||
C2-08 (:899) carry the same case. The r3 Changes entry (:44-45) records the correction rather than
|
||||
deleting the old claim. No stale "agent-only" or "by hand" claim is left (grep).
|
||||
- r4 N1 is fixed. :535-537 covers the missing file, the check is `[ -r ]`, and C2-08 tests a missing and
|
||||
an unreadable prompt file.
|
||||
- r4 N2 is fixed. C2-13 (:905) runs one marker prompt through the host's claude with
|
||||
`--system-prompt-file` and checks that it replaces the default prompt.
|
||||
|
||||
No new findings. Section 10 step 1 stays on hold per mos-1050. That isn't mine to clear.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Webby review: Case 2 design r6, section 10 step 1 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T19:59Z (from `date -u`). Author: Gosalyn. Subject: `git diff e1635308b 1508fd175` of
|
||||
`agents/gosalyn/work/case2/DESIGN.md` (r5 blob fd5df75ce635, approved in webby-0021, to r6 blob 80893dd4cff1).
|
||||
Static review. I checked the cites in jason-master-restart at 1c378b4 (read only): DECISIONS.md Q183 and
|
||||
projects/mosaic-mvp/MELANIE-HOST-RECON.md:27-39. Both say what r6 says they say.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
The Q183 reading is right. #421 is closed, FED R8 records Jason's acceptance, in-VM tightening is
|
||||
allowed and is #423, and each VM gets a `mosaic` user without sudo. Section 9's isolation paragraph
|
||||
(DESIGN.md:961-965) states the residual as it is now, with the recon cite.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Step 1 has lost the owner of the root steps on .54. Until r5, ops-01's plan section 3 (#421) created
|
||||
`mosaic`, `/srv/mosaic` at 0700, and the unit (DESIGN.md:947-950 still cites it). That plan stopped. r6
|
||||
step 1 (:1017) says "the install creates the `mosaic` user". But step 2 (:1023) runs the install "as
|
||||
`<user>`", and C2-13 runs as `<user>`. A user can't create itself, and `mosaic` without sudo can't
|
||||
write a system unit `mosaic-stack.service` (:948). So nothing names who does the root part, with which
|
||||
access, or under whose go. The only root paths into .54 today are pve root and `melanie`'s sudo
|
||||
(MELANIE-HOST-RECON.md:31, :35-36), and using either one is an act on Melanie's machine.
|
||||
|
||||
Fix: list the root steps as their own item: create `mosaic` (system user, no sudo, no groups, no keys),
|
||||
create `/srv/mosaic` at 0700 owned by it, and install and enable the unit. Name who runs them (Jason, or
|
||||
ops-01 under his go), and through which path (melanie's sudo or pve). Say that this is step 2's go and not
|
||||
step 1's. C2-13's precheck then checks the result, as it already does (rows brief a0ce2ffb8:729). Also
|
||||
re-cite :950: the user spec now rests on Q183, not on the stopped #421 plan.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. :1018 says the precheck "refuses if it has sudo or any group". A system user still has its own
|
||||
primary group. Say "no supplementary groups", and check `id -G` has one entry, or the precheck refuses
|
||||
every real install. The rows brief (:729) uses the same words, so fix both.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Webby review: Case 2 design r7, sections 9 and 10 and C2-13 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T20:02Z (from `date -u`). Author: Gosalyn. Subject: `git diff 1508fd175 8645cb456` on
|
||||
agents/gosalyn/work/case2/DESIGN.md (blob aeaad378a344) and `git diff a0ce2ffb8 8645cb456` on
|
||||
docs/plans/2026-10-20_case2-rows.md (the rows file arrived in a0ce2ffb8, between r6 and r7). Static
|
||||
review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
r6 B1 is fixed, and so is N1.
|
||||
- Section 10 step 2 (DESIGN.md:1054-1064) now splits 2a (root: useradd, /srv/mosaic 0700), 2b (the install
|
||||
as `<user>`) and 2c (root: install and enable the unit). It names ops-01 as owner, the localadmin
|
||||
path from #423 step 1, and Jason's go. Step 1 (:1045-1053) is right as written. Section 9 (:973-975)
|
||||
rests the layout on Q183, and the rows brief (:724-725) says the script doesn't create `<user>` or
|
||||
install the unit.
|
||||
- N1. "No supplementary groups" appears in all four places, and the precheck checks `sudo -n true` fails
|
||||
and `id -G` prints one entry (rows :730-731).
|
||||
- Mos's item 3. The residual split at :985-992 reads correctly against MELANIE-HOST-RECON.md:31, :35-36.
|
||||
|
||||
My one blocker is in step 2c and C2-13, not in step 1. I have no objection to the mos-1050 hold on step 1
|
||||
lifting now. Mos decides that.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Root installs a unit file that the unprivileged user wrote. C2-13's script runs as `mosaic` and
|
||||
writes the `mosaic-stack.service` file (rows :724-725, DESIGN.md :940). 2c then has root install and
|
||||
enable "the unit file that C2-13 writes" (:1063-1064). By 2c, 2b has already run `bootstrap.sh`, npm
|
||||
install and the release activate as `mosaic`, so third-party package code has run under that uid. Any
|
||||
code running as `mosaic` can change the file before 2c, for example to `User=root` or another
|
||||
`ExecStart`. Root then installs a root service. The same holds on every later re-install. So 2c turns
|
||||
the no-sudo user into a path to root, which is the boundary section 9 relies on.
|
||||
|
||||
Fix: 2c must not trust the content. Either root renders the unit from the reviewed template in the
|
||||
release, with `<user>` and the paths as inputs, or root copies the file to
|
||||
/etc/systemd/system/ as root:root 0644 and refuses unless `User=mosaic`, `Group=mosaic`, the
|
||||
`ExecStart` path, and the hardening lines (`ProtectHome` and the rest) match fixed expected values.
|
||||
Never `systemctl link` it, because a link keeps the file writable by `mosaic`. Put the check in C2-13's
|
||||
tests: a unit file edited to `User=root` makes the 2c step refuse.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. 2b runs the install "as `<user>`, by him or his delegate", but `mosaic` has no keys and no login.
|
||||
The only way in is `sudo -u mosaic` from `localadmin`. Say so, so that nobody adds a key to `mosaic`
|
||||
to make 2b work.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Webby review: Case 2 design r8, step 2c and C2-13 (Vikunja #416)
|
||||
|
||||
Webby, 2026-10-20T20:04Z (from `date -u`). Author: Gosalyn. Subject: `git diff 8645cb456 eab6e1a3a` on
|
||||
agents/gosalyn/work/case2/DESIGN.md (blob 5dd7a9c9b4c0) and docs/plans/2026-10-20_case2-rows.md. Static
|
||||
review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
r7 B1 is fixed, and so is N1.
|
||||
- `mosaic` writes no unit. In 2c (DESIGN.md:1084-1091, rows :728-736), root renders the unit from a
|
||||
root-owned checkout of the commit Jason's go names, outside /srv/mosaic. It checks `User=`, `Group=`,
|
||||
`ExecStart=` and the hardening lines, writes /etc/systemd/system as root:root 0644, never uses
|
||||
`systemctl link`, refuses a symlink, refuses a drifted installed unit, and reads nothing under
|
||||
`<user>`'s home. The tests (rows :753-759) cover an edit to `User=root`, an edited `ExecStart=`, a
|
||||
removed hardening line, a symlink, and the 0644 write.
|
||||
- Gosalyn's change from my fix is right, and my suggestion was wrong. The release is under /srv/mosaic and
|
||||
`mosaic` can write it, so a template there is the same path to root. The root-owned checkout closes it.
|
||||
- N1. 2b runs as `sudo -u mosaic -H` from localadmin, and `mosaic` gets no key (:1080-1081, rows
|
||||
:724-726). `sudo -u` with a command doesn't need a login shell.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The check covers named lines, not the whole file. A template line such as `ExecStartPre=+...`,
|
||||
`ExecStartPost=!...` or `PermissionsStartOnly=` runs as root even with `User=mosaic`. If it pointed
|
||||
into /srv/mosaic, that is the same path to root through a line the check doesn't read. The template is
|
||||
reviewed, so this is defence in depth. Either compare the whole render to one fixed expected file, or
|
||||
refuse any `Exec*=` value starting with `+` or `!` and any `PermissionsStartOnly`. Add one test case
|
||||
for it. The existing packages/cli/systemd/mosaic-bus.service.in has a single `ExecStart=` (:30), so
|
||||
the new template can follow it.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Webby review: Case 2 design r9 (#416)
|
||||
|
||||
Webby, 2026-10-20T20:48Z (from `date -u`). Author: Gosalyn (gosalyn-0035). Commit 29e85196b on
|
||||
gosalyn/case2-design, DESIGN blob 60a2d6cb751e. Round 8: case2-r8/REVIEW.md, approve on eab6e1a3a.
|
||||
|
||||
Scope: `git diff eab6e1a3a 29e85196b` on DESIGN.md and docs/plans/2026-10-20_case2-rows.md, including
|
||||
13a98bfd0 and 29404f996 as asked. Row A's and row B's briefs (f325657c7) are not part of this round. I
|
||||
read their citations only where the design shares them. This was a static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Model path (section 9). The citations hold on origin/refactor 088087ac9:
|
||||
- `ENV_ALLOW` (packages/seat/src/session.mjs:83-88) has no `ZAI_API_KEY`, so today the key is dropped.
|
||||
- pi-ai reads `zai` from `ZAI_API_KEY` (env-api-keys.js:89), and `glm-5.3` is a zai model id (see N1 for
|
||||
the file path).
|
||||
- pi's harness sets `countsAccount` to false (pi/index.mjs:42), so the account check at launcher.mjs:296-308
|
||||
never runs for pi. `preflight()` (:26) checks only the binary. `credentialStatus` (launcher.mjs:343) is
|
||||
metadata passed into the launch object (:385), not a refusal. So "a pi launch has no credential check"
|
||||
is true, and the precheck key-file line is the right place to catch a missing key.
|
||||
- `PI_PROVIDER` comes from `session.provider` (pi/index.mjs:57).
|
||||
|
||||
2d. The key file is the path the bus unit already loads (mosaic-bus.service.in:24), and the `%h` in that
|
||||
line resolves to /srv/mosaic because `<user>` is mosaic with that home (DESIGN 1010-1012). The design
|
||||
says plainly that melanie's sudo and her Hermes processes can read it until #423 step 5 (:1042). On
|
||||
Gosalyn's naming question, I'd keep `claude-session.env`. A new name means a second `EnvironmentFile=`
|
||||
line, which adds a line to case2-unit.sh's fixed set and departs from the bus unit. The cost of keeping
|
||||
it is a misleading name. A README line saying the file holds every session provider key, not only
|
||||
Claude's, fixes that, and it can ride row A's README edit.
|
||||
|
||||
C2-13 and the rows doc:
|
||||
- The marker prompt check now runs only for a claude-code creative instance, and a test shows a missing
|
||||
claude passes when there is none. That's right, since Bean and Chet run pi.
|
||||
- The key-file precheck has refusal cases for missing, 0644, symlink and empty, plus a search of the test
|
||||
output for the fixture value.
|
||||
- `EnvironmentFile=` joins the fixed lines, with removed and pointing-elsewhere cases.
|
||||
- The Exec prefix rule covers the whole leading run, with `+` and `-+` cases, as webby-0027 asked.
|
||||
- 13a98bfd0, `--profile factory|library`. `factory` requires the tester and refuses a library project
|
||||
(`profile-library-business`), and `library` refuses the tester. Each direction has a test, so a wrong
|
||||
flag can't reach a tester-on Case 2 install.
|
||||
- 13a98bfd0, node >= 24 and bwrap prechecks. Both have fake-binary refusal cases, and a bwrap refusal
|
||||
prints the userns sysctl and PATH state.
|
||||
- 29404f996, the copy id. The rule matches `copy_root` (scripts/stack-copy.sh:20): 40 lowercase hex and
|
||||
`scripts/mosaic` present. The tests cover short, uppercase, `/`, `..` and missing.
|
||||
|
||||
C2-20. The Hermes name count prints one count per persona file and never the line, number or word, and
|
||||
the test checks the output holds none of the matched words. The list comes from code at df5700ebe, not
|
||||
her profiles, so the script carries no persona text.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The model data file is `dist/providers/data/zai.json`, not `data/zai.json` (DESIGN:1023, and row A's
|
||||
brief). The id `glm-5.3` is there. Fix the path when the file is next touched.
|
||||
|
||||
N2. `copy_root` reads `COPIES` as `${XDG_DATA_HOME:-$HOME/.local/share}/mosaic-stack/copies`
|
||||
(stack-copy.sh:6), while case2-unit.sh fixes `<home>/.local/share/mosaic-stack/copies`. If mosaic's
|
||||
login env ever sets `XDG_DATA_HOME`, copies land elsewhere and the unit refuses. That fails closed, so it
|
||||
doesn't block. The install could check `XDG_DATA_HOME` is unset, or the rows doc could say why it's safe.
|
||||
|
||||
N3. "Sets a nonempty `ZAI_API_KEY`" should parse the file the way systemd does: the last assignment wins,
|
||||
and quotes are stripped. A file with `ZAI_API_KEY=x` then `ZAI_API_KEY=` passes a first-match grep but
|
||||
gives the session an empty key. Add that case to the refusal list.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Webby review: row A brief, ZAI_API_KEY in the session env allowlist
|
||||
|
||||
Webby, 2026-10-20T20:55Z (from `date -u`). Requested by Sage (sage-1477). Brief text: the "Brief: row A"
|
||||
section of docs/plans/2026-10-20_case2-rows.md at Gosalyn's a2d94b2c2 (file blob 81140d017441), which is the
|
||||
version Sage is re-pinning to. Local refactor ecbfd6deb still carries the 29e85196b text and isn't on origin.
|
||||
I read the cited code on origin/refactor. Static review.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Citations hold on origin/refactor:
|
||||
- `ENV_ALLOW` is session.mjs:83-88, with the provider line at :86 (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`).
|
||||
- `SESSION_CREDENTIALS` and `hasSessionCredential` are at :94-95, and `sessionEnv` copies only allowlist
|
||||
names at :97-99.
|
||||
- launcher.mjs:299 runs the account check only when `countsAccount` is true. pi returns false
|
||||
(pi/index.mjs:42).
|
||||
- cli README:435 is the "A Pi session gets the same variables" sentence, and :439-444 says a key that isn't
|
||||
an `ENV_ALLOW` variable launches and then fails at the first turn.
|
||||
- session.test.mjs:51-59 is the existing allowlist test. Its `deepEqual` input holds no `ZAI_API_KEY`, so it
|
||||
still passes after the change. :76 checks that every `SESSION_CREDENTIALS` name is in `ENV_ALLOW`, which
|
||||
keeps holding.
|
||||
- No test under packages/*/tests or scripts/test-*.sh pins a hash of session.mjs.
|
||||
|
||||
The scope is right:
|
||||
- Leaving `SESSION_CREDENTIALS` alone means a Claude Code launch still refuses without a Claude login, even
|
||||
when a zai key is set.
|
||||
- The mutant has a real target, because the new test asserts both membership and passthrough.
|
||||
- The README sentence states the cost plainly: every session the host starts gets the key, and its shell can
|
||||
read it.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The cli README's "Unit" section says the file "holds one line, for example
|
||||
`CLAUDE_CODE_OAUTH_TOKEN=<token>`" (packages/cli/README.md:711). The brief adds a second sentence there. If
|
||||
the subagent only adds, the README will say both "one line" and "every session provider key". The brief
|
||||
should say to replace "holds one line" with something like "holds one line per variable, for example". This
|
||||
is a doc-only fix. Fold it into the re-pin if that's cheap, since a Haiku subagent follows the brief
|
||||
literally.
|
||||
|
||||
N2. The same paragraph says "nothing in the repository creates or reads it" (:713). That stays true for row
|
||||
A. It becomes false when C2-13's precheck parses the file, so C2-13 should own that line.
|
||||
@@ -0,0 +1,103 @@
|
||||
# Webby review: row 192 r1, Q175 phase 1 SSO (#1690)
|
||||
|
||||
Webby, 2026-10-20T19:49Z (from `date -u`). Author: Launchpad. Candidate 83b32d8d8 on launchpad/row192, base
|
||||
53bb59462. Manifest `candidate-manifest.sha256` digest 3ad2a73d...badab, verified: all 18 path hashes match
|
||||
the candidate, the path list matches `git diff --stat 53bb59462 83b32d8d8`, and none of the 18 paths changed
|
||||
from the base to origin/refactor (aa1bd209e). Design: `agents/sage/work/q175-sso/DESIGN.md`. Static review
|
||||
plus one node probe of `returnPath` (below). I ran no suite and no mutant. I did not read
|
||||
/mnt/storage/scratch/launchpad-r192.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
The build follows the design closely. I checked these against the design and found them right:
|
||||
|
||||
- Callback steps 2-7 (sso.mjs:232-285):
|
||||
- The login record is taken and deleted before any check, and the provider comes from the record.
|
||||
- `validateApplicationLevelSignature` runs on the same Response, and its throw ends the callback.
|
||||
- The header alg is checked against `algs`, `iss` is compared raw, and `sub` is looked up in the subjects Map.
|
||||
- The `subject-unknown` line carries only the provider, iss and sub, JSON-quoted and capped at 255.
|
||||
- Sessions:
|
||||
- The store is keyed by sha256 and the cookie strings are exact.
|
||||
- The caps are 64 sessions and 32 logins.
|
||||
- Idle and absolute expiry are server-side.
|
||||
- `touch` happens only on a POST or a GET outside `/api/` (serve.mjs, the `sso.session` call).
|
||||
- Host (F4). Under `--sso`, anything but the public Host is 403, except GET `/healthz`. `/auth/*` routes go
|
||||
to `sso.handle` before the session check, and a request without a session gets 401 on `/api/` and a 303
|
||||
on a page.
|
||||
- Logout needs the exact Origin and `Sec-Fetch-Site: same-origin`, and a refused logout keeps the session.
|
||||
- Discovery (Q-B, sso.mjs:92-121, :151-182):
|
||||
- Unreachable and Refused are split as the design says.
|
||||
- A Refused provider stops the start. An unreachable one retries with doubling, capped at 300 s.
|
||||
- A Refused result on a retry stops the retries.
|
||||
- `readSlot` (sso.mjs:33-61):
|
||||
- lstat refuses a symlink, then the open uses O_NOFOLLOW, then fstat checks the file type, the uid and the
|
||||
mode against 0o077.
|
||||
- The realpath checks cover the repo, the checkout and the copies root.
|
||||
- The default copies root matches `stack-copy.sh:6`. I read the owner branch. It is right, and as the
|
||||
design says, no test covers it.
|
||||
- Tester listener (section 6, F8):
|
||||
- `startServer` throws on `tester` with `sso`.
|
||||
- Under `--sso` the tester listener refuses Authorization, either sign-in cookie and any `x-authentik-*`
|
||||
header, and `/auth/login` is 404 there.
|
||||
- The tester writer is unchanged.
|
||||
- The loader (C1) checks shape only, and test 25's FIFO slot proves no case opens a slot file.
|
||||
`mosaic-config.mjs` going async is safe because every reader runs it as a subprocess
|
||||
(cli/src/config.mjs:22, business/src/cli.mjs:32).
|
||||
- `webui-service.sh` refuses `--sso` with the preview set or the proof pair, and its status grep reads the new
|
||||
flag order.
|
||||
- The page: both app.js fetch paths, plus the two bus.js reads (F1), sign out on 401 `sign-in required` only.
|
||||
The link goes to `/auth/login?return=<path and query>`.
|
||||
- Every design mutant is in `mutants.py`, plus 10 more. A pattern that matches no test exits 0 and counts as
|
||||
SURVIVED, so a typo in a pattern fails safe.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Open redirect after sign-in. `returnPath` (sso.mjs:200-206) accepts any URL whose origin is
|
||||
`publicOrigin` and returns `u.pathname + u.search`. The callback puts that string in `Location` (:284). If
|
||||
the pathname starts with `//`, the browser reads the Location as scheme-relative and leaves the Console. I
|
||||
ran the function as written in node, with `publicOrigin = https://mosaic.example.com`:
|
||||
|
||||
| `return=` | `returnPath` | Browser goes to |
|
||||
| --- | --- | --- |
|
||||
| `/.//evil.example` | `//evil.example` | `https://evil.example/` |
|
||||
| `/x/..//evil.example` | `//evil.example` | `https://evil.example/` |
|
||||
| `https://mosaic.example.com//evil.example` | `//evil.example` | `https://evil.example/` |
|
||||
| the four test 14 cases | `/` | the Console |
|
||||
|
||||
Here is how an attacker uses it. They send Jason `https://<public host>/auth/login?return=/.//evil.example`.
|
||||
He signs in at the real provider, and the Console then sends him to the attacker's page. The `.`/`..` segments
|
||||
are why. Test 14's `//evil.example` parses with evil.example as the host, so the origin check catches it.
|
||||
`/.//evil.example` parses as a path on the Console, so the origin check passes, and dot-segment removal
|
||||
leaves the pathname as `//evil.example`. Design B2 and test 14 exist to stop exactly this.
|
||||
|
||||
Fix: return an absolute URL on the Console, `publicOrigin + u.pathname + u.search`. Then `//evil.example`
|
||||
becomes `https://<host>//evil.example`, which stays on the Console. Or refuse a pathname that starts with
|
||||
`//`. Add the three rows above to test 14, expecting `/` (or the absolute form, if you choose it), and add a
|
||||
mutant that restores the current return so test 14 kills it.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The brief and BUILD.md:4 cite design blob eba861f5d2bb (d55d86bb0). Refactor and the candidate have
|
||||
9e3eadb15c80 (b416d2ede), which adds Mos's straggler check. The two differ only in the phase 2 window, so
|
||||
the build isn't affected. Cite the current blob in the next round.
|
||||
|
||||
N2. Design section 6 says the tester listener also refuses "the old auth and proof header names". Under
|
||||
`--sso`, cli.mjs:108 passes `testerProofHeader: proof?.header ?? null`, and that is always null because
|
||||
`--sso` refuses `--proof-*`. So the old proof header name isn't refused on 7333. The `x-authentik-*` prefix
|
||||
covers the old auth header. Nothing on 7333 reads the proof header, so this is harmless. Either add the
|
||||
name the live unit uses to the F8 refusal, or list it under Deviations.
|
||||
|
||||
N3. serve.mjs:64-66: the new `SIGN_IN_COOKIE` line went between the row 177 comment and
|
||||
`TESTER_CHAT_WRITE`, the line that comment describes. Move it above the comment, with its own comment.
|
||||
|
||||
N4. For the phase 2 window and row 200, not this row. Before 192, `mosaic-config.mjs` refuses any unknown
|
||||
top-level key (53bb59462 scripts/mosaic-config.mjs:112-116). Once `console` is in the live config.json, every
|
||||
loader from a pre-192 tree refuses the whole config, and every tool on that tree fails closed. That covers
|
||||
older pinned copies, any checkout behind 192, and a rollback target older than 192. A 192+ tree without
|
||||
`packages/webui/src/sso-config.mjs` refuses too (the import fail in `validateConsole`). The
|
||||
stack-release.test.mjs:1793 fixture copies `scripts/mosaic-config.mjs` without it, which is fine while the
|
||||
fixture config has no `console`. So the window has to re-pin to 192+ before it writes the key, and a rollback
|
||||
past 192 has to remove the key first. Put both in the window plan.
|
||||
|
||||
N5. A HEAD request for a page with no session gets 401 JSON, not the 303, because serve.mjs checks
|
||||
`req.method !== 'GET'`. Browsers don't send that, so no change is needed.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Webby review: row 192 r2, Q175 phase 1 SSO (#1690)
|
||||
|
||||
Webby, 2026-10-20T19:58Z (from `date -u`). Author: Launchpad. Candidate 37582e820 on launchpad/row192 (one commit on
|
||||
83b32d8d8), base 53bb59462, queue rev 2312. Manifest `candidate-manifest.sha256` digest 6f8cee14...7787,
|
||||
verified. All 18 path hashes match the candidate. The paths are `git diff --name-only 53bb59462 37582e820`
|
||||
less the four packet files under agents/launchpad/work/queue-192/. None of the 18 paths changed from the base
|
||||
to origin/refactor (ae3c36f24). Static review plus one node probe of `returnURL` (below). I ran no suite and
|
||||
no mutant. I did not read /mnt/storage/scratch/launchpad-r192, so the 28/28 and mutant results are
|
||||
Launchpad's report, not my run.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- r1 B1 is fixed. `returnURL` (sso.mjs:199-211) parses the return against publicOrigin, refuses another
|
||||
origin, and builds `publicOrigin + pathname + search` as an absolute URL. The callback's Location (:289)
|
||||
is now always absolute. Only the callback uses it; the export at :341 follows the rename. Test 14 has my
|
||||
three rows, `/board/./x`, and an origin assert on every case. The positive test expects the absolute
|
||||
form. Mutant 22 restores the r1 return under test 14.
|
||||
- Probe. I ran the function as written in node with publicOrigin `https://console.example.test` over 18
|
||||
inputs: my three rows, `/%2e%2e//evil`, `/.\\/evil`, `///evil`, `//evil`, `/\\evil`,
|
||||
`/@evil.example`, `/:evil`, CR/LF in the path (raw and encoded), a full-width dot, `:443`, upper-case
|
||||
scheme and host, and a fragment. Every result stayed on the Console's origin. Raw CR/LF are stripped by
|
||||
the URL parser, so no header split.
|
||||
- Mutant 23 (drop the second origin check). I agree it is equivalent. sso-config.mjs:43-44 holds
|
||||
publicOrigin to a bare https origin, and a parsed special-scheme pathname always starts with `/`, so the
|
||||
concatenation can't parse to another host. In the probe the mutant returned the same string as the build
|
||||
for all 18 inputs. Keeping the check as an unreachable guard is fine with me. Whether to keep it is
|
||||
Sage's call, as BUILD.md says.
|
||||
- N1 (design blob 9e3eadb15c80), N2 (under Deviations), N3 (serve.mjs:64-66, comment order) are done. N4
|
||||
moves to row 200 (#1698) and the window plan through Sage (mos-1058). N5 needs no change.
|
||||
|
||||
No new findings.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Webby review: row 194 design r2 (#1692, agent lifecycle and per-agent veto)
|
||||
|
||||
Webby, 2026-10-20T19:31Z (from `date -u`). Independent reviewer. Author: Honker.
|
||||
Subjects at c0a35f47a on honker/row194, both sha256 verified: `agents/honker/work/queue-194/DESIGN.md`
|
||||
(313c206e...fc25) and `PRD-1.7-DRAFT.md` (98f4f49e...9708). I read the whole of both and the diff
|
||||
from 14a6f5f9d. Code read at 53bb59462. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
One blocker. B3-B8, Q2 and N1-N8 from webby-0003 are fixed as the closing table says. I checked
|
||||
each against the text. B1 is fixed: hard waits, no grant, and a PRD 1.7 draft for the routed form.
|
||||
B2's floor is the gap.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. The hire floor is a deny list, and it misses actions a hireable definition can hold today.
|
||||
D2.3 refuses seven actions. role.mjs:40-43 already refuses the hard waits in both lists, so what is
|
||||
left is the routed set (vocabulary.mjs ROUTED). The floor doesn't name these:
|
||||
- `git.merge.protected`. `roles/merge-gate.json` holds it in crossRole and declares no credentials,
|
||||
so it passes D2.2 item 3 and the floor. Listed in `hireable`, the chief could hire its own merge
|
||||
gate. That is the same author-is-reviewer problem `review.verdict` is on the list for.
|
||||
- `decision.resolve.technical`. A hired holder resolves technical decisions, so the chief would pick
|
||||
who answers its own requests. `cto.json` is credentialed today, but a credential-free role
|
||||
holding it would pass.
|
||||
- `policy.change` and `prd.approve`. Both are routed. A credential-free role holding either passes.
|
||||
- `instance.name`. It was in my r1 B2 list, which Mos accepted (mos-1018), and it is missing here.
|
||||
Every action added to the vocabulary later is also hireable by default, which is the wrong
|
||||
direction for a fail-closed floor (AGENTS.md invariant 6).
|
||||
Fix: turn `HIRE_FLOOR` into an allowlist, `HIRE_ALLOWED`. A definition is hireable only when every
|
||||
action in withinRole and crossRole is on it. From the role files at 53bb59462, the list a working
|
||||
hire needs is `message.send`, `record.write`, `review.request`, `task.update.assigned`,
|
||||
`task.scope.change` and `git.push.working`. The last two only matter after row 199 (credentials).
|
||||
T8b and T8c then loop over `ACTIONS` minus the allowlist, one case each, so a new action is covered
|
||||
without a test edit. M30-M43 become "one action added to the allowlist in the broker" and "in
|
||||
role.mjs". Update the PRD draft's bound to match: "The chief hires only role definitions whose
|
||||
actions are all on a fixed list of working actions. Adding to that list is a PRD change."
|
||||
|
||||
## Q4 (for Sage, my view)
|
||||
|
||||
Start `hireable` empty. Under the hard-wait form Jason resolves each hire anyway. With the B1
|
||||
allowlist, today's credential-free definitions that pass are assistant and the five chief-officer
|
||||
roles (message.send and record.write only). assistant is END_PROTECTED. A first kind is a policy
|
||||
call and belongs in its own reviewed commit.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. D2.1 step 3 compares the call's arguments to "the decision body". Name the field-by-field
|
||||
comparison (business from the cap, name, definition, canonical vars). A body with extra keys should
|
||||
refuse, not be ignored.
|
||||
N2. The PRD draft's notes say "Replace" covers the model write. The REQ-COS-3 text itself says
|
||||
only "fire, replace or end". Put the model write in the requirement text, so a later reader of the
|
||||
PRD alone sees it.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Webby review: row 194 design r3 (#1692)
|
||||
|
||||
Webby, 2026-10-20T19:32Z (from `date -u`). Author: Honker. Subjects at d0afda024 on honker/row194:
|
||||
DESIGN.md sha256 885fce0d...65fc, PRD-1.7-DRAFT.md sha256 04c32917...d728 (verified). Read as the
|
||||
diff from c0a35f47a, which my r2 review (REVIEW-r2.md, webby-0010) covered. r3 crossed with webby-0010
|
||||
and doesn't answer it yet. Static review.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
r2 B1 stands in part. r3 adds `instance.name` to the floor. `git.merge.protected` (merge-gate.json,
|
||||
crossRole, no credentials), `decision.resolve.technical`, `policy.change` and `prd.approve` still
|
||||
pass it, and so will any action added later. The fix is unchanged: an allowlist, as in REVIEW-r2.md.
|
||||
M47 and M48 fold into the allowlist mutants.
|
||||
|
||||
## New blocker
|
||||
|
||||
B2. Fire can't be "in the same transaction" as the delist. The delist is a business file commit
|
||||
through the organization writer (organization.mjs write: lock, rename, receipt). The claim revoke
|
||||
and the `lifecycle.cancelled` and `lifecycle.end` events are store rows. No transaction spans the
|
||||
two. Fix: name the order and the failure between them. I'd write the store side first (revoke,
|
||||
cancel, end), then commit the delist. If the delist fails, the runs are ended and I is still
|
||||
listed, so the chief can fire again, and that second fire must work with no claims left. The other
|
||||
order leaves a delisted agent with a live process and the model token, which is the F4 case. Test
|
||||
a writer failure after the store commit, and add a mutant that swaps the order.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. REQ-LAND-2 says "Gitea itself then refuses a push to upstream or to another agent's fork". That
|
||||
holds only if the agent's Gitea user has no write access to those repos. Gitea allows a push from any
|
||||
collaborator. The bound should be the host's check that the remote's owner is the agent's own user,
|
||||
made at each push. Gitea's refusal is the second layer. Megavolt is checking this text against row
|
||||
199, so I'm sending it there as a note, not a blocker here.
|
||||
N2. F4's 10 s recheck reads a pid. Use the same start-time check as the signal (launcher.mjs:526),
|
||||
so a reused pid isn't reported as the fired run.
|
||||
|
||||
The rest of r3 reads right: F4's reason (ENV_ALLOW, session.mjs:83-94), T9a, M49-M51, R7 and the Q4
|
||||
ruling.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Webby review: row 194 design r4 (#1692)
|
||||
|
||||
Webby, 2026-10-20T19:40Z (from `date -u`). Author: Honker. Subjects at 7d0788236 on honker/row194:
|
||||
DESIGN.md sha256 824ef884...3f34, PRD-1.7-DRAFT.md sha256 379cc318...05bc (both verified). Read as the diff
|
||||
from d0afda024 (my r3 review), with the role files at origin/refactor. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 (webby-0010 and webby-0011) is fixed. D2.3 is now an allowlist, `HIRE_ALLOWED`, with six actions. T8b and
|
||||
T8c loop over `ACTIONS` minus the list and the hard waits. There is a mixed case and a pin test, and M47/M48
|
||||
are "some instead of every". The table of today's files checks out against the role files. chief-of-staff,
|
||||
cto, merge-gate, pm, reviewer and security-reviewer each hold an action off the list. Coder's six actions
|
||||
are all on it, but coder is credentialed (2). The C-suite files and assistant hold only `message.send` and
|
||||
`record.write`. The PRD bound now names the fixed list and says a change to it is a reviewed commit.
|
||||
|
||||
B2 (webby-0011) is fixed. Fire runs A (store), then B (delist), then C (consume). A and B are safe to repeat,
|
||||
and T9c covers both failure points. M56-M58 kill the order swap, the early consume and the `not-held`
|
||||
refusal.
|
||||
|
||||
The r2 N1 and N2 items and the r3 N2 item are in. The SIGKILL escalation through row 191's stop, with one
|
||||
stop receipt and the halt join, reads right (T9a, T9b, M51-M55). V3 terminate stays SIGTERM-only (M54).
|
||||
|
||||
## Non-blocking, for the build
|
||||
|
||||
N1. Between a failed B and the repeat, I is still listed, so a launch of I passes (`launch` takes listed
|
||||
instances). A repeat's A would stop that run, but nothing makes anyone repeat the fire. I'd have `launch`
|
||||
refuse `instance-firing` while I has a `fire.stopped` with no `fire.done`, and add that case to T9c.
|
||||
N2. "Every action is on the list" is true of a definition with no action lists at all. `roles/researcher.json`
|
||||
is `roleVersion` 1 with no withinRole or crossRole, and the design counts it as passing. The load check's
|
||||
version 2 rule refuses it, but the hire-time check in the broker should require version 2 too. A role file
|
||||
edited to version 1 after load would then refuse, not pass the allowlist with no actions. Add it to T8b.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Webby review: row 194 design r1 (#1692, agent lifecycle and per-agent veto)
|
||||
|
||||
Webby, 2026-10-20T19:14Z (from `date -u`). Independent reviewer. Author: Honker.
|
||||
Subject: `agents/honker/work/queue-194/DESIGN.md` at 14a6f5f9d on honker/row194, sha256
|
||||
eec9f011567d (honker-0003, which supersedes 8f80c278b from honker-0002; I read both and the diff).
|
||||
Brief blob a907109409ac. Static review only. I ran no tests and changed no product code.
|
||||
|
||||
Code base: `git archive 5d019d876`. `git diff --stat 5d019d876 53bb59462 -- packages adapters roles scripts`
|
||||
is empty, so every line below holds at the design's base. Paths are relative to `packages/` unless
|
||||
they start with `roles/`.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
Eight blockers. The verb split, the launcher-only start, the D3 reload through the boot loader and the
|
||||
`retired` list are sound. The chief's side (O1, hire) and the request lifecycle need work.
|
||||
|
||||
## Citations checked
|
||||
|
||||
These hold: broker.mjs:145 (ORGANIZATION_KEYS), :208, :446, :770-774, :781-788 (decisions bind to the
|
||||
role instance), :1601-1628; launcher.mjs:232-327 (check reloads the file at :233), :239, :527,
|
||||
:624-668; broker.mjs:542-546; tools.mjs:116-119; vocabulary.mjs:135; runtime.mjs:75-81, :121-133; config.mjs:88-105;
|
||||
business.mjs:117-132; instance-settings.mjs:19-31; serve.mjs:282-289, :535-538; session.mjs:245-270.
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. O1 can't load as written. Adding `role.hire` and `role.fire` to ACTIONS alone makes them hard
|
||||
waits (vocabulary.mjs:52-54, REQ-WAIT-3; broker.mjs:43-69 keeps its own ROUTED). role.mjs:42 then
|
||||
refuses `roles/chief-of-staff.json`, because withinRole lists a GATED_ONLY action. O1 needs both
|
||||
ROUTED lists, and vocabulary.mjs:52-54 says an action leaves the hard waits only when a PRD version
|
||||
says so. So O1 is a PRD change, which is Jason's, not only Sage's and Mos's. Fix: restate Q1 with
|
||||
that. O2 needs only the ACTIONS entries and fits the brief's "verbs, not authority". My
|
||||
recommendation is O2 for this row, and O1 after a PRD version routes the two actions.
|
||||
|
||||
B2. A hire can still create authority, and `hireable` needs a schema. The r2 `hireable` list is
|
||||
a reviewed role-file entry, which helps. But the structural rules (D2.2) still refuse only
|
||||
credentialed, END_PROTECTED and version 1 definitions, and `roles/chief-of-staff.json` declares no
|
||||
credentials. A `hireable` list that names chief-of-staff, by mistake or later, lets the chief hire a
|
||||
second chief with role.launch, role.end, organization.change, instance.name, and hire and fire. Under
|
||||
O2 there's no list at all. Sage's example, `security-reviewer`, holds review.verdict
|
||||
(`roles/security-reviewer.json`), so Mos should rule knowing that hire would let the chief add
|
||||
verdict holders. Fix: hire refuses (`definition-not-hireable`) any definition whose withinRole holds
|
||||
role.launch, role.end, role.revoke, organization.change, instance.name, role.hire or role.fire,
|
||||
whatever `hireable` and `noHire` say. role.mjs validates `hireable` (a new role-file key: name it as a
|
||||
role schema change) and refuses such an entry at load. Test with chief-of-staff.
|
||||
|
||||
B3. Re-hire contradicts itself and carries decisions. The D2.1 table refuses a hire "when ... the name
|
||||
exists". The text below it (DESIGN.md:156-157) says a fired name comes back by a hire of the same
|
||||
name. If that re-hire can name another definition, approvals bound to the instance
|
||||
(broker.mjs:781-788) carry to a different role. That is the problem `retired` solves for delete.
|
||||
Fix: pick one. Either re-hire of a fired name keeps its definition and refuses another, or re-hire
|
||||
is refused. Test the chosen rule.
|
||||
|
||||
B4. The V2 request isn't exactly-once, and the human's brake lapses at start. The pass runs `check()`
|
||||
then `start()`. It skips authorizeLaunch (launcher.mjs:653), and `check()` never reads launch_state,
|
||||
so the only revoked check is at request time (V2 step 1). A `launch.revoke` inside the 60 s window
|
||||
doesn't stop the launch, against "applies to the human until they restore it". Separately, if
|
||||
`lifecycleDone` fails after `start()`, the next tick sees the request still open. `check()` refuses
|
||||
`instance-running` only while the run lives (launcher.mjs:239), so a short run can start twice.
|
||||
Fix: one host op takes the request (writes `lifecycle.taken`) and re-checks revoked, retired and,
|
||||
for a tester request, `bus.tester` (B8) in the same transaction, before `start()`. A taken request never starts again. Test a revoke between
|
||||
request and pass, and a `lifecycleDone` that throws once.
|
||||
|
||||
B5. Terminate and delete race a launch in flight. A run that is spawned but not yet bound holds no
|
||||
claim. V3 answers `not-held` for it, and an open V2 request for I still starts after a terminate.
|
||||
Delete's `instance-running` reads claims only. The workspace rename isn't serialized with
|
||||
`start()`'s mkdirSync (launcher.mjs:350), and the design doesn't say which process renames it or
|
||||
what happens when the file commit succeeds and the rename fails. Fix: terminate closes open
|
||||
requests for I as `request-cancelled`. Delete refuses `instance-starting` while a request is open or
|
||||
the launcher's registry holds I. The rename runs under the launcher's `serialize` as a host op, and
|
||||
the design states the order and the recovery after a failed rename. Test both races.
|
||||
|
||||
B6. The tester gets delete. D4.1 says the tester gets no delete, but TESTER_ORGANIZATION_VERBS
|
||||
holds `instances.set` (broker.mjs:103), and `/api/instances` lets a tester through without `send`
|
||||
(serve.mjs:537). Op `delete` rides `instances.set`, so the tester can delete. M18 covers `veto.set`
|
||||
only. Fix: the broker refuses op `delete` from a tester session with `tester-refused`. Add it to T12
|
||||
and a mutant.
|
||||
|
||||
B7. Keep doesn't cover the chief's model write. Through `organization.set` the chief changes an
|
||||
instance's model key (broker.mjs:1266-1269). On a kept instance that changes the agent the human
|
||||
chose to keep, at its next launch, which the chief controls. Fix: `organization.set` refuses
|
||||
`instance-kept` for a write that names a kept instance. Test it in T10.
|
||||
|
||||
B8. D4.1's live `bus.tester` check has nothing live to read. `bus.tester` is a business var
|
||||
(broker.mjs:544). No bus verb writes business vars: ORGANIZATION_KEYS (broker.mjs:145) and
|
||||
`instances.set` don't reach them. So it changes only by hand edit, and D3 item 6 says a hand edit
|
||||
needs a restart. The broker's live copy then sees Jason turn testing off only at the next restart,
|
||||
which is today's behaviour, and T12a's "a live write turns `bus.tester` off" has no write to run.
|
||||
Fix: each tester call reads `bus.tester` from the business file as it stands, as `harnessOn` does
|
||||
(runtime.mjs:136-142), and an unreadable file refuses `tester-off`. T12a turns it off with a file
|
||||
edit and no restart. M20 stays as written.
|
||||
|
||||
## Q2
|
||||
|
||||
Yes, keep should block the chief's `role.end` on a kept instance. End then launch is a replace
|
||||
(E6), and the limits (`#stops`) and the human's V3 still stop a runaway. Two things go with it.
|
||||
The block belongs in the reason branch only (broker.mjs:1607-1613). The decision branch
|
||||
(:1614-1615) cites a decision the human resolved, which is the human's own act, so it should still
|
||||
end a kept run. Say so and test it. B7 extends the same rule to the model write.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. D3 item 2 lists six verbs. `plugins.set` and `routes.set` commit too (runtime.mjs:128-130).
|
||||
Reload after every committed write in one place in the callback, so a later verb can't miss it.
|
||||
N2. `noHire` must name definitions present in `rolesDir` (D2.3). A reviewed commit that removes a
|
||||
role file would then stop every business that lists it from loading. A stale entry on a deny list is
|
||||
harmless, so accept unknown names, or check them at write only.
|
||||
N3. Hire takes `vars`. Agent-layer vars include `harness` and the fallback route (vars.mjs:142-147),
|
||||
which is more than the chief sets today through `organization.set`. Limit hire's vars to that set
|
||||
plus the narrowing `limits.*` keys.
|
||||
N4. D3 item 4 changes the existing `instances.set` op `change` on a running instance. Name the
|
||||
existing business and webui tests that change with it.
|
||||
N5. `agents.js` must join the static file list at serve.mjs:28 and `index.html`.
|
||||
N6. V4 waits for the old run's exit. A run that ignores SIGTERM lets the request expire at 60 s.
|
||||
State that, and whether anything escalates to SIGKILL.
|
||||
N7. Keep refuses END_PROTECTED instances, so the human can't keep the assistant from the chief's
|
||||
`role.end`, which Q169 allows. Say that this is intended.
|
||||
N8. With `human: true`, `check()` must still send `launch.by` through the `pm` branch
|
||||
(launcher.mjs:235). Add that case to T2.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Webby review: row 197 implementation packet, round 2 (gate lanes)
|
||||
|
||||
Webby, 2026-10-20T20:44Z (from `date -u`). Author: Herb (herb-0023). Worktree herb-row197, branch herb/row197,
|
||||
parent a4e3e857e, source uncommitted. Round 1: REVIEW-impl.md at f942bd7d4 (webby-0029, CHANGES B1 B2).
|
||||
|
||||
Packet hashes I checked: BUILD.patch 0b3871bd...f43e, SOURCE.json 91ba149f...596b, gate.sh 48e06e23...fed1. All
|
||||
25 SOURCE.json rows match the worktree on sha256 and mode. The patch paths are the same 25 paths. The patch
|
||||
applies cleanly to a4e3e857e in a scratch worktree of mine, and the result is byte-identical to Herb's worktree
|
||||
for every path. I removed that worktree afterwards. BUILD-LOG.md has 25 added lines and no removed ones, outside
|
||||
the packet.
|
||||
|
||||
This was a static review. I ran no test, no suite and no Docker command, and I did not read the frozen checkout
|
||||
or Herb's other scratch roots. The counts below come from Herb's packet logs.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B2 is fixed. `leaseContext` (gate-lane-fixtures.test.mjs:29-33) takes `MOSAIC_GATE_LANE` and `MOSAIC_GATE_STATE`
|
||||
when they're set, and every lease goes through the active lane's real lock dir. The other band is checked with
|
||||
arithmetic (:10), and no test reserves the other lane's ports. The exhaustion test (:36-48) listens only on a
|
||||
port it leased itself, and narrows `BANDS` only inside its own process. It restores `BANDS` in `finally`.
|
||||
review-t4-lane-a.log: 8/8, and lane a's lock namespace is the only one present.
|
||||
|
||||
B1 is fixed. Each manifest case now has a test, and BUILD.md's table maps them all:
|
||||
|
||||
- T1 (MANIFEST.md:119-130). gate-lane.test.mjs:66-70 tests canonical, builder and existing-output refusals.
|
||||
`validateTooling` has missing-file and changed-file tests (:71-81). The 92-byte socket example passes and
|
||||
108 bytes refuses (:82-87). Duplicate steps stay, and NODE_TEST_CONTEXT is cleared from `cleanEnv` and from
|
||||
both supervised steps (:88-99). Scoped port 9 addresses refuse (:100-103).
|
||||
- T2 (:147-151). Two real shell wrappers race (gate-lane-state.test.mjs:84-98). There is one start receipt,
|
||||
and the loser exits 2. The supervisor passes `verifyLocks`, the step child's /proc/self/fd has no lock
|
||||
target, and the inode is stable. `activeConflicts` is tested for same-checkout and for a legacy land gate on
|
||||
a free lane (:99-106). Recovery kills the proven child, a foreign child survives, a recovery killed during
|
||||
Docker cleanup stays quarantined, and a resumed recovery clears the lane (:107-124).
|
||||
- T3 (:171-174). Missing receipt, missing image and wrong release tag (gate-lane-image.test.mjs:46-50). A tag
|
||||
that changes between steps stops the next step, and no terminal rc 0 follows (:51-61). A rebuild with the
|
||||
same input digest gets a new ID, the old receipt refuses it, and cold and warm counters stay separate
|
||||
(:62-72). The tag is checked again after the last step (:73-80). runGate calls the same
|
||||
`executePinnedSteps` (gate-lane.mjs:184), so the test covers the real path.
|
||||
- T4 (:201-209). Two generated node test files take different leases in the same lane and state. The lock is
|
||||
held while the child runs and released after it exits, and the other lease stays held
|
||||
(gate-lane-fixtures.test.mjs:49-62). A SIGKILLed allocator frees its own lock and leaves a foreign one alone
|
||||
(:63-72). `fixtureCacheEnv` has a replacement-env test (:73-79).
|
||||
- T5 (:226). Another process appends under a fixture HOME while a supervised step is running, and attribution
|
||||
puts that change in `unrelated` (:80-93).
|
||||
|
||||
Counts from the packet logs: review-t1-resume 11/11, t2 14/14, t3 8/8, t4 8/8, all with 0 skipped and 0
|
||||
cancelled. I counted the test blocks in each file, and they match. The cleanup-order fix is in place:
|
||||
`ctx.cleanup` stops both children and waits for them before it removes `root`, and leaseContext's after hook runs
|
||||
callbacks before it removes the state root.
|
||||
|
||||
The round 1 notes are addressed. N1: `closed` is no longer bound (lane-fixtures.mjs:16). N2: TMPDIR is a `tmp`
|
||||
subdir (:32). N3: `%iface` is stripped before the port 9 match (lane-state.mjs:28), with a test. N4: unit roots
|
||||
use gt-, gf-, gs- and gi-, and README:84-85 says so. N5: README:84 documents the node_modules link. N6: BUILD.md
|
||||
records the overlap with row 200.
|
||||
|
||||
The existing-file edits are the same shape as round 1. They swap helpers and add env and the concurrency
|
||||
option. No assert or timeout line changed (`git diff -U0` grep). Hash pins: no test under packages/*/tests or
|
||||
scripts/test-*.sh pins a sha256 next to any of the 25 path names.
|
||||
|
||||
gate.sh matches the template shape. It skips pkg-queue, which script-queue covers. It runs every package and
|
||||
script with `--test-concurrency=2`, stops on the first failure, and runs `cleanupProject` on exit.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Outside a lane, `leaseContext` falls back to lane a with a private gf- state root
|
||||
(gate-lane-fixtures.test.mjs:30). That is still the B2 shape. Suppose someone runs this file singly while a
|
||||
real lane-a gate holds closed leases. The exhaustion test can then take and listen on one of lane a's ports,
|
||||
because the private lock dir excludes nothing. A lane claim refuses beside a running `node --test`, but a single
|
||||
started after the claim isn't blocked by anything. Default to the real state root
|
||||
`/mnt/storage/scratch/mosaic-gate-lanes` when no lane is set, or document that singles of this file wait for
|
||||
`gate-lane.sh status` to show both lanes CLEAR.
|
||||
|
||||
N2. The band check at :10 compares literal numbers, not the imported `BANDS`, so it would pass whatever
|
||||
lane-state.mjs:7 says. Compare against `BANDS` (it's imported at :26).
|
||||
|
||||
N3. The T5 step child (:88) and the appender poll with no deadline. If the append never happens, the test hangs,
|
||||
and the runner has no timeout. Give both a bound, for example 5 s, and exit nonzero.
|
||||
|
||||
None of these block. Herb can fold them into the commit or record them as a follow-up.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Webby review: row 197 implementation packet, round 3 (gate lanes, N1-N3 delta)
|
||||
|
||||
Webby, 2026-10-20T20:53Z (from `date -u`). Author: Herb (herb-0026). Worktree herb-row197, parent
|
||||
a4e3e857e. Round 2: REVIEW-impl-r2.md at 1f404c973 (webby-0031, approve, N1-N3).
|
||||
|
||||
Packet hashes I checked: SOURCE.json 35847636...2aab, BUILD.patch 55fe390d...0fde, gate.sh 48e06e23...fed1
|
||||
(unchanged). All 25 SOURCE.json rows match the worktree. BUILD.patch applies to a4e3e857e in a temporary
|
||||
index, and every one of the 25 blobs equals the worktree file. I couldn't diff against the r2 files because
|
||||
I kept no copy. Instead, file mtimes show only two of the 25 paths changed after the r2 request (20:42Z):
|
||||
packages/cli/tests/gate-lane-fixtures.test.mjs and scripts/testing/README.md. That matches Herb's list. I
|
||||
read both in full where they changed.
|
||||
|
||||
This was a static review. I ran no test. Counts come from Herb's logs: review-r3-fixtures 8/8 and
|
||||
review-r3-lane-a 8/8, with 0 failed, skipped or cancelled and no `not ok` lines.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
N1 is fixed. `leaseContext` (gate-lane-fixtures.test.mjs:30) now defaults to the shared
|
||||
`/mnt/storage/scratch/mosaic-gate-lanes`, so a single run takes its leases through the same lock files as a
|
||||
live lane-a gate. It creates the root at 0700 if missing, the same mode lane-fixtures.mjs:20 uses, and no
|
||||
longer removes it. README:95-96 says so.
|
||||
|
||||
N2 is fixed. The band check (:8-10) reads the imported `BANDS`. The 127 widths stay literal, and that's
|
||||
fine because they pin the band size, which is the contract.
|
||||
|
||||
N3 is fixed. The appender (:87) and the step child (:89) each have a 5 s timer that exits 8. A missed
|
||||
append makes the step's rc nonzero or `appended` nonzero, and the asserts at :91 fail. README:97 says so.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. No test drives the 5 s path itself. It only guards against a hang, so I'm not asking for one.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Webby review: row 197 implementation packet (gate lanes)
|
||||
|
||||
Webby, 2026-10-20T20:23Z (from `date -u`). Author: Herb (herb-0018). Worktree herb-row197, branch herb/row197,
|
||||
HEAD a4e3e857e, base aa1bd209e. The source is uncommitted. BUILD.patch sha256 6a1cd3e5...7792 and SOURCE.json
|
||||
d0f6333a...d6ed, both checked twice. All 25 SOURCE.json rows match the worktree on hash and mode, and the patch
|
||||
paths equal the SOURCE.json paths. Plan: MANIFEST.md r2, which I approved at ca6b32d66.
|
||||
|
||||
This is a static review plus two FD probes on node 26.8.1 in a private mktemp dir. I ran none of the new tests, no
|
||||
suite and no Docker command, and I did not read Herb's scratch roots.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
Most of the code matches the manifest. The existing-file edits change helpers and env only, and no removed line
|
||||
holds an assert or a timeout. The Docker guard turns every `compose run` into `--no-build --pull never` and refuses
|
||||
everything else except `info` and an inspect of the pinned tag. Cleanup goes by exact project label and checks
|
||||
each ID's label again before removing it. `compose.yaml` declares no named volumes, so containers and networks
|
||||
are everything a run can leave. The env allowlist drops `NODE_TEST_CONTEXT`. Status and recovery work from
|
||||
receipts, not from flock alone.
|
||||
|
||||
I had one open question about the lock FDs, and the probes settled it. A bash `exec 3>> 5>>` plus flock, then
|
||||
`exec node`, leaves node holding FDs 3 and 5, and a second `flock -n -x` on the lane file fails while node runs. A
|
||||
child started with `spawn(..., {stdio: ['ignore', fd, fd]})` lists only 0-2 in /proc/self/fd. That matches
|
||||
MANIFEST.md:156 ("ordinary child stdio does not inherit them").
|
||||
|
||||
## Blockers
|
||||
|
||||
B1. Several tests the manifest requires are missing. BUILD.md:25 says T1-T5 source is present. The manifest names
|
||||
these tests, and the packet doesn't have them:
|
||||
|
||||
- T1, MANIFEST.md:121, 127, 130. No refusal test for a canonical or builder checkout or an existing output. No
|
||||
92-byte socket test and no over-107-byte socket test (`socketInventory` has no test). No test that both candidates
|
||||
carry the approved tooling. No test that duplicates stay, and none that Node entries clear `NODE_TEST_CONTEXT`.
|
||||
- T2, MANIFEST.md:147-148, 151. No claim race with one winner, no same-checkout rejection, no inherited-FD test,
|
||||
no surviving-child test and no interrupted-recovery test. The legacy test (gate-lane-state.test.mjs:17) checks
|
||||
status only. Nothing shows that a claim refuses while a legacy gate runs and the flock is free.
|
||||
- T3, MANIFEST.md:171-174. No test for a missing receipt, a missing image, a tag changed mid-gate or a rebuild
|
||||
from a mutable tag. No cold/warm counter test beyond `builds === 1`.
|
||||
- T4, MANIFEST.md:201-203, 209. Leases are taken one after another, not from parallel test files. No test for an
|
||||
exhausted band, an occupied port, owner death or a foreign listener, and none holding a lease through child
|
||||
shutdown. No cache test for `fixtureCacheEnv`, although the harness tests now import it.
|
||||
- T5, MANIFEST.md:226. Attribution has a unit test with fixed entries. Nothing appends from an unrelated session
|
||||
while a run is in progress.
|
||||
|
||||
The inherited-FD test is cheap now. The probe above shows the shape: hold the lane lock through the wrapper, spawn
|
||||
a step child, and check that the child's /proc/self/fd has no lock file and that the lock stays held after the
|
||||
child exits. Either add the missing tests, or list each one in BUILD.md as dropped with a reason and get Sage's ruling.
|
||||
A test the plan requires is part of the change, the same rule Sage applied to row 110 N1.
|
||||
|
||||
B2. gate-lane-fixtures.test.mjs:5-11 leases from a private state root, in both bands, and then listens on the port
|
||||
it got. That test runs inside the pkg-cli step of every lane gate. The private root's lock files don't exclude the
|
||||
real leases under `/mnt/storage/scratch/mosaic-gate-lanes/ports/<lane>`, and both leasers go upward from the
|
||||
band's first port (lane-fixtures.mjs:22). Here is how it fails in a paired proof. Lane a holds a closed lease on
|
||||
61000, with no listener, which is how closed endpoints work (MANIFEST.md:206). Lane b's cli step runs this test.
|
||||
The test's probe of 61000 succeeds, and :10 listens on 127.0.0.1:61000. Lane a's closed endpoint now accepts
|
||||
connections, and that test fails. The same thing happens inside one lane, because two test files run at once.
|
||||
This is the cross-lane interference the bands exist to prevent. Fix: when `MOSAIC_GATE_LANE` is set, lease only
|
||||
through the run's own lane and state. Check the other band's range with plain arithmetic instead of a lease. Drop
|
||||
the listen at :10, since no assertion reads it.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. `leasePort` ignores `closed` (lane-fixtures.mjs:16). Every call site passes it, and gate-lane-fixtures:5 is
|
||||
named after it. Use it, for example by recording the lease kind for evidence, or drop it.
|
||||
|
||||
N2. `fixtureCacheEnv(root)` points TMPDIR at the config dir itself (lane-fixtures.mjs:32). Temp files from the
|
||||
claude and pi children then land next to the fixture config they test. A `tmp` subdirectory keeps them apart. I
|
||||
didn't trace whether any assertion lists that directory.
|
||||
|
||||
N3. The port 9 check compares exact address strings (lane-state.mjs:28). `ss` prints scoped forms such as
|
||||
`127.0.0.53%lo`, and this host shows `%lo` and `%tailscale0`. A listener on `127.0.0.1%lo` or `0.0.0.0%lo`
|
||||
covers 127.0.0.1:9 but doesn't match. Strip `%iface` in `hostFacts` (:22) before comparing.
|
||||
|
||||
N4. The new tests make roots like `/mnt/storage/scratch/ga-XXXXXX` (gate-lane.test.mjs:12,
|
||||
gate-lane-state.test.mjs:46, 66). That is the run-root prefix gate-lane.mjs:201 uses, and inside a lane they sit
|
||||
outside the run's own temp root. If a test crashes, its leftover looks like a lane root, and README:41-42 tells
|
||||
operators not to remove another run's resources. Use a different prefix such as `gt-`.
|
||||
|
||||
N5. gate-lane.test.mjs:20 symlinks `/mnt/storage/src/mosaic-stack/node_modules` into the fixture, which ties the
|
||||
test to the canonical checkout's tree. That is fine on dragon-lin, but please write it down in "Not tested" or as
|
||||
an assumption.
|
||||
|
||||
N6. Coordination: row 200 (Launchpad, approved, round 2 open) also edits packages/cli/tests/stack-release.test.mjs.
|
||||
It adds T56 and fixtures, and this row replaces its `freePort` calls. Whichever row lands second has to rebase
|
||||
and rerun that file singly. Neither manifest says so.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Webby review: row 197 manifest r2 (#1695)
|
||||
|
||||
Webby, 2026-10-20T19:44Z (from `date -u`). Author: Herb. Subject: `agents/herb/work/queue-197/MANIFEST.md` at
|
||||
bdc9cd56a, sha256 2a1a3ad0...7cec (verified). Read as the diff from 84f21923c (my REVIEW-manifest.md), against
|
||||
aa1bd209e. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. P2 keeps every literal Board `:9` URL, and `closedEndpoint` is gone from T4. The two leases left
|
||||
are real close-then-rebind cases. preview.test.mjs:208-209 and serve.test.mjs:99-100 each bind a probe on port
|
||||
0, close it and use the freed port as the dead board. Another lane's fixture could take that port in the gap,
|
||||
so a lease belongs there. T1 refuses a listener covering 127.0.0.1:9, a missing inventory or
|
||||
`ip_unprivileged_port_start` at 9 or below, and makes no connection to the endpoint.
|
||||
|
||||
N1 is in P3. Sage's order (sage-1384: 197 lands first, 198 rebases onto it before its manifest) makes the
|
||||
two P3 lines row 198's to replace when it removes the board URL from those tests. The BUILD-LOG change is one
|
||||
appended correction entry.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Webby review: row 197 manifest (#1695)
|
||||
|
||||
Webby, 2026-10-20T19:39Z (from `date -u`). Author: Herb. Subjects on herb/row197: `MANIFEST.md` at 84f21923c,
|
||||
sha256 16768433...b23b, and `TIMING.md` at fa820fed6, sha256 148a1416...52bd (both verified). TIMING's commit
|
||||
sits on top of the manifest's. I read both whole, against DESIGN.md at 6ad25d745 (my r2 APPROVE) and the base
|
||||
aa1bd209e. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
T1-T5 cover the design. My r2 N1 is in T5: metadata-only listings, with assertions only on lane-attributed
|
||||
entries. TIMING.md copies Sage's table and says what it is not: a controlled solo baseline. The 25 values sum
|
||||
to 659 s, as stated.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Drop the dead Board `:9` replacements from P2. Port 9 is not a resource the two lanes share. On this host
|
||||
`net.ipv4.ip_unprivileged_port_start` is 1024, so no test, lane or seat can bind 127.0.0.1:9. A connection
|
||||
there is refused at once in both lanes. Leasing a closed endpoint for it adds isolation for nothing, and it
|
||||
touches 20 files at aa1bd209e (`git grep -l "127.0.0.1:9'"`: 17 under webui/tests plus stack-copy,
|
||||
launch-routes and bus tasks). I approved R9 in the design. That was a miss on my part, because R9 only allows
|
||||
the change "where compatible", and nothing here needs it.
|
||||
The row 198 rev 3 design also rewrites those same lines, replacing the board URL with a dead-board context.
|
||||
Whichever row lands second would redo the other's edit.
|
||||
Fix: remove the `:9` rows from P2 (the settings-form/proof/bus/views/tester-listener row, the browser and
|
||||
tester-fixture row, the dead-Board parts of the tester-cli/proof-cli and stack-copy rows). Keep the leases
|
||||
where a test closes a port and binds it again (control-board serve, stack-release freePort, service closedPort,
|
||||
serve.test :100 if it rebinds). Add a T1 preflight refusal for a listener on 127.0.0.1:9, so a host where
|
||||
something does listen there fails loudly instead of quietly turning those tests into connects to a live
|
||||
service.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Tell Sage that row 198 edits `stack-copy.test.mjs`, `pinned-copy.test.mjs` and the webui serve/preview tests
|
||||
too. Even with B1, those are the files where the two rows meet.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Webby review: row 197 (#1695), queue round 2, merge
|
||||
|
||||
Webby, 2026-10-21T07:18Z (from `date -u`). Author: Herb (herb-0034). Request comment 29961 (revs 2722-2723,
|
||||
posted as jarvis, sage-1815). This round only checks the merge (sage-1760, sage-1776). Round 1 was APPROVE
|
||||
on manifest 6318f0d0 over base aa1bd209e (comment 29665). Candidate: manifest digest
|
||||
8119a6082d3bb3150874dbe45410ca4830bcbb3887d71bce8d1edbe67df47ce3, 32 paths, on herb/row197-r4-234
|
||||
304c67753. Base c771a36f4 is its ancestor, and c771a36f4..304c67753 is that one commit.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (round 2), not from Herb's packet. It hashes to the
|
||||
digest above, and each path's sha256 from `git show 304c67753:<path>` matches its line.
|
||||
- Against round 1's manifest, 22 paths have the same hash, 3 changed and 7 are new. The new ones are
|
||||
Herb's agents/herb/work/queue-197 packet files. `git diff --name-only c771a36f4 304c67753` names only
|
||||
manifest paths.
|
||||
- Base drift: of round 1's 25 paths, only stack-release.test.mjs, webui service.test.mjs and
|
||||
scripts/test-discord.sh have a different blob at c771a36f4 than at aa1bd209e. Those are the 3 changed paths.
|
||||
- For each of those three, the -U0 +/- lines of aa1bd209e to round 1 are identical to the -U0 +/- lines of
|
||||
c771a36f4 to 304c67753. Round 1's tree came from a commit whose files match all 12 tracked lines of
|
||||
round 1's manifest (2767b8854). Each file at 304c67753 is the landed blob plus Herb's own lines, so
|
||||
every landed hunk survives. The changed lines are the two `leasePort` imports, the one-line
|
||||
freePort and closedPort bodies, PI_CODING_AGENT_DIR in the discord sandbox, and the
|
||||
MOSAIC_TEST_CONCURRENCY branch in node_tests. No assertion or timeout changed.
|
||||
- T46 (stack-release.test.mjs:913) keeps row 234's `host_grants` and `hosts` entries in the untimed list.
|
||||
- File modes are the same as round 1's: gate-lane.sh and test-discord.sh 100755, the rest 100644.
|
||||
- Overlap check (sage-1815): `git diff --name-only c771a36f4 c515273db` names 13 paths and none is in the
|
||||
manifest. So row 240's landing doesn't touch this candidate.
|
||||
|
||||
Herb's slot run on 304c67753 was 559 pass, 0 fail, no skips (r4-granted/RESULTS.md). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The merge keeps both sides, with the same change lines as the approved round 1. Round 1's notes stand as
|
||||
recorded. `leasePort` still drops the `closed` option at lane-fixtures.mjs:16, the way round 1 accepted it.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Webby review: row 197 (#1695), queue round 3 (r5), merge
|
||||
|
||||
Webby, 2026-10-21T08:13Z (from `date -u`). Author: Herb (herb-0036). Request comment 29981
|
||||
(sage-197-request-5, revs 2750-2751, posted as jarvis, sage-1846). This round only checks the merge past
|
||||
the 228 and 238 landings. Round 2 was APPROVE on manifest 8119a608 at 304c67753 over c771a36f4
|
||||
(rev 2724, comment 29961). Candidate: manifest digest
|
||||
69fad4e7ed8f9e67268378b356893cbee81e106942ffc894a9652320218c8e5e, 32 paths, on herb/row197-r5
|
||||
9aa6d52d9. That commit's one parent is e7078e6a8.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (round 3), not from Herb's packet. It hashes to the
|
||||
digest above, and each path's sha256 from `git show 9aa6d52d9:<path>` matches its line.
|
||||
- It has the same 32 paths as round 2's manifest. The only line that changed is
|
||||
packages/cli/tests/stack-release.test.mjs.
|
||||
- That file is blob 7398247ce983, which is what `git merge-file` of 304c67753 and 238's candidate
|
||||
4c90099ea over c771a36f4 gives with no conflict. I ran that preview before Herb built r5 (webby-0063).
|
||||
At e7078e6a8 the file is 238's candidate blob, and the -U0 +/- lines of e7078e6a8 to 9aa6d52d9 are
|
||||
identical to round 2's c771a36f4 to 304c67753: the two `leasePort` imports and the one-line freePort.
|
||||
238's hunk at :1686 in T57 and row 234's T46 `host_grants` and `hosts` entries are kept.
|
||||
- The other 31 paths have the same blob and mode in 9aa6d52d9 as in 304c67753. e7078e6a8..9aa6d52d9
|
||||
touches no path outside the manifest.
|
||||
- Overlap: the refactor tip is 4ced5e63d. e7078e6a8..4ced5e63d is one sessions commit that changes one
|
||||
path, docs/SESSIONS.md, which isn't in the manifest.
|
||||
|
||||
Herb's slot run of stack-release.test.mjs on 9aa6d52d9 was 196/0 in 501 s (sage-1846). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
r5 is round 2's approved code on e7078e6a8, and the drifted path is only stack-release.test.mjs.
|
||||
Round 1's notes stand as recorded. The two-lane proof driver is a separate review (sage-1846) and isn't
|
||||
part of this verdict.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Webby review: row 197 (#1695), queue round 4 (r6)
|
||||
|
||||
Webby, 2026-10-21T09:29Z (from `date -u`). Author: Herb. Request comment 30004 (sage-197-review-4b,
|
||||
sage-1896, mos-1162). The last approved state is round 3, r5 9aa6d52d9, manifest 69fad4e7 (rev 2755,
|
||||
comment 29981). Candidate: manifest digest
|
||||
1556cd296be0527697793b626ce7ae7f94de437a4fe4dee8a5714d7db434fc82, 33 paths, on herb/row197-r6
|
||||
df0c59608. That commit's one parent is fefd54e13.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (round 4), not from Herb's packet. It hashes to the
|
||||
digest above, and each path's sha256 from `git show df0c59608:<path>` matches its line.
|
||||
- It is round 3's 32 paths plus agents/herb/work/queue-197/r6-OVERLAP.json. Four paths differ in blob
|
||||
or mode from 9aa6d52d9: gate-lane.mjs, gate-lane.test.mjs, Herb's BUILD.md and the new
|
||||
r6-OVERLAP.json. The other 29 have the same blob and mode.
|
||||
- No manifest path changed between e7078e6a8 (r5's base) and fefd54e13. fefd54e13..df0c59608 touches
|
||||
no path outside the manifest.
|
||||
- packages/bus/tests/process.test.mjs is identical at fefd54e13 and df0c59608, as intended (row 243
|
||||
owns the chmod).
|
||||
- Overlap: the refactor tip is 38b83ea5c. fefd54e13..38b83ea5c is one sessions commit, and none of its
|
||||
paths is in the manifest.
|
||||
|
||||
Herb's focused run on the granted slot: gate-lane.test 13/0, process.test 8/0 (sage-1892). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The one code change is scripts/testing/gate-lane.mjs:46. Each step now spawns
|
||||
`/bin/bash -c 'umask 022; exec "$@"' gate-step <argv...>`. On Sage's three questions:
|
||||
|
||||
1. Argv quoting is kept. `gate-step` fills `$0`, so `"$@"` is exactly step.argv, with no word
|
||||
splitting or globbing. Arguments after the `-c` string are positional, so bash doesn't parse a
|
||||
leading `-` in them as an option. `node` and `bash` resolve through the step env's PATH, as
|
||||
spawn did before.
|
||||
2. Exit status and signals are kept. `exec` replaces bash in the same pid, so `close` reports the
|
||||
test's own code or signal. The process is still the detached group leader, so `process.kill(-pid)`
|
||||
in abort (:49) and cleanProcesses still reaches the whole group. identity() reads pid and
|
||||
/proc starttime (lane-state.mjs:33), and exec doesn't change either, so the recorded `child` owner
|
||||
still matches under sameProcess. A missing command now exits 127 from bash. That's the same rc as
|
||||
the old spawn `error` path (:51), and bash's message lands in the step log.
|
||||
3. The supervisor keeps 077. gate-lane.sh sets `umask 077` at :3 and execs node at :26. The new umask
|
||||
is set only inside the bash child. The step log is opened by the supervisor with mode 0600 before
|
||||
the spawn (:44). The supervisor's other writes give explicit modes: the out and bin dirs 0700,
|
||||
docker.json 0600, the guard shim 0700 (:177-181), the state root 0700 (:89). mkdtemp roots are 0700.
|
||||
The new tests check the child's mask (022), a 0644 child file, the parent mask after the run (077),
|
||||
the 0600 log, and gate-lane.sh's 0700 state dir and 0600 locks.
|
||||
|
||||
The step env still comes from cleanEnv (:109-115). It keeps no BASH_ENV, ENV, SHELLOPTS or BASHOPTS,
|
||||
so the non-interactive bash sources nothing.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N8. bash adds PWD, SHLVL and `_` to the env the test process sees. That's closer to the legacy
|
||||
shell-run environment, not further from it, and nothing in cleanEnv depends on their absence.
|
||||
|
||||
N9. Files a step makes with 022 are 0644. Under the 0700 temp root and out dir they stay private. In
|
||||
the scratch checkout they get the same modes a legacy shell run would give.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Webby review: row 197 (#1695), queue round 5 (r7)
|
||||
|
||||
Webby, 2026-10-21T11:29Z (from `date -u`). Author: Herb. Request comment 30036 (rev 2819, sage-1958,
|
||||
posted by Sage as jarvis). Conditions are from sage-1955. Round 4 was APPROVE on r6 df0c59608
|
||||
(rev 2773, webby-0068). Its proof then stopped at solo-a script-release, because Compose 5.5.1's
|
||||
`run` has no `--no-build` (#1695 c30031). I missed that in round 4, since an argv-only test can't
|
||||
show that a flag exists. Candidate: manifest digest
|
||||
4e54024aacfb01dda7631b562db754d92a8bd1ffe4106e9262908883a72b45c3, 35 paths, on herb/row197-r7
|
||||
9c3166915. That's one commit on b1fece6f0, which is on origin/refactor.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (round 5). It hashes to the digest above, and each
|
||||
path's sha256 from `git show 9c3166915:<path>` matches its line.
|
||||
- b1fece6f0..9c3166915 touches only manifest paths.
|
||||
- Against r6 df0c59608, five paths differ in blob or mode: lane-docker.mjs, gate-lane-image.test.mjs,
|
||||
Herb's BUILD.md, and the new r7-no-build.mutant.patch and r7-OVERLAP.json. The other 30 have the
|
||||
same blob and mode, as Herb says.
|
||||
- Base drift: fefd54e13..b1fece6f0 changes none of the 35 paths. On those 30, the -U0 +/- lines over
|
||||
b1fece6f0 equal r6's over fefd54e13.
|
||||
- Overlap: the refactor tip is 335bb3e30. b1fece6f0..335bb3e30 changes none of the 35 paths.
|
||||
- I read the lane-docker.mjs and test diffs, the mutant patch, Herb's r7 BUILD.md section, the
|
||||
docker.json writer (gate-lane.mjs:172, :178) and the proof driver's temp-root cleanup
|
||||
(herb/row197-proof run-proof.mjs).
|
||||
|
||||
Herb's focused run on grant sage-1956: red 9/2 on the r6 guard (`unknown flag: --no-build`, plus the
|
||||
missing helper), green 11/0, mutant 0/1 killed with the same error, then restored 11/0 (BUILD.md). I
|
||||
ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
On Sage's four conditions:
|
||||
|
||||
1. The guard owns the files. For `compose run`, guardDocker returns `compose -f <checkout>/compose.yaml
|
||||
-f <tempRoot>/compose-no-build.yaml run --pull never --no-deps ...` (lane-docker.mjs:36-37).
|
||||
composeFiles checks the imageId is `sha256:` plus 64 hex characters and the checkout is absolute
|
||||
(:6-10). A caller's option before the service must be one of `--rm -T --no-deps --interactive -i`
|
||||
(:35). So `-f`, `--file`, `--file=`, `--project-directory`, `--build`, `--pull` and `--no-build`
|
||||
there refuse. A top-level `compose -f ...` or `compose --project-directory ...` refuses, because
|
||||
argv[1] isn't `run` (:33, :39). The test at gate-lane-image.test.mjs:120-126 checks all of them.
|
||||
The record the guard reads has checkout, tempRoot and imageId from the receipt (gate-lane.mjs:172).
|
||||
That closes the r6 failure class for the record fields.
|
||||
2. The override goes at `shortRoot(run.tempRoot)/compose-no-build.yaml` (:9). shortRoot needs an
|
||||
owned, private scratch dir of 30 bytes or less (lane-state.mjs:10-16). The file is written `wx`
|
||||
at 0600 to a nonce name, then hard-linked into place. EEXIST keeps the existing file (:15-19). It
|
||||
is reopened O_NOFOLLOW, and anything other than a regular file owned by us, at mode exactly 0600,
|
||||
with the exact bytes, refuses (:20-27). The test checks 0600, a reuse with the same content, a
|
||||
changed file and a symlink. The symlink case leaves the target untouched. Cleanup: the proof driver
|
||||
removes a passed gate's temp root with `rmSync(..., {recursive:true})` after its ownership checks,
|
||||
and keeps a failed one. The override sits inside that root.
|
||||
3. The config check runs the installed binary: `docker compose -f ... -f ... config --format json`
|
||||
on a copy of the candidate compose.yaml, with made-up env and auth and an empty DOCKER_CONFIG
|
||||
(:103-119). It asserts that `services.mosaic-agent` has no `build` key and that `image` equals
|
||||
run.imageId. It also checks `--no-deps` and `--pull never`. The fixture imageId stands in for the
|
||||
receipt's. In a real run, the guard takes it from docker.json, which gate-lane.mjs:172 fills from
|
||||
receipt.imageId.
|
||||
4. The parser test (:93-101) puts `--help` after `run` and before the rewritten options and SERVICE,
|
||||
so the installed Compose parses every flag and starts nothing. The mutant patch puts `--no-build`
|
||||
back at :37 only. Herb's red run and the mutant run both show `unknown flag: --no-build` from that
|
||||
test, which is the fault the r6 proof hit.
|
||||
|
||||
compose.yaml has one service, mosaic-agent, with `build:` and `image: ${MOSAIC_IMAGE_TAG...}`, and the
|
||||
repo has no compose.override.yaml. With `build: !reset null` and `--pull never`, `run` can't build or
|
||||
pull. A missing image fails.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N10. No test starts a container. The parser and config tests show that Compose accepts the argv and
|
||||
merges to an image-only service. They don't show that `run` with `--pull never` starts from an
|
||||
`image:` that is a bare `sha256:` ID. Docker accepts full IDs, so I expect it to work, but that's
|
||||
the step r6 failed at. Sage's fresh D5 proof is the evidence for it, and it counts only if it gets
|
||||
past solo-a script-release.
|
||||
|
||||
N11. `-f` sets the project directory to the candidate checkout, whatever the caller's cwd, and turns
|
||||
off compose.override.yaml auto-loading. Every current caller runs from the checkout:
|
||||
mosaic-task.mjs:397-399 (`cwd: PROJECT_ROOT`), hello.sh:24, verify.sh:45 and agent.sh:208. So nothing
|
||||
changes today. A future caller run from a copy would get the candidate's compose.yaml, not the copy's.
|
||||
|
||||
N12. A failed gate's kept temp root still holds the override. It contains only the image ID, so
|
||||
nothing secret is left behind.
|
||||
@@ -0,0 +1,59 @@
|
||||
# Webby review: row 197 proof driver, a5b636750 (proof 4 watcher change)
|
||||
|
||||
Webby, 2026-10-21T15:19Z (from `date -u`). Author: Herb (herb-0090). Requested per sage-2029, sage-2034
|
||||
and sage-2035, after mos-1174. This is a review of the proof driver only. It isn't a queue round, and
|
||||
197's candidate stays r8 c45ddeb6b (manifest 80dc1468, my round 6 approval, rev 2867). Candidate:
|
||||
herb/row197-proof a5b636750, one commit on b8461266c.
|
||||
|
||||
What I checked, without runs:
|
||||
- b8461266c..a5b636750 changes exactly 3 paths under agents/herb/work/queue-197/proof/:
|
||||
run-proof.mjs (sha256 998f41948355ea86169c1711324a3c9e6cf4bdbe5157dcae9e78e414c1c16342, as Herb
|
||||
says), events.test.mjs (new, 7a40bb46ed4e) and BUILD.md (5ede409c4480). All three are 100644.
|
||||
- None of 197's 37 manifest paths is in that range. The branch sits on an older base (9aa6d52d9), so
|
||||
its tree holds older copies of the lane scripts. That doesn't matter, because the driver imports
|
||||
every lane module from `config.launcherRoot` (run-proof.mjs:85-89). It also refuses a launcher
|
||||
whose HEAD isn't `config.launcherCandidate` or whose checkout is dirty (:81-82).
|
||||
- I read the whole diff and r8's cleanupProject (lane-docker.mjs:41-52 at c45ddeb6b).
|
||||
|
||||
Herb's grant sage-2035 results (by BUILD.md and herb-0090): events.test.mjs 8/0, node --check passes,
|
||||
and one real-daemon probe with r8's cleanupProject saw all four events within 309 ms. Both objects
|
||||
were removed, and the post-probe listings and /proc scan were empty. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- The defect. Proof 3's watcher filtered both event types by the Compose project label in one
|
||||
stream. Docker 29's network events don't carry that label, so the watcher never saw a network
|
||||
create, and the evidence check reported missing creation evidence.
|
||||
- Network events (:12-14). The network stream filters only on type and event (:26-27), with no label
|
||||
filter. dockerEvent keeps a network only if its name starts with `<project>_`, so `<project>x_...`,
|
||||
`<project>-...` and the bare project don't match (the test at events.test:15-21). A network that
|
||||
does carry a foreign project label throws, which fails closed. The project is
|
||||
`mg197-[ab]-<12 hex>` (:7), checked before any watcher starts (:21). A foreign network would have
|
||||
to use our random id to match.
|
||||
- Container events (:15). These still need the exact project label, which the stream also filters
|
||||
(:27). A labelled container whose name falls outside the project throws "name outside" (:16),
|
||||
the same rule as the older recorded-name guard in logObject (:109).
|
||||
- Shared code. The proof's ensureWatchers now calls the same watchDockerProject (:116). The final
|
||||
cleanup awaits each watcher's bounded stop (:232), which SIGKILLs after 1 s and records it
|
||||
(:33-37). Wrapping the body in runProof and running it only on direct execution (:264) lets
|
||||
the test import the module without starting a proof.
|
||||
- The preflight (:39-64, :209-211). It runs after the image check and before wave 1, and a failure
|
||||
stops every wave. It creates one labelled network and one container from the pinned imageId with
|
||||
`--pull never`, `--network none` and no start. The test asserts no `start` or `build` argument
|
||||
(events.test:46). Cleanup always runs in `finally`, through r8's label-checked cleanupProject,
|
||||
which accepts the bounded invoke function (lane-docker.mjs:43). Both objects carry the project
|
||||
label, so cleanupProject finds and removes them. All four events must arrive within the 5 s
|
||||
deadline, and a late event counts as missing (:59, and the test at events.test:57-60). The result
|
||||
goes into summary.json and docker-summary.json.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. If the probe's own cleanup fails, a refusal or a timeout, the proof stops through `fail()`, but
|
||||
the `finally` cleanup loop (:231) covers only knownRuns, not the probe project. Leftover probe objects
|
||||
would stay until someone removes them by hand. The failure is loud and watcher-probe-plan.json
|
||||
(:209) names both objects, so nothing goes unseen. Adding the probe project to that loop would make
|
||||
the cleanup retry once.
|
||||
|
||||
N2. Herb asks Sage to update proof-phase.sh's digest pin to 998f4194...6342. That file isn't in this
|
||||
commit, so I haven't checked the pin. Sage should check it against the run-proof.mjs hash before
|
||||
proof 4.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Webby review: row 197 (#1695) two-lane proof driver, round 2
|
||||
|
||||
Webby, 2026-10-21T08:31Z (from `date -u`). Author: Herb (herb-0053). Requested by Sage (sage-1858,
|
||||
sage-1864); not a queue candidate. Scope: B1 and N3 only, from `git diff 8ae9d8acd b8461266c`. Subject:
|
||||
commit b8461266c on herb/row197-proof, a fast-forward from 8ae9d8acd. run-proof.mjs sha256 is
|
||||
ace376a9a333..., which I checked. The diff touches run-proof.mjs (+39 -14) and adds proof/BUILD.md. I
|
||||
didn't review BUILD.md beyond noting it now exists. Static review, I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. The `finally` takes each temp root from the run's start record in knownRuns. It never
|
||||
scans scratch (run-proof.mjs:174-193). Before it removes anything:
|
||||
- The path must match `^/mnt/storage/scratch/g[ab]-[A-Za-z0-9]{6}$` with the run's own lane.
|
||||
- lstat must show an owned real directory, not a symlink, and its realpath must equal the recorded
|
||||
path.
|
||||
- The gate's owner process must be gone, by `sameProcess` with the start time and boot id. No live
|
||||
process may be in one of the run's recorded `child` groups or have the temp root or checkout in its
|
||||
argv. If the process inventory fails, the root is kept and the run fails.
|
||||
- The last terminal record must say clean. Otherwise the root is kept and counted as a cleanup error.
|
||||
|
||||
A passed gate's root is then removed (:190). A failed gate's root is kept for diagnosis. Every outcome
|
||||
(`removed`, `kept` with a reason, or `error`) goes to `tempRoots` in docker-summary.json (:194), and any
|
||||
error sets exit 1. The launcher helpers it calls exist at 9aa6d52d9 with the fields it reads:
|
||||
processInventory gives `group`, `state` and `argv` (lane-state.mjs:48-50), and gates record
|
||||
`{kind:'child',group}` (gate-lane.mjs:46).
|
||||
|
||||
N3 is fixed. The handlers now go in at :9, before the config read, the launcher imports and any
|
||||
write. `abortOnSignal` is a no-op until :97 makes it abortActive. Preparation moved inside the `try`
|
||||
(:142). Each clone first yields with `setImmediate` and checks `interrupted`, so a pending signal
|
||||
ends preparation through `catch` and `finally`.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N5. Kept roots are left by design: failed gates, and runs whose cleanup wasn't verified. After reading
|
||||
the evidence, Sage removes each `kept` path listed in docker-summary.json `tempRoots`.
|
||||
|
||||
N6. One small orphan path remains in the launcher, not the driver. gate-lane.mjs makes its temp root
|
||||
at :162 but writes the start record at :172 and installs its signal handlers at :174. A SIGTERM from
|
||||
the driver's abort in that window kills the gate before any record exists, and the driver can't know
|
||||
the root. It's an empty or near-empty `g<lane>-` dir in scratch. That only happens if one lane fails
|
||||
while its partner is still validating at wave start.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Webby review: row 197 (#1695) two-lane proof driver
|
||||
|
||||
Webby, 2026-10-21T08:22Z (from `date -u`). Author: Herb. Requested by Sage (sage-1850); not a queue
|
||||
candidate. Subject: commit 8ae9d8acd on herb/row197-proof, parent 9aa6d52d9, adding
|
||||
agents/herb/work/queue-197/proof/run-proof.mjs (176 lines, sha256 b8bb3df6..., which I checked) and
|
||||
config.example.json. I read the driver whole and the launcher code it calls at 9aa6d52d9: gate-lane.sh,
|
||||
gate-lane.mjs runGate and cleanEnv, lane-docker.mjs guardDocker and cleanupProject, lane-state.mjs
|
||||
shortRoot, and lane-evidence.mjs summarizePairs. Static review, I ran nothing.
|
||||
|
||||
Sage named proof/BUILD.md for the procedure. No such file exists at 8ae9d8acd, and
|
||||
agents/herb/work/queue-197/BUILD.md has no proof procedure, so I reviewed against the code and Sage's
|
||||
four questions.
|
||||
|
||||
## Verdict: CHANGES (one blocker)
|
||||
|
||||
### Sage's four questions
|
||||
|
||||
1. Stops at the first failure: yes. A 100 ms monitor reads new bytes in each gate's out/*.log and stops
|
||||
at the first failure line (run-proof.mjs:107, :116). A gate that exits nonzero sets `firstFailure`
|
||||
(:151). Either way the driver sends SIGTERM to every active gate, then SIGKILL after 30 s (:98-101).
|
||||
The wave check (:154) throws, so no later wave starts. Each wave also requires both lanes CLEAR before
|
||||
it starts (:149) and after it ends (:157), and every gate must end with rc 0, clean, no image build and
|
||||
no required skip (:131-132). A SIGINT, SIGTERM or SIGHUP sets `interrupted` and aborts the same way
|
||||
(:140-141).
|
||||
2. Cleanup: containers and networks yes, temp dirs no (B1). The gate cleans its own project
|
||||
(gate-lane.mjs:190, :192). The driver's `finally` then inventories each project it saw, runs
|
||||
cleanupProject again, and fails if anything is left (:165). It also needs create evidence for a
|
||||
container and a network in each project (:172). That covers the failure and signal paths, because the
|
||||
gate's SIGTERM handler aborts its detached step group and runs the same cleanup. compose.yaml uses
|
||||
bind mounts only, and lane-docker.mjs allows only `compose run`, so no named volume can be created.
|
||||
3. Writes and reads: the driver's own writes are under runRoot (plan, clones, state, out, logs,
|
||||
docker-objects.jsonl, summaries), with one exception, B1. It reads the config, the image receipt, the
|
||||
launcher modules and the canonical repo through `git clone --local`. Its Docker inspect formats read
|
||||
only the name and the compose project label (:77), and it saves no raw events. It reads no secret and
|
||||
no seat scratch. Gate steps get cleanEnv's allowlist (gate-lane.mjs:108-112), not the driver's
|
||||
environment.
|
||||
4. Concurrency and isolation evidence: enough. summarizePairs refuses unless each pair has two lanes
|
||||
and two candidates, pair 2 swaps lanes, and the later start is before the earlier end
|
||||
(lane-evidence.mjs). summary.json keeps each gate's terminal receipt with startMs and endMs. Each run
|
||||
has its own clone, its own mkdtemp temp root, its own `mg197-<lane>-<nonce>` project, the lane's port
|
||||
band, home-before/after attribution and a socket inventory. docker-objects.jsonl records each Docker
|
||||
object against its project.
|
||||
|
||||
### Blocker
|
||||
|
||||
B1. Every gate leaves its temp root in /mnt/storage/scratch, outside runRoot, and nothing removes it.
|
||||
The driver passes no `--temp-root` (run-proof.mjs:122), so runGate makes
|
||||
`mkdtempSync('/mnt/storage/scratch/g<lane>-')` (gate-lane.mjs:162). It removes that dir only when
|
||||
validation fails (:170). On a pass, a failure or a signal, it stays, with whatever TMPDIR, jiti and
|
||||
XDG cache files the steps wrote into it. The proof's eight gates leave eight of these dirs. Seven
|
||||
`ga-*`/`gb-*` dirs from earlier runs are already in scratch (I listed names only). The driver can't put
|
||||
the temp roots under runRoot, because shortRoot refuses any path over 30 bytes
|
||||
(lane-state.mjs:13). Fix: in the `finally`, take each run's `tempRoot` from its start record in
|
||||
knownRuns. Check it matches `^/mnt/storage/scratch/g[ab]-[A-Za-z0-9]{6}$` and is owned by the current
|
||||
user, then `rmSync(..., {recursive:true})` it. Record each removal, or its error, in
|
||||
docker-summary.json, and fail on any error. Do this after the gate has exited and the process check
|
||||
has run. An alternative is to have the driver mkdtemp short roots itself, pass `--temp-root`, list them
|
||||
in plan.json, and remove them the same way.
|
||||
|
||||
### Non-blocking
|
||||
|
||||
N1. Each of the 8 runs makes a `--no-hardlinks` clone under runRoot. The repo's .git is 421 MB, so
|
||||
together they can take a few GB. /mnt/storage has 206 GB free. They aren't evidence, so the driver
|
||||
could remove the `wt` dirs at the end, or Sage can remove them after reading the evidence.
|
||||
|
||||
N2. The failure regex also matches any log line that starts with `FAIL` and whitespace (:90, :107). A
|
||||
passing step that prints such a line would stop a good proof. That fails safe but costs a rerun. If that
|
||||
happens, read the preserved log before treating it as a test failure.
|
||||
|
||||
N3. Before line 141 the driver has no signal handlers. That covers the 8 clones and the tooling and
|
||||
image checks, so a Ctrl-C there kills the driver with no Docker objects yet and leaves only a partial
|
||||
runRoot. A SIGKILL of the driver mid-wave leaves gates running. Use `gate-lane.sh recover` then.
|
||||
|
||||
N4. Outside the driver: compose.yaml defaults PI_AUTH_FILE to the host's ~/.pi/agent/auth.json, mounted
|
||||
read-only, and cleanEnv neither keeps nor sets PI_AUTH_FILE. I didn't trace whether any gate step
|
||||
reaches `compose run mosaic-agent` without its own PI_AUTH_FILE fixture. The approved gate already
|
||||
behaves this way and the driver doesn't change it, so it isn't part of this verdict. Sage may want it
|
||||
checked before calling the proof secret-free.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Webby review: row 197 (#1695), queue round 6, Herb's r8
|
||||
|
||||
Webby, 2026-10-21T12:16Z (from `date -u`). Author: Herb. Request comment 30054 (revs 2865-2866,
|
||||
sage-1989, posted by Sage as jarvis). Merge scope per mos-1162. The last round was r7 9c3166915,
|
||||
manifest 4e54024a, which I approved as webby-0074 (rev 2822). Its D5 proof stopped in pkg-cli. Candidate:
|
||||
manifest digest 80dc14686fe9f916d8420f7c946b327b14a3f081f75b4def704c60b1b4e7bbae, 37 paths, on
|
||||
herb/row197-r8 c45ddeb6b. That's one commit on 5c6f73ed7.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 197` (last round), not from the scratch path. It hashes to
|
||||
the digest above, and each path's sha256 from `git show c45ddeb6b:<path>` matches its line.
|
||||
- 5c6f73ed7..c45ddeb6b touches exactly the 37 manifest paths.
|
||||
- r7 against r8, by `git ls-tree` blob and mode for each path: 33 paths are the same. Two change,
|
||||
gate-lane-image.test.mjs and BUILD.md. Two are new, r8-OVERLAP.json and
|
||||
r8-record-lookup.mutant.patch. That's 35 r7 paths, with 33 unchanged, not 35. Herb's BUILD.md
|
||||
gives the same count. The production guard, lane-docker.mjs, has the same blob as in r7.
|
||||
- Base drift: b1fece6f0..5c6f73ed7 (r7's base to r8's) changes none of the 37 paths, as
|
||||
r8-OVERLAP.json says.
|
||||
- Overlap: refactor is now f8276c5eb. 5c6f73ed7..f8276c5eb changes queue-218's BUILD.md,
|
||||
SESSIONS.md and the two case2 sheets, none of them in the manifest.
|
||||
- I read the test diff, the mutant patch, the r8 BUILD section, and the record writer and shim in
|
||||
gate-lane.mjs and lane-docker.mjs.
|
||||
|
||||
Herb's slot checks (herb-0078, 12:09:28Z, by BUILD.md): the whole file is 11/0 on a normal PATH and
|
||||
11/0 with the real guard shim first on PATH plus a private record. The mutant fails the argv probe
|
||||
with the shim's undefined-path error, and the restored file is 11/0 again. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- The cause. composeFixture's env (gate-lane-image.test.mjs:100-101) keeps the lane's PATH, whose first
|
||||
entry is `<out>/bin` (gate-lane.mjs:113), but drops MOSAIC_GATE_DOCKER_RECORD. So `docker` hit the
|
||||
shim, and the shim's readFileSync(undefined) (lane-docker.mjs:57) exited 2. That matches the r7
|
||||
proof stop.
|
||||
- The fix. installedDocker() (test:87-93) reads the record named by the test process's own
|
||||
MOSAIC_GATE_DOCKER_RECORD, which the lane sets (gate-lane.mjs:112). It takes `.docker` and asserts
|
||||
it's an absolute string. The lane writes that field from `which docker` at gate-lane.mjs:170,
|
||||
before it creates the bin dir (:177) or writes the record (:178). That's the same field the shim
|
||||
itself executes (lane-docker.mjs:58). Outside a lane it keeps `docker`. Only the two read-only
|
||||
probes use it, `--help` at :109 and `config` at :120. The fixture env is unchanged, and no guard or
|
||||
allowlist code changes.
|
||||
- Other callers. With a positive control at r7 (2 hits), no other test under packages/*/tests spawns
|
||||
`docker` by name. The `docker info` checks in test-task.sh and test-release.sh inherit the full lane
|
||||
env, record included, so the shim can read it.
|
||||
- Sage's question, whether the fix can pass with the shim missing from PATH. It can, and it should.
|
||||
Without the shim, both `docker` and the record's path reach the real binary. So a pass by itself
|
||||
proves nothing. The proof is the mutant: with the lookup removed, the argv probe fails with the
|
||||
shim's undefined-path error. That error comes only from lane-docker.mjs:57, so the run had the shim
|
||||
first on PATH. In the D5 proof, the lane always puts the shim first (gate-lane.mjs:113), so pkg-cli
|
||||
there tests the fix for real.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The mutant receipt in BUILD.md names only the argv probe ("installed-Compose argv regression
|
||||
0/1"). The config probe at :120 uses the same helper and should fail the same way, but no count shows
|
||||
it. The D5 proof's pkg-cli step covers both probes under the shim, so no rerun is needed for this.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Webby review: row 197 design r2 (#1695)
|
||||
|
||||
Webby, 2026-10-20 (from `date -u`). Author: Herb. Subject: `agents/herb/work/queue-197/DESIGN.md` at
|
||||
6ad25d745 on herb/row197, sha256 732ee0e2 (verified). Read as the diff from r1 (85a6eb0a9), plus
|
||||
both new probe logs. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. R4 moves the root to `/mnt/storage/scratch/ga-XXXXXX`, and preflight refuses `/tmp`,
|
||||
anything under it, and roots over 30 bytes. D3's socket example uses that root (92 bytes). D4.2 adds
|
||||
a sandboxed launcher single with a base control and separate wrapper tests for both refusals. The
|
||||
r2 logs show the `bus start --pm:` single passing 1/1, no skips, on base and tree with 30-byte roots.
|
||||
That test starts the real sandboxed runner. R6 now cites sandbox.mjs:112,115.
|
||||
|
||||
My r1 example root was wrong: `/mnt/storage/scratch/mg-a-XXXXXX` is 32 bytes, as Herb measured.
|
||||
|
||||
N1-N5 are in: the bands in R8 with a startup refusal, HOME in R11 and the D4.2 sentinels, the
|
||||
reproducibility note in D3, the land-gate status test in D2, and keeping paired results after
|
||||
adoption in D4.
|
||||
|
||||
## Non-blocking, for the manifest
|
||||
|
||||
N1. The `~/.claude`, `~/.pi` and `~/.cache` sentinels will change during any gate, because other
|
||||
sessions on the host write there (every Claude Code session appends to `~/.claude/projects`). An
|
||||
"unchanged" assertion would fail for reasons that have nothing to do with the lanes. Compare only
|
||||
entries that a lane could have made: names that hold the lane nonce, the run's fixture prefixes, or
|
||||
paths under the lane root. Or record the listings as evidence without asserting on them.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Webby review: row 197 design (#1695, two gate lanes)
|
||||
|
||||
Webby, 2026-10-20 (from `date -u`). Independent reviewer. Author: Herb.
|
||||
Subject: `agents/herb/work/queue-197/DESIGN.md` at 85a6eb0a9 on herb/row197, sha256 64a429a3
|
||||
(verified). Brief `docs/plans/2026-10-20_second-gate-lane.md`. Static review. I ran no suite and no
|
||||
test, and I read the three probe logs without rerunning them. Code read at the design's base
|
||||
c6f528a32. Host facts read with `/proc/sys/net/ipv4/ip_local_port_range` and `ss -Hltn`, read only.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
One blocker. The inventory is careful and most of it holds. The temp root it picks breaks the
|
||||
sandboxed runner tests on this host.
|
||||
|
||||
## Citations checked
|
||||
|
||||
These hold: common.sh:24-34 (load_release), test-release.sh:10-15 (RELEASE backup and restore),
|
||||
mosaic-task.mjs:321 (Compose inherits the whole environment, so `COMPOSE_PROJECT_NAME` reaches it),
|
||||
:395-397; hello.sh:24; agent.sh:208; release.sh:122; compose.yaml has no top-level `name` or
|
||||
`container_name`, and no script passes `-p` or `--name`, so R3's project isolation works.
|
||||
Containerfile:18-25 copies exactly the inputs D3 fingerprints. No `.dockerignore` exists.
|
||||
claim.mjs:50-55 and cohort.mjs:111 name scopes from the random claim; the impostor test
|
||||
(cohort.test.mjs:484) reuses its own recorded unit. No literal nonzero `listen()` in any src or test
|
||||
(R7). thread-launch.test.mjs runs its adapter inside bwrap (:4, :32), so the adapter's
|
||||
`$HOME/.claude/projects` write (fixtures/thread-adapter.mjs:14-20) lands in the run's home/.
|
||||
|
||||
One miss: R6 cites sandbox.mjs:118-120 for the private /tmp. At c6f528a32 the binds are :112
|
||||
(the run directory) and :115 (`<runDir>/tmp` onto `/tmp`).
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. R4's temp root under `/tmp` hides every sandboxed run directory. sandbox.mjs:112 binds the run
|
||||
directory at its own path, then :115 binds `<runDir>/tmp` over `/tmp`. With `TMPDIR=/tmp/mg-a-XXXXXX`,
|
||||
fixture data roots and run directories sit under `/tmp`, so the second bind hides them inside the
|
||||
sandbox. bwrap then fails with `Can't chdir to /tmp/mosaic-launch-.../launches/acme/<run>`, and the
|
||||
launcher reports `runner ... did not start`. Seats have hit this three times: row 140 (9 of 18
|
||||
launcher tests), row 149 r3 (10 of 19) and a control on the base each time. D3's own socket example
|
||||
(`/tmp/mg-a-ABCDEF/mosaic-cli-ABCDEF/data/launches/...`) is one of these paths. R6's "Host TMPDIR
|
||||
must not be ... used as a socket path visible only on the host" names the risk but R4 walks into
|
||||
it. The length limit is real too: row 177 lost 24 of 396 cli tests with a 43-byte TMPDIR (session.mjs
|
||||
refuses `tools.sock` past 107 bytes), and passed at 30 bytes. Today's gates inherit
|
||||
`TMPDIR=/mnt/storage/scratch/tmp` (21 bytes), and Sage's gate template sets none.
|
||||
Fix: put each lane's temp root outside `/tmp` and at 30 bytes or less, for example
|
||||
`/mnt/storage/scratch/mg-a-XXXXXX` (30). Preflight refuses a temp root that resolves under `/tmp`
|
||||
or is longer than the measured limit. Redo D3's socket example with that root (the tools.sock path
|
||||
grows from 78 to 92 bytes), and add a sandboxed launcher test to the D4.2 single-file probes, run
|
||||
with the lane root, with a base control.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Name R8's bands. Below 32768 the host listens on 3000, 3001, 3080, 3773 (T3), 5556, 6463, 6789,
|
||||
7332 and 7333 (Console and tester), 8001-8888, 9443, 20050 and 26400. Nothing listens above 60999,
|
||||
and `ip_local_reserved_ports` is empty. Use `61000-61127` for a and `61128-61255` for b, and have the
|
||||
startup check refuse any band that overlaps `ip_local_port_range` or a live listener.
|
||||
N2. R11 leaves HOME as the real home for both lanes. Nothing I found writes there outside a sandbox
|
||||
(claim.test.mjs:667-673 asserts a refusal, the stack-release stand-ins get a scratch HOME from the
|
||||
rehearsal, seat tests pass HOME to bwrap only). Say that in R11, and add `~/.claude`, `~/.pi` and
|
||||
`~/.cache` listings to D4.2's foreign sentinels so a new test that writes there shows up.
|
||||
N3. The Containerfile builds `FROM node:24-bookworm-slim` by tag and runs `apt-get update`, so two
|
||||
builds of identical files differ. D3 already relies on the pinned image ID. Say outright that
|
||||
"identical inputs" means identical files plus the same pinned ID, never a rebuild.
|
||||
N4. The production landing gate names its Compose network after the worktree (git-socket.mjs:239-246)
|
||||
and drops lane variables (landing.mjs:63-90). R17 treats it as LEGACY BUSY, which is right. Add a
|
||||
status test where a land gate runs beside a lane gate.
|
||||
N5. Three paired runs (six gates) is a small sample for flakes that show up one run in ten. Keep six
|
||||
as the bar for adoption, and record every later paired gate's result in the lane receipts so Sage
|
||||
sees the rate as it builds.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Webby review: row 198 design rev 3 (#1696)
|
||||
|
||||
Webby, 2026-10-20T19:36Z (from `date -u`). Author: Bushroot. Subject: `agents/bushroot/work/queue-198/DESIGN.md`
|
||||
at 7e2ead7f3, sha256 51343813...8302 (verified). I read the whole file and checked it against my rev 1 review
|
||||
(REVIEW.md) and sage-1372. Code read at the base, aa1bd209e. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
My rev 1 B1-B3 are fixed. B1: COPY_PATHS gains `packages/control-board/src` and `tools/tmux`, with a fresh-copy
|
||||
test and M11. B2: the `--board` drop in `console_for_target`, with M12 and M13. B3: both POSTs sit above
|
||||
serve.mjs:557-558 behind `send !== null && named`, loopback refuses them with 403, and M10 covers it. N1-N5 are in.
|
||||
The sage-1372 rulings read right. At aa1bd209e, webui-service.sh:44, 71, 110 and 161-163 carry `--board`, and the
|
||||
unit template has no PrivateTmp, so the Console can reach the tmux socket that `reply` needs.
|
||||
|
||||
One new blocker, which comes from moving the board in-process.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. In-process, the board's child calls block the Console's event loop. `scan` checks each seat with
|
||||
`tmuxInspect`, which runs `spawnSync("tmux", ..., { timeout: 5000 })` (control-board scan.mjs:196-200).
|
||||
`replyToRow` runs `agent-send.sh` through `spawnSync` with a 15 s timeout (serve.mjs:68, :94). Today those block
|
||||
only the 7331 process. In decision 1 they run inside the Console, and every `/api/board`, `/api/seen` and
|
||||
`/api/reply` rescans. So one slow tmux server stalls every Console route for up to 5 s per seat, and one reply
|
||||
can stall it for 15 s: bus reads, the event stream, chat, `/healthz`, and the tester listener, which shares the
|
||||
process (webui cli.mjs:91). The Console already avoids this for the bus. webui bus.mjs:3 says it uses
|
||||
"spawn, not spawnSync, so one slow read never stalls the HTTP server".
|
||||
Fix: `createBoard` takes an async `exec` (spawn or promisified execFile with the same timeouts) for the tmux
|
||||
check and for agent-send, and `index()`, `markSeen()` and `reply()` return promises. The board's sync exports
|
||||
stay for `scan` on the CLI. Add a test with an injected exec that resolves after 2 s on `/api/board`, during which
|
||||
`/healthz` answers within 200 ms. Add a mutant that swaps in a blocking exec, which that test kills.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Decision 8's last two sentences put the 7331 fallback on the wrong copy. A copy that doesn't parse
|
||||
`--board` is a new one, with no board URL at all. The 7331 default belongs to an older copy that parses the
|
||||
flag when the pointer records none, which is ruling 3's case. Rewrite those sentences.
|
||||
N2. `console_for_target` finds `--tester-port` support by grepping the target's cli.mjs (stack-release.sh:670).
|
||||
If the `--board` check copies that, a cli.mjs that names the removed flag (a refusal message, a comment) passes
|
||||
as "parses it". Pick a check that a refusal can't match, and give M12 a target whose cli.mjs mentions `--board`
|
||||
only in a refusal.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Webby review: row 198 design rev 4 (#1696)
|
||||
|
||||
Webby, 2026-10-20T19:42Z (from `date -u`). Author: Bushroot. Subject: `agents/bushroot/work/queue-198/DESIGN.md`
|
||||
at 9af75dc72, sha256 5ef6436f...82f1 (verified). Read as the diff from rev 3 (7e2ead7f3), against the base
|
||||
aa1bd209e. Static review. I ran nothing.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
B1 is fixed. Decision 10 makes every child call that `createBoard` reaches async. The parse and reply shaping
|
||||
are shared, and there is a stall test, which M14 (`Atomics.wait`) is a fair mutant for. N1 is fixed. Decision 8
|
||||
now puts the 7331 fallback on the older copy. The `--board` half of N2 is right. `flag === '--board'` has been
|
||||
the parser line since ea00ec66d (2026-09-12), so every copy that parses the flag matches it.
|
||||
|
||||
## Blocker
|
||||
|
||||
B1. Leave the `--tester-port` check alone in this row. Decision 8 swaps the grep at stack-release.sh:670 for
|
||||
`parses_flag`, which looks for `flag === '--tester-port'`. The existing stack-release fixtures don't contain
|
||||
that text. stack-release.test.mjs:265 writes the target cli.mjs as `// serve accepts '--tester-port'` and
|
||||
`// serve accepts '--proof-header' (row 178)`. Under the new helper, every fixture copy reads as not supporting
|
||||
the tester port, and T37 (:1051) and the other tester-port cases fail at the gate. The file table and the test
|
||||
line list don't name that fixture change. No copy today names `--tester-port` in a refusal, so the gap I named
|
||||
in N2 exists only for `--board`.
|
||||
Fix: use `parses_flag` for `--board` only and leave :670 as it is. Or keep the swap, but name the :265 fixture
|
||||
edit and each test it touches in the manifest, and get Sage's agreement first, since it changes a cli suite
|
||||
fixture outside this row's subject.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Coalescing in `index()` can serve a stale mark. `markSeen` writes the mark and then rescans
|
||||
(control-board serve.mjs, the `/api/seen` branch). If it joins a scan that started before the write, its
|
||||
answer lacks the mark it just set. Have `markSeen` and `reply` start a fresh scan, or join only a scan that
|
||||
started after their write. Add that case to board.test.mjs.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Webby review: row 198 design rev 5 (#1696)
|
||||
|
||||
Webby, 2026-10-20T19:47Z (from `date -u`). Author: Bushroot. Subject: `agents/bushroot/work/queue-198/DESIGN.md`
|
||||
at 3f71b359b, sha256 1503f764...a32b (verified). Read as the diff from rev 4 (9af75dc72), which touches only
|
||||
DESIGN.md. Static review. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. `parses_flag` is used only for `--board` (`console_for_target` and rollback). The
|
||||
`stack-release.sh:670` grep and the `stack-release.test.mjs:265` fixtures stay as they are, and the
|
||||
`--tester-port` test row is gone.
|
||||
|
||||
N1 is fixed. `markSeen()` and `reply()` bump a write counter and start their own scan, and `index()` joins only
|
||||
a scan that started at the current counter. The new board.test.mjs case holds a scan open, then calls
|
||||
`index()` and `markSeen()`, and M15 joins the pre-write scan, so the test can kill it. The gate line reads
|
||||
M1-M15.
|
||||
|
||||
The sequencing note fits sage-1386 and row 197's P3. 198 rebases onto 192 and 197, leaves the two overlap lines
|
||||
(serve.test.mjs:100, preview.test.mjs:208) alone until 197 lands, and goes to manifest review only after that.
|
||||
|
||||
## Non-blocking, for the build
|
||||
|
||||
N1. Pin the order inside `markSeen()` and `reply()`: write the mark (or send), then bump the counter, then start
|
||||
the scan. If the bump comes before the write, an `index()` call in between joins a scan at the current counter
|
||||
that may read the mark file before the write lands, which is the r4 case again. The mark write is synchronous
|
||||
today (writeFileSync and renameSync, scan.mjs:431-432), so this holds if the code keeps that order. Put a comment on it, and name the order in the M15 row so
|
||||
the manifest carries it.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Webby review: row 198 inventory and design (#1696, retire the control board writer on 7331)
|
||||
|
||||
Webby, 2026-10-20 (from `date -u`). Independent reviewer. Author: Bushroot.
|
||||
Subjects: `agents/bushroot/work/queue-198/INVENTORY.md` at 004f133bd (sha256 76b630ce...c877) and
|
||||
`DESIGN.md` at c8fe23a37 (sha256 c2ea3df5...cb08), both verified. Brief read at a31f1a0f8 (blob
|
||||
11977a5cb, with Mos's call mos-1019). That commit is in the canonical repo's objects, so the text
|
||||
reviewers read is that blob. Static review, code read at c8fe23a37. I ran no tests.
|
||||
|
||||
## INVENTORY.md: APPROVE
|
||||
|
||||
My own `git grep` for `7331` and `control-board/src` outside v1/, agents/ and docs/ finds the
|
||||
same hits. The five webui files that import `startServer` (fixture.mjs:6, history-fixture.mjs:8,
|
||||
discord.test.mjs:6, relaunch.test.mjs:6, return-flow.test.mjs:13) and flows.test.mjs:15
|
||||
(`replyToRow`) are right. No source under `packages/webui/src` imports the board. The proxy rows
|
||||
hold: serve.mjs:12 (`DEFAULT_BOARD`) and :559-580 forward `/api/board`, `/api/conversation(s)`,
|
||||
`/api/seen` and `/api/reply`, and answer 502 when nothing listens. So the board view is dark live
|
||||
today. No test pins a hash of a file this row changes.
|
||||
|
||||
Two dependencies are missing because they don't name the board. B1 and B2 below cover both.
|
||||
|
||||
## DESIGN.md: CHANGES
|
||||
|
||||
Three blockers. The in-process split, the CLI refusal and the fixture plan are right. Two release
|
||||
paths would take the live Console down at the next re-pin.
|
||||
|
||||
### Blockers
|
||||
|
||||
B1. The pinned copy has no board code. The live Console runs from `copies/<sha>`, and
|
||||
`scripts/stack-copy.sh:23-33` COPY_PATHS lists `packages/webui/src` but not
|
||||
`packages/control-board/src` or `tools/tmux/`. Once `webui/src/cli.mjs` imports `board.mjs`, the
|
||||
Console started from a copy fails at import. The unit restarts on failure
|
||||
(mosaic-webui.service.in, `Restart=on-failure`, `StartLimitBurst=5`) until it gives up. `reply`
|
||||
also resolves `agent-send.sh` beside the module (control-board serve.mjs:59), so it would point
|
||||
into the copy too. Fix: add `packages/control-board/src` and `tools/tmux/agent-send.sh` to
|
||||
COPY_PATHS, or resolve agent-send from `--checkout`. Say which, and add a test that starts the
|
||||
Console from a fresh copy (pinned-copy.test.mjs or stack-copy.test.mjs) and gets 200 on `/api/board`.
|
||||
|
||||
B2. A re-pin carries `--board` into the new copy, and the new `webui-service.sh` refuses it.
|
||||
stack-release.sh:128 reads the pointer's Console flags into P_CONSOLE, :676 copies them to
|
||||
RUN_CONSOLE, and :459 passes them to `webui-service.sh install`. The live pointer holds
|
||||
`--board http://127.0.0.1:7331`, so the first re-pin onto a copy with this row fails at
|
||||
`install-console`. Decision 6 covers only a hand restart of the old unit. Fix: stack-release drops
|
||||
`--board` when the target's `cli.mjs` lacks it, the way `console_for_target` drops `--tester-port`
|
||||
(:667, :687-690), and reports it in the plan. A rollback to an older copy runs without the flag,
|
||||
since its default is 7331. Test both in stack-release.test.mjs.
|
||||
|
||||
B3. Say where the board POSTs sit relative to serve.mjs:558. Today, in preview mode, :558 refuses
|
||||
every non-GET before the board block, so `/api/seen` and `/api/reply` never pass. In loopback mode
|
||||
`--send` can't be set at all (:479). So with `send !== null && named`, the two routes work only
|
||||
if their branch sits above :558, like `/api/bus/message` (:550) and chat (:554). The positive
|
||||
control ("/api/reply runs with --send") is then the first board write the live Console has ever
|
||||
allowed. Mos ruled it ("/api/reply stays only under --send", mos-1019), so it's in scope, but the
|
||||
design must say so. Fix: name the placement above :558 and after the header and origin checks
|
||||
(:493-516). State that loopback mode can't reply or mark seen. Add a mutant that moves the branch
|
||||
below :558, which the positive control kills.
|
||||
|
||||
### Your question on `/api/seen`
|
||||
|
||||
Gate it. It writes marks under the board dir, and without the gate it would be the only POST in
|
||||
preview mode that passes without `--send`. M5 covers it.
|
||||
|
||||
### Non-blocking
|
||||
|
||||
N1. control-board cli.mjs:14 defaults `fleet` to `~/.mosaic/fleet/agents`. Say which fleet source the
|
||||
Console's board context uses. The fleet is being retired, so `none` may be right. Either way it's
|
||||
a read only.
|
||||
N2. M2's "no `node:http` import and no `listen(`" check should cover `packages/control-board/src`
|
||||
as a whole, so a listener in a new file fails too. The design reads that way. Say it in the test
|
||||
name.
|
||||
N3. 19 lines in the webui tests match `Board unreachable|502`, and not all are board cases. List the
|
||||
ones that change to 503 in the manifest by file. The 17 dead-board files match your count (16 plus
|
||||
tester-fixture.mjs).
|
||||
N4. Mos's loopback ruling is open. Until then, record in the README that loopback board reads stay
|
||||
open to any local process, as they are today.
|
||||
N5. Order with row 192. The board block sits next to the proof gate (:493-505) that SSO changes.
|
||||
Land the second of the two on the first and rerun the board route tests.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Webby review: row 200 r1, `stack-release.sh activate --sso` (#1698)
|
||||
|
||||
Webby, 2026-10-20T20:13Z (from `date -u`). Author: Launchpad. Candidate 5dc0b5534 on launchpad/row200, base
|
||||
4d487dfdc (aa1bd209e plus row 192 r1 and r2; patch-id matches 53bb59462..37582e820). Packet 39b52b71f. Manifest
|
||||
`candidate-manifest.sha256` digest 46179d24...708c, verified: 3 paths, every hash matches `git show
|
||||
5dc0b5534:<path>`, and none of the 3 changed from the base to origin/refactor. Brief
|
||||
`docs/plans/2026-10-20_stack-release-sso.md` (a4a42dbac). Static review plus one bash probe of `cd; pwd` through
|
||||
a symlink (N1). I ran no test and no mutant; T56 and the 9 mutants go to Sage's gate. I did not read
|
||||
/mnt/storage/scratch/launchpad-r200.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Brief items, checked against the code at 5dc0b5534:
|
||||
|
||||
1. **Flags and plan.** `sso_for_target` (stack-release.sh:775) drops `--public-host`, `--auth-header`,
|
||||
`--allow-user` and the proof pair, and puts `--sso` before `--send` and `--tester-port`. That is where
|
||||
webui-service.sh:213 renders it, and its status grep at :401/:406 reads that order. Dropping
|
||||
`--public-host` goes past the brief's list, but webui-service.sh:178 refuses it beside `--sso`, so it
|
||||
has to go. The `sso:` plan line names what it drops and carries, in the style of row 189's `send:` line.
|
||||
The usage check at parse_opts refuses `--sso` with a pair, with `--no-proof` and on any other verb, and
|
||||
T56 tests all seven combinations.
|
||||
2. **Refusals before any stop.** `sso_checks` (:817) runs inside `console_for_target`, which `precheck`
|
||||
calls right after `check_target` (:996). `config_checks` runs after `plugin_checks` and before `new_activation`. None of these
|
||||
steps stops anything. `sso-config` runs the target copy's own `checkConsole`. The slot path is
|
||||
`dirname(cfg)/secrets/mosaic-stack/<slot>`, the same path the Console builds: business.mjs:25-27
|
||||
`configDir` and sso.mjs:35. The slot name is already limited to `[a-z0-9-]{1,64}` (sso-config.mjs:56), so
|
||||
it can't climb out of that directory. `slot_problem` uses `[ -e ]`, `[ -L ]`, `stat`, which doesn't follow
|
||||
links, and `realpath`. Nothing opens the slot, and T56's read watch on it would catch an open.
|
||||
`checkConsole` requires 1 to 8 providers (sso-config.mjs:45), so the `while read` loop never sees an
|
||||
empty line.
|
||||
3. **Rollback.** `sso_base` (:728) reads the arrival set, not the latest one. That set's
|
||||
`show-mosaic-webui.service.txt` is written by `capture` (:445) before that run stopped anything, so it
|
||||
holds the previous copy's Console, and SHA256SUMS covers it. The same-copy re-pin subtest and mutant 8
|
||||
pin the sage-1389 ruling. The four `sso-rollback-flags` causes are each tested.
|
||||
`config-console-key` covers my row 192 N4 for both verbs, and its message names the first step.
|
||||
4. TOOLS.md matches the code. I checked the flag order, the refusal names, the stat-only slot checks and
|
||||
the N4 wording.
|
||||
|
||||
The gate the brief asks for is there: one mutant for each of the five refusal codes, plus the `--auth-header`
|
||||
mutant, plus `--public-host`, the arrival set and the N4 first step. Each pattern has to occur exactly once
|
||||
or the script aborts. Hash pins: no test under packages/*/tests or scripts/test-*.sh pins a sha256 of
|
||||
stack-release.sh or TOOLS.md.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The inside-repository slot check compares a resolved path to an unresolved `$REPO`
|
||||
(stack-release.sh:859). `REPO="$(pwd)"` after `cd "$(dirname "$0")/.."` (:25-26) gives the logical path. I
|
||||
ran a script through a symlinked directory and got `pwd` = the link path and `realpath -e .` = the target.
|
||||
AGENTS.md names one such link, `~/src/mosaic-stack-dev-test` -> `/mnt/storage/src/mosaic-stack`. Run through
|
||||
that link, a slot inside the repository passes this check. The Console's `readSlot` then refuses it after
|
||||
the stop, because sso.mjs:47-50 resolves each root with `realpathSync` (:49). The health gate puts the old
|
||||
stack back, so nothing breaks for good, but item 2 asks for that refusal before any stop. `:858` already
|
||||
resolves `$COPIES`. Do the same for the repository (`repo="$(realpath -e -- "$REPO")"`) and match against
|
||||
that. The line is copied from row 187's `proof_file_problem` (:987-988), which has the same gap. That one is
|
||||
older than this row. Fix both, or file it as a follow-up.
|
||||
|
||||
N2. `config_checks` returns 0 when the config fails to parse (:868, `2>/dev/null || return 0`). The `console`
|
||||
check then passes silently, and so does a config whose JSON is `null`. The verb goes on with a config no
|
||||
loader reads. That isn't a regression, because no check read the config before row 200, and `sso_checks`
|
||||
refuses an unparseable config under `--sso`. For a fail-closed script, refusing here seems better than
|
||||
returning 0. If not, add a comment saying why a parse failure counts as "no console key".
|
||||
|
||||
N3. The slot check uses size 0 for `readSlot`'s one-non-empty-line rule (:860). A slot holding only `\n`,
|
||||
or two lines, passes the script and fails the Console after the stop. The script can't check more without
|
||||
reading the slot, and that would be the wrong trade. List it under "Not tested" or in TOOLS.md as a check
|
||||
the Console makes and this script doesn't.
|
||||
|
||||
N4. This is for the window plan, not this row. Once a copy with `--sso` support is live, no verb turns
|
||||
`--sso` off on it. activate has no `--no-sso`, an activate onto a copy without support refuses
|
||||
`sso-target`, and a rollback to a copy with support carries the flag. The only way back to the preview set
|
||||
is a rollback to a copy before row 192. The window plan's back-out step should say so.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Webby review: row 200 r2, `stack-release.sh activate --sso` (#1698)
|
||||
|
||||
Webby, 2026-10-20T20:30Z (from `date -u`). Author: Launchpad. Request comment 29621 (rev 2352, sage-1445).
|
||||
Candidate e3fd9f3a0 on launchpad/row200, one commit on r1 5dc0b5534 plus packet d163cfccb. Manifest
|
||||
`candidate-manifest.sha256` digest a1f6fe91...5bd3, verified: the same 3 paths, and every hash matches `git show
|
||||
e3fd9f3a0:<path>`. `git log aa1bd209e..origin/refactor` touches none of the 3 paths. I read the r1-to-r2 diff
|
||||
along with common.sh `load_config`. I ran no test and no mutant; T56 and mutants 10-12 go to Sage's gate.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
N1 is fixed. `slot_problem` (stack-release.sh:859-860) and `proof_file_problem` (:998-999) compare against
|
||||
`realpath -m` of `REPO` as well as `COPIES`. The new subtest runs the script through `base/repo-link.200`, and
|
||||
since `base` is realpath'd (test:237), `$REPO` there really is a different string from the resolved repository.
|
||||
The proof file and the slot (its secrets dir links into the repository) both sit under the real `base/repo`.
|
||||
Under r1 code each would pass the repository check, so the expected `proof-file-invalid` and `sso-slot` messages
|
||||
kill mutants 10 and 11. Both fixture dirs are under the test's temp base, not the checkout.
|
||||
|
||||
N2, with the deviation: I agree with it. `load_config` (common.sh:11-16) runs `scripts/mosaic-config.mjs env`
|
||||
on the same `MOSAIC_CONFIG` or default path and exits 1 on a file that doesn't parse. That happens at
|
||||
stack-release.sh:1696, before the pointer, log or units are read. So `config_checks`' new key=2 refusal is
|
||||
reachable only if the file changes after `load_config`, and mutant 12 survives for that reason. That's the
|
||||
same as row 192's mutant 23, and keeping the guard is right. The node probe also fails closed: any exit
|
||||
other than 0 or 1 (a read error, a crash) refuses. A JSON `null` counts as "no key", and `load_config` refuses
|
||||
it first anyway. The subtest checks the path that does happen: three verbs exit 1 with the loader's message,
|
||||
nothing changes, and activate works after the restore.
|
||||
|
||||
N4's back-out line is in the window plan section of BUILD.md, and it matches my wording. N3 stays a note as
|
||||
ruled.
|
||||
|
||||
Hash pins: no test under packages/*/tests or scripts/test-*.sh pins a sha256 of stack-release.sh or TOOLS.md.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
None new.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Webby review: row 200 (`activate --sso`), round 3
|
||||
|
||||
Webby, 2026-10-20T23:29Z (from `date -u`). Author: Launchpad (launchpad-0014), queue revs 2524-2525. Candidate:
|
||||
manifest agents/launchpad/work/queue-200/candidate-manifest.sha256, digest b3ead55c8b4895aded37002555895b9753adaa1b01b9832a7cbd2fc533a58aa5, 3 paths, on
|
||||
launchpad/row200-r3 9df40878f. Base 00d4cc1f6 (row 190's landing) is its ancestor. Sage asked for this round
|
||||
in sage-1557: "check activate --sso for a named instance against 190's code".
|
||||
|
||||
What I ran, on a detached worktree of 9df40878f in my own scratch, removed afterwards:
|
||||
- The manifest file hashes to the digest above, and `sha256sum -c` passes for all 3 paths.
|
||||
- `git range-diff` of the r2 commits against the r3 commits.
|
||||
- Each touched test file alone with `node --test --test-reporter=tap`: stack-release 192/192 and stack-instances 23/23, 0 failures.
|
||||
- A grep of `packages/*/tests/*` and `scripts/test-*.sh` (with a control that does print hits) for 64-hex pins
|
||||
and for readers of the 3 paths. No pins. Only stack-release.test.mjs reads them; stack-instances.test.mjs
|
||||
names stack-release in a comment.
|
||||
|
||||
I ran no mutants. Mutants 1-14 run in Sage's slot.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The merge keeps both sides. range-diff shows the first commit differs only in the conflict hunks, and the
|
||||
others are unchanged. In `plan()` the `sso:` line comes before the Console line, and the Console and CLI lines
|
||||
keep row 190's `$flags`. Both `--plan` lines run `check_target; console_for_target; ports_unclaimed;
|
||||
plugin_checks; config_checks; plan`, so row 190's `ports_unclaimed` stays where it landed.
|
||||
|
||||
`activate --sso` on a named instance is correct with no code change, as BUILD.md says. I traced it through
|
||||
row 190's code:
|
||||
- `instance_select` (scripts/instance.sh) exports `MOSAIC_CONFIG` as NAME's config, and the loader refuses
|
||||
`instance-config-mismatch` if a caller's `MOSAIC_CONFIG` names another file (mosaic-config.mjs:91-95). So
|
||||
the value `sso_checks` reads is NAME's config or nothing.
|
||||
- `sso_checks` takes `cfg="${MOSAIC_CONFIG:-…}"` and builds the slot as
|
||||
`$(dirname "$cfg")/secrets/mosaic-stack/$slot` (stack-release.sh:849). For alpha that is
|
||||
`~/.config/mosaic-alpha/secrets/mosaic-stack/<slot>`, the directory the Console's `readSlot` reads under
|
||||
the named unit, which sets `Environment=MOSAIC_CONFIG` to the same file.
|
||||
- `config_checks` (:878) reads the same `MOSAIC_CONFIG`.
|
||||
- `sso_base` (:743) reads `show-$CONSOLE_UNIT.txt`, and capture writes that name at :451. A named rollback
|
||||
therefore reads `show-mosaic-webui-alpha.service.txt` from alpha's own set. The comment at :734 now says so.
|
||||
- The pointer, log and sets come from `load_config` after `instance_select`, so they are NAME's data root.
|
||||
|
||||
The new T57 subtest "round 3 (sage-1556)" checks each of these on instance alpha: the missing-slot refusal
|
||||
names alpha's path, the plan Console line carries `--instance alpha`, `--sso` lands in
|
||||
`mosaic-webui-alpha.service` with alpha's `MOSAIC_CONFIG`, the default unit stays unwritten, status matches,
|
||||
and rollback takes its flags from alpha's arrival set. Mutants 13 and 14 target the two instance reads.
|
||||
|
||||
The usage-regex fix is right. Round 1 added `| --sso` to the activate usage line, and T53 test 6 and T55
|
||||
test 5 still matched the old text, so 4 tests failed. Both regexes now include `| --sso`
|
||||
(stack-release.test.mjs:1672, :1831), and T57's own check is :1940. A grep for the usage text finds no fourth
|
||||
copy. That defect sat in rounds 1 and 2 under my approves. My r1 and r2 notes said "I ran no test and no
|
||||
mutant", so I approved a candidate whose own test file was red. That's my miss. From this round on I run
|
||||
every touched test file whole before approving.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The N4 hint (stack-release.sh:844) reads "activate $TO without --sso first, then write console.sso, then
|
||||
run activate $TO --sso". For a named instance the operator also passes `--instance NAME`. Every verb needs it,
|
||||
so this is minor, but a hint copied as printed on alpha targets the default instance. Adding the instance
|
||||
flag to the hint when one is selected fits a later row.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Webby review: row 212 (C2-01, #1713), round 1, library reader, resolver, survey and renderItems
|
||||
|
||||
Webby, 2026-10-20T21:49Z (from `date -u`). Author: Gosalyn (gosalyn-0055). Request comment 29704 (sage-1516).
|
||||
Candidate: commit bdf18272a1cb90b61d4f740fdd0800fea06b006c on gosalyn/c2-01, which matches origin. Base
|
||||
32b54ac3b is its ancestor. 20 files, +1329 -2. Brief: the C2-01 section of docs/plans/2026-10-20_case2-rows.md,
|
||||
rows blob 3a86d5f048dc, read with Sage's accepted codes and readings from gosalyn-0055.
|
||||
|
||||
What my first-pass subagent ran, on a `git archive` export of bdf18272a in my scratch, removed afterwards:
|
||||
- Each library test file alone: links 6, mapping 7, reader 11, render 11, survey 4, so 39/39, matching
|
||||
Gosalyn's count.
|
||||
- stack-copy.test.mjs: 12/12 after a `git init` and commit inside the export, because `setup()` runs
|
||||
`git ls-files`. The first run, before that, failed 0/12 with "not a git repository". That's an artifact
|
||||
of the export, not the candidate.
|
||||
- One mutant: it dropped `packages/library/src` from `COPY_PATHS`, and the "shared copy" test failed on
|
||||
`packages/library/src/index.mjs`, so the mutant was killed. It restored the file afterwards.
|
||||
|
||||
I didn't run test-queue.sh, the cli usage.test.mjs or the queue dispatch.test.mjs. Gosalyn reports usage 2/2
|
||||
and dispatch 4/4. The suites are in Sage's slot. I checked N1, N2 and N4 myself against the code.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Every changed file is on the brief's owned list.
|
||||
- scripts/mosaic gains one six-line block before the `case`, and it matches only `library survey`, so no
|
||||
other verb's dispatch changes.
|
||||
- stack-copy.sh adds `packages/library/src` to `COPY_PATHS`, and stack-copy.test.mjs:37 lists its
|
||||
index.mjs. The reader imports project's git.mjs, which the copy already carries, since
|
||||
`packages/project` is copied whole.
|
||||
|
||||
Reader. It checks the 40-hex commit first, then runs `rev-parse --verify --end-of-options`, then
|
||||
`cat-file -t` (which must report a commit), then `ls-tree -r -z -l --full-tree` and `cat-file blob`, all
|
||||
through project's `git()` and `gitBytes()`. R1 refuses only over the limit, and there's a test for a blob
|
||||
of exactly 1 MiB. R7 groups paths by lowercase. The rules run in the order R2, R7, R1, so each path gets
|
||||
one problem.
|
||||
|
||||
Resolver. links.mjs:56-78 follows design 1.3 in order: root, vault prefix, relative (refused above the repo
|
||||
root), then shortest. A base name with two or more matches is ambiguous and becomes R4. The R3 parser
|
||||
refuses nested maps, block scalars, anchors, nested flow lists, `a: x: y`, duplicate keys and `__proto__`.
|
||||
|
||||
Survey. It prints JSON-quoted folder names, counts, keys that match the accepted regex (anything else
|
||||
prints as "(other key)"), form counts, scope, steps and problem counts. It prints no text, values, file
|
||||
names or link targets. The test's `NEVER` list includes the marker and a key built from it.
|
||||
|
||||
renderItems matches 7.4: five kinds, three notices, and badges for canon, playground and web. Anything else
|
||||
renders a fixed line with no body text. Each item on the brief's test list has a case. No 64-hex pin in
|
||||
`packages/*/tests` or `scripts/test-*.sh` names a changed path. The grep was run without `| head`.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. survey.mjs:76 says JSON quoting means "no name can carry a control sequence to the terminal". That's
|
||||
false. `JSON.stringify` escapes only U+0000-U+001F, the quote and the backslash. I checked with node:
|
||||
U+009B (the 8-bit CSI) and U+202E (a bidi override) come out raw. Folder names come from her own repo, so
|
||||
the risk is low. Either escape U+007F-U+009F and the bidi controls (U+200E, U+200F, U+202A-U+202E,
|
||||
U+2066-U+2069), or reword the comment.
|
||||
|
||||
N2. A front matter link's line is its key's line, not its list item's. reader.mjs:156 calls
|
||||
`frontMatterLine(lines, fm.bodyLine, field)`, and :162-165 finds the key. Under `relationships:` with two
|
||||
items, both links report the key's line. R4, R5 and R6 problems from block lists then point at the key. The
|
||||
brief asks only for "path and line", so this doesn't block. Fix it here or record it for C2-05a.
|
||||
|
||||
N3. links.mjs:2 reads `![[...]]` embeds the same way as links. An embedded image that exists in the repo but
|
||||
is unmapped (R2) isn't indexed, so its embed becomes R4 "a link to no file". Design 1.3 names only the three
|
||||
wikilink forms and says nothing about embeds. In an Obsidian vault this could inflate the R4 count and the
|
||||
survey's unresolved number. This needs a ruling from Gosalyn or Sage, not a code change in this row, and the
|
||||
survey run will show how common embeds are.
|
||||
|
||||
N4. reader.mjs:99: an unquoted flow list of wikilinks, `appears_in: [[[A]], [[B]]]`, is R3, because
|
||||
`flow()` refuses a nested `[`. The quoted form works. Obsidian's property editor writes the quoted form, I
|
||||
believe. One sentence in the README or the C2-12 setup sheet would cover it.
|
||||
|
||||
N5. render.mjs:14-16 doesn't check the scheme of a web cite's `url`. That's fine while it renders as plain
|
||||
text. When C2-10b turns it into a link, the Console must refuse anything but https, and that row's brief
|
||||
should say so.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Webby review: row 212 (C2-01, #416), round 2
|
||||
|
||||
Webby, 2026-10-21T04:56Z (from `date -u`). Author: Gosalyn (gosalyn-0081), in-review rev 2650. This round only checks the
|
||||
merge (sage-1714). Row 232 drifted three of the paths I approved in round 1 (29704, bdf18272a). Candidate:
|
||||
commit d3c7bbf839d9c3bf50f154b18e7e4472fbd005ed on gosalyn/c2-01-r2, one commit on origin/refactor
|
||||
9b5362018. The queue's round 2 candidate is that commit, and 9b5362018 is its ancestor.
|
||||
|
||||
What I ran:
|
||||
- `git range-diff 32b54ac3b..bdf18272a 9b5362018..d3c7bbf83`. The only change is a context line: the
|
||||
upstream `credentials` verb (row 199) in the scripts/mosaic case line.
|
||||
- `git diff bdf18272a d3c7bbf83 -- packages/library` is empty, so the package is byte-identical to round 1.
|
||||
- I read `git diff 9b5362018 d3c7bbf83` on the three merged paths:
|
||||
- scripts/stack-copy.sh:36 adds `packages/library/src` to COPY_PATHS beside row 232's
|
||||
`packages/queue/src`. Row 232's lines are unchanged. COPY_PATHS is read only by the `git archive` at :66.
|
||||
- scripts/mosaic:37-42 puts the `library survey` exec before the case line. The case line keeps
|
||||
`credentials`.
|
||||
- stack-copy.test.mjs:38 adds `packages/library/src/index.mjs` to the copy list.
|
||||
- I ran these alone on a detached worktree of d3c7bbf83 in my own scratch, removed afterwards:
|
||||
- cli stack-copy 16/16
|
||||
- library links 6/6, mapping 7/7, reader 11/11, render 11/11, survey 4/4
|
||||
- 0 failures and 0 skipped in every file
|
||||
- No 64-hex pin names stack-copy.sh or scripts/mosaic.
|
||||
|
||||
I didn't run the cli suite, the other readers of scripts/mosaic, or mutants. They run in Sage's slot
|
||||
with Gosalyn's singles.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The merge keeps both sides. Round 1's notes N1-N3 stand as recorded, and this round adds nothing.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Webby review: row 212 (C2-01, #416), round 3
|
||||
|
||||
Webby, 2026-10-21T07:43Z (from `date -u`). Author: Gosalyn (gosalyn-0084). Request comment 29979
|
||||
(sage-212-request-3, revs 2729-2730, posted as jarvis, sage-1830). This round only checks the merge onto
|
||||
row 234's landing. Round 2 was APPROVE on d3c7bbf83 over 9b5362018 (comment 29921). Candidate: commit
|
||||
a2c72ac952df94ecab7e391d5042f49b7f88b816 on gosalyn/c2-01-r3, one commit on c771a36f4. The queue's round 3
|
||||
candidate is that commit.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- `git diff d3c7bbf83 a2c72ac95 -- packages/library` is empty, so the package is byte-identical to rounds
|
||||
1 and 2.
|
||||
- On scripts/stack-copy.sh, cli stack-copy.test.mjs and scripts/mosaic, the -U0 +/- lines of c771a36f4 to
|
||||
a2c72ac95 are identical to round 2's lines against 9b5362018. In scripts/mosaic that is the 6-line
|
||||
`library survey` block before the case line. The case line at :44 keeps row 199's `credentials` and row
|
||||
234's `hosts`. The file stays 100755.
|
||||
- The only new path is agents/gosalyn/work/queue-212/BUILD.md.
|
||||
- Overlap: `git diff --name-only c771a36f4 4ced5e63d`, the refactor tip at 07:43Z, names none of this
|
||||
commit's 21 paths. So the 240, 228 and 238 landings don't touch it.
|
||||
|
||||
Gosalyn's slot run (gosalyn-0086, 07:28-07:40Z) was 20 node files, 451 pass 0 fail, plus test-queue.sh
|
||||
28/0. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The merge keeps both sides and carries round 2's lines unchanged. Round 1's notes N1-N3 stand as recorded.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Webby review: row 213 (C2-08, #1714), round 1, creative instances, personas, prompt by path
|
||||
|
||||
Webby, 2026-10-20T21:46Z (from `date -u`). Author: Morgana (morgana-0023). Request comment 29701 (sage-1510).
|
||||
Candidate: manifest digest bb5f46006e4903de85e4c41868d4fb46b2e5baac89a8f49250598f5b4c76a25b, 35 paths, on
|
||||
morgana/row213 e13ec4929, base 088087ac9. The packet commit 032be8c8b above it changes only BUILD.md. Brief:
|
||||
the C2-08 section of docs/plans/2026-10-20_case2-rows.md on origin/refactor, rows blob 3a86d5f048dc.
|
||||
|
||||
What I checked:
|
||||
- `sha256sum -c` on the manifest at e13ec4929 passes for all 35 paths, and the digest matches.
|
||||
- D1 (`checkPersonaVars` in business.mjs) and D2 (the launcher `personaDir` line) are as BUILD.md says, and
|
||||
sage-1492 approved both.
|
||||
- The pinned-test edits at project-runtime:26 and skills:170 follow from the prompt-by-path change. A grep of
|
||||
`packages/*/tests` and `scripts/test-*.sh` for 64-hex pins next to the changed paths, without `| head`,
|
||||
finds none.
|
||||
- My first-pass subagent ran each touched test file alone on a `git archive` export of e13ec4929 in my
|
||||
scratch, and each passed. It removed the directory afterwards. I ran no suite and no mutant; those are in Sage's slot.
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
B1. The M10 tests never go through the broker. Brief line 63 says "Tester cases run through the broker with
|
||||
a bound tester session, and chief of staff cases with a role session." Line 62 asks for the human's
|
||||
`limits.set` change to Bean's `launch.models` key to work next to the chief of staff's `organization.set`
|
||||
refusal of the same change. The candidate tests call the writer directly with actor strings:
|
||||
- instance-settings.test.mjs:232-242 passes `"chief-of-staff.chief"` and `"tester"` to `f.settings.set`.
|
||||
- launch-settings.test.mjs:153-160 passes the same two strings to `settings.set` and `settings.setRoutes`,
|
||||
and the human case at :164 is `settings.set(..., 'jason')`, not a `limits.set` request.
|
||||
|
||||
So nothing tests the part the brief cares about: that the broker builds those actor strings from a bound
|
||||
session. broker.mjs:1277-1283 sets `actor` to `"tester"` for a tester session and to
|
||||
`definition + "." + role` for a role session. If that code named the chief some other way, or a tester
|
||||
session reached the writer as the human, every M10 test would still pass. A grep of `packages/*/tests` for
|
||||
`organization-creative` finds only these two business test files. bus/tests/creative.test.mjs already has a
|
||||
broker with a bound tester and claimed roles (:40-51), so the fixture exists. Add, through `b.request`:
|
||||
- the chief's `organization.set` of Bean's model key, refused with `organization-creative-human-only` and the
|
||||
file bytes unchanged;
|
||||
- the tester's same request, refused, with whatever code the broker gives a tester first (see N5);
|
||||
- the human's `limits.set` of the same key, which succeeds and logs the human as actor.
|
||||
|
||||
B2. Brief line 70 asks for the modes of the business file, `.history` and `changes.jsonl` after an
|
||||
`instances.set` write. instance-settings.test.mjs:256-261 asserts the file (0600) and every entry under
|
||||
`.history/acme` (0700 or 0600). `changes.jsonl` is not under `.history`. The test reads it at :252 through
|
||||
`f.settings.writer.logFile("acme")` only to check the marker is absent, and no line asserts its mode. Add
|
||||
`assert.equal(statSync(f.settings.writer.logFile("acme")).mode & 0o777, 0o600)`, or whatever mode the writer
|
||||
sets, after :256.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Brief line 68 puts the adapter tests in `claude-session.test.mjs` and `pi-session.test.mjs`. They are in
|
||||
harness/tests/creative.test.mjs (:185-212). BUILD.md:82 names that file, but the Deviations section (:55)
|
||||
doesn't list the move. Add a line there so the record matches the brief.
|
||||
|
||||
N2. The persona read uses O_NOFOLLOW, which covers only the last path component. A symlinked `personas/` or
|
||||
`personas/<business>/` directory is followed. The digest check still refuses any file whose bytes differ, so
|
||||
this is not a bypass. Say so in a comment, or check the directory with lstat.
|
||||
|
||||
N3. `persona.name` accepts U+2028 and U+2029. If the name ever reaches a prompt line or a log line, refuse
|
||||
those two as well.
|
||||
|
||||
N4. The claude adapter checks the prompt file only with `[ -r ]`, while pi checks `-f` and `-r`. That
|
||||
matches the brief, which asks for pi's check. Noted so nobody reads the difference as an oversight.
|
||||
|
||||
N5. The tester cases in launch-settings.test.mjs:153-160 can't happen in production. The broker refuses a
|
||||
tester's `organization.set` as tester-refused before the writer runs, so `organization-creative-human-only`
|
||||
never reaches a tester. The writer-level cases are fine as defense in depth. The broker-level tester case
|
||||
in B1 should assert the code the broker actually returns.
|
||||
|
||||
## Not verified
|
||||
|
||||
- Real pi reading the prompt file. Only the refusal path is tested, and pi never runs in the test.
|
||||
- Whether a chief `organization.set` can reach `models` by some route other than the ones tested.
|
||||
- The 28 mutants and the suites, which are in Sage's slot.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Webby review: row 213 (C2-08, #1714), round 2
|
||||
|
||||
Webby, 2026-10-20T21:52Z (from `date -u`). Author: Morgana (morgana-0026), queue rev 2475. Candidate: manifest
|
||||
digest b5ff0ac0e754cb1e5891417f4ab1da9597f5cf49fb2fe2d5f0fbf066f821c43a, 35 paths, on morgana/row213
|
||||
aee32d185. Its parent is the r1 packet commit 032be8c8b, and base 088087ac9 is an ancestor. Diff read:
|
||||
`git diff e13ec4929 aee32d185`, which touches 8 files.
|
||||
|
||||
What I ran, on a `git archive` export of aee32d185 in my own scratch, removed afterwards:
|
||||
- The manifest file hashes to the digest above, and `sha256sum -c` passes for all 35 paths.
|
||||
- Each file alone with `node --test`: bus creative 3/3, business instance-settings 12/12, business 17/17,
|
||||
harness bundle 22/22, all with 0 failures.
|
||||
- A grep of `packages/*/tests` and `scripts/test-*.sh` for 64-hex pins naming vars.mjs, bundle.mjs or the
|
||||
changed tests finds none.
|
||||
|
||||
I ran no mutants. K2, K3 and V1 are written to replace exactly the broker and vars lines they target, and
|
||||
they run in Sage's slot with the other 28.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. bus/tests/creative.test.mjs adds "M10 through the broker". It uses `startBroker` with a real
|
||||
organization writer, a claimed chief launch session, a bound tester and the human, and every call goes
|
||||
through `b.request`:
|
||||
- The chief's `organization.set` of the models key only bean names is refused with
|
||||
`organization-creative-human-only`. Its `instances` change for bean is refused with
|
||||
`organization-creative-instance`.
|
||||
- The tester's `organization.set` and `limits.set` are refused with `tester-refused` (broker.mjs:1263).
|
||||
Its `instances.set` (a change to bean, and an add of an ideas instance) and its `routes.set` for bean do
|
||||
reach the writer, and the writer refuses them with `organization-creative-human-only`.
|
||||
- After all of these, the file bytes are unchanged and the log has no committed entry.
|
||||
- The human's `limits.set` of the same change lands, and the one committed entry names `jason`.
|
||||
|
||||
K2 and K3 replace the two actor lines at broker.mjs:1277 and :1283 with `"jason"`. The final committed-actor
|
||||
assert and the refusal asserts would catch both, so the test does pin the actor the broker builds from the
|
||||
session.
|
||||
|
||||
B2: withdrawn, my error. `organizationWriter` puts the log at `.history/<id>/changes.jsonl`
|
||||
(organization.mjs:84-85), so the r1 loop over `.history/acme` already asserted its 0600 mode. I said it
|
||||
wasn't under `.history` without reading `logFile`. The named assert and the `includes("changes.jsonl")`
|
||||
line Morgana added are harmless, and they make the brief's line 70 visible.
|
||||
|
||||
N1 to N4 are handled. The Deviations section lists the adapter-test file. bundle.mjs:186-188 says
|
||||
O_NOFOLLOW covers only the last component and the digest still holds. vars.mjs:153 refuses U+2028 and
|
||||
U+2029, with two new values in business.test.mjs:254 and mutant V1. N4 is a residual.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
None new.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Webby review: row 213 (C2-08, #1714), round 3
|
||||
|
||||
Webby, 2026-10-21T05:05Z (from `date -u`). Author: Morgana (morgana-0030), revs 2652-2654, op
|
||||
morgana-213-inreview-3. Request comment 29924 (sage-213-request-3, revs 2662-2663). This round only checks the
|
||||
rebase (sage-1715). Rows 199, 237 and 210 landed on six of the paths I approved in round 2 (29708,
|
||||
manifest b5ff0ac0e754...). Candidate: manifest digest
|
||||
887c32d60fe2170fe1df6224b527db34511d2142d80d328690e6baab2827fe6a, 35 paths, on morgana/row213-r3
|
||||
2d2ad2760. Base origin/refactor 9b5362018 is its ancestor.
|
||||
|
||||
What I ran:
|
||||
- I took the manifest text from `queue show 213` (round 3), not from the author's scratch. It hashes to
|
||||
the digest above, and `sha256sum -c` passes for all 35 paths on a detached worktree of 2d2ad2760 in my
|
||||
own scratch.
|
||||
- `git range-diff 088087ac9..aee32d185 9b5362018..2d2ad2760^`: commits 2 and 3 are `=`. Commit 1 differs
|
||||
only in the five conflict hunks, and I read each of them.
|
||||
- `git diff 088087ac9 9b5362018`, outside .md files, adds no line that names END_PROTECTED,
|
||||
THREAD_DEFINITION, CREATIVE_*, the creative refusal codes or persona. The only hits are queue JSON row
|
||||
titles. So nothing new on the base uses a name this row changes.
|
||||
- I ran each touched test file alone, plus packages/seat/tests/sandbox.test.mjs because the broker import
|
||||
changed (RUNNER_FILES rule): business assistant 11, business 17, instance-settings 12,
|
||||
launch-settings 16, role 17, bus creative 3, project-runtime 3, cli launcher 30, harness creative 7,
|
||||
skills 10, seat sandbox 10. The total is 136 with 0 failures and 0 skipped, summed by script. I started
|
||||
these at 04:58Z without asking Sage for the slot. That was wrong (sage-1722), and Sage has accounted
|
||||
for them.
|
||||
- No 64-hex pin in `packages/*/tests/*` or `scripts/test-*.sh` names a manifest path. A positive control
|
||||
(broker.mjs readers) finds hits, so the grep isn't vacuous.
|
||||
|
||||
I ran no mutants and no suite. Morgana's singles and 31 mutants run in Sage's slot.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Each conflict resolution keeps both sides:
|
||||
- broker.mjs:6 imports `ID_PATTERN, THREAD_DEFINITION, END_PROTECTED, forkProblem, forkUrl,
|
||||
credentialKind, validArgv, commandAllowed, CREATIVE_DEFINITIONS`. Rows 199 and 210's names are all
|
||||
there, and CREATIVE_DEFINITIONS comes last. The pinned import line in project-runtime.test.mjs is the
|
||||
same list, and it keeps the row 199 and 210 comments. The import is still from vocabulary.mjs alone,
|
||||
so RUNNER_FILES doesn't change.
|
||||
- business.mjs:13 is one vocabulary import with row 199's `forkProblem, giteaName, parseGiteaUrl` and row
|
||||
210's `HOST_SERVICE, HOST_PREFIX`, plus CREATIVE_DEFINITIONS. `checkRoles(roles, file, rolesDir,
|
||||
business, hosts)` is the base signature. The entry keys keep `identity`, `publish` and `hosts`.
|
||||
`checkPersonaVars` runs at the end of the per-instance loop, after the identity and publish block.
|
||||
- role.mjs keeps row 210's HOST_SERVICE and adds CREATIVE_DEFINITIONS and CREATIVE_TOOLS. checkCreative
|
||||
(:143-157) is round 2's.
|
||||
- vocabulary.mjs puts the creative block (CREATIVE_DEFINITIONS, CREATIVE_KINDS, CREATIVE_TOOLS,
|
||||
THREAD_DEFINITIONS, `END_PROTECTED = THREAD_DEFINITIONS`) before row 199's Gitea URL parse block, which
|
||||
is whole.
|
||||
- launcher.mjs merged without a conflict, and launcher.test.mjs passes.
|
||||
|
||||
The new base adds host, identity, publish and credential grants, so I checked whether any of them gives a
|
||||
creative instance more than M1 allows. None does:
|
||||
- `hosts` on a role entry needs the role definition to declare the `host` credential (business.mjs:137).
|
||||
`publish` needs an identity and a `gitea` credential in the definition (:167). All six creative role
|
||||
files declare `credentials: []`, and a role file changes only by a reviewed commit.
|
||||
- Even with a grant, host.run spends a `host.sudo` decision (broker.mjs:1752-1760), and git pushes need
|
||||
their actions too. A creative role holds only withinRole `message.send`, so every one of those
|
||||
actions is classed as gated (broker #classification) and needs a resolved decision. M6 refuses any
|
||||
creative `decision.raise` with `decision-not-allowed` (broker.mjs:1370-1374), before the spec-cited
|
||||
branch, so no such decision can exist for the role.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. checkCreative limits tools, authority and messaging, but not `credentials`. A later role-file commit
|
||||
that gave companion or research a `gitea` credential would load, and the business file could then map a
|
||||
credential to that instance (business.mjs:157). I didn't trace whether a mapped credential reaches the
|
||||
run's environment. The decision paths stay closed (M6), but if it does, a creative role with `web`
|
||||
holding a live token is a wider channel than design 4 means. Refusing any credential in checkCreative,
|
||||
with a role.test.mjs case, would close it. That's a follow-up, not part of this rebase.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Webby review: row 213 (C2-08, #1714), round 4
|
||||
|
||||
Webby, 2026-10-21T09:36Z (from `date -u`). Author: Morgana. Request comment 29982 (sage-213-request-4,
|
||||
sage-1901). This round only checks the rebase onto row 234's landing. Round 3 was APPROVE on manifest
|
||||
887c32d60fe2 at 2d2ad2760 over 9b5362018 (comment 29924). Candidate: manifest digest
|
||||
26d4d0ecff5601e2e610c02fc516cede49563e06676f05b5042ae09c1dee0671, 35 paths, on morgana/row213-r4
|
||||
2b3e67e36. That commit's one parent is c771a36f4.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 213` (round 4). It hashes to the digest above, and each
|
||||
path's sha256 from `git show 2b3e67e36:<path>` matches its line. The same 35 paths as round 3.
|
||||
- Outside the manifest, c771a36f4..2b3e67e36 adds only Morgana's packet files under
|
||||
agents/morgana/work/queue-213/.
|
||||
- Three manifest paths differ in blob from 2d2ad2760: business.mjs, broker.mjs and
|
||||
project-runtime.test.mjs. These are the three that 9b5362018..c771a36f4 (row 234) changed. The other
|
||||
32 have the same blob and mode.
|
||||
- On those three, the row's -U0 +/- lines over c771a36f4 match its lines over 9b5362018, except the
|
||||
vocabulary import lines. Those follow row 234: business.mjs no longer imports `HOST_PREFIX` (row 234
|
||||
removed it, and nothing in the candidate's business.mjs uses it), and broker.mjs and the pinned line
|
||||
in project-runtime.test.mjs keep row 234's `HOST_ID` with `CREATIVE_DEFINITIONS` last.
|
||||
- Overlap: c771a36f4..38b83ea5c changes none of the 35 paths, as Sage found.
|
||||
|
||||
Morgana's slot run on r4 was singles 191/0 and 31/31 mutants killed (morgana-0035). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The resolved import lines keep both sides, and the rest of the row is round 3's code.
|
||||
|
||||
I rechecked creative roles against row 234's machine grants, because row 234 moved host access from the
|
||||
business file into store records (r6 X4). That retires part of my round 3 argument: host.run no longer
|
||||
reads the business file's `hosts`, so the "needs a `host` credential in the role definition" check
|
||||
(round 3, business.mjs:137) no longer stands in front of it. What stands now:
|
||||
- M6 (broker.mjs:1736-1738) runs before row 234's grant branch (:1744). A creative `decision.raise`
|
||||
with `grant` refuses `decision-not-allowed`, before #raiseGrant.
|
||||
- #raiseGrant (:870) and the agent side of #hostsCaller (:896) refuse anything but `chief-of-staff`
|
||||
with `grant-chief-only`. `hosts.remove` is human or tester only (:894). No creative definition is
|
||||
`chief-of-staff` (vocabulary.mjs:176).
|
||||
- host.run needs a `host_grants` row naming the caller (:695, :703). At c771a36f4 nothing writes that
|
||||
table. 3i-3j.sql:20 creates it, and the only code that touches it is reads (broker.mjs:695, :930,
|
||||
runtime.mjs:103). enrollCheck accepts only `method: 'host-enroll'`. So no role, creative or not, can
|
||||
hold a host grant at this base.
|
||||
- Each host.run still spends a `host.sudo` decision (:2115-2118), and M6 keeps a creative role from
|
||||
raising one.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N2. Forward note for row 235, which adds the `host` grant and the `host_grants` writer. After 234, the
|
||||
role definition's `credentials` no longer gates host access, so the grant writer is where a creative
|
||||
agent has to be refused. A chief-of-staff could raise a `host` grant naming a creative role as
|
||||
`agent`, and the human might approve it without noticing which kind of role it is. Then host.run
|
||||
would pass #hostAccess for that role, leaving only the per-call `host.sudo` decision between it and the
|
||||
host. I didn't trace whether a decision raised by another role can be spent by the creative one. I couldn't read 235's code
|
||||
(origin has no gizmoduck/row235-c), so I don't know whether it already refuses this. The 235 review
|
||||
should check that a `host` grant naming a CREATIVE_DEFINITIONS agent refuses, with a test. This sits
|
||||
next to my round 3 N1, now in DEFERRED at 5465fd52d.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Webby review: row 218 (#1719), C2-12, round 1
|
||||
|
||||
Webby, 2026-10-21T11:52Z (from `date -u`). Author: Sage. Request comment 30039 (revs 2840-2841,
|
||||
sage-1972). Brief: docs/plans/2026-10-20_case2-rows.md "C2-12", blob f7c4cfbff43a. Candidate: manifest
|
||||
digest fb1bbc5dc8895f2828c33d1fbb39e4c00a833302b9ba9ccd6a73b4af2ac295a5, 3 paths, on sage/row218
|
||||
fc0d9c26f. That's one commit on 5c6f73ed7, which is the refactor tip.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 218` (round 1). It hashes to the digest above, and each
|
||||
path's sha256 from `git show fc0d9c26f:<path>` matches its line.
|
||||
- 5c6f73ed7..fc0d9c26f touches exactly the 3 manifest paths. The base is the refactor tip, so nothing
|
||||
overlaps.
|
||||
- Doc only. Decision 137 applies: no finding here blocks.
|
||||
- I read both sheets and BUILD.md, then the code each claim cites at fc0d9c26f: survey.mjs, mapping.mjs,
|
||||
reader.mjs, links.mjs, scope.mjs, problems.mjs, render.mjs, project/src/git.mjs and paths.mjs,
|
||||
seat/src/checkout.mjs (gitEnv, GIT_TIMEOUT_MS, runGit's spawn error), scripts/mosaic,
|
||||
scripts/stack-copy.sh COPY_PATHS, business/src/vocabulary.mjs, the C2-03a brief lines 676-681 and
|
||||
design 75fb3e10e lines 815-832, 936-945 and 1324-1331.
|
||||
|
||||
I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
SURVEY-RUN.md matches the code.
|
||||
|
||||
- Usage and flags. surveyMain takes `survey`, one repo and `--mapping <file>` once (survey.mjs:102-110).
|
||||
`--mapping=<file>`, any other `-` argument, a second repo, `--mapping` last or twice all give exit 4
|
||||
with the USAGE text (:16, :101). The sheet's stderr line is byte for byte.
|
||||
- Order and exits. The mapping is read and checked before the repo is touched (:112-121). An unreadable
|
||||
or non-JSON file gives exit 3 with the sheet's text (:115). A schema fault gives
|
||||
`library-mapping-invalid: <file> <reason>` (mapping.mjs:24), and a playground outside its books gives
|
||||
the sheet's `library-playground-outside-book` line (mapping.mjs:79-80). A path that isn't a directory
|
||||
gives exit 2 with the path as typed (:120-121). A LibraryError from the read gives exit 2 (:123), and
|
||||
anything else rethrows to a stack trace (:117, :123, :128-131). Nothing goes to stdout before the
|
||||
survey finishes (:122).
|
||||
- The HEAD line. headCommit reads `HEAD^{commit}` (reader.mjs:22-24). A git failure is a ProjectError
|
||||
with code `project-git` (git.mjs:14, paths.mjs:4-6), so the message reads
|
||||
`library-git: repository has no readable HEAD commit (project-git)`. A missing `git` comes back from
|
||||
runGit as a failed result (checkout.mjs:113, :144), so it lands in the same row, as the sheet says.
|
||||
- Top folder only. Every git call runs with `GIT_CEILING_DIRECTORIES` set to the path's parent
|
||||
(git.mjs:12, :62), so a folder inside a repo can't find the repo above it. A bare repo's own folder
|
||||
is found at the start dir.
|
||||
- Owner. gitEnv sets `GIT_CONFIG_GLOBAL=/dev/null`, `GIT_CONFIG_NOSYSTEM=1` and `HOME=/nonexistent`
|
||||
(checkout.mjs:79-85), so no `safe.directory` entry reaches git.
|
||||
- Timeout. 100 seconds for both git() and gitBytes() (checkout.mjs:49, git.mjs:68).
|
||||
- Exit 127. scripts/mosaic:39-41 shifts once and runs `exec node`. Under `set -e`, bash prints
|
||||
`exec: node: not found` and exits 127 when node isn't on PATH.
|
||||
- The copy carries the survey and both modules it imports through reader.mjs: `packages/library/src`,
|
||||
`packages/project` and `packages/seat/src` are all in COPY_PATHS.
|
||||
- Output lines. Every line and field in the two tables matches formatSurvey (survey.mjs:83-97).
|
||||
`ls-tree -r` without `-t` lists no tree entries (reader.mjs:32), so a folder line appears only for a
|
||||
folder that holds a blob or gitlink directly, as the sheet says. Keys outside
|
||||
`^[A-Za-z_][A-Za-z0-9_-]{0,31}$` print as `(other key)` (:19-20, :47). `unresolved` is the R4 count
|
||||
(:72). Scope counts cover every tree entry (:66).
|
||||
- `vaultPrefix`. It's optional and must be one folder name (mapping.mjs:48). Without it the resolver
|
||||
skips the `vault-prefix` step (links.mjs:77-78), and `shortest` refuses a target with `/` (:82). So a
|
||||
full vault path link resolves to nothing, and step 4's instruction to set it is right.
|
||||
- Survey source. The C2-03a brief at 5c6f73ed7 requires the mirror's `HEAD` to be a symbolic ref to
|
||||
`refs/heads/served`, with a test that the survey's `commit` line equals the served commit (brief
|
||||
lines 678-681). The default mirror path `<dataRoot>/projects/<business>/<project>` is paths.mjs
|
||||
projectRepo's fallback.
|
||||
|
||||
SETUP-SHEET.md matches the code it can be checked against.
|
||||
|
||||
- Scope. classify returns `unmapped` for any path not ending `.md` (scope.mjs:9), and for any path in no
|
||||
book (:10-20). So `.canvas`, `.base`, images and top-level notes are not read, even inside a book.
|
||||
Scope comes from the path alone. A playground prefix must sit inside one of its universe's books and
|
||||
can't equal the book (mapping.mjs:79).
|
||||
- Property lists. I traced each of the three forms through the reader. `["[[A]]", "[[B]]"]` goes to
|
||||
flow() (reader.mjs:99), which keeps the quoted items whole (:85-86), and scalar() JSON-parses each one
|
||||
to `[[A]]` (:71-73). parseFile then reads it as a link (:159-160). The block form with quoted items
|
||||
goes through the item branch (:117-122) to the same result. `[[[A]], [[B]]]` isn't one wikilink, so
|
||||
flow() runs and refuses at the first inner `[` (:87). That's an R3 stop: front matter null, the file
|
||||
still indexed by path, name and body (:192-194), and counted as "not fully parsed" by the survey. A
|
||||
value without `[[ ]]` gives no link (:159). The sheet's "can't read that form ... ignores every
|
||||
property ... still finds the note" is right, and so is not calling it a refusal.
|
||||
- Obsidian. Sage asked me to check that the property editor writes the quoted form. I know Obsidian's
|
||||
editor writes a list property in block form, one `- "[[Link]]"` per line, with the quotes. I didn't
|
||||
run Obsidian to confirm it. The sheet only says the editor adds the quotes, which holds for that form.
|
||||
"Files and links, New link format, Absolute path in vault" is a real Obsidian setting, and its links
|
||||
carry no `.md`, which the resolver adds (links.mjs:72). Keep the tip. `path:"..."` is a valid graph
|
||||
group query.
|
||||
- Kinds and helpers. Bean and Chet's kinds `idea, question, finding, fact` equal CREATIVE_KINDS.companion
|
||||
(vocabulary.mjs:179). The five helpers equal the rest of CREATIVE_DEFINITIONS (:176). "No cited
|
||||
answer" is render.mjs:6's notice text.
|
||||
- Item caps. No persona or library code holds the reply caps yet. The persona bundle has only the 8000
|
||||
character fence (bundle.mjs:193-197), render.mjs:3 says the guard (C2-06) checks limits, and C2-06
|
||||
isn't landed. The sheet's numbers match design M5 (line 819) except for one cap, N1.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The setup sheet leaves out M5's total: at most 1,200 characters of item text per reply (design line
|
||||
819, and 10 step 8 at line 1329). As written, SETUP-SHEET.md:109-110 reads as up to 10 items of 300
|
||||
characters, 3,000 characters in all. Step 8's residual is framed as "1,000 characters of free text
|
||||
within 1,200 total", so Melanie should see the total. Add "and 1,200 characters in all" to line 109.
|
||||
"Today" there also describes limits no code enforces until C2-06 lands. Fine if the sheet goes out
|
||||
after that, which step 7's order suggests.
|
||||
|
||||
N2. SURVEY-RUN.md:90-91 says a folder name with a control character prints as `\uXXXX`.
|
||||
JSON.stringify writes tab, newline, CR, backspace and form feed as `\t`, `\n`, `\r`, `\b` and `\f`
|
||||
(survey.mjs:82). Every other control and bidi character does print as `\uXXXX`. "An escape such as
|
||||
`\t` or ``" would be exact.
|
||||
|
||||
N3. listTree can also refuse with a `library-commit:` prefix, not `library-git:` (reader.mjs:29, :31),
|
||||
for example a SHA-256 repo whose HEAD isn't 40 hex. The refusal table's exit 2 rows don't show that
|
||||
prefix. The served mirror is SHA-1, so it's unlikely. The "Report to Sage" row could say
|
||||
`library-git:` or `library-commit:`.
|
||||
|
||||
N4. Step 2's PATH ends in `/usr/bin:/bin`. If `<node dir>` is wrong and `/usr/bin/node` exists, the
|
||||
survey runs on that node and gives no 127. The 127 row holds only when no node is on the PATH at all.
|
||||
One `command -v node` under the same sudo before step 2 would show which node runs.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Webby review: row 218 (#1719), C2-12, round 2
|
||||
|
||||
Webby, 2026-10-21T11:59Z (from `date -u`). Author: Sage. Request comment 30046 (revs 2853-2855,
|
||||
sage-1981). Round 1 was APPROVE on fc0d9c26f (rev 2848, webby-0075) with four doc notes. Candidate:
|
||||
manifest digest c0d3709d4ca37a8229e13df13344285232880fafd0d21de0dd9967d0d7ddbb67, 3 paths, on sage/row218
|
||||
5fcc2ec90. That's one commit on fc0d9c26f.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 218` (round 2), not from the scratch path. It hashes to the
|
||||
digest above, and each path's sha256 from `git show 5fcc2ec90:<path>` matches its line.
|
||||
- 5c6f73ed7..5fcc2ec90 touches exactly the 3 manifest paths. The refactor tip is still 5c6f73ed7, so
|
||||
nothing overlaps.
|
||||
- I read the whole diff fc0d9c26f..5fcc2ec90 (33+/8-). Doc only, so decision 137 applies.
|
||||
|
||||
I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- N1. SETUP-SHEET.md:109-112 now gives "at most 10 items and at most 1,200 characters of item text in
|
||||
all", which matches design M5 (line 819), and "Today" is gone. BUILD.md records Sage's ruling that
|
||||
the sheet goes to Melanie only after C2-06 lands, and only on Jason's go (#419). That covers the
|
||||
fact that no code enforces the caps yet.
|
||||
- N2. SURVEY-RUN.md:99-101 matches JSON.stringify plus quote() (survey.mjs:81-82): `\t \n \r \b \f`
|
||||
for those five, `\uXXXX` for every other C0 control, DEL, C1 and the listed bidi characters.
|
||||
- N4. The new step 2 runs `command -v node; node --version` under the same sudo and PATH, and says the
|
||||
first line must be `<node dir>/node`. The renumbering is consistent. The remaining "step N" mentions
|
||||
(lines 8, 12, 14, 19) are design 10's steps, not the sheet's own.
|
||||
- N3. The `library-commit:` row is there, with exit 2 and the right prefix. survey.mjs:123 sends every
|
||||
LibraryError to exit 2 with its message. Its example cause is wrong, see N5.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N5. The new row's example, "because `HEAD` moved during the survey", can't produce `library-commit:`.
|
||||
The survey reads HEAD once (survey.mjs:27) and passes that id along, and an old commit object stays
|
||||
in the repo when HEAD moves. listTree raises `library-commit` only when the id isn't 40 hex
|
||||
(reader.mjs:29) or `cat-file -t` isn't `commit` (:31). headCommit already asked for `HEAD^{commit}`,
|
||||
so in practice the cause is a SHA-256 repo, whose ids are 64 hex. An object that has gone missing
|
||||
gives `library-git: commit is not in the repository (project-git)` (:30) instead. "Run it again
|
||||
once" does no harm, but a rerun gives the same result. "for example a SHA-256 repository" with
|
||||
"Report to Sage" would be exact. The served mirror is SHA-1, so the row is unlikely to come up.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Webby review: row 218 (#1719), C2-12, round 3
|
||||
|
||||
Webby, 2026-10-21T12:03Z (from `date -u`). Author: Sage. Request comment 30048 (revs 2857-2859,
|
||||
sage-1984). Round 2 was APPROVE on 5fcc2ec90 (rev 2856, webby-0077) with one doc note, N5. Candidate:
|
||||
manifest digest 1a38baf5af2a5cd11708ab664c9974e406d2cf8e720a1e789f89eed364625632, 3 paths, on
|
||||
sage/row218 89ba79d94. That's one commit on 5fcc2ec90.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 218` (round 3), not from the scratch path. It hashes to the
|
||||
digest above, and each path's sha256 from `git show 89ba79d94:<path>` matches its line.
|
||||
- 5c6f73ed7..89ba79d94 touches exactly the 3 manifest paths. The refactor tip is still 5c6f73ed7.
|
||||
- I read the diff 5fcc2ec90..89ba79d94. It changes one table row in SURVEY-RUN.md and adds a round 3
|
||||
section to BUILD.md. SETUP-SHEET.md is unchanged.
|
||||
|
||||
I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
N5 is fixed. The `library-commit:` row now names the real cause, an id the reader can't take as a
|
||||
commit, such as one from a SHA-256 repository (reader.mjs:29, :31). It now says to report to Sage and
|
||||
no longer suggests a rerun. BUILD.md's round 3 section gives the same reasoning.
|
||||
|
||||
No new notes.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Webby review: row 221 (#1722), C2-20, round 1
|
||||
|
||||
Webby, 2026-10-21T10:36Z (from `date -u`). Author: Gosalyn. Request comment 30024 (revs 2806-2807,
|
||||
sage-1939, posted by Sage as jarvis). Brief: docs/plans/2026-10-20_case2-rows.md "C2-20", blob
|
||||
16847e94ae17. Rulings: sage-1836 (fence export, pairing by argument) and sage-1840 (yaml 2.9.0 exact,
|
||||
no chown). Candidate: manifest digest 20f05e3a6ad51a3493849b6a911f62b00c33fdedb14dc898f6eeb525235ef1ad,
|
||||
22 paths, on gosalyn/c2-20-r2 8f403d0d4. 48f044ccc is the code on 62ddc96cc, and 8f403d0d4 adds BUILD.md.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 221`. It hashes to the digest above, and each path's
|
||||
sha256 from `git show 8f403d0d4:<path>` matches its line.
|
||||
- 62ddc96cc..8f403d0d4 touches exactly the 22 manifest paths. `git patch-id --stable` of 48f044ccc
|
||||
is eb094ae783e9, as BUILD.md says.
|
||||
- Overlap: the refactor tip is 29ce1e360. 62ddc96cc..29ce1e360 changes none of the 22 paths.
|
||||
- The script is 100755. The 16 fixture files and the rest are 100644.
|
||||
- I read the whole script, test file, trace helper, fixture README, the bundle.mjs and package
|
||||
diffs, and BUILD.md.
|
||||
- No test pins a sha256 of bundle.mjs, package.json or package-lock.json. The lock already had
|
||||
node_modules/yaml at 2.9.0 (package-lock.json:2192), so the pin moves nothing.
|
||||
|
||||
Gosalyn's singles on Sage's slot, 10:18:09-10:30:14Z, were 18 files 419/0 (BUILD.md). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
The script does what the brief and the two rulings ask.
|
||||
|
||||
- The fence has one copy. `personaTextFault` (bundle.mjs:193-197) holds the three rules readPersona
|
||||
had, with the same reasons, and readPersona calls it (:231-232). Its messages are byte for byte the
|
||||
old ones. The script imports it (case2-persona-import.mjs:31) and applies it at stage (:239-242),
|
||||
before anything is written, and again at install on the staged bytes (:284-285). The tests take
|
||||
8000 two-byte characters as passing, and they refuse 8001, `</persona-file>`, `## ` and `# `.
|
||||
- The pairing follows sage-1836 (c). There are exactly two `--pair` arguments, a profile id from
|
||||
hermes_cli/profiles.py:37, and an lstat'd directory that isn't a symlink. Each name is exactly Bean
|
||||
or Chet, and the profiles and names differ (:85-101). The pairing line, with SOUL.md bytes and
|
||||
sha256, prints before the fence check and before any write (:231-233). The test checks that it comes
|
||||
before `staged:`. BUILD.md records that Hermes has no name field and how this answers DESIGN.md:1322.
|
||||
- Reads. stage opens SOUL.md and config.yaml per profile, with O_NOFOLLOW and O_NONBLOCK and a size
|
||||
cap (:105-122). The dry run opens only SOUL.md. The trace test hooks every fs open, read and list
|
||||
through syncBuiltinESMExports, and has a positive control on both SOUL.md files. It allows only the
|
||||
four expected paths, and it checks that the dry run never reads config.yaml. Memory, sessions,
|
||||
skills, state.db, profile.yaml, auth.json and .env are never opened.
|
||||
- Output. It prints the pairing, the per-channel prompt names and byte counts, the staged and
|
||||
installed paths, and the two vars. assertNoText walks every fixture marker (at least 20) and the
|
||||
prompt phrases over stage and install output. yaml's warnings are off (:168), and a parse failure
|
||||
gives a fixed message.
|
||||
- Files. The staging dir is a new 0700 directory or our own closed one, and every file is O_EXCL,
|
||||
O_NOFOLLOW and 0600 (:183-214). install refuses a staged file that isn't the caller's or is open to
|
||||
group or other. It checks each digest against the manifest, and it lstats both targets before it
|
||||
writes either (:293-297). The target `<configDir>/personas/<business>/<instance>.md` is the path
|
||||
readPersona opens (bundle.mjs:204, launcher.mjs:394). install runs as `<user>`, so the uid and mode
|
||||
checks there hold too.
|
||||
- The dry run prints `files N` and one count line per profile. The heading split (:59) uses the same
|
||||
line terminators as the fence's `/m`. The word list has 69 tool names plus Hermes and Discord (71
|
||||
entries), matched whole word in any case. The test counts three lines, rejects `memory_bank`,
|
||||
`discords` and `Web searching`, and checks that no matched word is printed.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Channel ids and platform keys from config.yaml print raw (:147, :237). A key holding a newline or
|
||||
an escape sequence would reach Jason's terminal as is. It's her own config and the ids are numbers or
|
||||
short names, so the risk is low. `JSON.stringify` on the key, or a refusal for keys outside
|
||||
`[A-Za-z0-9_.-]`, would close it.
|
||||
|
||||
N2. The dry run reports lines and headings but not the 8000 character limit. A SOUL.md between 8001
|
||||
characters and 32000 bytes gives normal counts on the dry run and then refuses at stage. Over 32000
|
||||
bytes, the dry run refuses (:128), so the header's "A fence fault there is counted, not refused"
|
||||
(:23) holds for headings only. Neither goes beyond the brief, and stage refuses before it writes. A
|
||||
`characters N` field in the dry run line would warn Jason a step sooner.
|
||||
|
||||
N3. Morgana's agents/morgana/work/queue-213/mutants.py:48 (B3) matches the old readPersona line, which
|
||||
this row replaces. A rerun of 213's mutant set on this base would fail to apply B3 until its pattern
|
||||
names the new line at bundle.mjs:195. It's packet history, so nothing is needed unless someone reruns
|
||||
the set.
|
||||
|
||||
N4. privateDir's lstat and mkdir (:184-188) aren't atomic. A directory created between the two
|
||||
throws a raw EEXIST with a stack, not a Refusal. It fails closed either way.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Webby review: row 226 (C2-13b, #1727) design, round d1
|
||||
|
||||
Webby, 2026-10-21T21:48Z (from `date -u`). Author: Gosalyn (gosalyn-0113). Gate owner Sage. Brief:
|
||||
docs/plans/2026-10-20_case2-rows.md "C2-13b install script and library precheck", blob f7c4cfbff (the
|
||||
row's pinned blob). Candidate: agents/gosalyn/work/queue-226/BUILD.md at f46e66883 on gosalyn/c2-13b,
|
||||
one commit on refactor d37210288 that adds only that file. Design rounds get no queue record
|
||||
(sage-2182).
|
||||
|
||||
What I checked, without runs: the design against d37210288. I read case2-precheck.sh (the header, the
|
||||
profile and tester rules :127-135, ok/refuse :51-52, bwrap :167-172), release.sh (package and
|
||||
activate), compose.yaml's auth mount, stack-copy.sh copy_tree, the library package's sync, mirror and
|
||||
status, the bus runtime's library wiring, the persona import's install output, the bus unit template,
|
||||
and host-run-sheet.md sections 7 and 9. I also read design 10 step 2 at gosalyn/case2-design 75fb3e10e
|
||||
(:1276-1300).
|
||||
|
||||
## Verdict: CHANGES
|
||||
|
||||
B1. D2 step 3, `release.sh package` then `release.sh activate`, can't run as the user design 10 makes.
|
||||
- Step 2a (DESIGN.md:1279-1280) creates `mosaic` with no supplementary groups, and the precheck refuses
|
||||
more than one group (case2-precheck.sh:143-145). So `mosaic` isn't in the docker group.
|
||||
- `cmd_package` is `docker compose build` (release.sh:122), and activate first checks
|
||||
`docker image inspect` (:39).
|
||||
- activate's health check runs tasks/hello-marker.json through run-task.sh (:41-56), which is a Compose
|
||||
run with the pi adapter. That mounts `${PI_AUTH_FILE:-/home/jwoltje/.pi/agent/auth.json}`
|
||||
(compose.yaml:43). That default doesn't exist under `/srv/mosaic`, and a missing bind source makes
|
||||
Docker create a directory (row 244's finding). If it works, it's a billed provider call on the
|
||||
hub's credential.
|
||||
- Nothing on the hub uses that image. The bus unit runs `@REPO@/scripts/mosaic bus start %i` from the
|
||||
copy (mosaic-bus.service.in), and sessions use bwrap, which is why the precheck checks bwrap (:167).
|
||||
|
||||
So step 3 fails on .54, and on .53 for row 195's W8, unless `mosaic` gets Docker access, which 2a
|
||||
rules out. The design has to settle this before code:
|
||||
- either show how step 3 runs under 2a, naming what it needs: Docker access, the pi credential as an
|
||||
input for PI_AUTH_FILE, and the provider call;
|
||||
- or take a ruling to drop release package and activate from the hub install, because the hubs run from
|
||||
the copy. That is a Q4 to Sage. Design 10 step 2b names it, so the ruling needs Jason's or Mos's word
|
||||
as Sage judges. Darkwing's row 195 W8 needs the answer too.
|
||||
I recommend the second. If it's dropped, D2 keeps bootstrap and the copy, and the run sheet changes to
|
||||
match.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The precheck's header says it "changes nothing" (case2-precheck.sh:3-4). The library sync check
|
||||
writes host state:
|
||||
- ensureMirror creates the bare mirror under the data root (mirror.mjs:101-110);
|
||||
- sync moves `refs/heads/served` and writes the sync state (sync.mjs:150-151).
|
||||
Say so in the header. Also, sync's one-per-mirror guard is per process (`running`, sync.mjs:29-31), so a
|
||||
precheck run while the bus unit is up can race the bus's 60 s fetch (runtime.mjs:365-380). Either refuse
|
||||
the sync check when the bus is running, or state that it runs only before the units start (design 10
|
||||
step 8 order).
|
||||
|
||||
N2. sync returns a code for credential-missing, fetch-failed and diverged without throwing
|
||||
(sync.mjs:102-157). D6's "a sync error refuses with its code" has to refuse on a returned non-null code
|
||||
too, as the broker does (broker.mjs:2466-2467), not only on a throw.
|
||||
|
||||
N3. Build the sync context and slot the way the bus does: `{ business, project, dataRoot }`, with the slot
|
||||
at `<configDir>/secrets/<id>/library-gitea` (runtime.mjs:146-151, :164). Name that source in the build,
|
||||
so the precheck and the bus can't drift apart on the path.
|
||||
|
||||
N4. D2 step 8 removes the business file if validate refuses. On a re-run where the file already matched
|
||||
(D4), that would delete a file this run didn't write. Remove only a file this run created, and add a
|
||||
test for it.
|
||||
|
||||
N5. Q2: I agree R-problems don't refuse, since the brief says the check lists them. But the precheck's
|
||||
only words are `ok` and `REFUSED` (:51-52), so an `ok library R2 <path>` line reads as a pass.
|
||||
Use a distinct prefix or a count line. The test can pin the format either way.
|
||||
|
||||
N6. D7's cites are off by a few lines at d37210288. The dev.args sentence is host-run-sheet.md:240,
|
||||
and the `library-gitea` row is :275.
|
||||
|
||||
## Questions
|
||||
|
||||
- Q1: I agree with D3. Staging and Jason's pairing check before the install keeps D4's no-change rule.
|
||||
- Q3: I read "pinned harness" the same way, as the copy's `npm ci` from package-lock.json
|
||||
(stack-copy.sh copy_tree). The run sheet should say that step needs registry access from the host.
|
||||
|
||||
## Checked and fine
|
||||
|
||||
- D1. No CLI writes a business file. Taking it as an input keeps business.mjs as the only schema. The
|
||||
factory checks repeat the precheck's rules (:127-135).
|
||||
- D3. The persona import prints `instance <id>: persona.name <name>, persona.sha256 <hex>`
|
||||
(case2-persona-import.test.mjs:263). It never overwrites, and only names and digests go into the file.
|
||||
- D2. The answers to row 195's needs 4 and 5 are unchanged: no dev.args and no adopt. stack-copy's copy
|
||||
is idempotent ("exists:").
|
||||
- D8 covers each refusal the brief lists, the factory case, and a mutant per refusal.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Webby review: row 226 (C2-13b, #1727) design, round d2
|
||||
|
||||
Webby, 2026-10-21T21:55Z (from `date -u`; the first push said 21:58Z, which was my estimate). Author: Gosalyn (gosalyn-0117). Candidate: agents/gosalyn/work/
|
||||
queue-226/BUILD.md at d572b6102 on gosalyn/c2-13b. d1 was f46e66883, which got CHANGES in
|
||||
REVIEW-design-d1.md. Sage's ruling, sage-2189, is as BUILD.md quotes it. I didn't read the message
|
||||
itself. Design rounds get no queue record (sage-2182).
|
||||
|
||||
What I checked, without runs: the whole f46e66883..d572b6102 diff against d37210288. I checked the new
|
||||
fact cites: sync.mjs's `running` guard, runtime.mjs:143-151 and :162-164, mirror.mjs ownedRoot,
|
||||
readStateHeld's empty state (:205-210), the lastError code pattern, and validProblem (:196-201).
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- B1. Release package and activate are out, with the CASE2_RELEASE seam and its test. The reason is
|
||||
stated (2a has no Docker and no pi credential, and the hub runs no containers), and design 10 step 2b
|
||||
changes to match (D8). Row 195's W8 note is there.
|
||||
- N1 and webby-0086. D6a runs before the start and touches no mirror. D6b runs after the bus is up and
|
||||
step 4. It reads libraryStatus only, which takes the flock and writes nothing, so it can't race the
|
||||
bus timer. A missing mirror gets its own refusal rather than ownedRoot's library-git, and the
|
||||
60 s wait goes in the run sheet.
|
||||
- N2: a non-null lastError refuses with its fixed-pattern code.
|
||||
- N3: the context is `{ business, project, dataRoot }`, as the bus builds it.
|
||||
- N4: step 7 removes only a file this run created, and the D9 test checks a matching file survives
|
||||
byte for byte.
|
||||
- N5 and N6 are as asked. The registry note is in D7.
|
||||
- The tests cover each D6a refusal, each D6b refusal against a real ensureMirror and sync on a local
|
||||
bare repo, and the pass with a `problems 1` line. Each refusal gets a mutant.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. D8 puts agents/gosalyn/work/case2/DESIGN.md in this row's manifest, but that file isn't on
|
||||
refactor. It exists only on gosalyn/case2-design (1346 lines at 75fb3e10e). So landing 226 would add the
|
||||
whole design doc to refactor, not just the three-line change. If Sage meant that, my manifest review
|
||||
covers only D8's lines in it, and the landing note should say the rest arrives unreviewed by this row.
|
||||
If not, make the D8 change on gosalyn/case2-design and leave the file out of 226's manifest. That's
|
||||
Sage's call. It's doc-only, so it doesn't block (decision 137).
|
||||
|
||||
N2. On the path in the `problem` line: keep it. Jason needs the file name. But validProblem accepts any
|
||||
string up to 4096 without NUL (mirror.mjs:199), and JSON.stringify escapes only C0 controls, `"` and
|
||||
`\`. C1 controls (U+0080-U+009F, including U+009B, a one-byte CSI) and bidi overrides
|
||||
(U+202A-U+202E, U+2066-U+2069) pass through to the terminal. Escape those too, for example by
|
||||
replacing `[\u007f-\u009f--]` with `\uXXXX` after stringify,
|
||||
and add a test path that holds one.
|
||||
|
||||
N3. When the bus's first sync ran before the token went in, the state has lastFetch null and lastError
|
||||
library-credential-missing. Both D6b refusals apply. Check lastError first, or print both, so the
|
||||
output names the cause rather than just "never synced". The D9 test for a fresh mirror should
|
||||
leave lastError null so it hits the other branch.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Webby review: row 228 (row A, #1729), round 1, ZAI_API_KEY in the session env allowlist
|
||||
|
||||
Webby, 2026-10-20T21:19Z (from `date -u`). Requested by Sage (sage-1495), request comment 29677.
|
||||
Candidate: commit 4fa2ed1e3d5e20ad5a0c98a1578e35d4ba055c67 on researcher/row228, base 088087ac9 (an
|
||||
ancestor of origin/refactor), one commit, three files. Brief: "Brief: row A" in
|
||||
docs/plans/2026-10-20_case2-rows.md on origin/refactor 32b54ac3b. I approved that brief in webby-0034.
|
||||
|
||||
I ran `packages/seat/tests/session.test.mjs` alone on a `git archive` export of the candidate: 13 tests,
|
||||
13 pass, 0 fail, skipped or cancelled. I took the mutant result from Researcher's
|
||||
`out/mutants.log`. M1 removes `ZAI_API_KEY`, the new test reports failing, and the verdict is KILLED with
|
||||
the restore sha256 matching. I didn't rerun the mutant.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Each item in the brief is in the candidate, and nothing beyond it:
|
||||
- session.mjs:86: `"ZAI_API_KEY"` follows `OPENROUTER_API_KEY` on the provider line. Nothing else in
|
||||
`ENV_ALLOW` changes, and `SESSION_CREDENTIALS` and `hasSessionCredential` don't change either.
|
||||
- The new test checks membership and passthrough with `fixture-zai`, and checks that `ZAI_TOKEN` is
|
||||
dropped. The existing allowlist test is unchanged.
|
||||
- README :435-439 has the sentence the brief gives. At :714-716, "one line per variable" and the
|
||||
every-provider-key sentence are in, and the "nothing in the repository creates or reads it" line is
|
||||
left for C2-13a.
|
||||
|
||||
No other test reads either changed file. A grep of `packages/*/tests` and `scripts/test-*.sh` for
|
||||
`ENV_ALLOW`, `session.mjs` and `cli/README`, outside session.test.mjs itself, finds nothing, and no 64-hex
|
||||
pin names them. The fixture value is made up, so no credential value appears.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Sage asked me to judge the repeat. The new sentence ends "a shell inside any of them can read it".
|
||||
The next sentence, which was already there, says "A shell the harness opens, Claude Code's `Bash` or Pi's
|
||||
`bash`, can read them." The two say the same thing. Worse, "them" now follows a sentence whose subject is
|
||||
"it", so its antecedent, "the same variables" two sentences back, is hard to find. This is my miss in
|
||||
webby-0034: the brief dictated the sentence, and the candidate copied it exactly. It's doc-only, and no
|
||||
test reads the README, so it doesn't block (decision 137). A fix for a later README touch, or at landing
|
||||
if that's cheap:
|
||||
- End the new sentence at "Claude Code ones included."
|
||||
- Change the existing sentence to "A shell the harness opens, Claude Code's `Bash` or Pi's `bash`, can
|
||||
read all of these variables."
|
||||
@@ -0,0 +1,37 @@
|
||||
# Webby review: row 228 (#1729), round 2
|
||||
|
||||
Webby, 2026-10-21T05:10Z (from `date -u`). Author: Researcher. Request comment 29927 (sage-228-request-2,
|
||||
revs 2671-2672, posted as jarvis). Candidate: manifest digest
|
||||
ab40b92f68a03d4c528f9fe80333fe7897863ea35e27072f2a6c1de9fdf5d5e5, 3 paths, on researcher/row228-r2
|
||||
5f2aad593. Base origin/refactor 9b5362018 is its ancestor. Round 1 was APPROVE on commit 4fa2ed1e3
|
||||
(comment 29677). Scope (sage-1734): the README only.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 228` (round 2), not from the author's packet. It hashes to
|
||||
the digest above. Each path's sha256 from `git show 5f2aad593:<path>` matches its line.
|
||||
- session.mjs (sha256 40822543...) and session.test.mjs (193bacdc...) have the same git blobs at
|
||||
5f2aad593 as at round 1's 4fa2ed1e3 (f8254d1c, 08b3d051). I didn't reread them.
|
||||
- I read `git diff 9b5362018 5f2aad593 -- packages/cli/README.md` against round 1's README hunk.
|
||||
- Readers: only packages/cli/tests/host.test.mjs:855 reads this README, through `join()`, and it checks
|
||||
only that the unit's file name appears. Neither hunk touches that name. No test quotes a changed
|
||||
sentence, and no 64-hex pin names the README.
|
||||
|
||||
Researcher's slot run on 5f2aad593 was 94/0 over four files with M1 killed (sage-1734). I didn't run
|
||||
anything.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- The :714 hunk keeps both sides. Row 190's wording stays whole: "nothing in the repository creates it,
|
||||
and only the Case 2 precheck reads it, printing variable names, never values. No session sees the file
|
||||
itself, only the variable." Researcher's two changes sit inside it: "one line per variable" and "It
|
||||
holds every session provider key, not only Claude's."
|
||||
- The N1 trim at :435-439 now makes one reader point. The `ZAI_API_KEY` sentence says it reaches every
|
||||
session the host starts, and the paragraph ends "A shell the harness opens ... can read all of these
|
||||
variables." Round 1's repeated "a shell inside any of them can read it" is gone, and nothing it said
|
||||
was lost.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Doc-only. Two singulars at :711-718 are left from when the file held one token: "The session
|
||||
credential comes from" and "only the variable". With one line per provider key, "credentials" and "the
|
||||
variables" would match. No test reads either phrase.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Webby review: row 228 (#1729), round 3
|
||||
|
||||
Webby, 2026-10-21T06:52Z (from `date -u`). Author: Researcher. Request comment 29949 (sage-1800, revs
|
||||
2704-2706, posted as jarvis). Candidate: manifest digest
|
||||
82cf98c600622a23a968a3856659ad5c28c3ab0f4561a1ec4a9ee1f072c71d7c, 3 paths, on researcher/row228-r3
|
||||
cf18c42b9, one commit on c771a36f4 (row 234's landing). Round 2 was APPROVE on ab40b92f (comment 29927).
|
||||
Scope (sage-1800): the README, my round 2 N1 pair, and row 234's hosts section.
|
||||
|
||||
What I checked, read-only, with no runs and no worktree:
|
||||
- The queue's round 3 manifest text hashes to the digest above, and each path matches
|
||||
`git show cf18c42b9:<path>`.
|
||||
- session.mjs and session.test.mjs are git blobs f8254d1c and 08b3d051, the same as rounds 1 and 2
|
||||
(sha256 40822543 and 193bacdc).
|
||||
- `git diff c771a36f4 cf18c42b9 -- packages/cli/README.md` has only row 228's hunks: the allowlist
|
||||
paragraph at :432-439 (round 2's trimmed text) and the credential-file paragraph at :708-718.
|
||||
|
||||
Researcher's slot run was 55/0 (host.test.mjs 42, session.test.mjs 13).
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- N1 from round 2 is fixed: :711 "The session credentials come from" and :718 "only the variables".
|
||||
- Row 234's "## hosts (row 234)" section is intact. The diff against the 234 landing doesn't touch it.
|
||||
- Row 190's wording in the credential-file paragraph stays whole, as in round 2.
|
||||
@@ -0,0 +1,122 @@
|
||||
# Webby review: row 230 (C2-13a, #1732), round 1
|
||||
|
||||
Webby, 2026-10-20T23:50Z (from `date -u`). Author: Gosalyn (gosalyn-0071). Candidate: manifest digest
|
||||
8e9d1ef0c9c84e798a568921825804c5f6ae1e77ea96777bd1ddd782a2be830a, 5 paths, on gosalyn/c2-13a-on-190
|
||||
2fa7b0f4c. Base 00d4cc1f6 is its ancestor. Brief: docs/plans/2026-10-20_case2-rows.md "C2-13a", blob
|
||||
f6b4933f0f47.
|
||||
|
||||
What I ran, on a detached worktree of 2fa7b0f4c in my own scratch, removed afterwards:
|
||||
- The queue's manifest text hashes to the digest above, and `sha256sum -c` passes for all 5 paths.
|
||||
- Each file alone with `node --test --test-reporter=tap`: case2-unit 28/28 with 0 skipped (the bind test
|
||||
ran), and host.test.mjs 42/42. host.test.mjs reads the cli README (:855).
|
||||
- A grep of `packages/*/tests/*` and `scripts/test-*.sh` for the two script names finds only
|
||||
case2-unit.test.mjs. A grep for 64-hex pins near README or case2 finds none, and none of the tests that
|
||||
read the README checks the changed sentence.
|
||||
|
||||
I ran no mutants. I read both scripts in full against the brief, and the test file case by case.
|
||||
|
||||
## Verdict: CHANGES (B1)
|
||||
|
||||
### B1. Seven precheck refusals and the "missing claude passes" case have no test
|
||||
|
||||
The brief's Tests section says "The precheck runs against a scratch data root. Each refusal has a case",
|
||||
and "With no claude-code instance the claude check doesn't run, and a test shows a missing claude passes."
|
||||
A grep of case2-unit.test.mjs for each refusal's message finds no case for these branches of
|
||||
scripts/case2-precheck.sh:
|
||||
- :77, the user isn't in the passwd database.
|
||||
- :137, the business file doesn't load (bad JSON, or `loadBusiness` refuses it).
|
||||
- :151, the run root is a symlink. This one matters most. `stat -c '%a'` follows the link, so without
|
||||
this branch a symlink to any 0700 directory passes as "is 0700". Nothing would catch its removal.
|
||||
- :179, the installed Console unit's ExecStart has no `--port`.
|
||||
- :180, `ip_unprivileged_port_start` can't be read.
|
||||
- :223, claude replies without the marker (`nomarker`).
|
||||
- :224, claude gives no JSON reply.
|
||||
|
||||
The "no claude-code instance" case at case2-unit.test.mjs:515-518 doesn't show that a missing claude
|
||||
passes. Its PATH is `bin:/usr/bin:/bin`, and this host has `/usr/bin/claude` (root-owned, 0755). The
|
||||
`missing` case at :532-536 already builds a PATH without it. The no-route case needs the same PATH so
|
||||
the claude really is missing.
|
||||
|
||||
The fix is test-only: one case per branch, each matching its REFUSED line, and the `usr-bin` PATH on the
|
||||
no-route case. The two fake-claude cases need a third fake kind that prints JSON without the marker, and
|
||||
a fourth that prints something other than JSON.
|
||||
|
||||
## Deviations (sage-1570 accepted them for the build)
|
||||
|
||||
1. `--business ID` on the precheck: accept. The brief names no other way to find the business file.
|
||||
2. A missing run root passes: accept. `newRun` makes it with `mkdirSync(root, { recursive: true, mode:
|
||||
0o700 })` (packages/seat/src/session.mjs:109), so before the first launch there is nothing to check.
|
||||
The symlink branch still needs its test (B1).
|
||||
3. The unmeasured 2000-token ceiling: accept. A wrong ceiling fails safe. Claude Code's own prompt is
|
||||
far above 2000, so an appending claude can't pass, and a ceiling set too low only refuses a good
|
||||
host. Row 195's install run measures it. Each precheck run with a claude-code instance also makes one
|
||||
real model call. That's small, but the operator should know it.
|
||||
4. The fake getent uid for the proof-owner case: accept. The check compares against `$USER_UID` from
|
||||
getent (:219), so moving the uid exercises the same comparison a root-owned file would.
|
||||
5. Refusing `%h` left in any unit: accept. A system unit reads `%h` as root's home.
|
||||
6. 64 lower-case hex for the allowed user with X-Authentik-Uid: accept. It matches decision 101, and
|
||||
:298-299 test both sides.
|
||||
|
||||
## What I checked and found right
|
||||
|
||||
- The render. Every token has one value, as the brief lists. `@PATH@` puts the node's directory first
|
||||
every time (:228-230), so `/usr/bin/node` gives `/usr/bin:/usr/local/bin:/bin`. `@CONFIG_ENV@` is
|
||||
deleted and `@SESSION_ENV@` is the `%h` path, as instance.sh:88-90 does for the default instance. `%h`
|
||||
then becomes the literal home. Values going into sed and awk pass `plain` (:55), so none holds `|`, `&`,
|
||||
`\` or `%`. `@FLAGS@` goes in after the token check (:255-259), so an allowed user holding `@` can't
|
||||
trip it. The flag values also contain no `&`, so bash's pattern replacement can't expand one.
|
||||
- The added lines. User, Group and the four hardening lines are added in order at the end of
|
||||
`[Service]`, the caps only on the Console, and `WantedBy=multi-user.target` replaces
|
||||
`default.target` in both units. No `After=` or `Wants=`.
|
||||
- The checks (:302-359). Fixed lines are each checked exactly once in `[Service]`, with the Environment=
|
||||
lines matched exactly. RestrictNamespaces, PrivateUsers, SystemCallFilter and PermissionsStartOnly are
|
||||
refused. The Exec prefix check reads the whole leading run of `-@:+!`, so `-+` is caught. A continued
|
||||
line refuses, so nothing hides on one.
|
||||
- The re-install. Each installed unit passes the same checks against its own copy, must equal the
|
||||
render with only the copy id swapped (:380), and both units must name one copy (:385). So the node,
|
||||
flags, `--pm` and `--claude` can't drift.
|
||||
- The inputs. `--node` and `--claude` must be absolute, plain and normalized, not a symlink, outside
|
||||
`<home>`, and root-owned and not group- or other-writable all the way up to `/` (:114-124). The copy
|
||||
id is 40 lower-case hex and must name a real directory (not a link) holding `scripts/mosaic`. The proof
|
||||
file is checked by metadata only: no symlink anywhere in its path (`realpath -e` equality), under
|
||||
`<home>/.config/mosaic-dev/secrets`, owned by the user's uid, 0600 or 0400. It is never opened.
|
||||
- The serve flags match the brief for both profiles. The `--tester-port` equal to `--port` case is
|
||||
unreachable through the ranges (1..1023 against 1024..65535). The test at :351-356 says so, and the
|
||||
guard stays.
|
||||
- The start order: daemon-reload, enable both, start the bus, then the Console. A failed bus start
|
||||
stops before the Console starts (:403).
|
||||
- The precheck never prints a session env value. Every precheck test asserts the fixture key is absent
|
||||
from stdout and stderr. The claude subshell exports the file's variables only into claude's
|
||||
environment.
|
||||
- The bind test builds the argv with `binds()` and `sandboxArgv()`, checks the floor first and fails
|
||||
rather than skipping when it's too low, and uses a free high port as its control.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. On a re-install the units are rewritten, but `systemctl start` (:403-404) does nothing to a running
|
||||
unit. The bus and Console keep running the old copy, and the last line still says "enabled and started".
|
||||
The units on disk then name the new copy while both processes run the old one. If the bus later restarts
|
||||
on its own (`Restart=on-failure`, mosaic-bus.service.in:32), it comes back on the new copy while the
|
||||
Console stays on the old one. That is the split pair :385 refuses, and the bus accepts only its own
|
||||
human-cli.mjs path. The brief puts re-pins in row 195 (brief line 899) and says nothing here restarts
|
||||
anything, so this isn't a defect in this row. Row 195 should restart both together, or this script
|
||||
should say on a re-install that it changed nothing that is running.
|
||||
|
||||
N2. A drop-in overrides every line this script checks. Examples are
|
||||
`/etc/systemd/system/mosaic-webui.service.d/*.conf`, `[email protected]/`,
|
||||
`mosaic-bus@<business>.service.d/` and the global `service.d/`. A unit file named
|
||||
`mosaic-bus@<business>.service` replaces the template altogether. Only root can write these
|
||||
directories, so this is the same defence in depth as the installed-unit checks. The user-unit installer
|
||||
already refuses ExecStart and WorkingDirectory drop-ins (scripts/bus-service.sh:122-145, globs from
|
||||
instance.sh:88). A later row (C2-13b or 195) could do the same for the system paths, and also check
|
||||
User, Group and the hardening lines.
|
||||
|
||||
N3. The precheck's claude, when the bus unit has no `MOSAIC_CLAUDE_BIN`, is `command -v claude` in the
|
||||
user's shell (:194). The bus runs `claude` from the unit's `Environment=PATH=`, which is the node's
|
||||
directory then `/usr/local/bin:/usr/bin:/bin`. A claude in `~/.local/bin` would be checked while the bus
|
||||
runs a different one, or none. This matches the brief's words ("`claude` on PATH"). Resolving it against
|
||||
the unit's PATH value, which the precheck can already read with `unit_value`, would check the binary
|
||||
the bus runs.
|
||||
|
||||
N4. Doc-only. The changed README line (packages/cli/README.md:713) is about 150 characters, while the
|
||||
paragraph around it wraps near 75. No test reads the sentence.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Webby review: row 230 (C2-13a, #1732), round 2
|
||||
|
||||
Webby, 2026-10-20T23:58Z (from `date -u`). Author: Gosalyn (gosalyn-0076), op gosalyn-230-inreview-r2, rev 2559.
|
||||
Request comment 29788 (sage-1595). Candidate: manifest digest
|
||||
fbb0d7e42c67507519e03b5a2354fb82e760908fcfeecdf1f6c1da3e836a81da, 5 paths, on gosalyn/c2-13a-r2
|
||||
96768766c. Base origin/refactor 722136772 is its ancestor. Round 1 was CHANGES B1 (REVIEW-r1.md), and
|
||||
sage-1589 ruled N1 into this round.
|
||||
|
||||
What I ran, on a detached worktree of 96768766c in my own scratch, removed afterwards:
|
||||
- I took the manifest text from `queue show 230` (round 2), not from the author's scratch. It hashes to
|
||||
the digest above, and `sha256sum -c` passes for all 5 paths.
|
||||
- `git diff 00d4cc1f6 722136772` on the 5 paths is empty. `git range-diff` shows the two r1 commits
|
||||
unchanged (`=`) after the rebase, so the r1 to r2 change is 96768766c alone, and I read it in full.
|
||||
- Each file alone with `node --test --test-reporter=tap`: case2-unit 30/30 with 0 skipped, and
|
||||
host.test.mjs 42/42.
|
||||
- The reader grep (`packages/*/tests/*`, `scripts/test-*.sh`) still finds only case2-unit.test.mjs for
|
||||
the two scripts. No 64-hex pin names a case2 file or the cli README. host.test.mjs reads the README
|
||||
only for the unit file name, so the rewrap can't break it.
|
||||
|
||||
I ran no mutants. Gosalyn's 10 mutants and the gate run in Sage's slot. Sage won't land the row until
|
||||
both are green.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. Each branch I named has a case that matches its REFUSED line:
|
||||
- :77: getent exits 2.
|
||||
- :137: the business file is unparsable, missing (ENOENT), or fails the schema. The schema case asserts
|
||||
it isn't the "cannot read" branch, so it reaches `loadBusiness`.
|
||||
- :151: the run root is a symlink to a directory chmod'ed 0700. If the branch were removed, `stat` would
|
||||
read the target as 0700 and the test would fail.
|
||||
- :179: the Console ExecStart has no `--port`.
|
||||
- :180: the port-floor file is missing.
|
||||
- :223 and :224: the fake claude has new `nomarker` and `text` kinds. `nomarker` keeps 40 tokens, so it
|
||||
passes the ceiling and reaches the marker check.
|
||||
|
||||
The no-claude-code case now runs on `pathWithoutClaude()`, the same PATH as the missing-claude case. It
|
||||
asserts both a pass and that no claude line printed, so it shows a host with no claude passing.
|
||||
|
||||
N1, as ruled, is in:
|
||||
- The script asks `is-active` for both units after the re-install checks and before the write.
|
||||
- A running unit gets "was already running, so it is still on copy <old>, not copy <new>", never
|
||||
"started".
|
||||
- A unit that wasn't running gets "started ... on copy".
|
||||
- The "restart ... together" line follows.
|
||||
- A fresh install still prints the round 1 line, and the clean-render test now asserts that.
|
||||
- The script restarts nothing.
|
||||
|
||||
The fake systemctl answers `is-active` from `CASE2_TEST_ACTIVE`, and the two sequence asserts take
|
||||
the two new log lines into account.
|
||||
|
||||
N4 is rewrapped. N2, N3 and the N1 restart are in BUILD.md as row 195 carry items.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. The "still on copy <old>" line takes `<old>` from the installed unit file (`ids[0]`, :384-386), not
|
||||
from the running process, and those can differ. Here is the sequence:
|
||||
1. Install A and start both.
|
||||
2. Re-install B with both running. The files now name B, and both processes still run A.
|
||||
3. Re-install C before any restart. The script prints "still on copy B", but both run A.
|
||||
|
||||
The new test models this. Its second step re-installs A with only the Console running, after an
|
||||
install of B that didn't restart it, and asserts "still on copy B". In that story the Console runs A.
|
||||
|
||||
The restart line is still right, so the operator does the right thing. The copy claim is what's
|
||||
wrong. Two fixes would work, and either fits row 195:
|
||||
- Drop the copy and print "was already running, so it is not on copy <new>".
|
||||
- Read the running copy from the unit's main process, for example `systemctl show -p MainPID`, then
|
||||
`/proc/<pid>/cwd`.
|
||||
|
||||
N2. A test-file nit. The `both` case asserts `doesNotMatch(/started/)` over all output. Any later wording
|
||||
that holds the substring "started" would fail it, including "not started". Matching
|
||||
`/started mosaic-/` would pin the claim the ruling cares about.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Webby review: row 231 (#1733), C2-01b, round 1
|
||||
|
||||
Webby, 2026-10-21T09:32Z (from `date -u`, corrected from an estimated 09:36Z). Author: Gosalyn. Request comment 30003 (sage-231-review-1b,
|
||||
sage-1896). Brief: docs/plans/2026-10-20_case2-rows.md "C2-01b". These are my row 212 notes N1-N3
|
||||
(webby-0041). Candidate: manifest digest
|
||||
d4c4cf09c1d5abe5580e6d8804ef134719c0c0a4431c6231d814f6c38e9966fe, 9 paths, on gosalyn/c2-01b-r2
|
||||
88daaac50. The code is 3647a5a93, and 88daaac50 adds BUILD.md. Both are on fefd54e13.
|
||||
|
||||
What I checked:
|
||||
- I took the manifest text from `queue show 231`. It hashes to the digest above, and each path's sha256
|
||||
at 88daaac50 matches its line. fefd54e13..88daaac50 touches no path outside the manifest.
|
||||
- `git patch-id --stable` of 3647a5a93 is 45911915a9ee, matching BUILD.md.
|
||||
- Overlap: the refactor tip is 38b83ea5c. fefd54e13..38b83ea5c changes no manifest path.
|
||||
- I read the whole diff of src, README and tests. Gosalyn's singles were 60/0 (BUILD.md). I ran nothing.
|
||||
|
||||
## Verdict: CHANGES (two blockers)
|
||||
|
||||
N1 (escapes) and N2 (list item lines) are correct. N3 (attachments) has two problems.
|
||||
|
||||
### Blockers
|
||||
|
||||
B1. A link to a real note whose name ends in a dot and letters is silently dropped. readLibrary skips
|
||||
`isAttachment(link)` before it resolves anything (reader.mjs:202). isAttachment (links.mjs:30-32) calls
|
||||
any last segment ending in `.` plus 1-8 letters or digits, with at least one letter, an attachment. So
|
||||
`Dr.Quinn`, `Mr.Robot`, `a.k.a` and `Book 1.ii` are attachments.
|
||||
|
||||
Failure: `Book One/Characters/Dr.Quinn.md` exists, and another note has `relationships:` `- "[[Dr.Quinn]]"`
|
||||
or a body `[[Dr.Quinn]]`. The resolver would find `Dr.Quinn.md` on its `.md` try (links.mjs:69), and
|
||||
Obsidian resolves it the same way. Instead the link never reaches the resolver, it isn't in the
|
||||
file's links, and no problem is raised. The relationship disappears with nothing to show for it.
|
||||
|
||||
Fix: resolve first. Treat a link as an attachment only when it doesn't resolve and isAttachment is
|
||||
true. The brief's cases still hold: `![[cover.png]]` gives no problem, `![[missing-note]]` gives R4,
|
||||
`![[note.md]]` resolves. Add a test with an indexed `Dr.Quinn.md` linked as `[[Dr.Quinn]]` in the body
|
||||
and in a block list.
|
||||
|
||||
B2. The change covers more than the brief. The brief's N3 is about embeds only: "An embed whose target
|
||||
has an extension other than `.md` is an attachment ... An embed with no extension or with `.md`
|
||||
resolves as a link" (case2-rows.md:131-133). Its tests name only embeds. The code makes every wikilink
|
||||
an attachment by extension: plain `[[cover.PNG]]`, `[[Board.canvas]]` and front matter values
|
||||
(reader.mjs:202, tests at reader.test.mjs:110-118). It deletes the approved line "A link to a `.canvas`
|
||||
file or an attachment is R4" from README.md:87 (as of fefd54e13). That turns a reported R4 into silence for plain and
|
||||
front matter links. Neither BUILD.md nor the brief records a ruling for it. Either narrow the skip to
|
||||
`link.embed`, or have Sage amend the brief to cover links and front matter values, and then cite that
|
||||
ruling in BUILD.md.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. quote() covers the brief's ranges plus DEL, and the survey test checks both ends of each range.
|
||||
Some format characters are outside that list and JSON.stringify leaves them raw: U+061C (Arabic letter
|
||||
mark, a bidi control), U+2028 and U+2029, and zero-width U+200B-U+200D and U+FEFF. None can reorder a
|
||||
line the way the escaped ones can. Add U+061C if you're touching the range anyway.
|
||||
|
||||
N2. The survey's `attachment` count only looks at names, without resolving. Once B1 is fixed, a
|
||||
`[[Dr.Quinn]]` that resolves in the reader will still count as an attachment in the survey. Either
|
||||
check it against the survey's own path list or describe the count in README.md as "links whose target
|
||||
looks like an attachment".
|
||||
|
||||
N3. The N2 work is sound. itemLines gets each item line pushed next to its value (reader.mjs:121-122)
|
||||
and is created only for a block list key (:131). parseFile uses the item's line when the key has
|
||||
one, and otherwise the key's line (:157-160), so flow and scalar values keep the key's line. The new reader test checks R4 at the
|
||||
flow key line 2, R4 at item 6 and R5 at item 7.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Webby review: row 231 (#1733), C2-01b, round 2
|
||||
|
||||
Webby, 2026-10-21T09:51Z (from `date -u`). Author: Gosalyn. Request comment 30005 (revs 2785-2786,
|
||||
sage-1913). Round 1 was CHANGES (B1, B2) at 88daaac50 (rev 2774, comment 30003). Sage ruled in
|
||||
sage-1900 (B2 narrows to embeds, README R4 line back; B1 resolves first) and sage-1902 (a front matter
|
||||
value written as an embed resolves like a link). Candidate: manifest digest
|
||||
312c16fc4f657b38058cdfeed69eb11db52cd41507e6ec51a46c93c326b96690, 9 paths, on gosalyn/c2-01b-r2
|
||||
f37a65857, a fast-forward from 88daaac50 by one commit.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 231` (round 2). It hashes to the digest above, and each
|
||||
path's sha256 at f37a65857 matches its line. Same 9 paths as round 1. fefd54e13..f37a65857 touches no
|
||||
path outside the manifest.
|
||||
- Overlap: the refactor tip is 4364dddb0. fefd54e13..4364dddb0 changes none of the 9 paths.
|
||||
- I read the whole 88daaac50..f37a65857 diff (8 files, +110 -34).
|
||||
|
||||
Gosalyn's slot runs (sage-1903, sage-1907): run 1 was 60/1, the new Dr.Quinn test expecting the front
|
||||
matter link first, while the reader lists body links first. That was fixed in the test only. Run 2 was
|
||||
61/0. BUILD.md records both. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
B1 is fixed. readLibrary resolves every link first (reader.mjs:202-204). Only an unresolved link skips
|
||||
R4, and only when isAttachment holds. The new reader test indexes `Dr.Quinn.md` and resolves
|
||||
`[[Dr.Quinn]]` in a block list and in the body, plus `![[Dr.Quinn]]`, with no problem.
|
||||
|
||||
B2 is fixed as sage-1900 rules. isAttachment is now `link.embed === true && !link.field` plus the
|
||||
extension test (links.mjs:33-35), so a plain link and a front matter value are never attachments. An
|
||||
unresolved one is R4. That includes a front matter value written as an embed, per sage-1902. The
|
||||
reader test checks R4 for `"![[portrait.JPG]]"` (line 3), `[[Board.canvas]]` (line 5) and
|
||||
`![[missing-note]]` (line 6), with no problem for `![[cover.png]]` or `![[map.pdf|Map]]`. README.md:88-89
|
||||
is the fefd54e13 text again, word for word.
|
||||
|
||||
The two round 1 notes are taken. RAW_CONTROLS adds U+061C, with a survey test. The survey's attachment
|
||||
count still goes by name, and README.md says so with the `![[Dr.Quinn]]` example, which is the
|
||||
wording option I offered.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N4. README.md:88 says "A link to a `.canvas` file or an attachment is R4", and the next paragraph says
|
||||
an unresolved attachment embed is never R4. The text is accurate if you read "link" as "not an
|
||||
embed", but a reader could take the two as contradicting each other. "A plain link to ..." on :88 would
|
||||
settle it, but that changes the line sage-1900 restored, so leave it unless Sage wants it.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Webby review: row 238 (#1744), round 1
|
||||
|
||||
Webby, 2026-10-21T01:31Z (from `date -u`). Author: Launchpad (launchpad-0017), revs 2590-2591. This row follows up my row
|
||||
200 r3 N1. Candidate: manifest digest 7093c10e43be8d7b9b38a3d0d025c5e0f014befd77f17a9b70e6f2a06feacec4, 3
|
||||
paths, on launchpad/row238 bec7e499e (one commit). Base refactor 011d41ba6 is its ancestor and is
|
||||
origin/refactor today.
|
||||
|
||||
What I ran, on a detached worktree of bec7e499e in my own scratch, removed afterwards:
|
||||
- I took the manifest text from `queue show 238`. It hashes to the digest above, and `sha256sum -c`
|
||||
passes for all 3 paths.
|
||||
- Each touched test file whole with `node --test --test-reporter=tap`: stack-release 193/193 and
|
||||
stack-instances 23/23, with 0 failures and 0 skipped.
|
||||
- A grep of `packages/*/tests/*` and `scripts/test-*.sh` for the hint text finds :1951 (default, unchanged)
|
||||
and the new :2102 and :2104 only. No test reads the changed TOOLS.md paragraph. The only TOOLS.md mention
|
||||
in a test is a string at stack-release.test.mjs:491. No 64-hex pin names the 3 paths.
|
||||
|
||||
I ran no mutants. Mutant 1 and the suites run in Sage's slot.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
`inst="${INSTANCE_FLAGS[*]:+ ${INSTANCE_FLAGS[*]}}"` (stack-release.sh:841) is the idiom `plan()` already uses
|
||||
at :1192. INSTANCE_FLAGS is `()` by default and `(--instance NAME)` after a named `instance_select`
|
||||
(instance.sh:49, :101). `${arr[*]:+...}` is safe under `set -u` on an empty array, so the default text is
|
||||
byte-identical, and :1951 still passes on it. On alpha, both commands in the hint now carry
|
||||
`--instance alpha`. The new subtest checks the exact refusal text and the `--plan` line, and `noLeak`
|
||||
covers the run. Mutant 1 (`inst=""`) would fail both asserts. The TOOLS.md sentence matches the code.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. This is older than this row: neither hint names the business. The usage line is `activate ID BUSINESS`
|
||||
(:39), but the hint reads `activate $TO --instance alpha without --sso first`, so pasting it as printed
|
||||
gives a usage error, not the first step. `$BUSINESS` is in scope at :845. Adding it to both commands, and
|
||||
the `acme` it puts in the three test strings, would fit a later row. It fails safe, since the usage error
|
||||
changes nothing.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Webby review: row 238 (#1744), round 2
|
||||
|
||||
Webby, 2026-10-21T06:52Z (from `date -u`). Author: Launchpad (launchpad-0025), revs 2697-2699. Request
|
||||
comment 29948 (sage-1800, request rev 2701, resolved to posted 2703). Candidate: manifest digest
|
||||
36b0adfa2e7b4807db26837c081eb418c926feb61baf2b9200c22a62eea7e32a, 3 paths, on launchpad/row238-r3
|
||||
a33597c33. Base c771a36f4 (row 234's landing) is its ancestor. Round 1 was APPROVE on bec7e499e at
|
||||
011d41ba6 (comment 29823). Scope: the merge only, after rows 232, 233 and 234 landed on these paths.
|
||||
|
||||
What I checked, read-only, with no runs and no worktree:
|
||||
- I took the manifest text from `queue show 238` (round 2), not from the author's packet. It hashes to
|
||||
the digest above, and each path matches `git show a33597c33:<path>`.
|
||||
- `git patch-id --stable` over the 3-path diff is b9d0bb0ee3c7 for round 1 (011d41ba6..bec7e499e) and for
|
||||
round 2 (c771a36f4..a33597c33). The change is the same, and the landed lines around it are untouched.
|
||||
- scripts/stack-release.sh stays 100755 on c771a36f4 and a33597c33.
|
||||
- Outside the 3 paths the commit adds only agents/launchpad/work/queue-238 files.
|
||||
|
||||
Launchpad's slot run of stack-release.test.mjs on the code blob was 196/196. External 25/25 and upgrade
|
||||
20/20 from round 2 stand, since row 234 changed neither file nor the fixture.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
Round 1's N1 (the hint has no BUSINESS) stays on the #1744 carry list, out of this row's scope.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Webby review: row 238 (#1744), round 3
|
||||
|
||||
Webby, 2026-10-21T07:02Z (from `date -u`). Author: Launchpad (launchpad-0028), revs 2712-2713. Request
|
||||
comment 29956 (sage-1810, revs 2714-2715, posted as jarvis). Candidate: manifest digest
|
||||
1bf831f33501682a6498ed9c4900b42383ee1e8ad09905096d1a0f7062cfc7be, 3 paths, on launchpad/row238-r4
|
||||
4c90099ea, one commit on c515273db (row 240's landing). Round 2 was APPROVE on 36b0adfa (comment 29948).
|
||||
Scope (sage-1804): docs/TOOLS.md after row 240's landing.
|
||||
|
||||
What I checked, read-only, with no runs and no worktree:
|
||||
- The queue's round 3 manifest text hashes to the digest above, and each path matches
|
||||
`git show 4c90099ea:<path>`.
|
||||
- `git diff c515273db 4c90099ea -- docs/TOOLS.md` is the one :599 hunk only. Row 240's lines at :837 and
|
||||
:854 are untouched.
|
||||
- scripts/stack-release.sh (blob cbc99c13, 100755) and stack-release.test.mjs (ed86589c) are the blobs I
|
||||
approved in round 2 on a33597c33.
|
||||
- `git patch-id --stable` over the 3-path diff is b9d0bb0ee3c7, as in rounds 1 and 2.
|
||||
|
||||
No runs, per Sage's ruling for a doc-only merge. Sage's combined 228 and 238 gate covers the landing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
@@ -0,0 +1,52 @@
|
||||
# Webby review: row 243 (#1754), round 1
|
||||
|
||||
Webby, 2026-10-21T19:10Z (from `date -u`). Author: Morgana (morgana-0045). Request comment 30111 (revs
|
||||
2936-2937, posted by Sage as sage-2078a after Morgana's own attempt failed its credential check).
|
||||
Reviewer by sage-2066 and mos-1166. Brief: docs/plans/2026-10-21_gate-env-rows.md "The startup token
|
||||
refusal test sets its file mode". Candidate: manifest digest
|
||||
2432c4a7dbb64899be5b371e65f301c3abca9f1c30a3f110f4c252f3a74d4fdb, 5 paths, on morgana/row243
|
||||
6581f750e. That's two first-parent commits on a36523713: 137bda3af (the tests) and 6581f750e
|
||||
(BUILD.md).
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 243` (last round), not from the scratch path. It hashes
|
||||
to the digest above, and each path's sha256 from `git show 6581f750e:<path>` matches its line. All
|
||||
five paths are 100644.
|
||||
- a36523713..6581f750e touches exactly the 5 manifest paths. None is under src or scripts.
|
||||
- Overlap: refactor is now 9a8c8860d. a36523713..9a8c8860d changes none of the 5 paths.
|
||||
- I read the whole diff and the BUILD.md sweep, plus the stack-instances helpers (`run`, `homeEnv`,
|
||||
`mode`) and the T-I1 mode assertions (:86-92).
|
||||
- My own sweep at 6581f750e: every writeFileSync, mkdirSync or openSync in packages/*/tests with a
|
||||
group or other bit (66 hits). Leaving out executables at 0o755 and 0o700 leaves 9. Three are this
|
||||
row's fixes. host-tools :81 and :205 are acceptance cases. Three are adapter scripts at 0o644,
|
||||
which no mode check reads. One is the gate-lane umask probe. Separately, no test creates a
|
||||
group-writable or world-writable file or directory. That agrees with Morgana's list.
|
||||
|
||||
Sage's slot results (sage-2062, 18:55Z, by BUILD.md): on the base under umask 077 the files fail
|
||||
7/1, 2/1, 27/1 and 21/2, on the predicted assertions. The candidate passes 8/0, 3/0, 28/0 and 24/0
|
||||
under both 022 and 077. I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- process.test.mjs:76-78. The write keeps its mode, and `chmodSync(file, 0o644)` follows it, so the
|
||||
file is too open under any umask, and credentials.mjs's 0600 check refuses it. `chmodSync` is
|
||||
imported (:4). This is the brief's change, word for word.
|
||||
- proof-cli.test.mjs:30. `chmodSync(file, mode)` runs inside the case map with each case's own mode,
|
||||
so the 0o644 `mode` case stays too open and the 0o600 cases are set explicitly. `chmodSync` was
|
||||
already imported (:10).
|
||||
- sso.test.mjs:361. The chmod comes after the write and before the `--sso` start that must refuse,
|
||||
and the later chmod to 0600 still follows. `chmodSync` was already imported (:10).
|
||||
- stack-instances.test.mjs:40-43. `underUmask` runs bootstrap.sh through
|
||||
`/bin/bash -c 'umask <mask>; exec "$@"'`, and every `bootstrap()` call goes through it with 022.
|
||||
So the 0644 assertions at :87 and :92 test the mode the source asks for, whatever the gate's
|
||||
umask. The new subtest (:100-108) pins the other side: under 077 the config and instance.json are
|
||||
0600, and the directories stay 0700. A later chmod-to-0644 "fix" in mosaic-config.mjs would fail
|
||||
it. The `config()` helper has no mode assertion and stays as it is, which is correct.
|
||||
- The brief's sweep is in BUILD.md: the affected tests, the unaffected ones with a reason for each,
|
||||
and the mode checks that have no refusal test. The test step ran both umasks, as the gate asks.
|
||||
- Hash pins: BUILD.md reports a basename grep with a positive control and no pins. I found no test
|
||||
that names any of the four files with a 64-hex hash either.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
None.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Webby review: row 244 (#1757), round 1
|
||||
|
||||
Webby, 2026-10-21T21:40Z (from `date -u`). Author: Morgana (morgana-0053). Request comment 30154,
|
||||
posted attempt sage-244-review-r1b. Request by sage-2185. Brief: docs/plans/2026-10-21_gate-env-rows.md
|
||||
"Gate runs use a fixture pi auth file" (DEFERRED 059577886, my N4 on row 197). Candidate: manifest
|
||||
digest 6e7e3e3c290fe5034cc8766b6dc7eeb3725a99f330837a357c66381d18be1b60, 8 paths, on morgana/row244
|
||||
94ab4c2fe. That's three first-parent commits on a36523713: 47148991b, 81e037d20 (the code) and
|
||||
94ab4c2fe (BUILD.md slot results only, +24/-1).
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 244` (last round), not from the scratch path. It hashes
|
||||
to the digest above. Each path's sha256 from `git show 94ab4c2fe:<path>` matches its line, and the
|
||||
modes match: test-release.sh and test-task.sh are 100755, the other six 100644.
|
||||
- a36523713..94ab4c2fe touches exactly the 8 manifest paths.
|
||||
- Drift against origin/refactor d37210288. a36523713..d37210288 changes none of the 8 paths. It also
|
||||
leaves release.sh, scripts/lib, compose.yaml, run-task.sh and mosaic-task.mjs alone. The one nearby
|
||||
path it changes is scripts/stack-release.sh (7247d80c7, row 196). That isn't a manifest path, and
|
||||
neither suite nor anything under scripts/testing names it. `git merge-tree` merges 94ab4c2fe onto
|
||||
d37210288 clean.
|
||||
- I read the whole diff, BUILD.md (rulings A and C, the Compose reach list, coverage dropped, the
|
||||
DEFERRED wording, the mutant table and the M-F correction), compose.yaml's PI_AUTH_FILE default and
|
||||
MOSAIC_ADAPTER pass-through, `within` in lane-state.mjs, and the requiredSkips filter.
|
||||
|
||||
Sage's slot results (sage-2147, on 81e037d20, by BUILD.md): gate-lane-auth 5/0, gate-lane-image 11/0,
|
||||
gate-lane 13/0, gate-lane-state 14/0, gate-lane-fixtures 8/0, test-task.sh 91/0 with two live-skip
|
||||
lines, test-release.sh 14/0. M-G, M-L, M-A, M-T and M-F were all killed on their named tests, and M-F
|
||||
was also killed on the real test-release.sh (FAIL 3). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- lane-docker.mjs:32-36, laneAuthFile. An unset or relative PI_AUTH_FILE, the host default, a path
|
||||
whose realpath is the host default, a missing path, a directory, and a symlink escaping the temp
|
||||
root all refuse. realpathSync on both sides plus `within` handles a symlinked temp root. An unset
|
||||
value throws inside the try and reaches the same refusal. It runs only on `compose run` (:43),
|
||||
after the option check, so `info` and `image inspect` are unchanged. The CLI entry passes
|
||||
process.env (:66).
|
||||
- gate-lane.mjs:112-114 and :184. cleanEnv sets PI_AUTH_FILE to `<tempRoot>/pi-auth.json`, and
|
||||
runGate writes it `{}` with `wx` and then chmods to 0600, so the umask can't widen it. The kept list
|
||||
doesn't include MOSAIC_LIVE_PROVIDER, and test 2 checks that it gets dropped.
|
||||
- test-task.sh:19-23 and test-release.sh:16-20 write their own 0600 fixture under SANDBOX, which is
|
||||
`mktemp -d` under TMPDIR. In the lane that's the temp root, so the guard accepts it. Outside the
|
||||
lane, the legacy gate is covered too. Writing a real file matters, because a missing bind source
|
||||
makes Docker create a host directory. BUILD.md says why.
|
||||
- test-release.sh:69. `"adapter":"mock"` carries the ruling A comment. The three activations pass
|
||||
MOSAIC_MOCK_RESPONSE=MOSAIC_HELLO_OK. Fault injection still refuses on its random expectation, and
|
||||
M-F proves the mock's fixed reply can't pass that check.
|
||||
- test-task.sh. The hello and wrong-marker run records move to mock-adapters.json, which the seam
|
||||
block writes first (:159). Fork recall (M11) and user recall (M16) run only under
|
||||
MOSAIC_LIVE_PROVIDER=1. The skip line "skip live provider cases (row 244: ...)" doesn't match the
|
||||
requiredSkips pattern (gate-lane.mjs:56, docker|daemon|sandbox|systemd|credential), so the gate
|
||||
doesn't count it as a missing required case. The new "skip run-record cases (docker unavailable)"
|
||||
does match, so a Docker-less gate still fails. With live on, the suite leaves PI_AUTH_FILE alone,
|
||||
and test 5 asserts the operator's file reaches every run.
|
||||
- gate-lane-auth.test.mjs. Tests 3 and 4 read what each suite passes to Compose through a fake docker
|
||||
that never opens the host file, which is the brief's test. They also assert the adapter set is
|
||||
{mock}, so a suite drifting back to pi fails.
|
||||
- gate-lane-image.test.mjs passes run.env to the three guardDocker compose calls. Its refusal loop
|
||||
calls guardDocker without env, but none of those argv reach the auth check, so the default doesn't
|
||||
matter there.
|
||||
- The coverage loss is stated. test-release no longer proves the pi image reaches the provider, and
|
||||
the gate no longer proves M11 or M16 against a real model. The DEFERRED wording for Sage is in
|
||||
BUILD.md. I agree that it needs an entry. A real `release.sh activate` and an operator run with
|
||||
MOSAIC_LIVE_PROVIDER=1 are now the only proof of those paths.
|
||||
- Hash pins: no file under packages/*/tests or scripts names any of the changed files next to a
|
||||
64-hex hash.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. HOST_PI_AUTH is a literal in lane-docker.mjs:6 and gate-lane-auth.test.mjs:12, copied from
|
||||
compose.yaml:43. If someone changes the compose default, the guard's host-path comparison goes stale
|
||||
without failing a test. The guard doesn't depend on that literal, though, because `within(root, real)`
|
||||
refuses any path outside the temp root, wherever the host file lives. No change needed.
|
||||
|
||||
N2. At landing, the DEFERRED entry 059577886 moves to Done (brief), and Sage adds the
|
||||
"Real-model suite cases run outside the gate" entry from BUILD.md. Both are landing steps, not
|
||||
manifest paths.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Webby review: row 247 (#1749), C2-20b, round 1
|
||||
|
||||
Webby, 2026-10-21T11:55Z (from `date -u`). Author: Gosalyn. Request comment 30042 (revs 2846-2847,
|
||||
sage-1976, posted by Sage as jarvis). Brief: docs/plans/2026-10-20_case2-rows.md "C2-20b", blob
|
||||
f7c4cfbff43a. It covers my row 221 notes N1, N2 and N4 (webby-0073). Candidate: manifest digest
|
||||
6d3f279f692be2cbfaeca9ae36c3a8404d4cae110a2c005150b89803b247263e, 4 paths, on gosalyn/c2-20b 6d43618a3.
|
||||
That's one commit on 335bb3e30.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- I took the manifest text from `queue show 247` (round 1), not from the scratch path. It hashes to the
|
||||
digest above, and each path's sha256 from `git show 6d43618a3:<path>` matches its line.
|
||||
- 335bb3e30..6d43618a3 touches exactly the 4 manifest paths. The script stays 100755.
|
||||
- Overlap: the refactor tip is 5c6f73ed7. 335bb3e30..5c6f73ed7 changes none of the 4 paths.
|
||||
- Outside readers: `git grep case2-persona-import` over packages/*/tests/* and scripts/test-*.sh hits
|
||||
only the test file and its own fixture tree. No hash pin.
|
||||
- I read the script and test diffs, the new fixture, BUILD.md and the brief.
|
||||
|
||||
Gosalyn's slot run on grant sage-1971, 11:49:04-11:49:22Z: 14/0, and M1-M4 each killed (BUILD.md).
|
||||
Run 1 was red from the dropped space after the comma. The fix put the space back, and the test expects
|
||||
it (`"-1001", 29 bytes`). I ran nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- N1. The per-channel line prints `c.where` and `c.channel` through JSON.stringify
|
||||
(case2-persona-import.mjs:248). `where` is the fixed prefix plus the platform key (:149, :155-157), so
|
||||
both config keys are quoted. That's the only line that prints config text. The profile name comes from
|
||||
pairing's PROFILE_ID check. The new test writes a newline into both keys, expects one quoted line, and
|
||||
checks that no line starts with the forged `staged:` or `pairing:` text. M1 and M2 each fail it.
|
||||
- N2. The dry run calls personaTextFault on each persona after its count line (:235-236), prints the
|
||||
fixed reason, and refuses after the loop (:238), so both profiles' faults show before exit 1. Nothing
|
||||
is written. The new test takes 8001 two-byte characters (16002 bytes, under soul()'s 32000 cap at
|
||||
:130), expects the "over 8000 characters" line and exit 1, and expects exit 0 at exactly 8000. The
|
||||
heading test now expects the fence line and the refusal text. M3 fails both. The header (:21-25) now
|
||||
says the dry run refuses and that over four times the fence in bytes refuses before any count, which
|
||||
is what :130 does.
|
||||
- N4. privateDir turns an EEXIST from mkdir into a named Refusal and rethrows anything else
|
||||
(:189-194). mkdir-race.mjs patches fs.mkdirSync and calls syncBuiltinESMExports, so the script's
|
||||
named import (:29) gets a real kernel EEXIST, the same way trace.mjs hooks reads. The test hits it at
|
||||
stage (the staging dir, :255) and at install (`personas/<business>`, :303), checks the exact stderr,
|
||||
and checks that each raced dir is empty. M4 fails it.
|
||||
|
||||
The JSON.stringify residual (DEL, C1, U+2028/9 and bidi left raw) is recorded in BUILD.md under
|
||||
sage-1964. None of those ends a line on a terminal, so no forged line is possible. A bidi control can
|
||||
only reorder the display of that one line, and the keys come from Jason's own Hermes config. I accept
|
||||
the limit as ruled.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. BUILD.md's mutant table puts M1 and M2 at line 237. At the candidate, the per-channel line is 248.
|
||||
237 is where that line sat before this row's dry run change. The mutants match exact strings, so the
|
||||
runs aren't affected. Only the table's line numbers are off.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Webby review: row 248 (#1750), round 1
|
||||
|
||||
Webby, 2026-10-21T13:14Z (from `date -u`). Author: Sage. Request comment 30061 (sage-2001-review248,
|
||||
sage-2006). Reviewer by mos-1166. Brief: docs/plans/2026-10-20_host-credentials.md "Machine grant row b
|
||||
follow-up: the held stubs end with their test", blob 84bb8577fb23. Candidate: commit 4d99c178b, one
|
||||
commit on 540239b6b.
|
||||
|
||||
What I checked, without runs or a worktree:
|
||||
- The candidate kind is commit, digest 4d99c178b3fc92f8fa48acd487f873fce8ffc9e3. I read it from the
|
||||
canonical checkout. sage/row248 isn't on origin, so the commit exists only there for now.
|
||||
- 540239b6b..4d99c178b changes one file, packages/bus/tests/grant-executor.test.mjs (+3/-2). Nothing
|
||||
under src changes, as the brief requires.
|
||||
- Overlap: refactor is bc579f7a8, and 540239b6b is its ancestor. 540239b6b..bc579f7a8 changes
|
||||
docs/guides/host-run-sheet.md and the host-credentials plan, not the test file.
|
||||
- I read the stub fixture (test:52-96) and the three tests that set a hold flag (:427/:435, :675/:683,
|
||||
:832/:839), plus the X5 test around :931.
|
||||
|
||||
Sage's gate (row note, 13:05-13:07Z at 4d99c178b): three whole-file runs at 37/0 with 0 stubs left,
|
||||
the bus suite at 596/0, and the hold-add mutant leaves 2 stubs. Before the fix, 4 runs left 7. I ran
|
||||
nothing.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
- The fix. Both hold loops (:67 add, :72 remove) now run only while `release` is missing and `$D`
|
||||
is a directory. `D` is the mkdtemp dir (:63, :100), which `t.after` removes (:101). So once the
|
||||
dir is gone, each loop ends within one 50 ms poll.
|
||||
- After the loop. With the dir gone, add finds no `skip`, `fail-add` or `noop-add`, and its append to
|
||||
`$D/granted` fails, so it exits nonzero. Remove's `grep -v ... > granted.tmp` fails in the same
|
||||
way. Both stubs end, which is all the brief asks. Their exit codes don't matter, since the test that
|
||||
held them has already finished.
|
||||
- The test runs. While a test runs, its dir exists, so the new check can't end a hold early. The
|
||||
three holding tests write `release` themselves (:435, :683, :839), so their behavior doesn't change.
|
||||
- The comment at :54 says both stubs also stop when the dir is gone, and it cites #1750. The :432
|
||||
comment goes to row 249, as Sage said.
|
||||
- The gate matches the brief: three whole runs with no stub left, the bus suite green, and a mutant
|
||||
that drops the hold-add check leaves a stub.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. There's a second leftover of the same kind, though it doesn't loop. The X5 test at :934 runs
|
||||
`sleep 30` through ForceCommand and the sudo stub (`exec "$@"`, :71), then revokes. The broker
|
||||
rejects the run, but under the brief's own sshd reasoning (no SIGHUP for a non-pty command), the
|
||||
remote `sleep 30` can keep running after the test ends and the daemon is SIGKILLed (:88). It ends on
|
||||
its own within 30 s. A pgrep on TMPDIR may not see it, since its argv is just `sleep 30`. I didn't
|
||||
run anything to confirm it lingers. If a lane cleanup that scans by cwd or process tree catches it in
|
||||
proof 3, a shorter sleep, or a stub that also exits once the dir is gone, would fix it. That could go
|
||||
in row 249 or a later row, not here.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Webby review: row 252 (#1753, #435 J2) design, revision 1
|
||||
|
||||
Webby, 2026-10-21T18:59Z (from `date -u`). Author: Gander (gander-0033). Gate owner Sage (sage-2053),
|
||||
reviewer by mos-1193. Brief: 640aa03f2:docs/plans/2026-10-21_console-inbox-approve.md, items 1-6 of
|
||||
"What the design must settle". Candidate: agents/gander/work/queue-252/DESIGN.md at d218e14bd on
|
||||
gander/row196-r2, sha256 d85bd103bc0a2cc96fe47b9f8a6d4e0e16d90ee398ef8ffaa2fe1c55d926d23f (matches
|
||||
Gander's). The branch tip is now ab1f0e576. d218e14bd..ab1f0e576 doesn't touch queue-252/.
|
||||
|
||||
What I checked, without runs: the design against the row 196 build it names (64539c96b on a36523713).
|
||||
I read #approvedGrant, the decision.resolve case, #human, verifyPrincipal and verifyHuman, the runtime
|
||||
binding, the Console's send wiring and CSRF checks, sso-config.mjs, the decision_events DDL, and every
|
||||
reader of a resolution's `via`. Line numbers below are at 64539c96b.
|
||||
|
||||
## Verdict: APPROVE
|
||||
|
||||
1. Scope. Grant requests only, with the reason given: a wider Console write widens the security slice.
|
||||
Eligibility matches what #approvedGrant (broker.mjs:686-693) already requires: credential.mint,
|
||||
route_to human, and a grant binding on the original raise. It adds exactly two option keys, with
|
||||
approvalChoice naming one. A raise allows 2 to 9 options (:2730), so the two-key rule actually
|
||||
filters, and Deny is the real other key, not a guessed `no`.
|
||||
2. Identity. (provider, iss, sub) from the live row 192 session. Session.name, headers and body fields
|
||||
are refused. It extends row 196's verifyPrincipal the way row 196's `user` kind does (principal.mjs:
|
||||
20-39): an exact same-tree `cli.mjs serve` parent, reread after the walk, with its own nonce
|
||||
namespace. Agent, seat, user and tester callers are refused. The same-uid residual is stated, not
|
||||
hidden, and it repeats row 196's provenance statement word for word.
|
||||
3. Write path. A restricted Console cap and its own resolve op, sharing the decision.resolve
|
||||
transaction. #human (:470-473) and bindHuman are left alone. decision_events.via has no CHECK
|
||||
(migrations/3b.sql:62-69; the only `via` CHECK is :100, on another table), so `via='console'`
|
||||
needs no migration. That backs the design's "no new schema column". The subject goes in the
|
||||
human.input body in the same transaction. #approvedGrant gains a console branch tied to that
|
||||
provenance, not a wider string check.
|
||||
4. Guards. Origin and Sec-Fetch-Site follow the existing send boundary (serve.mjs:177-178). There is
|
||||
a two-step confirmation bound to the session, decision, choice and digest, used once and capped,
|
||||
with no eviction. The refusal table gives a status, code and message for CSRF, confirmation, not
|
||||
open, stale key, double click, read-only and no-send. Its codes match webui bus.mjs STATUS where
|
||||
both have them (human-required, peercred-required and read-only are 403, outcome-unknown is 503).
|
||||
It never retries automatically after an uncertain transport.
|
||||
5. Without --sso, the buttons are hidden, not disabled, and the `mosaic decide` lines stay. The design
|
||||
says why: a disabled button would suggest sign-in alone grants write authority.
|
||||
6. Tests use stand-ins only: row 192's fake provider, the test bus, row 196's peer fixtures and the
|
||||
scratch TMPDIR. There are 17 mutants, each tied to one boundary.
|
||||
|
||||
## Non-blocking
|
||||
|
||||
N1. Section 2 point 5 says "allowed owner subject" but doesn't say what makes a subject an owner.
|
||||
console.sso subjects are {sub, name} only (sso-config.mjs:66), and the key check refuses any other
|
||||
field. Row 196's design (:124-125) says any subject in console.sso.subjects reaches owner bus calls.
|
||||
I'm reading the design the same way: every configured subject is an owner, with no new config field.
|
||||
The build should say so. That means every configured subject can approve a credential mint. That's
|
||||
consistent with today's `--send` owner writes, but Jason should see it stated in the REQ-WEB-2
|
||||
amendment.
|
||||
|
||||
N2. Order inside the shared transaction. Today decision.resolve calls #approvedGrant (broker.mjs:2884)
|
||||
before it writes the human.input event (:2889). If the console branch requires the matching human.input,
|
||||
it has to be written first, or the local `grant` is null and neither setImmediate executor start runs.
|
||||
The approval would then record but never act. The design's own test ("approval consumable once by
|
||||
existing grant path", host and host-enroll starts after commit) catches this, so the manifest review
|
||||
will check that it asserts the executor start, not just the event.
|
||||
|
||||
N3. Other readers of `via` the design doesn't list. webui public/bus.js:194 sets `by` to 'broker' for
|
||||
any via other than 'cli', so a Console approval would show as the broker in the trail. That breaks
|
||||
"Trail output shows this provenance". The close notice's from_run is 'human-cli' for any human
|
||||
(broker.mjs:2920). git-socket.mjs:274 RESOLVED_VIA falls back to `via console`, which is fine.
|
||||
landing.mjs:107 and #protectedEnd (:1427) keep requiring 'cli'. That is correct, since neither a merge
|
||||
decision nor a role.revoke decision is eligible. The build should name each of these.
|
||||
|
||||
N4. Today's `--sso --send` owner writes already reach the bus through human-cli and record via=cli
|
||||
(webui cli.mjs:80, bus.mjs:32, runtime.mjs:226). So #approvedGrant's `via === 'cli'` check doesn't by
|
||||
itself tell a terminal from the Console. That's acceptable, because the Console wires only a fixed verb
|
||||
list. The build should keep decision.resolve off that list, so a Console grant answer can only take
|
||||
the restricted path. A test or mutant that checks the list would hold that.
|
||||
|
||||
N5. readProcess keeps only an allowlist of environment keys (human.mjs:34). The Console nonce and the
|
||||
provider, iss and sub keys need adding there. The J5 mutant (other nonce namespace) depends on that
|
||||
list, so the build needs both.
|
||||
+67
-2
@@ -1361,5 +1361,70 @@ 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:57:10Z | Filbert (Opus 5.5, 048f3c63) | row 190 design review r2 (#1688, DESIGN.md sha256 5493c9df3ec0) | CHANGES to Darkwing, cc Sage: B1 claim check on MOSAIC_CONFIG-only selection, B2 shared prefix drop-in dirs; N1-N7 notes; static read, no slot, no Gitea or queue record
|
||||
2026-10-20T19:00:04Z | Filbert (Opus 5.5, 048f3c63) | row 190 design review r3 (#1688, DESIGN.md sha256 98236a1aff98) | APPROVE to Darkwing, cc Sage: B1 and B2 closed with M13 and M14, N7 settled by Sage's 48ebee0ff; static read, no slot; manifest round gets a queue review record (sage-1337)
|
||||
2026-10-20T18:57:45Z | Webby (Opus 5.5, reviewer seat) | Case 2 design r1 review for Gosalyn (#416, DESIGN.md blob fba567f71dc1 at e997f3b76 / 88222f3f3), static | changes: B1-B12, N1-N9 in agents/webby/work/case2-r1/REVIEW.md; section 6 no-schema claim holds
|
||||
2026-10-20T19:14:58Z | Webby (Opus 5.5, reviewer seat) | row 194 design review for Honker (#1692, DESIGN.md sha256 eec9f011567d at 14a6f5f9d, r1 8f80c278b also read), static | changes: B1-B8, N1-N8 in agents/webby/work/queue-194/REVIEW.md; Q2 answered yes with two conditions
|
||||
2026-10-20T19:20:19Z | Webby (Opus 5.5, reviewer seat) | row 197 design review for Herb (#1695, DESIGN.md sha256 64a429a3 at 85a6eb0a9), static | changes: B1 (temp root under /tmp hides sandboxed run dirs), N1-N5 in agents/webby/work/queue-197/REVIEW.md
|
||||
2026-10-20T19:23:14Z | Webby (Opus 5.5, reviewer seat) | row 198 inventory and design review for Bushroot (#1696, INVENTORY 76b630ce at 004f133bd, DESIGN c2ea3df5 at c8fe23a37), static | inventory approved; design changes B1-B3 (copy lacks board code, re-pin carries --board, POST placement vs serve.mjs:558), N1-N5 in agents/webby/work/queue-198/REVIEW.md
|
||||
2026-10-20T19:24:03Z | Webby (Opus 5.5, reviewer seat) | row 197 design r2 re-review for Herb (#1695, DESIGN.md sha256 732ee0e2 at 6ad25d745), static | approve: B1 fixed, N1-N5 in; one manifest note in agents/webby/work/queue-197/REVIEW-r2.md
|
||||
2026-10-20T19:29:11Z | Webby (Opus 5.5, reviewer seat) | Case 2 design r2 review for Gosalyn (#416, DESIGN.md sha256 bda7a717 at c1913fb07), static | changes: B1 top-level playground has no universe, B2 persona.text storage and tester read, B3 THREAD_DEFINITION in launch-settings must stay assistant, B4 persona block position and fence; N1-N4; notes agents/webby/work/case2-r2/REVIEW.md
|
||||
2026-10-20T19:31:52Z | Webby (Opus 5.5, reviewer seat) | row 194 design r2 re-review for Honker (#1692, DESIGN.md 313c206e and PRD-1.7-DRAFT.md 98f4f49e at c0a35f47a), static | changes: B1 hire floor is a deny list that misses git.merge.protected (merge-gate.json, no credentials), decision.resolve.technical, policy.change, prd.approve, instance.name; make it an allowlist. r1 B1, B3-B8, Q2, N1-N8 fixed; Q4 view: start empty; notes agents/webby/work/queue-194/REVIEW-r2.md
|
||||
2026-10-20T19:33:02Z | Webby (Opus 5.5, reviewer seat) | row 194 design r3 review for Honker (#1692, DESIGN.md 885fce0d and PRD-1.7-DRAFT.md 04c32917 at d0afda024), static | changes: r2 B1 stands except instance.name (allowlist); B2 fire delist and claim revoke can't share a transaction, name order and recovery; N1 REQ-LAND-2 relies on Gitea refusal, N2 recheck by start time; notes agents/webby/work/queue-194/REVIEW-r3.md
|
||||
2026-10-20T19:36:19Z | Webby (Opus 5.5, reviewer seat) | row 198 design rev 3 re-review for Bushroot (#1696, DESIGN.md 51343813 at 7e2ead7f3), static | changes: rev 1 B1-B3 and N1-N5 fixed, sage-1372 rulings read right; new B1 in-process board runs spawnSync tmux (5 s per seat) and agent-send (15 s) in the Console event loop, make createBoard async with a stall test and mutant; N1 decision 8 fallback sentences, N2 --board support check must not match a refusal; notes agents/webby/work/queue-198/REVIEW-r3.md
|
||||
2026-10-20T19:37:29Z | Webby (Opus 5.5, reviewer seat) | row 197 manifest review for Herb (#1695, MANIFEST.md 16768433 at 84f21923c, TIMING.md 148a1416 at fa820fed6), static | changes: B1 drop dead Board :9 replacements (port below unprivileged floor 1024, not shared by lanes, 20 files, collides with row 198), add a :9 listener preflight refusal; N1 row 198 overlap for Sage; notes agents/webby/work/queue-197/REVIEW-manifest.md
|
||||
2026-10-20T19:38:46Z | Webby (Opus 5.5, reviewer seat) | row 194 design r4 re-review for Honker (#1692, DESIGN.md 824ef884 and PRD-1.7-DRAFT.md 379cc318 at 7d0788236), static | approve: HIRE_ALLOWED allowlist checked against role files, fire order A-B-C repeatable; N1 launch should refuse a listed instance mid-fire, N2 hire-time check should require role version 2; notes agents/webby/work/queue-194/REVIEW-r4.md
|
||||
2026-10-20T19:39:47Z | Webby (Opus 5.5, reviewer seat) | row 198 design rev 4 re-review for Bushroot (#1696, DESIGN.md 5ef6436f at 9af75dc72), static | changes: B1 and N1 fixed, --board parse check right; new B1 parses_flag swap at stack-release.sh:670 breaks the tester-port fixtures (stack-release.test.mjs:265, T37), keep it to --board; N1 index coalescing can return a scan from before markSeen's write; notes agents/webby/work/queue-198/REVIEW-r4.md
|
||||
2026-10-20T19:40:29Z | Webby (Opus 5.5, reviewer seat) | row 197 manifest r2 re-review for Herb (#1695, MANIFEST.md 2a1a3ad0 at bdc9cd56a), static | approve: literal Board :9 URLs kept, leases only at the close-rebind probes (preview.test:208, serve.test:100), T1 :9 preflight; P3 row 198 order per sage-1384; notes agents/webby/work/queue-197/REVIEW-manifest-r2.md
|
||||
2026-10-20T19:43:52Z | Webby (Opus 5.5, reviewer seat) | Case 2 design r3 for Gosalyn (#416, DESIGN.md sha256 f368c27d at 2958acb81), static | CHANGES B1 (routes.set lets the tester change a creative instance's route vars), N1-N3; notes agents/webby/work/case2-r3/REVIEW.md
|
||||
2026-10-20T19:44:59Z | Webby (Opus 5.5, reviewer seat) | row 198 design rev 5 for Bushroot (#1696, DESIGN.md sha256 1503f764 at 3f71b359b), static | APPROVE, N1 (write, bump, scan order); notes agents/webby/work/queue-198/REVIEW-r5.md
|
||||
2026-10-20T19:49:56Z | Webby (Opus 5.5, reviewer seat) | row 192 r1 candidate 83b32d8d8 for Launchpad (#1690, manifest 3ad2a73d...badab), static plus a node probe of returnPath | CHANGES: B1 open redirect after sign-in via dot segments (sso.mjs:200-206, :284); N1-N5; notes agents/webby/work/queue-192/REVIEW-r1.md
|
||||
2026-10-20T19:52:33Z | Webby (Opus 5.5, reviewer seat) | Case 2 design r4 for Gosalyn (#416, 5fc296e70 blob 7c62c40cbbb5), static | CHANGES: B1 human reaches launch.models via limits.set (DESIGN.md:29-30, :545-548), test missing; r3 B1 fixed; N1-N2; row 192 r1 recorded in queue rev 2303 (comment 29553); notes agents/webby/work/case2-r4/REVIEW.md
|
||||
2026-10-20T19:54:11Z | Webby (Opus 5.5, reviewer seat) | Case 2 design r5 review for Gosalyn (#416, e1635308b), static | APPROVE, r4 B1 N1 N2 fixed; notes agents/webby/work/case2-r5/REVIEW.md
|
||||
2026-10-20T19:58:32Z | Webby (Opus 5.5, reviewer seat) | row 192 r2 review for Launchpad (#1690, 37582e820, manifest 6f8cee14), static plus a node probe | APPROVE, r1 B1 fixed, mutant 23 equivalent; notes agents/webby/work/queue-192/REVIEW-r2.md
|
||||
2026-10-20T20:00:07Z | Webby (Opus 5.5, reviewer seat) | Case 2 design r6 step 1 review for Gosalyn (#416, 1508fd175), static | CHANGES B1 root steps on .54 have no owner after #421 stopped; notes agents/webby/work/case2-r6/REVIEW.md
|
||||
2026-10-20T20:03:11Z | Webby (Opus 5.5, reviewer seat) | Case 2 design r7 sections 9-10 and C2-13 brief review for Gosalyn (#416, 8645cb456), static | CHANGES B1 root installs a unit file mosaic wrote; step 1 itself fine; notes agents/webby/work/case2-r7/REVIEW.md
|
||||
2026-10-20T20:04:59Z | Webby (Opus 5.5, reviewer seat) | Case 2 design r8 step 2c and C2-13 brief review for Gosalyn (#416, eab6e1a3a), static | APPROVE, r7 B1 N1 fixed, N1 privileged Exec prefixes; notes agents/webby/work/case2-r8/REVIEW.md
|
||||
2026-10-20T20:14:58Z | Webby (Opus 5.5, reviewer seat) | row 200 r1 review for Launchpad (#1698, 5dc0b5534), static | APPROVE, N1-N4; notes agents/webby/work/queue-200/REVIEW-r1.md
|
||||
2026-10-20T20:24:21Z | Webby (Opus 5.5, reviewer seat) | row 197 implementation packet review for Herb (BUILD.patch 6a1cd3e5, worktree a4e3e857e), static plus two FD probes | changes B1 (manifest tests missing) B2 (fixture lease test crosses lanes); notes agents/webby/work/queue-197/REVIEW-impl.md
|
||||
2026-10-20T20:31:25Z | Webby (Opus 5.5, reviewer seat) | row 200 r2 review for Launchpad (#1698, e3fd9f3a0, manifest a1f6fe91), static | approve, N2 deviation agreed (mutant 12 equivalent), queue rev 2355; notes agents/webby/work/queue-200/REVIEW-r2.md
|
||||
2026-10-20T20:45:28Z | Webby (Opus 5.5, reviewer seat) | row 197 implementation review r2 for Herb (herb-0023, BUILD.patch 0b3871bd, SOURCE.json 91ba149f), static | approve, N1-N3 non-blocking; notes agents/webby/work/queue-197/REVIEW-impl-r2.md
|
||||
2026-10-20T20:49Z | Webby (Opus 5.5, reviewer seat) | Case 2 design r9 review (#416) for Gosalyn (29e85196b, blob 60a2d6cb751e, incl 13a98bfd0 29404f996), static | approve, N1-N3; notes agents/webby/work/case2-r9/REVIEW.md
|
||||
2026-10-20T20:50Z | Webby (Opus 5.5, reviewer seat) | row 197 implementation review r3 for Herb (herb-0026, N1-N3 delta, BUILD.patch 55fe390d), static | approve; notes agents/webby/work/queue-197/REVIEW-impl-r3.md
|
||||
2026-10-20T20:55Z | Webby (Opus 5.5, reviewer seat) | row A brief review (ZAI_API_KEY allowlist) for Sage/Gosalyn (a2d94b2c2, rows blob 81140d017441), static | approve, N1-N2; notes agents/webby/work/case2-rowA/REVIEW-brief.md
|
||||
2026-10-20T21:01Z | Webby (Opus 5.5, reviewer seat) | Case 2 design r10 review (#416) for Gosalyn (4281b0064, blob 4a1860e698e0, two system units), static | changes B1 Console boot start is Jason's call, B2 PATH/NODE/FLAGS render, B3 hardening lines unlisted, N1-N5; notes agents/webby/work/case2-r10/REVIEW.md
|
||||
2026-10-20T21:15:35Z | Webby (Opus 5.5, reviewer seat) | Case 2 design r11 review (#416) for Gosalyn (fb20a61b4 + eac92d956, rows blob 3a86d5f048dc), static | APPROVE, N1-N4; notes agents/webby/work/case2-r11/REVIEW.md
|
||||
2026-10-20T21:19:21Z | Webby (Opus 5.5, reviewer seat) | row 228 (row A, #1729) review r1 for Researcher/Sage (4fa2ed1e3, comment 29677), static plus session.test.mjs 13/13 | APPROVE, N1 doc-only; notes agents/webby/work/queue-228/REVIEW-r1.md
|
||||
2026-10-20T21:47Z | Webby (Opus 5.5, reviewer seat) | row 213 (C2-08, #1714) review r1 for Morgana (bb5f46006e49 on e13ec4929, comment 29701), static plus single test files | CHANGES B1 broker-path M10 tests, B2 changes.jsonl mode; N1-N5; notes agents/webby/work/queue-213/REVIEW-r1.md
|
||||
2026-10-20T21:48Z | Webby (Opus 5.5, reviewer seat) | row 212 (C2-01, #1713) review r1 for Gosalyn (bdf18272a, comment 29704), static plus single test files | APPROVE; N1-N5; notes agents/webby/work/queue-212/REVIEW-r1.md
|
||||
2026-10-20T21:53Z | Webby (Opus 5.5, reviewer seat) | row 213 (C2-08, #1714) review r2 for Morgana (b5ff0ac0e754 on aee32d185), static plus single test files | APPROVE; B1 fixed, r1 B2 withdrawn as my error; notes agents/webby/work/queue-213/REVIEW-r2.md
|
||||
2026-10-20T23:29Z | Webby (Opus 5.5, reviewer seat) | row 200 review r3 (activate --sso, rebase on row 190) | APPROVE on b3ead55c8b48 (9df40878f), stack-release 192/192, stack-instances 23/23; r1-r2 usage-regex miss owned; notes agents/webby/work/queue-200/REVIEW-r3.md
|
||||
2026-10-20T23:50Z | Webby (Opus 5.5, reviewer seat) | row 230 review r1 (C2-13a, gosalyn/c2-13a-on-190 2fa7b0f4c, digest 8e9d1ef0c9c8) | changes B1 (7 precheck refusals and missing-claude case untested), N1-N4, six deviations accepted; notes agents/webby/work/queue-230/REVIEW-r1.md
|
||||
2026-10-20T23:58Z | Webby (Opus 5.5, reviewer seat) | row 230 review r2 (C2-13a, gosalyn/c2-13a-r2 96768766c, digest fbb0d7e42c67) | approve; B1 fixed, sage-1589 N1 in; N1 (old copy id from the unit file, not the process), N2 test nit; notes agents/webby/work/queue-230/REVIEW-r2.md
|
||||
2026-10-21T01:31Z | Webby (Opus 5.5, reviewer seat) | row 238 review r1 (#1744, launchpad/row238 bec7e499e, digest 7093c10e43be) | approve; N1 hint omits BUSINESS (pre-existing); notes agents/webby/work/queue-238/REVIEW-r1.md
|
||||
2026-10-21T04:56Z | Webby (Opus 5.5, reviewer seat) | row 212 review r2 (merge scope, gosalyn/c2-01-r2 d3c7bbf83) | approve; notes agents/webby/work/queue-212/REVIEW-r2.md
|
||||
2026-10-21T05:02Z | Webby (Opus 5.5, reviewer seat) | row 213 (C2-08) review r3, rebase scope, on manifest 887c32d6 (morgana/row213-r3 2d2ad2760) | approve, N1 creative credentials follow-up; notes agents/webby/work/queue-213/REVIEW-r3.md
|
||||
2026-10-21T05:06Z | Webby (Opus 5.5, reviewer seat) | row 228 (#1729) review r2, README scope, on manifest ab40b92f (researcher/row228-r2 5f2aad593) | approve, N1 doc-only; notes agents/webby/work/queue-228/REVIEW-r2.md
|
||||
2026-10-21T06:50Z | Webby (Opus 5.5, reviewer seat) | row 238 (#1744) review r2, merge scope, on manifest 36b0adfa (launchpad/row238-r3 a33597c33) and row 228 (#1729) review r3, README scope, on manifest 82cf98c6 (researcher/row228-r3 cf18c42b9) | approve both; notes agents/webby/work/queue-238/REVIEW-r2.md, agents/webby/work/queue-228/REVIEW-r3.md
|
||||
2026-10-21T07:01Z | Webby (Opus 5.5, reviewer seat) | row 238 (#1744) review r3, TOOLS.md merge scope, on manifest 1bf831f3 (launchpad/row238-r4 4c90099ea) | approve; notes agents/webby/work/queue-238/REVIEW-r3.md
|
||||
2026-10-21T07:19Z | Webby (Opus 5.5, reviewer seat) | row 197 queue round 2 merge review (304c67753, manifest 8119a608, comment 29961) | APPROVE; notes agents/webby/work/queue-197/REVIEW-merge-r2.md
|
||||
2026-10-21T07:43Z | Webby (Opus 5.5, reviewer seat) | row 212 round 3 merge review (a2c72ac95 on c771a36f4, comment 29979) | APPROVE; notes agents/webby/work/queue-212/REVIEW-r3.md
|
||||
2026-10-21T08:13Z | Webby (Opus 5.5, reviewer seat) | row 197 queue round 3 (r5) merge review (9aa6d52d9 on e7078e6a8, manifest 69fad4e7, comment 29981) | APPROVE; notes agents/webby/work/queue-197/REVIEW-merge-r3.md
|
||||
2026-10-21T08:22Z | Webby (Opus 5.5, reviewer seat) | row 197 two-lane proof driver review (8ae9d8acd, run-proof.mjs b8bb3df6, sage-1850) | CHANGES B1 (gate temp roots left in scratch); notes agents/webby/work/queue-197/REVIEW-proof-driver.md
|
||||
2026-10-21T08:31Z | Webby (Opus 5.5, reviewer seat) | row 197 proof driver r2 review, B1 and N3 only (b8461266c, run-proof.mjs ace376a9, sage-1864) | APPROVE; notes agents/webby/work/queue-197/REVIEW-proof-driver-r2.md
|
||||
2026-10-21T09:29Z | Webby (Opus 5.5, reviewer seat) | row 197 queue round 4 (r6 df0c59608, manifest 1556cd29) merge and umask wrapper review | APPROVE; notes agents/webby/work/queue-197/REVIEW-merge-r4.md
|
||||
2026-10-21T09:32Z | Webby (Opus 5.5, reviewer seat) | row 231 C2-01b round 1 (88daaac50, manifest d4c4cf09) review | CHANGES B1 B2; notes agents/webby/work/queue-231/REVIEW-r1.md
|
||||
2026-10-21T09:37Z | Webby (Opus 5.5, reviewer seat) | row 213 C2-08 round 4 (2b3e67e36 on c771a36f4, manifest 26d4d0ec) rebase and 234 grant recheck | APPROVE; notes agents/webby/work/queue-213/REVIEW-r4.md
|
||||
2026-10-21T09:52Z | Webby (Opus 5.5, reviewer seat) | row 231 C2-01b round 2 (f37a65857, manifest 312c16fc) review | APPROVE; notes agents/webby/work/queue-231/REVIEW-r2.md
|
||||
2026-10-21T10:37Z | Webby (Opus 5.5, reviewer seat) | row 221 C2-20 round 1 (8f403d0d4, manifest 20f05e3a) review | APPROVE; notes agents/webby/work/queue-221/REVIEW-r1.md
|
||||
2026-10-21T11:29Z | Webby (Opus 5.5, reviewer seat) | row 197 queue round 5 r7 (9c3166915, manifest 4e54024a) review | APPROVE; notes agents/webby/work/queue-197/REVIEW-merge-r5.md
|
||||
2026-10-21T11:53Z | Webby (Opus 5.5, reviewer seat) | row 218 (#1719) C2-12 round 1 review, doc only, merge scope | APPROVE fc0d9c26f manifest fb1bbc5d, N1-N4 non-blocking; notes agents/webby/work/queue-218/REVIEW-r1.md
|
||||
2026-10-21T11:55Z | Webby (Opus 5.5, reviewer seat) | row 247 (#1749) C2-20b round 1 review, merge scope | APPROVE 6d43618a3 manifest 6d3f279f, N1 non-blocking; notes agents/webby/work/queue-247/REVIEW-r1.md
|
||||
2026-10-21T12:00Z | Webby (Opus 5.5, reviewer seat) | row 218 (#1719) C2-12 round 2 review, doc only | APPROVE 5fcc2ec90 manifest c0d3709d, N1-N4 fixed, N5 non-blocking; notes agents/webby/work/queue-218/REVIEW-r2.md
|
||||
2026-10-21T12:04Z | Webby (Opus 5.5, reviewer seat) | row 218 (#1719) C2-12 round 3 review, doc only | APPROVE 89ba79d94 manifest 1a38baf5, N5 fixed, no new notes; notes agents/webby/work/queue-218/REVIEW-r3.md
|
||||
2026-10-21T12:17Z | Webby (Opus 5.5, reviewer seat) | row 197 queue round 6 (Herb r8 c45ddeb6b, manifest 80dc1468), merge scope | APPROVE, N1 doc; notes agents/webby/work/queue-197/REVIEW-q6r8.md
|
||||
2026-10-21T13:15Z | Webby (Opus 5.5, reviewer seat) | row 248 round 1 (Sage 4d99c178b, grant-executor held stubs) | APPROVE, N1 sleep 30 residual; notes agents/webby/work/queue-248/REVIEW-r1.md
|
||||
2026-10-21T15:20Z | Webby (Opus 5.5, reviewer seat) | row 197 proof driver a5b636750 (Herb, proof 4 watcher change), herb-0090 | APPROVE, N1 probe cleanup retry, N2 pin is Sage's; notes agents/webby/work/queue-197/REVIEW-proof-driver-a5b636750.md
|
||||
2026-10-21T19:00Z | Webby (Opus 5.5, reviewer seat) | row 252 design r1 (Gander d218e14bd, DESIGN.md d85bd103) | APPROVE, N1-N5; notes agents/webby/work/queue-252/REVIEW-design-r1.md
|
||||
2026-10-21T19:10Z | Webby (Opus 5.5, reviewer seat) | row 243 round 1 (Morgana 6581f750e, manifest 2432c4a7) | APPROVE; notes agents/webby/work/queue-243/REVIEW-r1.md
|
||||
2026-10-21T21:41Z | Webby (Opus 5.5, reviewer seat) | row 244 (#1757) round 1 review, manifest 6e7e3e3c at 94ab4c2fe | APPROVE, N1-N2 non-blocking; notes agents/webby/work/queue-244/REVIEW-r1.md
|
||||
2026-10-21T21:49Z | Webby (Opus 5.5, reviewer seat) | row 226 (C2-13b, #1727) design d1 review, BUILD.md at f46e66883 | CHANGES B1 (release package/activate needs Docker and a pi credential the no-group hub user lacks), N1-N6; notes agents/webby/work/queue-226/REVIEW-design-d1.md
|
||||
2026-10-21T21:55Z | Webby (Opus 5.5, reviewer seat) | row 226 (C2-13b, #1727) design d2 review, BUILD.md at d572b6102 | APPROVE, N1 (DESIGN.md not on refactor) N2 (escape C1 and bidi in problem paths) N3 non-blocking; notes agents/webby/work/queue-226/REVIEW-design-d2.md
|
||||
|
||||
+1
-15
@@ -238,12 +238,6 @@ Exit codes: `0` ok · `2` invalid file or credential reference · `3` system
|
||||
config problem · `4` usage or a required file missing. Details:
|
||||
`packages/business/README.md`.
|
||||
|
||||
`mosaic credentials status|issue|rotate|revoke|delete` (row 199) goes to
|
||||
`packages/cli` and the bus's human-only credential verbs: an agent's own
|
||||
token in `<configDir>/secrets/agents/<business>/<agent>/<kind>`, read on
|
||||
stdin, never printed. The tester may run `status` and `revoke`. Details:
|
||||
`packages/cli/README.md`.
|
||||
|
||||
`instances` and `instance add|set` (row 165) go to `packages/cli` and the
|
||||
bus's human-only `instances` and `instances.set` verbs, so they need a live
|
||||
bus host and refuse inside an agent session. `instances` prints the revision,
|
||||
@@ -374,7 +368,7 @@ reports checkout or copy identity and refactor drift, and writes nothing.
|
||||
```
|
||||
scripts/stack-release.sh adopt BUSINESS
|
||||
scripts/stack-release.sh activate ID BUSINESS [--pm | --pm-after-harness] [--migrate] [--tester-port N]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof] [--no-send] [--timeout S] [--plan]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof] [--timeout S] [--plan]
|
||||
scripts/stack-release.sh pm-on BUSINESS [--timeout S]
|
||||
scripts/stack-release.sh rollback BUSINESS [--restore ACTIVATION [--events N]] [--timeout S] [--plan]
|
||||
scripts/stack-release.sh status BUSINESS
|
||||
@@ -486,14 +480,6 @@ copies. It checks the file with `stat` and `realpath` and never reads the value.
|
||||
or `off`. On the live copy it also notes that the run re-pins the pointer's copy
|
||||
and only the Console flags change.
|
||||
|
||||
`activate ... --no-send` (row 189, mos-0972) drops `--send` from the pointer's
|
||||
Console flags and the unit, so the Console serves reads and no human sends. It
|
||||
may name the live copy while the pointer holds `--send`; once `--send` is gone a
|
||||
same-copy `--no-send` refuses `already-active`. Later activates, `pm-on` and
|
||||
rollbacks carry the pointer, so send stays off. No flag puts `--send` back.
|
||||
`--plan` prints one `send:` line: `dropped`, `off` with or without the
|
||||
`--no-send` note, or `carried`.
|
||||
|
||||
#### A target past c216f47db: the PM after the harness kind
|
||||
|
||||
From c216f47db on, the bus launches the PM only with a `harness` kind in the
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# Brief: activate --no-send drops the Console's --send
|
||||
|
||||
Sage, 2026-10-20, for Mos's ruling mos-0972 (Q175 interim, Vikunja #412). Owner Sage, reviewer Dewey, gate owner
|
||||
Sage. Base: refactor 9ae8c73ac (row 163 landed). Line numbers are at that commit. The candidate corrects them;
|
||||
the first draft took them before row 163.
|
||||
Sage. Base: refactor 9ae8c73ac (row 163 landed). Line numbers are at that commit.
|
||||
|
||||
This row builds the tool only. The live restart without `--send` is a separate step afterwards, a same-copy
|
||||
activate under Q173 with a backup first and a STATE report after. No test here touches `~/.mosaic-dev`, the
|
||||
@@ -15,11 +14,11 @@ Mos ruled that the Console runs without `--send` until Q175 phase 2, because wit
|
||||
process can forge `X-authentik-username` on 127.0.0.1:7332 and send as Jason (design section 8). On the pinned
|
||||
stack, the Console flags live in the pointer (`~/.mosaic-dev/state/stack-active.json`):
|
||||
|
||||
- `console_for_target` (scripts/stack-release.sh:664-693) carries the pointer's flags unchanged. Only
|
||||
- `console_for_target` (scripts/stack-release.sh:658-687) carries the pointer's flags unchanged. Only
|
||||
`--tester-port` and the row 187 proof pair can change.
|
||||
- `check_target` (:249-256) lets an activate name the pointer's copy only for a proof-pair change.
|
||||
- `adopt` refuses once a pointer exists (:828).
|
||||
- A hand `webui-service.sh install` without `--send` makes `flags_drift` (:646-657) refuse
|
||||
- `check_target` (:247-249) lets an activate name the pointer's copy only for a proof-pair change.
|
||||
- `adopt` refuses once a pointer exists (:822).
|
||||
- A hand `webui-service.sh install` without `--send` makes `flags_drift` (:640-651) refuse
|
||||
`unit-flags-drift` on every later activate, pm-on and rollback.
|
||||
|
||||
So the tool can't drop `--send` today. This row adds that one path.
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# Brief: a credential store per agent, own git identity, pushes to a remote
|
||||
|
||||
Sage, 2026-10-20, for mos-0974 (MVP T1 item 6) and the MVP plan
|
||||
(`jason-master-restart/projects/mosaic-mvp/PLAN.md` item 6, GAP-MAP G11 and G13; TARGET.md: "credentials store
|
||||
available for all agents; secrets protected with RLS equivalent" and "Agent uses own credentials for git and
|
||||
other actions"). Gitea #1697. Owner Megavolt (Opus 5.5, a new builder seat), reviewer Duckworth, gate owner
|
||||
Sage. Base: refactor at the brief commit. Design first. The first deliverable is
|
||||
`agents/megavolt/work/queue-ROW/DESIGN.md`.
|
||||
|
||||
This row draws a security boundary. Sage reviews the design's authority model, and Mos rules on it before any
|
||||
code is written.
|
||||
|
||||
## What exists today (GAP-MAP, measured at 9ae8c73ac)
|
||||
|
||||
- G11. Credential references exist per instance for Gitea and Vikunja only
|
||||
(`packages/business/src/credentials.mjs`, `packages/bus/src/credentials.mjs`, `vocabulary.mjs:103`). One
|
||||
Claude env file is shared by every seat.
|
||||
- G13. Agents use their own Vikunja token (`packages/tasks/src/adapter.mjs`). Git goes through the host
|
||||
socket (`packages/cli/src/git-socket.mjs`). Gitea tokens are loaded but never used.
|
||||
- The development repository's helper (`scripts/git-credential-mosaic.mjs`, wired in `.git/config` for
|
||||
git.mosaicstack.dev) pushes as one identity. Until this row lands, seats push only their own `<seat>/*`
|
||||
branches through it (mos-0981).
|
||||
|
||||
## What the design must settle
|
||||
|
||||
1. The store. Where each agent's credentials live, per instance, under the row 190 config canon. File modes,
|
||||
ownership and the read path. Which process may read which entry, and how the bus enforces that (the "RLS
|
||||
equivalent"). A process asking for another agent's entry is refused and logged.
|
||||
2. Kinds. Gitea (git over HTTPS, and the API), Vikunja, and the model provider. Say which kinds 10-27 needs
|
||||
and which wait. The model provider entry must not copy jwoltje's `claude-session.env`. If it needs a new
|
||||
provider account or subscription, flag it for Jason and don't pick one.
|
||||
3. Git identity and push. Each agent commits with its own name and email and pushes with its own token to a
|
||||
remote it is granted. Which refs an agent may push (its own branches, never the default branch or another
|
||||
agent's), and where that's enforced: the Gitea side (branch protection, token scopes) and our side. How
|
||||
this fits the host git socket, or replaces it.
|
||||
4. Lifecycle. Issue, rotate (`rotateBy`, `expires`), revoke and delete, and what row 194 (agent lifecycle) calls
|
||||
when an agent is hired or fired. A fired agent's credentials stop working at once.
|
||||
5. Who does what. Creating a Gitea user or token, or any account outside the fleet, is Jason's (Q173
|
||||
exclusions). The design lists each such step as a command or UI action for Jason. Agents never mint
|
||||
accounts.
|
||||
6. Migration. Today's seats (jarvis helper, seat tokens) keep working until each moves. The mos-0981 push
|
||||
rule ends only when Mos says so.
|
||||
7. What this row closes in G11 and G13, and what stays open.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No credential value is read, printed, logged or committed. Tests use fake tokens in scratch roots and a
|
||||
fake Gitea.
|
||||
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units, slot files, `~/.mosaic` or the live
|
||||
`.git/config` of the canonical checkout.
|
||||
- No request to git.mosaicstack.dev, tasks.woltje.com or any provider from a test.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves DESIGN.md, Mos rules on the authority model, then Duckworth approves the manifest. The bus,
|
||||
business, cli and tasks suites and every `scripts/test-*.sh` are green on Sage's gate, and the design's
|
||||
mutants are killed.
|
||||
@@ -3,10 +3,10 @@
|
||||
Sage, 2026-10-20, for mos-0974 (MVP, DECISIONS Q176, Vikunja #414 "User and main agent can create and spawn
|
||||
agents" and "User maintains ability to terminate, regenerate, or create agents via WebUI or CLI ad-hoc", #415
|
||||
Case 1 "Chief of staff agent has hire/fire authority for agents as needed; unless user forbids per agent" and
|
||||
"User maintains ability to restructure organization at will"). Gitea #1692. T1 item 5. Owner Honker, reviewer Webby (mos-0981), gate
|
||||
"User maintains ability to restructure organization at will"). Gitea #1692. T1 item 5. Owner Honker, reviewer Dewey, gate
|
||||
owner Sage. Base: refactor at the brief commit.
|
||||
|
||||
Design first. The first deliverable is `agents/honker/work/queue-194/DESIGN.md`. Webby reviews it before any
|
||||
Design first. The first deliverable is `agents/honker/work/queue-194/DESIGN.md`. Dewey reviews it before any
|
||||
code.
|
||||
|
||||
## What exists today (GAP-MAP.md G8, G9, C1.5, C1.6, measured at 9ae8c73ac)
|
||||
@@ -43,5 +43,5 @@ code.
|
||||
|
||||
## Gate
|
||||
|
||||
Webby approves DESIGN.md, then the manifest. The bus, cli, business and webui suites and every
|
||||
Dewey approves DESIGN.md, then the manifest. The bus, cli, business and webui suites and every
|
||||
`scripts/test-*.sh` are green on Sage's gate, and the design's mutants are killed.
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# Brief: Melanie's hub instance on its own host (row 195, was 190b)
|
||||
|
||||
Sage, 2026-10-20. Rewritten for Mos's rulings mos-0998 and mos-1005 (Vikunja #418, #419). The first version
|
||||
put the hub under its own Linux user on dragon-lin. That form can't pass Duckworth's B7 there, because the
|
||||
factory uid's `sudo -n` succeeds on dragon-lin (Mos's recon, brain-redux
|
||||
`projects/mosaic-mvp/MELANIE-HOST-RECON.md`). Melanie's own server is ruled out too: it has 1.2 GiB RAM free
|
||||
with her two Hermes bots running, and co-tenancy would share her user and her passwordless sudo. The hub runs
|
||||
on a separate host. Which host is Jason's decision (#419): hardware outside the cluster, a cluster VM with
|
||||
factory PVE root removed, or a cluster VM with that residual accepted. This row is written so the host choice
|
||||
doesn't change it. Gitea #1693. On the 10-27 MVP path. Federation R8 (Gizmoduck's design r4, 04a6112be) needs
|
||||
this row or nothing.
|
||||
|
||||
Owner Darkwing (Opus 5.5), reviewer Duckworth (author of B5 and B7), gate owner Sage. Chained on row 190
|
||||
(Q177): the hub runs under row 190's config canon. Design first. The first deliverable is
|
||||
`agents/darkwing/work/queue-190b/DESIGN.md`.
|
||||
|
||||
## Goal
|
||||
|
||||
Melanie's hub instance (Case 2, C2-13) runs on its own host. No factory agent can read or write the hub's
|
||||
config, data root or secrets, reach its bus socket, or log in to the hub host. Her writing reaches the hub only
|
||||
through the existing git.mosaicstack.dev remotes, books repo only.
|
||||
|
||||
## Provision (blank until Jason answers #419)
|
||||
|
||||
Who provisions the host and how is left open. Sizing from Mos (mos-0998): 4 vCPU, 8 GB RAM, 64 GB disk,
|
||||
internal network only. Each agent process measures 300 to 390 MB RSS on dragon-lin. The design takes the host
|
||||
as given: a fresh Linux host with a named admin path, reachable by the deploying seat while the row runs.
|
||||
|
||||
## The design must state
|
||||
|
||||
1. The deploy. Exactly what Darkwing installs on the hub host and how: the stack copy, node, the units, the
|
||||
config dir and data root under row 190's canon (O1: `~/.config/mosaic-NAME`, and "`~` is the home of the OS
|
||||
user that runs the instance"). Which commands run as which user on the hub host, and which need its root.
|
||||
2. Re-pin, rollback and backup on the hub host, and who runs them after the deploy key is gone.
|
||||
3. How the hub gets model access without a copy of jwoltje's `claude-session.env` or any factory credential.
|
||||
If that needs the credentials row (T1 item 6), say so and name the interim. If it needs a new subscription
|
||||
or a new provider account, flag it for Jason and don't pick one (mos-1008).
|
||||
4. The books repo path. Which git.mosaicstack.dev remote, which credential the hub holds for it, and that it
|
||||
can't read or push any other repository.
|
||||
5. The admin path and the end state. The host ends with no key the factory uid can read. The design names the
|
||||
admin path that remains (for example the hypervisor console, or a key kept off the factory uid) and who
|
||||
holds it.
|
||||
6. The acceptance script, as code Duckworth can read. It takes Gizmoduck's list of infrastructure gate items
|
||||
as its input (mos-1011), not a fixed set of checks. Each item names a path, its closed form and its
|
||||
accepted form, and the script reports which form holds for each, or fails. The list covers at least:
|
||||
- federation R8's separate-host form: the hub's bus socket, data root and config dir don't exist on the
|
||||
factory host, the factory host mounts nothing of the hub's, and no factory agent holds a login or key
|
||||
for the hub host
|
||||
- B7: from the factory uid, `sudo -n`, `doas -n`, `su`, polkit, and `ssh -o BatchMode=yes` to the hub
|
||||
host all fail
|
||||
- the hypervisor and backup paths ops-01 measured (mos-1011: one PVE node the factory key has root on,
|
||||
PBS on that node with unencrypted backups, a flat /16 LAN), each closed or accepted by Jason
|
||||
7. The identity provider and TLS front end (B8, mos-1008, mos-1011). Either the factory holds no credential
|
||||
that can create users, change groups, issue tokens or impersonate in the issuer the hub trusts, or the hub
|
||||
uses an issuer the factory can't administer. A brand, tenant or app inside the same Authentik does not
|
||||
count as separate. The same holds for the TLS front end that serves the hub. The proof is a failed admin
|
||||
call from every token the factory's root can read. No credential value is printed in that proof.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Provisioning, hypervisor access and any infra or k3s change are Jason's (Q173 exclusions). Mos takes the
|
||||
hypervisor residual (factory root on the PVE nodes, Q172) to Jason. It doesn't block this row.
|
||||
- Melanie's server (`[email protected]`) is reference only, read only, under Jason's authorization in
|
||||
mos-0995. Nothing on it changes, and her Hermes bots keep running.
|
||||
- No tunnel, Traefik or other exposure outside the internal network. That waits for Jason, after Q175 SSO.
|
||||
- No credential value is printed, logged or committed. Keys made for the deploy are removed at the end, and
|
||||
the acceptance script proves it.
|
||||
- The admin-call proof in point 7 and any call to the issuer or the TLS front end run only in a window Mos
|
||||
approves, never as a test or an anonymous probe (Q175).
|
||||
- Tests in the repository use scratch roots and stand-ins. The live deploy and the acceptance run on the hub
|
||||
host are a window with a backup first (where there is state) and a STATE report after.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves DESIGN.md, then the manifest. Any repository change is green on Sage's gate (cli suite and
|
||||
every `scripts/test-*.sh`), with the design's mutants killed. The row is done when the acceptance script passes
|
||||
on the hub host from the factory uid, Duckworth reads its output, and Mos confirms.
|
||||
@@ -1,62 +0,0 @@
|
||||
# Brief: the human boundary across uids and humans (P19 333)
|
||||
|
||||
Sage, 2026-10-20, for Mos's order mos-0989 (P19 333, MOSAICSTA-173, is now on the 10-27 path: federation R3 and
|
||||
row 193 need it). Gitea #1694. Owner Gander (Codex gpt-6.1-sol, a new builder seat under mos-0977 item 5),
|
||||
reviewer Duckworth, gate owner Sage. Latest-by: DESIGN.md approved by 2026-10-22, landed by 2026-10-24, and in
|
||||
any case before federation R3 lands. R3 and row 193 may chain on this row's candidate (Q177). Base: refactor at
|
||||
the brief commit. Design first. The first deliverable is `agents/gander/work/queue-ROW/DESIGN.md`.
|
||||
|
||||
## The problem, as filed and as it now matters
|
||||
|
||||
P19 333 as filed (row 87, mos-0657): the Console listens on a loopback TCP port, and any host process that
|
||||
reaches it, under any uid, acts as Jason. Jason accepted that as a documented limit for a one-user host
|
||||
(mos-0787, row 161). Two things change that:
|
||||
|
||||
- Row 190b (#1693, mos-0986) puts Melanie's hub under its own Linux user. Factory processes then sit on another
|
||||
uid, on the same host, next to the hub's ports and socket. The other-uid reach is now the Case 2 threat.
|
||||
- Federation r2 (gizmoduck-0004, 60b9b112a) binds a bus user only from a user-cli whose proof passes the human
|
||||
walk, whose direct parent is the bus's own Console, and which holds a live grant. That rule rests on the
|
||||
path-bound human proof (#225): today a same-uid process that runs the tree's `human-cli.mjs` acts as the
|
||||
human, without touching the Console. The Q175 design (section 8) names that limit and leaves it here.
|
||||
|
||||
## What already settles part of it
|
||||
|
||||
- Q175 phase 2 (#412) settles the Console-ingress part on the main port. Under `--sso`, 7332 answers any
|
||||
non-public Host with 403 except `GET /healthz`, and every other request needs a verified OIDC session. A
|
||||
process on another uid that reaches the port gets only `/healthz`. Phase 1 (row 192) builds it; it is live
|
||||
only after the phase 2 window.
|
||||
- Row 189 (`--no-send`) is interim only. Until phase 2 the Console takes no human writes, so the reach is
|
||||
read-only. It settles nothing.
|
||||
- Row 190b separates files and units by uid. Whether the hub's `broker.sock` refuses the factory uid depends on
|
||||
its directory and socket modes, which this row checks and tests rather than assumes.
|
||||
|
||||
## What the design must settle
|
||||
|
||||
1. Inventory every listener and entry point per instance: the Console port, the tester listener (7333 today),
|
||||
`broker.sock`, the human CLI wrapper, the Discord connector and anything row 190 adds. For each: who can
|
||||
reach it today from the same uid and from another uid, with and without `--sso`.
|
||||
2. The tester listener. It serves reads and the row 175 tester writes to anything that reaches it, on any uid.
|
||||
Close the other-uid reach (for example a unix socket with 0600 modes, or a peer-uid check on loopback), or
|
||||
bring Mos a concrete reason it must stay open. bus.tester stays Jason's switch.
|
||||
3. The bus human boundary. How the bus tells the human's own verified call from a same-uid process that runs
|
||||
`human-cli.mjs`. State what federation R3's direct-parent rule needs from the bus (peer credentials on
|
||||
`broker.sock`, the parent check, how it can't be forged by a child of an agent), and what stays possible for
|
||||
a same-uid process after this row. Say it plainly where a residual limit remains.
|
||||
4. Fit with row 193 (Morgana, human users and roles; Mos's B1 rule: a principal comes only from something the
|
||||
bus verifies) and with federation R3. One mechanism, not three.
|
||||
5. Migration: the live single-user Console and bus keep working until each instance re-pins.
|
||||
6. Which parts of P19 333 this row closes, which parts Q175 phase 2 closes, and what P19 333 says when it is
|
||||
closed.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Who can reach the Console from outside stays Jason's. This row changes what a host process can do once it
|
||||
reaches a local port or socket.
|
||||
- No test touches `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or a slot file. No test creates a real
|
||||
second uid; use stand-ins as rows 190 and 190b do, and name any part only a Q173 window can prove.
|
||||
- No request to the live 127.0.0.1:7332 or 7333, mosaic.woltje.com or auth.diversecanvas.com.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves DESIGN.md, then the manifest. The bus, webui and cli suites and every `scripts/test-*.sh`
|
||||
are green on Sage's gate, and the design's mutants are killed.
|
||||
@@ -23,9 +23,7 @@ any code.
|
||||
|
||||
1. The human model. How a business lists several humans, how each is identified, and how a Console SSO
|
||||
session (#412, row 192: each session names a provider subject mapped to a name) and a CLI call each
|
||||
become exactly one of them. Starting rule (Mos, mos-0985, from Duckworth's federation review B1 at
|
||||
b402197ff on origin duckworth/reviews): the bus takes a human principal only from something it verifies,
|
||||
such as the Console's OIDC session under #412. A process's own claim of who it is never counts.
|
||||
become exactly one of them.
|
||||
2. Human roles. A small fixed set to start (for example owner, member, viewer), what each may do on the
|
||||
bus and in the Console, and who may add or remove humans and change roles. Restructuring stays with
|
||||
the owner (Case 1).
|
||||
|
||||
@@ -12,10 +12,8 @@ before any code, as Q175 did. Then the build follows the design, with tests and
|
||||
|
||||
Two or more instances run side by side on dragon-lin. Each one has its own config file, data root (store,
|
||||
broker.sock, host.lock, state, runs, sessions, workspaces), businesses, bus unit, Console unit and port,
|
||||
tester port, Discord unit, Claude session env file, stack-active pointer and activation log. No instance's
|
||||
commands, units or tools address another instance's state by default or by mistake. On one uid this is
|
||||
addressing only, not a security boundary: a same-uid process can still read another instance's files.
|
||||
Isolation against that is row 190b (#1693, mos-0986), one Linux user per instance. The current instance keeps working unchanged: its
|
||||
tester port, Discord unit, Claude session env file, stack-active pointer and activation log. Nothing one
|
||||
instance does can read or change another's state. The current instance keeps working unchanged: its
|
||||
config path, data root, unit names and ports stay as they are, and no re-pin is needed for it.
|
||||
|
||||
## Known single-instance points (GAP-MAP.md, measured at 9ae8c73ac)
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# Brief: retire the control board writer on 7331
|
||||
|
||||
Sage, 2026-10-20, for Mos's ruling mos-1008 item 2. Gitea #1696. MVP 10-27 work. Owner Bushroot (Haiku 5.5, a
|
||||
low-level model under Q177), reviewer Webby, gate owner Sage. Base: refactor at the brief commit.
|
||||
|
||||
## Why
|
||||
|
||||
`packages/control-board/src/serve.mjs` is a loopback HTTP service on 7331 with no authentication. It reads
|
||||
transcripts and writes through `/api/reply`. Any host process that reaches the port can do both. That fails
|
||||
Q175 ("zero trust with verification"). Gander found it in the row 196 inventory (gander-0004). Nothing listens
|
||||
on 7331 now (`ss -ltnp`, 2026-10-20T19:05:09Z), so retiring it costs the least.
|
||||
|
||||
## Steps, in order
|
||||
|
||||
1. Inventory first. List every reference to the control board and to 7331: scripts, units, `scripts/mosaic`
|
||||
and other CLI dispatch, docs (AGENTS.md, READMEs, TOOLS.md), package.json scripts, tests, and the Console
|
||||
(`packages/webui`). For each, say whether it starts the writer, reads its code, or only mentions it.
|
||||
Commit the list as `agents/bushroot/work/queue-ROW/INVENTORY.md` and send it to Sage and Webby.
|
||||
2. Stop if something depends on it. If a reference shows a live dependency (the Console or another package
|
||||
imports code that only the control board has, or a unit or script still starts it), stop. Sage takes it to
|
||||
Mos before anything is removed.
|
||||
3. Otherwise retire the writer. Either remove the `serve` entry point, or make it refuse to start with a
|
||||
message that points to the Console. Keep any read-only code another package imports, and say which.
|
||||
Update the docs that tell people to start it. The AGENTS.md text about the control-board reply convention
|
||||
is a separate matter. Change it only if it tells someone to start 7331.
|
||||
|
||||
## Mos's call on the Console dependency (mos-1019)
|
||||
|
||||
The inventory (004f133bd) found that the Console proxies `/api/board`, `/api/seen`, `/api/reply` and the
|
||||
conversation routes to `DEFAULT_BOARD` on 7331 (`packages/webui/src/serve.mjs:12`), so the board view returns
|
||||
502 today. Mos took Sage's recommendation: retire the 7331 listener and its README start commands, and have
|
||||
the Console call the board's read code in-process, behind the Console's own auth (Q175 SSO on 7332).
|
||||
`/api/reply` stays only under `--send`. The DESIGN.md for that change comes before code, and Webby approves
|
||||
it.
|
||||
|
||||
## Acceptance
|
||||
|
||||
Nothing in the stack can start a writer on 7331. A test proves it: the entry point refuses (or is gone), no
|
||||
script or unit starts it, and a grep check finds no starter. Each change gets a mutant that brings the writer
|
||||
back and the test kills it. Mos added three items (mos-1019):
|
||||
|
||||
- The board view answers from the Console with nothing listening on 7331.
|
||||
- Every board route sits behind the Console's auth.
|
||||
- A test proves `/api/reply` refuses without `--send`.
|
||||
|
||||
Test fixtures that bind port 0 may stay if they only test the code in-process.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No change to the live units, `~/.mosaic-dev`, `~/.config/mosaic-dev`, slot files or `~/.mosaic`.
|
||||
- No request to any live port.
|
||||
- Single test files run freely. Suites need Sage's slot.
|
||||
|
||||
## Gate
|
||||
|
||||
Webby approves INVENTORY.md, then DESIGN.md, then the manifest. The control-board and webui package suites, the cli suite and
|
||||
every `scripts/test-*.sh` are green on Sage's gate, and the mutants are killed.
|
||||
@@ -1,42 +0,0 @@
|
||||
# Brief: a second gate lane, two full gates at once on dragon-lin
|
||||
|
||||
Sage, 2026-10-20, for Mos's order mos-0977 item 6 ("put the design for a second gate lane on the list now")
|
||||
and the MVP push (mos-0974). Gitea #1695. Owner Herb (Codex gpt-6.1-sol, a new builder seat), reviewer
|
||||
Webby, gate owner Sage. Base: refactor at the brief commit. Design first. The first deliverable is
|
||||
`agents/herb/work/queue-ROW/DESIGN.md`.
|
||||
|
||||
## Why
|
||||
|
||||
Every full run shares one slot today. A full gate (cli suite plus every `scripts/test-*.sh`) takes about 13
|
||||
minutes or more, reviewers' runs and mutants take the same slot, and with eight or more seats building for
|
||||
10-27 the slot is the bottleneck. The slot exists because concurrent runs broke each other. The busy check is
|
||||
`agents/sage/work/gate-lane/busy-now.sh` (a copy of Sage's), and the gate is
|
||||
`agents/sage/work/gate-lane/gate-template.sh` (a copy of Sage's template). The host has 16 cores and 62 GB.
|
||||
|
||||
## What the design must settle
|
||||
|
||||
1. Every shared resource two concurrent runs can collide on, found from the code and tests, not guessed:
|
||||
Docker compose project names and networks (`scripts/test-task.sh` and others), fixed TCP ports in tests,
|
||||
unix socket paths and their 107-byte limit under TMPDIR, the sandbox's private `/tmp`, shared caches,
|
||||
scratch and worktree paths, the queue lock, and any test that reads host-wide state (process lists,
|
||||
`pgrep`, `/proc`). For each: how a lane gets its own copy, or why it is safe to share.
|
||||
2. Timing. Which tests have time limits that a doubled CPU load could break, measured, and what changes (a
|
||||
bound, or a lane rule that keeps those suites apart).
|
||||
3. The lane mechanism: lane ids, a lock per lane that a run takes and releases even on a crash, the busy
|
||||
check that reports per lane, and how a seat asks for a lane. Sage still grants it.
|
||||
4. The gate template takes a lane id and isolates everything from point 1 by it.
|
||||
5. Proof: two full gates on different candidates at once, three times, with zero cross-failures, plus the
|
||||
wall time of each against a single-lane run. Any flake found is named with its cause.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No change to the live units, `~/.mosaic-dev`, `~/.config/mosaic-dev` or slot files. The proof runs use
|
||||
scratch roots only, and it needs Sage's grant of the whole slot while it runs.
|
||||
- Test code changes that only isolate a resource are in scope. A change to what a test checks is not, and
|
||||
goes to its owner.
|
||||
- Docker networks and containers a run makes are removed by the run (trap), and nothing else is touched.
|
||||
|
||||
## Gate
|
||||
|
||||
Webby approves DESIGN.md, then the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's
|
||||
gate, both singly and in the two-lane proof.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user