Back to list
Skywire Development Update — September 15, 2026

Skywire Development Update — September 15, 2026

The fifteenth was the biggest single day of the run so far, and it shows in the shape of the work: a docs site rebuilt to actually isolate, serve and link itself; a desk pairing flow walked end to end in a real browser until every rough edge it hit got its own fix; a hypervisor split across two ports instead of one flag choosing between them; and underneath all of it, a run of transport-layer bugs — a browser visor dialing protocols it does not have, a QUIC ALPN collision killing every session at thirty seconds, an off-by-one in the inbound TCP demux that had made stcpr quietly impossible on some visors. The day closed with v1.3.94 tagged and a history rewrite that stripped 232MB of superseded command-module blobs from the commits after it, moving no tag and changing no tree. Two design documents, not yet code, set out where the route-multiplexing campaign goes next.

4882 gave the docs site the browser feature its terminal depends on and never had. Without cross-origin isolation there is no SharedArrayBuffer, so the in-browser filesystem bridge cannot exist and every command the docs-site terminal tried to run landed on the thread drawing the page instead of a worker — and GitHub Pages sends neither of the COOP/COEP headers that isolation requires. A service worker now synthesizes them for its own clients, at the cost of one reload on first visit, lifted out of two repositories that had been carrying a byte-for-byte duplicate of it into 0magnet/bottle, which this repo already depends on for its filesystem and process primitives. 4884 found the docs server itself running with no visible sign of life — bound correctly, but launched with no terminal attached, so the only evidence it worked was a browser tab appearing some seconds later. It now opens as a second tab of the same terminal window, behind the help tab, so a reader still lands on skywire --help first.

4881 then found that none of the links on the now-isolated, now-visible site actually worked, walked by hand as a reader would: the command reference, every subcommand page and the prose index each landed on a GitHub 404 or the wrong site. The navigation used absolute hrefs against a server that does not know where it is mounted — through the vnet service worker it sits several path segments deep, and a leading slash leaves the desk entirely — while the page renderer emitted links in a file layout the route handler did not match. Both are fixed together, and the fix also needed 4882’s isolation bump, since the service worker there had been pinning the page’s own <base> to the port root and discarding the depth a relative link needs.

4893 cleared drift the link fix had made visible: deployment guides still instructed operators to enable a DHT mirror and set a Redis address for it, on a feature with no package, no CLI subcommand and no code path that ever writes to the keyspace it describes — followed literally, the advice buys an operator a Redis and a password for nothing, and the architecture doc describing it as live is deleted outright. The same pass corrected a standalone-pty guide still naming commands folded away months earlier, a jq filter against a config field that has never existed, and a path reference into a vendor directory dmsg no longer occupies now that it lives in-tree.

4894 did the equivalent sweep for accuracy rather than existence: the worst finding was configuration.md telling a reader locked out of the dashboard to stop the visor and delete a database at a path that does not exist, when hv passwd --force already does exactly that reset, alongside several flags documented years after their removal and a runtime fact — the log ring’s actual size — that had simply drifted. Its prose index also switched from filing documents by filename to reading each one’s own Status: line, so a proposal that had since shipped stopped being filed as a proposal.

Skywire: Pairing a Tab, and Fixing What a Frame Was Swallowing

4899 built the tool that made the rest of this section’s fixes verifiable rather than merely plausible. hv eval’s synthetic MouseEvent fires a page’s listener directly, wherever it happens to be attached, which is useless for testing how a browser routes pointer input across a frame boundary — exactly what a recent run of frame bugs was about. hv input dispatches through the browser’s own input pipeline instead, so a click or drag crosses, and is swallowed by, the same things a real pointer would meet; it confirmed one fix immediately (a click inside a background window’s iframe now raises it) and, just as usefully, disproved a defensive assumption in another, which the code’s own comment now says plainly rather than claiming a reproduction that input-level testing could not produce.

4897 used exactly that kind of evidence to close four things a frame had been swallowing — a window drag that crossed into another window’s iframe and never received its mouseup, an app menu that would not dismiss when the dismissing click landed inside a browser or terminal frame, and the browser’s own Back button doing nothing while reading a natively rendered page — alongside an unrelated but live bug where proxy start called a flag-hiding helper on a flag the command no longer declares, silently rejecting every invocation of two still-documented spellings.

4900 fixed the matching symptom at the title bar itself: a window’s drag handle was listening for mousedown in the capture phase and stopping it on the way down, so every tab press in a window’s title bar armed a window drag instead of switching tabs, leaving no way to click between the browser, terminal or desk’s own tabs at all.

4895 walked the pairing flow in a real browser and found three faults that had each been hiding the next one. The visor-served desk had its documentation turned off on the reasoning that a native hypervisor serves its own — it does not, so the one desk standing in for the hypervisor UI was the one with no docs to open from. The pairing check then gave up on its first RPC failure rather than waiting for the in-tab visor to finish booting, so the tab never reopened the page it exists to show. And the click-to-raise fix for a framed window had been inferred from a blur event that a focus change into a same-origin frame simply does not fire; it now listens for the frame’s own mousedown directly.

4894’s desk-side half closed two more gaps in the same flow: status.skysocks opened unconditionally even when no proxy was running, swallowing the failure in a silent catch, and an unapproved hypervisor key was redialed forever at debug with no visible sign anything was wrong, now warned once after ten consecutive absent dials.

4907 then found that none of this helped a tab that was not logged into the dashboard at all, since the probe deciding whether to attach used a path that answers 401 without a session; it now probes the attach path itself, which authenticates by key rather than by login, so pairing no longer has a hidden login prerequisite.

Skywire: The Hypervisor Splits Into Two Ports

4904 ended the standing choice between the Angular dashboard and the desk sharing one port behind a flag. Both are served now, the dashboard at the root of the hypervisor’s main address and the desk at the root of its own desk_addr, one handler on two listeners so the API, login and every page behave identically on each — and a build with no command module simply starts no desk listener and says so once, rather than silently falling back. The same change gave the browser’s new-tab page somewhere real to point: the desk’s own pages as tiles rather than a placeholder describing the browser.

4906 caught the new default port colliding with an existing convention — :8002 is already skycoin-web’s own default bind and also the port the docs take on a tab’s virtual loopback, so a desk bound there read as the same thing from inside and outside the tab; :8010 collides with nothing skywire binds. 4908 closed the loop for anyone checking from the outside: hv status had only ever printed enabled or disabled, and now names the actual dashboard and desk addresses the visor bound, under --json as well as the human form.

Every fix in this stretch that touched the desk’s own UI needed its compiled module staged before a browser could see it, which happened five separate times over the day (4896, 4898, 4902, 4905, 4909) — the mechanical cost, restated each time by make check-exec-wasm, of a UI that ships as a compiled blob rather than plain source. One of those re-embeds mattered beyond timing: the staged module was still two PRs behind at one point, which meant the in-tab visor was still carrying a nil-pointer panic on every boot rather than the fix for it.

Skywire: Core Transport Fixes

4887 found the browser visor dialing two protocols it fundamentally cannot use. The TCP and UDP socket clients already refuse to start under GOOS=js, but the QUIC and STUN clients had never been given the same guard, and a wasm visor is a standard Go build rather than TinyGo, so neither constraint that normally excludes it applied — QUIC registered as a usable carrier and autoconnect spent its full timeout failing to dial it to every public peer, while STUN logged a NAT-probe warning per configured server for a verdict nothing downstream would ever read. 4892 was the same-day correction: the guard closed STUN’s readiness channel without a client ever having probed anything, and the one caller reading that channel treated “closed” as “ready” and dereferenced a client that did not exist, panicking the browser visor on every network-visualizer refresh.

4916 traced a problem six of the network’s nine dmsg servers were living with: dmsg-over-QUIC and the squicr transport advertised the identical ALPN string, so on any host where the visor’s own transport acceptor owned the UDP port, a dmsg QUIC session landed there instead, waited thirty seconds for a stream a dmsg session never opens, and was closed as timed out — while the client’s handshake had already succeeded, so its TCP fallback never fired and it simply redialed QUIC every thirty seconds forever. Each protocol now claims its own ALPN, with dmsg servers still accepting the old one so existing clients keep working, and a server whose QUIC sessions keep dying young is dialed over TCP for five minutes.

4891 found a second, unrelated impossibility: an stcpr handshake writes nine bytes and then waits, but the demux matcher sniffing for it read a tenth byte that initiator would never send, stalling every inbound stcpr connection on a visor sharing its transport port with an in-process dmsg server until the initiator’s own timeout gave up and the responder wrote its reply into an already-closed socket — on a live exit, 125 accepted connections and zero settlements in an hour. The same PR fixed an accept loop running the handshake inline, where one silent peer could hold the listener for its full ten-second timeout while every other dialer gave up and vanished before being accepted.

4921 fixed two related mis-orderings in how a visor reaches out on its own. sudph transports, hole-punched UDP with no keepalive of its own, were dying three to eight minutes after a NAT silently rebound the underlying flow between pings spaced sixty seconds apart — now pinged every twenty. And autoconnect had been dialing sudph to public visors first and then skipping any peer that already had an automatic transport of any kind, so a visor reached first over sudph never got the chance at the better stcpr or squicr; the phases now follow the configured transport preference instead. 4918 closed the matching gap on the direct-dial path, where a route had been taking whichever live transport a map iteration happened to produce first rather than the best one — measured live as 4MB/s against roughly 1Mbit/s with gaps, from the identical three transports to the identical exit, depending only on which restart it was.

Skywire: Observability and Control Surface

4919 gave a disappearing transport a reason attached to it. A transport present in tp ls at one reading and gone the next left nothing behind the log ring didn’t already age out, so the manager now keeps the last 256 opens and closes with each close naming why — a missed pong, an unwritable ping, a read error, a deliberate deletion — exposed as diag.transport_events in visor state. 4920 brought the router’s own settings, minimum hops, existing-transports-only, local route calculation and transport-type preference, fully into the CLI rather than leaving three of the four reachable only through the hypervisor’s HTTP handler, and fixed a route calc --by-latency --json command that printed nothing at all whenever a hop had no latency measurement, because an unmeasured hop summed to +Inf and encoding/json refuses to encode it.

4901 quieted a false alarm rather than adding a new signal: the uptime heartbeat warned on every failed tick, including the ones that fail routinely while the dmsg session to the tracker is still coming up after boot, so only a second consecutive failure now warns, with the run length recorded alongside it. 4917 gave the mux campaign’s load-testing rig a way to check a transfer’s integrity rather than only its size, serving an exact byte count with a SHA-256 header and accepting an upload that echoes the same check back.

Skywire: Config and Build Correctness

4889 found a host’s entire proxy chain silently disabled after a routine regenerate, with no dmsg_web or skynet_web block in the resulting config at all. The flag-to-environment-variable map config gen walks in both directions never included any of the resolving-proxy knobs, so a flag that set one emitted a config file with the matching line still commented out — and a second, independent bug meant that even adding the mapping would not have uncommented it, since the matcher checked for a default of true against a template written as false. Both are fixed together, with a test asserting every such flag appears in the map so the next one cannot go missing the same way.

4914 caught the matching bug on the browser side: the //go:build js config marshaller still wrote the mux_routes field removed in 4803, invisible to CI because nothing there compiles that file, and surfaced only when a downstream generator was rebuilt against the new release and failed outright. 4880 closed a smaller but operationally sharp gap: BROWSESUFFIX and its TLS counterparts had always worked, keeping a visor’s browse origin on a separate domain from its own identity so browsed pages stay cookie-isolated from it, but none of the three appeared in the skywire.conf template an operator would actually read to set them.

Skywire: Release Mechanics and the History Rewrite

4888 cleared five tests that had been red on the windows lane for days, none of them a bug in the code under test: a geoip cache rewrite racing two readers that still held the old file memory-mapped, which Windows refuses outright where Unix allows it; a JSON-log test writing to what it intended as a null device but had actually wrapped as file descriptor zero; and an intermittent CXO test given a tighter deadline than a slower runner could reliably clear.

4911 and 4912 fixed a release pipeline that had been failing linux builds of v1.3.94 specifically: actions/checkout’s default cleanup was deleting an already-unpacked musl cross toolchain, first discovered after the fact and fixed by keeping untracked files, then found to still fail because checkout itself deletes the whole workspace when run second — checkout now runs first. A workflow_dispatch path was added so the already-tagged release’s assets could be rebuilt without moving the tag.

Three changelog passes and one housekeeping note closed out the release itself: 4890 backfilled entries through the desk and docs-site work and re-embedded a command module that had drifted twelve commits behind; 4908 covered the two-port hypervisor change; and 4910 recorded that the commits following the v1.3.94-alpha1 tag were rewritten on 2026-09-15 to strip six superseded command-module blobs, 232MB, producing new commit hashes with no tag moved and no change to any tree.

4922 cleaned up the last of the day’s loose ends, updating autoconfig’s tests to the renamed desk-address flag and tidying eight stale go.sum lines that had been failing the module-integrity check since before the day began.

Skywire: Design Docs for the Mux Campaign

Two planning documents, no code. 4915 writes the intended default routing policy in the operator’s own words: forward traffic over the direct or lowest-latency route, fan the reverse path over multiplexed multihop routes, flip toward direct on demand when upload exceeds download, always decay to a connection that works, and hold as many standby routes as can exist. 4913 lays out the campaign to get there, framed against the operator’s own meaning of “tested” — proven live, on the fleet, not merely in a benchmark — stating where the multiplexer currently stands, the exit criteria a frozen rig will measure, and the order of the remaining work: per-leg send window and control-frame priority first, scheduler inputs second, and the reorder frontier only if the measurements still call for it.