Back to list
Skywire Development Update — September 14, 2026
The fourteenth belonged to the desk’s own survivability. The liveness watchdog that shipped the day before gained the two cases it had not yet covered — a host that restarts underneath an open tab, and a reload that would otherwise re-run code already fixed upstream — and then a file-scope bug shipped not once but twice in exactly the same way, caught the second time only because the embedded JS now has to prove it loads rather than merely parse cleanly. The documentation site was rebuilt around the desk it had previously only framed in an iframe. Two separate hardenings of the tab’s own identity, encryption at rest and the ability to come back online somewhere the original hypervisor cannot reach, rounded out the wasm side, alongside a dependency sweep, a changelog backfill covering three releases nobody had written down, and the ordinary run of small UI breakage.
Skywire: The Desk Learns to Notice When It’s Dead
4864 gave the desk’s session flag a reason to exist beyond suppressing an autostart. Observed live on theskywirenetwork.net: no visor process, RPC refused on vnet:3435, nothing listening on vnet:8001, and the dashboard still aimed at a dead port while localStorage happily reported {"visorRunning":true} — a stale record, not a signal, recoverable only by a manual reload. The fix polls the RPC port instead of trusting the flag, and restarts once the visor has been down long enough not to be a boot blip. It deliberately does not restart after a clean operator stop, after a load that already recorded stopped, or without bound, since every attempt opens a terminal tab and a visor that cannot start must not paper the desk with them.
4869 found the one case that watchdog could not yet tell apart from a deliberate stop: a host restart. saveSession formed its verdict at pagehide from “down after having been up”, and a visor the operator stopped and a visor whose host restarted underneath it both exit 0 at that point — so a host restart plus a reload recorded visorRunning:false, which suppressed both the next autostart and the new watchdog’s recovery, precisely the case it exists for. The decision moves to the transition instead, probed by one same-origin GET that only asks whether the host is still there at all — any HTTP response counts, including the 426 a plain GET to a WebSocket route returns.
4865 closed the remaining two gaps, both found by asking what happens when the wasm module itself changes underneath a running tab. A restart was spawning into the worker and module the page already had, since the exec worker dies only with the page — so a visor crashing from a bug already fixed upstream would restart onto the same broken code, repeatedly, with autoupdate unable to help because no reload ever happens. The served fingerprint is now checked before restarting, with a reload offered when it has moved on. Separately, a boot that never finishes had no recovery at all, because the watchdog lives inside the function doing the booting — the exact state this was found in, dashboard aimed at a dead port, taskbar up, no error. A deadline now arms on attempt and offers a reload if the desk never appears, through the same prompt-and-countdown pattern autoupdate.js already uses rather than a silent swap or a silent refusal.
Skywire: The Same Regression Twice, and a Check That Would Have Caught It
4870 was desk-boot.js breaking every desk on the mesh for the second time in the same way. 4869’s attachOrigin assignment landed twice — once correctly inside skywireDeskBoot, and once more at line 3 of the file, referencing a parameter of a function it is not inside. That throws a ReferenceError at load, which kills the whole IIFE before it ever assigns skywireDeskBoot — so no desk boots, no visor starts, and the watchdog and boot guard that live inside that same function never get the chance to arm either. This is 4675 exactly: an anchored insert landing at the top of this file and throwing this same error, and the recorded lesson from that incident, run node --check after editing embedded JS, had in fact been followed and had passed, because a statement at the wrong scope is syntactically perfect.
4871 is the fix that actually closes the gap node --check left open: make check-browseui now loads each embedded script the way a page does and asserts it defines its entry point, rather than merely parsing it. Demonstrated against the exact regression — node --check passes, check-browseui reports desk-boot.js threw at load: opts is not defined on the same file. A short allowlist of scripts that legitimately return early off-environment and define nothing is excluded, so the check fails for the right reason rather than a false one, and it is wired into make check so this class of break cannot reach develop a third time.
Skywire: The Docs Site Becomes the Desk
4875 rebuilt the documentation site around a backwards arrangement: mkdocs owned the root and opened the desk over it in an iframe from /playground/, so the documentation wrapped the terminal, where every other deployment of this page puts the desk at the root with everything else inside it. The fix overlays the desk onto the built site/ directory after mkdocs build, replacing only index.html so every other static page, the sitemap and search indexing stay untouched — and moves deploy from gh-deploy, which rebuilds before publishing and would discard the overlay, to ghp-import. A guard left over from when the page expected to be framed, redirecting to its parent whenever it was not, would have been an infinite redirect loop once unframed at the root; it is removed along with the shell layer that used to mount it.
That move needed its own cleanup. 4877 found the old /playground/ path left behind a copy of the desk page whose relative asset paths now resolved to the root instead, answering 200 and then failing to boot; it is replaced with a redirect, since old bookmarks and any cached shell still point there. 4878 was subtler: the docs site is the only deployment served from a GitHub Pages subpath rather than an origin root, and the code that builds a served vnet URL dropped that base path entirely, so a service worker registered under /skywire/vnet/ never intercepted a request built as /vnet/8002/ — the browser went to the network instead and GitHub Pages answered its own 404 in place of the docs. It now asks vnet for the already-correct mounted path rather than reconstructing one.
4876 walked the pairing recovery flow on a live hypervisor end to end and wrote down the parts that were missing around an already-correct pairing guide — the actual answer to a forgotten UI password (stop the visor, remove the account file, start again, nothing else affected), the fact that resetting a tab’s identity lives in IndexedDB and is blocked while the exec worker holds it open, and which side of a pairing ends up listing the other in hv ls. 4879 gave the desk page the favicon it had never had, and both changes closed out the day’s documentation work alongside 4875’s restructuring.
Skywire: A Source Build Was Not a Real Build
4872 closed a gap between what shipped and what go install produced: a release binary embeds the js/wasm command module inline as part of CI, but a plain source build carried none at all, falling back to the legacy Angular page — not the same product, and increasingly not an acceptable one now that the desk is the hypervisor UI rather than an optional extra. The module is committed instead, at a cost stated plainly: 38MB gzipped, built from the whole repository under GOOS=js, so nominally stale on most PRs — the same mechanism that grew this repository to 3GB before its history was rewritten, left deliberately unaddressed here rather than solved badly.
4863 found that neither release.yml nor publish-binary.yml wrote the revision file make embed-exec-wasm writes beside the module, so every shipped binary’s staleness check was hard-false and silent on exactly the builds it exists to guard — prod02 reported stale:false with no revision field at all, and no way to tell whether that was true. 4886 and 4883 cleared two false-positive CI blockers in the same spirit: a byte-for-byte comparison of the compiled routing-policy bundle had been red on develop for days over a 34-byte difference traced to the build simply not being reproducible across machines, demoted to a note now that a field-for-field parity test already guards what actually matters; and a missing build tag on one gotop test file had been breaking vet under the withoutgotop tag on every single PR and platform lane.
Skywire: Securing the Tab’s Identity, and Letting It Roam
4874 addressed the plainest risk in a browser-resident key: anyone with access to the device who opens the tab boots the visor as its owner, because the key sits in storage unencrypted. SkywireIdentityVault derives a key from a passphrase with PBKDF2-SHA256 at 600,000 iterations and wraps the secret in AES-GCM, so a wrong passphrase and a tampered record fail identically rather than leaking which one happened. It is explicit about its own limit — it defends a stolen device, not a compromised origin, since the decrypted key is necessarily in memory while the visor runs — and it drops a plaintext 32-byte key left in localStorage from before the identity moved into the jsfs config, verified to be genuinely unused on two live tabs that were carrying it regardless.
4873 gave a visor somewhere to go when its hypervisor disappears. A page served by a hypervisor pinned its visor to that origin’s WebSocket unconditionally — correct on the LAN, but a dead transport and no way to find a peer for a PWA that took its identity somewhere new. The attach flags are now gated on the host actually answering a probe the boot sequence already performs, so a silent host turns the visor into a standalone one: public autoconnect finds peers, relay nomination reaches dmsg through them, same identity and storage, different peers, decided fresh on every load rather than stored and left to go stale.
Skywire: Small Fixes and Housekeeping
4862 fixed a continuous error in the embedded hypervisor UI: the node list’s empty-state message read allNodes.length before that array had ever been assigned, which a native hypervisor’s dashboard passes through once at startup but the wasm visor’s embedded UI re-enters on every poll, logging the TypeError forever. 4866 cleared three reported annoyances in gotop at once — a selected table row that rendered unreadable dark-on-dark because reversing a foreground-as-cursor-color pair swaps it onto the terminal’s own default background, a version overlay that flickered in a frame late because it was drawn in a second render pass, and a websh startup banner nobody asked for — fixed upstream in 0magnet/gotop with a regression test asserting the actual drawn cell.
4867 swept 27 dependencies across host, js/wasm, windows and darwin builds, including this week’s org-wide bump of the 0magnet modules, while deliberately leaving gvisor pinned after -u moved it to a revision that ships a template without its generated files. 4868 backfilled the changelog for three releases that had shipped with no entry at all, generated from each tag’s actual PR range rather than a restatement of commit messages.