18 KiB
Part B window: turn on the row 178 proxy proof on the live copy
Sage, 2026-10-20, draft 2, for Mos's A2 check (mos-0928 Part B, P19 385, Gitea #1676). Written on refactor at
a843ae284, which lands row 187 (#1685, A0). Plan b05eb85e (repin-rows-177-185.md) stays as it is. This file
replaces its Part B "Agent steps" table for the run and carries the B-R1 correction Mos accepted in mos-0954.
Draft 2 makes Mos's two mos-0957 fixes: B6 and B-R1 name the stop's rows (F1), and PB6 runs (a)-(h) (F2).
Nothing in this file runs until all of these hold:
- Jason's go on Part B (J1).
- Queue row 188 (Gitea #1686), the tests for P19 409 (M8) and 410 (M9), has landed (mos-0956). If it changes the tool blob named below, Mos checks this file again (mos-0849/0850).
- Q1 below is settled: the order of J2 and this window.
- Mos's A2 check of this file passed.
Part B changes who can reach the Console from outside, so it runs only with Jason present. B5 and J6 are his.
Who, and what nobody does
- Darkwing operates, as the tester. Mos verifies each step's output before the next step starts. Sage owns the plan and the STATE report (P19 385, Gitea #1676). Jason's steps are J1-J6 below.
- Nobody runs anything as Jason: no
--by jason, no human shell, no typing into a tmux pane or a human Console session. Every bus read and write carries--tester. - Nobody prints, copies, hashes or sends the proof value. The slot file is checked with
statandrealpathonly. No request in any matrix carries the real value, Jason's user id or any credential. bus.testerstays as it is. No pi (mos-0865 D2, P19 #383). No Save in the human Console.mosaic namestays out.- No plain
rollbackanywhere in Part B. It goes tofrom_log previous(stack-release.sh:155), the newest ok line whosetois not the pointer's copy. That is still89af38a83, so it would undo rows 177-185 along with the proof (mos-0954).
Names
- LIVE:
490e2771d3d057ab76991e2c7859cd251857e64c, activation20261020T075800Z-490e2771d3d0,pm: true, schema 3i (pointer read at 10:26Z). The window doesn't move off it. - FLAGS_OFF, the pointer's Console flags today:
--port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje --send --tester-port 7333. - FLAGS_ON, what B3 installs:
--port 7332 --public-host mosaic.woltje.com --auth-header X-authentik-username --allow-user jason.woltje --proof-header X-Mosaic-Proof --proof-file $SLOT --send --tester-port 7333. The pair goes after the preview flags and before--send, the orderwebui-service.sh renderwrites andflags_driftcompares. - REPO: the canonical checkout.
activateruns REPO'sstack-release.shand REPO'swebui-service.sh(stack-release.sh:453ata843ae284). The run uses the row 187 tool from REPO; the copy doesn't need it. - TOOL:
scripts/stack-release.shblob2586a2a37cd2e7c7aeaba6d31490e5b7920fb4b2ata843ae284.scripts/webui-service.shis blobd34af09dccdee16430d185826a966785734776e0at both LIVE anda843ae284. - No re-pin and no new copy.
git diff --stat 490e2771d a843ae284 -- packages scripts roles contracts adapters skillslists onlyscripts/stack-release.shandpackages/cli/tests/stack-release.test.mjs. LIVE's Console already takes the pair (row 178,packages/webui/src/cli.mjs:49), soproof_for_targetfinds support. - SLOT:
~/.config/mosaic-dev/secrets/mosaic-stack/console-proof.token, written as an absolute path. It's outside REPO and the copies, which the tool and the render both require. - HDR:
X-Mosaic-Proof. It passesproof_checks: lowercased it matches^[a-z0-9][a-z0-9-]{0,63}$and is none ofhost,origin,x-mosaic-testerorx-authentik-username. DATA,P,T=7333andSNAPas in Part A.SNAPis$WIN/snapshot.mjs, sha256639fd28a2674aaa03ce824e3394b4052f21cdef7d6fa49bae61a9927ed341e67, copied from Part A's window directory.WIN:agents/darkwing/work/window-partB-proof/, where every transcript goes.
What the window changes
Only the Console flags. B3 is an activate of the pointer's copy, which row 187 allows when the run sets a
proof pair (check_target, stack-release.sh:241-250). It runs like any activate with FROM equal to TO: stop
both units, back up the stopped store (row 185), install, start, gate. With --pm the bus start launches a new
chief. There's no schema step, since the store and the target are both 3i, and no plug-in, role or skill change.
Backups (Q173: a backup before every change)
| Before | Backup | Taken by |
|---|---|---|
| B3, and B-R1 if it runs | the stopped store, $DATA/backups/stack/<activation id>/ with SHA256SUMS, DATA_ROOT, SCHEMA 3i and STAMP |
stack-release.sh (row 185). Darkwing records the path and STAMP |
| B3, and B-R1 if it runs | the rollback set the activation captures (units, wrapper, pointer) | stack-release.sh (capture) |
| B0 | the business file, cp -p to ~/.config/mosaic-dev/businesses/mosaic-stack.json.pre-partB-<stamp> with its sha256. Nothing in the window writes it |
Darkwing |
The slot file gets no backup. A lost slot means writing a new value (A1) and Jason resealing it (J4).
Jason's steps
J1-J6 as on P19 385 and plan b05eb85e, unchanged (mos-0954). B-R1's command is the one below.
| # | Step |
|---|---|
| J1 | The go on Part B. |
| J2 | The uid separation on #385. See Q1 for its order against this window. |
| J3 | The k3s side: a SealedSecret holding the proof value, and a Traefik headers.customRequestHeaders middleware on the Console route in the mosaic-preview Argo app that sets X-Mosaic-Proof from it. |
| J4 | Sealing the value into J3's SealedSecret from the slot, without printing it. |
| J5 | The outside check after B4: sign in through https://mosaic.woltje.com/ and see the Console. |
| J6 | The go on B-R1 if B3, B4 or J5 fails, and then taking the middleware off. Flags off first, middleware second. |
Before the window (nothing stopped)
| # | Who | Step |
|---|---|---|
| PB0 | Mos | The preconditions at the top hold: J1's go quoted with its time, the P19 409/410 row done with its landing sha, Q1's answer, and this file's blob as checked. |
| PB1 | Darkwing | The business file, read only, the same values as Part A P1 and nothing else: bus.tester true, the plug-in keys, launch.by chief, no pi harness. Its sha256, compared with the value Part A's STATE report recorded; any difference is reported with the reason. |
| PB2 | Darkwing | The tunnel, read only, as Part A P4: exactly one R: spec, R:0.0.0.0:7332:127.0.0.1:7332, and DropInPaths empty. |
| PB3 | Darkwing | git -C $REPO rev-parse HEAD:scripts/stack-release.sh HEAD:scripts/webui-service.sh prints the TOOL blob and the webui-service.sh blob above, and git -C $REPO status --porcelain -- scripts/stack-release.sh scripts/webui-service.sh scripts/bus-service.sh scripts/stack-copy.sh is empty. Those four must not move until B7. |
| PB4 | Darkwing | A1, if not done: umask 077, then one line of at least 43 characters from [A-Za-z0-9_-] to SLOT from a random source, never printed. Then, for A1 old or new: stat -c '%a %U %s %F' $SLOT prints 600 jwoltje N regular file with N 44 or more, test ! -L $SLOT, and realpath -e $SLOT is outside REPO and $DATA/copies. Nothing else reads the file. |
| PB5 | Darkwing | The rehearsal on a scratch root seeded at LIVE and 3i, with REPO's TOOL, the example business file plus the plug-in keys, bus.tester true and scratch ports. The proof file is a scratch file holding a dummy value written for the rehearsal, not the slot. A held snapshot of the scratch store goes before (1), between (1) and (2), and after (2), so the counts show which rows the stop, the PM launch and the matrix each write. Steps: (1) B1's plan and B3's run with the pair; (2) the B4 matrix, plus one request with the dummy value and the auth header on the public Host, which must get past the proof to the sign-in check; (3) activate <scratch LIVE> --pm --no-proof, plan and run, and the PB6 matrix again, identical to the first; (4) rollback --plan after (1), whose to must not be LIVE, which shows why Part B never uses it; (5) a grep of every file under the scratch state and data root and every saved stdout and stderr for the dummy value: no match outside the proof file itself. Output in $WIN/pb5/ with sha256 values Sage checks. |
| PB6 | Darkwing | The 7332 matrix with the proof off, rows (a)-(h), saved as $WIN/matrix-off.txt. Lines (a)-(g) must be byte-identical to Part A's matrix-new.txt (sha256 93c07e15), and (h) must be 200. |
| PB7 | Jason | J3 and J4 applied, the middleware live on the route. Then Darkwing reruns PB6 as $WIN/matrix-off-mw.txt, under PB6's rule: (a)-(g) equal matrix-new.txt and (h) is 200, so the file equals matrix-off.txt: the loopback requests don't go through the middleware, and with the proof off the Console ignores the extra header. Jason's own sign-in through the public name still works; that check is his. |
The window
| # | Who | Step |
|---|---|---|
| B0 | Darkwing | The business file copy in the Backups table, its sha256 in $WIN/backups.txt. The idle check: mosaic agents --business mosaic-stack --tester (chief not working) and mosaic launches list. Start stamp from date -u +%FT%TZ. Then S1, node $SNAP --store "$DATA/bus/bus.sqlite" --held. |
| B1 | Darkwing | scripts/stack-release.sh status mosaic-stack: pointer on LIVE, pm: true, schema 3i, last line ok, unit flags match the pointer. Then scripts/stack-release.sh activate $LIVE mosaic-stack --pm --proof-header X-Mosaic-Proof --proof-file $SLOT --plan. It must print from: and to: LIVE, note: <LIVE> is the pointer's copy; the run re-pins it and only the Console flags change, migration: none; the store is at the target's version, the backup line, proof: on, header X-Mosaic-Proof, file <SLOT>, and the Console line equal to FLAGS_ON. No refusal. It must match PB5 (1) on the same lines. Any other difference stops the window. |
| B2 | Darkwing | The units, read only, as Part A W4: DropInPaths empty on both, bus ExecStart has --pm, both name LIVE, no ", ' or \ in the bus Environment= or either ExecStart. |
| B3 | Darkwing, on Mos's go | The same activate without --plan. Exit 0, final result ok, every gate check including pm_launched. Record the activation id, the backup path and STAMP, the backup's SHA256SUMS sha256, and the new chief's launch id from mosaic launches list in $WIN/backups.txt. Then status: pointer on LIVE with FLAGS_ON. Then journalctl --user -u mosaic-webui.service --since "<B3 stamp> UTC" --no-pager | grep -c 'Proxy: proxy proof on' prints 1. |
| B4 | Darkwing | The matrix with the proof on, below, saved as $WIN/matrix-on.txt. |
| B5 | Jason | J5. |
| B6 | Darkwing | S2, node $SNAP --store "$DATA/bus/bus.sqlite" --held. Against S1, S1's rows are unchanged, and the new rows are only these, which Darkwing lists by table with seq, stamp and kind: (1) the stop's rows for the running chief r34520a581318, written by LIVE's broker.mjs endLaunch before the backup STAMP, as in Part A: an appended role_claims release row and an events session.ended row, so the B3 backup is not equal to S1 by design and S1 is a prefix of it; (2) B3's PM launch rows for the new chief, in the tables and counts PB5 (1) showed; (3) the B4 matrix writes none: every refused request returns before any store read, and (a), (g) and (h) only read, which PB5's snapshot around (2) must confirm. If PB5 showed rows there, those kinds and counts are expected instead; (4) rows from J5, if any, named with what Jason reports doing. Any other row stops before B7 and goes to Mos. |
| B7 | Darkwing, then Sage | The STATE report: stamps, activation id, backup path and sha256 values, pointer before and after, unit ExecStart lines (no Environment values), S1 and S2, PB5's sha256 values, every matrix, the journal line, J5's result as Jason reports it, and any stop. Sage checks it against the transcripts and posts a summary on P19 385 and #1676. |
The B4 matrix
Each request is a curl -s -o /dev/null -w '%{http_code} ' plus the JSON error field, from Darkwing's shell.
<dummy> is a fixed 43-letter value written into this file's run, not the slot value.
| # | Request | Proof off (PB6) | Proof on (B4) |
|---|---|---|---|
| a | GET http://127.0.0.1:7332/healthz |
200 | 200 |
| b | GET / on 7332, Host: mosaic.woltje.com, no auth header |
401 sign-in required |
401 proxy proof required |
| c | as (b) plus X-Mosaic-Proof: <dummy> |
401 sign-in required |
401 proxy proof required |
| d | GET / on 7332, loopback Host, X-authentik-username: tester-probe |
403 proxy Host refused |
403 proxy Host refused |
| e | GET / on 7332 with x-mosaic-tester: 1 |
403 tester listener required |
403 tester listener required |
| f | GET / on 7333 with X-authentik-username: tester-probe |
403 proxy header refused |
403 proxy header refused |
| g | GET / on 7333 with X-Mosaic-Proof: <dummy> |
200 | 403 proxy proof header refused |
| h | GET / on 7332, loopback Host, X-Mosaic-Proof: <dummy> |
200 | 403 proxy proof Host refused |
The proof-on column follows serve.mjs:483-503 at LIVE: the tester listener refuses the proof header by name
(cli.mjs:92 passes testerProofHeader), and the proof check runs before any auth-header read. Row (h) is new
in Part B. With the proof off it's an ordinary loopback request with no auth header, the operator's local path
(serve.mjs:508-512), so 200. PB5 and PB6 measure it; any cell that differs from this table stops the window
before B3, and Mos decides whether the table or the window changes.
Nothing here tests the pass path with the real value. PB5 (2) tests it with the dummy, and J5 tests it live.
Stop conditions
Before B3, any refusal or mismatch ends the window with nothing stopped.
- B3 exit 1:
rollback_in_runput back the captured unit files and wrapper and started them, so the Console runs with FLAGS_OFF. The pointer is unchanged and no restore runs (no migration). Report the log line and the failing check to Mos and Jason. - B3 exit 2,
rollback-failed: both units stay stopped. Mos settles it from the rollback set the message names, with Jason there. - A B4, B5 or B6 failure after B3 passed: B-R1 on Jason's go (J6).
- Two failures in one window: stop and report (mos-0823).
B-R1, the way back
- A held snapshot S3 before the run, as B0's S1.
scripts/stack-release.sh activate $LIVE mosaic-stack --pm --no-proof --plan. It must print the note line,proof: dropped; --no-proof drops header X-Mosaic-Proof,migration: none, the backup line, and the Console line equal to FLAGS_OFF.- The real run on Jason's go. Exit 0,
result ok,pm_launched. The steps log names the drop. The journal since that stamp saysProxy: proxy proof offonce. Record the backup path and STAMP. - PB6's matrix again, saved as
$WIN/matrix-back.txt, under PB6's rule: (a)-(g) equalmatrix-new.txtand (h) is 200. Then S4. Against S3, S3's rows are unchanged and the new rows are B6's kinds (1) and (2) again: the stop'srole_claimsrelease andsession.endedfor the chief B3 launched (the id inbackups.txt), then the new PM launch rows. Any other row goes to Mos. - Then Jason takes the middleware off (J6). The order matters: with the middleware on and the proof off, the Console ignores the extra header. With the middleware off and the proof on, every public request gets 401, Jason included.
B-R1 keeps the store as it is, with B3's PM launch rows; it needs no --restore. A restore to the B3 backup is
Mos's call with Jason's go and follows Part A's D3 and --events N steps.
Rollback copies
Prune nothing. Keep every copy, rollback set and backup Part A kept, plus B3's and any B-R1's.
Questions for the plan check
- Q1, J2 against this window. mos-0928 makes the uid separation a precondition. The current code doesn't allow
a proof file owned by any uid but the one that runs it:
proof_file_problemrefuses a file not owned byid -u(stack-release.sh:777),webui-service.sh renderdoes the same (:201), andproofOptionsrefuses a file whose uid isn'tprocess.getuid()(serve.mjs:228). Both scripts drive the Console assystemctl --userunits of jwoltje. So with J2 done first, B3 as written refusesproof-file-invalid, and the Console under another uid needs a design from Jason (which uid, which unit manager, how it reaches the bus) plus a tool row for both scripts. Then this file gets redrafted. The other order is the brief's (row 178 brief :61): this window under jwoltje now, J2 after. With the proof on, only a process that can read SLOT can pass it: a request from another host, a hostNetwork pod or a local process under any other uid gets 401. A process running as jwoltje still can, and so can every seat, but today every seat can already reach 127.0.0.1:7332 with a forged auth header and gets the Console, so the proof narrows the access and never widens it. I recommend that order, with J2 kept open on #385 as its own row. It's Jason's call, since mos-0928 set the precondition. - Q2, PB5 sends a dummy proof value to a scratch Console. It's written for the rehearsal, it isn't the slot value and no cluster object holds it. OK to keep? Mos: yes, as written (mos-0957).
Q1 is on Jason's morning list next to J1-J6, with this recommendation and Mos's reading: Part B under jwoltje first, J2 as its own row after (mos-0957).
What Sage ran for this file
Read only, 2026-10-20 between 10:26Z and 10:29Z for draft 1, and after 10:31Z for draft 2. Nothing live changed.
git fetchandgit rev-parse origin/refactor(a843ae284), the pointer's copy, activation, pm, schema and console fields.git diff --stat 490e2771d a843ae284 -- packages scripts roles contracts adapters skills, and the blobs ofscripts/stack-release.shandscripts/webui-service.shat both ends.stack-release.shata843ae284:check_target,proof_for_target,proof_checks,proof_file_problem,plan,rollback_in_run,from_log.webui-service.sh render(:185-205).serve.mjsproofOptions(:217-240) and the request checks (:476-520).cli.mjs:49,:92and:101.- Draft 2: Part A's
STATE.md(S1, B and S2 table and the endLaunch rows) andmatrix-new.txt, and a grep ofpackages/webui/src/serve.mjsfor store writes on the matrix paths (none).