From 5b8def2f141e852c9bd4cf7eaf7dda6b87b770fc Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 20 Oct 2026 16:22:47 -0500 Subject: [PATCH] plans: Case 2 row briefs from Gosalyn's eac92d956 (design r11, approved webby-0038: C2-13 split into C2-13a unit render and host precheck and C2-13b install script and library precheck, C2-04 owner Herb, Console unit without [Install] per mos-1097) --- docs/plans/2026-10-20_case2-rows.md | 330 ++++++++++++++++++++-------- 1 file changed, 239 insertions(+), 91 deletions(-) diff --git a/docs/plans/2026-10-20_case2-rows.md b/docs/plans/2026-10-20_case2-rows.md index fa147ae78..3a86d5f04 100644 --- a/docs/plans/2026-10-20_case2-rows.md +++ b/docs/plans/2026-10-20_case2-rows.md @@ -18,9 +18,9 @@ packages adapters roles scripts` touches only `scripts/stack-release.sh` and its is in either. Order (design 8, "Parallel waves"): -1. Start now: C2-01, C2-08, C2-11a. +1. Start now: C2-01, C2-08, C2-11a, C2-13a. 2. On C2-01's candidate: C2-04, C2-05a, C2-06, C2-12. On C2-08's: C2-09a, C2-10a, C2-20. -3. Then C2-03a and C2-07. Then C2-05b, C2-10b and C2-13. +3. Then C2-03a and C2-07. Then C2-05b, C2-10b and C2-13b. 4. Last: C2-14. A row whose parent hasn't landed builds on the parent's candidate and rebases onto its landing (Q177). @@ -34,7 +34,7 @@ Rules every row keeps: text wherever a test checks that output holds none. - No credential value in any output, test, log or message (CONSTITUTION gate 13). Tests use a fixture token file with a made-up value. -- No live stack change. Nothing here re-pins, restarts or activates anything. C2-13's run on the host +- No live stack change. Nothing here re-pins, restarts or activates anything. C2-13a and C2-13b's runs on the host needs Jason's go (design 10). - Before in-review, grep `packages/*/tests` and `scripts/test-*.sh` for each changed path and for any 64-hex pin of it, and run every changed or touched test file on its own. Suites need Sage's slot. @@ -172,7 +172,7 @@ Tests, all named in the design 8 C2-08 row: ### Out of scope Thread creation per instance and the launcher (C2-09a), the lookup tools (C2-05b), the cited-reply guard -(C2-06). Checking the host's claude for `--system-prompt-file` is C2-13's precheck. If the C2-20 dry run +(C2-06). Checking the host's claude for `--system-prompt-file` is C2-13a's precheck. If the C2-20 dry run finds heading lines in a real persona, the fence rule changes in a follow-up to this row, reviewed, before the import (design 9.1). @@ -230,7 +230,7 @@ from changing `projects`, and nothing creates a mirror. ### Owner and reviewer -- Owner: Rocko, gpt-6.1-sol. +- Owner: Herb, gpt-6.1-sol. Herb replaces Rocko, who holds five other rows (sage-1482). - Reviewer: Filbert. - After: C2-01. @@ -706,27 +706,236 @@ Dewey approves the manifest. The webui suite is green on Sage's gate, along with --- -## C2-13 install script and precheck for the host +## C2-13a unit render and host precheck ### Problem -The hub runs on 10.1.1.54 as `mosaic` under `/srv/mosaic`, as `mosaic-stack.service` with -`ProtectHome` (design 9, Q181). Nothing installs a library business there, or checks the host before it -starts. +The hub runs on 10.1.1.54 as `mosaic` under `/srv/mosaic`, as two system units, +`mosaic-bus@.service` and `mosaic-webui.service` (design 9 and 10 step 2c, r11). Nothing +renders those units for a system install, and nothing checks the host before the start. Row 195 needs +the same render and precheck for Case 1 on 10.1.1.53, so this part comes first and has no library +dependency (sage-1480, Q177). + +### Owner and reviewer + +- Owner: Gosalyn, Opus 5.5. The scripts run on a host only under Jason's go, by him or his delegate + (design 10). +- Reviewer: Webby. +- After: nothing. Row 195 chains on this row's candidate. + +### Files owned + +`scripts/case2-unit.sh` and `scripts/case2-precheck.sh` (new), `packages/cli/tests/case2-unit.test.mjs` +(new), and one line of `packages/cli/README.md`: at `:713`, "nothing in the repository creates or reads +it" changes to say that nothing creates it, that only the Case 2 precheck reads it, and that the +precheck prints only variable names, never values (webby-0034 N2, webby-0035). + +### What ships: `case2-unit.sh` + +`case2-unit.sh` is the 2c step. Root runs it from a root-owned checkout of the reviewed commit outside +`/srv/mosaic`, never from the release, because `mosaic` can write the release. + +Inputs. `--profile factory|library` (default `library`), `--user`, `--home`, `--business`, `--copy ID`, +`--node PATH`, and the Console's serve flags after `--`. Row 195 calls it with those names. +- `--node` must be an absolute path to a root-owned regular file outside ``. +- The copy id must be exactly 40 lowercase hex characters naming a directory that holds + `scripts/mosaic`, directly under `/.local/share/mosaic-stack/copies`. That is the rule + `copy_root` uses (`scripts/stack-copy.sh:6`, `:20`). Anything else refuses before the render. +- The serve flags. The script copies the checks from `serve_flags` (`scripts/webui-service.sh:146-207`) + into its own function, with its own tests. It doesn't source or change that script, because its + proof-file check compares the owner to `id -u` (`:201`), which is root here, and its render finds + node with `command -v` (`:237`), not `--node` (webby-0036 N3). The library profile takes exactly + these flags, each once, and refuses any other: + - `--port N`, with N in 1..1023 (design 9.3). + - `--business ID`, equal to `--business`. + - `--public-host NAME`, `--auth-header HEADER` and `--allow-user ID`, checked as + `webui-service.sh` checks them. Their values come from design 10 step 5. + - `--send`, because C2-10b's chat needs it. + - `--proof-header NAME` and `--proof-file PATH`. The proof is required, not optional (design 9.3). + The file must be a regular file, not a symlink, owned by ``'s uid from `getent passwd`, mode + 0600 or 0400, under `/.config/mosaic-dev/secrets/`. That keeps it out of the data root, where + a session's run directory is. The script stats it and never opens it. + - It refuses `--tester-port`. The tester listener takes instance, route, name and chat writes from + loopback with no sign-in (`packages/webui/src/serve.mjs:472`, `:532-542`). + - It refuses `--checkout`. Her host has no canonical checkout, and the queue store refuses a read + from the copy, so the queue panel isn't part of Case 2 (webby-0036 N4). + - It refuses `--board`. Case 2 runs no control board, and design 9.3 says why the default stays. + The factory profile takes the same set plus an optional `--tester-port N` in 1024..65535, never equal + to `--port`, because Case 1's acceptance uses the tester. Row 195 names any other difference in its + own brief. + +The render. It renders two system units from the checkout's existing templates and changes neither: +`mosaic-bus@.service` from `packages/cli/systemd/mosaic-bus.service.in`, enabled as +`mosaic-bus@.service`, and `mosaic-webui.service` from +`packages/webui/systemd/mosaic-webui.service.in`. Every token has one value (webby-0036 B2): +- `@REPO@` is the copy directory. +- `@NODE@` is `--node`. +- `@PATH@` is the directory of `--node` first, then whichever of `/usr/local/bin`, `/usr/bin` and `/bin` + it isn't, in that order. The bus's `ExecStart=` is `scripts/mosaic`, which runs `exec node` from + PATH (`scripts/mosaic:22-40`), so this line picks the bus's node. It differs from + `scripts/webui-service.sh:246-248` in one case on purpose. That script leaves the list alone when the + node's directory is already in it, so with `--node /usr/bin/node` a node in `/usr/local/bin` would + come first and the bus would run it. +- `@FLAGS@` is the checked serve flags, in the order given. +- `@PM_FLAGS@` and `@CLAUDE_ENV@` render empty. +- `%h` becomes the literal `--home`, because in a system unit `%h` is root's home. +- After the render it refuses any `@[A-Z_]+@` or `%h` left in either unit. + +Lines it adds or replaces. At the end of `[Service]` in both units it adds `User=`, +`Group=` and these four lines, in this order (webby-0036 B3): +``` +ProtectSystem=strict +ReadWritePaths= +ProtectHome=yes +PrivateTmp=yes +``` +The templates' own `NoNewPrivileges=yes` stays. The Console unit also gets +`AmbientCapabilities=CAP_NET_BIND_SERVICE` and `CapabilityBoundingSet=CAP_NET_BIND_SERVICE`, so it can +bind its port below 1024 (design 9.3). The bus unit gets no capability line. It adds nothing else. No +line restricts namespaces or system calls. A session's bwrap needs a user, PID and IPC namespace +(`packages/seat/src/sandbox.mjs:67`) and a mount namespace, and `RestrictNamespaces=`, +`PrivateUsers=` or `SystemCallFilter=` can each take one away, so it refuses a template or render that +holds any of them. +- `[Install]`. In the bus unit, `WantedBy=default.target` is replaced by `WantedBy=multi-user.target`, + not followed by a second line (webby-0036 N1). The Console unit renders with no `[Install]` section + (webby-0036 B1, mos-1097). The script enables only the bus unit. Jason starts the Console at design + 10 step 9 and after each boot. +- No `After=` or `Wants=`. The Console never starts at boot, so there is no boot order to set, and + serve calls the bus per request, so it starts without the bus (webby-0036 N2). The step 9 order is + manual: the bus, then the Console. Stop in reverse. + +Checks. It refuses unless `User=`, `Group=`, `ExecStart=`, the bus unit's `EnvironmentFile=`, the +hardening lines and the Console's two capability lines match fixed values. `EnvironmentFile=` is the +line in `packages/cli/systemd/mosaic-bus.service.in:24`, with the literal home. The Console unit has +none, and it refuses one. It also refuses: +- any `PermissionsStartOnly=`, and any `Exec*=` value whose leading run of prefix characters (`@`, `-`, + `:`, `+`, `!`) holds a `+` or `!`. systemd takes those prefixes in any order, so `-+/srv/mosaic/x` + runs as root even with `User=mosaic`. The named-line check doesn't read these lines (webby-0026 N1, + webby-0027, defence in depth). +- on a re-install, an installed pair whose two units name different copy ids (webby-0036 N5). The bus + accepts only its own `human-cli.mjs` path, so a split pair breaks the human proof + (`scripts/webui-service.sh:266-273`). +- on a re-install, an installed unit that differs from the render in anything but the copy id. So the + node path and serve flags must equal the installed ones. The copy id is the one value that may + change, so a re-pin or a rollback to another copy re-renders both units together (row 195). + +It writes both units under `/etc/systemd/system/` as `root:root` 0644. It never uses `systemctl link`, +and it refuses a symlink at either unit path. It opens no file under ``. It only stats the copy +directory, its `scripts/mosaic` and the proof file. + +### What ships: `case2-precheck.sh` + +It runs as `` and takes `--profile factory|library`, default `library`. C2-13a ships the host +checks below. C2-13b adds the library checks to the same file. It refuses unless all of these hold: +- The config validates. +- `sudo -n true` fails as ``, and `id -G` prints exactly one entry, its primary group. A system + user always has one, so the check is for supplementary groups. +- `bus.tester`: `library` refuses it on. `factory` requires it on, because Case 1's acceptance uses the + tester. `factory` refuses a business file that has a `kind: "library"` project, with + `profile-library-business`, so Case 2 can't reach a tester-on check through a wrong flag. +- The run root is 0700. +- `XDG_DATA_HOME` is unset or is `/.local/share`. `copy_root` reads the copies dir from it + (`scripts/stack-copy.sh:6`), and the unit fixes `/.local/share`, so any other value would put + the copy where the unit refuses it (webby-0032 N2). +- `node --version` is 24 or later, because every package's `engines` field says `>=24` (row 195: .53 + has v22.22.2). +- `bwrap --unshare-all --ro-bind / / true` exits 0 as ``. Without it no session starts. Row 195 + found no bwrap on .53, and `kernel.apparmor_restrict_unprivileged_userns=1`, and .54 may be the same. + On a refusal, the precheck prints the sysctl's value and whether `bwrap` is on PATH. This runs in + ``'s shell, outside the unit's hardening, so it can't show that the unit lets bwrap run. Design + 10 step 9 checks that. +- `net.ipv4.ip_unprivileged_port_start` is above the Console's `--port`, read from the installed + Console unit. It refuses when the floor is at or below the port, because a lowered floor lets any + `mosaic` process bind the port, a session included, and makes the Console's capability pointless + (design 9.3, mos-1100). +- When some instance's harness is claude-code, one marker prompt run through the host's claude with + `--system-prompt-file` shows that it replaces the default prompt. The claude version is printed. With + the design's model path, Bean and Chet run pi, so this check is skipped unless she adds a claude-code + specialist. +- When an instance's provider is `zai`, `/secrets/claude-session.env` exists, is a regular + file owned by `` at 0600, and sets a nonempty `ZAI_API_KEY` (design 10 step 2d). It parses the + file the way systemd does, so the last assignment of a name wins (webby-0032 N3). A pi launch has no + credential check, so without this the first turn is where a missing key shows. The precheck prints + the name and never the value. + +It also prints free memory and verifies that HOME is ``. + +### Tests + +`case2-unit.sh` runs against a scratch unit directory. A clean render writes 0644 files, and these hold: +- the bus unit's `Environment=PATH=` starts with the `--node` directory, both for a `--node` outside the + default list and for `/usr/bin/node`, which gives `/usr/bin:/usr/local/bin:/bin`. Its `ExecStart=` is + the copy's `scripts/mosaic` +- the Console unit's `ExecStart=` starts with `--node` and holds the flags in order +- the bus unit has exactly one `WantedBy=`, `multi-user.target`, and the Console unit has no `[Install]` +- both units hold the four hardening lines once, in order, and only the Console holds the two + capability lines +- a check of the script for `systemctl link` finds nothing + +These cases refuse: +- an installed unit edited to `User=root` +- an edited `ExecStart=` +- an installed unit with a hardening line removed, and a template with `RestrictNamespaces=`, + `PrivateUsers=` or `SystemCallFilter=` added +- a bus template with the `EnvironmentFile=` line removed or pointing elsewhere, and a Console template + with one added +- a symlink at either unit path +- a `--node` that is relative, not root-owned, or under `` +- a re-install with a different `--node` or different serve flags +- an installed pair whose units name different copy ids +- a template holding an unknown `@TOKEN@`, and an unrendered `%h` left in either unit +- a template with `ExecStartPre=+/srv/mosaic/x`, and one with `ExecStartPre=-+/srv/mosaic/x` +- a copy id that is short, uppercase, holds `/` or `..`, or names no directory +- with `library`: `--port 7332`, a missing proof pair, a proof file owned by root, at 0644, a symlink, or + under the data root, `--tester-port`, `--checkout`, `--board`, a `--business` different from the + unit's, and an unknown flag. With `factory`: `--tester-port` equal to `--port`. + +The precheck runs against a scratch data root. Each refusal has a case. A fake `node` printing v22 +refuses, a fake `bwrap` that exits 1 refuses, and an unprivileged port start at or below the Console's +port refuses. The claude check runs against a fake claude that appends rather than replaces, and that +case refuses. With no claude-code instance the claude check doesn't run, and a test shows a missing +claude passes. The key file refuses when missing, at 0644, a symlink, with `ZAI_API_KEY=` empty, and +with `ZAI_API_KEY=x` followed by `ZAI_API_KEY=`. A search of the test output for the fixture key value +finds nothing. `factory` passes with the tester on and refuses a business file with a library project. +`library` refuses with the tester on. + +The bind test (mos-1100). A session can't take the Console's port. The test builds the argv a real +session gets, with `binds()` and `sandboxArgv()` from `packages/seat/src/sandbox.mjs`, around a node +one-liner that binds `127.0.0.1` on a port below 1024 and prints the error code. Inside bwrap the bind +fails with `EACCES`. The control runs the same one-liner in the same sandbox on a free port of 1024 or +above, and that bind succeeds, so the failure comes from the port floor and not from a broken sandbox. +The test reads `net.ipv4.ip_unprivileged_port_start` first. If the test host's floor is at or below the +chosen port, the test fails and says so, rather than skipping. + +### Out of scope + +Running either script on a host. Design 10 steps 2 to 9 are Jason's, and the root step 2c is ops-01's +over ssh as `localadmin`. The library checks and the install are C2-13b. + +### Gate + +Webby approves the manifest. The test file and the cli suite are green on Sage's gate, along with every +`scripts/test-*.sh`. A run on a host is a separate window with Jason's go. + +--- + +## C2-13b install script and library precheck + +### Problem + +Nothing installs a library business on her host, and C2-13a's precheck doesn't yet check the library +parts. ### Owner and reviewer - Owner: Gosalyn, Opus 5.5. The script runs only under Jason's go, by him or his delegate (design 10). - Reviewer: Webby. -- After: C2-04, C2-03a, C2-20. +- After: C2-13a, C2-04, C2-03a, C2-20. ### Files owned -`scripts/case2-install.sh`, `scripts/case2-precheck.sh` and `scripts/case2-unit.sh` (new), the template -`packages/cli/systemd/mosaic-stack.service.in` (new), `packages/cli/tests/case2-install.test.mjs` -(new), and one line of `packages/cli/README.md`: at `:713`, "nothing in the repository creates or reads -it" changes to say that nothing creates it, that only the Case 2 precheck reads it, and that the -precheck prints only variable names, never values (webby-0034 N2, webby-0035). +`scripts/case2-install.sh` (new), `scripts/case2-precheck.sh` (the library checks only) and +`packages/cli/tests/case2-install.test.mjs` (new). ### What ships @@ -736,92 +945,31 @@ path. It runs as `` through `sudo -u mosaic -H` from `localadmin`, and `mo doesn't create `` and writes no unit file. Root does those in design 10 steps 2a and 2c. Every host value is an input, so a later host move changes only inputs. -Row 195 runs these scripts by input for Case 1 on 10.1.1.53 (darkwing to gosalyn, design at -`agents/darkwing/work/queue-195/DESIGN.md` section 2). They don't copy them. So the install and the -precheck take `--profile factory|library`, default `library`: +Row 195 runs the install by input for Case 1 on 10.1.1.53 (darkwing to gosalyn, design at +`agents/darkwing/work/queue-195/DESIGN.md` section 2). It doesn't copy it. So the install takes +`--profile factory|library`, default `library`, as the precheck does: - `library` is everything this brief lists. - `factory` skips the library mapping, the persona files and digests, the token slot, and - `library.sync` and `library.status`. It requires `bus.tester` on, because Case 1's acceptance uses the - tester. -- `factory` refuses a business file that has a `kind: "library"` project, with - `profile-library-business`. So Case 2 can't reach a tester-on check through a wrong flag. -- `library` refuses with `bus.tester` on, as before. + `library.sync` and `library.status`. -`case2-unit.sh` is the 2c step. Root runs it from a root-owned checkout of the reviewed commit outside -`/srv/mosaic`, never from the release, because `mosaic` can write the release. It does the following: -- It renders the unit from the checkout's template, with the user, the home and the copy id as inputs. -- It refuses unless `User=`, `Group=`, `ExecStart=`, `EnvironmentFile=` and the hardening lines match - fixed values. `EnvironmentFile=` is the line in `packages/cli/systemd/mosaic-bus.service.in:24`. The - copy id is the one part of `ExecStart=` that varies, so a re-pin to a new copy can re-render the - unit (row 195). It must be exactly 40 lowercase hex characters naming a copy that holds `scripts/mosaic`, - directly under `/.local/share/mosaic-stack/copies`. That is the rule `copy_root` uses - (`scripts/stack-copy.sh:6`, `:20`). Anything else refuses before the render. -- It refuses any `PermissionsStartOnly=`, and any `Exec*=` value whose leading run of prefix - characters (`@`, `-`, `:`, `+`, `!`) holds a `+` or `!`. systemd takes those prefixes in any order, - so `-+/srv/mosaic/x` runs as root even with `User=mosaic`. The named-line check doesn't read these - lines (webby-0026 N1, webby-0027, defence in depth). -- It writes `/etc/systemd/system/mosaic-stack.service` as `root:root` 0644. It never uses - `systemctl link`, and it refuses a symlink at the unit path. -- On a re-install, it refuses an installed unit that differs from the render. -- It reads nothing from under ``'s home. - -The precheck refuses unless all of these hold: -- The config validates. -- `sudo -n true` fails as ``, and `id -G` prints exactly one entry, its primary group. A system - user always has one, so the check is for supplementary groups. `bus.tester` is not on. -- The business file, `.history`, `changes.jsonl` and persona files have the modes in design 4, and - each persona digest matches. -- The run root is 0700. -- `XDG_DATA_HOME` is unset or is `/.local/share`. `copy_root` reads the copies dir from it - (`scripts/stack-copy.sh:6`), and the unit fixes `/.local/share`, so any other value would - put the copy where the unit refuses it (webby-0032 N2). A test sets it elsewhere and the precheck - refuses. -- `node --version` is 24 or later, because every package's `engines` field says `>=24` (row 195: .53 - has v22.22.2). -- `bwrap --unshare-all --ro-bind / / true` exits 0 as ``. Without it no session starts. Row 195 - found no bwrap on .53, and `kernel.apparmor_restrict_unprivileged_userns=1`, and .54 may be the same. - On a refusal, the precheck prints the sysctl's value and whether `bwrap` is on PATH. -- When some creative instance's harness is claude-code, one marker prompt run through the host's claude - with `--system-prompt-file` shows that it replaces the default prompt. The claude version is printed. - With design r9's model path, Bean and Chet run pi, so this check is skipped unless she adds a - claude-code specialist. -- When an instance's provider is `zai`, `/secrets/claude-session.env` exists, is a regular - file owned by `` at 0600, and sets a nonempty `ZAI_API_KEY` (design 10 step 2d). It parses the - file the way systemd does, so the last assignment of a name wins (webby-0032 N3). A pi launch - has no credential check, so without this the first turn is where a missing key shows. The precheck - prints the name and never the value. +The precheck's library profile adds these checks to C2-13a's: +- The business file, `.history`, `changes.jsonl` and persona files have the modes in design 4, and each + persona digest matches. - `library.sync` fetches her repo read-only, and `library.status` lists R-problems. - No Discord binding exists. -It also prints free memory and verifies HOME is `/srv/mosaic`. +### Tests -Tests run against a scratch data root and a local bare repo. Each precheck refusal has a case. The -claude check runs against a fake claude that appends rather than replaces, and that case refuses. With -no claude-code instance the claude check doesn't run, and a test shows a missing claude passes. The key -file refuses when missing, at 0644, a symlink, with `ZAI_API_KEY=` empty, and with `ZAI_API_KEY=x` -followed by `ZAI_API_KEY=`. A search of the test -output for the fixture key value finds nothing. -`case2-unit.sh` runs against a scratch unit directory. These cases refuse: -- an installed unit edited to `User=root` -- an edited `ExecStart=` -- a template with a hardening line removed -- a template with the `EnvironmentFile=` line removed or pointing elsewhere -- a symlink at the unit path -- a template with `ExecStartPre=+/srv/mosaic/x` -- a template with `ExecStartPre=-+/srv/mosaic/x` -- a copy id that is short, uppercase, holds `/` or `..`, or names no directory - -Precheck cases for the new lines: a fake `node` printing v22 refuses, and a fake `bwrap` that exits 1 -refuses. `--profile factory` passes with the tester on and no library items, and refuses a business -file with a library project. `--profile library` refuses with the tester on. -A clean render writes a 0644 file. A test checks the script for `systemctl link`. +They run against a scratch data root and a local bare repo. Each new precheck refusal has a case: a +persona file at 0644, a digest mismatch, a business file at the wrong mode, a sync failure and a +Discord binding. The install's `factory` profile writes no library mapping, persona file or token slot. ### Out of scope Running it on the host. Design 10 steps 2 to 7 are Jason's. Step 1 no longer waits on #421 (Q183), and -Mos lifted its hold (mos-1074). The root steps 2a and 2c are ops-01's, over ssh as `localadmin`, -so the install waits on #423 step 1 and on nothing else in #423. The Gitea bot account and the token slot are steps 3 and 4. Stopping Hermes -is step 9 and her choice. +Mos lifted its hold (mos-1074). The root steps 2a and 2c are ops-01's, over ssh as `localadmin`, so the +install waits on #423 step 1 and on nothing else in #423. The Gitea bot account and the token slot are +steps 3 and 4. Stopping Hermes is step 9 and her choice. ### Gate @@ -896,7 +1044,7 @@ its first turn, as `packages/cli/README.md:439-444` says. shell inside any of them can read it. Where the README names `claude-session.env` ("Unit", `:711`), "The file holds one line" becomes "The file holds one line per variable", followed by a sentence saying it holds every session provider key, not only Claude's (webby-0032, webby-0034 N1). Leave - `:713`, "nothing in the repository creates or reads it", alone. C2-13 owns that line. + `:713`, "nothing in the repository creates or reads it", alone. C2-13a owns that line. - One test in `session.test.mjs`: `ENV_ALLOW` includes `ZAI_API_KEY`, and `sessionEnv({ ZAI_API_KEY: "fixture-zai", ZAI_TOKEN: "t" }, run)` returns `ZAI_API_KEY` with the fixture value and no `ZAI_TOKEN`. The existing allowlist test (`:51-59`) stays as it is.