Back to list
Skywire Development Update — September 13, 2026
If the twelfth was about making a relay useful, the thirteenth was about making the paths around it honest. A proxy that had been doing exactly what it was asked kept reporting itself as broken, because every surface built to describe a route only knew how to draw a route group, and a direct dial over a named transport builds none on purpose. Three more fixes chased that visibility down through the rendering stack it touches. Elsewhere the day cleared out API fields that have read as zero forever because nothing ever assigned them, caught a wasm command module quietly serving builds days stale across several installs, and gave two long-standing defaults — production log level, hypervisor auth — a way to be stated in the config file that survives a package update instead of only in a flag that doesn’t.
Skywire: The Direct Path Becomes Visible
4839 gave the direct-dial shortcut something to say about itself. When min_hops <= 1 and no per-dial --mux is set, AppDirectMux skips route-group setup entirely and dials a named transport directly — by design, and invisible to status.skysocks, proxy status and proxy tree, which all answer “the route” by rendering a route group. A proxy reported Route: (no active route group) and route_groups stuck at zero while it moved real traffic to a verified exit, because the app name that reaches the routing-policy hook was dropped at the DialOnTransport boundary and no stream could be attributed back to it. VStream now keeps its app name, open time and byte counters; VStreamMux.StreamInfo reports them; and the three surfaces model a direct stream as the one-leg tunnel it actually is when there’s no route group to draw. DialOnTransport keeps its old signature for internal callers like setup-RPC, so an attribution bug can’t make an internal stream look like a proxy’s.
The visibility arrived in pieces. 4852 found that the status page’s tree still drew nothing beneath a direct leg, because hopClassMap skips any leg that isn’t Alive and the new direct legs never set that flag — a stream in the mux’s live map is alive by definition, StreamInfo lists no other kind, so the fix is one assignment plus filling TpType from the transport manager.
4856 closed the gap one layer down: a leg with no Hops never reached the renderer that prints transport type, id and RTT, so the direct path still showed a bare public key underneath the visor. The visor now attaches a single synthetic hop, self to remote, carrying the real transport id, type and the ManagedTransport’s own measured latency, and every existing renderer works on it unchanged. 4857 fixed what the direct path actually picked: VStreamMux.Dial stopped at the first non-dmsg transport a map iteration happened to yield, so a visor holding both a stcpr transport and a seventh-choice webrtc transport to the same exit had been running a proxy over the worse of the two with nothing in the logs to explain why. It now reads routing.transport_preference, the same order the router itself applies, so the direct and routed paths agree on what “best” means.
Skywire: Fields That Were Never Assigned
4852 also shipped the tool that found the rest of the day’s cleanup: make check-dead-api-fields flags a json-tagged struct field that encoding/json reads — so it looks used — while nothing in the tree ever assigns it, leaving an API to report a zero value forever. It exits 2 rather than 0 when it matches nothing, after a first version silently passed with a deliberately planted dead field left undetected; a checker that passes when it can’t see anything is worse than none. It is currently clean at 159 fields, having already caught the three it was built to explain.
4849 was the one that prompted it: Overview.Hypervisors and ConnectedHypervisor are declared and assigned nowhere, so every visor — including one with three hypervisors configured and connected — reported "hypervisors": null, "connected_hypervisor": null forever. A state field that always reads empty is worse than an absent one, since callers trust it and conclude there are none; both are now populated from the config and the live connection map, sorted so a diff between two calls doesn’t reorder on Go’s randomized map iteration alone.
4850 found the same shape in the resolver CLI: EmbeddedProxyInfo.WebAddr is a column in cli resolver and cli visor proxies that has printed - on every visor, always, because it documents a listener — dmsg web’s standalone mode 2 — that the embedded resolver never opens by construction. There was nothing to populate it with, so the field and both columns are gone rather than left permanently blank. 4851 removed skybian_build_version the same way, chasing a chain that read an environment variable, rendered behind an Angular @if, and never connected the two; Skybian is a retired Skycoin OS image and nothing else in the tree touches the variable.
Skywire: A Module That Lied About Its Age
4845 chased down an unsettling failure mode: go build . embeds whatever compiled wasm blob is already sitting in pkg/wasmhv/execwasm/blob, and only make build-embedded regenerates it, so a freshly installed binary can serve a command module from days earlier while reporting its own, current version string. Two merged fixes appeared to have no effect, because the module built to test them was not the one actually running. The module is now built with -buildvcs=true, its revision recorded beside it at embed time and compared at serve time against the running binary’s own commit, with make check-exec-wasm able to answer the same question without building anything. One trap survived testing: a vcs.revision= line’s prefix is not anchored at the start of the line, and an anchored extraction recorded an empty revision that then read as permanently stale.
That warning needed somewhere to land. 4846 found ServeWasm building its config without a logger, so it fell back to a standalone package logger whose output never reached the visor’s own log file — grep -ic wasm ./local/log/skywire.log returned zero on a visor serving :8443 correctly the whole time, silently swallowing the new staleness warning along with everything else wasm-serve reports.
4847 gave the same fact a form someone could query after the fact rather than catch scrolling past in a rebuild loop: cli visor state --select modules now carries exec_wasm, naming the embedded commit, the binary’s own commit, and whether they differ — Stale is only asserted when both are known, since a source build or a dev binary can record neither, and calling that stale would send someone into a 176MB rebuild for nothing.
4858 was the same-day correction: the revision read landed in a file shared between both build targets but behind a native-only build tag, so make exec-wasm failed to produce a module at all on GOOS=js — caught only because the staleness check it was meant to support ran straight into the bug blocking its own target. The day’s other observability addition serves the same instinct for evidence over assumption: 4848 mirrors every log entry to a skywire.jsonl file beside the text log, opt-in and additive, giving fields like module something jq can select on directly rather than grepping a format that flattens them into the message — for boot ordering and crash sequences, the kind of transient history a visor state snapshot cannot express by construction.
Skywire: dmsg Housekeeping
4840 closed a visibility gap in deployment auditing: a visor-folded dmsg server builds its config from dmsg.DefaultServerConfig() and never set Version, so its discovery entry reported the default "0.0.1" on six of nine production servers, with no way to tell from discovery which had picked up a given fix, while the two still running standalone reported real version strings. The folded path now exports and reuses the same helper the standalone server already had, so both advertise what the binary and /health actually report. 4859 found that a dmsg server which rotates its IP leaves a stale address baked permanently into a running visor: initDmsgHTTP already merges the on-disk dmsg_servers.json cache over the configured list for exactly this reason, but the session path skipped it and pre-seeded the client’s entry cache straight from config, so a visor pointed at a decommissioned host stalled roughly fourteen seconds and failed with dmsg error 202 on every dial that selected it — read in monitoring as an intermittent DOWN on a service that was healthy the whole time.
4861 widened the hypervisor-RPC upgrade off the dmsg relay, which had hardcoded a try-stcpr-then-sudph order and gave up entirely on a visor with neither — exactly the shape of a browser visor, which opens swtr/swsr/webrtc and nothing else, and so never upgraded off the relay for the life of the process. It now walks the full transport_preference order instead of a hardcoded subset of it.
Skywire: Config Knobs for What Operators Already Wanted
4854 let the visor host more than its fixed pair of resolving proxies. dmsg_web and skynet_web have always been the only two, addressed by name throughout the CLI, status pages and RPC surface, so turning them into an array would invalidate every config and caller on disk for no gain; resolvers is instead a purely additive list beside them, absent by default, with each entry naming its own knobs explicitly rather than embedding the two existing configs and silently ignoring fields — secret_key, forward_proxy — that don’t apply to its kind.
4860 gave hypervisor.enable_auth a skywire.conf variable, HVAUTH, closing a gap where a fresh config generate always turned the password gate on and an operator with no prior config to retain a setting from had no way to say otherwise; an explicit --auth/--noauth still wins, and an unparseable value is warned about rather than silently resolved, since guessing wrong there either exposes an unauthenticated UI or locks the operator out.
4841 fixed a default that had already cost a host its disk: the shipped systemd unit hardcoded --loglvl debug against a visor default of info, and one production host hit 100% disk with 19GB of mostly-uncompressed syslog, wedging its own auto-updater behind a 2GB free-space guard it could no longer clear. Existing installs keep the flag until their unit is reinstalled or LOGLVL is set and autoconfig re-run.
Skywire: Small Fixes to the Desk and the Mesh
4844 moved the PWA install prompt out from under the desk panel, where the page shell had drawn it as a fixed button in the exact corner the panel occupies — the single affordance for installing the desk was obscured by the desk. It now publishes the offer on window.__skywireInstall and the desk lists an Install Skywire entry among its other apps, held until the desk is ready to read it, since the browser’s beforeinstallprompt fires roughly a second into a boot that takes far longer inside a 170MB wasm module.
4843 found the manager UI’s geoip lookups being blocked outright on the :8443 HTTPS harness — nine mixed-content rejections at once, one per dmsg server — because the configured URL was still plain http:// for a resource the browser itself fetches to place each server on a map; both backing hosts already serve the identical JSON over a valid TLS chain, so the fix is the scheme. 4842 chased down why remote command sweeps looked flaky: pty exec’s Ctrl-C watcher raced the command’s own successful exit through a shared context, so seven of twelve clean runs on a healthy host reported exec: interrupted with exit 130 appended after the real output had already been written. Tracking completion explicitly before the watcher’s context closes fixed it at zero of twelve — a healthy host had been reading as unreachable.
Skywire: Writing Down What the Code Already Assumes
Two documentation gaps closed on the same day. 4855 added a 254-line glossary of the vocabulary the codebase already uses for keys, remote access, config and components, rescued from a pre-rewrite worktree branch that would otherwise have been lost in the history rewrite. 4853 wrote down the c<layer>-<domain>-<system> file-header scheme nearly every Go file already carries but that nothing in the repository explained, adding make check-descriptors to enforce that every non-test file carries one and that every file in a package agrees with its neighbors — deliberately not a lint rule on which domain a package belongs to, since that’s a documentation grouping with no Go analogue, only on the two mechanical facts: presence and internal consistency. Current drift is listed rather than hidden: 96 of roughly 1580 files carry no descriptor yet, and 12 packages disagree internally.