Windows runner (experimental) - #18
Merged
Merged
Conversation
First step toward a Windows runner. GOOS=windows now builds and vets, with real Windows implementations for the primitives the runner needs: - fsidentity: volume serial and file index, the identity os.SameFile uses - filelock: LockFileEx behind one API, replacing direct flock calls - durable: directory syncs skip Windows directories, which can't be flushed - renameNoReplace: handle-relative NtSetInformationFile, like renameat2 - fsowner: owner SID check in place of the uid check - proctree: Job Objects; jobs and runtime probes start suspended, join a job, then resume, so every descendant is contained and killed together Unix behavior is unchanged: the process-group code moves behind small processScope methods. Unix-only tests are tagged or skip on Windows, and CI gains a windows-2025 job that vets, builds and tests the packages with Windows support so far. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
- Executable lookup applies PATHEXT, so "cargo" runs cargo.exe and "npm" runs npm.cmd, and accepts either slash in relative paths. - Jobs get the Windows environment programs rely on (profile, temp, system and program folders, PATHEXT, ComSpec), and environment names match case-insensitively. - Arguments cmd.exe would reinterpret when running a .bat or .cmd file are refused with a clear error instead of running something else. - Job cleanup gives the console host a moment to exit on its own. Tests: Windows-only tests end in OnWindows and run in CI; the gitignore oracle skips names Windows can't create; the Job Object test allows a console host in the job. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
Windows AF_UNIX sockets report the peer's PID (SIO_AF_UNIX_GETPEERPID). The peer's token user SID is compared with this process's, so the daemon and its client still only trust the same account. Windows has no numeric uids, so the UID field only records whether the SIDs matched, and the account name comes from the SID. Also keeps executableFile where it was in placement.go, now Unix-only, with the shared placement code in placement_facts.go. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
serviceruntime keeps a retained errand.exe copy on Windows and checks ownership instead of POSIX modes. unixpeer.ConnectionRefused recognizes a stale AF_UNIX socket on Windows (WSAECONNREFUSED) as well as Unix. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
errand setup on Windows registers a task that starts at sign-in and runs the retained runtime copy with serve --log-file, so replacing errand.exe never fights a running daemon. Setup stops a running task before re-registering it, verifies an answering runner is the task's process, and refuses to replace a task definition it did not write without --force. New Windows runners default to the tailnet transport because SSH callers are not supported there yet. Tailscale discovery also looks in Program Files, and doctor knows the task definition's path. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
- fsmode: NTFS has no POSIX modes, so Windows reads files as 0644 (0444 when read-only) and directories as 0755, and mode checks compare only the write bit. Collected results inherit each path's submitted mode, so exec bits from a Mac survive a job and only real edits come back. The errand_logicalmodes build tag exercises this path on Unix. - archive: on Windows, reject paths and symlink targets Windows would read differently (backslashes, colons, device names, trailing dots or spaces, GIT~N). - Named caches are off on Windows runners for now. - Release: GoReleaser builds windows amd64/arm64 zips, the release workflow uploads and checks them, RELEASING.md describes them. - docs/WINDOWS.md covers install, setup and how Windows jobs differ. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
The named cache store refused its root because Windows directories report mode 0777, so the daemon could not start on Windows. Private now means owned by the current user there, as it does for the runtime directory. A Windows-only daemon test runs a cmd.exe job from a client snapshot and checks its exit code and retained changes. The temporary CI survey now prints failure messages. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
The setup tests drive a fake system with Unix paths, including the Windows task decisions, so they run on Unix. Two runtime tests rely on POSIX modes or exec and skip on Windows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
- Change-base capture closes its handle on the staging tree before renaming it; Windows refuses to rename a directory with an open handle. - Staged directories are flushed through durable.Sync, which skips the directory flush Windows can't do. - placement.go is shared again and matches main except for the per-OS calls, so code scanning doesn't report its existing lines as new. - serviceruntime and setup tests run in the Windows job. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
Owner
Author
|
Generated by Claude Code |
Windows refuses to delete a file or directory while a handle to it is open. Two runner paths deleted through a handle they still held: - changes.RemoveTree removed the tree's root while its os.Root was open. That broke startup recovery of interrupted change collection and cleanup of failed workspace uploads. It now empties the tree through the root, closes it, re-checks the root's identity and removes it. - The blob cache deleted a corrupt blob while its read handle was open, so the blob stayed and every later job re-read it. It now closes the handle first and compares identities captured from the handle, not a path-based FileInfo that Windows resolves lazily. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
The Windows release step (empty through the root, close it, re-check the root's identity) moves to remove_windows.go. RemoveTree keeps main's removal by path, which on Windows now finds an empty, released directory. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
Testing setup on a Windows 11 ARM VM found four failures: - A fresh install stopped before creating any state: dialing the runner's socket in a directory that does not exist yet reports WSAENETDOWN, not a missing file. It now counts as no listener. - An administrator's token without UAC filtering makes BUILTIN\Administrators the owner of everything it creates, so errand's own runtime and cache directories failed the owner check. Files owned by the token's default owner now count as the user's. - Verifying the scheduled task runs PowerShell, which takes 2-3 seconds to start, inside the probe's 2 second deadline. It now has its own 30 second budget, and doctor's service check gets 10 seconds. - On upgrade, the new runner started before the stopped one released its state lock. A Windows runner now waits up to 10 seconds for it. The runner also once exited with status 2 and nothing in its log. With --log-file, fatal runtime errors now go to the log, and a Windows runner logs the console event that stops it instead of exiting silently. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
A runner started by Task Scheduler detaches from its console, which closes the console's handles. Two things still named them: the standard handles, and the *os.File values serve replaced with the log file. When the garbage collector finalized those files, it closed whatever objects had reused the handle values since. On a Windows 11 VM that crashed the runner with "netpoll failed" during its second job after every start, which was also the unexplained exit status 2. The replaced files now stay reachable, and after detaching, the standard handles point at the log and NUL. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
The survey listed which packages failed on Windows while the runner was built. The archive package now passes, so it joins the supported list; the rest still assume Unix fixtures and are covered by their OnWindows tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
lydakis
marked this pull request as ready for review
October 1, 2026 00:41
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
A Windows runner has no console of its own, so every console program it starts gets a new visible window. The tailscale CLI runs for each request to identify the caller, so the desktop showed a Windows Terminal window flashing for every peers call, job and file transfer. nowindow.Hide starts a program with CREATE_NO_WINDOW. The tailscale CLI calls and git merge-file use it, and proctree.Prepare now shares it.
Client commands on Windows got as far as submitting, then failed on a Unix-only state permission check. They now stop at once and point to docs/WINDOWS.md; runner commands are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
The directory-link change turned the existing os.Symlink line into an else branch, which CodeQL reported as two new path alerts. Windows links now return early, so that line and its context are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
The workspace version of this test failed on Windows CI: the client's local state check reads POSIX modes, which Windows reports as writable by everyone. The Windows client isn't supported yet, so the end-to-end check now runs as a job, and an archive test covers a pushed link whose target is known only from the complete manifest. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
The runner rejects job environment variables without provenance, so the submission failed with 400 before the PATHEXT case ran. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No
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.
Requested by George · project thread
Before: errand builds only for macOS and Linux. A Windows PC can't be a runner.
After: a Windows 10/11 PC runs
errand.exe setupand takes jobs from a Mac over Tailscale.errand --on winpc -- cargo teststreams logs and returns the real exit code, Ctrl-C ends the whole job tree, and changed files come back with their Mac file modes. Releases include Windows zips. It is marked experimental and documented indocs/WINDOWS.md.Tested on a real Windows 11 ARM64 machine (a VM, driven from a Mac over Tailscale):
errand.exe setupwith no manual steps, an upgrade to a second build, and a re-run of setup with the same build all exit 0 with the runner task running.cmd /c verwith file round trips: exit codes come back, new files return as 0644, and a modified 0755 script keeps its mode.ping -tleaves nothing running,.cmdshims run and a%argument is refused, the installed binary can be replaced while the runner runs, and the runner stops at sign-out and comes back at sign-in.What's in it
filelock,durable,fsowner,fsidentity) so shared code stays platform-neutral..bat/.cmdarguments thatcmd.exewould reinterpret are refused.errand setupregisters a Task Scheduler task that starts at sign-in and runs a retained copy oferrand.exe, so replacing the installed binary never fights a running daemon. New Windows runners default to tailnet only. Tailscale discovery also looks in Program Files.internal/fsmode). Collected results keep each path's submitted mode, so exec bits survive and only real edits come back. Theerrand_logicalmodesbuild tag exercises this path on Unix.CON,aux.c), trailing dots or spaces, orGIT~Nare refused, as are symlink targets with backslashes or drives.errand.exe.Unix behavior is unchanged. The Unix paths moved behind small per-OS files (
processScope,fsmode,serviceruntime,unixpeer), with the same logic. Two shared fixes apply everywhere: retained trees and corrupt cache blobs are deleted only after their handles close.CI. The Windows job runs the full tests of every package that supports Windows, plus
OnWindowstests elsewhere, including a full job round trip through the daemon and a detached-console runner under constant garbage collection. Other packages' tests still assume Unix fixtures (/bin/sh,/tmp) or client code that isn't ported yet.Known follow-up: if a runner dies mid-job, the client waits instead of failing quickly.
How: see
docs/WINDOWS.mdfor the user-facing behavior. The plan is in the project thread.🤖 Generated with Claude Code
https://claude.ai/code/session_01Qzjh5YFvH2uew2AQsqp3No