Skip to content

feat(bin): serve review boards on each vessel's own tailnet address - #38

Merged
Freudator86 merged 9 commits into
mainfrom
fm/fleet-lavish-tailnet-access
Jul 30, 2026
Merged

feat(bin): serve review boards on each vessel's own tailnet address#38
Freudator86 merged 9 commits into
mainfrom
fm/fleet-lavish-tailnet-access

Conversation

@Freudator86

Copy link
Copy Markdown
Owner

Intent

Make Lavish review boards reachable from the captain's own devices, permanently, on every vessel. Today every vessel emits a http://127.0.0.1:/... link that opens nothing on his PC or phone, and it fails silently because the board renders correctly on the machine that made it. He caught this himself after this vessel wrote the problem onto a board and then handed him a loopback link to that same board. The ask was to fix the mechanism, not one board.

Constraints the captain set, all binding:

  • Do NOT solve this by publishing boards to third-party hosting (ht-ml.app / lavish-axi share). Boards carry fleet-private material: vessel names, security findings, captain decisions. The transport must stay inside the tailnet.
  • Do not bind 0.0.0.0. An all-interfaces bind was explicitly refused as too broad, even though it would avoid the loopback-probe trap. Bind the specific tailnet address instead.
  • Documentation is not an acceptable fix. ~/.bashrc returns early for non-interactive shells and the agent tool shell loads its own snapshot, so a profile export never reaches an agent's invocation. This had already failed once on agent memory. It had to be a firstmate-owned mechanism that is hard to bypass by habit.
  • No vessel may assume a fixed port: several vessels run on one physical machine as separate UNIX accounts (this host carries three), and lavish-axi's compiled-in default 4387 was already held by a different account.
  • Work had to land in this repo (the pin source). An earlier pass was stopped for editing the fleet checkout, whose drift check refuses edits to vendored paths.
  • Four design decisions were pre-settled by the captain and are deliberate, not oversights: (1) this repository is the target; (2) the PreToolUse guard is scoped to the presence of the wrapper, NOT to the primary checkout, because boards get opened from crew worktrees too and a guard inert where the mistake happens is not a guard - this is a deliberate divergence from bin/fm-cd-pretool-check.sh, which IS primary-checkout-only; (3) refusing the third-party publish path by default WITH an explicit override, so the capability is not removed, only made intentional; (4) build the startup regression check, accepting its instruction-surface cost, because this exact fix already regressed once while queued.

An earlier scout produced a 490-line design (data/fleet-lavish-tailnet-access/design.md, outside this repo). It was followed. Two deliberate departures from it, both on measured evidence: the design attributed the false 'failed to start' to a loopback probe inside Lavish, but reading the installed lavish-axi 0.1.43 shows its own probe already targets the bind host, so the trap lives in our own hand-run verification and the rule became 'any probe we write targets the address we bound'; and the design's ownership test (ss -ltnp plus /proc//environ) was replaced with a per-home claim token written into Lavish's own Host allowlist, because /proc is Linux-only and pid-based identity is recycling-fragile, whereas 'Host: returns 200' is portable and exact.

What was built:

  • bin/fm-service-port.sh: a GENERAL vessel-local service port allocator; Lavish is its first consumer, not its subject, because any vessel-local service has this problem. Deliberate design positions: a successful bind is the ONLY proof a port is free (no registry may be consulted for availability, because no file can stop another UNIX account taking a port, and a stale entry would be one more instrument reporting success while wrong); the record it writes is a published fact, not a lock, and says so in its own bytes; the deterministic seat is keyed on the realpath of FM_HOME rather than the vessel id alone, because a secondmate home shares both the id and the UNIX account with its parent; window 4400-4499 sits above lavish's 4387 default and far below the ephemeral range; exhaustion refuses loudly with a non-zero exit rather than downgrading to loopback, because a silent downgrade would reproduce the original bug somewhere new. An unbindable address is a DISTINCT exit code from a port collision on purpose, so a network-interface problem is not reported as contention.
  • bin/fm-service-port-probe.mjs: the bind/DNS/readiness oracle. bind performs the same net.Server.listen() a Node service will, so its answer is the answer the real service gets; it never holds a port, and that residual race is handled by the consumer retrying rather than pretended away.
  • bin/fm-lavish.sh: the one entry point. Sets HOST to the tailnet address (never a wildcard), PORT from the allocator, LINK_HOST to the tailnet DNS name only after confirming it resolves to the bound address, a closed ALLOWED_HOSTS (never '*'), and a per-home STATE_DIR so a secondmate does not share one server and session store with its parent.

Two failures found by reading the installed lavish-axi are handled explicitly and are the reason for the claim token: (a) ensureServer() decides reuse purely from the /health body and every same-version lavish-axi returns a byte-identical one, so a vessel configured onto a port another UNIX account already serves silently ADOPTS that account's server rather than colliding loudly - measured on this host, our server and a neighbouring account's answer loopback identically, so loopback cannot be the discriminator and a per-home token in the Host allowlist can; (b) the session URL is built inside the server process from the environment it was born with, so exporting a corrected LINK_HOST on a later invocation changes nothing while a healthy server is still running - the wrapper records what it launched a server with and restarts on mismatch, which is safe ONLY because the token already proved the process is this home's.

Adversarial runs changed two behaviours mid-work, both deliberate: a first cut stopped the vessel's own working board BEFORE discovering the new port was unavailable, turning a failed move into a lost board, so relocation now preserves the working board until a replacement port is actually secured; and a proven-own port outside a narrowed FM_SERVICE_PORT_RANGE was being kept forever, so an operator narrowing the window to escape a conflict never took effect - that now counts as staleness, with same-port staleness stopping first (to reclaim the seat) and different-port staleness allocating first (to not lose the board).

Honest degradation is a stated design rule, not an accident: no tailnet falls back to loopback and says in one plain sentence that the board opens only on this machine; an unresolvable tailnet name drops to the bare address with a reason rather than writing an unchecked name into the captain's link; a link host that does not answer after the board opens names the address form that does work.

  • bin/fm-lavish-pretool-check.sh + bin/fm-lavish-command-policy.mjs: the PreToolUse guard, following the four existing sibling guards' transport contract exactly and importing the shell classifier from fm-arm-command-policy.mjs so it never duplicates shell lexing. 'command -v lavish-axi' and 'type lavish-axi' are deliberately ALLOWED because bin/fm-bootstrap.sh does exactly that and denying it would break tool detection to prevent nothing. Wired into .claude/settings.json, .codex/hooks.json, and a new .grok/hooks/ file.
  • bin/fm-brief.sh: one rule line naming the wrapper, because the guard is registered by firstmate's own harness config and therefore structurally cannot reach a worktree of some OTHER project - that limit is stated in the doc rather than papered over.
  • bin/fm-bootstrap.sh: a detect-only LAVISH_ACCESS check. It reads this home's own session store unfiltered, but filters lavish's account-global ~/.lavish-axi store to sessions whose board file lives under THIS home - an earlier version of my own check lacked that filter and blamed one home's boards on another, and broke an unrelated bootstrap test by depending on the developer's real home state.

Instruction surface was kept minimal per firstmate-coding-guidelines: AGENTS.md grew 5 net lines (two lavish-axi mentions became the wrapper, one section 13 diagnostic prefix, two state-layout lines whose point is that the port record is NEVER a lock), with the owner procedure in the bootstrap-diagnostics skill and the reasoning in docs/lavish-access.md.

Acceptance status, reported honestly: two vessels on one machine serving concurrently without a clash is PROVEN with real requests (ports 4411 and 4484, both HTTP 200, both bound tailnet-only, no coordination between them). The second criterion - an HTTP 200 fetched from a DIFFERENT device on the tailnet - is NOT completed and is explicitly recorded as an open gap in docs/lavish-access.md: the other Linux nodes refuse SSH and advertise no Tailscale SSH host key, and the captain's PC and phone cannot be driven from a crewmate. Everything on this side of the wire is proven instead. That gap is deliberate and disclosed, not an omission.

The cross-vessel port registry (docs/fleet-service-port-registry.md) is deliberately a WRITTEN PROPOSAL ONLY and is not implemented or wired to anything: it changes a shared schema in coditan-bridge that other vessels depend on, and the captain's instruction was to put it to them through Bridge before it binds. It also records that the roster's host field cannot detect co-hosting at all, since three vessels claim 'local-wsl'.

One thing deliberately NOT solved and stated rather than implied: a board remains an unauthenticated server readable by anything on the tailnet. That is the fleet's existing trust boundary; this work does not widen it (specific-address bind, closed allowlist, nothing published outside the tailnet) and does not close it either. No authentication was added, on purpose.

What Changed

  • Adds bin/fm-lavish.sh as the single entry point for opening a Lavish review board: it binds the vessel's own tailnet address (never a wildcard), writes the tailnet DNS name into the captain's link only after confirming it resolves to that address, keeps a closed ALLOWED_HOSTS and a per-home state dir, proves server ownership with a per-home claim token in Lavish's Host allowlist (so co-hosted vessels neither adopt nor stop each other's servers, including stop --port), refuses the third-party share path unless --fm-allow-share is passed, and degrades honestly — no tailnet, an unresolvable name, or an unanswering link host each say so in one plain sentence instead of handing over a link that opens nothing. Board-less flag invocations pass straight through, and the "board is serving" note fires only when the run actually emitted a session URL.
  • Adds a general vessel-local service port allocator, bin/fm-service-port.sh with the bin/fm-service-port-probe.mjs bind/DNS/readiness oracle: a successful bind is the only proof a port is free, the seat is keyed on the realpath of FM_HOME, the window is 4400-4499, the record written is a published fact rather than a lock, an unbindable address exits distinctly from port contention, and exhaustion refuses loudly rather than downgrading to loopback. Relocation keeps the working board until a replacement port is secured, and a proven-own port outside a narrowed FM_SERVICE_PORT_RANGE now counts as stale.
  • Wires the mechanism so habit cannot bypass it: a new PreToolUse guard (bin/fm-lavish-pretool-check.sh + bin/fm-lavish-command-policy.mjs, reusing the arm guard's shell classifier) denies bare lavish-axi board opens — including flag-led ones — while allowing command -v/type detection and non-serving subcommands, registered in .claude/settings.json, .codex/hooks.json, .grok/hooks/, .opencode/plugins/, and the Pi turn-end extension; bin/fm-brief.sh names the wrapper in worker rules and bin/fm-bootstrap.sh gains a detect-only LAVISH_ACCESS check. Covered by tests/fm-lavish-access.test.sh and an extended tests/fm-bootstrap.test.sh, with the reasoning in docs/lavish-access.md — which also records at the top that the off-device HTTP 200 criterion could not be produced from this host and stays unverified — and docs/fleet-service-port-registry.md as a written proposal only, not implemented or wired.

Risk Assessment

✅ Low: The required honest-degradation warning is now stated up front and no longer depends on any third-party output shape, the link claim stays correctly gated on an observed open, the remaining shape dependency is pinned against the installed tool with a visible skip, and the only remaining item is a misleading failure message inside that new test.

Testing

Ran the two owning suites (tests/fm-lavish-access.test.sh, tests/fm-bootstrap.test.sh) green with no gate skips, then verified the change the way the captain will meet it: two scratch vessels on this one machine each opened a real board through bin/fm-lavish.sh, landed on separate ports with no coordination, bound only the tailnet address, answered HTTP 200 over the MagicDNS link they emitted, refused loopback on those same ports, and were each distinguished from the other by their own claim token; a real browser then loaded vessel A's tailnet link and rendered the board. For the round-1 fix I rendered docs/lavish-access.md and screenshotted the first viewport, which shows the unproven off-device criterion stated immediately under the intro rather than buried at the end. The off-device 200 still cannot be produced from this host and remains the disclosed open gap only the captain can close; everything else is proven with live requests, and all servers, scratch homes, and browser processes were cleaned up with the worktree left clean.

  • Evidence: Rendered docs/lavish-access.md, first viewport - the unproven criterion now sits directly under the intro (local file: /home/coditan/.cache/no-mistakes-tmp/no-mistakes-evidence/01KYRESJWR6WTNZ7NX9HD7MFBC/doc-top-disclosure.png)
  • Evidence: Vessel A's board rendered in a real browser over its wrapper-emitted tailnet URL (local file: /home/coditan/.cache/no-mistakes-tmp/no-mistakes-evidence/01KYRESJWR6WTNZ7NX9HD7MFBC/board-over-tailnet.png)
Evidence: Two vessels on one machine opening boards through the wrapper - tailnet links, distinct ports

$ FM_HOME=<scratch>/vesselA bin/fm-lavish.sh $FM_HOME/.lavish/board.html --no-open session: url: "http://crew-hlr.tail7b8448.ts.net:4478/session/33b59da25c9ed50a&#34;&#10;status: opened exit=0 $ FM_HOME=<scratch>/vesselB bin/fm-lavish.sh $FM_HOME/.lavish/board.html --no-open session: url: "http://crew-hlr.tail7b8448.ts.net:4443/session/cfc23645cad98547&#34;&#10;status: opened exit=0

### vessel A: open a board through the wrapper
$ FM_HOME=/tmp/fm-lavish-e2e.ye9krW/vesselA bin/fm-lavish.sh $FM_HOME/.lavish/board.html --no-open
session:
  file: /tmp/fm-lavish-e2e.ye9krW/vesselA/.lavish/board.html
  url: "http://crew-hlr.tail7b8448.ts.net:4478/session/33b59da25c9ed50a"
  status: opened
exit=0

### vessel B: a second vessel on the same machine, no coordination
$ FM_HOME=/tmp/fm-lavish-e2e.ye9krW/vesselB bin/fm-lavish.sh $FM_HOME/.lavish/board.html --no-open
session:
  file: /tmp/fm-lavish-e2e.ye9krW/vesselB/.lavish/board.html
  url: "http://crew-hlr.tail7b8448.ts.net:4443/session/cfc23645cad98547"
  status: opened
exit=0
Evidence: Reachability proof: tailnet-only bind, 200 over the MagicDNS name, loopback refused, claim token, peer path

$ ss -ltnH | awk '$4 ~ /:(4478|4443)$/ {print $1, $4}' LISTEN 100.121.172.63:4443 LISTEN 100.121.172.63:4478 $ curl -o /dev/null -w %{http_code} http://crew-hlr.tail7b8448.ts.net:4478/session/33b59da25c9ed50a&#10;200&#10;$ curl -o /dev/null -w %{http_code} http://crew-hlr.tail7b8448.ts.net:4443/session/cfc23645cad98547&#10;200&#10;&#10;$ curl http://127.0.0.1:4478/health -> connection refused (exit 7) $ curl http://127.0.0.1:4443/health -> connection refused (exit 7) $ node bin/fm-service-port-probe.mjs resolve crew-hlr.tail7b8448.ts.net 100.121.172.63 -> exit=0 $ curl -H 'Host: <A claim token>' http://100.121.172.63:4478/health -> 200 $ curl -H 'Host: <A claim token>' http://100.121.172.63:4443/health -> 403 $ curl -H 'Host: <B claim token>' http://100.121.172.63:4443/health -> 200 $ tailscale ping --c 1 timbook pong from timbook (100.101.182.48) via 191.177.192.184:6299 in 177ms

### both boards listen on the tailnet address only - no loopback, no wildcard
$ ss -ltnH | awk '$4 ~ /:(4478|4443)$/ {print $1, $4}'
LISTEN 100.121.172.63:4443
LISTEN 100.121.172.63:4478

### an HTTP fetch over the tailnet DNS name the captain receives
$ curl -s -o /dev/null -w %{http_code} http://crew-hlr.tail7b8448.ts.net:4478/session/33b59da25c9ed50a
200
$ curl -s -o /dev/null -w %{http_code} http://crew-hlr.tail7b8448.ts.net:4443/session/cfc23645cad98547
200

### the same ports on loopback - not served, which is why a 127.0.0.1 link was dead
$ curl -s -o /dev/null -w %{http_code} http://127.0.0.1:4478/health
000
connection refused (exit 7)
$ curl -s -o /dev/null -w %{http_code} http://127.0.0.1:4443/health
000
connection refused (exit 7)

### the emitted hostname resolves to the address that was bound
$ node bin/fm-service-port-probe.mjs resolve crew-hlr.tail7b8448.ts.net 100.121.172.63; echo $?
exit=0

### the per-home claim token identifies whose server answers a port
$ curl -H "Host: <A claim token>" http://100.121.172.63:4478/health -> 200
$ curl -H "Host: <A-token-on-B claim token>" http://100.121.172.63:4443/health -> 403
$ curl -H "Host: <B claim token>" http://100.121.172.63:4443/health -> 200

### the tailnet path to the captain's own PC answers from this vessel
$ tailscale ping --c 1 timbook
pong from timbook (100.101.182.48) via 191.177.192.184:6299 in 177ms
Evidence: What an agent sees when it reaches for bare lavish-axi out of habit

$ bin/fm-lavish-pretool-check.sh --command 'lavish-axi .lavish/board.html' {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny"},"systemMessage":"[bare-lavish-axi] bare lavish-axi is blocked in a firstmate checkout because it binds loopback and writes a http://127.0.0.1 link that opens nothing on the captain's own devices, and it fails silently. Run &lt;checkout&gt;/bin/fm-lavish.sh with the same arguments instead ..."} exit=2 $ bin/fm-lavish-pretool-check.sh --command 'bin/fm-lavish.sh .lavish/board.html' -> exit=0 $ bin/fm-lavish-pretool-check.sh --command 'command -v lavish-axi' -> exit=0

### what an agent sees when it reaches for the old command out of habit
$ bin/fm-lavish-pretool-check.sh --command 'lavish-axi .lavish/board.html'; echo $?
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny"},"systemMessage":"[bare-lavish-axi] bare `lavish-axi` is blocked in a firstmate checkout because it binds loopback and writes a http://127.0.0.1 link that opens nothing on the captain's own devices, and it fails silently. Run `/home/coditan/.no-mistakes/worktrees/6e487fc7bf03/01KYRESJWR6WTNZ7NX9HD7MFBC/bin/fm-lavish.sh` with the same arguments instead: it resolves this vessel's tailnet address and a port no co-hosted vessel is using, and it says so plainly when there is no tailnet rather than emitting a link that will not open."}
{"decision":"deny","reason":"[bare-lavish-axi] bare `lavish-axi` is blocked in a firstmate checkout because it binds loopback and writes a http://127.0.0.1 link that opens nothing on the captain's own devices, and it fails silently. Run `/home/coditan/.no-mistakes/worktrees/6e487fc7bf03/01KYRESJWR6WTNZ7NX9HD7MFBC/bin/fm-lavish.sh` with the same arguments instead: it resolves this vessel's tailnet address and a port no co-hosted vessel is using, and it says so plainly when there is no tailnet rather than emitting a link that will not open."}
exit=2

$ bin/fm-lavish-pretool-check.sh --command 'bin/fm-lavish.sh .lavish/board.html'; echo $?
exit=0

$ bin/fm-lavish-pretool-check.sh --command 'command -v lavish-axi'; echo $?   # tool detection still works
exit=0
Evidence: Rendered docs/lavish-access.md (open in a browser to read the full page as a reviewer would)
<!doctype html><html><head><meta charset="utf-8"><title>docs/lavish-access.md</title><style>
body{margin:0;background:#f6f8fa;font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;color:#1f2328}
.chrome{max-width:1012px;margin:24px auto;background:#fff;border:1px solid #d1d9e0;border-radius:6px}
.filebar{padding:8px 16px;border-bottom:1px solid #d1d9e0;background:#f6f8fa;font:13px ui-monospace,SFMono-Regular,Menlo,monospace;color:#59636e;border-radius:6px 6px 0 0}
.md{padding:32px 40px}h1{font-size:2em;padding-bottom:.3em;border-bottom:1px solid #d1d9e0;margin-top:0}
h2{font-size:1.5em;padding-bottom:.3em;border-bottom:1px solid #d1d9e0;margin-top:24px}h3{font-size:1.25em}
code{background:#eff1f3;padding:.2em .4em;border-radius:6px;font:85% ui-monospace,SFMono-Regular,Menlo,monospace}
pre{background:#f6f8fa;padding:16px;border-radius:6px;overflow:auto}pre code{background:none;padding:0}
table{border-collapse:collapse}th,td{border:1px solid #d1d9e0;padding:6px 13px}strong{font-weight:600}
</style></head><body><div class="chrome"><div class="filebar">docs/lavish-access.md</div><div class="md">
<h1>Reachable review boards</h1>
<p>Lavish review boards are how firstmate hands the captain a decision surface.
A bare <code>lavish-axi</code> binds loopback and writes <code>http://127.0.0.1:&lt;port&gt;/...</code> into the link it hands over, which opens nothing on his PC or phone.
The failure is silent: the board renders correctly on the machine that made it, so nothing signals that the link is dead everywhere else.</p>
<p><code>bin/fm-lavish.sh</code> is the entry point that fixes it, <code>bin/fm-service-port.sh</code> is the general port allocator underneath it, and <code>bin/fm-lavish-pretool-check.sh</code> is the guard that stops an agent reaching for the old command out of habit.
Each script&#39;s header owns its exact flags and mechanics; this document owns the contract between them, the reasoning, and the validation record.</p>
<h2>Read this first: one acceptance criterion is not proven</h2>
<p>The second acceptance criterion - an HTTP 200 for a board fetched from a <em>different</em> device on the tailnet - is <strong>not proven</strong>.
It could not be proven from the machine this was built on: the other Linux nodes reject <code>tailscale ssh</code> with no known host key, and the captain&#39;s PC and phone cannot be driven by an agent.</p>
<p>What is proven is everything on this side of the wire: the server binds only the tailnet address and loopback is not served, the emitted hostname resolves to that address, a request carrying the tailnet name is accepted by the Host allowlist and returns 200, and the tailnet peer path to the captain&#39;s PC answers.
So the mechanism is demonstrated up to the last hop, and the last hop is the part nobody here can perform.</p>
<p>One fetch of a wrapper-emitted link, from the captain&#39;s own PC or phone, closes it.
The phone is the case worth doing specifically, for the reason recorded under &quot;Open gap: the off-device request&quot; at the end of this document, which also carries the full attempt log.</p>
<h2>Why this could not be a documented environment variable</h2>
<p><code>~/.bashrc</code> returns immediately for non-interactive shells, and an agent tool shell loads its own snapshot instead, so an operator&#39;s profile export never reaches an agent&#39;s invocation.
That route had already been tried here and had already failed once: the problem was written onto a review board, and the link to that same board went out on loopback.
The fix therefore has to be a firstmate-owned mechanism that is hard to bypass by habit, not a note somebody has to remember.</p>
<h2>What the entry point sets</h2>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Value</th>
<th>Why</th>
</tr>
</thead>
<tbody><tr>
<td><code>LAVISH_AXI_HOST</code></td>
<td>this vessel&#39;s tailnet IPv4 address</td>
<td>reachable off this machine, and never a wildcard, because an all-interfaces bind is broader than was approved</td>
</tr>
<tr>
<td><code>LAVISH_AXI_PORT</code></td>
<td>a port proved free by <code>bin/fm-service-port.sh</code></td>
<td>vessels sharing one machine do not collide</td>
</tr>
<tr>
<td><code>LAVISH_AXI_LINK_HOST</code></td>
<td>the tailnet DNS name, but only once it has been confirmed to resolve to the bound address; otherwise the address</td>
<td>the hostname the captain actually receives is checked before he receives it</td>
</tr>
<tr>
<td><code>LAVISH_AXI_ALLOWED_HOSTS</code></td>
<td>this vessel&#39;s DNS name, short node name, address, and this home&#39;s claim token; never <code>*</code></td>
<td>a closed Host allowlist rather than an open one</td>
</tr>
<tr>
<td><code>LAVISH_AXI_STATE_DIR</code></td>
<td><code>FM_HOME/state/lavish</code></td>
<td>a secondmate home shares its UNIX account with its parent, so without this they share one server and one session store</td>
</tr>
</tbody></table>
<p>Every value is resolved at runtime from <code>tailscale status --json</code>.
Nothing vessel-specific is compiled in, which is what makes this work on every vessel including secondmate homes rather than only on the one it was built on.</p>
<h2>The three failures this design is built around</h2>
<p>Each was measured on <code>crew-hlr</code> on 2026-07-30, not inferred.</p>
<h3>A fixed default port is contended across UNIX accounts</h3>
<p>This host carries three accounts (<code>coditan</code>, <code>crew</code>, <code>tugboat</code>).
<code>lavish-axi</code>&#39;s compiled-in default is 4387, and on this host 4387 was already held by a different account by something that is not even an HTTP server.
No vessel may assume a fixed port, so <code>bin/fm-service-port.sh</code> derives a deterministic preferred seat and probes forward from it.</p>
<h3>A same-version neighbour is silently adopted, not refused</h3>
<p><code>ensureServer()</code> decides whether to reuse an existing server purely from the <code>/health</code> body, and <code>shouldRestartServer</code> returns false when the version matches.
Every <code>lavish-axi</code> of the same version returns a byte-identical body, so a vessel configured onto a port another UNIX account already serves does not collide loudly - it adopts that account&#39;s server process and emits a URL pointing at it.</p>
<p>This is why <code>bin/fm-lavish.sh</code> writes a per-home <strong>claim token</strong> into the Host allowlist.
A request carrying <code>Host: &lt;token&gt;</code> is accepted only by a server that was started with that token, so one probe answers &quot;is the server on my port MY server&quot;, which no shared health body can answer.
The token adds no exposure: anything that can already reach the port can already reach it under the address, and the token only makes one specific Host header identify one specific home&#39;s server.</p>
<h3>Setting the environment at call time does not fix an already-running server</h3>
<p>The session URL is built inside the server process from the environment it was born with, and the server is spawned detached by whichever invocation first started it.
So exporting a corrected <code>LAVISH_AXI_LINK_HOST</code> on a later invocation changes nothing while a healthy server is still running on that port.
<code>bin/fm-lavish.sh</code> records the configuration it launched a server with, compares it on the next run, and restarts its own server on a mismatch - which is safe only because the claim token has already proved the process belongs to this home.</p>
<p>Stopping reaches into a running process the same way, so it carries the same proof, and an explicitly named port is no exception.
<code>lavish-axi stop</code> shuts down whatever answers <code>/health</code> with a lavish-axi body on the address it is handed, without authentication and without regard for the owning UNIX account, and every co-hosted vessel binds that same address.
So <code>bin/fm-lavish.sh stop --port &lt;n&gt;</code> runs the claim-token probe against <code>&lt;n&gt;</code> before it reaches <code>lavish-axi</code> at all: a port that answers but is not this home&#39;s is refused by name and left running, and a port that answers noth

... [15711 bytes truncated] ...

<li><strong>Secondmate homes</strong> - covered by construction: the seat key includes the realpath of <code>FM_HOME</code>, and <code>LAVISH_AXI_STATE_DIR</code> is per home, so a secondmate and its parent never share a seat or a session store despite sharing a UNIX account.</li>
<li><strong>Hosts without tailscale</strong> - covered by the loopback degradation path, the only branch where behaviour matches today&#39;s, apart from the honest warning.</li>
<li><strong>Hosts without <code>jq</code></strong> - the allocator degrades to <code>reachability=loopback</code> with a reason, and the startup check stays silent.</li>
</ul>
<h2>Validation record</h2>
<p>Host <code>crew-hlr</code>, tailnet <code>tail7b8448.ts.net</code>, address <code>100.121.172.63</code>, UNIX account <code>coditan</code>, <code>lavish-axi</code> 0.1.43, 2026-07-30.</p>
<h3>Two vessels on one machine, no clash</h3>
<p>Two scratch homes, no coordination between them, each opening a board through the wrapper:</p>
<pre><code>$ FM_HOME=&lt;scratch&gt;/vesselA bin/fm-lavish.sh &lt;scratch&gt;/vesselA/.lavish/board.html --no-open
  url: &quot;http://crew-hlr.tail7b8448.ts.net:4411/session/4cedced659385d92&quot;
$ FM_HOME=&lt;scratch&gt;/vesselB bin/fm-lavish.sh &lt;scratch&gt;/vesselB/.lavish/board.html --no-open
  url: &quot;http://crew-hlr.tail7b8448.ts.net:4484/session/7a4994b5c3bdd3fe&quot;

$ ss -ltnH | awk &#39;$4 ~ /:(44[0-9][0-9])$/ {print $4}&#39;
100.121.172.63:4484
100.121.172.63:4411

$ curl -s -o /dev/null -w &#39;%{http_code}&#39; http://crew-hlr.tail7b8448.ts.net:4411/session/4cedced659385d92
200
$ curl -s -o /dev/null -w &#39;%{http_code}&#39; http://crew-hlr.tail7b8448.ts.net:4484/session/7a4994b5c3bdd3fe
200
</code></pre>
<p>Both bind the tailnet address only, on different ports, and both serve concurrently.</p>
<h3>The bind probe is authoritative across UNIX accounts</h3>
<pre><code>$ node bin/fm-service-port-probe.mjs bind 100.121.172.63 4388 4387 4392 4405
4405
$ node bin/fm-service-port-probe.mjs bind 100.121.172.63 4392 ; echo $?
3
$ node bin/fm-service-port-probe.mjs bind 10.9.9.9 4400 ; echo $?
fm-service-port-probe: cannot bind 10.9.9.9 on this host (EADDRNOTAVAIL)
4
</code></pre>
<p>4388 was this account&#39;s own server, 4387 and 4392 were held by other accounts, and 4392 was a wildcard bind - all three were correctly refused, and the unusable address was reported as exit 4 rather than as a collision.</p>
<h3>The claim token distinguishes our server from a neighbour&#39;s</h3>
<pre><code>$ curl -s -o /dev/null -w &#39;%{http_code}&#39; -H &#39;Host: crew-hlr&#39;            http://100.121.172.63:4388/health
200
$ curl -s -o /dev/null -w &#39;%{http_code}&#39; -H &#39;Host: crew-hlr&#39;            http://100.121.172.63:4392/health
403
$ curl -s -o /dev/null -w &#39;%{http_code}&#39; -H &#39;Host: fm-claim-testnonce123&#39; http://100.121.172.63:4388/health
403
$ curl -s -o /dev/null -w &#39;%{http_code}&#39; -H &#39;Host: 127.0.0.1&#39;           http://100.121.172.63:4392/health
200
</code></pre>
<p>4392 is another account&#39;s server, and it answers loopback identically to ours - which is exactly why loopback cannot be the discriminator and a per-home token can.</p>
<h3>A neighbour&#39;s server is refused, and the working board survives the refusal</h3>
<p>Vessel B pointed at the port vessel A&#39;s server already holds, using the real wrapper end to end:</p>
<pre><code>$ FM_HOME=&lt;scratch&gt;/vesselB FM_SERVICE_PORT_RANGE=4411-4411 bin/fm-lavish.sh &lt;board&gt; --no-open
SERVICE_PORT: no free port in 4411-4411 on 100.121.172.63 for lavish; every candidate is held by another process, ...
fm-lavish: the board already serving on port 4484 was left running, so nothing was lost
fm-lavish: no port is available for a review board on this vessel, so no board was opened
exit=5

$ ss -ltnH | awk &#39;$4 ~ /:(44[0-9][0-9])$/ {print $4}&#39;
100.121.172.63:4484
100.121.172.63:4411
$ curl -s -o /dev/null -w &#39;%{http_code}&#39; http://crew-hlr.tail7b8448.ts.net:4484/session/...
200
</code></pre>
<p>No adoption, no board, and vessel B&#39;s existing board still serving.
The first attempt at this design did stop B&#39;s own server before discovering the new port was unavailable, which is why the preserve-before-move rule above exists.</p>
<h3>Reuse, restart, and relocation</h3>
<pre><code>$ FM_HOME=&lt;scratch&gt;/vesselB bin/fm-lavish.sh &lt;board&gt; --no-open        # stale link host recorded
fm-lavish: the running board server was started with a different address configuration; restarting it so links are correct
  url: &quot;http://crew-hlr.tail7b8448.ts.net:4484/session/...&quot;           # same port reclaimed

$ FM_HOME=&lt;scratch&gt;/vesselB FM_SERVICE_PORT_RANGE=4470-4479 bin/fm-lavish.sh &lt;board&gt; --no-open
fm-lavish: moving this vessel&#39;s boards from port 4484 to 4474; links already handed over on the old port stop working
  url: &quot;http://crew-hlr.tail7b8448.ts.net:4474/session/...&quot;

$ FM_HOME=&lt;scratch&gt;/vesselB bin/fm-lavish.sh stop       # -&gt; status: stopped, port: 4474
$ FM_HOME=&lt;scratch&gt;/vesselB bin/fm-lavish.sh stop       # -&gt; nothing to stop
</code></pre>
<p>A second open of an unchanged board reuses the running server and leaves exactly one listener.</p>
<h3>The loopback readiness trap, reproduced</h3>
<pre><code>$ node bin/fm-service-port-probe.mjs http http://127.0.0.1:4411/health ; echo $?
fm-service-port-probe: http://127.0.0.1:4411/health did not answer (connect ECONNREFUSED 127.0.0.1:4411)
1
$ node bin/fm-service-port-probe.mjs http http://100.121.172.63:4411/health ; echo $?
200
0
</code></pre>
<p>The server on 4411 is healthy and serving throughout; loopback simply is not where it lives.</p>
<h3>Guard behaviour</h3>
<pre><code>$ bin/fm-lavish-pretool-check.sh --command &#39;lavish-axi board.html&#39; ; echo $?
{&quot;hookSpecificOutput&quot;:{...,&quot;permissionDecision&quot;:&quot;deny&quot;},&quot;systemMessage&quot;:&quot;[bare-lavish-axi] ...&quot;}
{&quot;decision&quot;:&quot;deny&quot;,&quot;reason&quot;:&quot;[bare-lavish-axi] ...&quot;}
2
$ bin/fm-lavish-pretool-check.sh --claude --command &#39;lavish-axi board.html&#39; &gt;/dev/null ; echo $?
2
$ bin/fm-lavish-pretool-check.sh --command &#39;bin/fm-lavish.sh board.html&#39; ; echo $?
0
$ FM_ROOT_OVERRIDE=/tmp bin/fm-lavish-pretool-check.sh --command &#39;lavish-axi x.html&#39; ; echo $?
0
</code></pre>
<p>Claude&#39;s <code>--claude</code> deny leaves stdout empty, Grok&#39;s stdin shape returns the stdout decision object, and a checkout without the wrapper is inert.
<code>tests/fm-lavish-access.test.sh</code> owns the full acceptance matrix.</p>
<h3>Open gap: the off-device request</h3>
<p>Acceptance asked for an HTTP 200 fetched from a different device on the tailnet.
That was not completed from this task&#39;s worktree: the two other Linux nodes (<code>aurora</code>, <code>crew-allesknut</code>) refuse SSH with <code>Permission denied (publickey,password)</code> and do not advertise a Tailscale SSH host key, and the captain&#39;s own <code>timbook</code> and <code>s26-ultra-von-tim</code> cannot be driven from here.</p>
<p>What was proved instead is everything on this side of the wire: the server binds only the tailnet address (loopback is refused), the emitted hostname resolves to that address, a request carrying the tailnet name is accepted by the Host allowlist and returns 200, and <code>tailscale ping timbook</code> answers directly.
The remaining check is one request from the captain&#39;s PC or phone against a link the wrapper emits.
The phone is the one worth doing specifically: the MagicDNS name carries both an A and an AAAA record while Lavish binds one address, so a v6-preferring client reaches a closed port first and has to recover through Happy Eyeballs.</p>
<h2>Related</h2>
<ul>
<li><code>docs/fleet-service-port-registry.md</code> - the cross-vessel published-record proposal, which is a proposal and not binding.</li>
<li><code>docs/cd-guard.md</code>, <code>docs/arm-pretool-check.md</code>, <code>docs/subagent-guard.md</code> - the sibling PreToolUse guards this one follows.</li>
</ul>

</div></div></body></html>
Evidence: tests/fm-lavish-access.test.sh full run output (42 assertions, no gate skips)
FM_TEST_BEGIN 2026-07-30T04:17:15Z tests/fm-lavish-access.test.sh family=session-bootstrap expected_gate_skip=none
ok - the bind probe treats a real listener as authoritative and reports exhaustion distinctly
ok - an unusable address is reported apart from a port collision
ok - a refused port keeps the walk going and is counted apart from a held one
ok - the resolve mode refuses a name that does not point at the bound address
ok - one home gets the same deterministic seat every run
ok - two co-hosted homes are separated without talking to each other
ok - a shared vessel id does not collapse two homes onto one seat
ok - --check resolves identity without claiming a port or writing a record
ok - the record names itself a published fact so nobody mistakes it for an allocator
ok - a held seat advances, and the record reports the port actually taken
ok - an exhausted window refuses loudly instead of downgrading silently
ok - a port the caller already owns is returned without a self-defeating bind probe
ok - a vessel with no usable tailnet says so concretely instead of implying reach
ok - a tailnet name that does not resolve to the bound address is dropped, not written into a link
ok - malformed input is refused rather than guessed at
ok - the entry point hands lavish-axi a reachable address, a free port, and a closed allowlist
ok - the claim token is one durable identity per home
ok - poll and other follow-ups resolve the same server the open call did
ok - a flag-led invocation carrying no board file claims nothing and reports on nothing
ok - a flag-led invocation that does carry a board file takes the full open path
ok - an argv that dispatches open but opens nothing makes no claim about a link
ok - a genuine open still gets every line the captain needs about its link
ok - the shape the wrapper reads as an open is the shape the installed lavish-axi prints
ok - a vessel with no tailnet still opens the board and never implies it is reachable
ok - publishing outside the tailnet requires stated intent, and stays available with it
ok - stop refuses to reach a server this vessel has not proved is its own
ok - the guard denies the invocation that emits an unreachable link and nothing else
ok - non-serving subcommands stay available, because denying them prevents nothing
ok - a version or help flag never buys an invocation past the guard
ok - the install commands this repo itself emits are not denied by its own guard
ok - every tracked harness entry and output shape is honoured
ok - a broken transport never denies a shell command
ok - the guard is scoped to the wrapper's presence, so it covers crew worktrees too
ok - the guard is registered on all five tracked harness surfaces, with Pi's crew limit stated rather than rounded up
ok - the instruction surface names one owner and one entry point
ok - a vessel reuses its own running board server instead of starting another
ok - a same-version server belonging to another account is refused, never silently adopted
ok - a server started with a stale configuration is restarted rather than reused
ok - a failed relocation leaves the working board untouched
ok - a successful relocation reports the consequence for links already handed over
ok - stop reaches this vessel's own server and then reports honestly that none is left
ok - stop --port is held to the same ownership proof as a bare stop
FM_TEST_END 2026-07-30T04:17:31Z tests/fm-lavish-access.test.sh exit=0 duration_ms=15810 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=15900
FM_TEST_SUMMARY_FAMILY family=session-bootstrap count=1 duration_ms=15810 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-lavish-access.test.sh duration_ms=15810
- Outcome: ⚠️ 1 info across 2 runs (27m35s)

Pipeline

Updates from git push no-mistakes

... (3 earlier update rounds omitted to keep the PR body within GitHub's 65536-char limit; full history is in the run log.)

⚠️ **Review** - 1 info

🔧 Fix: close stop --port ownership hole and wire lavish guard everywhere
3 issues (2 warnings, 1 info) still open:

  • ⚠️ bin/fm-lavish-command-policy.mjs:38 - The version and help flags in NON_SERVING let a board-opening invocation through the guard. Verified by invoking the policy: lavish-axi --version board.html -> allow, lavish-axi -v /tmp/b.html -> allow, lavish-axi --help board.html -> allow, lavish-axi -h board.htm -> allow. Installed lavish-axi 0.1.43's normalizeArgv (dist/cli.mjs:7449-7456) reads: if the first argument starts with -, it returns [&#34;open&#34;, ...argv] whenever any argument satisfies isHtmlPath. So each of those commands runs open and emits exactly the http://127.0.0.1 link this guard exists to prevent. The premise stated in the new comment at bin/fm-lavish-command-policy.mjs:69-71 - "Mirror lavish-axi's own dispatch: the first argument decides the command, and a first argument that is not a known subcommand is the board file" - is where the divergence sits: for a flag-led argv the first argument does NOT decide the command. The four subcommand entries are safe and I confirmed why: export (dist/cli.mjs:7695), playbook, design, and setup are all in lavish's own COMMANDS set (line 7369), so an HTML argument cannot promote them to open, and none of them calls ensureServer. Narrowest fix that still honours the instruction to allow --version/-v: mirror normalizeArgv by allowing the flag entries only when no word in the invocation looks like an HTML path, and add the flag-plus-board shapes to the deny cases in tests/fm-lavish-access.test.sh alongside the existing allow cases.
  • ⚠️ docs/lavish-access.md:147 - Pi's new registration does not reach the crew worktrees that motivated the guard's wider scope, so the amended coverage prose overstates reach again. bin/fm-spawn.sh:466-472 launches Pi with -e __PITURNEND__ -e __PIWATCH__ only when kind = secondmate; for a ship or scout it launches -e __PIEXT__, which resolves to $STATE/&lt;id&gt;.pi-ext.ts (bin/fm-spawn.sh:1328). That generated extension is written at bin/fm-spawn.sh:1188-1197 and registers a single turn_end handler with no tool_call hook, so a Pi crewmate or scout never loads .pi/extensions/fm-primary-turnend-guard.ts and never runs runLavishCheck. The lavish guard on Pi therefore covers the primary session and Pi secondmate homes, not crew worktrees. Two statements read as claiming otherwise: docs/lavish-access.md:147 says registering only the three JSON surfaces "would leave the guard inert on two harnesses crewmates are spawned on, which is the same 'inert exactly where the mistake happens' failure the scope decision above exists to avoid", and the amended comment at .pi/extensions/fm-primary-turnend-guard.ts:86-88 says "the lavish guard fires wherever bin/fm-lavish.sh exists, including a crew worktree" - true of the owner script's own scope, but not of what Pi delivers from this file. OpenCode is genuinely covered: its crewmate launch (bin/fm-spawn.sh:465) runs in the worktree and .opencode/plugins/ is auto-discovered from the project root. Either state Pi's limit precisely in the doc and the comment, or add the tool_call lavish check to the generated per-task extension so Pi crewmates are actually covered.
  • ℹ️ bin/fm-service-port.sh:325 - The new exit-5 branch states both causes as fact when the probe only proves one of them. bin/fm-service-port-probe.mjs:143 returns 5 whenever refused &gt; 0 after the walk, regardless of whether any candidate was actually EADDRINUSE, so an all-refused window (for example FM_SERVICE_PORT_RANGE=80-89 under an unprivileged account, every candidate EACCES) still produces "some candidates are held by another process and this host refused others outright" - a concrete diagnosis that is untrue, which is the same class the DNS-reason fix in this commit just corrected. Compounding it, the probe header (lines 36-40) promises "Each such port is named on stderr", but the only caller discards that stderr at bin/fm-service-port.sh:306 (node &#34;$PROBE&#34; bind &#34;$ADDR&#34; $CANDIDATES 2&gt;/dev/null), so the actual errno never reaches a human. An unknown errno now also counts as refused, so a systemic per-attempt failure such as EMFILE walks the whole window and is reported as "FM_SERVICE_PORT_RANGE names a window this account may bind", sending the operator at the port window rather than the real cause. Either have the probe report the taken and refused counts (or the distinct errnos) so the allocator names what happened, or soften the wording to cover both cases without asserting either.

🔧 Fix: deny flag-led board opens and state Pi guard reach honestly
1 warning still open:

  • ⚠️ bin/fm-lavish.sh:145 - The remediation the guard now names mishandles the exact shapes this commit newly denies. Narrowing NON_SERVING to four subcommands means lavish-axi --version, -v, --help, and -h are denied (I confirmed all four via the policy), and the deny reason at bin/fm-lavish-command-policy.mjs:41 tells the reader to "Run &lt;wrapper&gt; with the same arguments instead". But the wrapper's dispatch mirror at bin/fm-lavish.sh:145-147 matches only poll|end|stop|server|playbook|design|setup|export|share|open, so a flag-led first argument leaves SUBCOMMAND=open and NEEDS_PORT=1 (line 168). bin/fm-lavish.sh --version therefore: claims a port and rewrites state/service-port.lavish through the allocator at line 308-318; rewrites state/lavish/fm-owner at line 363-366; stops and restarts this vessel's running board server if the owner record mismatched, via the STALE_SAME_PORT branch at line 302-305; runs lavish-axi --version, which exits 0 because installed lavish-axi 0.1.43's normalizeArgv (dist/cli.mjs:7449-7456) leaves a flag-led argv alone when no argument is an HTML path; and then, at line 388-391, probes http://$LINK_HOST:$PORT/health, fails because nothing was started, and prints "the board is serving, but the link's hostname ($LINK_HOST) does not answer here - hand over http://$ADDR:$PORT/... instead and check this vessel's tailnet name." That sentence asserts a serving board that does not exist, which is the same false-concrete-diagnosis class the DNS reason (bin/fm-service-port.sh:218-227) and the exit-5 message (line 328) were both corrected for on this branch. Before this commit the shapes were allowed by the guard, so nothing pushed an agent down this path; the deny is what makes it reachable. Two candidate fixes: extend the wrapper's own dispatch mirror - the correct owner for mirroring lavish's argv handling, unlike the guard policy where duplicating it was explicitly rejected - so a flag-led argv with no .html/.htm word is a passthrough with NEEDS_PORT=0 and no post-run link probe; or, more narrowly, gate the line-390 note on the invocation having actually opened a session, so it can never claim a board is serving when none is.

🔧 Fix: treat board-less flag invocations as pure passthrough
1 warning still open:

  • ⚠️ bin/fm-lavish.sh:416 - The new dispatch mirror asks whether lavish-axi will dispatch open, not whether open will actually open, so the false-serving claim is still reachable through --help. In axi-sdk-js/dist/cli.js (the layer under lavish's own dispatch), runAxiCli lines 73-79 look up getCommandHelp(command) and return its text without ever calling the handler whenever the exact string --help appears among the arguments after the command word, and lavish's createCommandHelp().open at dist/cli.mjs:8226 is a non-empty string, so that short-circuit always fires. Concrete path: bin/fm-lavish.sh --help board.html reaches the block at bin/fm-lavish.sh:168-183, finds an html path at line 178, so OPENS_BOARD=1 and SUBCOMMAND stays open; NEEDS_PORT=1 at line 193 then allocates a port and rewrites state/service-port.lavish (lines 334-344) and state/lavish/fm-owner (lines 389-392), and can stop and restart this vessel's server through the STALE_SAME_PORT branch at line 328-331 or relocate it at line 345-348 - which announces that links already handed to the captain stop working. lavish-axi then prints the open help and exits 0 without starting anything, so the probe at line 417 fails and line 418 prints "the board is serving, but the link's hostname ($LINK_HOST) does not answer here - hand over http://$ADDR:$PORT/... instead and check this vessel's tailnet name" for a board that was never opened. bin/fm-lavish.sh open --help hits the same path and is the more plausible shape, since it is what an agent would type to learn the open flags; it predates this commit rather than being introduced by it. -h board.html is correctly handled and does open a board, because args.includes(&#34;--help&#34;) is an exact match and -h does not short-circuit. Rather than adding a third argv-shape mirror, the boundary that closes the class is the round-3 finding's second option: gate the line-417 probe and the line-418 note on evidence that a session was actually opened - for example only probe when the run's output carries a session URL, or when a health check on $PORT succeeds - so the wrapper can never assert a serving board it did not observe.

🔧 Fix: gate board claims on an observed session link
1 info still open:

  • ℹ️ bin/fm-lavish.sh:433 - The honest-degradation warning is now gated on a string match against lavish-axi's output shape, and nothing in the repo pins that shape against the real tool. The intent marks as required that "no tailnet falls back to loopback and says in one plain sentence that the board opens only on this machine"; after this commit that sentence fires only when $OUTPUT matches *://*/session/* at line 433 and OPENED=1 at line 437. If lavish-axi's open output ever stops carrying a /session/ URL in that form, the wrapper goes silent on precisely the host class the warning exists for, and it goes silent the same way the original bug did. bin/fm-bootstrap.sh's LAVISH_ACCESS check cannot backstop it, because lavish_access_check returns early unless reachability=tailnet, while this note is the tailnet-less case. The test fixture cannot catch it either: tests/fm-lavish-access.test.sh:83 prints url: &#34;http://%s:%s/session/deadbeef&#34;, so the suite defines the very format the wrapper depends on and would keep passing after a real-tool change. I am not disputing the observe-rather-than-predict design, which is verifiably better than the argv mirrors it replaced and which I confirmed matches both createOpenOutput and createUserEndedOpenOutput in lavish 0.1.43 - the trigger is a future lavish release rather than anything reachable today, which is why this is info. The cheap hardening that keeps the same design is to corroborate with something the wrapper owns: treat STATUS=0 plus a successful health check on $PORT as additional evidence a board opened, so an output-format change degrades to warning anyway instead of to silence. Failing that, a comment at line 433 naming the observed real output already recorded in docs/lavish-access.md's validation record would at least make the dependency visible to whoever next reads it.

🔧 Fix: state host reachability regardless of observed board open
1 info still open:

  • ℹ️ tests/fm-lavish-access.test.sh:430 - The new real-tool pin discards the one signal that separates its two failure causes, so a failed open is reported as an output-shape change. Line 430 ends the capture with || true, so the open's exit status is thrown away, and the case at lines 435-437 then fails with "the installed lavish-axi no longer prints a session URL in the shape the wrapper reads" for any output lacking the pattern - including output that lacks it because no board was opened at all. The concrete trigger is the residual race this design documents rather than hides: node &#34;$PROBE&#34; bind at line 425 closes each candidate before returning it (bin/fm-service-port-probe.mjs tryBind closes before reporting free), so another process can take $real_port before lavish listens, ensureServer throws "Lavish Editor server did not start", and the test names the wrong cause. A blocked or slow telemetry close would read the same way. That is the same assert-a-cause-you-did-not-establish class this branch corrected in the DNS reason, the exit-5 message, and the link note, now reproduced inside the test added to guard the last of them. Capture the status into a variable and branch on it, so a non-zero open fails with "the installed lavish-axi could not open a board here, so the shape was not checked" and only a successful open with a missing pattern fails as a shape change. While in that block, note that the || true on the stop at line 433 means a failed stop leaks a real server, and the existing kill_test_servers trap cannot reap it because the real tool writes no pidfile under $FM_TEST_SERVERS - lavish does self-stop when idle, so this is cleanup latency rather than a leak, but it is worth a word in the comment.
⚠️ **Test** - 1 info
  • ⚠️ docs/lavish-access.md - The intent's second acceptance criterion - an HTTP 200 for a board fetched from a DIFFERENT device on the tailnet - could not be produced from this host, so it remains unverified by testing. Both Linux peers (aurora, crew-allesknut) reject tailscale ssh with "No ED25519 host key is known", and the captain's Windows PC and Android phone cannot be driven from here. The author already discloses this gap in docs/lavish-access.md, so this is not a hidden omission; everything on this side of the wire is proven instead (specific-address bind on 100.121.172.63, HTTP 200 via the tailnet DNS name, board rendering in a browser). Only the captain can close it by opening one of the emitted links on his own device.
  • bin/fm-test-run.sh tests/fm-lavish-access.test.sh (36 behaviour checks: allocator identity/determinism/exhaustion/degradation, wrapper environment and link claims, guard decision matrix and harness wiring, server-backed ownership/reuse/refusal-to-adopt, relocation safety, stop --port ownership) - all pass
  • bin/fm-test-run.sh tests/fm-bootstrap.test.sh (includes the new detect-only LAVISH_ACCESS check) - all pass
  • Manual e2e: FM_HOME=&lt;sandbox-a&gt; bin/fm-lavish.sh &lt;board&gt;.html --no-open and FM_HOME=&lt;sandbox-b&gt; bin/fm-lavish.sh &lt;board&gt;.html --no-open against the real tailnet and the real installed lavish-axi 0.1.43 - distinct ports 4449 / 4446, tailnet DNS links, port records written as published facts
  • ss -ltn on ports 4446/4449 plus a grep for any 0.0.0.0:/[::]: listener on them - both bound only 100.121.172.63, no wildcard bind
  • curl -o /dev/null -w &#39;%{http_code}&#39; http://crew-hlr.tail7b8448.ts.net:&lt;port&gt;/session/&lt;id&gt; and the same via 100.121.172.63 (HTTP 200 both) and via 127.0.0.1 (no connection) for both boards, plus concurrent /health on both ports
  • Real-browser render of both boards from their tailnet URLs via Chrome DevTools Protocol screenshots (Chrome 148 headless)
  • echo &#39;{&#34;tool_input&#34;:{&#34;command&#34;:&#34;lavish-axi .lavish/board.html&#34;}}&#39; | bin/fm-lavish-pretool-check.sh --claude (deny, exit 2) and the same for lavish-axi poll ... (deny), command -v lavish-axi (allow), bin/fm-lavish.sh .lavish/board.html (allow)
  • FM_HOME=&lt;sandbox-a&gt; bin/fm-lavish.sh share &lt;board&gt;.html - refused with exit 3 naming --fm-allow-share
  • FM_HOME=&lt;sandbox-b&gt; bin/fm-lavish.sh stop --port 4449 - refused with exit 7, and vessel-a's board still answered HTTP 200 afterwards
  • PATH=&lt;stub-tailscale&gt;:$PATH FM_HOME=&lt;sandbox-c&gt; bin/fm-lavish.sh &lt;board&gt;.html --no-open - one plain "no tailnet on this host ... opens only on this machine" sentence plus a loopback link, board still opened
  • FM_BOOTSTRAP_DETECT_ONLY=1 FM_HOME=&lt;sandbox-d&gt; bin/fm-bootstrap.sh with a loopback session link (LAVISH_ACCESS line printed) and with the same board on a tailnet link (silent)
  • tailscale status plus tailscale ssh aurora|crew-allesknut &#34;curl ...&#34; - attempted the cross-device fetch; both peers advertise no SSH host key
  • Cleanup verification: all three sandbox board servers stopped via bin/fm-lavish.sh stop, no listeners left in 4400-4499, git status --porcelain clean, sandbox and browser temp files removed, captain's own lavish session store unchanged

🔧 Fix: surface the unproven off-device criterion at the doc top
1 info still open:

  • ℹ️ docs/lavish-access.md:328 - The doc's stated reason for testing the phone specifically - "the MagicDNS name carries both an A and an AAAA record while Lavish binds one address" - was not reproducible from this host. dig +short @100.100.100.100 AAAA crew-hlr.tail7b8448.ts.net returns nothing (exit 0, resolver answering), and the same holds for timbook and s26-ultra-von-tim, while the A queries all answer. Tailscale suppresses AAAA for clients without IPv6 support, so a phone may still receive one and the recommendation to test there stays sound - the record-set claim is simply unverified from this side. Nothing about the shipped mechanism depends on it; noted only because this document's value is that its factual claims hold.
  • bin/fm-test-run.sh tests/fm-lavish-access.test.sh tests/fm-bootstrap.test.sh - the two owning suites, 42 lavish assertions plus the bootstrap LAVISH_ACCESS detection, exit 0 with no gate skips
  • FM_HOME=&lt;scratchA&gt; bin/fm-lavish.sh &lt;board&gt;.html --no-open and the same for a second scratch home - two vessels on one machine opening real boards through the wrapper with the installed lavish-axi 0.1.43
  • ss -ltnH | awk &#39;$4 ~ /:(4478|4443)$/&#39; - confirmed both servers bind 100.121.172.63 only, no wildcard and no loopback listener
  • curl -s -o /dev/null -w &#39;%{http_code}&#39; http://crew-hlr.tail7b8448.ts.net:4478/session/&lt;id&gt; and the vessel B equivalent - HTTP 200 over the MagicDNS name actually written into the captain's link
  • curl http://127.0.0.1:4478/health and :4443/health - connection refused, showing the loopback link the old path handed over is dead on those very ports
  • node bin/fm-service-port-probe.mjs resolve crew-hlr.tail7b8448.ts.net 100.121.172.63 - the emitted hostname resolves to the bound address
  • curl -H &#39;Host: &lt;per-home claim token&gt;&#39; http://100.121.172.63:&lt;port&gt;/health for each home against both ports - 200 for its own server, 403 for the neighbour's
  • tailscale ping --c 1 timbook - the tailnet peer path to the captain's PC answers (pong in 177ms)
  • Loaded http://crew-hlr.tail7b8448.ts.net:4478/session/&lt;id&gt; in headless Chrome 148 over CDP and captured the rendered board
  • npx marked -i docs/lavish-access.md rendered into a GitHub-styled page, screenshotted at 1280x1000 to show the new disclosure inside the first viewport
  • bin/fm-lavish-pretool-check.sh --command &#39;lavish-axi .lavish/board.html&#39; plus the wrapper and command -v cases - deny with the wrapper named, allow for the wrapper and for tool detection
  • FM_HOME=&lt;scratch&gt; bin/fm-lavish.sh stop for both vessels, then re-checked 4400-4499 listeners and git status --porcelain - servers down, scratch homes and browser profile removed, worktree clean
  • dig +short @100.100.100.100 AAAA crew-hlr.tail7b8448.ts.net (and for timbook, s26-ultra-von-tim) - probe behind the informational finding
⚠️ **Document** - 1 info
  • ℹ️ docs/subagent-guard.md:219 - docs/subagent-guard.md:219 enumerates Grok's tracked PreToolUse hooks as .grok/hooks/fm-primary-pretool-check.json and .grok/hooks/fm-primary-cd-check.json; this change adds a third (.grok/hooks/fm-primary-lavish-check.json). Left unedited on purpose: that line sits inside the archived 2026-07-22 "inspected but not wired" analysis whose load-bearing claim is the hook's stdin/deny shape, which is unchanged, and editing a dated evidence record to keep a parenthetical exhaustive would make the record less honest rather than more accurate. Flagging it as a judgment call in case the executor prefers the list to be complete.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…nd port

Every vessel handed the captain a http://127.0.0.1:<port>/... review-board
link, which opens nothing on his PC or phone. The failure is silent: the board
renders correctly on the machine that made it, so nothing signals that the link
is dead everywhere else. Documenting an environment variable could not fix it -
~/.bashrc returns early for non-interactive shells and an agent tool shell loads
its own snapshot, so a profile export never reaches the invocation, and that
route had already failed here once.

bin/fm-lavish.sh is now the entry point. It resolves this host's tailnet address
at runtime from tailscale, takes a port proved free, closes the Host allowlist to
this vessel's own names, and gives each home its own server state directory so a
secondmate does not share one server with its parent.

bin/fm-service-port.sh is a general vessel-local port allocator that Lavish is
merely the first consumer of. Several vessels share one machine as separate UNIX
accounts, so it treats a successful bind as the only proof a port is free and
writes its record as a published fact that nothing may read to decide
availability. A deterministic seat keyed on the realpath of FM_HOME keeps a
secondmate off its parent's port; an exhausted window refuses loudly rather than
downgrading to loopback, which would reproduce the original bug somewhere new.

Two failures found by reading the installed lavish-axi are handled explicitly.
A same-version server belonging to another UNIX account returns a byte-identical
health body and is silently adopted, so each home writes a claim token into its
own Host allowlist and asks whether the server on its port is its own. And a
running server keeps emitting the link host it was born with, so a changed
configuration restarts it instead of exporting at it - preserving the working
board until a replacement port is secured whenever a different port is wanted.

Every degradation says what is missing: no tailnet falls back to loopback and
says the board opens only on this machine, an unresolvable tailnet name drops
back to the address, and `share` is refused unless --fm-allow-share makes
publishing fleet-private material outside the tailnet explicit.

A PreToolUse guard denies bare lavish-axi wherever the wrapper exists, including
task worktrees, since boards get opened from crew worktrees too. Generated briefs
name the wrapper for the project worktrees the guard cannot reach, and a
detect-only LAVISH_ACCESS bootstrap check reports boards still on loopback so
this cannot regress unnoticed, as it already did once.

docs/lavish-access.md carries the reasoning and the validation record.
docs/fleet-service-port-registry.md is the cross-vessel published-record
proposal, deliberately non-binding until the other vessels have seen it.
@Freudator86
Freudator86 merged commit bbf0023 into main Jul 30, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant