Skip to content

ShadowCode 2.3.0: Windows - #10

Draft
ShadowfetchStudio wants to merge 56 commits into
mainfrom
feat/windows-phase1
Draft

ShadowfetchStudio wants to merge 56 commits into
mainfrom
feat/windows-phase1

Conversation

@ShadowfetchStudio

@ShadowfetchStudio ShadowfetchStudio commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

ShadowCode 2.3.0: ShadowCode runs on Windows.

What's in it

  • Windows build of the whole workspace (engine, desktop app, all test targets) with warnings as errors, on GitHub's windows-latest runner (.github/workflows/windows.yml). All engine unit tests and integration suites pass there.
  • Platform work: named-pipe engine connection limited to the current user; ConPTY terminals (PowerShell); job objects so stopping or closing ShadowCode ends everything a command started; Windows paths (dunce, / in agent-facing paths); %APPDATA%/%LOCALAPPDATA% folders; llama.cpp built with MSVC (CPU); SQLite inspector without following links; vendor runner pipes via PeekNamedPipe.
  • Windows sandbox: agent shell commands run in an AppContainer (project and scratch writable, Windows/Program Files/toolchains read-only, rest of home hidden, network off or on; allow-list refused). Commands run in a bundled busybox-w32 sh built from a pinned commit with one patch (the container may not open NUL, so /dev/null uses a handle the launcher opens). Doctor and Settings report it.
  • Windows release: release-windows.yml builds the per-user NSIS installer with llama.cpp and the sandbox shell, installs it through scripts/install-windows.ps1, requires the doctor's sandbox check, and attaches the installer, the complete busybox source (GPL-2.0) and ShadowCode_<v>_windows.sha256. release.yml dispatches it after publishing.
  • Fixes found on the way: contested settings writes retried on Windows; in-process writers queue for SQLite's write lock instead of polling.
  • Native runtime pins: Alpine 3.21 libcurl/libexpat security updates; libcurl pinned per architecture while Alpine's aarch64 builder catches up (aarch64:libcurl=8.14.1-r2).
  • Version 2.3.0: version files, trust range 0.33.0–2.3.0, release notes and changelog.

Not covered

  • No run on a real Windows PC yet (CI runners only); subscription CLIs tested on Windows with stand-in programs only; local models CPU-only on Windows; installer not code-signed.

Details: docs/WINDOWS_PORT.md.

🤖 Generated with Claude Code

windows.yml compiles the whole workspace on GitHub's Windows runner and
reports every error at once (--keep-going, full log kept as an artifact).
docs/WINDOWS_PORT.md lists the phases and the Windows design for each
Unix-only area (shell, sandbox, job objects, ConPTY, named pipes,
Credential Manager, %APPDATA%, packaging).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

ShadowfetchStudio and others added 21 commits October 17, 2026 19:52
The first Windows build (GitHub runner) compiled every C/C++ dependency
and stopped at 87 errors in the engine and a missing app icon:

- Doctor, the project map and change history no longer need Unix: the
  profile privacy check is per platform and PATH lookups find .exe/.cmd.
- Windows stand-ins where the Unix version is not ported yet, each saying
  so: the vendor CLI runner's types, the drawer's terminals (need ConPTY),
  managed worktree removal (kept, never removed less carefully).
- Agent shell commands on Windows run without a sandbox, with the existing
  warning; Require sandbox and the network allow-list refuse them. They
  use Git for Windows' bash (never System32's WSL bash), else PowerShell.
- The Flatpak relay socket and held-directory paths are Unix-only; voice
  input uses cpal on Windows (WASAPI).
- Tests that need symlinks or #! scripts are Unix-only; script helpers
  skip the execute bit on Windows.
- tauri.windows.conf.json and assets/icons/shadow-agent.ico (16-256 px)
  for the Windows resource file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and remote

The engine's control channel (control/transport.rs) keeps its Unix socket
and peer-user checks; on Windows it is a named pipe that refuses remote
clients, whose only access rule is this user's SID, and whose server a
client checks runs as this user before sending anything. The CLI, MCP,
ACP, plugins, lifecycle and remote modules now build on Windows: the
approval prompt reads stdin on a detached thread, an MCP server's process
tree is ended with taskkill, and the AppImage parent check is Unix-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… modes are Unix-only

The control-socket suites and the fake vendor CLI suites (shell scripts
behind a Unix-only runner) are gated per file, as other suites already
are; single tests that make a FIFO, a symlink or a file mode are gated
per test. Fake gh/glab scripts are only marked executable on Unix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ns; unit tests run

- paths::sync_directory: Windows cannot open a folder as a file, so the
  fsync after a rename (atomic_write, workspace edits, worktree records)
  is skipped there instead of failing every save.
- Workspace file opens never follow a final symlink on Windows either.
- Linking exported rules fails clearly on Windows instead of reporting
  links it never made.
- The Windows job treats warnings as errors and now runs the engine's unit
  tests (phase 2, not yet required), listing the failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hs, memory figures

Found by the engine's unit tests on Windows (554 of 605 passed):
- Remote access tokens and the local model server's key read random bytes
  from /dev/urandom; they now use getrandom (already in the lockfile).
- canonicalize gave `\\?\C:\...` verbatim paths, which Git and people do
  not expect; paths::Canonical (dunce) returns the plain form when it names
  the same file, and is the standard call on Unix.
- Project-relative paths shown to the agent and in approvals are joined
  with `/` (paths::slash). Approval checks compare them as text: with `\`,
  deleting .git\objects was not recognized as unrecoverable.
- The model picker reads installed and free memory on Windows
  (GlobalMemoryStatusEx).
- ACP tool locations are the system's own absolute form.
- .gitattributes keeps LF in every checkout: bundled templates are hashed
  and sources are read as written.
- Tests built on AppImage, Flatpak, bubblewrap or Unix-path fixtures are
  gated to their systems.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fixtures gated

- The worktree advisory lock opens its folder with backup semantics (and
  without following a link): Windows refuses a plain open of a folder,
  which failed every worktree task with "Access is denied".
- Model and runtime file identity on Windows adds the volume serial, file
  index and change time, so a same-size replacement with its time restored
  is still noticed, as inode and ctime do on Unix.
- Hook fingerprints: the "made runnable" step is Unix-only (no mode bit).
- Tests whose fixtures are shell scripts or Unix paths are gated to Unix.
- The Windows unit-test step prints error backtraces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…est-helper errors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kspace listings

- Windows flushes a file (FlushFileBuffers) and changes its time only
  through a handle that may write; paths::sync_file and Compare's index
  copy open one. These failed backups and every worktree task's snapshot
  with "Access is denied".
- Workspace list, read, search and find report project paths with `/`, as
  do profile-relative rule paths, so the file tree, the agent's tools,
  mentions and skill paths match on every system.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What a shell command or a worktree setup script starts joins its job
(process::Job, kill-on-close). Cancelling, a timeout or dropping the owner
ends the whole tree, as killing the process group does on Unix; closing
ShadowCode closes the job too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ine endings in tests

- A profile skill's supporting files are linked beside the staged SKILL.md
  on Unix; Windows needs extra rights for symlinks, so regular files up to
  4 MB are copied instead (folders are not).
- The Windows job turns off core.autocrlf for the tests, whose
  repositories expect the bytes they committed.
- Test fixtures change file times through writable handles, split shown
  paths on either separator, and retry a delete a virus scanner may hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A second job in the Windows workflow builds the NSIS installer (per-user,
no administrator rights; WebView2 is fetched by its bootstrapper when
missing), checks that the built app reports its version, and keeps the
installer as a 14-day artifact. It is not attached to any release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ound failure

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cleanup that quarantines a checkout before Git removes it now runs on
Windows instead of refusing:
- Identities are the volume serial and file index, read from open handles
  (cap-std stats by opening; cap_fs_ext exposes dev/ino/nlink), so a
  swapped folder or file is still refused.
- Folders are opened with backup semantics and never through a link or
  junction; files are opened without following links.
- The exclusive rename is SetFileInformationByHandle(FileRenameInfo) with
  the held destination folder as root and ReplaceIfExists false, the
  counterpart of renameat2(RENAME_NOREPLACE).
- Git writes its own paths with `/` on Windows; the expected pointers in
  cleanup and repair are rendered that way.
- No modes or folder flushes on Windows; the Unix scenarios stay Unix-only.

Linux and macOS behave as before (their unit and worktree tests pass).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SetFileInformationByHandle refuses a relative root (error 87); the
destination is the held folder's final path plus the plain name, and the
source is still opened through its held folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ShadowfetchStudio ShadowfetchStudio changed the title Windows port, phase 1: compile on Windows Windows port: compile, engine tests and a preview installer Oct 18, 2026
ShadowfetchStudio and others added 6 commits October 18, 2026 01:11
…istings

From the first Windows run of the integration tests:
- Workspace::relative_text names a project path with `/`. Checkpoints,
  reviews, change tracking, the editor feed and Git requests use it; with
  `\`, an edit inside "Only change these" was treated as outside it, and
  "Ignore this file" would have written `src\foo` into .gitignore, where
  `\` is an escape. in_scope also accepts either separator on Windows.
- Folder listings report their path and parent with `/`.
- Test fixtures put `/` paths into Git config and JSON, which read `\` as
  an escape.
- Suites built on parts not ported yet are Unix-only, each with its reason:
  local models (fake llama-server), terminals, the vendor runner, fake
  language servers, and fake gh/glab; so are single tests of the vendor
  runner and the SQLite inspector.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… gates

- Fixture paths go into SQL/JSON, Git config and shell commands with `/`;
  a check's output may end in CRLF on Windows (Python's print).
- Timings compare within the precision SQLite keeps for JSON reals (15
  digits), a last-bit difference that can occur on any system.
- A refused local connection takes seconds on Windows; two tests that
  wait on one get 30 seconds.
- A literal search for `\` is skipped on Windows, whose project paths
  contain it.
- Unix-only, each for its input: file modes, a SIGTERM graceful stop, MSYS
  process IDs from `$!`, `bash` that is WSL's, fake shell-script `gh`, and
  file names Windows forbids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e Unix-only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eplace it

Windows counts Instant from boot, so a just-started runner cannot hold an
instant ten minutes old; and an in-place rewrite of the same size within
one tick of its file clock changes nothing Windows reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unix keeps the database's folder pinned through a custom VFS on
/proc/self/fd (or openat on macOS). On Windows each folder on the way is
opened without following links and without sharing delete access, so the
held parent cannot be renamed or removed while SQLite reads, and the
database is opened read-only by that folder's own path through SQLite's
Windows VFS. The inspector route and tools are available on Windows; only
its symlink and FIFO tests stay Unix-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- scripts/build-llama.cpp.sh builds the pinned llama.cpp on Windows (Git
  Bash): every x86-64 CPU variant as a loadable module, compiled by
  clang-cl through the Visual Studio generator as upstream does, without
  OpenMP, plus Vulkan when the Vulkan SDK is installed.
- The Windows installer job builds the CPU runtime, checks that
  llama-server.exe runs, and bundles it as llama.cpp\ beside the app.
- The runtime is found as llama-server.exe in that folder or under
  %LOCALAPPDATA%\ShadowCode\llama.cpp; it runs in a job object (it ends
  with ShadowCode) and keeps the variables Windows DLLs need to load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ShadowfetchStudio and others added 28 commits October 18, 2026 09:35
…em variables

- paths::home_dir reads USERPROFILE on Windows: a program started from
  Explorer has no HOME, so the app would have refused to start ("HOME is
  not set"). Every reader of the home folder uses it.
- App folders on Windows: settings in %APPDATA%\ShadowCode\config, history,
  models and caches in %LOCALAPPDATA%\ShadowCode.
- process::platform_env keeps SystemRoot and the other variables every
  Windows program needs (without it even Winsock fails) wherever a child is
  started from a cleared environment: commands, MCP servers, vendor probes,
  language-server installs and the local model runtime.
- The installer job runs the built app without HOME and checks where its
  folders went.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m variables for language servers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runner that drives Codex, Claude Code, Cursor, Grok and Antigravity is
no longer Unix-only:
- Its pipe readiness check (bytes waiting before an approval reply) is
  PeekNamedPipe on Windows, the counterpart of poll(POLLIN).
- The vendor and everything it starts run in a kill-on-close job object;
  stopping a turn ends the job.
- Vendor turns, their checkpoints, usage and MCP servers are recorded as on
  Unix; the engine no longer refuses them on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… run on Windows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The drawer's terminals now work on Windows, with the Unix version's
contract: the user's shell (PowerShell 7 when installed, else Windows
PowerShell) on a pseudo console in the project folder with the user's
environment, a bounded scrollback read by cursor, coalesced output
wake-ups, resize, and hang-up by closing the console. The shell and what
it starts run in a job object, ended after the hang-up's grace period.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent shell commands on Windows run in ShadowCode's AppContainer:
- Writable: the project and a fresh scratch folder (also HOME, TEMP, TMP).
- Readable: Windows and Program Files (as for every container), plus the
  home layout's toolchain folders, the user's own PATH folders and the
  shell's folder, granted read-only.
- Hidden: the rest of the home folder: credentials, other projects.
- Network off: no capabilities, so no network at all, loopback included.
  Network on: the internet and private-network capabilities. The
  allow-list is refused, since the container cannot reach its proxy on
  this computer.
- Grants are access entries for ShadowCode's container SID only, added
  once per folder (checked before re-applying, since propagation through a
  large project is slow).
- A process attribute Rust cannot set yet starts the container, so the
  command runs through a launcher: ShadowCode's executable with
  --shadowcode-sandbox, or the engine's shadowcode-sandbox binary in tests.
- When the sandbox cannot be set up, commands fall back as on macOS:
  refused under "Require sandbox", otherwise run with a warning.

A Windows-only test checks that the project is written, a secret in the
home folder is not read, and nothing is written outside the project.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t is Unix-only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nts use

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…andles

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every program loads user32, which cannot start (0xC0000142) when its
AppContainer has no access to the window station and desktop. On the
interactive desktop (WinSta0) containers already have it and nothing is
changed; elsewhere (a service, a CI runner) the launcher grants ShadowCode's
container SID use of its own window station and desktop.

Tests: the fake Codex's shim runs Python by its full path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dates

The pinned libcurl 8.14.1-r2 and libexpat 2.8.4-r0 left the Alpine 3.21
repository (replaced by 8.14.1-r3 and 2.9.0-r0), so the pinned build
container could no longer install its tools and every native package
build failed, on main as well. They are build-tool dependencies; the
runtime links statically from its recorded sources. The whole list was
checked to resolve in the pinned container.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ites LF

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the sandbox

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The diagnostics on the Windows runner showed system programs and
PowerShell 7 start in the AppContainer, Git for Windows' bash (MSYS2) does
not (its runtime fails with 0xC0000142) and Windows PowerShell cannot load
its own modules there. Sandboxed commands therefore run in PowerShell 7;
the command result says so. Without PowerShell 7 the sandbox reports
itself unavailable and the usual fallback applies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…test waits for the pause boundary

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…source

On the Windows runner PowerShell 7 started in the AppContainer but could
not enter the project: it lists every parent folder to normalize a path,
and the container may not list C:\Users (only an administrator could
change that). busybox-w32 is a native Windows sh with the usual tools that
changes folder with the plain Win32 call; commands keep their POSIX syntax.

- scripts/build-busybox-w32.sh cross-compiles the commit pinned in
  tools/busybox-w32.pin with mingw-w64, as upstream does; the output keeps
  its GPL-2.0 LICENSE and a COMMIT naming the exact source.
- A Linux job in the Windows workflow builds it; the tests and the
  installer (as busybox\ beside the app) use that build.
- Without the bundled shell the sandbox is unavailable and the usual
  fallback applies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ents

- The sandbox isolation test passes on Windows: the project is written,
  a home-folder secret is not read, nothing is written outside.
- busybox-w32 opens /dev/null as `nul` with create and truncate flags,
  which an AppContainer is refused; a small patch (shipped with the
  binary) opens the device without them.
- Replacing a file is briefly refused on Windows while another
  replacement of it is in flight or a scanner holds it; atomic writes
  retry for up to about a second (concurrent config updates failed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s lock

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An AppContainer may not open the NUL device (cmd's >nul is refused as
well). The launcher opens it, passes the handle inherited and names it in
SHADOWCODE_NULL_HANDLE; the busybox-w32 patch falls back to a copy of it.
It also stands in for a missing standard handle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd sandbox pass on CI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release-windows.yml builds the per-user NSIS installer with llama.cpp and
the sandbox shell, installs it through scripts/install-windows.ps1 and
requires the doctor's new AppContainer check to pass, then attaches the
installer, the complete busybox-w32 source (GPL-2.0) and
ShadowCode_<v>_windows.sha256. release.yml dispatches it after publishing.

Windows now reports its own sandbox in doctor and Settings (not bubblewrap),
and the allow-list option says it is refused there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows: a per-user installer with local models, terminals, subscription
tools and an AppContainer sandbox for agent commands. Version files,
trust range 0.33.0–2.3.0, release notes and changelog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… own folder

The busybox test relied on the other test's grant of busybox's folder,
which ran in parallel and rewrote that folder's permissions under it. It
now runs the doctor's probe (which grants the folder) first, and the two
tests take turns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…4 catches up

Alpine published libcurl 8.14.1-r3 for x86_64 on October 18; the 3.21
aarch64 builder has not updated its index since (still r2), so the exact
pin broke every aarch64 build. apk-packages.txt may now replace one pin on
one architecture ("aarch64:libcurl=8.14.1-r2"); every package is still
pinned exactly, and an override must name a pinned package. libcurl is a
build-container dependency (Git's), not part of the shipped runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ShadowfetchStudio ShadowfetchStudio changed the title Windows port: compile, engine tests and a preview installer ShadowCode 2.3.0: Windows Oct 19, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant