Files
stack/docs/plans/2026-10-24_case2-proxy-unit-brief.md

5.8 KiB

Brief: the Case 2 proxy unit, so a hub installs P1's proxy unit

Written 2026-10-24 by Sage for Mos's mos-1616 ruling 1, which approved this row. Mos ruled that Sage approves the brief if it only adds P1 item 7's unit to scripts/case2-install.sh and scripts/case2-unit.sh: the same unit text, the same sha256 refusal and the Wants/After lines. Anything beyond that goes to Mos. Read the rows doc's P1 brief (docs/plans/2026-10-24_model-proxy-rows.md, P1 item 7) and row 209's brief (docs/plans/2026-10-24_row209-hub-54-window.md, entry check 2) first. The rows doc's "Rules for every row" apply here unchanged. Line numbers are at origin/refactor 58b2bbb3a, and P1's paths are at its round 2 candidate magica/row329 86cb5c221. Check both once P1 lands.

Brief: Case 2 proxy unit in case2-unit.sh and case2-install.sh

Why. A Case 2 hub (.54) installs through scripts/case2-install.sh (as the stack user) and scripts/case2-unit.sh (as root, system units). P1 adds mosaic-proxy.service only to the user-unit path (scripts/instance.sh, scripts/proxy-service.sh, scripts/bus-service.sh). case2-unit.sh writes the bus and Console units and nothing else (:3-5, :56-57), and its bus unit has no Wants= or After= on a proxy. Without this row, row 209 phase A can't run the proxy on .54.

What ships.

  1. The proxy unit, from P1's template. case2-unit.sh renders a third system unit, mosaic-proxy.service, from P1's packages/cli/systemd/mosaic-proxy.service.in, which this row doesn't change.
    • It uses the same render as the bus (render() :435): the copy for @REPO@, the unit PATH, --node for @NODE@, and @CONFIG_ENV@ deleted, because case2-unit.sh renders the default instance only.
    • It gets the same added lines as the bus: User=, Group= and the four hardening lines, with no capabilities and no EnvironmentFile=. WantedBy=multi-user.target replaces default.target, as for the other two units.
    • check_unit gets a proxy kind. Its fixed lines are those of P1's template: ExecStart= exactly <node> <copy>/packages/model-proxy/src/run.mjs, and NoNewPrivileges=yes. Every check that applies to both units today applies to this one too.
    • A new test seam CASE2_PROXY_TEMPLATE, like CASE2_BUS_TEMPLATE (:44, :54).
  2. The sha256 refusal is P1's. The unit starts P1's run.mjs, which checks the binary against PIN before it spawns it. This row adds no second check and no other way to start the proxy. A test runs the rendered ExecStart= in a temp root, with a made-up config and a binary that doesn't match PIN, and checks P1's refusal code and exit status.
  3. Only when the instance has a proxy. Root passes a new bare flag --proxy when the instance's config has a proxy section.
    • Without it, the render is byte for byte what it is today, and no proxy unit is written.
    • With it, the bus unit gets Wants=mosaic-proxy.service and After=mosaic-proxy.service, in the place where P1's bus-service.sh puts them, right after Description=. The Console unit doesn't change.
    • Root still never imports copy code or reads the config (:16-21). A wrong flag fails closed: with no proxy section, P1's runner refuses and the unit stops. With a proxy section and no flag, the proxy doesn't run and P2's launch preflight refuses. BUILD.md checks both against the landed P1 and P2 code and names the refusal codes.
  4. Re-install and start.
    • With --proxy, the three installed units must name one copy, and each must equal the render once its copy id is the new one, the same rule as now (:557-571).
    • Without --proxy, an installed mosaic-proxy.service refuses the run. The script never deletes a unit file.
    • Enable all of them, then start the proxy, then the bus, then the Console. A unit that was already running is reported the way the bus and Console are now (:576-610), and the message names all three as the units to restart together.
  5. case2-install.sh. Only its closing "Next:" line (:231) changes, to tell root to add --proxy when the config has a proxy section. If the builder finds that line needs no change, BUILD.md says why and the file stays as it is.

Out of scope. P1's template, runner and PIN; the user-unit path; any named-instance render; reading the config from root; the proxy's config or key; mosaic proxy init; any live host, unit or /etc write outside a test's temp root.

Tests, in packages/cli/tests/case2-unit.test.mjs (and case2-install.test.mjs if item 5 changes the script):

  • with no --proxy, the bus and Console renders equal today's, byte for byte;
  • with --proxy, the proxy unit's content, and the bus unit's Wants= and After= lines;
  • each check_unit refusal on the proxy kind, by its message;
  • P1's PIN refusal through the rendered ExecStart=;
  • a re-install whose three units name two copies refuses;
  • an installed proxy unit with no --proxy refuses and deletes nothing;
  • the start order through the CASE2_SYSTEMCTL seam's log.

Gate.

  • The builder is a new Codex gpt-6.1-sol thread and worktree off refactor (Q246, mos-1542). The row starts when P1 (row 329) lands, and the builder rebases on it before any run.
  • Boyd gives the first Codex review. Webby gives the Claude review of the root-run unit boundary and the landing. Sage runs the landing gate.
  • The run list for every whole-file run comes from imports and goes in the builder's report before the run. packages/cli/tests/case1-accept.test.mjs stays off unless Mos approves (mos-1612).
  • Every touched test file runs whole. Every package suite but queue, plus every scripts/test-*.sh, is green on Sage's landing gate.
  • Named mutants: the bus unit loses its After= line under --proxy; the proxy unit's ExecStart= skips run.mjs and starts the binary directly.