feat(bin): serve review boards on each vessel's own tailnet address - #38
Merged
Conversation
…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.
… guard everywhere
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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:
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.
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
bin/fm-lavish.shas 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 closedALLOWED_HOSTSand 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, includingstop --port), refuses the third-partysharepath unless--fm-allow-shareis 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.bin/fm-service-port.shwith thebin/fm-service-port-probe.mjsbind/DNS/readiness oracle: a successful bind is the only proof a port is free, the seat is keyed on the realpath ofFM_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 narrowedFM_SERVICE_PORT_RANGEnow counts as stale.bin/fm-lavish-pretool-check.sh+bin/fm-lavish-command-policy.mjs, reusing the arm guard's shell classifier) denies barelavish-axiboard opens — including flag-led ones — while allowingcommand -v/typedetection 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.shnames the wrapper in worker rules andbin/fm-bootstrap.shgains a detect-onlyLAVISH_ACCESScheck. Covered bytests/fm-lavish-access.test.shand an extendedtests/fm-bootstrap.test.sh, with the reasoning indocs/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 — anddocs/fleet-service-port-registry.mdas 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.
/home/coditan/.cache/no-mistakes-tmp/no-mistakes-evidence/01KYRESJWR6WTNZ7NX9HD7MFBC/doc-top-disclosure.png)/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" 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" status: opened exit=0Evidence: 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 200 $ curl -o /dev/null -w %{http_code} http://crew-hlr.tail7b8448.ts.net:4443/session/cfc23645cad98547 200 $ 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 177msEvidence: 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] barelavish-axiis 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<checkout>/bin/fm-lavish.shwith 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=0Evidence: Rendered docs/lavish-access.md (open in a browser to read the full page as a reviewer would)
Evidence: tests/fm-lavish-access.test.sh full run output (42 assertions, no gate skips)
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.)
🔧 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'snormalizeArgv(dist/cli.mjs:7449-7456) reads: if the first argument starts with-, it returns["open", ...argv]whenever any argument satisfiesisHtmlPath. So each of those commands runsopenand 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, andsetupare all in lavish's own COMMANDS set (line 7369), so an HTML argument cannot promote them toopen, and none of them callsensureServer. Narrowest fix that still honours the instruction to allow--version/-v: mirrornormalizeArgvby 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 whenkind = secondmate; for a ship or scout it launches-e __PIEXT__, which resolves to$STATE/<id>.pi-ext.ts(bin/fm-spawn.sh:1328). That generated extension is written at bin/fm-spawn.sh:1188-1197 and registers a singleturn_endhandler with notool_callhook, so a Pi crewmate or scout never loads.pi/extensions/fm-primary-turnend-guard.tsand never runsrunLavishCheck. 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 thetool_calllavish 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 wheneverrefused > 0after the walk, regardless of whether any candidate was actually EADDRINUSE, so an all-refused window (for exampleFM_SERVICE_PORT_RANGE=80-89under 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 "$PROBE" bind "$ADDR" $CANDIDATES 2>/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 meanslavish-axi --version,-v,--help, and-hare 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<wrapper>with the same arguments instead". But the wrapper's dispatch mirror at bin/fm-lavish.sh:145-147 matches onlypoll|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 --versiontherefore: 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; runslavish-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, probeshttp://$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/.htmword 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 dispatchopen, not whetheropenwill 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 upgetCommandHelp(command)and return its text without ever calling the handler whenever the exact string--helpappears among the arguments after the command word, and lavish'screateCommandHelp().openat dist/cli.mjs:8226 is a non-empty string, so that short-circuit always fires. Concrete path:bin/fm-lavish.sh --help board.htmlreaches the block at bin/fm-lavish.sh:168-183, finds an html path at line 178, so OPENS_BOARD=1 and SUBCOMMAND staysopen; 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 --helphits 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.htmlis correctly handled and does open a board, becauseargs.includes("--help")is an exact match and-hdoes 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 unlessreachability=tailnet, while this note is the tailnet-less case. The test fixture cannot catch it either: tests/fm-lavish-access.test.sh:83 printsurl: "http://%s:%s/session/deadbeef", 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 "$PROBE" bindat 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|| trueon 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.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) rejecttailscale sshwith "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 passbin/fm-test-run.sh tests/fm-bootstrap.test.sh(includes the new detect-only LAVISH_ACCESS check) - all passManual e2e:FM_HOME=<sandbox-a> bin/fm-lavish.sh <board>.html --no-openandFM_HOME=<sandbox-b> bin/fm-lavish.sh <board>.html --no-openagainst the real tailnet and the real installed lavish-axi 0.1.43 - distinct ports 4449 / 4446, tailnet DNS links, port records written as published factsss -ltnon ports 4446/4449 plus a grep for any0.0.0.0:/[::]:listener on them - both bound only 100.121.172.63, no wildcard bindcurl -o /dev/null -w '%{http_code}' http://crew-hlr.tail7b8448.ts.net:<port>/session/<id>and the same via 100.121.172.63 (HTTP 200 both) and via 127.0.0.1 (no connection) for both boards, plus concurrent/healthon both portsReal-browser render of both boards from their tailnet URLs via Chrome DevTools Protocol screenshots (Chrome 148 headless)echo '{"tool_input":{"command":"lavish-axi .lavish/board.html"}}' | bin/fm-lavish-pretool-check.sh --claude(deny, exit 2) and the same forlavish-axi poll ...(deny),command -v lavish-axi(allow),bin/fm-lavish.sh .lavish/board.html(allow)FM_HOME=<sandbox-a> bin/fm-lavish.sh share <board>.html- refused with exit 3 naming --fm-allow-shareFM_HOME=<sandbox-b> bin/fm-lavish.sh stop --port 4449- refused with exit 7, and vessel-a's board still answered HTTP 200 afterwardsPATH=<stub-tailscale>:$PATH FM_HOME=<sandbox-c> bin/fm-lavish.sh <board>.html --no-open- one plain "no tailnet on this host ... opens only on this machine" sentence plus a loopback link, board still openedFM_BOOTSTRAP_DETECT_ONLY=1 FM_HOME=<sandbox-d> bin/fm-bootstrap.shwith a loopback session link (LAVISH_ACCESS line printed) and with the same board on a tailnet link (silent)tailscale statusplustailscale ssh aurora|crew-allesknut "curl ..."- attempted the cross-device fetch; both peers advertise no SSH host keyCleanup verification: all three sandbox board servers stopped viabin/fm-lavish.sh stop, no listeners left in 4400-4499,git status --porcelainclean, 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.netreturns nothing (exit 0, resolver answering), and the same holds fortimbookands26-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 skipsFM_HOME=<scratchA> bin/fm-lavish.sh <board>.html --no-openand 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.43ss -ltnH | awk '$4 ~ /:(4478|4443)$/'- confirmed both servers bind 100.121.172.63 only, no wildcard and no loopback listenercurl -s -o /dev/null -w '%{http_code}' http://crew-hlr.tail7b8448.ts.net:4478/session/<id>and the vessel B equivalent - HTTP 200 over the MagicDNS name actually written into the captain's linkcurl http://127.0.0.1:4478/healthand:4443/health- connection refused, showing the loopback link the old path handed over is dead on those very portsnode bin/fm-service-port-probe.mjs resolve crew-hlr.tail7b8448.ts.net 100.121.172.63- the emitted hostname resolves to the bound addresscurl -H 'Host: <per-home claim token>' http://100.121.172.63:<port>/healthfor each home against both ports - 200 for its own server, 403 for the neighbour'stailscale ping --c 1 timbook- the tailnet peer path to the captain's PC answers (pong in 177ms)Loadedhttp://crew-hlr.tail7b8448.ts.net:4478/session/<id>in headless Chrome 148 over CDP and captured the rendered boardnpx marked -i docs/lavish-access.mdrendered into a GitHub-styled page, screenshotted at 1280x1000 to show the new disclosure inside the first viewportbin/fm-lavish-pretool-check.sh --command 'lavish-axi .lavish/board.html'plus the wrapper andcommand -vcases - deny with the wrapper named, allow for the wrapper and for tool detectionFM_HOME=<scratch> bin/fm-lavish.sh stopfor both vessels, then re-checked 4400-4499 listeners andgit status --porcelain- servers down, scratch homes and browser profile removed, worktree cleandig +short @100.100.100.100 AAAA crew-hlr.tail7b8448.ts.net(and for timbook, s26-ultra-von-tim) - probe behind the informational findingdocs/subagent-guard.md:219- docs/subagent-guard.md:219 enumerates Grok's tracked PreToolUse hooks as.grok/hooks/fm-primary-pretool-check.jsonand.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.