Compare commits
60
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82df17347d | ||
|
|
47c2cb33c3 | ||
|
|
762a6711ff | ||
|
|
de928da2dc | ||
|
|
781d91624a | ||
|
|
d16ccdb910 | ||
|
|
60f5ca8fa8 | ||
|
|
3c30f1036a | ||
|
|
2ad8c4f5b0 | ||
|
|
29cb93aa6a | ||
|
|
0622680756 | ||
|
|
8ddd8ada58 | ||
|
|
eecb77b549 | ||
|
|
d46f0aa6de | ||
|
|
56af37187e | ||
|
|
f953b1d383 | ||
|
|
b149ba3e04 | ||
|
|
a3a378204d | ||
|
|
8de42b55b0 | ||
|
|
4641555b0a | ||
|
|
9619734322 | ||
|
|
e95c917a14 | ||
|
|
f39f518d0d | ||
|
|
f41729178e | ||
|
|
305857c8f9 | ||
|
|
176a6bf4b9 | ||
|
|
5b8def2f14 | ||
|
|
4429a104f6 | ||
|
|
32b54ac3b0 | ||
|
|
6025ddd9cf | ||
|
|
ecbfd6deb6 | ||
|
|
0498c0613f | ||
|
|
49d1f3a231 | ||
|
|
628e2ca5cd | ||
|
|
088087ac96 | ||
|
|
a0fbefd48d | ||
|
|
f24f2cd40d | ||
|
|
9ba31b5b3f | ||
|
|
9db578ff54 | ||
|
|
8eb9ddd12c | ||
|
|
ae3c36f248 | ||
|
|
a2af7ab495 | ||
|
|
aa1bd209e3 | ||
|
|
ace27dc5eb | ||
|
|
a4a42dbac2 | ||
|
|
a31f1a0f8b | ||
|
|
992650232f | ||
|
|
a7661ada06 | ||
|
|
f871cb3096 | ||
|
|
0bbd23c619 | ||
|
|
0217b71c8f | ||
|
|
c6f528a32f | ||
|
|
96f243fafc | ||
|
|
c862699352 | ||
|
|
48ebee0ffe | ||
|
|
66bdd84b8f | ||
|
|
6966164f0c | ||
|
|
53bb594621 | ||
|
|
5e01c36833 | ||
|
|
9f56d2098d |
@@ -16,6 +16,17 @@ 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 |
|
||||
| Quackerjack | Builder, federation R5 and R2 (#414) | T3 session (Claude, claude-haiku-5-5), started by Sage under mos-1023 | none, T3 only |
|
||||
| Gizmoduck | Federation design (#414) and builder of FED R1 | T3 session (Claude, Opus 5.5, thread 9312a896) | 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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,8 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Gizmoduck
|
||||
|
||||
Design and builder seat, Claude Opus 5.5, a T3 thread (9312a896). Author of the federation design
|
||||
(`agents/gizmoduck/work/federation/DESIGN.md` on `gizmoduck/federation-design`, #414). Builds FED R1, the
|
||||
access.json loader and validator, under Mos's order mos-1023 item 1.
|
||||
|
||||
No launcher in this directory. The seat works in its own worktree and has no credentials of its own; until
|
||||
the credentials row settles per-agent identity, it pushes only its own `gizmoduck/*` branches through the
|
||||
repository credential helper (mos-0981), never `refactor`, `main` or another seat's branch.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Gosalyn
|
||||
|
||||
Worker seat for Mosaic Stack, MVP track T2 (Case 2, the creative writing hub, Vikunja #416).
|
||||
|
||||
- T3 thread: 7604438a-0ded-41c8-bd3f-26f57fdb5cd2
|
||||
- Model: Opus 5.5 (Claude Code, T3)
|
||||
- Launched by Mos (Jason's main agent, T3 38bc4c81-6270-4021-b00c-6a15c43b9b0e), who briefs the seat
|
||||
- Repository lead and queue: Sage (T3 1ef1e4f8-3ead-4208-beca-38f9f1add079)
|
||||
- Branch: gosalyn/case2-design, pushed only by this seat, landed on refactor by Sage
|
||||
|
||||
Work lives in `work/`. The Case 2 design is `work/case2/DESIGN.md`.
|
||||
@@ -0,0 +1,8 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,476 @@
|
||||
# Row 194 build notes
|
||||
|
||||
Design r4 approved: DESIGN.md sha256 824ef884 at 7d0788236 (Webby webby-0014, review at e1e64ff01 on
|
||||
webby/reviews; Mos mos-1042; Sage sage-1387 "build now"). Build base: refactor aa1bd209e merged into
|
||||
honker/row194 at fa9eaa4c3.
|
||||
|
||||
## Build notes from the approval (each gets a test and a mutant)
|
||||
|
||||
- W-N1 (webby-0014 N1): launch refuses `instance-firing` while I has a `fire.stopped` with no `fire.done`.
|
||||
Test case in T9c. Mutant M59: the check is dropped.
|
||||
- W-N2 (webby-0014 N2): the broker's hire-time check requires roleVersion 2, so a definition with no
|
||||
action lists refuses. Test case in T8b. Mutant M60: the version check is dropped.
|
||||
- Grace (sage-1387): 5 s, one constant shared with row 191.
|
||||
|
||||
## Chain state (2026-10-20T19:45Z)
|
||||
|
||||
Rows 190, 191 and 193 have no product commits yet (rocko/row191 and darkwing/row190 sit at da230ca48, the
|
||||
brief commit). Slices that don't need row 191 build first. S6 waits for row 191's candidate.
|
||||
|
||||
## Slices
|
||||
|
||||
| Slice | Scope | Tests | State |
|
||||
|---|---|---|---|
|
||||
| S1 | business: vocabulary (role.hire, role.fire hard waits, HIRE_ALLOWED), role.mjs `hireable`, business.mjs keep/noHire/retired, instance-settings delete and veto ops, chief-of-staff.json `hireable: []` | T6 (business), T7, T8c, T17, D2.2 writer | done, see below |
|
||||
| S2 | bus: reloadBusiness and runtime callback, instance.launch/end/regenerate requests, instances.set delete, veto.set, instance.hire/fire, keep checks, D4 hook, tester checks | T8-T10, T9c, T12-T14, T18 | S2a (D3), S2b (veto, hook, keep, T13), S2c (requests, terminate, delete) and S2d (hire, fire, W-N1) and S2e (PRD 1.7 routing, sage-1471) done |
|
||||
| S3 | cli: launcher lifecyclePass, host ops, `business instance` verbs | T1-T6, T11 | S3a (launcher pass) and S3b (verbs, veto, end to end, T2a, T4, T6, T11) done |
|
||||
| S4 | harness: launch tool schema, hire and fire tools | T8 (D3 item 5) | done, see S4 notes |
|
||||
| S5 | webui: Agents section and routes | T16 | done, see S5 notes. The "still running after fire" flag waits for S6 |
|
||||
| S6 | row 191 chain: halted checks, fire SIGKILL through row 191's stop | T9a, T9b, T15 | waits on row 191 |
|
||||
|
||||
## Changed assertions (D3 item 4, N4)
|
||||
|
||||
Listed here as the build changes them.
|
||||
|
||||
- `packages/business/tests/instance-settings.test.mjs:53, :67, :72`: the row 165 read and add views
|
||||
gain `kept: false` (D1, the read gains `kept` per instance). No other field changed.
|
||||
- `packages/business/tests/instance-settings.test.mjs:70` (`added.restart` true to false) and `:91`
|
||||
(`redefined.restart` true to false): the writer no longer asks for a restart. The bus decides,
|
||||
after its reload (D3 items 2-3). The two test names say so.
|
||||
- `packages/cli/tests/instances.test.mjs`: `setup` boots the bus from the file through `bootConfig`, as
|
||||
the unit does, instead of a one-role stub. The add test (T1) now expects the "no restart" line and
|
||||
binds `sec-1` on the running bus. The `RESTART_NEEDED` text pins moved, unchanged, to a new test that
|
||||
boots the old stub (`setup(t, { stub: true })`). Its coder has no credential references and the
|
||||
file's has, so the bus doesn't reload, prints `RESTART_NEEDED` and doesn't bind `sec-1` (D3 item 3).
|
||||
- `packages/webui/tests/instances.test.mjs:25`: the fake writer's default answer is `restart: false`,
|
||||
the usual answer now. The real-broker test boots through `bootConfig` and expects `restart: false`
|
||||
(`:71`) and the log line `done restart=false` (`:74`). The answer-shape rows at `:140-145` keep
|
||||
`restart: true`. Each row tests another field, or `restart: 'yes'`, so the value is not what they
|
||||
check.
|
||||
- `packages/bus/tests/instances-verb.test.mjs` T12 (S2c): the human delete now answers `{request}`
|
||||
where S2b refused `invalid-request`. The stub writer answers `instances.check` with the revision,
|
||||
and the calls list gains that one check, actor `jason`. Nothing else in the test changed.
|
||||
- `packages/bus/tests/tester-writes.test.mjs:19` (row 175 pin): TESTER_VERBS gains `instance.launch`,
|
||||
`instance.end` and `instance.request` (D4.1). TESTER_ORGANIZATION_VERBS is unchanged.
|
||||
- `packages/bus/tests/instances-verb.test.mjs:78` keeps `restart: true`, now for a D3 item 3 reason:
|
||||
its boot copy is a stub without the file's credential references. A comment there says so.
|
||||
`reload.test.mjs` covers the reload from a file boot.
|
||||
- `packages/bus/tests/hard-waits.test.mjs:38` (S2d): the every-hard-wait loop gives `role.hire` and
|
||||
`role.fire` a target and `arguments`, which their raise now requires (D2.1). The class and route it
|
||||
checks are unchanged.
|
||||
- S2e (PRD 1.7 REQ-COS-3) takes back the S2d entry above. `role.hire` and `role.fire` left the hard
|
||||
waits, so `packages/bus/tests/hard-waits.test.mjs` and `packages/business/tests/hard-waits.test.mjs`
|
||||
drop them from WAITS, and the bus file's every-hard-wait loop is back to its S2c form. The business
|
||||
file gains a test that both are routed and not in GATED_ONLY.
|
||||
- `packages/bus/tests/reload.test.mjs` (S2e): its hard-wait example used `role.hire`, now routed. It
|
||||
uses `spend`. The check is unchanged.
|
||||
- `packages/business/tests/chief-of-staff.test.mjs` (S2e): the chief's pinned authority and the test
|
||||
name gain `role.hire` and `role.fire`. `packages/business/tests/role.test.mjs` gains the check that
|
||||
no other shipped role holds either.
|
||||
- `packages/harness/tests/tools.test.mjs` and `packages/harness/tests/bundle.test.mjs` (S2e): the
|
||||
chief's pinned tool lists gain `hire` and `fire`. Every held action needs a typed tool
|
||||
(`untypedActions`), so the tools come forward from S4.
|
||||
- `packages/bus/tests/chief-of-staff.test.mjs` (S2e): the S2d decision-form tests are rewritten to the
|
||||
reason form (below). The S2d raise tests with `arguments` are gone with the key.
|
||||
- `packages/bus/tests/lifecycle-verbs.test.mjs` V5 retry (S3a): `lifecycleDelete` on a retry now
|
||||
answers `{transaction, retry: true}` with the failed delete's transaction, where S2c answered
|
||||
`{transaction: null, retry: true}`. The launcher needs it to name `.retired/I-<transaction>`. The
|
||||
test checks the value and that the retry still neither checks nor writes the file.
|
||||
- `packages/harness/tests/tools.test.mjs:48` (S4): the launch tool's `instance` was an enum of the
|
||||
start-time listing. It is now `{type: "string", description, pattern: ID_PATTERN.source}`, and the
|
||||
listing moves to the tool description, which the test matches. `:107` (the pm test's chief check)
|
||||
asserts there is no enum and the description names `chief.launch.instances`.
|
||||
- `packages/business/tests/chief-of-staff.test.mjs` (S4): the contract test gains a match on the new
|
||||
sentence that a hire is listed for `launch` at once but runs only after the human sets its route.
|
||||
|
||||
- `packages/webui/tests/browser.test.mjs:21-22` (S5): the page's script list pins the scripts in
|
||||
order. It now has `/agents.js` between `/chat.js` and `/bus.js`, and the comment names row 194.
|
||||
|
||||
## S1 notes
|
||||
|
||||
- Tests: `tests/lifecycle-settings.test.mjs` (new) holds T6's business part (V5 order, last listed,
|
||||
clean delete, retired refusals for add and hire), T7 (V6) and the writer's hire and fire (D2.2).
|
||||
T8c is in `role.test.mjs`, T17 in `business.test.mjs`.
|
||||
- `deleteRefusal`'s entry half (`entry.tracker`, `entry.credentials`) follows V5's wording "holds
|
||||
credentials or a tracker bot". In a file that validates it is the same as the definition check: a
|
||||
definition with no credential refuses an entry tracker or credentials at load. So no test reaches it
|
||||
alone, and no mutant is listed for it.
|
||||
- END_PROTECTED is `assistant` only. The chief's own instance refuses delete and fire as
|
||||
`organization-instance-referenced` (launch.by), not as protected.
|
||||
- The writer refuses an `assistant` hire `organization-definition-refused` through `definitionFor`, as
|
||||
row 165's add does.
|
||||
- PRD draft: 69544f342 (sha256 c1f52d98…), sage-1395: adding to the fixed list is a PRD change.
|
||||
|
||||
## S2a notes (D3 live reload)
|
||||
|
||||
- `Broker.reloadBusiness(id, doc)` runs the boot check (`checkBusiness`, moved out of the
|
||||
constructor unchanged) on a clone, then refuses a changed `human` and swaps the document in.
|
||||
- The runtime reloads after every committed write, through the chain `bootConfig` uses
|
||||
(`loadBusiness`, `resolveInstance` with `organization.system`, `busBusiness`). Then `limits.refresh`
|
||||
runs. `instances.set` answers `restart: !live`. Any other verb name now throws `invalid-request`
|
||||
where it used to fall through to `plugins.set`.
|
||||
- D3 item 3 check: a role in the boot copy must keep its credential references and tracker bot. A role
|
||||
not in the boot copy must have neither. The tracker sync block must match. Otherwise no reload and
|
||||
`restart: true`. The first version compared one fingerprint over all roles, so every create failed
|
||||
it. T14's create test caught that.
|
||||
- Consumers checked against item 3 (`runtime.mjs` startBroker):
|
||||
- The Credentials store takes the role and sync references and the project roots it protects at
|
||||
start. The tasks adapter takes the bots at start. All of these are compared.
|
||||
- Limits read `settings.current` at each ask. The human proof reads `human` from the boot copy,
|
||||
and `reloadBusiness` refuses a changed human, so the two agree.
|
||||
- Web grants: the broker reads `roles.<I>.web` from its own document (`broker.mjs:1545`), so a
|
||||
reload carries the grant. A hireable definition has no `web` tool.
|
||||
- Usage loads its `usage-source` plug-ins from `b.plugins` at start. That was already so before
|
||||
this row (`plugins.set` never restarted them). A reload doesn't change it, and it isn't compared.
|
||||
- Instance-running: `instances.set` op change with a `definition` on a declared instance with a
|
||||
claim holder (or thread holders for a thread instance) refuses `instance-running` before the
|
||||
writer. A vars or launch change still passes.
|
||||
- Tests: `packages/bus/tests/reload.test.mjs` (T14, 6 tests).
|
||||
|
||||
## S2b notes (veto.set, the D4 hook, keep checks)
|
||||
|
||||
- `LIFECYCLE_HUMAN_VERBS` (exported from `broker.mjs`) has one entry per verb and per `instances.set`
|
||||
op: `instance.launch`, `instance.end`, `instance.regenerate`, `veto.set`, `instances.set add`,
|
||||
`instances.set change`, `instances.set delete`. Choice: `#mayHuman(s, verb, target)` refuses
|
||||
`human-required` for an entry missing from the list. So the list and the calls can't drift apart,
|
||||
and T12 shows the hook got the exact entry: each listed human call passes. The tradeoff is that a
|
||||
new human verb has to join the list before it works, which is what row 193 needs anyway.
|
||||
- `bindHuman` already refuses any human but the owner, so the hook's owner check can't fail on any
|
||||
path today. It is the body row 193 replaces.
|
||||
- M46 (`veto.set` skips `#human`) is equivalent while the generic check runs first: no session
|
||||
without `s.human` gets past `human-required`, and only `bindHuman` sets `s.human`, always with
|
||||
`via: 'cli'` and `outsideAgent: true`. T18 shows the refusal. `#human` stays as the design asks.
|
||||
- `instances.set` now checks `op` in the broker (`add`, `change`, `delete`, else `invalid-request`).
|
||||
The tester's delete refuses `tester-refused` (B6). Until S2c records the delete request, a human
|
||||
delete refuses `invalid-request` after the hook. Without that, the writer's `set` op `delete` would
|
||||
commit with no running or starting check and no workspace rename.
|
||||
- Keep: the `role.revoke` reason branch refuses `instance-kept` for any caller, not only the chief.
|
||||
Only the chief holds `role.end` today, so this is the same set. A caller with `role.end` added
|
||||
later is held to the forbid too. The `organization.set` check refuses when any key of
|
||||
`args.instances` is kept, before the writer and before the grant is spent. The human's `limits.set`
|
||||
isn't checked: the human owns the forbid.
|
||||
- T13 needed no code. `organization.set` and `limits.set` take only `ORGANIZATION_KEYS`, and
|
||||
`limits` and `models` write only `launch.limits` and `launch.models`.
|
||||
- Tests: `chief-of-staff.test.mjs` T10 (reason end, decision end, model write), T13 and T18's
|
||||
veto.set and instances.set paths on a stub writer. T18's file-bytes check and its hire and fire
|
||||
cases come with S2d. `instances-verb.test.mjs` T12 (sessions, tester, hook). `reload.test.mjs`
|
||||
gains veto.set.
|
||||
|
||||
## S2c notes (lifecycle requests, terminate, delete)
|
||||
|
||||
- A request is an `action.allowed` event, action `lifecycle.launch`, `.regenerate`, `.delete` or
|
||||
`.end`, body `{action, request, via, by, ...}`. The human's also gets `human.input {kind: 'admin'}`.
|
||||
The tester's gets none. Steps are `lifecycle.taken`, `lifecycle.done {request, run?, transaction?,
|
||||
refusal?}` and `lifecycle.cancelled {request, by}`, with no role or run in the session. No new
|
||||
table or event kind, so no migration.
|
||||
- Host ops for the launcher pass (S3), each in LAUNCH_OPS: `lifecycleRequests`, `lifecycleTake`,
|
||||
`lifecycleDelete`, `lifecycleDone`. Choice: `lifecycleDelete` commits through the bus's own
|
||||
organization callback, so the D3 reload runs after it as after any other write. The tradeoff is
|
||||
one more host op instead of the launcher calling the writer.
|
||||
- The take re-checks: request-expired (60 s, `LIFECYCLE_REQUEST_MS`), then launch-revoked and
|
||||
instance-retired for launch and regenerate only, then tester-off for a tester request. A refused
|
||||
take writes `lifecycle.done` with the refusal, so the request closes. `lifecycle.end` never
|
||||
expires: the runs it names still need the signal.
|
||||
- instance-starting counts any taken request of I, and any open one still under 60 s, delete
|
||||
included. An open request past 60 s can only close request-expired, so it doesn't count. The end
|
||||
request doesn't count.
|
||||
- Delete order at the verb (V5): keys and identifier, the 64-hex revision, the retired-name retry
|
||||
gate, then `instances.check` (the writer's plan and validation, no write). A named organization
|
||||
refusal fails at once. A BusinessError (the last-listed refusal) is held until instance-running
|
||||
and instance-starting have passed, then answers invalid-request. Then revision-conflict, then the
|
||||
request with `{revision}`. `lifecycleDelete` checks running and starting again after the take
|
||||
(other requests only), then calls `instances.set` op delete with actor = the request's `by`.
|
||||
- Retry: a retired name that isn't in roles deletes again only when its last delete closed
|
||||
`workspace-rename-failed`. The request carries `retry: true`, and `lifecycleDelete` answers
|
||||
`{transaction: null, retry: true}` with no check and no write. The launcher (S3) retries the rename.
|
||||
- `lifecycleDone` carries `transaction`, so the delete's done names the commit the rename used.
|
||||
- Terminate (V3): every claim holder of I, or every thread holder for a thread instance, is revoked
|
||||
with the reason (default "ended by the human" or "ended by the tester"). The answer names the held
|
||||
runs plus the bound runs of I not yet ended, cancels open requests of I and lists taken ones. With
|
||||
none of these it refuses not-held. The tester ends only its own threads and cancels only its own
|
||||
requests.
|
||||
- The tester's business scope needs no check: its cap is bound to one business (M19), so another
|
||||
business's instance answers unknown-role. `instance.request` needs no tester-file check: a tester
|
||||
reads only requests it made.
|
||||
- The tester switch is read through a `testerOn` option. The runtime reads `bus.tester` from the
|
||||
live file through `settings.current`, so a change applies with no restart. Anything but `true`,
|
||||
or a read error, is off.
|
||||
- Refactor: the organization error mapping moved into `#organizationFail(e, verb)`, shared by the
|
||||
organization verbs and `lifecycleDelete`. Same mapping as before.
|
||||
- Deferred: the halted checks (S6, row 191) and W-N1 (S2d with fire).
|
||||
- Tests: `packages/bus/tests/lifecycle-verbs.test.mjs` (12 tests: T12 lifecycle part, T12a, V2-V5,
|
||||
the take and the runtime delete end to end). Probe mutants on the expiry, starting (verb and
|
||||
take), the take's expiry and tester checks, the retry gate, the tester's own-request read and
|
||||
not-held all died. They are not the M-list run.
|
||||
|
||||
## S2d notes (the chief's hire and fire, W-N1)
|
||||
|
||||
- Still hard waits (PRD 1.6). Under PRD 1.7 they move to ROUTED in S2e, with the reason form.
|
||||
- The raise: `decision.raise` takes `arguments`, only for `role.hire` (`{name, definition, vars?}`)
|
||||
or `role.fire` (`{name, reason}`), and requires it for those. `target` must equal
|
||||
`arguments.name`. The raise event records it, and the human's decision view shows it under
|
||||
`authorization.arguments`. The shape checks are the call's (`#hireFireArgs`): identifiers, not
|
||||
`human` or `tester`, vars only `model` and `limits.*`, reason 1-500 characters.
|
||||
- The call: `instance.hire {name, definition, vars?, decision}` and `instance.fire {name, reason,
|
||||
decision}`, from a chief session only. Order: tester-refused, read-only and agent-required
|
||||
(`#agent`), keys, `role-not-chief` (launch.by), not-configured, decision-mismatch for a decision of
|
||||
another business, `#checkAuthority`, the call's arguments against the raise's (canonical JSON,
|
||||
decision-mismatch), decision-consumed, instance-kept. The writer gets the chief as actor
|
||||
(`chief-of-staff.chief`) and the context `{instance, run, decision}`, which changes.jsonl records.
|
||||
- Hire: `#hireable` loads the definition and the chief's own role file through the new `roleFile`
|
||||
option (the runtime passes `loadRole(rolesDir, name)`). Version 1, any action off HIRE_ALLOWED
|
||||
(`unhireableAction`), not on the chief's `hireable` list, on `noHire` or no file all refuse
|
||||
definition-not-hireable before the writer. Then the writer's hire, then the consume with
|
||||
`{transaction}`. Without `roleFile` a hire refuses not-configured.
|
||||
- Fire, before step A: instance-kept, unknown-role, organization-definition-refused (END_PROTECTED),
|
||||
organization-instance-referenced (launch.by), and invalid-request for the last listed name.
|
||||
Step A, one transaction: revoke the claims with the decision and reason, cancel open requests,
|
||||
list taken ones, write `lifecycle.end {via: 'chief', by, runs, reason, kill: true}` when runs exist,
|
||||
and `fire.stopped {cites, request}`. Step B: the writer's fire (already delisted is done). Step C,
|
||||
one transaction: the consume with `{reason, transaction}` and `fire.done {cites, transaction}`.
|
||||
- The fire's own events cite the decision under `cites`, not `decision`, so `#spent` counts only the
|
||||
consume. A failure at B or C leaves the decision unspent, so the repeat runs.
|
||||
- Choice: a repeat after a failed B or C asks for the kill again for any run of I still bound. A
|
||||
revoked run stays bound until the launcher reports it ended, and a second signal to a dead
|
||||
process does nothing. The tradeoff is a duplicate `lifecycle.end` against a run that may outlive
|
||||
the first one.
|
||||
- W-N1 `#firing`: a `fire.stopped` with no `fire.done` for the same `cites`. It refuses
|
||||
instance-firing at the human and tester verbs (`#launchable`), at the take of a launch or
|
||||
regenerate (`#takeRefusal`), and at the chief's `role.launch` (`#authorityFor`).
|
||||
- `#organizationFail` passes the writer's own definition-not-hireable and instance-kept through by
|
||||
name for hire and fire. They come from the file as it stands, after the broker's checks passed.
|
||||
- `lifecycleView` shows `kill: true` when the request carries it.
|
||||
- Deferred: the kill escalation in the launcher (S3, S6).
|
||||
- Tests: `packages/bus/tests/chief-of-staff.test.mjs`, 16 new (T8 x5, T8a, T8b x2, T9 x2, T9c x2,
|
||||
T10, T18, the runtime end to end, W-N1 at the take). 39 in the file pass. Probe mutants on 21
|
||||
lines (the arguments check, each W-N1 site, M60's version check, keep, the other-business check,
|
||||
last-listed, kill, the chief's list, noHire, role-not-chief, the pre-check consume, both consumes'
|
||||
events, the raise's argument rules, the revoke, the cancel, the pass-through, the view's kill) all
|
||||
died. They are not the M-list run.
|
||||
|
||||
## S2e notes (PRD 1.7 REQ-COS-3 routing, sage-1471, mos-1089)
|
||||
|
||||
- Role-policy change, for Webby to review on its own: `roles/chief-of-staff.json` withinRole gains
|
||||
`role.hire` and `role.fire`. `packages/business/src/vocabulary.mjs` and `packages/bus/src/broker.mjs`
|
||||
move both from the hard waits to ROUTED. `roles/chief-of-staff.md` and `roles/pm.md` drop them from
|
||||
the hard-wait list, which `workflow-roles.test.mjs:113` pins to GATED_ONLY. No other shipped role
|
||||
gains either action, and `role.test.mjs` pins that.
|
||||
- The call takes the reason form, as `role.end` does: `instance.hire {name, definition, vars?, reason}`
|
||||
and `instance.fire {name, reason}`, with no decision. The reason is 1-500 characters for both.
|
||||
- Choice: the decision form is gone, not kept beside the reason form. A within-role raise closes at
|
||||
once, so a decision there adds an event and no check. The `decision.raise` `arguments` key from S2d
|
||||
goes with it. Tradeoff: if a later PRD gates hire again, the raise path comes back as new code.
|
||||
- Choice: hire takes a reason too. The PRD logs every hire and fire with its reason.
|
||||
- Order: tester-refused, read-only and agent-required, keys, `role-not-chief` (launch.by),
|
||||
not-configured, `#checkAuthority` with `{target: name}`, instance-kept. `role-not-chief` comes before
|
||||
the authority check, so a non-chief role refuses `role-not-chief` even when its file grants the
|
||||
actions. A chief whose file lacks the grant refuses `decision-required`.
|
||||
- The writer gets only its own keys. The reason goes in the change context `{instance, run, reason}`,
|
||||
which changes.jsonl records. The hire's consume detail is `{reason, transaction}`.
|
||||
- W-N1 `#firing` now goes by order, since the reason form has no decision id: a `fire.stopped` of the
|
||||
subject with no later `fire.done` (`seq` after it) for the same subject. `fire.stopped` carries
|
||||
`{action, request}` and `fire.done` `{action, transaction}`.
|
||||
- Harness: the typed `hire` and `fire` tools (verbs `instance.hire` and `instance.fire`) come forward
|
||||
from S4, because a held action with no typed tool fails `untypedActions`. Each tool follows its own
|
||||
grant. `roles/chief-of-staff.md` already names them. The rest of the S4 contract text and the runner
|
||||
end to end for hire and fire stay in S4.
|
||||
- Going live is a later re-pin step with Mos (sage-1471). Landing the change doesn't put it on the live
|
||||
stack.
|
||||
- Correction to S2d (found in S2e): S2d's broker imported `business/src/role.mjs`, which is not in
|
||||
`seat/src/sandbox.mjs` RUNNER_FILES. The runner imports broker.mjs, so a session died with
|
||||
ERR_MODULE_NOT_FOUND: cli `launcher.test.mjs` failed 15 of 29 and seat `sandbox.test.mjs` 4 of 10 at
|
||||
089b92ca3. A bisect showed S2c passes 29 of 29. I had not run those two files for S2d. Fix:
|
||||
`unhireableAction` moves into vocabulary.mjs, which is mounted, and role.mjs re-exports it.
|
||||
RUNNER_FILES is unchanged, so the sandbox boundary does not widen. Rule from here (sage-1497): a
|
||||
change to broker.mjs imports, or those of any RUNNER_FILES file, runs these four singly: seat
|
||||
`sandbox.test.mjs`, cli `launcher.test.mjs`, cli `thread-launch.test.mjs` and cli
|
||||
`git-socket.test.mjs`.
|
||||
- Mutant (sage-1497): broker.mjs imports `unhireableAction` from role.mjs again. Killed: sandbox.test
|
||||
fails 4 of 10 (role.mjs outside RUNNER_FILES), launcher.test 15 of 29, thread-launch 7 of 11. git-socket
|
||||
passes 24 of 24 under it, so it does not catch this one and stays on the list for its own sandbox runs.
|
||||
- Tests: `packages/bus/tests/chief-of-staff.test.mjs` S2d block rewritten to the reason form (39 pass),
|
||||
with new tests for the reason bounds, `role-not-chief` with the grant, `decision-required` for a chief
|
||||
without each grant, W-N1 by order, and the runtime end to end with the reason in the receipt context.
|
||||
`packages/harness/tests/tools.test.mjs` gains the hire and fire tool shape test, and that no coder,
|
||||
reviewer or pm gets either tool.
|
||||
- Probe (not the M-list run): 16 mutants, all killed. ROUTED drops of each action in the broker and in
|
||||
vocabulary, the role file drops of each, `#firing` without the order and without the subject, the
|
||||
reason unchecked, the writer given the reason, the context without it, no `#checkAuthority`, no
|
||||
`role-not-chief`, the consume without the reason, the hire tool's reason optional, and the fire tool
|
||||
gated on the hire grant. The last one survived at first and a one-grant check in tools.test killed it.
|
||||
|
||||
## S3a notes (the launcher's lifecycle pass)
|
||||
|
||||
- `packages/cli/src/launcher.mjs` `lifecyclePass` runs on the tick after the thread and land passes.
|
||||
It lists the open requests (host op `lifecycleRequests`) and carries each under `serialize`: the
|
||||
take, the act, then `lifecycleDone`. A take that answers no closes nothing more here. A refusal
|
||||
goes in the launch log as `refused` with the request id. A done that throws leaves the request
|
||||
taken with no done, which S3b's CLI reports as `outcome-unknown`. The log line names a run that
|
||||
did start.
|
||||
- Launch: `check()` and `start()` gain `human`. The human's request may name any declared instance,
|
||||
as V2 asks. The listing limits the chief. launch.by still takes the `pm` branch (N8). A tester
|
||||
request launches with `launchedBy: "tester"`.
|
||||
- End: SIGTERM to each run the verb named and to any other run of the instance in the registry,
|
||||
each after the start-time check, with a `lifecycle-end` entry. The exit handler writes the one
|
||||
`end` record. A thread instance's end signals only the runs the verb named, so the tester's end
|
||||
leaves the human's threads alone. No SIGKILL escalation here: that is row 191's stop (S6).
|
||||
- Regenerate: the end's signals, then a wait until the old run leaves the registry, then a launch.
|
||||
The wait runs under `serialize`, so no other start runs meanwhile. Past the request's mark
|
||||
(`at` + 60 s, `lifecycleRequestMs` for tests) it closes `regenerate-timeout` and starts nothing. A
|
||||
closing host closes it `host-closing`. Tradeoff: one stuck regenerate holds other starts for up to
|
||||
60 s. The other choice, a second pass that comes back for it, needed state across ticks for the
|
||||
same result.
|
||||
- Delete (V5 steps 1-3): `instance-running` against the registry first, then `lifecycleDelete` (the
|
||||
writer's commit), then the rename of `workspaces/<b>/I/` to `workspaces/<b>/.retired/I-<tx>/`,
|
||||
both 0700. The file goes first, so a failed rename can't leave a launchable name on the old
|
||||
workspace. A failed rename closes `workspace-rename-failed` with the transaction. A later delete of
|
||||
the name is the retry, and the broker now answers the failed transaction, so the rename target is
|
||||
the same (changed assertion above). No workspace means nothing to rename.
|
||||
- `--no-launch` (row 163): the ticker now runs on a no-launch host, but only the lifecycle pass, and
|
||||
`act` closes each request `launches-disabled` before any signal, start or rename. The thread and
|
||||
land passes stay off, so `launcher.test.mjs`'s no-launch tests pass unchanged.
|
||||
- Tests: new `packages/cli/tests/lifecycle.test.mjs`, 9 tests on the in-process host of
|
||||
thread-launch.test.mjs, with ticks called by the test: T2 (unlisted and launch.by launches, a
|
||||
request past 60 s, a check refusal with its code in the log), T3 (`--no-launch`, `capacity-full`
|
||||
with the limit in the log, `instance-running`), T4 (end at the verb, the signal, one end record,
|
||||
the cancelled request never runs), T5 (a new run on the same workspace, `regenerate-timeout`
|
||||
with a SIGSTOPped runner, `regenerate-thread` at the verb), V5 (a failed rename then the retry,
|
||||
modes 0700, no workspace).
|
||||
- Singles at S3a: seat sandbox 10/10, cli launcher 29/29, thread-launch 11/11, git-socket 24/24,
|
||||
bus lifecycle-verbs 12/12, cli instances 6/6, cli lifecycle 9/9, bus process 8/8. broker.mjs
|
||||
imports did not change, and the four must-run files ran anyway.
|
||||
|
||||
## S3b notes (the CLI verbs)
|
||||
|
||||
- `packages/cli/src/instances.mjs`: `business instance launch|regenerate BUSINESS I [--wait S]
|
||||
[--tester]`, `terminate BUSINESS I [--reason TEXT] [--tester]`, `delete BUSINESS I --revision REV
|
||||
[--wait S]` and `business veto BUSINESS keep|unkeep INSTANCE|nohire|hire DEFINITION --revision
|
||||
REV`. `--wait` takes 0 to 600 s and defaults to 30. Every usage error exits 4 before any bus call.
|
||||
add and set refuse `--wait` and `--reason`.
|
||||
- Launch, regenerate and delete print the request id on stderr, then poll `instance.request` every
|
||||
200 ms. Closed with a run or a transaction prints it and exits 0. A refusal prints its reason, the
|
||||
request id and any transaction. A cancel prints who cancelled it. Still open at `--wait` says so and
|
||||
names the 60 s expiry. Taken with no done is `outcome-unknown` and points at the launch log.
|
||||
Terminate doesn't wait: it prints the revoked runs, the cancelled requests and any taken request
|
||||
past cancelling.
|
||||
- Exit codes: every lifecycle outcome that isn't done (refusal, cancel, still open, outcome unknown)
|
||||
exits 3, as the design's `--wait` timeout does. Tradeoff: errors.mjs has 1 for an unknown outcome
|
||||
elsewhere. One code lets a script treat "the request did not do it" as one case, and the message
|
||||
says which. A refusal at the verb keeps the transport's code (2), as add and set do.
|
||||
- The bus `instances` read gains `running` per instance (`#running`, from the claims), so the CLI
|
||||
and S5 can show it without a second verb. An answer with no instances map passes through as
|
||||
before. `bus instances-verb.test.mjs`'s stub writer answers `{ok: true}` and caught the first
|
||||
version, which threw. The CLI view marks `kept` and `running r1, r2` and adds `retired:` and
|
||||
`the chief may not hire:` lines.
|
||||
- `scripts/mosaic` sends `business veto` to packages/cli with instances and instance.
|
||||
- Tests: cli `instances.test.mjs` +8: the V6 veto ops and kept-delete refusal, the `running` read,
|
||||
launch and regenerate printing the run, a closed refusal with its id (exit 3), `--wait 0` still
|
||||
open and taken `outcome-unknown`, request-cancelled, terminate output and `not-held`, delete to
|
||||
its transaction and `workspace-rename-failed` naming it, usage errors (exit 4, no call), and the
|
||||
scripts/mosaic veto route. The existing route test gains the veto assertions. cli
|
||||
`lifecycle.test.mjs` +5: the CLI end to end through the pass (launch, regenerate, `capacity-full`
|
||||
with the request id, terminate, delete), T2a (`launch-revoked` at the take with no process, a
|
||||
done that fails once leaves the request taken and the next pass starts nothing), T4 (a spawned
|
||||
run the bus never bound is signalled, a mismatched start time is not), T6 (run evidence bytes
|
||||
unchanged by a delete, the launch log only grows, a create and a launch of the retired name
|
||||
refuse), T11 (two roots with business `acme`: a launch, a terminate and a delete in one leave the
|
||||
other's files byte-identical). bus `chief-of-staff.test.mjs` +1: T11's bus half, a chief on a bus
|
||||
with two businesses hires and fires only in its own.
|
||||
- Not covered here: T6's `instance-starting` and the V5 refusal order are bus tests from S2c.
|
||||
`check()` refusing a retired name is shown through the verb and the take, since `check()` reads a
|
||||
file that no longer lists the name.
|
||||
- Singles at S3b: cli instances 14/14, cli lifecycle 14/14, bus lifecycle-verbs 12/12, bus
|
||||
instances-verb 3/3 (after the fix), bus chief-of-staff 40/40, webui instances 5/5, seat sandbox
|
||||
10/10, cli launcher 29/29, thread-launch 11/11, git-socket 24/24, usage 2/2, project 4/4, app
|
||||
30/30, host 42/42, tester-threads 5/5, tester-reads 6/6, stack-copy 12/12, queue dispatch 4/4,
|
||||
runs task-cli 9/9, webui pinned-copy 10/10, service 31/31, views 3/3. The broker change adds no
|
||||
import. The four must-run files ran while the `instances` guard was being written, and the guard
|
||||
touches only the read. No test pins a hash of scripts/mosaic.
|
||||
|
||||
## S4 notes (the launch tool takes a hire, D3 item 5)
|
||||
|
||||
- `packages/harness/src/tools.mjs`: the launch tool's `instance` is a string with the ID_PATTERN
|
||||
pattern, not an enum fixed when the bundle was built. The start-time listing stays in the
|
||||
description. The host checks the listing at each launch (`instance-not-listed`), so the chief
|
||||
launches its own hire in the same session. The hire and fire tools came forward in S2e.
|
||||
- The runner does not enforce a tool's schema pattern. A bad name reaches the host and refuses
|
||||
`instance-not-listed`, so the end-to-end test asserts that. M15 (the enum kept) is killed by the
|
||||
tools.test shape assertion, not by the end-to-end test.
|
||||
- `packages/cli/tests/lifecycle.test.mjs` gains the T8 end to end: the chief's session hires
|
||||
`sec-2` (definition chief-information-security-officer, the shipped definition `hireable` accepts:
|
||||
message.send and record.write, no credential) with `TOOL hire`, a launch refuses `no-route`, the
|
||||
human sets the route with `instances.set` op change, the same session launches it (launchedBy
|
||||
chief), `NOT A NAME` and `reviewer` refuse `instance-not-listed`, then `TOOL fire` ends its run,
|
||||
leaves it `launched: false` and a later launch refuses `instance-not-listed`.
|
||||
- Route gap, raised with Sage (honker-0027): harness, model and thinking are agent-layer vars
|
||||
(`business/src/vars.mjs:142-147`) and hire vars allow only model and limits.* (N3). So a hire runs
|
||||
only after the human sets harness and thinking. D3 item 5 says the same session launches it, and
|
||||
it does, after that human step. Letting hire vars carry the route would widen the chief's
|
||||
authority, a policy change that is not mine to make. As built the human step stays, and
|
||||
`roles/chief-of-staff.md` says so. Sage ruled (b) in sage-1521: the human route step stays, and
|
||||
the manifest notes say that launch refuses `no-route` until the human sets the route.
|
||||
- sage-1521 also keeps exit 3 for every lifecycle outcome but done (outcome-unknown included, the
|
||||
S3b tradeoff line above stands for Webby) and the S3a choices (a `--no-launch` host runs only the
|
||||
lifecycle pass, `lifecycleDelete` answers the transaction on a retry). `roles/chief-of-staff.md`
|
||||
is role policy: the manifest names it and the review request flags it as a policy change.
|
||||
- `roles/chief-of-staff.md` (role policy, named in the manifest): one paragraph on the hire's route
|
||||
step and on a fired instance staying in the file off the list.
|
||||
- Singles, each run alone: business chief-of-staff 10/10, spec-review-contracts 3/3, role 19/19,
|
||||
resolve 10/10, harness bundle 22/22, tools 23/23, runner 34/34, pi-session 8/8, seat sandbox
|
||||
10/10, cli launcher 29/29, thread-launch 11/11, git-socket 24/24, lifecycle 15/15, webui reads
|
||||
11/11. No test pins a hash of tools.mjs or roles/chief-of-staff.md. tools.mjs gains an import from
|
||||
vocabulary.mjs, already in RUNNER_FILES, and the four must-run files passed after it.
|
||||
|
||||
## Rebase onto row 192 (2026-10-20T22:10Z)
|
||||
|
||||
Row 192 (4429a104f, Q175 phase 1 SSO) changed webui `serve.mjs`, `cli.mjs`, `app.js` and
|
||||
`index.html`, the files S5 changes. Before S5 the branch was rebased onto origin/refactor
|
||||
f41729178 as `honker/row194-b2`. `honker/row194` stays at f70b4da50 (no force-push). The only file
|
||||
both sides changed was `PRD-1.7-DRAFT.md`, whose blob was the same on both (9bfc6f76), so its five
|
||||
draft commits dropped as already upstream. The tree check: the rebased tip differs from f70b4da50
|
||||
by exactly the base's changes. Every test file the branch changes, the four must-run files and
|
||||
webui sso, service and reads were run again singly on the new base: 23 files, all pass.
|
||||
|
||||
## S5 notes (the Console's Agents section, D7)
|
||||
|
||||
- `packages/webui/src/public/agents.js` (new, `window.MosaicAgents.section`): Settings draws it after
|
||||
the role instances, from the same instances read. The table has instance, definition, launched, kept
|
||||
and running runs, and each row has Launch, a reason box and Terminate. A human also gets Regenerate,
|
||||
Keep or Unkeep, and Delete. The protected assistant has no Delete. Each definition has a No-hire or
|
||||
Allow-hiring button, and a Create form adds an instance of an accepted definition. Retired names show
|
||||
as one line. D7's "retired" column is that line, since a retired name has no instance row.
|
||||
- Regenerate and Delete take a second click. The confirm says what happens: for Delete, "Its evidence
|
||||
and its workspace under .retired stay." Cancel asks nothing.
|
||||
- Choice: every answer that names a request is followed at `/api/agent/request` every 2 s, up to 90
|
||||
reads, until the request closes. D7 names launch and regenerate. Delete and terminate also answer a
|
||||
request (S2c), so the page follows them too, and the receipt shows the close or its refusal reason.
|
||||
A failed read is shown and the poll goes on. The request is never sent again. Past 90 reads the page
|
||||
says the request is still open and asks for a fresh read. A sign-out stops the poll.
|
||||
- With `send` false the section shows "Agent actions are unavailable here. Use mosaic business instance
|
||||
from the instance CLI." and every control is disabled. The tester sees Launch and Terminate only.
|
||||
- `serve.mjs`: `/api/instances` takes op `delete` with the revision and the instance alone and answers
|
||||
the request. `/api/agent/launch`, `/regenerate`, `/terminate` and `/api/veto` each have
|
||||
`instanceSend`'s boundary and one call to `agentWrite`, with only the route's keys. A reason is never
|
||||
logged. `GET /api/agent/request?id=` takes one request id and adds the reason for a closed refusal or
|
||||
a cancel. Every refusal code the S2/S3 verbs answer has a reason line (`AGENT_REASONS`), and a code
|
||||
missing there shows as itself. No route has a `--send` exception.
|
||||
- `cli.mjs`: `agentWrite` reaches only `instance.launch`, `instance.regenerate`, `instance.end`,
|
||||
`veto.set` and `instance.request` (`agentCaller` throws for any other). The tester listener gets
|
||||
launch, end and request only (D4.1).
|
||||
- D4.1 at the webui: the tester listener lets launch and terminate through and refuses regenerate and
|
||||
veto with no call. A tester delete on `/api/instances` is refused in `instanceSend` with no call
|
||||
(`tester-refused`). The bus refuses it too (broker tester-refused), and this keeps the webui from
|
||||
asking. The agents test covers it, and with the guard removed that test fails (control run).
|
||||
- Deferred to S6: the "still running after fire" flag needs the `lifecycle.end.alive` event, which
|
||||
comes with row 191's stop.
|
||||
- Tests: webui `agents.test.mjs` (new, 8: actions and logs, boundary refusals, read-only, refusal and
|
||||
unknown outcomes, the request read, delete, tester routes, agents.js served) and
|
||||
`agents-browser.test.mjs` (new, 1: read-only, launch followed to close, confirms, terminate reason,
|
||||
refusal, keep and unknown outcome).
|
||||
- Singles, each run alone, after the last code change: webui agents 8/8, agents-browser 1/1, browser
|
||||
1/1, instances 5/5, instances-browser 1/1, serve 4/4, service 31/31, sso 28/28, sso-browser 2/2,
|
||||
routes 5/5, routes-browser 1/1, tester-listener 10/10, tester-browser 2/2, tester-cli 2/2,
|
||||
settings-write 4/4, send 13/13, send-browser 2/2, shell 5/5, cli launcher 29/29, thread-launch
|
||||
11/11, git-socket 24/24, stack-copy 12/12, cli stack-release 174/174, seat sandbox 10/10. No test
|
||||
pins a hash of a file S5 changes. shell.test reads bus.js strings and its POST count, which S5 leaves
|
||||
alone because agents.js holds the section's writes.
|
||||
@@ -0,0 +1,707 @@
|
||||
# Row 194 design: agent lifecycle from the Console and the CLI, with a per-agent veto
|
||||
|
||||
Brief: `docs/plans/2026-10-20_agent-lifecycle.md` (blob a907109409ac). Gitea #1692. MVP T1 item 5 (mos-0974,
|
||||
mos-0977, DECISIONS Q176). Owner Honker, reviewer Webby, gate owner Sage. Base: refactor at 53bb59462.
|
||||
Branch `honker/row194`.
|
||||
|
||||
Related designs read: row 190 `agents/darkwing/work/queue-190/DESIGN.md` (r2, after mos-0986), row 191
|
||||
`agents/rocko/work/queue-191/DESIGN-r2.md`. Row 193 is still in design. All line numbers below are at
|
||||
53bb59462.
|
||||
|
||||
## Summary
|
||||
|
||||
- One verb family on the CLI, `mosaic business instance <verb> BUSINESS INSTANCE`, and one Console section.
|
||||
Create and delete change the business file. Launch, terminate and regenerate act on runs.
|
||||
- Every human verb goes through the bus with the human proof. Launch, regenerate and delete are requests the
|
||||
launcher takes once and carries out under `serialize`. Launches go through the one `start()`, so
|
||||
`--no-launch`, the limits, `launch.revoke` and row 191's switch all apply.
|
||||
- The chief hires (adds and lists an instance) and fires (ends its runs and delists it). It never deletes.
|
||||
`role.hire` and `role.fire` are new hard waits, so Jason resolves each one (Mos, mos-1017). The routed
|
||||
form waits on a PRD 1.7 draft in this packet.
|
||||
- A hire allowlist in code: the chief can hire only a definition whose every action is on `HIRE_ALLOWED`
|
||||
(message, record, review request, assigned-task updates, and two actions that matter after row 199).
|
||||
Launch, end, organization, naming, hire, verdict, merge and every later action stay out.
|
||||
- The per-agent forbid is two lists in the business file that only the human writes: `launch.keep`
|
||||
(instances the chief can't fire, replace or end) and `launch.noHire` (definitions the chief can't hire).
|
||||
- The broker reloads a business from its file after each committed write. Create, delete, hire, fire and
|
||||
veto changes then count without a bus restart. Hand edits still need one, except `bus.tester`.
|
||||
- No bus schema change and no migration. Named for review: two new vocabulary actions (hard waits), the
|
||||
role-file `hireable` key, `"hireable": []` in `roles/chief-of-staff.json`, and three business-file keys.
|
||||
|
||||
## What exists today (checked at 53bb59462)
|
||||
|
||||
| # | Fact | Where |
|
||||
|---|---|---|
|
||||
| E1 | Instances are keys of the business file's `roles`. `launch.instances` lists those the chief may launch. The list must be non-empty | `business.mjs:121-132` |
|
||||
| E2 | The human adds an instance and changes its definition, vars and listing. No delete. Credentialed definitions are refused | `instance-settings.mjs:1-5, 22-31, 75-156` |
|
||||
| E3 | The broker copies the businesses once at start and refuses a role it doesn't know | `broker.mjs:208, 446` |
|
||||
| E4 | The chief launches over `launch.sock`. `fromSession` checks identity, then `check()` (which reloads the file), then `authorizeLaunch`, then `start()` | `launcher.mjs:232-327, 624-668` |
|
||||
| E5 | The chief's `launch` tool takes an enum of the instances listed when its bundle was built | `harness/src/tools.mjs:116-119` |
|
||||
| E6 | The chief ends a run with `role.revoke` plus a reason (`role.end`). Ending then launching again is already a replace | `broker.mjs:1601-1628`, `tools.mjs:167` |
|
||||
| E7 | `mosaic stop RUN` is a same-uid SIGTERM. It has no bus call and no human proof | `cli.mjs:447-454`, `seat/src/session.mjs:249-270` |
|
||||
| E8 | The Console stops only the assistant (`assistant.stop`) and writes instances through `instances.set` | `serve.mjs:282-289, 315-363, 535` |
|
||||
| E9 | The launcher already starts runs for the human on its 2 s tick: threads with pending messages, and merge holders | `launcher.mjs:133, 487-597` |
|
||||
| E10 | `launch.revoke` and `launch.restore` are human-only. A revoked business refuses `role.launch` | `broker.mjs:770-774, 2087-2095` |
|
||||
| E11 | Only the assistant is END_PROTECTED. Coder, reviewer, pm and cto all declare gitea and vikunja credentials | `vocabulary.mjs:135`, `roles/*.json` |
|
||||
|
||||
The chief's veto today comes from the human-only membership in E2. The chief can only launch what the human
|
||||
listed. It has no way to create an instance, and the human has no way to stop the chief ending a given
|
||||
instance's runs.
|
||||
|
||||
## D1. The verbs
|
||||
|
||||
All of them are human verbs. Each is a broker verb behind `#human(s)` and then the D4 hook. Each logs its
|
||||
actor. The CLI usage joins `INSTANCES_USAGE` (`cli/src/instances.mjs:9`).
|
||||
|
||||
| Verb | CLI | Bus verb | Effect |
|
||||
|---|---|---|---|
|
||||
| V1 create | `business instance add B I --definition DEF [--vars FILE] [--launch] --revision REV` | `instances.set` op `add` | as today, with no restart (D3) |
|
||||
| V2 launch | `business instance launch B I [--wait SECONDS]` | `instance.launch` | one run, through the launcher |
|
||||
| V3 terminate | `business instance terminate B I [--reason TEXT]` | `instance.end` | ends every run of I and cancels I's open requests |
|
||||
| V4 regenerate | `business instance regenerate B I [--wait SECONDS]` | `instance.regenerate` | V3, then V2 once the run has exited |
|
||||
| V5 delete | `business instance delete B I --revision REV [--wait SECONDS]` | `instances.set` op `delete` | a request. The launcher removes I from the business file and renames its workspace |
|
||||
| V6 veto | `business veto B (keep\|unkeep) I` and `business veto B (nohire\|hire) DEF`, each `--revision REV` | `veto.set` | edits `launch.keep` and `launch.noHire` |
|
||||
|
||||
`mosaic business instances B` (read) gains, per instance: `running` (run ids from the bus claims), `kept`
|
||||
and `retired`. The top level gains `noHire`. `mosaic stop RUN` stays as it is. It is the same-uid operator
|
||||
tool, and V3 is the human verb.
|
||||
|
||||
### D1.1 Requests (V2, V4, V5)
|
||||
|
||||
The broker never starts a process or touches a workspace. Launch, regenerate and delete are requests the
|
||||
launcher carries out, as E9 already does for the human's threads. A request has three states, open, taken
|
||||
and closed, all recorded as `action.allowed` events with reserved action names:
|
||||
|
||||
| Event | Written by | Body |
|
||||
|---|---|---|
|
||||
| `lifecycle.launch`, `lifecycle.regenerate`, `lifecycle.delete` | the broker, at the verb | `{ request, via }`, subject I, actor the human or `tester`. A `human.input` event goes with it, as `launch.revoke` does |
|
||||
| `lifecycle.taken` | host op `lifecycleTake` | `{ request }` |
|
||||
| `lifecycle.done` | host op `lifecycleDone` | `{ request, run? , refusal? }` |
|
||||
| `lifecycle.cancelled` | V3, for each open request of I | `{ request, by }` |
|
||||
|
||||
1. The launcher's new `lifecyclePass` runs on the same tick as `threadPass` and `landPass`. It lists open
|
||||
requests with host op `lifecycleRequests`.
|
||||
2. For each one, under `serialize`, it calls `lifecycleTake(request)`. That op runs in one store
|
||||
transaction. It re-checks, in order: the request is still open (not taken, cancelled or closed), it is
|
||||
less than 60 s old (else it closes as `request-expired`), the business isn't revoked (`launch_state`,
|
||||
`launch-revoked`), I isn't retired, row 191's switch is off (`processing-halted`) for launch and
|
||||
regenerate, and for a tester request `bus.tester` is true in the file (D4.1). Any failure closes the
|
||||
request with that code. Otherwise it writes `lifecycle.taken` and answers go.
|
||||
3. Only after go does the pass act: `check(I, { human: true })` then `start()` for a launch, the V5 steps
|
||||
for a delete.
|
||||
4. The pass then calls `lifecycleDone`. A taken request never runs again, whatever happens after the take.
|
||||
If `lifecycleDone` throws, the request stays taken with no done event. The CLI reports that as
|
||||
`outcome-unknown` with the request id, and the launcher log names the run.
|
||||
5. The CLI polls a read verb, `instance.request`, until the request closes or `--wait` runs out (default
|
||||
30 s). A timeout exits 3 with the request id. An untaken request can still be taken within its 60 s.
|
||||
|
||||
So the human's `launch.revoke` stops every untaken request, not only new ones (B4), and a bus restart
|
||||
can't start a request older than 60 s.
|
||||
|
||||
### V2 launch
|
||||
|
||||
1. At the verb the broker checks that I exists, isn't a thread instance (`not-thread-launch`: threads start
|
||||
on a message), isn't retired, and that the business isn't revoked and, with row 191, isn't halted. These
|
||||
repeat at the take (D1.1 item 2).
|
||||
2. After the take, `check(I, { human: true })` and `start({ instance: I, launchedBy })`, where
|
||||
`launchedBy` is the business human or `tester`. `human: true` lets the human launch an instance that
|
||||
isn't in `launch.instances`. The listing limits the chief, not the human. `launch.by` still goes through
|
||||
the `pm: true` branch (`launcher.mjs:235`), as `--pm` does today. Every other step of `check()` and
|
||||
`start()` is unchanged.
|
||||
|
||||
### V3 terminate
|
||||
|
||||
1. The broker revokes every claim I holds (`#claimEnd(s, h, 'revoke', null, reason, actor)`). A thread
|
||||
instance has all its thread claims revoked, which is exactly `assistant.stop`.
|
||||
2. In the same transaction it writes `lifecycle.cancelled` for every open (untaken) request of I, so no
|
||||
launch the human asked for before the terminate starts after it (B5).
|
||||
3. It writes `lifecycle.end` with the runs. The launcher pass sends SIGTERM to each run's runner after
|
||||
checking its start time, as the idle stop does (`launcher.mjs:526`). It also signals any run of I in the
|
||||
launcher's registry that has spawned but not yet bound, which holds no claim yet. The existing exit
|
||||
handler records the end, so the launch journal and `session.ended` are unchanged.
|
||||
4. Nothing held, nothing open and nothing in the registry answers `not-held`.
|
||||
5. Terminate is allowed while row 191's switch is on. It starts nothing.
|
||||
|
||||
A taken launch request is past cancelling. Its run reaches the registry under `serialize`, and the next
|
||||
terminate ends it. The CLI's terminate output says so when a taken request exists.
|
||||
|
||||
There is no SIGKILL escalation in this row. A run that ignores SIGTERM keeps running. `mosaic stop` and the
|
||||
limits are the operator's tools for it (N6).
|
||||
|
||||
### V4 regenerate
|
||||
|
||||
A fresh session with the same identity and role. It is V3 then V2 under one request. The pass takes the
|
||||
request, waits for the old run's exit handler, then runs `check()` and `start()`. The workspace stays, as
|
||||
it does across every launch today. The session is new because every non-thread launch gets a new run dir
|
||||
(`seat/src/session.mjs:107`, newRun). If the old run hasn't exited by the request's 60 s mark, the request
|
||||
closes as `regenerate-timeout`, nothing starts, and the old run is still running (N6).
|
||||
|
||||
A thread instance (the assistant) refuses `regenerate-thread`. Its context lives in the thread's session
|
||||
directory, and clearing that is T1 item 7 (clear context). This row doesn't take it.
|
||||
|
||||
### V5 delete
|
||||
|
||||
`instances.set` gains op `delete`. At the verb the broker and the writer refuse, in this order:
|
||||
|
||||
| Refusal | When |
|
||||
|---|---|
|
||||
| `tester-refused` | the caller is the tester (B6) |
|
||||
| `organization-definition-refused` | I is END_PROTECTED |
|
||||
| `organization-instance-referenced` | I is `launch.by` or an arbiter. The message names the reference |
|
||||
| `organization-instance-kept` | I is in `launch.keep`. The human runs `veto unkeep` first. Delete never clears a veto silently |
|
||||
| `organization-credentials-out-of-scope` | I holds credentials or a tracker bot. That stays a hand edit until the credentials row (row 199) |
|
||||
| `instance-running` | the broker sees a claim for I. The human terminates first |
|
||||
| `instance-starting` | an open or taken request for I has no done event |
|
||||
| BusinessError | I is the last name in `launch.instances` (E1). The message says so |
|
||||
|
||||
The checks pass with no write, and the broker records a `lifecycle.delete` request. After the take, the
|
||||
launcher pass, under `serialize` so no `start()` of I runs alongside it:
|
||||
|
||||
1. Re-checks the registry for I (`instance-running`) and re-runs the writer's checks against the file as it
|
||||
stands. The `--revision` the human gave must still match, else `revision-conflict`.
|
||||
2. Commits the file through the organization writer. It removes `roles.I` and the references that exist
|
||||
only for I: its `launch.instances` entry, its `launch.limits.tokens.instances` entry and its
|
||||
per-instance model entry. It adds I to a new top-level list, `retired`. The build lists every place
|
||||
`validateBusinessDocument` reads a role instance name and puts each one in one of the two groups (refuse
|
||||
or remove). A reference the table doesn't name stops the build and goes back to Webby.
|
||||
3. Renames `workspaces/<b>/I/` to `workspaces/<b>/.retired/I-<transaction>/`, mode 0700. No workspace is
|
||||
no rename.
|
||||
4. Calls `lifecycleDone` with the transaction.
|
||||
|
||||
The file goes first. Once it is committed, `check()` refuses I as unknown, so no launch can open the old
|
||||
workspace even if step 3 fails. A failed rename closes the request with `workspace-rename-failed`, the
|
||||
transaction and the path left behind. A second `delete` of a retired name skips steps 1 and 2 and retries
|
||||
step 3 only. The file and evidence are already right, so this is the whole recovery.
|
||||
|
||||
What stays:
|
||||
|
||||
- Run evidence (`launches/<b>/<run>/`, `launches/<b>.jsonl`) and bus rows are untouched. They are
|
||||
write-once.
|
||||
- The workspace, under `.retired`. This row deletes no file. A prune with a receipt is a later act.
|
||||
- `retired` stops a later create or hire from reusing the name. Decisions bind to the role instance and
|
||||
outlive restarts (`broker.mjs:783-788`). A new `coder` would otherwise inherit an approval raised by the
|
||||
old one. The tradeoff is a name that can't come back without a hand edit. I chose that over voiding
|
||||
decisions, which would touch a second store.
|
||||
|
||||
### V6 veto
|
||||
|
||||
`veto.set` args: `{ revision, op: "keep" | "unkeep" | "nohire" | "hire", name }`. It goes through the
|
||||
organization writer, so it is logged with actor and diff in `changes.jsonl`. `keep` needs a declared,
|
||||
non-END_PROTECTED instance. `nohire` needs a name that matches the id pattern. A no-op answers
|
||||
`organization-no-change`. It is human-only and isn't in `TESTER_ORGANIZATION_VERBS`.
|
||||
|
||||
## D2. Chief hire and fire
|
||||
|
||||
### D2.1 Authority (Mos, mos-1015 and mos-1017, through Sage, sage-1362 and sage-1365)
|
||||
|
||||
This row adds `role.hire` and `role.fire` to `ACTIONS` (`vocabulary.mjs:11`). Under PRD REQ-WAIT-3 a new
|
||||
action is a hard wait until a PRD version says it isn't (`vocabulary.mjs:52-57`), so both ship as hard
|
||||
waits. Jason resolves each one. There is no PRD change and no grant in a role file: `role.mjs:42` refuses
|
||||
a withinRole that lists a hard wait, and REQ-WAIT-2 forbids it.
|
||||
|
||||
The flow in this row:
|
||||
|
||||
1. The chief raises a `role.hire` or `role.fire` decision whose body carries the arguments (business,
|
||||
name, definition, vars, or name and reason). A hard wait goes to the human only. No arbiter resolves it.
|
||||
2. Jason resolves it in the Console or the CLI.
|
||||
3. The chief calls `instance.hire` or `instance.fire` citing the decision. The broker checks it with
|
||||
`#consumeAuthority(s, 'role.hire', { decision, target })`, as the `role.revoke` decision branch does,
|
||||
and the arguments must equal the decision body field by field (N1, webby-0010). For hire that is
|
||||
`name`, `definition` and `vars` (vars compared with `canonicalJson`, absent equal to absent only). For
|
||||
fire it is `name` and `reason`. A key in the call that the body lacks, or a body key the call lacks,
|
||||
refuses `decision-mismatch`. Every check below runs again at this call, so a veto set after the resolve
|
||||
still refuses.
|
||||
|
||||
The routed form, where the chief hires and fires without a decision each time, needs a PRD version that
|
||||
moves both actions to `ROUTED`. My draft is `agents/honker/work/queue-194/PRD-1.7-DRAFT.md`. It quotes
|
||||
TARGET.md line 66 and carries Mos's bounds. Jason approves it as a `prd.approve` decision in the Console,
|
||||
and Sage opens that decision. When it lands, a later commit adds the two actions to both `ROUTED` lists
|
||||
(`vocabulary.mjs`, `broker.mjs:43-69`) and to `roles/chief-of-staff.json` withinRole. The broker then
|
||||
takes the `reason` form instead of a decision. The verbs and every check below stay the same. This row
|
||||
builds and tests the hard-wait form only. It writes the `hireable` key and its allowlist now (D2.3), because
|
||||
both forms use them.
|
||||
|
||||
### D2.2 What the chief may do
|
||||
|
||||
| Act | Bus verb | Rule |
|
||||
|---|---|---|
|
||||
| launch | `launch` (host) | as today: listed instances only |
|
||||
| end a run | `role.revoke` + reason | as today, and refused `instance-kept` when the target is kept (D2.4) |
|
||||
| hire | `instance.hire { name, definition, vars?, decision }` | adds and lists an instance. Refusals below |
|
||||
| fire | `instance.fire { name, reason, decision }` | ends I's runs, cancels I's open requests, and removes I from `launch.instances`. Refusals below |
|
||||
|
||||
Only the business's chief, the instance named by `launch.by`, may call either verb. Any other caller
|
||||
refuses `role-not-chief`, even with a resolved decision, as `#classification` already limits
|
||||
`role.launch`. The broker reads `hireable` from that chief's own role file. Both act only in the chief's
|
||||
own business. Neither verb takes a business argument. The broker uses the
|
||||
cap's business, and a decision raised in another business refuses `decision-mismatch`. Row 191's switch
|
||||
refuses both while halted (D6). Every hire and fire writes `changes.jsonl` through the organization writer
|
||||
with actor the chief's instance and run, and the decision id.
|
||||
|
||||
Hire refuses, in order:
|
||||
|
||||
1. `definition-not-hireable` when the definition holds an action outside `HIRE_ALLOWED` (D2.3), whatever
|
||||
any list says.
|
||||
2. `definition-not-hireable` when the definition isn't in the chief role file's `hireable` list, or is in
|
||||
the business's `launch.noHire`.
|
||||
3. `organization-definition-refused` for END_PROTECTED, `organization-credentials-out-of-scope` for a
|
||||
credentialed definition (until the credentials row, row 199), `invalid-request` for version 1.
|
||||
4. `organization-instance-retired` when the name is retired.
|
||||
5. When the name exists: a fired instance (in `roles`, not listed) is listed again only if `definition`
|
||||
equals its current one and `vars` is absent. Any other definition or vars refuses
|
||||
`organization-instance-exists`, and so does a listed name. A re-hire never moves an instance's
|
||||
decisions to a different role (B3).
|
||||
|
||||
Hire's `vars` may hold only the keys the chief can already set on an instance through `organization.set`
|
||||
(the model key, `broker.mjs:1266-1269`) and narrowing `limits.*` keys. Anything else, such as `harness`
|
||||
or a route, refuses `invalid-request`. The human sets those (N3).
|
||||
|
||||
Fire refuses `instance-kept` for a kept instance, `organization-definition-refused` for END_PROTECTED,
|
||||
`organization-instance-referenced` for `launch.by`, and a BusinessError for the last listed instance. A
|
||||
fired instance stays in `roles`. The chief never deletes. Delete stays a human act because it retires the
|
||||
name.
|
||||
|
||||
Fire ends the fired instance's processes, not only its claims (F4, Megavolt through sage-1374). Every run
|
||||
gets the shared model token at start: `ENV_ALLOW` (`packages/seat/src/session.mjs:83-94`) passes
|
||||
`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN` and the other provider variables into the sandbox. A revoke
|
||||
can't take that token back, so a fired agent whose process lives on still holds it.
|
||||
|
||||
The delist is a business file commit through the organization writer, and the revoke and events are store
|
||||
rows. No transaction covers both (webby-0011 B2), so fire runs in a fixed order, store side first:
|
||||
|
||||
A. One store transaction: steps 1 to 3 below, plus `fire.stopped` with the decision id. The decision isn't
|
||||
consumed yet.
|
||||
B. The delist through the organization writer (`launch.instances` loses I, logged in `changes.jsonl`).
|
||||
C. One store write consuming the decision and recording `fire.done`.
|
||||
|
||||
If B or C fails, fire answers the step's code and the decision stays unconsumed. A second fire citing the
|
||||
same decision runs again. A finds nothing to revoke, cancel or stop, and that isn't a refusal for fire
|
||||
(unlike V3's `not-held`). B finds I already delisted and counts as done. C consumes. So a fire is safe to
|
||||
repeat until it's consumed. The other order would leave a delisted agent with a live process and its model
|
||||
token when the store step fails, which is the case F4 exists to stop. A halt between A and B doesn't stop
|
||||
B or C, because neither starts anything. A second fire while halted refuses (D6), and the halt's own stop
|
||||
covers the runs.
|
||||
|
||||
Steps 1 to 3 are V3's, with one change:
|
||||
|
||||
1. It revokes every claim the instance holds.
|
||||
2. It writes `lifecycle.cancelled` for each of the instance's open requests.
|
||||
3. It writes `lifecycle.end` with the runs and `kill: true`. The launcher pass stops each runner, spawned
|
||||
but unbound registry runs included, with escalation (sage-1380 item 1):
|
||||
- SIGTERM after the identity check.
|
||||
- After the grace, SIGKILL to any runner still alive, after the same identity check again, and
|
||||
`lifecycle.end.killed` with the run and pid.
|
||||
- A runner still alive after SIGKILL and the observe wait gets `lifecycle.end.alive`. The Console's
|
||||
Agents section shows it to the human.
|
||||
A runner whose identity no longer matches is never signalled. Every check, the recheck after the
|
||||
grace included, uses the identity (start ticks plus boot id), never a bare pid, so a reused pid never
|
||||
counts as the fired run (webby-0011 N2).
|
||||
|
||||
Fire's answer lists the runs it is stopping. The delist stops any new chief launch (`launch` takes listed
|
||||
instances only). A human launch request already taken is past cancelling, as in V3. The human made it, the
|
||||
fire answer names it, and the human's terminate ends its run.
|
||||
|
||||
The stop is row 191's per-run stop, not a second one, so the two can't fight (sage-1380 item 1). Row 191's
|
||||
design r2 (`agents/rocko/work/queue-191/DESIGN-r2.md`, "Stop policy and completion") sends SIGTERM, waits
|
||||
5 s, sends SIGKILL to verified groups and namespaces, and observes for 2 s. Its identity check is start
|
||||
ticks plus boot id, which replaces the start-time check at `launcher.mjs:526`. Fire calls that routine for
|
||||
one instance's runs and uses its constants, so the grace is row 191's 5 s, not the 10 s in r3. The order:
|
||||
|
||||
- Fire is refused while halted (D6), so a halt sweep never meets a fire that started after it.
|
||||
- A halt that comes during a fire's stop finds those runs already stopping. Each run has one stop receipt
|
||||
(row 191's), and the first stop owns it. The halt sweep joins it instead of sending a second SIGTERM or
|
||||
restarting the deadline, and counts the run in its remaining or quiescent totals.
|
||||
- The record says which stop owned it: reason `fire` with the decision, or row 191's `kill-switch` with
|
||||
its generation.
|
||||
|
||||
V3 terminate is unchanged. It still sends SIGTERM only (N6). Only fire escalates, because only a fired
|
||||
agent must lose the token at once. If row 191 hasn't landed when this row builds, the build chains on its
|
||||
candidate (Q177), as D6 already says.
|
||||
|
||||
### D2.3 The hire allowlist (B2, sage-1365 item 4, webby-0010 B1)
|
||||
|
||||
r2 and r3 had a deny list, `HIRE_FLOOR`. Webby's B1 showed it missed routed actions (`git.merge.protected`,
|
||||
which `roles/merge-gate.json` holds with no credentials, `decision.resolve.technical`, `policy.change`,
|
||||
`prd.approve`), and that any action added later would be hireable by default. That isn't fail-closed. r4
|
||||
replaces it with an allowlist.
|
||||
|
||||
A definition is hireable only if every action in its withinRole and crossRole is on `HIRE_ALLOWED`:
|
||||
|
||||
| Allowed action | Held today by |
|
||||
|---|---|
|
||||
| `message.send`, `record.write` | every v2 role file |
|
||||
| `review.request`, `task.update.assigned` | coder, cto, pm, reviewer |
|
||||
| `task.scope.change`, `git.push.working` | coder, cto, pm (credentialed, so they matter only after row 199) |
|
||||
|
||||
Everything else in `ACTIONS` fails, including the actions of the old floor (`role.launch`, `role.end`,
|
||||
`role.revoke`, `organization.change`, `instance.name`, `review.verdict`, `role.hire`, `role.fire`) and those
|
||||
B1 named. An action added to `ACTIONS` later fails until a reviewed commit adds it here.
|
||||
`git.push.remote` (row 199, routed to coder by PRD 1.7) isn't on the list. A coder holding it stays
|
||||
unhireable until a reviewed commit adds it, after the credentials bound is lifted.
|
||||
|
||||
Against today's role files: chief-of-staff, cto, merge-gate, pm, reviewer and security-reviewer fail.
|
||||
Assistant, researcher, coder and the five C-suite files pass the allowlist. Assistant is END_PROTECTED and
|
||||
coder is credentialed, so D2.2 item 3 refuses them. The C-suite files hold only `message.send` and
|
||||
`record.write`, so hiring one gives no authority beyond its name. `hireable` still has to list it.
|
||||
|
||||
The list is a constant in `business/src/vocabulary.mjs`, `HIRE_ALLOWED`. It is checked twice:
|
||||
|
||||
- At load. `role.mjs` validates the chief role file's `hireable` list: distinct definition names, each
|
||||
present in `rolesDir`, version 2, and every action on `HIRE_ALLOWED`. A list naming one that isn't refuses
|
||||
the role file (`hireable names <def>, which holds <action>`). The check reads only the named file's authority lists,
|
||||
never its `hireable`, so a list can't recurse. `hireable` is an optional key on any role file. This is a
|
||||
role schema change, named for review.
|
||||
- At hire, in the broker, against the definition as loaded then. A role file edited after load is caught
|
||||
here.
|
||||
|
||||
`hireable` in `roles/chief-of-staff.json` stays empty in this row (`[]`, Q4, sage-1374). Naming the first
|
||||
role kind is a separate policy commit in `roles/`, made after Jason resolves the `prd.approve` decision, as
|
||||
AGENTS.md invariant 7 requires. With an empty list the chief can hire nothing, even with Jason's resolve,
|
||||
until that commit. The row's tests use scratch role files whose `hireable` names a kind.
|
||||
|
||||
### D2.4 The per-agent forbid and the veto (sage-1365 item 3)
|
||||
|
||||
TARGET.md line 66: "Chief of staff agent has hire/fire authority for agents as needed; unless user forbids
|
||||
per agent". The forbid is two lists in the business file that only the human writes:
|
||||
|
||||
- `launch.keep`: instance names. The chief can't fire, replace or end a kept instance. The broker refuses
|
||||
`instance-kept` for every chief act aimed at it except launch:
|
||||
- `instance.fire`, even with a resolved `role.fire` decision. Mos's limit is that Jason's veto applies
|
||||
to every hire and fire, so the human unkeeps before a fire.
|
||||
- a hire of that name.
|
||||
- `role.revoke` in the reason branch (`broker.mjs:1607-1613`). End then launch is a replace (E6).
|
||||
- an `organization.set` write of that instance's model key (B7). A model change is a replace at the next
|
||||
launch.
|
||||
|
||||
The `role.revoke` decision branch (`broker.mjs:1614-1615`) still ends a kept run. It ends a run, it
|
||||
doesn't fire or replace, and it cites a decision the human resolved (Webby's Q2 ruling). The human's V3
|
||||
and the limits also still end a kept run.
|
||||
- `launch.noHire`: definition names the chief can't hire into this business (D2.2 item 2).
|
||||
|
||||
Why the chief can't change the forbid:
|
||||
|
||||
- Only `veto.set` writes the lists. It is human-only (`#human(s)` then the D4 hook) and refuses the tester.
|
||||
- `organization.set`, the chief's one write verb, reaches only `ORGANIZATION_KEYS` (`broker.mjs:145`),
|
||||
which exclude `launch` and `retired`.
|
||||
- `instances.set` is human or tester only (`broker.mjs:1248-1298`).
|
||||
- Hire and fire carry no field that writes either list.
|
||||
|
||||
T18 tests each of those paths from a chief session and checks the business file bytes are unchanged.
|
||||
|
||||
END_PROTECTED instances can't be kept (N7). That is intended: Q169 lets the chief end the assistant's
|
||||
runs, and the assistant's own stop is `assistant.stop`. Default, per "unless user forbids": nothing is
|
||||
kept and nothing is on `noHire`.
|
||||
|
||||
### D2.5 Where it lives and how the broker enforces it
|
||||
|
||||
The forbid lives in the business file, under `launch`, beside the membership it limits:
|
||||
`launch.keep: [instance]`, `launch.noHire: [definition]`, and top-level `retired: [instance]`. `hireable`
|
||||
lives in the chief's role file, because it bounds the role, not one business.
|
||||
|
||||
`checkLaunch` (`business.mjs:118`) gains the two keys. Each list is distinct. `keep` names declared,
|
||||
non-protected instances. `noHire` entries need only the id pattern (N2). A stale deny entry is harmless,
|
||||
and a reviewed commit that removes a role file must not stop every business that lists it. `retired`
|
||||
names none of `roles`. This is the business-file schema change for review. Old files without the keys load
|
||||
unchanged. An old bus loader rejects a file that has them (`rejectUnknownKeys`), so a rollback past this
|
||||
row needs the keys removed first. The activation plan has to say so (R4).
|
||||
|
||||
The broker enforces from its live copy of the business, which D3 keeps current. The checks sit in the
|
||||
`role.revoke` reason branch, in `organization.set`'s model write and in the two chief verbs, before any
|
||||
write.
|
||||
|
||||
## D3. Live changes
|
||||
|
||||
Today the broker's `#businesses` is fixed at start (E3), and `instances.set` answers `restart: true` for
|
||||
add and redefine.
|
||||
|
||||
1. `Broker.reloadBusiness(id, document)` swaps in one business's validated document. It is synchronous, so
|
||||
no request sees half of it. It refuses an unknown id and a document whose `human` changed (that is row
|
||||
193's ground).
|
||||
2. The runtime's organization callback (`runtime.mjs:121-133`) calls it in one place, after every verb
|
||||
that commits a write, whatever the verb (N1). Today that is `limits.set`, `instances.set`, `routes.set`
|
||||
and `plugins.set`, and after this row `veto.set`, `instance.hire`, `instance.fire` and the launcher's
|
||||
delete through the same writer. It loads from the file through the loader `bootConfig` uses
|
||||
(`cli/src/config.mjs:88-105`), so the broker holds exactly what a restart would give it. Then
|
||||
`limits.refresh(id)` runs, as `plugins.set` does now.
|
||||
3. Other startup consumers of `businesses` in `startBroker` (`runtime.mjs:75-81`): credential references,
|
||||
usage, the web services and the tasks adapter's bots. A create or hire adds no credentials, no tracker
|
||||
bot and no web grant, because credentialed definitions are refused. The build checks each consumer
|
||||
against that claim. Any change that would need one of them still answers `restart: true` and doesn't
|
||||
reload.
|
||||
4. A definition change on a running instance refuses `instance-running`. A live cap must not change
|
||||
authority under its run. This is new: today the change waits for the restart. Existing tests that pin
|
||||
`restart: true` change with it (N4): `business/tests/instance-settings.test.mjs`,
|
||||
`bus/tests/instances-verb.test.mjs`, `cli/tests/instances.test.mjs:76-94` (the `RESTART_NEEDED` text and
|
||||
the restart line) and `webui/tests/instances.test.mjs:25, 71, 74, 140-145`. The build names each
|
||||
changed assertion in its manifest.
|
||||
5. The chief's `launch` tool (E5) takes `instance` as a string with the id pattern instead of the start-time
|
||||
enum. Its description says the host refuses an unlisted instance. `check()` already refuses one
|
||||
(`launcher.mjs:235`). Without this, the chief can't launch its own hire until its session restarts.
|
||||
`tools.mjs` may carry hash pins in tests. The build greps for them (R5).
|
||||
6. Hand edits of the business file still need a restart. Nothing watches the file. The one exception is
|
||||
`bus.tester` (D4.1), read from the file at each tester call.
|
||||
|
||||
After this, `restart` is false for create, delete, hire, fire and veto changes, and true only under item 3.
|
||||
|
||||
## D4. RBAC until row 193
|
||||
|
||||
Every human verb above calls `#human(s)` and then one hook, `#mayHuman(s, verb, target)`. Until row 193
|
||||
lands, the hook passes for the business's one `human`, which is the owner. The verbs are listed once, in
|
||||
an exported `LIFECYCLE_HUMAN_VERBS` (`instance.launch`, `instance.end`, `instance.regenerate`, `veto.set`,
|
||||
and `instances.set` ops `add`, `change`, `delete`), so row 193 maps each verb to the human roles that may
|
||||
use it. Row 193 replaces the hook body and nothing else.
|
||||
|
||||
### D4.1 The tester (Sage, sage-1355)
|
||||
|
||||
What the tester may do after this row:
|
||||
|
||||
| Verb | Tester |
|
||||
|---|---|
|
||||
| `instance.launch`, `instance.end` | yes, new |
|
||||
| `instances.set` ops `add`, `change` | yes, as today (row 175, `broker.mjs:103`, `serve.mjs:537`) |
|
||||
| `instances.set` op `delete` | no, `tester-refused` in the broker (B6) |
|
||||
| `instance.regenerate`, `veto.set` | no, `tester-refused` |
|
||||
|
||||
The tester's cap is bound to one business, and only when that business sets `bus.tester` to true
|
||||
(`broker.mjs:542-546`). That stays Jason's switch, and no bus verb writes it. Two checks run on each tester
|
||||
lifecycle call, before any effect, and again at the take (D1.1):
|
||||
|
||||
1. The request's business is the cap's business. Any other refuses `tester-refused`.
|
||||
2. `bus.tester` is exactly true in the business file as it stands, read through `settings.current(id)` as
|
||||
`harnessOn` does (`runtime.mjs:136-142`), not from the broker's copy (B8). An unreadable file refuses
|
||||
`tester-off`. So a hand edit turning testing off counts at the next tester call, without a restart.
|
||||
|
||||
The request and done events record actor `tester`. The tester's Q173 tests of the Agents section then cover
|
||||
launch and terminate on a scratch business, and the rest through fixtures.
|
||||
|
||||
## D5. One stack instance
|
||||
|
||||
Row 190 carries the stack instance as the config `MOSAIC_CONFIG` names. These verbs take no instance name.
|
||||
The CLI reaches the broker socket under that config's `dataRoot`. The broker acts on its own businesses,
|
||||
launcher and data root. A business id that exists in two stack instances is two separate businesses. A test
|
||||
runs two scratch roots with the same business id and shows a launch, a terminate and a delete in one leave
|
||||
the other byte-identical (T11).
|
||||
|
||||
## D6. The kill switch
|
||||
|
||||
- Launch and regenerate end in `start()`, which row 191 gates (its D3 inventory, `launcher.mjs:329-457`).
|
||||
The new pass adds no second start path.
|
||||
- The broker refuses `instance.launch` and `instance.regenerate` with `processing-halted` while the switch
|
||||
is on (V2 step 1), and the take checks again (D1.1), so no request is waiting when a human resumes.
|
||||
- `instance.hire` and `instance.fire` are agent work verbs, so row 191's default refuses them while halted.
|
||||
The build adds them to row 191's work table, not its read list. A hire Jason resolved before a halt
|
||||
still refuses while halted, and the chief calls it again after the resume.
|
||||
- Terminate, create, delete and veto start nothing and stay allowed.
|
||||
- If row 191 hasn't landed when this row builds, the build chains on its candidate (Q177). The two halted
|
||||
checks are written against its reader and tested with its fixture.
|
||||
|
||||
## D7. The Console
|
||||
|
||||
A new Settings section, "Agents", in its own `agents.js` file. Settings itself isn't rewritten. It lists
|
||||
each instance with definition, listed, kept, retired and running runs, and flags a run with a
|
||||
`lifecycle.end.alive` event as "still running after fire" (D2.2). It has these actions:
|
||||
|
||||
- Create (definition select, from `definitions`), Delete (with a confirm naming what stays: the evidence
|
||||
and the workspace under `.retired`).
|
||||
- Launch, Terminate (with an optional reason), Regenerate (with a confirm).
|
||||
- Keep and Unkeep per instance, and a No-hire toggle per definition.
|
||||
|
||||
The routes are `POST /api/instances` (op `delete` joins `add` and `change`), `POST /api/agent/launch`,
|
||||
`/api/agent/terminate`, `/api/agent/regenerate` and `POST /api/veto`. Each uses `instanceSend`'s boundary:
|
||||
signed-in user, exact Origin, same-origin Fetch Metadata, JSON within 16 KiB, strict keys, one bus call
|
||||
through the human transport, and `outcome-unknown` on an unexpected answer. Launch and regenerate show the
|
||||
request, then poll `GET /api/agent/request?id=` until it closes.
|
||||
|
||||
`agents.js` joins the static file list (`serve.mjs:28`) and `index.html` (N5).
|
||||
|
||||
When `/api/config` says `send` is false, every action button shows as unavailable with the text "Agent
|
||||
actions are unavailable here. Use mosaic business instance from the instance CLI." This is the live state
|
||||
until Q175 phase 2. Every write test uses a fixture server and fixture writers, as rows 165 and 191 do. No
|
||||
route gets a `--send` exception.
|
||||
|
||||
## D8. Tests
|
||||
|
||||
Every test uses scratch roots under `TMPDIR` and the existing fixtures (`business/tests/helpers.mjs`,
|
||||
`cli/tests/helpers.mjs`, `webui/tests/tester-fixture.mjs`). None touches `~/.mosaic-dev`,
|
||||
`~/.config/mosaic-dev`, a live unit or a slot file.
|
||||
|
||||
| Test | Shows | File |
|
||||
|---|---|---|
|
||||
| T1 | V1 create on a running bus: a launch of the new instance binds with no restart, and the answer says `restart: false` | cli `instances.test.mjs` |
|
||||
| T2 | V2: a human launch of an unlisted instance runs. `launch.by` goes through the `pm` branch (N8). A thread instance, a retired one and a revoked business refuse at the verb. A request older than 60 s never launches. A refused `check()` closes the request with its code | cli `lifecycle.test.mjs` (new) |
|
||||
| T2a | Take: a `launch.revoke` between the request and the pass closes it `launch-revoked` with no process. A `lifecycleDone` that throws once leaves the request taken, and the next tick starts nothing. A taken request is never taken twice | same |
|
||||
| T3 | V2 with `--no-launch` and with each limit refusal: no process, request closed with the code | same |
|
||||
| T4 | V3: claims revoked, runner SIGTERMed after the start-time check, one end record. A mismatched start time sends no signal. An open request is cancelled and never starts. A spawned, unbound run in the registry is signalled | same |
|
||||
| T5 | V4: the new run starts only after the old exit, with a new run dir and the same workspace. A run that ignores SIGTERM closes the request `regenerate-timeout` with nothing started. The assistant refuses `regenerate-thread` | same |
|
||||
| T6 | V5: each refusal in the V5 table, in order, including `instance-starting` with an open request. A clean delete leaves the evidence bytes unchanged, commits the file before the rename, adds `retired`, and a create or hire of the same name refuses. A failed rename closes `workspace-rename-failed`, `check()` refuses the name, and a second delete renames only | business `instance-settings.test.mjs` and cli `lifecycle.test.mjs` |
|
||||
| T7 | V6: each op, the no-op refusal, an unknown `noHire` name accepted, and a `changes.jsonl` entry with actor `human` | business |
|
||||
| T8 | Chief hire (hard-wait form): no decision refuses. A decision raised by the chief goes to the human only, no arbiter. After the resolve, the hire adds and lists the instance, logs actor and decision, and the same chief session launches it (D3 item 5). Arguments that differ from the decision body refuse. A non-chief caller with a resolved decision refuses `role-not-chief`. Outside `hireable`, in `noHire`, credentialed, END_PROTECTED, retired, and extra vars each refuse | bus `chief-of-staff.test.mjs` |
|
||||
| T8a | Re-hire (B3): a fired name with the same definition and no vars is listed again. Another definition or vars refuses `organization-instance-exists` | same |
|
||||
| T8b | Hire allowlist (D2.3): one case per action in `ACTIONS` that is neither on `HIRE_ALLOWED` nor a hard wait (`role.mjs` already refuses those), looped from the lists, not typed by hand. A fixture definition, clean when the chief's role file loads and listed in `hireable`, is rewritten to hold that action before the hire. The hire refuses `definition-not-hireable`. A definition holding one allowed and one refused action refuses. One holding only allowed actions hires. A pin test fixes `HIRE_ALLOWED` to the six actions | same |
|
||||
| T8c | Allowlist at load: a role file whose `hireable` names a definition holding each refused action refuses at `loadRole`, one case per action, looped the same way. The mixed case refuses. A clean list loads. A missing, version 1 or duplicate name refuses | business `role.test.mjs` |
|
||||
| T9 | Chief fire: runs end, open requests cancelled, delisted, still in `roles`. Kept (even with a resolved decision), END_PROTECTED, `launch.by` and last-listed refuse | bus `chief-of-staff.test.mjs` |
|
||||
| T9a | Fire ends processes (F4, sage-1380 item 1): with a bound run and a spawned but unbound registry run of I, both fixture runners holding a stand-in model variable, fire answers both runs. After the launcher pass, neither runner pid is alive and I holds no claim. A fixture runner that ignores SIGTERM gets SIGKILL after the grace, is gone, and has `lifecycle.end.killed` with its run and pid. A stand-in whose kill is stubbed to fail gets `lifecycle.end.alive`. A runner whose identity changed before the SIGTERM, or between SIGTERM and SIGKILL, gets no signal. V3 terminate of a SIGTERM-ignoring runner sends no SIGKILL | cli `launcher.test.mjs` |
|
||||
| T9b | Fire and row 191's halt: a halt during a fire's stop sends no second SIGTERM, keeps the fire's deadline, writes one stop receipt with reason `fire` and the decision, and counts the run in its totals. A fire while halted refuses (D6) | cli `launcher.test.mjs`, on row 191's fixture |
|
||||
| T9c | Fire order (webby-0011 B2): the organization writer stubbed to fail after A commits. Fire answers the writer's code, I's claims are revoked and its runs stopped, I is still listed, and the decision is unconsumed. A second fire citing the same decision delists I and consumes it. A third fire refuses `decision-consumed`. With C stubbed to fail, the second fire finds I delisted and only consumes | bus `chief-of-staff.test.mjs` |
|
||||
| T10 | Keep: chief `role.revoke` with a reason, fire, hire of that name and an `organization.set` model write aimed at a kept instance each refuse `instance-kept`. A `role.revoke` citing a human-resolved decision ends the kept run (Q2). The human's V3 works | same |
|
||||
| T11 | Two scratch stack instances with the same business id: V2, V3 and V5 in A leave B's root and config byte-identical. A chief of business A can't hire into business B on the same bus | cli `lifecycle.test.mjs`, bus |
|
||||
| T12 | Agent and reader sessions refuse every human verb before any effect. The tester gets exactly the D4.1 table, including `tester-refused` for `instances.set` op `delete` (B6). The D4 hook is called with the verb | bus `instances-verb.test.mjs` |
|
||||
| T12a | Tester launch and terminate work on the cap's business with `bus.tester` true, record actor `tester`, and refuse `tester-refused` for a second business on the same bus. A file edit setting `bus.tester` to false, with no restart, refuses the next call and the take `tester-off`. An unreadable file refuses `tester-off` | bus `instances-verb.test.mjs` |
|
||||
| T13 | `organization.set` with `launch`, `retired`, `keep` or `noHire` keys refuses | bus |
|
||||
| T14 | Live reload: after each committed verb, `plugins.set` and `routes.set` included, the broker answers from the new document. A definition change on a running instance refuses `instance-running` | bus |
|
||||
| T15 | Kill switch (with row 191's fixture): launch and regenerate refuse `processing-halted` at the verb and at the take, hire and fire refuse, terminate works | bus and cli |
|
||||
| T16 | Console: each route's boundary refusals, a fixture write per action, `outcome-unknown` on a bad shape, unavailable buttons when `send` is false, request polling, and `agents.js` served from the static list | webui `agents.test.mjs` (new) |
|
||||
| T17 | Business file: the new keys validate, each bad entry refuses, and a file without them loads byte for byte as before | business `business.test.mjs` |
|
||||
| T18 | The forbid can't be changed by the chief (D2.4): from a chief session, `veto.set`, `organization.set` with each list key, `instances.set`, and hire and fire with extra fields all refuse, and the business file bytes are unchanged | bus `chief-of-staff.test.mjs` |
|
||||
|
||||
### Mutants
|
||||
|
||||
Each one must fail at least one test. They run against the candidate, and the file is restored and its
|
||||
sha256 checked after each one.
|
||||
|
||||
| # | Change | Killer |
|
||||
|---|---|---|
|
||||
| M1 | `instance.launch` skips `#human` | T12 |
|
||||
| M2 | the pass calls `check()` without `human: true` | T2 |
|
||||
| M3 | the take accepts a request older than 60 s | T2 |
|
||||
| M4 | the take skips the `launch_state` check | T2a |
|
||||
| M5 | terminate signals without the start-time check | T4 |
|
||||
| M6 | regenerate starts before the old exit | T5 |
|
||||
| M7 | delete skips the running check | T6 |
|
||||
| M8 | delete skips `retired` | T6 |
|
||||
| M9 | delete clears `keep` instead of refusing | T6 |
|
||||
| M10 | delete removes the workspace instead of renaming it | T6 |
|
||||
| M11 | the `keep` check is removed from the `role.revoke` reason branch | T10 |
|
||||
| M12 | `noHire` is not read by hire | T8 |
|
||||
| M13 | fire deletes the instance from `roles` | T9 |
|
||||
| M14 | `reloadBusiness` isn't called after `instances.set` | T1, T14 |
|
||||
| M15 | the tool enum is kept | T8 |
|
||||
| M16 | the halted check is dropped from `instance.launch` | T15 |
|
||||
| M17 | the Console route skips the Origin check | T16 |
|
||||
| M18 | `veto.set` joins `TESTER_ORGANIZATION_VERBS` | T12 |
|
||||
| M19 | the tester's business scope check is dropped | T12a |
|
||||
| M20 | the tester's `bus.tester` file read is dropped | T12a |
|
||||
| M21 | hire skips the role file's `hireable` list | T8 |
|
||||
| M22 | the take doesn't write `lifecycle.taken`, so a request can be taken twice | T2a |
|
||||
| M23 | terminate doesn't cancel open requests | T4 |
|
||||
| M24 | delete skips the `instance-starting` check | T6 |
|
||||
| M25 | delete renames before it commits the file | T6 |
|
||||
| M26 | the tester's `instances.set` op `delete` refusal is dropped | T12 |
|
||||
| M27 | the `keep` check is dropped from `organization.set`'s model write | T10 |
|
||||
| M28 | the `keep` check is added to the `role.revoke` decision branch | T10 |
|
||||
| M29 | a re-hire accepts a different definition | T8a |
|
||||
| M30 to M36 | the broker's hire check accepts one action outside `HIRE_ALLOWED`, one mutant per action: `role.launch`, `role.end`, `organization.change`, `instance.name`, `review.verdict`, `git.merge.protected`, `decision.resolve.technical` | T8b |
|
||||
| M37 to M43 | the same seven, accepted by the load check in `role.mjs` only | T8c |
|
||||
| M44 | fire skips `instance-kept` when a decision is cited | T9 |
|
||||
| M45 | `instance.hire` skips the `launch.by` caller check | T8 |
|
||||
| M46 | `veto.set` skips `#human` | T18 |
|
||||
| M47 | the broker's hire check needs some action on `HIRE_ALLOWED` instead of every action | T8b |
|
||||
| M48 | the same in `role.mjs` only | T8c |
|
||||
| M49 | fire revokes the claims but writes no `lifecycle.end`, so no runner is signalled | T9a |
|
||||
| M50 | fire's signal skips spawned but unbound registry runs | T9a |
|
||||
| M51 | fire's stop drops the SIGKILL escalation | T9a |
|
||||
| M52 | SIGKILL is sent without the second identity check | T9a |
|
||||
| M53 | a killed runner gets no `lifecycle.end.killed` | T9a |
|
||||
| M54 | V3 terminate escalates to SIGKILL as fire does | T9a |
|
||||
| M55 | the halt sweep sends its own SIGTERM and restarts the deadline on a run fire is stopping | T9b |
|
||||
| M56 | fire delists before the store step | T9c |
|
||||
| M57 | fire consumes the decision in step A | T9c |
|
||||
| M58 | a second fire with nothing to revoke refuses `not-held` | T9c |
|
||||
|
||||
## Scope and boundaries
|
||||
|
||||
- In the row: the verbs and passes above in `business` (vocabulary, role validation, instance-settings,
|
||||
business validation), `bus` (broker verbs, reload, runtime callback), `cli` (instances command, launcher
|
||||
pass, host ops), `harness` (the launch tool's schema, and the hire and fire tools), `webui` (routes and
|
||||
the Agents section), the tests, the package READMEs and the PRD draft in this packet. `docs/TOOLS.md`
|
||||
text goes to Sage.
|
||||
- Named for review:
|
||||
- Vocabulary: `role.hire` and `role.fire` join `ACTIONS` as hard waits, under REQ-WAIT-3. The commit
|
||||
message names them as hard waits, as REQ-WAIT-3 requires.
|
||||
- Role schema: the optional `hireable` key, validated in `role.mjs` (D2.3).
|
||||
- Role file: `roles/chief-of-staff.json` gains `"hireable": []`. No authority change.
|
||||
- Business file schema: `launch.keep`, `launch.noHire`, `retired` (D2.5).
|
||||
- Not in the row: the routed form (after PRD 1.7), filling `hireable`, credentialed hires and deletes (row
|
||||
199), clear context for the assistant (T1 item 7), human roles (row 193), SIGKILL escalation, and any
|
||||
live activation or Console start.
|
||||
|
||||
## Risks
|
||||
|
||||
- R1. Case 1's working roles (coder, reviewer, pm, cto) all declare credentials (E11), and pm, cto and
|
||||
reviewer fail the hire allowlist anyway. Neither the human verb nor the chief can create or delete credentialed
|
||||
roles until the credentials row (row 199). That is the largest gap against Case 1, and it isn't this
|
||||
row's to close.
|
||||
- R2. The human proof is path-bound and same-uid (P19 333). Any same-uid process that runs the tree's
|
||||
`human-cli.mjs` can launch and delete. Row 190b's separate uid and row 193 narrow it. This row adds no
|
||||
check of its own.
|
||||
- R3. The 60 s request window means a slow `check()` (checkouts) can still start a run up to that point
|
||||
after the CLI gave up. The CLI says so on timeout.
|
||||
- R4. Rollback past this row needs `keep`, `noHire` and `retired` removed from each business file, and
|
||||
`hireable` from the role file, or the old loaders refuse them. The activation plan states this.
|
||||
- R5. `tools.mjs`, `serve.mjs`, `broker.mjs`, `vocabulary.mjs` and `role.mjs` may carry sha256 pins in
|
||||
other packages' tests. The build greps the tests for every changed path plus a 64-hex value before
|
||||
in-review, without a `head`.
|
||||
- R6. With `hireable` empty, the chief can hire nothing until the policy commit in `roles/` that follows
|
||||
the `prd.approve` resolve names a kind (Q4). Case 1's "as needed" waits on that commit and on PRD 1.7.
|
||||
- R7. Fire's SIGKILL closes the SIGTERM-ignoring case (sage-1380). A runner still alive after SIGKILL
|
||||
(an unkillable process, or a stop whose identity check fails) keeps the shared model token until the
|
||||
human acts on `lifecycle.end.alive`. Only a per-agent model token (row 199 Q-M4, a later proxy row)
|
||||
would let a fire revoke the token itself.
|
||||
|
||||
## Questions and rulings
|
||||
|
||||
- Q1, ruled by Mos (mos-1015, mos-1017, through sage-1365): `role.hire` and `role.fire` ship as hard
|
||||
waits Jason resolves. The routed form needs PRD 1.7, drafted in `PRD-1.7-DRAFT.md`. D2.1.
|
||||
- Q2, ruled by Webby (webby-0003): keep blocks the chief's `role.revoke` reason branch, not its decision
|
||||
branch. D2.4.
|
||||
- Q3, ruled by Sage (sage-1355): launch and terminate are tester verbs scoped to the tester's own
|
||||
business, behind `bus.tester`. D4.1.
|
||||
- Q4, ruled by Sage (sage-1374): `hireable` stays empty in this row. Naming the first role kind is a
|
||||
separate policy commit in `roles/` after Jason resolves the `prd.approve` decision. Tests use scratch role
|
||||
files that name a kind. D2.3.
|
||||
|
||||
## r2 changes against webby-0003
|
||||
|
||||
| Finding | Where |
|
||||
|---|---|
|
||||
| B1 | D2.1: hard waits under REQ-WAIT-3, no role grant. The routed form is PRD 1.7 (packet draft) |
|
||||
| B2 | D2.3: the floor from sage-1365 item 4, checked at load and at hire, T8b, T8c, M30 to M43 (an allowlist since r4) |
|
||||
| B3 | D2.2 hire item 5: a re-hire keeps the definition, T8a, M29 |
|
||||
| B4 | D1.1: one `lifecycleTake` transaction re-checks and writes `lifecycle.taken`, T2a, M4, M22 |
|
||||
| B5 | V3 cancels open requests and signals unbound runs. V5 refuses `instance-starting`, runs under `serialize`, commits the file before the rename, and states the recovery. T4, T6, M23 to M25 |
|
||||
| B6 | V5 table and D4.1: the tester's delete refuses, T12, M26 |
|
||||
| B7 | D2.4: keep covers `organization.set`'s model write, T10, M27 |
|
||||
| B8 | D4.1: `bus.tester` is read from the file at each call and at the take, T12a, M20 |
|
||||
| Q2 | D2.4, T10, M28 |
|
||||
| N1 | D3 item 2: one reload after every committed write |
|
||||
| N2 | D2.5: `noHire` takes any id-pattern name |
|
||||
| N3 | D2.2: hire's vars limited to the model key and narrowing `limits.*` |
|
||||
| N4 | D3 item 4 names the tests that change |
|
||||
| N5 | D7 and T16: `agents.js` joins `serve.mjs:28` and `index.html` |
|
||||
| N6 | V3 and V4: no SIGKILL, `regenerate-timeout` |
|
||||
| N7 | D2.4: intended, Q169 |
|
||||
| N8 | V2 item 2 and T2 |
|
||||
|
||||
## r3 changes against sage-1374, sage-1377 and Megavolt's F4
|
||||
|
||||
| Item | Where |
|
||||
|---|---|
|
||||
| Q4 ruled: `hireable` stays empty, the first kind is a later policy commit in `roles/` | D2.3, R6, Questions |
|
||||
| `instance.name` joins the floor (Webby B2, mos-1023 item 6) | D2.3 table, T8b, T8c, M47, M48 |
|
||||
| F4: fire ends the fired agent's processes, since `ENV_ALLOW` gives every run the shared model token | D2.2, D7, T9a, M49 to M51, R7 |
|
||||
| `git.push.remote` routed to coder only (mos-1028 item 3, row 199 r3) | `PRD-1.7-DRAFT.md`, REQ-LAND-2 and the REQ-WAIT-3 append |
|
||||
|
||||
## r4 changes against webby-0010, sage-1380 and megavolt-0008
|
||||
|
||||
| Item | Where |
|
||||
|---|---|
|
||||
| webby-0010 B1: the deny-list floor becomes the allowlist `HIRE_ALLOWED`, so merge-gate, `decision.resolve.technical`, `policy.change`, `prd.approve` and any later action are refused | D2.3, Summary, R1, T8b, T8c, M30 to M43, M47, M48, PRD REQ-COS-3 bound |
|
||||
| webby-0010 N1: field-by-field compare of the call against the decision body, extra or missing keys refuse | D2.1 item 3 |
|
||||
| webby-0010 N2: the model write is in REQ-COS-3's text | `PRD-1.7-DRAFT.md` |
|
||||
| sage-1380 item 1: fire escalates to SIGKILL through row 191's per-run stop, and the order against a halt is fixed | D2.2, T9a, T9b, M51 to M55, R7 |
|
||||
| sage-1380 item 2 and megavolt-0008: REQ-LAND-2 binds one commit oid, and Megavolt's three text changes against row 199 r4 (e1aa6216c) | `PRD-1.7-DRAFT.md` |
|
||||
| webby-0011 B1: same as webby-0010 B1, M47 and M48 now allowlist mutants | D2.3 |
|
||||
| webby-0011 B2: fire runs store side first, then the delist, then the consume, safe to repeat | D2.2 order A to C, T9c, M56 to M58 |
|
||||
| webby-0011 N1: the remote-owner host check at each push | sent to Megavolt for row 199 |
|
||||
| webby-0011 N2: identity, not bare pid, at every check | D2.2 step 3 |
|
||||
| Q4: Webby's view matches Sage's ruling (empty) | no change |
|
||||
@@ -0,0 +1,158 @@
|
||||
# PRD 1.7 draft: the chief of staff's hire and fire authority, the coder's push to its fork, and agent
|
||||
# host access through the credential store
|
||||
|
||||
Draft by Honker for row 194 (#1692), under Mos's ruling mos-1017 (through Sage, sage-1365). Not approved.
|
||||
It changes nothing until Jason approves it as a `prd.approve` decision in the Console. Sage opens that
|
||||
decision and sends Mos its id. Base: `docs/prd/mosaic-stack.md` version 1.6 at 53bb59462.
|
||||
|
||||
## What it changes
|
||||
|
||||
Row 194 adds `role.hire` and `role.fire` to the vocabulary, and row 199 adds `git.push.remote`. Under
|
||||
REQ-WAIT-3 all three are hard waits, so today Jason resolves every hire, every fire and every push to a
|
||||
remote. This version routes hire and fire for the chief of staff within fixed bounds, so the chief hires
|
||||
and fires as needed and the user's per-agent forbid still holds. It routes `git.push.remote` for the coder
|
||||
only (Mos, mos-1028 item 3). It adds agent host access with sudo through the credential store for
|
||||
agents with a grant (Q183, Q184, Vikunja #424, due 2026-10-27). One `prd.approve` decision covers it all
|
||||
(Vikunja #420).
|
||||
|
||||
When this version lands, a reviewed commit:
|
||||
|
||||
1. adds `role.hire`, `role.fire` and `git.push.remote` to `ROUTED` in
|
||||
`packages/business/src/vocabulary.mjs` and in `packages/bus/src/broker.mjs`.
|
||||
2. adds `role.hire` and `role.fire` to `roles/chief-of-staff.json` withinRole, and `git.push.remote` to
|
||||
`roles/coder.json` withinRole. No other role file gets `git.push.remote`.
|
||||
3. switches the broker's check on `instance.hire` and `instance.fire` from a cited decision to the
|
||||
`reason` form, as `role.end` works for REQ-ROLE-2, and makes `publish`'s `decision` optional for a
|
||||
within-role class (row 199 D9 step 1).
|
||||
4. states, for a `git.push.remote` with no decision, where the source commit comes from: the latest work
|
||||
commit the bus recorded for the coder's task. The check still matches it against the work ref tip and
|
||||
the push sends it by oid (row 199, duckworth-0022 N9).
|
||||
5. adds the host credential grant check and the host access it allows (REQ-CRED-3, Vikunja #424).
|
||||
|
||||
Every other check stays as rows 194 and 199 build it. Filling `hireable` stays a separate reviewed role
|
||||
file commit in `roles/`.
|
||||
|
||||
## Text for the PRD
|
||||
|
||||
Status line, appended to the current paragraph:
|
||||
|
||||
> Version 1.7 routes the chief of staff's hire and fire actions and the coder's push to its own fork,
|
||||
> and adds agent host access through the credential store, approved by Jason in decision
|
||||
> <prd.approve decision id>.
|
||||
|
||||
Source, a new bullet above "Version 1.6 source":
|
||||
|
||||
> - Version 1.7 source, TARGET.md line 66: "Chief of staff agent has hire/fire authority for agents as
|
||||
> needed; unless user forbids per agent". And Jason through Mos (mos-1067), DECISIONS Q183, in part: "I will
|
||||
> still want to allow infra agent(s) on my .53 instance to access other servers, etc. However, not all
|
||||
> agents will need to have that access. I envision a credential store within Mosaic Stack that works
|
||||
> as some sort of variable for the agent to call. The system will enforce access to the credentials to
|
||||
> keep the agents sandboxed with proper security. I need to be able to trust my agents with assisting
|
||||
> me. This will require sudo access to several systems. If we can utilize the credential store in
|
||||
> Mosaic Stack and allow agents to perform their jobs with NOPASSWD sudo on various machines, I am a
|
||||
> happy man." DECISIONS Q184, on whether that is in place by 10-27: "Q5: by 10.27"
|
||||
|
||||
Under "Added in version 1.2", after REQ-COS-2:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-COS-3.** The chief of staff hires and fires role instances without a decision each time. Hiring
|
||||
> adds a role instance to the organization file and lists it for launch. Firing ends the instance's
|
||||
> runs and removes it from the launch list. It stays in the organization file. Only the user deletes
|
||||
> an instance. These actions are `role.hire` and `role.fire`. They leave the hard waits and route
|
||||
> under this requirement, within these bounds:
|
||||
> - The chief acts only in its own organization.
|
||||
> - The chief hires only role definitions that need no credentials, until a later version adds
|
||||
> credentialed roles.
|
||||
> - The chief hires only the role kinds its role file lists. Changing that list is a reviewed commit.
|
||||
> - The chief hires only a role whose every action is on a fixed list of plain work actions: sending
|
||||
> messages, writing records, asking for review, updating its assigned tasks, and the scope and
|
||||
> working-push actions of an assigned task. A role holding any other action, such as launch, end,
|
||||
> organization, naming, hire, review verdict or merge authority, or any action added later, is never
|
||||
> hired. So the chief never hires a chief, never picks its own reviewers or merge gate, and the author
|
||||
> is never the reviewer. This holds whatever the role file's list says. Adding to that list is a PRD
|
||||
> change.
|
||||
> - The chief may set a hired instance's model and narrow its limits, and nothing else about it.
|
||||
> - The user can forbid per agent. The user marks an instance so the chief can't fire it, end its runs
|
||||
> or change its model, and marks role kinds the chief can't hire. Only the user changes these marks, through the
|
||||
> WebUI or the CLI.
|
||||
> - The kill switch stops every hire and fire while it is on.
|
||||
> - Every hire and fire is logged with the chief's instance, its run and the reason.
|
||||
|
||||
Under "Landing", after REQ-LAND-1:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-LAND-2.** A coder pushes its own work to its own fork. The action is `git.push.remote`.
|
||||
> - What it pushes: one commit, by its oid, never a ref name. That commit is the latest work commit the
|
||||
> bus recorded for the coder's task, and the coder made that push. A decision that authorizes a push
|
||||
> names that commit, and it doesn't authorize any other.
|
||||
> - Where it lands: only the branch `refs/heads/<its Gitea user>/<branch>`. The host builds that
|
||||
> name, the agent never does. A push never forces.
|
||||
> - Which remote: the agent's own fork of the project's upstream. The business file names the upstream,
|
||||
> and the host never contacts it. For each push the host builds the fork's address over https from
|
||||
> the upstream's host, port and repository name and the agent's own Gitea user. The history check,
|
||||
> the push and the credential match all use that address. So the agent's own user owns the receiving
|
||||
> repository on every push, and the agent never names it.
|
||||
> - The host refuses an upstream on a host or port other than the agent's Gitea, and an upstream the
|
||||
> agent's own user owns. It refuses a fork that is missing, empty or doesn't share the project's
|
||||
> history. Gitea's push-to-create stays off, so a push never creates a repository.
|
||||
> - Who may: the action routes under REQ-DEC-2 and is granted within-role to the coder role only. No
|
||||
> other role file grants it.
|
||||
> - Before version 1.7 the action was a hard wait under REQ-WAIT-3, so each push needed a decision
|
||||
> the human resolved, and that decision named the commit.
|
||||
>
|
||||
> Every push is logged with the remote, the ref, the commit and, when one was given, the decision.
|
||||
|
||||
Under "Credentials", after REQ-CRED-2:
|
||||
|
||||
> Added in version 1.7:
|
||||
>
|
||||
> - **REQ-CRED-3.** An agent with a grant can run commands with sudo on another host. The agent asks
|
||||
> for the host credential by name. The stack checks that a grant exists for that agent and that host
|
||||
> before the credential is used. The agent never sees the key or any other secret value (REQ-CRED-1,
|
||||
> REQ-VAR-2). An agent without a grant for that host is refused. Not every agent gets a grant. Who
|
||||
> grants a host credential, and how, is set by the #424 design, which Jason approves.
|
||||
|
||||
REQ-WAIT-3, appended:
|
||||
|
||||
> `role.hire` and `role.fire` were added as hard waits and route under REQ-COS-3 since version 1.7.
|
||||
> `git.push.remote` was added as a hard wait and routes under REQ-LAND-2 since version 1.7.
|
||||
|
||||
## Notes for the reviewer and for Mos
|
||||
|
||||
- REQ-COS-1 (1.2) already says the chief "hires and terminates role instances". REQ-COS-3 gives that
|
||||
sentence its actions and bounds. It doesn't widen REQ-COS-1.
|
||||
- The forbid now names what r3 called "replace": ending a kept instance's run with a reason and changing
|
||||
its model (webby-0010 N2). A user-resolved decision still ends a kept run (row 194 D2.4).
|
||||
- The fixed list is `HIRE_ALLOWED` in row 194 D2.3, an allowlist since r4 (webby-0010 B1). r3's text
|
||||
named the refused authorities instead, which let merge-gate and any later action through.
|
||||
- The kill switch is row 191's. If row 191 hasn't landed when this version is approved, the bound still
|
||||
holds through row 194's chain on its candidate.
|
||||
- REQ-LAND-2 follows row 199's design r6 (4d3007ec9 on megavolt/row199, approved by Duckworth in
|
||||
duckworth-0027: D9 to D11, D9a, D9c, Q-M1 and Q-M2, and the oid binding from mos-1034 item 5) and
|
||||
Megavolt's wording in megavolt-0005, megavolt-0008 and megavolt-0014, through Sage (sage-1377,
|
||||
sage-1380, sage-1387). The fork bullet answers duckworth B5 and B6 and webby-0011 N1. Commit-email
|
||||
checks, the refusal codes and the URL parse rules (D9c) are host checks with tests in row 199, not
|
||||
bounds, so they stay out of the text (megavolt-0008).
|
||||
- Once the routing lands and the decision is optional for the within-role class, the commit is the task's
|
||||
latest recorded push at check time. It is still checked against the work ref tip and pushed by oid, so
|
||||
the binding holds without a decision (megavolt-0008). Step 4 of "When this version lands" puts that in
|
||||
the routing commit (duckworth-0022 N9, sage-1387).
|
||||
- REQ-LAND-2 and REQ-COS-3 meet at the coder. Coder declares Gitea credentials, so REQ-COS-3's
|
||||
credentials bound keeps the chief from hiring one until a later version lifts it. `git.push.remote` isn't
|
||||
on REQ-COS-3's fixed list either, so a coder holding it stays unhireable until a PRD version adds it
|
||||
(row 194 D2.3).
|
||||
- Adding to the fixed action list is a PRD change (mos-1035 item 3, sage-1395). The role file's
|
||||
`hireable` list stays a reviewed commit in `roles/`.
|
||||
- The credentials bound names no row number, so the text doesn't go stale when row 199 lands. A later
|
||||
version lifts it.
|
||||
- REQ-CRED-3 states only what Jason said in Q183 and Q184 (sage-1422). It names the outcome and the
|
||||
check, not how the stack reaches the host. Proxying the ssh and a signing socket are both designs, and
|
||||
the #424 design picks one (duckworth-0038 B1).
|
||||
- Open point: Q183 doesn't say who grants host access or how, so the text leaves it open and defines no
|
||||
mechanism. The #424 design settles it, Jason approves that design, and Duckworth reviews it because it
|
||||
gives agents sudo on hosts (Q184, Mos's reading, duckworth-0038 B2). The build is row 210 (#1707,
|
||||
owner Gizmoduck), a new row on top of row 199 (megavolt-0019, sage-1430).
|
||||
- This draft follows the PRD's own style: plain words, no semicolons in new sentences, and the status
|
||||
paragraph never edited in place except to append.
|
||||
@@ -0,0 +1,342 @@
|
||||
# PRD 1.8 draft: trusted actions within role guardrails, the manual mode, and grants the chief of staff
|
||||
# requests and the user approves
|
||||
|
||||
Draft by Honker for Vikunja #434, under Mos's assignment mos-1117 (through Sage, sage-1525). Not approved.
|
||||
It changes nothing until Jason approves it as a `prd.approve` decision. Sage opens that decision and sends
|
||||
Mos its id. Base: `docs/prd/mosaic-stack.md` version 1.6 at 008746b6e, plus the version 1.7 text Jason
|
||||
approved (PRD-1.7-DRAFT.md r2, 9ba31b5b3, decision d52febb5, mos-1089). Where 1.7 is not yet written into
|
||||
the PRD file, this draft's anchors name 1.7's sections.
|
||||
|
||||
Revision 2 makes Mos's edits a to f (mos-1124, through Sage, sage-1534) and fixes Duckworth's B1 and B2
|
||||
with notes N1 to N6 (duckworth-0057) on r1 at 3c30f1036. Revision 3 fixes Duckworth's C1 and takes
|
||||
notes N1 and N2 (duckworth-0058) on r2 at 60f5ca8fa, with Gizmoduck's enrollment wording
|
||||
(gizmoduck-0067). Revision 4 makes Mos's rulings on duckworth-0057 (mos-1129, through Sage, sage-1538)
|
||||
on r3 at d16ccdb91: only an Approve creates a grant, one decision for a grant that makes an account, and
|
||||
the arbiter route for `host.sudo`. Revision 5 fixes two stale notes from Duckworth's r4 check
|
||||
(duckworth-0061 N1), with no change to the requirement text. Revision 7 is on r5 at 762a6711f, not r6
|
||||
(Jason, Q191, through Mos mos-1149 and Sage sage-1623): the assistant and the chief of staff are separate
|
||||
roles, and only the chief raises a grant request. It also takes Mos's rulings on r6 items 1, 3 and 4
|
||||
(mos-1152): the heading below reads "For Jason at approval:" again with Mos's reading of the Q188 revokers,
|
||||
the machine-grant citation names r1 to r4, and the revoke note no longer says their Q-S5 is answered.
|
||||
|
||||
Only the chief of staff raises a grant request. The assistant, the user's main agent, never raises one.
|
||||
It passes the user's request to the chief and can revoke a grant in its own organization.
|
||||
|
||||
For Jason at approval:
|
||||
|
||||
- Q188's 'you, Mos or the tester' is read as the user, the assistant for grants in its own organization, and the
|
||||
tester only on a dev instance (Mos's reading, mos-1124 d).
|
||||
- Only an Approve creates a grant. A grant written into the business file by hand makes nothing. An agent
|
||||
that runs as the same OS user as the stack can still write the stack's own store. That is the B7
|
||||
residual (Q182 on dragon-lin, #423 on .53 and .54), and this version doesn't close it.
|
||||
- Who decides a root command on another host changes. Once `host.sudo` is routed, a raise for it that
|
||||
cites the spec and isn't within-role goes to the arbiter for its domain, as version 1.7 set for
|
||||
`git.push.remote`, not to you. The grant and its allowlist still limit the command. In manual mode the
|
||||
decision comes to you (REQ-TRUST-2).
|
||||
|
||||
## What it changes
|
||||
|
||||
Today an agent acts without a raise only on actions its role file grants within-role, and the role
|
||||
files grant few. Every other use of a tool or command waits on a decision. Row 210 builds `host.sudo` as a
|
||||
hard wait, so Jason would decide each command run on another machine. Grants themselves are hand work:
|
||||
Jason makes every token and key from a runbook (REQ-CRED-1).
|
||||
|
||||
Jason's ruling Q188 turns this around. Roles set what each agent may do, and by default agents do it at
|
||||
will within their sandbox and guardrails. A per-use raise happens only in a manual mode. No agent creates
|
||||
a grant. The chief of staff brings a grant request to the user's inbox, the user clicks Approve or Deny, and
|
||||
protected stack code does every step of making the grant. The user's own tokens are entered once in the
|
||||
WebUI and never reach an agent.
|
||||
|
||||
This version adds:
|
||||
|
||||
1. Trusted actions within role guardrails as the default mode (REQ-TRUST-1).
|
||||
2. Manual mode, a list of actions that each need a decision per use: who sets it, where it lives and what
|
||||
it changes (REQ-TRUST-2).
|
||||
3. Grant requests, raised only by the chief of staff through the chain of command (REQ-GRANT-1).
|
||||
4. Approve and the protected grant code, with three grant methods: machine key, Gitea and GitHub
|
||||
(REQ-GRANT-2).
|
||||
5. The user's tokens, entered in the WebUI and used only by the grant code (REQ-GRANT-3).
|
||||
6. Row 210's REQ-CRED-4, generalized from `host.sudo` to every role-listed action that uses a grant
|
||||
(REQ-CRED-4).
|
||||
7. Instant revoke (REQ-GRANT-4).
|
||||
|
||||
The hard waits stay with the user (Mos, mos-1117): spending money, deleting data, new external accounts,
|
||||
messages outside the fleet and the rest of REQ-WAIT-1. Approving a grant is the user resolving one of
|
||||
those hard waits, `credential.mint`. A grant that makes an account, such as a seat's own Gitea user
|
||||
(Q190), is the same one decision: its method says it makes an account, and the inbox shows the account
|
||||
name before Approve. So no hard wait moves to an agent.
|
||||
|
||||
When this version lands, reviewed commits:
|
||||
|
||||
1. add the manual-mode list to the organization file schema, written only on the human's path
|
||||
(REQ-TRUST-2), and make the broker classify a listed action as needing a decision for each use, before
|
||||
any other routing.
|
||||
2. add the grant request: a decision the chief of staff raises with action `credential.mint`, naming the
|
||||
agent, the method and target, the exact rights and, when the method makes an account, the account
|
||||
name (REQ-GRANT-1). The broker refuses a `credential.mint` raise from any other role. An
|
||||
`account.create` raise isn't a grant request and routes as it does in version 1.7, such as a PM's
|
||||
sign-up for a service.
|
||||
3. add the grant code that runs on a resolved Approve, one method at a time, and the grant record it
|
||||
writes (REQ-GRANT-2, Vikunja #435 and #436).
|
||||
4. add the user's token store, written from the WebUI (or the CLI until the Console knows the signed-in
|
||||
human) and read only by the grant code (REQ-GRANT-3).
|
||||
5. move `host.sudo` into both `ROUTED` lists, as row 199 does for `git.push.remote`, and route it under
|
||||
REQ-CRED-4, with the arbiter route version 1.7 set for `git.push.remote`. Which definitions list it
|
||||
within-role is a separate reviewed role commit in `roles/`.
|
||||
6. update the role contracts that list the hard waits: `roles/chief-of-staff.md:75` raises grant
|
||||
requests with the REQ-GRANT-1 fields, and `roles/pm.md:55` asks the chief of staff for a grant instead
|
||||
of raising `credential.mint` itself (line numbers on origin/refactor). Other roles that list the hard
|
||||
waits get the same line as the PM. A role's messaging list carries the chain of command.
|
||||
|
||||
## Text for the PRD
|
||||
|
||||
Status line, appended to the current paragraph:
|
||||
|
||||
> Version 1.8 records Jason's ruling Q188 of 2026-10-20: trusted actions within role guardrails by
|
||||
> default, a manual mode for per-use raises, and grants the chief of staff requests and the user
|
||||
> approves, approved by Jason in decision <prd.approve decision id>.
|
||||
|
||||
Source, two new bullets above "Version 1.7 source":
|
||||
|
||||
> - Version 1.8 source, Jason's ruling Q188 (DECISIONS, 2026-10-20), his answers to Q8 and Q10, in part.
|
||||
> Q8: "I would rather grant the grant right to an agent that runs with strict guardrails. That agent
|
||||
> presents the final authorization for the user. Upon approval, the grant is created programmatically.
|
||||
> I see this as: User configures their PATs, API tokens, authorizations within the WebUI. The agent
|
||||
> presents the request for grant with X rights for another agent. The user reviews, then clicks
|
||||
> Approve/Deny. Click of Approve fires off protected mechanical methods to handle the grants on Github,
|
||||
> Gitea, .... services as needed. The tooling would need to exist and must be guarded." And "Disallowing
|
||||
> the agents to create grants autonomously is a necessary evil for better security. Requiring the user
|
||||
> to manually perform the steps is a no-fly." Q10: "Agents should have tools and command access based on
|
||||
> roles. Those roles dictate the allowed tools and commands. The use of a tool or command should not be
|
||||
> raised to the user unless the system is configured with a manual mode that requires user
|
||||
> interaction. By default I want to have the agents capable of running commands and tools at will
|
||||
> within their sandbox/guardrails. This allows autonomous actions by agents within their lanes. Perhaps
|
||||
> a "Trusted actions within approved guardrails" methodology." And "The main agent or chief of staff
|
||||
> would be responsible for surfacing the request to the user's inbox for approval. Not all agents are
|
||||
> capable or authorized to request directly. It must follow the chain of command properly." He agreed
|
||||
> with Mos's Q10 points 3 to 6: protected stack code does every step on Approve, his tokens are entered
|
||||
> in the WebUI and agents never see them, a command on the approved list runs without a click and any
|
||||
> other is refused, and he, Mos or the tester can revoke a grant at once.
|
||||
> - Version 1.8 source, Jason's ruling Q190 (DECISIONS, 2026-10-20), his answer "Q12: O1": each agent seat
|
||||
> gets its own account on his Gitea, created through the Q188 approval path once the Gitea grants exist.
|
||||
|
||||
Under "Decisions", after REQ-DEC-6, a new section:
|
||||
|
||||
> ### Trusted actions
|
||||
>
|
||||
> Added in version 1.8 (Q188).
|
||||
>
|
||||
> - **REQ-TRUST-1.** Agents act within their role guardrails without asking. The default mode is
|
||||
> "trusted actions within approved guardrails". An action a role file grants within-role is done at
|
||||
> will, inside the agent's sandbox, its tool limit and its grants, and is logged. It raises nothing to
|
||||
> the user. An action the file doesn't grant within-role still routes by REQ-DEC-2, and a hard wait
|
||||
> still goes to the user (REQ-WAIT-1). Only a reviewed role change adds an action to a role file, and
|
||||
> REQ-WAIT-2 still keeps every hard wait out of every role file.
|
||||
> - **REQ-TRUST-2.** The user can put chosen actions in manual mode. The organization file holds the list
|
||||
> of actions in manual mode, empty by default. While an action is on the list, every use of it by any
|
||||
> role becomes a decision for that use, raised to the user, and the action waits until the user
|
||||
> resolves it. A decision denied, or not resolved, refuses the action. Each decision is used once. For a
|
||||
> command on another host, the decision names the host, the command and its input. Actions not on the
|
||||
> list are unchanged. Only the user changes the list, through the WebUI or the CLI. The chief of
|
||||
> staff's organization writes (REQ-ROLE-3) can't change it, and no agent can. Every change is logged
|
||||
> with who made it. A change applies to the next use, and a run already in progress keeps no
|
||||
> exemption.
|
||||
|
||||
Under "Credentials", after REQ-CRED-3 (version 1.7):
|
||||
|
||||
> Added in version 1.8 (Q188):
|
||||
>
|
||||
> - **REQ-CRED-4.** An action a role definition lists within-role, and that uses a grant, runs without a
|
||||
> decision for each use only when all of these hold:
|
||||
> - the definition declares the credential kind the action uses;
|
||||
> - a grant names that agent, and that host or service;
|
||||
> - for a command, the command matches the grant's allowlist. A grant lists exact argv prefixes. A
|
||||
> wildcard grant is refused;
|
||||
> - the agent's key or token for that grant is issued and not revoked.
|
||||
>
|
||||
> A command off the allowlist is refused, and adding one takes a new grant request (REQ-GRANT-1). An
|
||||
> action whose definition doesn't list it within-role still needs a decision for each use. Only a
|
||||
> reviewed role change adds an action to a definition. `host.sudo` routes under this requirement.
|
||||
> - **REQ-GRANT-1.** No agent creates or writes a grant. A grant request follows the chain of command,
|
||||
> and only the chief of staff raises it to the user's inbox. Any other role that needs a grant for
|
||||
> itself or for an agent it directs asks the chief of staff, through its chain: a worker role through its
|
||||
> PM, the PM and the CTO directly. The assistant passes the user's own request to the chief of staff
|
||||
> (REQ-ASSIST-1). The request names the agent, the grant method and its target, the exact rights, and a
|
||||
> rotate-by or expiry date. The chief raises one only for an agent whose reviewed role definition
|
||||
> allows that kind of credential, and the broker refuses any other. A grant can't name a wildcard, a
|
||||
> shell or an empty command. The request is a decision with the action `credential.mint`, so it goes
|
||||
> to the user as a hard wait (REQ-WAIT-1). A grant whose method makes an account, such as a seat's own
|
||||
> Gitea user (Q190), is the same one decision: the method in the grant says it makes an account, the
|
||||
> request names the account, and the inbox shows the account name before Approve. The user's one
|
||||
> resolution covers the account and its credential, so the hard-waits list doesn't change. An
|
||||
> `account.create` raise isn't a grant request and routes as version 1.7 has it. The broker checks
|
||||
> only that the chief of staff raised the request. The chain of command before that is in the
|
||||
> messaging lists and the role contracts, not a broker check. The assistant and the chief of staff are
|
||||
> separate roles. No agent holds both, and the broker refuses a claim that would.
|
||||
> - **REQ-GRANT-2.** The user approves or denies each grant request in the inbox. Approve is the only thing
|
||||
> that creates a grant, and the code uses a grant only when it has a matching Approve record. No agent
|
||||
> creates a grant. A grant written into the business file by hand makes nothing. Row 210's hand-written
|
||||
> grants stay a test path until Vikunja #436a moves `host.run` to grants made by Approve. On Approve,
|
||||
> protected stack code does every step, not an agent and not the user: it records the grant, makes the
|
||||
> key or token, installs it on the machine or creates it on the service, and logs each step. On Deny
|
||||
> nothing is made, and the denial is logged. The grant methods are:
|
||||
> - machine key: a key for one agent on one enrolled host, with the command allowlist of REQ-CRED-4. A
|
||||
> host's identity comes from the host side when the user approves its enrollment, never from a first
|
||||
> connection. A host user with sudo receives a grant only after the user approved that user by name;
|
||||
> - Gitea: a token for one agent, and for an agent seat its own Gitea user (Q190), with an expiry. The
|
||||
> all scope and the admin scope are never granted;
|
||||
> - GitHub: a grant for one agent.
|
||||
>
|
||||
> The Gitea and GitHub rights units come in a later design that Jason approves.
|
||||
>
|
||||
> The approval binds the exact rights the user saw and the target or its enrolled identity. A change to
|
||||
> the agent, its definition, the target, the target's identity or the rights after the raise voids it. An
|
||||
> approval is used once. A grant the code couldn't finish is undone at once on the stack's side, so it
|
||||
> is never used. What it left on the host or service stays visible as pending until it is removed, as
|
||||
> for a revoke (REQ-GRANT-4). A new grant needs a new approval, never an automatic retry. The grant
|
||||
> code runs only on a request the user resolved. A request resolved by an arbiter, the tester or any
|
||||
> agent makes nothing. A `credential.mint` decision raised before version 1.8, or without the fields of
|
||||
> REQ-GRANT-1, makes nothing when it is approved. Until the Console
|
||||
> knows the signed-in human (Vikunja #412), Approve is `mosaic decide` in the CLI (REQ-DEC-3).
|
||||
> - **REQ-GRANT-3.** The user enters their own personal access tokens and API tokens in the WebUI, or the
|
||||
> CLI. Only the grant code reads them, to make grants. No agent ever sees a token value or a key's
|
||||
> private half. Values appear in no event, log, message, prompt or agent file, and no agent can read
|
||||
> them back (REQ-VAR-2). Until the Console knows the signed-in human, they are entered through the
|
||||
> CLI.
|
||||
> - **REQ-GRANT-4.** A grant can be revoked at once by the user, by the assistant for a grant in its own
|
||||
> organization, or by the tester on a dev instance (Q173). A revoke needs no decision and no new trust.
|
||||
> It takes effect at once on the stack's side, without waiting on the network, so the next use is
|
||||
> refused. Removing the key or token at the host or service follows. Until that removal is done it stays
|
||||
> visible as pending. A revoke while the grant is being made stops it, and nothing it made stays usable.
|
||||
> Every revoke is logged with who made it.
|
||||
|
||||
REQ-CRED-1, appended:
|
||||
|
||||
> Since version 1.8, a token or key for a grant is made by the grant code on the user's Approve
|
||||
> (REQ-GRANT-2), not by hand. Tokens that exist stay as they are.
|
||||
|
||||
REQ-WAIT-3, appended:
|
||||
|
||||
> `host.sudo` was added as a hard wait and routes under REQ-CRED-4 since version 1.8.
|
||||
|
||||
Under "Risks and open questions", a new bullet:
|
||||
|
||||
> - Trusted mode lets an agent run its allowed commands at will. On another host, an allowed command that
|
||||
> writes a file as root or starts a shell can plant a key, and the seat sandbox doesn't confine the
|
||||
> network, so root can outlive the grant and a revoke. The allowlist narrows this. A seat sandbox with
|
||||
> its own network namespace closes it. Until then the host's own audit log is the check after the fact
|
||||
> (row 210 design section 6).
|
||||
|
||||
## Notes for the reviewer and for Mos
|
||||
|
||||
- REQ-TRUST-1 restates what REQ-DEC-2 already does for within-role actions ("routine and within-role
|
||||
decisions are logged") and names it as the default mode. It grants nothing new by itself. What widens
|
||||
is the role files, one reviewed commit at a time.
|
||||
- The hard waits stay with the user (mos-1117). REQ-GRANT-1 makes the grant request a `credential.mint`
|
||||
decision, already a hard wait under REQ-WAIT-1, so Approve is the user resolving it. REQ-WAIT-2 stands:
|
||||
no role file grants `credential.mint`. The grant code acts on the user's resolution, not on a role
|
||||
grant. A grant that makes an account, such as a seat's Gitea user under Q190, is the same one
|
||||
`credential.mint` decision. So 1.8 doesn't change the hard-waits list (mos-1124 a, mos-1129 N4).
|
||||
- Manual mode: Sage asked who sets it, where it lives and what it changes. Mos accepted these choices
|
||||
(mos-1124 b), and Jason can change them: a list of actions in the organization file, empty by
|
||||
default, changed only by the user, outside the chief's organization writes. A list rather than one
|
||||
switch follows Gizmoduck's #436 draft section 7 (gizmoduck/machine-grant-design f6b6a04b6), so Jason
|
||||
can make host commands manual without making every action manual. Listing every within-role action
|
||||
gives the whole-organization manual mode. Row 210's per-call `host.run` decision, bound to host, argv
|
||||
and stdin and spent once, is this mode's decision for a host command (Q188, Mos's reading).
|
||||
- Gizmoduck's draft proposes REQ-CRED-5 (section 8) for the machine grant. Its content is in REQ-GRANT-1
|
||||
to REQ-GRANT-4 here, generalized to all three methods, so 1.8 has no REQ-CRED-5. Their bounds in
|
||||
gizmoduck-0062 items 4a to 4g are in the text: the chief raises only for an allowed definition, the
|
||||
approval binds the rights shown, it is used once with no automatic retry, no value is ever seen, revoke
|
||||
is at once on the stack's side with the host or service removal visible until done, every grant has a
|
||||
rotate-by or expiry date, and the host identity comes from the host side. Duckworth has reviewed their
|
||||
design r1 to r4 (duckworth-0056 and duckworth-0060), so a later change there may need a change here.
|
||||
- B1 (duckworth-0057, mos-1129): a grant the code couldn't finish is undone at once on the stack's side.
|
||||
What it left on the host or service stays visible as pending until it is removed, as for a revoke.
|
||||
- B2 (duckworth-0057, mos-1129): Mos chose Duckworth's second option. Only an Approve creates a grant,
|
||||
and the code uses a grant only when it has a matching Approve record. A grant written by hand makes
|
||||
nothing. Jason's Q188 gives the reason: "Disallowing the agents to create grants autonomously is a
|
||||
necessary evil for better security" and "Upon approval, the grant is created programmatically." Row
|
||||
210's hand-written grants stay a test path until #436a moves `host.run` to grants made by Approve. r2
|
||||
and r3 had a hand path in the business file, and r4 takes it out, with r3's load check on hand grants
|
||||
(duckworth-0058 N1) and the note that a revoke never rewrites the user's file (duckworth-0058 N2).
|
||||
The same-uid write to the stack's own store stays the B7 residual, and the top of this draft names it
|
||||
for Jason.
|
||||
- C1 (duckworth-0058, sage-1538): `account.create` is the hard wait for any new external account, not
|
||||
only a grant (REQ-WAIT-1, `vocabulary.mjs:43-46`, `roles/pm.md:55`). It stays as version 1.7 has it.
|
||||
Only `credential.mint` is chief-only, and a grant that makes an account is a `credential.mint`
|
||||
decision (N4), so a PM's sign-up for a service routes as before.
|
||||
- Gizmoduck's machine-grant r2 (eb4c756a1, gizmoduck-0067): enrollment is a request the user approves
|
||||
and the stack runs (mos-1120), and a host user with sudo is enrolled by name, the user's Approve
|
||||
covering that user (mos-1125). REQ-GRANT-2's machine key says both. Their design r3 and r4 (1c5705d6d
|
||||
section 6) give revoke to the user, the assistant in its own organization and the tester on a dev
|
||||
instance, as REQ-GRANT-4 does (mos-1124 d).
|
||||
- N1 (duckworth-0057, mos-1129): the approval binds the target or its enrolled identity.
|
||||
- N2 (duckworth-0057, mos-1129): a revoke during creation stops it, and nothing it made stays usable.
|
||||
- N3 (duckworth-0057, mos-1129): landing step 6 lists the role contract updates (`roles/pm.md:55`,
|
||||
`roles/chief-of-staff.md:75`). A `credential.mint` decision raised before 1.8, or one without the
|
||||
grant fields, makes nothing when it is approved.
|
||||
- N4 (duckworth-0057, mos-1129): a decision carries one action (`a.action`, broker.mjs:1678 on
|
||||
origin/refactor). A grant that makes an account is one `credential.mint` decision: the method in the
|
||||
grant says it makes an account, and the inbox shows the account name before Approve. The user clicks
|
||||
once, and there is no separate `account.create` action for it. Gizmoduck's machine-grant r2 reads it
|
||||
the same way.
|
||||
- N5 (duckworth-0057, mos-1129): `host.sudo` keeps the arbiter route version 1.7 set for
|
||||
`git.push.remote`. A raise for it that cites the spec and isn't within-role goes to the arbiter for
|
||||
its domain (broker.mjs:1677-1678 on origin/refactor), and the grant and its allowlist still limit the
|
||||
command. In manual mode the decision goes to the user. The top of this draft names this for Jason as a
|
||||
change in who decides a root command on another host.
|
||||
- N6 (duckworth-0057, mos-1129): the broker enforces only that the chief of staff raised a grant request.
|
||||
The rest of the chain of command is the messaging lists and the role contracts (step 6 of "When this
|
||||
version lands"). REQ-GRANT-1 says so.
|
||||
- Approving a grant doesn't by itself let a command run without a click. That needs REQ-CRED-4 and
|
||||
`host.sudo` in the routed lists, step 5 of "When this version lands" (gizmoduck-0062 item 4i).
|
||||
- Which roles may request: Q188 says "Not all agents are capable or authorized to request directly. It
|
||||
must follow the chain of command properly." The text makes the chief of staff the only role that raises,
|
||||
the PM and the CTO ask it directly, and worker roles ask through their PM. Jason's words also name "the
|
||||
main agent". That is the assistant, a separate role from the chief of staff. Reading the two as one role
|
||||
would give grant rights to both and overlap their authority (Jason, Q191). So the assistant never raises
|
||||
a grant request: it passes the user's request to the chief (REQ-ASSIST-1) and can revoke a grant in its
|
||||
own organization (REQ-GRANT-4). Jason accepted this at Q191 ("Q13: that looks better").
|
||||
- What stops one holder claiming both roles today, at refactor 011d41ba6. Nothing names the pair. The
|
||||
separation holds by construction, and no check or test asserts it:
|
||||
- An instance has exactly one definition (`packages/business/src/business.mjs:118-120` in checkRoles).
|
||||
An assistant is an instance of the definition `assistant` (`packages/business/src/vocabulary.mjs:134`)
|
||||
and the chief of staff is an instance of `chief-of-staff` (`packages/bus/src/broker.mjs:164`, `:2137`).
|
||||
- A run binds one instance. `bindLaunch` takes one `role` (`broker.mjs:451-456`), refuses a second bind of
|
||||
the same run in the business (`duplicate-run`, `:479`), and refuses a rebind under another role
|
||||
(`launch-record-mismatch`, `:499`). The launcher spawns one process per launch and binds it to one
|
||||
instance (`packages/cli/src/launcher.mjs:438-449`).
|
||||
- `role.claim` claims only the run's own bound role (`broker.mjs:1689-1734`). An assistant run claims its
|
||||
thread in `thread_claims` and never writes `role_claims` (`:1693-1709`). The `role_one_holder` trigger
|
||||
(`packages/bus/src/migrations/3b.sql:117-119`) allows one claim per role, and nothing limits the roles
|
||||
one run holds, because a run has only one.
|
||||
Not enforced: the run key is per business (`broker.mjs:478`), so nothing stops one run id being bound
|
||||
in two businesses. Nothing ties two runs to two different processes or OS users beyond the launcher's
|
||||
own record (the B7 residual). `credential.mint` is an action (`broker.mjs:34`) and a hard wait
|
||||
(`:71`), but no check limits which role raises it. The landing step adds a broker refusal for a claim
|
||||
that would put both roles on one holder, with a test for each path above.
|
||||
- REQ-CRED-4 keeps row 210's conditions (origin/gizmoduck/row210-on-199-r2,
|
||||
agents/gizmoduck/work/queue-210/DESIGN.md:376-392): the definition declares the kind, the grant names the
|
||||
agent, an exact argv-prefix allowlist with wildcards refused, the key issued, and only a reviewed role
|
||||
change adds the action. Row 210 put the grant in a hand-edited business file, which stays a test path until
|
||||
#436a. Under 1.8 the grant is what Approve records, so "the business file grants that agent that host"
|
||||
becomes "a grant names that agent and that host".
|
||||
- Revoke: Q188 says "you, Mos or the tester". Mos is Jason's assistant (REQ-ROLE-4), so the text says the
|
||||
assistant, limited to grants in its own organization, and the tester only on a dev instance (Q173).
|
||||
Mos accepted this with those limits (mos-1124 d).
|
||||
- Approve in the Console needs the Console to know the signed-in human (Q175 phase 2, #412). Until then
|
||||
Approve is `mosaic decide`, which REQ-DEC-3 already allows (mos-1124 e). This draft doesn't take
|
||||
REQ-WEB-2, which stays with the Console decide buttons amendment (Vikunja 19/266). That amendment, or a
|
||||
later version, moves Approve into the Console.
|
||||
- Roles already use a scoped Gitea token (`<read|write>:<category>`, all and admin never granted,
|
||||
`packages/business/src/vocabulary.mjs:105-106` on origin/refactor). It is a likely Gitea unit, not a settled
|
||||
one. Only the machine key is designed so far. Gitea and GitHub are a later #436 draft, and minting a Gitea
|
||||
token needs an admin credential the user enters, which is a design of its own (Gizmoduck's draft section
|
||||
11). GitHub's rights unit isn't settled. So the text leaves both units to a later design Jason approves
|
||||
(mos-1124 f), and keeps only the bounds: an expiry, and never the all or admin scope. The PRD names outcomes
|
||||
and checks, not the grant code's steps, as REQ-CRED-3 does.
|
||||
- Jason expects the inbox Approve and the Gitea and GitHub grants by 10-27 (Q188 point 6). That is a
|
||||
delivery date, so it is in the source and not in the requirement text.
|
||||
- This draft follows the PRD's own style: plain words, no semicolons in new sentences, and the status
|
||||
paragraph never edited in place except to append.
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,8 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Quackerjack
|
||||
|
||||
Builder seat, Claude Haiku 5.5 at high effort, started by Sage under mos-1023 item 1 and Q177 for federation
|
||||
rows R5 (`mosaic access list, grant, revoke`) and R2 (the `federation` config key), per Gizmoduck's design
|
||||
section 7 (#414).
|
||||
|
||||
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
|
||||
`quackerjack/*` branches through the repository credential helper (mos-0981), never `refactor`, `main` or
|
||||
another seat's branch.
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/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
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,36 @@
|
||||
# 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%.
|
||||
@@ -0,0 +1,86 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,5 @@
|
||||
M1 KILLED fail=5
|
||||
M2 KILLED fail=2
|
||||
M3 KILLED fail=7
|
||||
M4 KILLED fail=2
|
||||
M5 KILLED fail=2
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,48 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,124 @@
|
||||
# Re-pin 490e2771d to 4429a104f (row 192, Q175 phase 1), with npm ci in the new copy
|
||||
|
||||
Sage, 2026-10-20, draft 1, for mos-1099 item 3 under DECISIONS Q173. Mos checks this file at its commit before
|
||||
P0. Any change to this file or to NEW after the check means Mos checks again (mos-0849/0850).
|
||||
|
||||
This is a smaller window than `repin-rows-177-185.md` (draft 2, which Mos checked in mos-0929). Where a step is
|
||||
the same, this file names that file's step and says what differs. The 7332 matrix, the snapshot, the W9 list and
|
||||
the rollback compare are reused unchanged.
|
||||
|
||||
## Who, and what nobody does
|
||||
|
||||
- Sage operates, as the tester. Darkwing holds the gate lane for row 190 round 3 tonight, so I take the
|
||||
operator seat. Mos verifies each step's output before the next one and runs `rehearse` (P3), which reads the
|
||||
live config and a live backup (`docs/TOOLS.md` `rehearse`: Jason or Mos runs it).
|
||||
- Jason has no step. Q173 covers a re-pin of this test system with a backup first and a STATE report after.
|
||||
Nothing is pruned.
|
||||
- Nobody acts as Jason: no `--by jason`, no human shell or tmux pane, every bus read and write carries
|
||||
`--tester`, and no request carries `jason.woltje`. `bus.tester` stays as it is; if it isn't `true`, stop at P1.
|
||||
- No pi (mos-0865 D2, P19 #383). No Save in the human Console, no `mosaic name`.
|
||||
- Outside reach doesn't change. The Console keeps its current flags. `--sso` stays off: row 192's sign-in code is
|
||||
dormant without the flag, and `cli.mjs:57` refuses `--sso` with the preview flags the pointer carries anyway.
|
||||
The proof pair stays unset. The tunnel still forwards 7332 only (P4 and W10 check it).
|
||||
|
||||
## Names
|
||||
|
||||
- OLD, live: `490e2771d3d057ab76991e2c7859cd251857e64c`, activation `20261020T192956Z-490e2771d3d0`, pointer
|
||||
`pm: true`, schema 3i, Console `--port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username
|
||||
--allow-user jason.woltje --tester-port 7333` (no `--send`, read from `stack-release.sh status` between 21:36Z and 21:39Z).
|
||||
- NEW: `4429a104f6d65ccf165f6c83b5d1dce3147dfde0`, row 192's landing, 106 commits past OLD counting records.
|
||||
The commits after it (5b8def2f1 plans, 176a6bf4b sessions, 305857c8f queue) are records and don't move NEW. If
|
||||
another code row lands before W0 (row 190 is the likely one), NEW stays here unless Mos rules otherwise.
|
||||
- REPO: the canonical checkout. `activate` runs REPO's `webui-service.sh`, so the four service scripts are held
|
||||
at NEW for the window (P5).
|
||||
- `P=/mnt/storage/scratch/sage-pin-de352dfaf` (`business-check.mjs`, `console-check.sh`, checked against
|
||||
`agents/sage/work/window-plan/repin-de352dfaf/SCRIPTS.sha256`).
|
||||
- `WIN=agents/sage/work/window-repin-4429a104f/`, where every transcript goes.
|
||||
- `SNAP=$WIN/snapshot.mjs`, a copy of `agents/darkwing/work/window-rows-177-185/snapshot.mjs`, sha256
|
||||
`639fd28a2674aaa03ce824e3394b4052f21cdef7d6fa49bae61a9927ed341e67` (mos-0899). I check the sha before S1, S2
|
||||
and any D3.
|
||||
|
||||
## What NEW carries over OLD
|
||||
|
||||
The path-limited log over `packages`, `scripts`, `roles`, `contracts`, `adapters`, `skills` and the package files:
|
||||
|
||||
| Row | Commit | Live effect |
|
||||
|---|---|---|
|
||||
| 186 | 269134d8d | Tool only: the activate plan's backup line says what the run will do. |
|
||||
| 187 | 9a71e4e80 | Tool: activate sets, carries and drops the proof pair. The pointer has no pair, so nothing changes. |
|
||||
| 188 | 8fc0ee216 | Tool: proof file checks on a carried pair. No pair, no effect. |
|
||||
| 163 | 8ecc7a239 | Tool: `rehearse`, which P3 uses, and `bus start --no-launch`. CLI `cli.mjs` and `launcher.mjs` gain the `--no-launch` path; the default start is unchanged. |
|
||||
| 189 | ace27dc5e | Tool: `activate --no-send`. Live already runs without `--send`; the pointer carries that. |
|
||||
| 192 | 4429a104f | Console `--sso` (off here), `scripts/mosaic-config.mjs` load becomes async, `webui-service.sh` renders `--sso` only when given, and a new dependency, oauth4webapi 3.8.8. `cli.mjs` imports `sso.mjs` at the top, so the Console won't start from a copy without that module. |
|
||||
|
||||
No schema step: `SCHEMA_VERSION` (`packages/bus/src/store.mjs:17`) is 3i at OLD and NEW, and no migration was
|
||||
added. `--plan` must say `migration: none`. No roles, skills, contracts, adapters or plug-in kinds changed. So: no
|
||||
harness set, no skill carry, no `pm-on`. One `activate --pm`.
|
||||
|
||||
## npm ci in the new copy
|
||||
|
||||
mos-1099 asks for npm ci in the new copy. `stack-copy.sh copy` already does it: it extracts the archive to a temp
|
||||
dir, refuses without `package.json` and `package-lock.json` (:55), runs `npm ci --no-audit --no-fund` there and
|
||||
writes no copy if it fails (:56). W0 checks the result: `node_modules/oauth4webapi/package.json` in the copy reads
|
||||
3.8.8, and `node --input-type=module -e "await import('oauth4webapi')"` from the copy exits 0. The live copy is
|
||||
163M; `/home` has 30G free (94% used), enough for one more.
|
||||
|
||||
## Backups
|
||||
|
||||
| Before | Backup | Taken by |
|
||||
|---|---|---|
|
||||
| W5 | the stopped store, `$DATA/backups/stack/<W5 activation id>/` with SHA256SUMS, DATA_ROOT, SCHEMA 3i, STAMP | `stack-release.sh` (row 185: every activate) |
|
||||
| W5 | the rollback set (units, wrapper, pointer) | `stack-release.sh` |
|
||||
| W9 | the business file, `cp -p` to `~/.config/mosaic-dev/businesses/mosaic-stack.json.pre-window-<stamp>` with its sha256, as evidence nothing wrote it | Sage, in W0b |
|
||||
|
||||
## Before the window (nothing stopped)
|
||||
|
||||
| # | Who | Step |
|
||||
|---|-----|------|
|
||||
| P0 | Mos | The `$P` sha check, as in the 177-185 plan P0. |
|
||||
| P1 | Sage | The business file, read only, the same values as the 177-185 plan P1. I report its sha256 now rather than assume the 06:15Z value. `business-check.mjs` for OLD now and for NEW after W0: LOAD_OK both. |
|
||||
| W0 | Sage | `cd $REPO && scripts/stack-copy.sh copy $NEW`, in a gap between gate-lane runs, since npm ci is heavy. Then the two npm checks above, and `diff <(git -C $REPO show $NEW:package-lock.json) $COPIES/$NEW/package-lock.json` empty. W0 writes only under `copies/` and changes nothing live, so it moves before P2 and P3, which need the copy. |
|
||||
| P2 | Sage | `console-check.sh $NEW` against a stub Console of NEW on a scratch port, once without and once with `--tester-port` on a second scratch port. CONSOLE_PASS both. |
|
||||
| P3 | Mos | `scripts/stack-release.sh rehearse $NEW mosaic-stack --backup 20261020T192956Z-490e2771d3d0 --port <scratch>`. It must pass every gate check but the PM's, with the pointer's Console flags. It stops no unit and writes no pointer. Transcript in `$WIN/p3/`. |
|
||||
| P4 | Sage | The tunnel read, as in the 177-185 plan P4. |
|
||||
| P5 | Sage | `git -C $REPO diff --quiet $NEW HEAD -- scripts/stack-release.sh scripts/webui-service.sh scripts/bus-service.sh scripts/stack-copy.sh` and an empty `git status --porcelain` on the same four. Row 190 round 3 touches `webui-service.sh`; if it lands before W5, P5 fails and the window waits for Mos's ruling. |
|
||||
| P6 | Sage | The 7332 matrix (a)-(g) on OLD, as in the 177-185 plan, saved as `$WIN/matrix-old.txt`. |
|
||||
|
||||
## The window
|
||||
|
||||
| # | Who | Step |
|
||||
|---|-----|------|
|
||||
| W0b | Sage | The business file copy. |
|
||||
| W1 | Sage | Idle check (`mosaic agents --business mosaic-stack --tester`, `mosaic launches list`), the start stamp from `date -u`, then S1 `node $SNAP --store "$DATA/bus/bus.sqlite" --held`. |
|
||||
| W2 | Sage | `stack-release.sh status mosaic-stack`: pointer on OLD, `pm: true`, 3i, last line ok, unit flags match. |
|
||||
| W3 | Sage | `stack-release.sh activate $NEW mosaic-stack --pm --plan`. Must show from OLD to NEW, `migration: none`, the backup line, and the Console line with OLD's flags exactly: no `--send`, no `--sso`, no proof pair, `--tester-port 7333`. Any other difference stops here. |
|
||||
| W4 | Sage | The unit reads, as in the 177-185 plan W4. |
|
||||
| W5 | Sage, on Mos's go | `stack-release.sh activate $NEW mosaic-stack --pm`. Exit 0, `result ok`, every gate check including `pm_launched`. Record the activation id, the backup path and STAMP, and the backup SHA256SUMS sha256. Then `status`. |
|
||||
| W9 | Sage, after Mos verifies W5 | The 177-185 plan's W9 list unchanged, the chief turn and the one `assistant` thread turn included (mos-0929 Q3), plus two lines: the webui journal since the W5 stamp (with `UTC`) has one `Proxy: proxy proof off` and no `Sign-in:` line, and `ls -l /proc/<Console pid>/cwd` names the NEW copy. |
|
||||
| W10 | Sage | The browser check on 127.0.0.1:7333 and the matrix again as `$WIN/matrix-new.txt`, identical to `matrix-old.txt`. Then P4 again. |
|
||||
| W11 | Sage | STATE report in `$WIN/STATE.md`, posted on P19 386 and P19 412 and on Gitea #1690. |
|
||||
|
||||
## Stop conditions and rollback
|
||||
|
||||
The same as the 177-185 plan. Two failures in one window: stop and report (mos-0823).
|
||||
|
||||
Rollback: `stack-release.sh rollback mosaic-stack --plan`, then the real run. The previous distinct copy in the
|
||||
log is 490e2771d, both are 3i, so no `--restore` and no data loss. OLD takes the same Console flags. The R1 and
|
||||
R1post compares are as in the 177-185 plan. A restore past W5 follows that plan's D3 steps, with Mos deciding.
|
||||
|
||||
Keep every copy. Prune nothing.
|
||||
|
||||
## Questions for the plan check
|
||||
|
||||
- Q1: NEW is row 192 alone, not bundled with row 190. Row 190 (several instances) changes `stack-release.sh`
|
||||
and `webui-service.sh` and deserves its own window. I recommend running this one first.
|
||||
- Q2: Sage operates instead of Darkwing, who is on the row 190 gate. OK?
|
||||
- Q3: W0 before P2 and P3. It writes a copy only. OK?
|
||||
|
||||
## What Sage ran for this plan
|
||||
|
||||
Read only, 2026-10-20 between 21:36Z and 21:39Z (clock read 21:39:50Z at the commit): `stack-release.sh status mosaic-stack` (pointer, last five log
|
||||
lines, unit flags match, 106 commits of drift), `git rev-list --count`, `SCHEMA_VERSION` at both ends, the
|
||||
path-limited log, `git diff --stat` on the service scripts, CLI and config loader, `stack-copy.sh:23-56`,
|
||||
`webui/src/cli.mjs:15-120`, `docs/TOOLS.md` on `rehearse`, `du -sh` of the live copy and `df -h /home`. Nothing
|
||||
live was changed.
|
||||
@@ -0,0 +1,7 @@
|
||||
# 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.
|
||||
@@ -1,173 +0,0 @@
|
||||
# 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).
|
||||
@@ -1,74 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,74 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,85 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,79 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,61 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,21 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,34 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,29 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,61 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,40 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,103 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,31 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,52 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,37 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,31 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,118 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,79 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,31 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,84 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,17 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,34 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,33 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,31 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,55 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,79 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,59 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,41 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,78 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,55 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,27 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,66 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,40 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,32 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,27 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,76 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,71 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,34 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,56 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,72 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,29 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,25 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,71 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,47 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,67 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,55 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,111 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,40 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,23 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,76 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,39 +0,0 @@
|
||||
# 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."
|
||||
@@ -1,37 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,23 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,122 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,72 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,62 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,43 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,34 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,22 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,20 +0,0 @@
|
||||
# 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
|
||||
@@ -1,52 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,51 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,47 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,75 +0,0 @@
|
||||
# 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.
|
||||
+4
-64
@@ -1361,67 +1361,7 @@ 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: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-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-20T20:40:15Z | Filbert (Opus 5.5, 048f3c63) | row 190 (#1688) manifest round 1 review, candidate 8862feccc (static) | CHANGES: B1 base drift, row 189 ace27dc5e conflicts in stack-release.sh, rebase + full gate rerun; N1-N7 notes; Gitea 29634, queue rev 2362 (uncommitted, left for Sage)
|
||||
2026-10-20T21:29:20Z | Filbert (Opus 5.5, 048f3c63) | row 190 (#1688) manifest round 2 review, candidate 537a02392 (static) | CHANGES: B1 base drift, row 192 4429a104f conflicts in mosaic-config.mjs and webui-service.sh (async load); round 2 content fine; Gitea 29685, queue rev 2447 (uncommitted, left for Sage)
|
||||
|
||||
+9
-1
@@ -368,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] [--timeout S] [--plan]
|
||||
[--proof-header NAME --proof-file PATH | --no-proof] [--no-send] [--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
|
||||
@@ -480,6 +480,14 @@ 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,7 +1,8 @@
|
||||
# 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.
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -14,11 +15,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:658-687) carries the pointer's flags unchanged. Only
|
||||
- `console_for_target` (scripts/stack-release.sh:664-693) carries the pointer's flags unchanged. Only
|
||||
`--tester-port` and the row 187 proof pair can change.
|
||||
- `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
|
||||
- `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
|
||||
`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.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# 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 Dewey, gate
|
||||
"User maintains ability to restructure organization at will"). Gitea #1692. T1 item 5. Owner Honker, reviewer Webby (mos-0981), gate
|
||||
owner Sage. Base: refactor at the brief commit.
|
||||
|
||||
Design first. The first deliverable is `agents/honker/work/queue-194/DESIGN.md`. Dewey reviews it before any
|
||||
Design first. The first deliverable is `agents/honker/work/queue-194/DESIGN.md`. Webby 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
|
||||
|
||||
Dewey approves DESIGN.md, then the manifest. The bus, cli, business and webui suites and every
|
||||
Webby 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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,120 @@
|
||||
# Federation build rows R1, R2 and R5 (#414)
|
||||
|
||||
Sage, 2026-10-20, for Mos's orders mos-1023 item 1, mos-1028 item 2 and mos-1034 item 1. The design is
|
||||
Gizmoduck's `agents/gizmoduck/work/federation/DESIGN.md` at a1daa3c07 on `gizmoduck/federation-design` (r4.9,
|
||||
sha256 a31cbebd070f9e3a5ca4ccb158b1747c6f5a83c3469855705b0026a581fb5c6a). Duckworth approved r4.5
|
||||
(duckworth-0012), r4.6, r4.7 (duckworth-0020) and r4.8 (duckworth-0021, 8a22d0897 on `duckworth/reviews`),
|
||||
and acked r4.9 (duckworth-0024). r4.7 applies Jason's Q181 hosts, r4.8 adds N22's R8 checks and r4.9 names who
|
||||
runs them. None of them changes R1, R2 or R5. Section 7's table
|
||||
sets each row's scope and test gate. Vikunja P19 #414. Seats push only their own `<seat>/*` branches
|
||||
(mos-0981).
|
||||
|
||||
The other R rows are filed when their parents have candidates: R3 after R1 and row 196, R4 after R3, R5 and
|
||||
row 193's viewer level (193a, mos-1019), R6 after #412 phase 1, R1, R2 and R3, R7 after R1-R6 and row 190.
|
||||
R8 is a live window, not a code row. Its brief is the last section here.
|
||||
|
||||
## Brief: FED R1, the access.json loader and validator
|
||||
|
||||
Owner Gizmoduck, reviewer Duckworth, gate owner Sage. Base: refactor at this brief's commit. No parent.
|
||||
|
||||
`packages/business/src/access.mjs`: the access.json loader and validator from DESIGN.md section 3.5, and the
|
||||
pure `covers`. Providers, peers and businesses come in as arguments. One read plus the file checks. The
|
||||
exported interface is what R5 and row 193a build against, so the manifest names it, and any change to it in
|
||||
review goes to Sage before it lands. Section 7 names `covers` but not the loader and validator exports or
|
||||
the context shape. Morgana's 193a (508ee4d61 on `morgana/row193a`) assumes `checkAccess(doc, {providers,
|
||||
peers, businesses})`, with `businesses` mapping an id to `{human, instances}`, throwing a `BusinessError` that
|
||||
names the key path. Use those names, or tell Morgana and Quackerjack yours before the manifest goes to
|
||||
Duckworth. Row 193a later widens the level check to `viewer` on R1's candidate (mos-1019).
|
||||
|
||||
Tests, in `packages/business/tests/`: every rule in 3.5 refuses with the key path named. That covers a
|
||||
symlink, a non-regular file, another owner, group or other write, expiry, `*`, a grant disjoint from the given
|
||||
subjects, `human` and `tester` ids, any level other than `collaborator`, and a project entry that isn't a
|
||||
UUID. For `covers`: `*` and listed ids match, while an unlisted project, instance or business doesn't, an
|
||||
expired grant doesn't, and a level filter rejects another level. One mutant per refusal, each killed.
|
||||
|
||||
Boundaries: no change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or any live access.json. Tests
|
||||
use scratch roots. Single test files run freely, and suites need Sage's slot.
|
||||
|
||||
Gate: Duckworth approves the manifest. The business suite and every `scripts/test-*.sh` are green on Sage's
|
||||
gate, and the mutants are killed.
|
||||
|
||||
## Brief: FED R5, mosaic access list, grant and revoke
|
||||
|
||||
Owner Quackerjack, reviewer Duckworth, gate owner Sage. Chained on R1 (Q177). The build starts now against
|
||||
R1's exported interface as the R1 manifest names it, and R5 lands after R1 and rebases onto R1's landing.
|
||||
|
||||
`mosaic access list`, `grant` and `revoke`: human proof, atomic write, revision bump, a receipt line. The help
|
||||
text states the `instances` limit and the provider-removal order (DESIGN.md section 7, row R5). R4 later makes
|
||||
these verbs call `access.reload`, so R5 leaves no other reload path.
|
||||
|
||||
Context (sage-1419). `ssoSubjects` is row 192's `console.sso.providers[].subjects` flattened to `{provider,
|
||||
sub}` pairs; the design's "`console.sso.subjects`" in 3.5 is shorthand for it. `providers` is the list of
|
||||
`providers[].id`, read through `loadSystem` and row 192's `checkConsole`. `peers` is `[]` until R2 lands, so a grant
|
||||
naming a peer home refuses. R5 rebases onto row 192's landing as well as R1's.
|
||||
|
||||
Tests, in `packages/cli/tests/`: tester and agent callers are refused, a concurrent write refuses on a
|
||||
revision conflict, the receipt line is appended, and a bad grant is refused before anything is written. One
|
||||
mutant per refusal, each killed.
|
||||
|
||||
Boundaries: no change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, the live units or any live access.json. The
|
||||
human proof in tests uses the scratch stand-in pattern (a scratch bus verifies its own human-cli path).
|
||||
Single test files run freely, and suites need Sage's slot.
|
||||
|
||||
Gate: Duckworth approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate,
|
||||
and the mutants are killed.
|
||||
|
||||
## Brief: FED R2, the federation key in the system config
|
||||
|
||||
Owner Quackerjack, reviewer Webby, gate owner Sage. Chained on row 192 (#412 phase 1). The build starts when
|
||||
row 192 has a candidate, on that candidate.
|
||||
|
||||
A `federation` key in `scripts/mosaic-config.mjs` with the shape in DESIGN.md section 3. A config with no
|
||||
`federation` key still loads as it does today. This changes the config schema, so the manifest lists every
|
||||
reader of the key.
|
||||
|
||||
Tests, in `scripts/test-config.sh`: a valid key loads, an unknown sub-key and a bad origin refuse, and a
|
||||
config without the key loads. One mutant per refusal, each killed.
|
||||
|
||||
Boundaries: `~/.config/mosaic-dev/config.json` is user-authored and is not touched. Tests use scratch config
|
||||
dirs only.
|
||||
|
||||
Gate: Webby approves the manifest. The cli suite and every `scripts/test-*.sh` are green on Sage's gate, and
|
||||
the mutants are killed.
|
||||
|
||||
## Brief: FED R8, the hub window on 10.1.1.54
|
||||
|
||||
A live window, not a code row (DESIGN.md section 7, row R8). The brief follows r4.11, b93f098f0 on
|
||||
`gizmoduck/federation-design` (sha256 1bcee8df0a3a8628a6646535b756c936ca6d9e7bcfa7e1dd2bf1d3f01715c8e6),
|
||||
which Duckworth acked with r4.10 (duckworth-0034). Mos closed design rounds there (mos-1072). Run by Jason and
|
||||
Mos. Nothing here authorizes a step on any host; each step waits for the window's go.
|
||||
|
||||
What changed since r4.9 (DECISIONS Q182, Q183, mos-1055, mos-1063). R8 is a recorded acceptance. It starts
|
||||
with Jason's acceptance of the factory paths to .54 on record, quoted in section 8. The privilege and host
|
||||
checks still run, and each result goes into the R8 record with the name of whoever ran it. They report and
|
||||
don't refuse. The four functional checks named at the end of section 7's R8 row still stop R8 when they fail
|
||||
(mos-1066 item 1).
|
||||
|
||||
Order (mos-1042 item 2). R8 runs after the Case 2 install on .54 (#416, due by 2026-10-27, Jason's latest-by
|
||||
date in Q176, mos-1069), after Bean and Chet are live in Case 2, and after both Hermes gateways on 10.1.1.54
|
||||
have stopped. Case 2 go-live doesn't wait on R8. The risk, stated for 10-27: federation between the two
|
||||
instances depends on Melanie's switch-over date.
|
||||
|
||||
Entry checks, all before anything starts:
|
||||
- R7, #412 phase 2 and Jason's #419 answer, as section 7 lists.
|
||||
- The four hub user checks on 10.1.1.54 (N22, mos-1035 item 1): `sudo -l -U mosaic` grants nothing, `id
|
||||
mosaic` shows only its own group, `~mosaic/.ssh/authorized_keys` is absent or empty, and no file under
|
||||
`/etc/sudoers.d` names `mosaic`. An agent or Jason runs them as localadmin (mos-1055), and the output goes
|
||||
into the R8 record with who ran them.
|
||||
- The factory privilege checks, the key fingerprint comparison and the section 8 privilege path table, each
|
||||
as section 7's R8 row states them.
|
||||
|
||||
Build notes:
|
||||
- N1 (Duckworth, duckworth-0034, mos-1072). A check that fails on a path Jason didn't name in Q182 or Q183 is
|
||||
marked "not named in Q182 or Q183" in the record, not filed under the acceptance. Examples: a factory mount
|
||||
of hub data, another agent on .54, a provider admin token. Mos takes each one to Jason.
|
||||
- N2 (Duckworth, doc-only). The Q183 and Q4 quotes in section 8 are excerpts. When the R8 record quotes them,
|
||||
it marks each omission with "…" and checks the full text at DECISIONS.md:1535-1541 in jason-master-restart
|
||||
(read only).
|
||||
|
||||
Done when every entry check has its output recorded, Jason signs in to the hub and sends one message, Melanie
|
||||
revokes, and his next request is refused.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Brief: a pre-push hook that refuses force-pushes, and a busy check that fails on BUSY (#426)
|
||||
|
||||
Sage, 2026-10-20, for Mos's order mos-1078 (Vikunja P19 #426) and its go mos-1083 item 1. Owner Rocko,
|
||||
reviewer Dewey, gate owner Sage. Base: refactor at this brief's commit. Starts after row 191.
|
||||
|
||||
## Why
|
||||
|
||||
Morgana force-pushed a seat branch on 2026-10-20 (morgana-0016). Mos took it to Jason. Force-push is a
|
||||
Q173 exclusion, and canon says never force-push, but nothing mechanical stops one. Mos: "Done means a
|
||||
force-push from a seat worktree is refused, with a test that shows it."
|
||||
|
||||
Earlier the same day, a busy check chained as `| tail -1 &&` passed while the slot was BUSY. The script it
|
||||
called prints BUSY or CLEAR, but it exits 0 either way.
|
||||
|
||||
The guard has two layers:
|
||||
- (a) Gitea protection with force-push off on seat branch patterns and on refactor, next and main. ops-01
|
||||
sets it up, through Mos (mos-1082). That part isn't this row.
|
||||
- (b) The client hook below. It can be bypassed with `--no-verify`, so (a) is the real guard and (b) is the
|
||||
early refusal.
|
||||
|
||||
## What ships
|
||||
|
||||
1. `scripts/git-hooks/pre-push` (new). For each ref line git passes on stdin:
|
||||
- It refuses when the remote sha isn't all zeros and isn't an ancestor of the local sha (`git
|
||||
merge-base --is-ancestor`). The refusal names the ref and both shas, and the hook exits 1.
|
||||
- A new branch passes. A delete (local sha all zeros) is refused.
|
||||
- Any failure of the check refuses too, for example an unknown remote object. The hook fetches nothing.
|
||||
If the remote tip isn't present locally, it refuses and says to fetch first.
|
||||
2. Install, following `scripts/queue-commit.sh --install-hook`. Either add a `--install-push-hook` there or
|
||||
ship a small `scripts/install-git-hooks.sh`, whichever fits the existing code better. Say which in BUILD.md.
|
||||
- The install copies the hook into the common `.git/hooks`, which every seat worktree shares.
|
||||
- It refuses when `core.hooksPath` is set (the husky trap, 2026-10-09). It refuses when an installed
|
||||
pre-push differs from the repository copy, unless `--force-install` is given.
|
||||
- It never edits the installed copy in place.
|
||||
3. `scripts/slot-busy.sh` (new), the shared busy check.
|
||||
- It prints one BUSY line per matching process, or CLEAR.
|
||||
- It exits 0 on CLEAR, 1 on BUSY and 2 on its own error.
|
||||
- It matches `node --test`, `gate.sh`, `mutant`, `scripts/test-*.sh` and `docker compose` command lines.
|
||||
It skips its own process and shell snapshots.
|
||||
- It reads `/proc/<pid>/cmdline` and the cwd link only, never file contents under another seat's
|
||||
directory.
|
||||
- Usage in `docs/TOOLS.md`: `bash scripts/slot-busy.sh > LOG && gate`, with `set -o pipefail` in any
|
||||
script that pipes it. Never `| tail -1 &&`.
|
||||
|
||||
## Tests
|
||||
|
||||
Add them to `scripts/test-land-work.sh` or a new `scripts/test-git-guard.sh` (say which). Run against a scratch
|
||||
bare remote and a scratch clone with a worktree, never against origin:
|
||||
- A rewritten branch pushed with `--force` from a worktree is refused, and the remote ref is unchanged.
|
||||
- A `+refspec` push is refused.
|
||||
- A fast-forward push passes.
|
||||
- A new branch passes.
|
||||
- A delete is refused.
|
||||
- A remote tip that's missing locally is refused.
|
||||
- The install refuses with `core.hooksPath` set, and refuses a differing installed hook.
|
||||
- `slot-busy.sh` exits 1 with a fake `node --test` process running (a sleeping stand-in with that argv),
|
||||
and 0 without one.
|
||||
- `slot-busy.sh | tail -1 && false` doesn't apply here. The test runs `bash scripts/slot-busy.sh >/dev/null
|
||||
&& echo ran`, and asserts it doesn't print `ran` while BUSY.
|
||||
|
||||
Mutants, each killed:
|
||||
- drop the ancestor check
|
||||
- treat an unknown remote object as passing
|
||||
- let a delete pass
|
||||
- install over a set `core.hooksPath`
|
||||
- `slot-busy.sh` exits 0 on BUSY
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Install the hook in the canonical checkout only after landing, as its own step with Sage. Tests use
|
||||
scratch repositories.
|
||||
- No push to origin from a test. No change to Gitea config. That's ops-01's.
|
||||
- Don't touch `~/.mosaic`, `~/.mosaic-dev` or `~/.config/mosaic-dev`.
|
||||
|
||||
## Gate
|
||||
|
||||
Dewey approves the manifest. The new or changed `scripts/test-*.sh` file and every other `scripts/test-*.sh`
|
||||
are green on Sage's gate, and the mutants are killed. Done when the hook is installed in the canonical
|
||||
checkout and a scripted force-push from a seat worktree to a scratch remote is refused there. ops-01's
|
||||
server proof (a refused force-push, plus a fast-forward push and a new branch that go through) is recorded
|
||||
in Vikunja #426 by Mos.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Brief: agent host access with sudo through the credential store (#424)
|
||||
|
||||
Sage, 2026-10-20, for Jason's rulings Q183 and Q184 relayed by Mos (mos-1056 item 3, mos-1067). Gitea #1707, Vikunja P19
|
||||
#424, due 2026-10-27 (Q184: "Q5: by 10.27"). Owner Gizmoduck (design and build), reviewer Duckworth, gate owner
|
||||
Sage. Chained on row 199's candidate (Q177): row 199 design r6 (4d3007ec9 on `megavolt/row199`, approved in
|
||||
duckworth-0027) is not reopened. This row lands after 199 and rebases onto its landing.
|
||||
|
||||
## What Jason asked for
|
||||
|
||||
Q183, through Mos: "I envision a credential store within Mosaic Stack that works as some sort of variable for
|
||||
the agent to call. The system will enforce access to the credentials to keep the agents sandboxed with proper
|
||||
security... If we can utilize the credential store in Mosaic Stack and allow agents to perform their jobs with
|
||||
NOPASSWD sudo on various machines, I am a happy man." Q183 also says not every agent needs that access.
|
||||
|
||||
Mos's requirements (mos-1067 item 1):
|
||||
- An agent asks for a host credential by name.
|
||||
- The stack checks a grant for that agent and that host, and does the ssh and sudo for it.
|
||||
- The agent never sees the key or any secret value.
|
||||
- Only agents with a grant can do it.
|
||||
|
||||
The PRD text is REQ-CRED-3, drafted by Honker for PRD 1.7 (sage-1422). If it misses 1.7 it goes into 1.8, and
|
||||
this row lands only after the version that holds it is approved.
|
||||
|
||||
## What row 199 gives and what it doesn't (megavolt-0019)
|
||||
|
||||
It builds on 199 as 199 stands: the store layout under `<configDir>/secrets/agents/<business>/<agent>/`, the
|
||||
lifecycle verbs and their `changes.jsonl` receipts, `useFor(session, service, fn)` with the D5 allowlist test,
|
||||
`assertClean` on every result, and the grant shape of `publish` (a list on the role entry, checked by the
|
||||
broker, plus a hard-wait action that isn't routed).
|
||||
|
||||
This row's own changes:
|
||||
- A host kind in the closed kind set (vocabulary.mjs `SERVICES`, credentials.mjs, the broker's service check,
|
||||
the CLI date flag, the remove regex), with a naming rule for one credential per host or per agent.
|
||||
- A per-kind value check. An OpenSSH private key fails today's `TOKEN` pattern.
|
||||
- Handing the key to ssh without the agent seeing it: an ssh-agent on a private socket, or a 0600 temp file
|
||||
removed on every path, the way publish's one-shot helper works.
|
||||
- Pinned host keys per host, and no trust on first use.
|
||||
- The command boundary: what an agent may run under sudo, and how output returns without echoing the secret.
|
||||
This is the main design work.
|
||||
- The executor: a ticket and a host op running in the bus child, the same split row 199 uses for publish
|
||||
(Megavolt's deviation in 199's BUILD.md).
|
||||
|
||||
## Steps
|
||||
|
||||
1. DESIGN.md first, in `agents/gizmoduck/work/queue-ROW/`. It answers each item above, names the grant shape
|
||||
(`roles.<agent>.hosts` and a `host.sudo` action, or a reasoned alternative), says who grants (Jason, through
|
||||
a reviewed role change, unless REQ-CRED-3 says otherwise), and lists tests and mutants. Duckworth approves it
|
||||
before code. Sage reads it too, because it gives agents sudo on hosts.
|
||||
2. Build on 199's candidate. Single test files run freely. Suites and mutant batches need Sage's slot.
|
||||
3. The live part is not this row. Putting a key in the store and granting a host is a separate window with
|
||||
Jason or Mos, after landing.
|
||||
|
||||
## Hosts
|
||||
|
||||
The first targets are `localadmin` with NOPASSWD sudo on 10.1.1.53 and 10.1.1.54 (#423 step 1, applied by
|
||||
ops-01 at 20:05:47Z), and the servers agents already reach as localadmin (Q172). ops-01 owns the host side.
|
||||
Tests use scratch sshd fixtures on 127.0.0.1 or fakes, never a real host.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- No change to `~/.mosaic-dev`, `~/.config/mosaic-dev`, slot files, the live units or `~/.mosaic`. No key is
|
||||
read, copied or printed. No ssh to any real host from a test or a seat.
|
||||
- No widening of row 199's kinds or regex inside row 199. Both rows touch them, so this row rebases after 199
|
||||
lands.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves DESIGN.md, then the manifest. The bus, business and cli suites and every
|
||||
`scripts/test-*.sh` are green on Sage's gate, and the mutants are killed (one per refusal, plus one that leaks
|
||||
the key into the agent's output and one that skips the host key pin).
|
||||
@@ -0,0 +1,66 @@
|
||||
# Brief: the Case 1 install on 10.1.1.53 (row 195)
|
||||
|
||||
Sage, 2026-10-20, third version, for Darkwing's rewrite request and sage-1439. Gitea #1693.
|
||||
|
||||
Why it changed:
|
||||
- The first two versions put Melanie's hub on its own Linux user, then on its own host, Jason's choice under
|
||||
#419.
|
||||
- Q181 settled the hosts: Case 1 (Jason's software factory) runs on 10.1.1.53 and Case 2 (Melanie's hub) on
|
||||
10.1.1.54.
|
||||
- Q182 and Q183 made the hub's privilege checks a recorded acceptance. They report and don't refuse
|
||||
(federation R8, mos-1055, mos-1063, mos-1066).
|
||||
- The Case 2 install on .54 is Gosalyn's C2-13 plus its window (#416, due by 2026-10-27). It is not this row.
|
||||
|
||||
This row now installs Case 1 on .53. On the 10-27 MVP path (mos-0974, T1 item 1).
|
||||
|
||||
Owner Darkwing (Opus 5.5), reviewer Duckworth, gate owner Sage. Chained on row 190's candidate (the config
|
||||
canon) and on C2-13's candidate (the install script), Q177. It reuses C2-13's `case2-install.sh`,
|
||||
`case2-precheck.sh` and `case2-unit.sh` by input, not by copy. A gap in them goes to Gosalyn as a C2-13
|
||||
change, or into this row's manifest as a named, reviewed change to those files. Design first:
|
||||
`agents/darkwing/work/queue-195/DESIGN.md`.
|
||||
|
||||
## Goal
|
||||
|
||||
Jason's software factory runs on 10.1.1.53 as `mosaic` under `/srv/mosaic`, in the same unit form as Case 2
|
||||
(design 9, Q181). It uses row 190's config canon and the model account the factory uses today (Q185,
|
||||
mos-1081 item 1). No new provider account.
|
||||
|
||||
## Provision
|
||||
|
||||
- ops-01 runs the root steps over ssh as `localadmin`: `useradd mosaic`, `/srv/mosaic` and the unit through
|
||||
`case2-unit.sh` from a root-owned checkout. #423 step 1 (localadmin with NOPASSWD sudo on .53 and .54) was
|
||||
applied at 20:05:47Z (mos-1066, mos-1077), and nothing else in #423 gates this row.
|
||||
- #423 step 4 (jarvis sudo) waits until Case 1 is live on .53.
|
||||
- The host list comes from Q181. Agents may run read-only checks on .53 as `localadmin`.
|
||||
|
||||
## The design must state
|
||||
|
||||
1. The install as input values to C2-13's scripts: user, home, business file, roles, the human id, the
|
||||
token slot path. Which commands run as `mosaic` through `sudo -u mosaic -H`, and which as root through
|
||||
ops-01.
|
||||
2. Where Case 1 differs from Case 2: the factory roles in place of the library business, no library sync,
|
||||
and any precheck item that doesn't apply, each with a reason.
|
||||
3. Model access through the existing factory account (Q185). Placing the credential on .53 is a step under
|
||||
the Q173 secrets rule, done in the window. It is never copied by the script, and its value is never
|
||||
printed.
|
||||
4. Re-pin, rollback and backup on .53, and who runs them.
|
||||
5. The acceptance checks, as code Duckworth can read. Each check names a path and gives it one of three
|
||||
marks: closed, accepted under Q182 or Q183, or "not named in Q182 or Q183". An accepted path doesn't
|
||||
fail the row. An unnamed one is reported to Mos, who takes it to Jason (the R8 N1 rule). The functional
|
||||
checks fail the row when they fail: the unit runs as `mosaic`, the Console answers on its port on the
|
||||
internal network only, and one tester message gets a reply.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Infra and k3s changes, exposure outside the internal network, and spend stay Jason's (Q173 exclusions).
|
||||
- No change to dragon-lin's `~/.mosaic-dev`, `~/.config/mosaic-dev`, its live units or `~/.mosaic`.
|
||||
- No credential value is printed, logged or committed.
|
||||
- Tests use scratch roots and stand-ins. Nothing in a test reaches .53.
|
||||
- The run on .53 is a window with Jason's or Mos's go, a backup first where state exists, 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 window's acceptance
|
||||
run passes on .53, Duckworth reads its output, and Mos confirms.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user