← the Atlas

Port Forwarding — Library & Inspector Port Links

Features·seedling·accepted·

A shared @kolu/port-forward library and live openable port chips for every terminal in the Inspector — no new daemon.

Agents constantly start dev servers, and the URL scrolls away into the scrollback. This feature answers the one question kolu can’t answer today — where is the thing the agent just ran? — with exactly one concept: listening TCP ports, attributed to their terminal, one click from your browser.The inspiration, kero.sh, also shows a process list with CPU% and memory. We drop processes from the plan entirely (KISS): CPU% is the most expensive part to build for the least payoff, and a name+pid list is decoration. The process-subtree walk survives only inside the scanner, as the mechanism that attributes a port to a terminal. It ships as a shared library — @kolu/port-forward — consumed by kolu’s Inspector. PRT0 first proved the library with a standalone TUI (vazhi; since removed); that second consumer is what forced the extraction to stay free of kolu types.

The plan is built for kolu’s real deployment shape: the server runs on a headless Linux box (pureintent) while the browser runs elsewhere (zest, a macbook on the tailnet). Everything below follows from one fact: “localhost” in a link means the viewer’s machine, so the only universally reachable place is the kolu server host — the name already in your address bar. Every port is made to answer there, and the client builds URLs from window.location.hostname, never a literal “localhost”.

REMOTE HOST — OWNS THE TERMINAL fish 27872 ├─ node 30588 │ listens :5173 └─ workerd 30596 the shell's process subtree padi portScan · new (PRT1) /proc walk (linux) · ps + lsof (darwin) — / 5 s subtree join → ports + bind address padi TerminalSnapshot — exists + ports (new field) KOLU SERVER HOST — e.g. pureintent (HEADLESS) kolu server surface — exists embeds @kolu/port-forward · new (PRT2) forwards = a cell on the existing surface policy: lazy on click · auto-cancel on port death 0.0.0.0:61000 ⇄ tunnel / relay @kolu/port-forward · library (PRT0) ssh -L · TCP relay · ForwardManager no kolu types cross the boundary proven standalone; kolu embeds it in PRT2 @kolu/port-forward — one library (PRT0) ssh -L *:61000:127.0.0.1:5173 surface mirror VIEWER — e.g. zest (MACBOOK, TAILSCALE) kolu UI in the browser PORTS 5173 node ⤴ ⇄ :61000 61922 workerd ⤴ Forwarded Ports · + forward a port… Inspector Ports section — new (PRT1 · PRT2) new tab http://pureintent:61000 location.hostname — never "localhost" window.open kolu UI raw TCP over tailscale exists today PRT0 — @kolu/port-forward library PRT1 — detection + Ports UI PRT2 — kolu embeds the library
Detection (green → blue): a 5-second scan in padi fills a new ports field on TerminalSnapshot, which rides the existing ssh surface mirror like every other terminal fact. Reachability (purple / orange): the kolu server embeds @kolu/port-forward, which makes any (host, port) answer on the kolu server host — an ssh -L tunnel for remote hosts, a plain TCP relay for kolu-host loopback — and the viewer's browser opens it via location.hostname. The library package itself is mechanism-only: no kolu types cross its boundary.

How a click becomes a page — the three cases

where the server listens how it becomes reachable what the chip opens
kolu host, 127.0.0.1:5173 TCP relay: listen on 0.0.0.0:61000, pipe to loopback http://pureintent:61000
remote host, 127.0.0.1:5173 ssh -L *:61000:127.0.0.1:5173 on a connection of its own http://pureintent:61000
kolu host, 0.0.0.0:5173 already reachable — nothing to do http://pureintent:5173

In plain words: a dev server on pureintent’s loopback is invisible to your macbook (loopback never leaves a machine), so the library opens a door on pureintent’s network side and copies bytes across. A dev server on a remote host is one hop further: the same door on pureintent, with the ssh connection kolu already holds to that host carrying the last leg. And a server that bound 0.0.0.0 on pureintent needs no door at all — the scanner records the bind address, so kolu tells these cases apart automatically.Sub-case of case 2: the remote host is the viewer’s own machine (zest as a kolu remote host, viewed from zest). The plan first called the loop “roundabout but correct, not worth a special case”; field use proved it baffling, and PRT2 shipped the recognition — see “When the viewer IS the host” below.

One library, two apps — and no third daemon

kaval and padi remain the only daemons. The reusable capability is a library package, packages/port-forward/ (@kolu/port-forward): the forward map ((host, remotePort) → local listener, N hosts × N ports), one dedicated ssh -L process per remote forward, the loopback TCP relay, port picking, and teardown. For a remote target the local port is the remote port whenever that number is free herepu-dev:4123 answers on 0.0.0.0:4123, a port you can predict and bookmark — and only when the number is taken does the kernel pick a free one instead; a forward is never refused merely because its number is busy, and there is no knob either way.Read the rule as remote-only: a local (loopback-relay) target never gets the preference and cannot, because both of its ends are on this machine — a listener on 0.0.0.0:<port> relaying to 127.0.0.1:<port> is pointed at ITSELF, and one connection to it opened ~29,000 file descriptors in 1.5s before the build caught it. The kernel always picks for a relay.

A forward lives exactly as long as the process that opened it. That is the one property the mechanism is chosen for, and it is why each forward gets its own ssh connection (ControlPath=none, so it can neither ride nor create a shared master) rather than a listener on a master shared with kolu. Sharing was the original design and it was wrong: OpenSSH gives a master’s forwards a lifetime of their own — a listener added with -O forward outlives its requester until the master’s ControlPersist idle timer reaps it, killing a mux client does not take its forward down, and there is no -O list, so a restarted process can neither see nor adopt what it left behind. An empty list beside live ports is the worst possible pair, and it is what the shared design actually produced.Both facts measured on a live sshd before the rewrite: forwards survived their creator by the full ControlPersist window, and a SIGKILLed mux client left its listener serving. Sharing a master and kernel-tied lifetime are mutually exclusive in OpenSSH; lifetime wins. The connection is held open by a remote command (cat) reading a pipe the library never writes to, so the kernel closing that pipe — on quit, on crash, on kill -9 — ends the session and with it the listener, with no timer in the loop.-N (no remote command) does NOT have this property: measured, an ssh -N -L child SURVIVES its parent’s SIGKILL and keeps serving the port. The hold-open command is what makes the lifetime kernel-tied, and a library test pins it — open a forward in a child process, SIGKILL it, require the port to refuse. The costs, stated: one ssh handshake per forward instead of a channel on a warm master, and a SIGKILL leaves the ssh children visible for as long as it takes their connections to notice — they die with those connections, they do not linger on a timer.

The per-forward connection also needs its own authentication, and that costs nothing new: kolu’s mirror already requires non-interactive ssh (BatchMode=yes and the rest of SSH_OPT_PAIRS), so any host kolu can mirror is a host a dedicated forward connection can reach with the same keys and the same ~/.ssh/config. host here is just an ssh destination — user@box, an alias, whatever kolu’s remote hosts already say — with no kolu-specific host model inside the library. And the ForwardManager API is the PRT2 contract: create (idempotent by target) · cancel · list · dispose · onLost is exactly what kolu’s Inspector will wire against, so the mechanism could be replaced under it — as it just was — without either app noticing. Mechanism in the library, policy in the consumers.Two shape questions PRT2 was to settle, and both settled as NO CHANGE — recorded because “we looked and the second consumer wanted the same thing” is a different fact from “nobody got round to it”. (1) list() stays pull-only. The prediction was that a second consumer would want a subscription; kolu instead re-reads the map after every act — and it does so because the surface CELL it feeds is itself a re-read on a change edge. A second independent consumer converging on pull is evidence the pull shape is right, not that the subscription is overdue. (2) create still rejects with the mechanism’s own message. The prediction was that the Inspector would branch on a kind (retry, pick another port, mark the host down); it branches on none of them, because the map ALREADY handles the only recoverable case — a taken local port falls back to a free one inside openPreferringPort, so it never reaches a consumer as an error. What is left is genuinely terminal (no ssh, host unreachable, tunnel never came up) and its one honest treatment is to show the user what happened. A tag with no reader would be speculative structure.

One thing DID change, and it went the other way — into the library rather than into a consumer: a target that comes back gets the local port it had before. That is what makes a link survive a dev-server restart, and it belongs to the MAP because it is bookkeeping the map already holds. Left to consumers, each would have kept the same table beside the same map and got its eviction rules subtly different.

One production consumer, the library still shaped as a shared capability (shared libraries, independent apps — the drishti pattern):

How the tunnel works, from zero

No new machinery, no daemon, nothing installed on either machine: a forward is one ordinary ssh process — the same tool that logs you in — told to hold a door open. Here is the actual command kolu runs, taken apart:

THE ONE COMMAND KOLU RUNS ssh -L *:5173:[::1]:5173 naiveintent 'echo READY; cat' -L — “open a Listening door” *:5173 — the door: port 5173, every interface of the kolu host [::1]:5173 — where it leads: the server’s own loopback, far side naiveintent — the far machine, reached over ssh 'echo READY; cat' — say ready, then hold the line open forever WHAT HAPPENS WHEN YOU LOAD THE PAGE your browser http://pureintent:5173 it only ever talks to the kolu host — nothing else 1 dials the door KOLU HOST — pureintent the ssh child — listens on *:5173 every connection it accepts is wrapped and sent down one encrypted pipe — no bytes touch the network in the clear 2 encrypted, one pipe REMOTE HOST — naiveintent sshd the pipe’s far end 3 knocks on the loopback for you dev server [::1]:5173 loopback-only — invisible to the network, until now 4 the page rides the same pipe back Nothing installed on either machine, no firewall changed, no server reconfigured — ssh is the only tool involved. The full flag set (BatchMode, ExitOnForwardFailure, ControlPath=none) is the mechanism section’s story told as options — see the footnote in the prose.
The command, dissected — and the numbered path a page request takes through it.

A dev server bound to loopback answers only its own machine — 127.0.0.1 and ::1 both just mean “this computer”.Two spellings because there are two loopbacks, IPv4’s and IPv6’s — and a server listening on one is NOT reachable on the other, which is why the command dials the exact loopback the scanner saw the server bind. Dialling the wrong one was a real shipped bug in this very track. The tunnel lends that private server a public door: ssh listens on the kolu host, and everything that knocks is carried — inside the same encrypted connection ssh always uses — to the far machine, where sshd knocks on the server’s own loopback on your behalf. Your browser never learns any of this happened; it just loads a URL on the kolu host.The unlabelled flags in the real invocation are the mechanism section’s story in option form: BatchMode (keys only, never prompt), ExitOnForwardFailure (if the door can’t open, fail loudly instead of half-connecting), ControlPath=none (never share a connection — the lifetime argument above).

THE DOOR LIVES EXACTLY AS LONG AS ITS PROCESS birth kolu spawns the ssh child… …and waits for one line back: PORT-FORWARD-READY only then is the chip marked forwarded life the remote cat just… waits. It reads input that never comes, holding the connection open — and the door with it. no timers, no keep-alive code in kolu death — any of these you click ⨯ → kolu kills the child the dev server stops → auto-cancel kolu itself dies → the pipe snaps, ssh exits on its own the moment the process ends, the kernel closes its listener — instantly There is nothing to clean up and nothing that can be forgotten: a door cannot outlive its owner, because the door IS the owner's open socket. This is why quitting kolu never leaves ports mapped — a shared-master orphan is unspellable here.
Birth, life, death: the door is the process's own open socket, so it cannot outlive it.

That is the entire protocol: one READY line at birth, a cat that waits forever, and a kernel that closes the listener the instant the process ends — which is why there is no cleanup code to forget and no way to leak a port. The mechanism section above carries the expert-level detail (why each forward gets its own connection, what -N would break, the measured evidence).

PORTS 3
🌐 5173 node ⤴ open ⇄ :61000
🌐 9229 node ⤴ open
🌐 61922 workerd ⤴ open
+ Forward a port…
FORWARDED PORTS
⇄ pu-dev:5173 → :61000 · auto · ⨯   ⇄ zest:8080 → :61003 · manual · ⨯

A Ports section joins the existing blocks in packages/client/src/right-panel/MetadataInspector.tsx (pure rendering — the data arrives on the terminal’s metadata like everything else). The flows:

Detection: ports, attributed to the terminal

The scanner is padi’s port detection (packages/padi/src/ports/scan.ts + sampler.ts), driven by a padi sensor, not a kaval module. The OS volatility that once justified @kolu/port-scan as its own package now lives in osfacts; the TypeScript face is osfacts-client (that repo’s client-ts/), and kolu policy (bind classification, U-row blindness, fold) sits beside the sampler in padi. Two separate calls, and both matter.

Why not a package any more: the OS axis of change is osfacts’s job. padi owns the cadence and the policy; the client is the tool’s own leaf (and drishti’s next import). Test ③ (a second consumer) is met by osfacts-client + the imminent drishti adoption.

Why padi drives it: the repo’s own taxonomy draws the line. Facts that require the PTY itself live in kaval (foreground needs tcgetpgrp on the PTY fd, cwd is parsed from OSC sequences), while facts derived by OS inspection from a snapshot key live in padi (git status is derived from cwd; kaval knows nothing about git by design). The port scan needs zero PTY access — its only input is each shell’s root pid, which padi already reads off kaval’s list surface — so it sits beside the git sensor, keeps subprocess-spawning out of the memory-sensitive kaval, and changes no kaval wire contract (contract skew on remote hosts has its own bug note; not growing ptyHostSurface is a feature). One host-wide pass every 5 secondsCadence and the unref’d, non-overlapping poll shape copy packages/padi/src/memorySampler.ts and its derived.cell(source({ read, install: everyMsOr(…) })) wiring in servePadi.ts:247 — a padi idiom, which is one more reason the scanner lives there. builds the pid→ppid table once, partitions it into per-shell subtrees, and joins listening TCP sockets against each subtree:

The successor: osfacts

The shipped readers are deliberately not the destination. The destination — one scoped, single-pass Rust binary serving kolu and drishti — was proposed, gated on measurements, refuted, reopened on new evidence, and is now built as osfacts; OSF2 migrates this very scanner onto it. The candidate scoreboard, the gate results, the listeners bug we fixed upstream, and every number behind those verdicts live in that note and in git history, not here.The bar the incumbents set, kept for context: the darwin C helper measured 8.45–10.6 ms per pass and the linux /proc reader 14–18 ms, both parity-checked against lsof/ss on live boxes. osfacts clears it: ~5 ms darwin, ~8 ms scoped linux.

Attribution is the live ppid subtreeAttribution is the live ppid subtree — and nothing more.** Backgrounded (&) processes, pipelines, and grandchildren all keep the shell as an ancestor, so the walk sees them; a true daemon (setsid/double-fork, reparented to init) leaves the subtree and is deliberately out of scope — no session-id heuristics, no host-wide orphan matching. If you daemonized it, it is no longer “this terminal’s server”.

Cadence: a 5-second baseline, nudged by output. The tick alone means up to ~5 s between a dev server’s “ready” banner and its chip — the one UX cost the spike numbers don’t excuse. So terminal-output bursts and OSC 633 command marks (the same signals the foreground sampler keys on) trigger an immediate off-schedule scan — single-flight, ≥1 s floor between passes — while the tick remains the baseline that catches quiet binds and port death (which auto-cancel in PRT2 rides on). Output is only ever a hint about when to look; the socket table stays the sole source of facts — a printed URL never creates a chip.VS Code’s output mode is the cautionary tale for crossing that line: its terminal-URL regex creates forwards with no liveness check, and its own settings text documents the consequence — bogus URLs yield stale forwards that survive until reload, and its process scanner exists only on Linux remotes (macOS/Windows get the regex, full stop). Most of its knob matrix — autoForwardPortsSource, the 20-forward hybrid fallback, per-port action overrides, focus-gated toasts — compensates for eager forwarding on uncertain evidence. Kolu’s lazy, scanner-derived design needs none of it.

Measured end-to-end through the shipped scanner (non-root): a complete linux scan is ~3 ms (~0.06% of a core at the 5 s cadence); the macOS pair is 17 ms on a quiet box and 93 ms on a busy one (~1.9% of a core). The 5-second cadence has room to spare on both.Parser subtleties, all fixture-worthy: /proc/net/tcp addresses are byte-reversed hex and IPv6 words are little-endian per 32-bit word, not across the address; v4-mapped IPv6 must be normalized (::ffff:0.0.0.0 is a wildcard, ::ffff:127.0.0.1 is not, and they differ in four bytes) and dual-stack rows deduplicated on the final structural PortInfo; a fork-inherited socket maps one inode to several pids, so attribution folds at subtree level before dedup; lsof brackets IPv6 hosts ([::1]:5173), which is what makes “port after the last colon” unambiguous, and spells the wildcard bind *. One more, found live: /proc/<pid>/stat’s comm is the THREAD name, and Node renames its main thread — a plain node dev server reports MainThread, so the displayed name comes from /proc/<pid>/cmdline’s argv[0] instead.

Each terminal’s sample — PortInfo { port, name, scope, family } in @kolu/terminal-vocab (ports.ts), beside ForegroundSchema:145 — is deduped by structural equality before it touches the snapshot, so an unchanged scan emits nothing: the churn guard that keeps a 5-second ticker from thrashing SolidJS. From there every layer exists: the sensor set folds it into TerminalSnapshot, the surface serves it (snapshot-then-deltas invariant, .claude/rules/streaming.md), and the reconcile-backed store in useTerminalMetadata.ts:180 delivers leaf-grained ticks to the section.

Build plan — the PRT track

What each phase changes for the user — the sentence you’d put in a release note:

id ships needs proof
PRT0 ✅ @kolu/port-forward library (standalone TUI proof, since removed) — #1975 standalone forward on a real remote box, no kolu running
PRT1 ✅ padi port sensor · Ports section · direct open — #1982 e2e: spawned listener’s chip appears + opens
PRT2 ✅ kolu embeds the library — #1991: lazy forward · merged Ports UI · manual · auto-cancel PRT0 + PRT1 real remote box: chip → tunneled HTTP 200; kill the server → ports refuse
PRT3 embedded in-kolu preview PRT2 parked — #2001 closed at the HTTPS-page × HTTP-door browser wall
PRT4 ✅ printed URLs join the scanner — click localhost:5173, get the door — #2004 PRT2 e2e: printed URL → door → path preserved; live card upgrade; join unit table

PRT0 shipped in #1975 and PRT1 in #1982; PRT2 shipped in #1991; PRT3 is parked at a browser wall recorded in its section below; PRT4 shipped in #2004.

PRT0 — the library, proven standalone (shipped: #1975)

packages/port-forward/, first proven by a standalone TUI consumer against a real remote box with no kolu running. Two structural facts remain load-bearing: that consumer imported only @kolu/port-forward — the proof that made PRT2’s embed mechanical — and kill -9 leaves no port answering, the property the per-forward connection exists to give. The TUI itself is gone; the library and those proofs remain.

PRT1 — detection + Ports section (shipped: #1982)

Scanner (padi portScan + osfacts-client), the ports schema field, the sensor fold, the Inspector section, direct-open chips, the tip. What remains load-bearing: the pass holds no OS state between ticks (parse, join, emit the structural PortInfo, drop everything else); it repartitions from the current terminal root pids every tick, so a closed or re-keyed terminal never leaves a stale subtree; and it is single-flight (the output-nudge and the baseline tick share one non-overlapping runner). Fixtures alone proved insufficient twice on darwin, so a live suite — real listeners, the real scanner, both platforms — rides CI beside them.The reading mechanism itself is OSF2’s to replace; the discipline above outlives the swap.

PRT2 — kolu embeds the library (shipped: #1991)

⌂ pureintent  ·  zest  ·  naiveintent  ·  kolu-bot
PORTS inspector
8124 node ⇄ :61000 ⧉ ⨯ ↗ open
5173 vite ⇄ :5173 ⧉ ⨯ ↗ open
9229 node ↗ forward & open
3000 workerd ↗ open
also forwarded on this host
8080 ⇄ :61003 pinned ⧉ ⨯ ↗ open
⌘K  forw 
⇄ Forward a port… host:port → opens a tunnel

What ships, at a glance — detail lives in the footnotes:

VIEWER — zest Inspector chip (PRT1) 8124 node ⤴ open 5173 vite ⤴ open every chip now opens — that is PRT2 new tab http://pureintent:8124 location.hostname, never "localhost" click → create → open KOLU SERVER HOST — pureintent kolu server embeds @kolu/port-forward · new (PRT2) forwards = a cell on the existing surface · lazy on click ⌘K "Forward a port…" · host-tab ⇄ badge · Forwarded Ports lists case 1 — TCP relay 0.0.0.0:8124 → 127.0.0.1:8124 in-process; for THIS host's loopback-bound servers case 2 — ssh child ssh -L *:5173: 127.0.0.1:5173 box ONE child per forward; ControlPath=none a forward lives exactly as long as its owner cancel row → kill(child) · close relay scanner sees the port die → auto-cancel (auto forwards only) kolu killed → kernel closes every listener, instantly padi port sensor (PRT1) — the death signal auto-cancel rides HTTP REMOTE HOST — e.g. kolu-bot sshd the tunnel's far end dev server 127.0.0.1:5173 loopback — unreachable from anywhere, until now encrypted local hop shipped (PRT1) new in PRT2 @kolu/port-forward mechanisms (shipped in PRT0)
One click, both mechanisms. A loopback port on the kolu host gets an in-process relay (case 1); a port on a remote host gets a dedicated ssh -L child whose death is the forward's death (case 2). The browser only ever opens the name already in its address bar. Auto-cancel rides PRT1's scanner; a killed kolu server takes every door with it, kernel-guaranteed.

The port fact, as shipped: scope + family, one judge

PortInfo carries scope: "any" | "loopback" | "interface" — a boolean could not spell the interface-bind case, which answers at its own address and which no door can reach, so portReach has an honest no-mechanism arm — and family beside it, because 127.0.0.1 and ::1 are both loopback and are not the same address: scope decides whether a door is needed, family decides what it dials.One classifier (addressScope) stays the single judge both platforms reach through their own decoders, so they cannot disagree about ::ffff:0.0.0.0; the fold widens (any > interface > loopback), keeping it order-independent so a 5-second scanner never republishes a phantom change. The dial family is read SERVER-side off kolu’s own scan — a client’s stale copy would open a door onto nothing — and the one unobservable case (a manual forward to a port no terminal serves) assumes v4 at a named constant, loud at the point of use. Both fields were bought by field defects: wildcard lost the interface case, scope-alone lost the family — each a fact the OS handed us, collapsed below the consumer’s need, invisible until something dialled the answer. scope is an OBSERVATION, not a verdict: whether a port answers for a given viewer also needs the host, so the judgment stays portReach(scope, onKoluHost) in kolu’s own vocabulary. And ports is a discriminated known / unknown two-way — a terminal that has never been successfully scanned is distinguishable on the wire from one that serves nothing, with three verified routes to blindness that never touch the shell.A first-pass lsof timeout on darwin, a transient /proc/net/tcp read failure on linux, any unexpected /proc errno. Blind holds a last-good sample only for what it can actually cover.

Embedded, and field-hardened

The kolu server embeds @kolu/port-forward: forward state is a cell on the existing surface, chip click → lazy create, one merged Ports list, ⌘K manual forward, auto-cancel — with two UX details adopted from VS Code’s Ports view (re-forwarding prefers the previous local port; every row carries copy-address). The first deploy surfaced three field defects — a fused-cadence publish loop that froze the whole server, the missing address family, and forwarding offered to the machine the viewer was sitting at — each fixed in the same PR with a regression test written red first.The freeze’s general lesson outlived its fix and went upstream (next section): neither module was wrong in isolation, so only a test of the JOIN — reproducing the fused wiring and asserting a bounded read count — could catch it. The reconciliation now reports by returning, so “reap and announce” is no longer spellable; the reactor’s provenance guard makes the whole class crash loudly instead of freezing.

**When the viewer IS the host.**When the viewer IS the host. A machine in kolu’s fleet can be the machine you are reading kolu from — the plan’s own footnote called this “roundabout but correct, and not worth a special case in v1”, and in practice it was baffling: every chip on zest’s terminals, viewed from zest, offered to open a door on pureintent so that zest’s browser could reach a port on zest. So kolu now recognises it, from the one fact that can settle it: the peer address of the viewer’s own connection, which only the server can see, compared against the addresses the host resolves to.

Two things make an inexact comparison acceptable here, and neither is incidental. First, the failure direction is safe: a match opens the port directly on the viewer’s own loopback, and every way of failing to recognise the viewer — a NAT, a proxy, an ssh alias DNS cannot resolve — leaves the forward exactly as it was, which works. Second, a direct port is never rewritten to localhost: it already answers on a name that means the same thing from every machine, and localhost is the one hostname that does not — trading the first for the second would undo the rule this whole feature is built on.

It rides a ROOT rpc rather than a surface member, and that is structural rather than convenient: the answer differs per viewer, and a surface cell is broadcast, so it has no shape that can carry a different answer to each connected browser.The reliable version of this needs a fact kolu does not have — the host’s own interface addresses, which padi could report but does not — so what ships resolves the ssh destination instead. That is genuinely weaker for alias-named hosts, where it simply never fires. Recorded as a limit rather than hidden, because the alternative was to add a padi surface field inside an incident fix.

The viewer’s address is taken from the X-Forwarded-For header when, and only when, the direct peer is a trusted local hop — loopback, or one of this host’s own addresses, which is exactly what a local reverse proxy looks like (kolu’s real deployment is reached through tailscale serve, whose dial arrives from the host’s own address — the reason a bare TCP-peer comparison never fired once in the field). A header on an untrusted connection is ignored entirely (anyone who can reach kolu directly can invent one); the last entry wins, because each proxy appends the address it received from while the leftmost is whatever the client typed; and there is no trusted-proxy list, because the trust condition is a fact about the connection rather than a knob.A deployment shape that hides the viewer more thoroughly — a proxy on a different machine, or one that strips the header — lands on the same no-match side as before, which keeps the forward that works. The mechanism is allowed to not fire; it is not allowed to be wrong.

What the automated suite actually covers, and what it deliberately does not. The e2e scenarios drive the relay case end to end — a real loopback listener in a real PTY, a real click, and the listener’s own body arriving in the opened tab through a port it never bound — plus the cancel twin, which proves the door is shut by dialling the recorded URL from node rather than by watching a row disappear. The ssh -L case is not simulated: it needs a second machine, and a loopback ssh hop standing in for one would exercise a different code path from the one it claimed to cover, so it stays with the remote-host-testing harness above. Stated rather than left implicit, because “the port forwarding suite is green” would otherwise read as covering both mechanisms.

PRT3 — embedded preview (parked at a browser wall)

An implementation was completed and closed unmerged (#2001): kolu in production always runs behind tailscale serve HTTPS, and browsers categorically forbid an HTTP frame inside an HTTPS page — so a Preview pane cannot show door content on any real deployment. No workaround survived scrutiny.The options, each found wanting: honest-degrade ships an empty pane on 100% of real deployments; a same-origin subpath proxy breaks any app using absolute asset URLs (Vite’s own /@vite/client included) — which is why the industry uses per-port subdomains with wildcard certs, unavailable on ts.net; per-door TLS via runtime tailscale serve mutation makes serve config outlive the process, the orphan class PRT0 made unspellable; a statically declared HTTPS door pool (serve entries + door range baked in one home-manager module) was judged unsatisfactory. Full record on the closed PR.

What the attempt proved out survives as salvage on its branch (port-forwarding-prt3), beside #111’s: the per-terminal server chrome seam, the previewOpen/previewClose procedures with server-side no-raw-URLs validation, the MCP door-verbs with navigate-on-repeat, and the pane components. The wall is only the framing scheme. Parked until a design exists that is not a compromise; when one does, this section is rewritten, not layered.

PRT4 — the printed URL joins the scanner (shipped: #2004)

VITE v8.1.0 ready in 450 ms
➜ Local: http://localhost:5173/
⇄ 5173 this terminal serves it · node
"localhost" here means naiveintent — opens via a door on pureintent, path included.
⇄ forward & open ↗  ⧉ copy door URL

Agents print http://localhost:5173/ constantly, and “localhost” in the viewer’s browser is the wrong machine — the fact this whole note opened on. Clicking such a URL now raises a small card at the cursor that answers through the door machinery. The printed URL is an entry point, never a fact: every affordance is earned by a click-time JOIN against the scanner’s observations and the live forwards — nothing is created from text.VS Code’s regex-created forwards are this note’s standing cautionary tale (see the cadence footnote in the detection section): its terminal-URL regex creates forwards with no liveness check, and its knob matrix exists to walk that back. The join inverts the direction — text may only ever LOOK UP what observation already established. This lands what PRT3 died reaching for — the printed URL becomes one correct click — with zero iframes, so the HTTPS wall never enters the picture.

Done-criteria — met in #2004. (1) e2e printed-url.feature: a real PTY prints a real URL, the click lands the page’s own body through the door, path preserved (coordinate click on the xterm link held). (2) open card upgrades live when the listener appears after the card opened. (3) join decision table in unit, including blind-vs-unbacked. (4) -click bypasses. (5) the chip’s e2e passes through the extracted flow; PortsSection net-negative after extraction.

What this track sent upstream

PRT2’s incident drove four dispatches into @kolu/surface; three shipped, one was withdrawn when verification refuted its premise. Shipped: the reactor’s loop guard built on provenance — an AsyncLocalStorage read context per cell; three consecutive self-caused ticks crash naming the cellTiming-plus-value-equality heuristics were adversarially refuted in both directions first: they crash an honest poll slower than its interval, and they miss a real cycle whose read does work. Causation, not timing — the repo’s own echo-loop doctrine. — with assertCellConverges as its authoring-time twin; viewer identity, graduated to @kolu/surface/viewerIdentity; and the doctrine written down: a per-viewer answer is a root RPC and never a broadcast cell; a poll read may reconcile but publishes only by returning. Withdrawn: Observed<T> — its “same shape three times” premise was factually wrong (ProcessRss is a load-bearing three-way; the honest duplication elsewhere is ~6 lines of loop skeleton), recorded so the roadmap does not claim work it did not do. Revisit only if osfacts’ kolu-side adoption ever creates real duplicated fold logic.