Repository navigation
Audit fixes: updates after a stream, Docker trust, pairing, input order, clipboard, power, config, service lifecycle, Windows setup, CI - #28
Open
MrBeldum wants to merge 31 commits into
Conversation
…quester's own pairing A viewer that disconnects leaves its Desktop launched for resuming, and Sunshine keeps reporting it as currentgame until the engine restarts. Since PR #26 read that as "a stream is running", a Windows host refused every pushed update after the first ordinary stream, and the Mac re-sent the whole exe every three minutes. Whether someone watches now comes from the engine log's last session line (CLIENT CONNECTED against CLIENT DISCONNECTED or a Ping Timeout, which is all a sleeping Mac leaves), checked against /serverinfo. An unknown never blocks the update, which does not touch the engine; the stream-profile upgrade, which restarts it, still waits on unknowns and closes a Desktop nobody watches before it restarts. The PIN went to whichever pending pairing Sunshine listed first, which can be another device's or an abandoned one, and an empty 2026.10 list made an id-less post that engine always refuses. The request is now picked by the caller's address (by name where everything is loopback), and an empty or foreign-only list answers "waiting" without posting. Only posts with a pairing id get the 600 ms "taken" rule. Refusals are logged once per reason instead of once per retry, and engine calls run curl with -S so a failure says why instead of "curl: ". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…connection The Docker node runs tailscaled with --tun=userspace-networking, which relays every tailnet peer's TCP connection from 127.0.0.1. The service took any loopback connection for its own window, so any machine the ACL let reach port 47850 skipped the owner check: it could approve its own pairing PIN, read the clipboard, power the node off, read the service log and quit it. Status now reads Tailscale's TUN flag (kept once seen, and read before the first request). With TUN false, a loopback request is judged after it has been read, when tailscaled has recorded the relayed peer: `tailscale whois --proto=tcp 127.0.0.1:<port>` names it and Owner::admits decides, never as local (no log, no /v1/quit). Only a port Tailscale knows nothing about is a local process; a failed lookup admits nobody. Loopback answers are never cached, since every relayed peer shares 127.0.0.1. Kernel-TUN machines (the Mac, Windows, stuzha) keep today's behaviour with no extra whois. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…paths unknown, fix DERP ids
- `tailscale status --json` writes empty maps and lists as null ("Peer":
null on a machine alone in its tailnet; Peer, User and Self.TailscaleIPs
when signed out). The parse failed, so a fresh host showed "parse
tailscale status" instead of working, and a signed-out Mac never said so.
Nulls now read as empty, and the state reads "signed out", not
"needslogin".
- public_ipv4() took any CurAddr, so two machines on one LAN overwrote the
stored wake address (the router that forwards UDP 9) with a LAN address.
Only internet addresses count now.
- direct() called every idle peer relayed, because Relay (the home DERP) is
set for all of them; a direct PC flashed "Relayed via Los Angeles" at
every launch. Without traffic the path is unknown, as documented.
- machine_peers() listed Mullvad's exit nodes (hundreds with the add-on)
and machines this one is shared out to, which `tailscale status` hides.
- The DERP id table was off by one from 14 up (Amsterdam was missing), so a
Mac in Los Angeles logged "nearest relay Paris". The table now matches
1.102's derp-map, and the host prefers the home region Tailscale names
in its own status.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fixes apply() only replaced egui's dark style. egui picks the style by the system theme every frame, so a Mac or Windows PC in Light mode drew with egui's stock light style, which has no caption or title text styles, and the first caption panicked: Latch quit at launch, or mid-stream when Auto appearance flipped at sunrise. The theme is now pinned to dark and both slots get Latch's style. Menus: an Escape that closes a menu or a select list is used up, so the stream no longer forwards it to the PC and Stream settings no longer closes behind a list. A menu is known by its label instead of its button's position, so a row added above no longer hands an open "More for X" menu to another machine, and a menu whose button is no longer drawn (the toolbar folded into More, the stream ended) is closed at the end of the pass instead of blocking stream input forever. Accessibility: a select is named after its setting row and reports its choice; the chosen option in a list is drawn in full-strength text (the accent on the accent wash was 3.7:1); the bitrate slider takes a screen reader's increment, decrement and set-value, and reports its range. A spinner in a window that is not in front slows to four frames a second, and the system's Korean/CJK font is the last fallback of every family, so a Korean PC's error messages are no longer empty boxes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clipboard::fit capped raw bytes at 32 KiB, but a quote or newline takes two bytes in JSON and other control characters six, so 32 KiB of newlines or control-heavy text became a body over the 64 KiB limit both sides enforce: the PC's copy never reached the Mac (the reply was refused as too large, silently, until the PC's clipboard changed) and a paste failed with "body too large". fit now counts the escaped length in the same pass and stops at 60 KiB of it, leaving room for the other fields; a test pins the per-character lengths to serde_json's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rvice keeps its last good host.toml load() set the whole file aside and returned defaults on any parse error. A client.toml with resolution = "P720" read by a build without 720p (4.1.1, or a branch cut before PR #26) lost every pairing certificate, Hermes's wake MAC and the GitHub token. Well-formed TOML that does not fit is now salvaged key by key (a whole table when it fits, else its parts), the original kept as .bad-* beside it and the readable part written back, so only stream.resolution falls back. Text that is not TOML at all goes aside as before, and so does a file that is not UTF-8. A read error other than "not found" (permissions, a sharing violation) counted as a missing file, and update() then wrote defaults over it. Such a file is now an error for update(), which leaves it alone. The service re-read host.toml every 5 s with that loader, so a typo in a hand edit on a headless node moved the file away and ran with remote power allowed and no engine login. It now uses a strict try_load that never moves anything, treats an empty file (an editor mid-save) as unreadable, keeps the last good settings, and logs once per problem and once when it is fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PIN went to Latch Host on the PC in front of the first pairing
request: the handshake could only go on once that loop returned, up to
75 s later, whether or not the PC had long answered (the stuzha stall
with Sunshine 2026.10). Now the request is polled every 100 ms and the
hand-off runs on its own thread beside it: the handshake goes on the
moment the PC answers, however the PIN got in, a first request that
fails shows its error at once, and Cancel works throughout. A wait that
runs out reads "nobody entered the PIN" instead of a bare "timed out".
The host's reply is read whatever its status, so its refusal reason is
shown ("isn't answering" is kept for silence). A refusal for good (400,
403, 422, twice in a row) ends the attempt with the host's reason; a
host that cannot log in to its engine gets 15 s, then the card says to
type the PIN there; passing refusals are asked again less and less
often. Tests run the handshake against a fake host on loopback ports.
Connecting: a cancel no longer quits an app it rejoined, nor one that
was running before anything was touched; failures are logged with the
step and the whole cause chain, and the banner shows the chain, with
each step naming itself ("Asking stuzha to start the stream: ...").
The Desktop is relaunched for its size only where the desktop follows
the viewer (a Linux host without a fixed size), and the size it was
launched at is kept in client.toml so an app restart does not relaunch
it. Saved machines that have left the tailnet for 30 days are forgotten,
guarded by the tailnet they were seen on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A self-update that failed set the release aside and skipped the host pushes until the next check, then downloaded the same 5 MB again every 15 minutes: a release whose Mac build cannot install (a version that does not match its tag, say) kept every PC on the old host. The failure is now remembered on its own: the pushes go on, a failed download is tried again after 15 minutes, and a download that cannot work is not fetched again until the release or its asset changes. The app that is installed is the one just checked: the tarball is re-verified against GitHub's digest at install time and unpacked straight into the staging folder beside the running app, where its signature and version are checked, instead of reusing whatever an earlier run left unpacked under the same tag. manual_update_message says what to do about a Linux host or container behind the release, which this Mac does not update (the lobby shows it from the next commit). The stream handover finds a pre-3.1 host under its BroLink process name and path and replaces it in place, so its firewall rules still apply, and the GitHub token is looked up on the handover's thread, not the window's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Power actions found their machine by display name, which a tailnet does not keep unique: Restart, Shut down and the sleep offer now go by node id, and a machine that has gone away gets a notice, not a request to its namesake. A power action on one machine no longer cancels a connection to another, and the stream from its own machine ends only once the host has agreed, so a refusal leaves it up and is shown over the picture. The stream's machine menu follows the owner's power setting (no menu when it is off), asks before Sleep as before Restart, and offers Sleep only for a Windows PC: a Linux VPS or a Mac would stay asleep with nothing here able to wake it. The lobby row menu and the sleep offer after a session follow the same rule. A machine that is away can be forgotten from its row menu, after a question that says what goes with it (its pairing, how to wake it). A Mac with Share this Mac off reads "only connects to your other machines", not "the background service is stopped". Hosts this Mac cannot update are named once a newer release is out. The PC is asked why only about a black picture, never about video that stopped or cannot be decoded, and a Linux or Mac host, a failed ask or an empty answer all leave an answer, so "Asking the PC why…" no longer stays up. A display change reconnects only when it changes what the stream asks for. Full screen follows what the system reports (the green button), survives a reconnect without leaving and coming back, and the window returns to what it was before the stream. Stream failures and abnormal ends are logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, survive bad updates Starting and restarting: - The window started the service as its own child, outside launchd, so a crash was never restarted, and Share on started a second copy beside launchd's (whichever lost exited 0, and launchd then left its job stopped). ensure_service_running now asks the login item first: launchctl bootstrap/kickstart (no -k) on a Mac, systemctl --user start --no-block on Linux, a bare copy only where there is none. After an update the window quits the old service, waits for the port, and starts it the same way. - Turning Share on used legacy `launchctl load`, which silently skips a label macOS marked disabled (this Mac's state: not loaded, disabled, the service running unsupervised). It now enables the label, bootstraps it, and reports when macOS still does not load it. - A Mac or Linux install replaced under a running service (same version, so the window saw nothing) kept serving old code indefinitely. The service notices its program file changed and, once nobody watches, restarts into it: by exiting non-zero under a supervisor, or by handing over to the new file. Respawn storms: - The window restarted a service that exits by design every 4 s for hours (6,438 spawns overnight on 2026-10-12) because it never re-read host.toml. ensure_service_running and the poller now check that the Mac is shared (the window becomes a viewer if not), retries back off from 2 s to once a minute, a copy that stops as it starts is reported with its exit status, and the --background error reaches service.log. - A window opened from an older copy relaunched the same old file every second, forever. It relaunches only when its own file was replaced, and otherwise says to open Latch from Applications/the Start menu and leaves the service alone. Updates: - The rollback only caught a new exe that died within 2 s while it was still waiting for the port, and the new service deleted the last good exe as soon as it bound. The old service now starts a watcher from the retired exe (--probation) that puts it back unless the new version answers /v1/status and keeps answering; the retired file is removed only after ten minutes. - host.toml accept_updates (default on, a toggle on Windows) refuses pushed updates; accepted and refused pushes are logged with sender and SHA-256. A panic hook appends the panic and its place to service.log/panel.log before panic=abort ends the process. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When Latch Host entered the PIN but the engine never answered the first request, the timeout said nobody entered it; it now says the engine never finished. The wording of a failed pairing (a refusal in the host's words, a timeout with its reason, else the handshake's error under "Pairing with <PC>") is one function with a test, and a power confirmation that finds its machine gone says it may also have stopped taking power actions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stream view translated a frame's events with the frame-end state, so order inside a frame was lost: modifiers were synced once before the keys (Shift let go with T in one frame typed "t", ⌘Z with ⌘ let go sent a bare Z), and a click went out before the frame's pointer position or captured movement. Each key and click now goes with its own modifiers and position, in order; the frame-end state follows for modifier-only changes. Horizontal scroll had the wrong sign (egui's +x scrolls left, Moonlight's right). Pastes: the chord was pressed by a worker after the POST, so Return typed after ⌘V reached a PC 150 ms away before the paste. The worker now only sends the text; the window presses the chord through Held and holds everything typed or clicked after it until the text has arrived (at most 2 s, then the PC pastes what it has). ⌘V with an image or nothing on the Mac clipboard was swallowed whole by egui-winit; its V release now sends the paste. A copy made on the PC (files, an image, or text not yet read after a ⌘C) is no longer overwritten by the Mac's older text while the Mac clipboard's change count is unchanged. A paste over 32 KB is sent cut, says so, and its cut copy no longer comes back over the Mac's clipboard; a Windows PC's CRLF read-back is no echo either. Host key and capture: the host key lets go of everything held before toggling (a key let go during the chord stuck down on the PC), closes a panel, question or menu before capturing, waits for both keys to be up (no lone Alt), and can free a capture before the stream connects. A click on the picture no longer captures the mouse while connecting. In free mode egui gives up focus while the toolbar is hidden, so Tab reaches the PC instead of the Stats panel. Mac: while the stream has the keys, the app menu's Quit, Hide and Hide Others lose their shortcuts (⌘Q quit Latch mid-stream with Command as Ctrl) and egui's ⌘+/⌘-/⌘0 zoom is off. A Mac host gets ⌘ as Command whatever "Command acts as Ctrl" says. The keypad's + sends VK_ADD instead of the = key. The translation sends through a Sink trait, so tests record what the PC would see: order, masks and signs for each case above. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…estarts fail, keep a shared Mac awake - One click on Sleep for stuzha (an OCI KVM guest, s2idle, no wake source, polkit allowing it) would have taken Tailscale, SSH, Latch and the Hermes agent down until someone reset the VM from the cloud console. The host now refuses Sleep on a Linux VM and on a Mac (Latch has no wake path to either), refuses every action in a container (no systemd), and asks logind before ending anyone's stream. Restart and Shut down stay, behind power_allowed as before. A refusal comes back as an Ack with the reason, which every Mac client shows as it is. - On a Mac, Restart and Shut down go through System Events, which needs an Automation permission macOS asks for on the Mac's own screen; the Mac that asked was told it worked. osascript now runs before the reply, at most 3 s, and its failure is reported in words (not allowed, nobody answered the prompt, an app cancelled). Failures of the actions that run after the reply reach the service log instead of only stderr. - keep_awake did nothing off Windows while the log said "keeping this PC awake" at every start, on stuzha and on a shared MacBook that went on to idle-sleep off the tailnet. A shared Mac now holds a PreventUserIdleSystemSleep assertion while plugged in (not PreventSystemSleep, which turns a requested sleep into dark wake), the switch shows on the Mac's Sharing page, and the log speaks only when it starts or stops being true. - The wake-packet listener ran everywhere but can only match a MAC on Windows: it held UDP 9 on every interface of the Mac and logged a permission error at every start on Linux. It is Windows-only now, and the Sharing page no longer shows "LAN address None" where none is known. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… for Latch - On a native Linux node the engine is latch-engine.service, with the desktop's environment and its CPU priority. Setup pkilled it (a clean exit, which Restart=on-failure does not restart) and started its own copy; run over SSH that copy had no DISPLAY, bound its ports and said "started" while every stream failed. With the unit present, setup now writes the config and restarts the unit (latch.service follows through Requires=), and the service's keep-alive starts the unit rather than a copy beside it. - The macOS firewall blocked incoming connections to the ad-hoc signed Latch.app on this Mac, so other machines could not reach the control service at all (no PIN entry, no status) while its window said "Shared". The service now notices and lists it as a setup item, and setup asks for an administrator to allow Latch through, verifying after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…om taking it over This Mac's launch agent was marked disabled in launchd while its plist existed and host.toml said shared: the toggle showed on, the service ran with nothing to restart it, and it would not have come back after a restart. The service now lists that as a setup item, and setup (or turning Share on) enables the label even when the job is loaded. The service rewrote the login item to whatever copy it ran from, so a build started from dist.noindex, a disk image or App Translocation took it over. It now points it only at an app installed in /Applications or ~/Applications; turning Share on from any copy still does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tate A 4.0 PC's engine folder, with the pairing state in its config, was moved to C:\Program Files\Latch\engine with one Move-Item. A held handle (an antivirus scan, an Explorer window, an engine restarted in the gap) failed the move, setup then installed a fresh engine with a new identity and no paired devices, and the old state stayed stranded where nothing read it again. A part-filled Latch\engine left by an earlier failed run made the move nest the old folder at engine\engine, with the same result. The move is now tried five times, stopping any engine that runs from the old folder before each try. A folder in the way is removed when empty and kept aside as engine.partial-<time> otherwise. If the move still fails the old engine stays put and $retiredState names its config: the install step copies the state (server identity, paired devices, apps, settings) into the fresh engine before it is configured and registered. Nothing throws, since the old firewall rules are already gone and setup has to go on to open the new ones. The fragment takes its old layout as parameters, so a test can dump it for temporary folders and a dummy service (LATCH_DUMP_RETIRE_*); the Windows CI job runs it there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…TEM engine beside it Run over SSH (session 0, as the Hermes migration does), setup's engine take-over stopped the working service engine and started sunshine.exe in session 0, which has no desktop: a black stream that nothing repaired until a reboot. The helper now refuses to act from session 0 (NOT_INTERACTIVE). Setup run there installs the LatchEngineUser logon task for its account, stops the service and every engine, and has the task start the engine in the signed-in user's session; if no engine appears in 30 s (nobody signed in) the LatchStream service runs it again. A take-over that fails from the desktop session also leaves the service engine running instead of none. Re-running setup with the user's engine up started the stopped LatchStream service (Restart-Service starts a stopped service), and the helper looked only at the first sunshine.exe listed, the user's, so the SYSTEM engine stayed beside it on the same ports. The restart now touches only a running service, and the helper weighs every engine process and the service state: anything not this user's in this session, or a running service, is taken over. Without administrator rights it stops before killing the user's own engine. Setup runs it forced, so the user's engine rereads the login and settings setup just wrote. install-host.ps1 stopped the running host and, with -NoStart or over SSH, left none running until the next logon (or started one in the SSH session). The background service now always comes back: through a one-off Interactive, limited scheduled task with no time limit when run from session 0, as --background otherwise; -NoStart only keeps the window shut. Also: - Setup clicked during a stream is turned down once with an explanation: the UAC prompt sits on the secure desktop, which a stream cannot show or click, and setup restarts the engine anyway. A second click within two minutes runs it. A viewer counts as connected while the engine's last session line is CLIENT CONNECTED (sessions also end on a Ping Timeout with no disconnect line, as stuzha's log shows). - An engine archive beside the exe that is not the pinned release (left by an exe-only update after an engine bump, or cut short) is skipped for the pinned download instead of failing every later setup. - The audio take-over, which compiled C# every 30 s for as long as the engine had no sound (forever on a PC with no playback device), backs off from 30 s to 10 minutes per complaint and compiles only when there is a device to set. - /v1/display lists the engine's service under LatchStream, BroLinkStream or any name whose program is sunshinesvc.exe; it matched only *unshine*. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r good On X11 arboard keeps one connection for the whole process. When the X server restarts under a Latch Host that keeps running, that connection is dead, and dropping the handle (what the code did on any error, saying it would be replaced) hands back the same dead context on the next call: every /v1/clipboard read and write failed until someone restarted the service by hand, and a Mac paste fell back to whatever the X selection held. Only a new process gets a working clipboard. After two unknown arboard errors in a row, if a fresh connection to the display works, the service logs and exits 1 for systemd (Restart=on-failure, in the user unit Latch writes and in deploy/native) to start it again. When the display itself does not answer, nothing is restarted, so a desktop that is down cannot cause a restart loop. macOS keeps the old behaviour: it has no connection to lose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…updates, desktop after logout Docker node: tailscaled ran in userspace mode, which hands every peer's connection to Latch Host from 127.0.0.1. Latch Host trusts loopback as the machine itself, so any device that could reach the node over the tailnet could pair its own client, use the clipboard and stop the service without the owner check. The node now runs tailscaled with kernel networking (/dev/net/tun, NET_ADMIN and NET_RAW in the compose file), so peers keep their own address, and refuses to start Tailscale without the device rather than fall back to userspace. The selftest fails on userspace mode. .dockerignore excluded only the root .env, so deploy/node/.env and deploy/relay/.env (Tailscale auth keys) went into the node's build stage, which `docker build --target build -t ...` keeps as an image. Every .env and .env.* is excluded at any depth now, with dist.noindex, app bundles and __pycache__; the node selftest checks it. The image builds with --locked. Relay: Compose fetches tailscale/tailscale:latest only when it has no copy, so the internet-facing relay kept the tailscaled it was first installed with (1.102.3 on sj-instance, two releases behind). install-relay.sh pulls the image before `up -d` (a failed pull keeps the local copy) and prints the version it runs; RELAY.md says how to update. Native desktop: XFCE's Log Out ends xfce4-session with status 0, which Restart=on-failure left stopped, so every later stream showed an empty screen until someone started the unit over SSH. latch-desktop.service now restarts always, slowing from 2 s to 2 minutes instead of giving up, as stuzha's drop-in already does. The README says how to install the Linux host from a release and how a unit change reaches a running node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rsion, --locked, retire step run on Windows Both production Linux hosts (stuzha and the relay VPS) are arm64 and ran hand cross-built binaries that no CI job had compiled for their architecture; an i8/c_char mismatch once reached the VPS that way. CI now runs clippy, the tests and a release build on ubuntu-24.04-arm as well as x64, and the release workflow builds, tests and publishes latch-host-linux-arm64.tar.gz and latch-host-linux-x64.tar.gz (the host, LICENSE, NOTICE and deploy/native), each with a .sha256 beside it. A tag pushed before the Cargo.toml bump published a release every Mac downloads, rejects for its bundle version, deletes and fetches again every 15 minutes. The publish job now fails unless the tag is the workspace version, read the way bundle-macos.sh reads it, and the macOS job checks the bundle's own version on tags. Every cargo build, test and clippy runs with --locked, so a stale Cargo.lock fails instead of being re-resolved on the runner. The Windows job runs the BroLink -> Latch retire step for real, through Windows PowerShell, on temporary folders and a dummy service: a clean move, a second run that changes nothing, an empty and a part-filled engine folder in the way, and a move that fails on a held file, which must still leave the pairing state in the new engine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…loy kits, CI and release
… UI crate stream.rs: both branches added helpers after machine_noun (W2's power menu helpers, W1's is_mac_os); both are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…back to its idle size The host now checks every other tick whether a viewer is connected and reports it as Streamer.viewer. The Mac skips a host update while it is true instead of sending the whole executable to be turned down, and an older host that still answers 409 (4.0 and 4.1 call any Desktop left running a stream) is shown an Info notice and asked less often each time, up to once an hour, instead of every three minutes. On a desktop that follows its viewer (LATCH_DISPLAY_FOLLOWS_VIEWER, a Linux VPS) a session nobody has watched for two minutes is ended, so the engine runs its prep command's undo and the desktop goes back to its idle size instead of staying at the last viewer's (stuzha sat at 3024x1964 between streams, which an agent's screenshots then paid for). setup.rs's viewer check now reads the log through the streamer's reader, so both treat a "Ping Timeout" as the end of a session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at once Older versions saved whatever address Tailscale last reached a machine on as its public address, which can be its LAN address; a wake packet sent there goes nowhere. Saved addresses are now filtered through tailscale::is_public_v4 when they are read. Peers are probed in parallel: each can take two timeouts when nothing answers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
an_address_that_refuses_is_skipped_for_the_next_one freed an ephemeral port to get a closed address, and another test running in parallel could bind that same port. Port 0 can never be connected to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shared Mac restarts and shuts down through System Events. Without NSAppleEventsUsageDescription macOS never asks, and a hardened-runtime (Developer ID) build is not allowed to send Apple Events at all without com.apple.security.automation.apple-events. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MACOS.md: keys in order, Command for a Mac host, Cmd-Q/H/+ to the PC while the stream has the keys, paste with an image or a file on the clipboard, power items that ask first. Settings help for Command acts as Ctrl, and the manual-update texts point Linux at the release's latch-host-linux tarball. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows takes about a second to refuse a connection to a closed localhost port (it retries the SYN), so the notice was not up yet when the test looked; it failed on the Windows runner. The closed address is now port 0, which no test running beside it can take. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The new Windows CI step caught it: with a file in the old engine held open, every try failed as it should, but Move-Item left the destination folder behind, so the next try moved the old engine into it (engine\engine) and setup then installed beside a nested copy. [IO.Directory]::Move is one rename that happens whole or not at all; should a failed try still leave a folder there, it is kept aside like any other partial one before the fresh engine goes in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a_refused_first_request_surfaces_at_once took 1.07 s on a macOS runner against a 600 ms bound. The bounds now only tell 'at once' from the 75 to 90 s these flows used to take, and fixed sleeps before checking a notice are a wait of up to five seconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
A senior-dev audit of Latch, stacked on #26 (base
fix/vps-stream-clipboard). Sixteen reviewers swept every crate, the deploy kits, CI and the live Mac and stuzha logs. Each finding was then checked by a skeptic told to disprove it, and five fix workstreams worked in separate branches, merged here. The CHANGELOG's Unreleased section has the user-facing list.Must-fix before #26 merges
streamer::session_activecounted a Desktop left running as a live stream), and the Mac re-sent the whole exe every 3 minutes. The host now defers only while a viewer is connected;Ping TimeoutwithoutCLIENT DISCONNECTEDcounts as gone, as seen live on stuzha. It also reportsstreamer.viewerin/v1/status, so the Mac waits instead of uploading. An old host that still answers 409 gets asked with backoff, up to once an hour.tailscale whois 127.0.0.1:<port>whenTUNis false. The node kit runs Tailscale with kernel networking.By area
View::inputnow has a test harness.power_allowed.client.tomlwith its pairing certs, because the parse salvages everything else and keeps a copy of the original. The service keeps its last goodhost.toml.accept_updatesswitch.public_ip(also when already saved), idle peers no longer "relayed", Mullvad exit nodes filtered, DERP ids from 14 up (LA was "Paris"), peers probed in parallel.LatchStream.latch-host-linux-{arm64,x64}.tar.gzwith digests; tag must match the workspace version;--locked; the BroLink retire step runs on a fake layout on Windows CI;.dockerignoreexcludes every.env; the relay pulls current Tailscale.Verified
cargo fmt --check,cargo clippy --workspace --all-targets -D warningsandcargo test --workspace(375 tests) all pass.cargo-zigbuild clippy --target x86_64-pc-windows-gnu -D warnings.httprefusing-address test is fixed.Not verified on real hardware (Hermes is offline)
The draft #27 (Windows cursor) will be restacked on this branch separately.
🤖 Generated with Claude Code