Files
stack/agents/sage/work/window-plan/repin-partB-proof.md

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:

  1. Jason's go on Part B (J1).
  2. 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).
  3. Q1 below is settled: the order of J2 and this window.
  4. 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 stat and realpath only. No request in any matrix carries the real value, Jason's user id or any credential.
  • bus.tester stays as it is. No pi (mos-0865 D2, P19 #383). No Save in the human Console. mosaic name stays out.
  • No plain rollback anywhere in Part B. It goes to from_log previous (stack-release.sh:155), the newest ok line whose to is not the pointer's copy. That is still 89af38a83, so it would undo rows 177-185 along with the proof (mos-0954).

Names

  • LIVE: 490e2771d3d057ab76991e2c7859cd251857e64c, activation 20261020T075800Z-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 order webui-service.sh render writes and flags_drift compares.
  • REPO: the canonical checkout. activate runs REPO's stack-release.sh and REPO's webui-service.sh (stack-release.sh:453 at a843ae284). The run uses the row 187 tool from REPO; the copy doesn't need it.
  • TOOL: scripts/stack-release.sh blob 2586a2a37cd2e7c7aeaba6d31490e5b7920fb4b2 at a843ae284. scripts/webui-service.sh is blob d34af09dccdee16430d185826a966785734776e0 at both LIVE and a843ae284.
  • No re-pin and no new copy. git diff --stat 490e2771d a843ae284 -- packages scripts roles contracts adapters skills lists only scripts/stack-release.sh and packages/cli/tests/stack-release.test.mjs. LIVE's Console already takes the pair (row 178, packages/webui/src/cli.mjs:49), so proof_for_target finds 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 passes proof_checks: lowercased it matches ^[a-z0-9][a-z0-9-]{0,63}$ and is none of host, origin, x-mosaic-tester or x-authentik-username.
  • DATA, P, T=7333 and SNAP as in Part A. SNAP is $WIN/snapshot.mjs, sha256 639fd28a2674aaa03ce824e3394b4052f21cdef7d6fa49bae61a9927ed341e67, 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_run put 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

  1. A held snapshot S3 before the run, as B0's S1.
  2. 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.
  3. The real run on Jason's go. Exit 0, result ok, pm_launched. The steps log names the drop. The journal since that stamp says Proxy: proxy proof off once. Record the backup path and STAMP.
  4. PB6's matrix again, saved as $WIN/matrix-back.txt, under PB6's rule: (a)-(g) equal matrix-new.txt and (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's role_claims release and session.ended for the chief B3 launched (the id in backups.txt), then the new PM launch rows. Any other row goes to Mos.
  5. 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_problem refuses a file not owned by id -u (stack-release.sh:777), webui-service.sh render does the same (:201), and proofOptions refuses a file whose uid isn't process.getuid() (serve.mjs:228). Both scripts drive the Console as systemctl --user units of jwoltje. So with J2 done first, B3 as written refuses proof-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 fetch and git 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 of scripts/stack-release.sh and scripts/webui-service.sh at both ends.
  • stack-release.sh at a843ae284: check_target, proof_for_target, proof_checks, proof_file_problem, plan, rollback_in_run, from_log. webui-service.sh render (:185-205). serve.mjs proofOptions (:217-240) and the request checks (:476-520). cli.mjs:49, :92 and :101.
  • Draft 2: Part A's STATE.md (S1, B and S2 table and the endLaunch rows) and matrix-new.txt, and a grep of packages/webui/src/serve.mjs for store writes on the matrix paths (none).