Files
nucleic/docs/WINDOWS_PORT.md
T

62 KiB
Raw Blame History

WINDOWS_PORT — Nucleic for Windows, full implementation plan

Status: Approved plan, 2026-07-28. This document is the working spec for the native Windows port. It is written to be self-contained: an agent picking up any work item below should be able to execute it from this document plus the referenced source files, without access to the planning conversation.

Goal: full feature parity with the macOS app except the macOS-VM / GUI-Linux-VM computer-use subsystem (Sources/NucleicCore/MacVM/), which cannot exist on Windows. Reuse as much existing Swift as possible via the official Swift-for-Windows toolchain. The UI is WinUI 3 (C#). Anything not covered by existing Swift is written in C#, per Microsoft's recommendation for the WSL Container API.


0. Locked decisions (user-approved — do not relitigate)

# Decision
D1 Headless Swift core + C# renderer. nucleic-hostd.exe (Swift, Windows toolchain) owns sessions, git, storage, approvals, and sync. The WinUI 3 app is a renderer over the existing Noise/CBOR sync protocol via local loopback — the same "N renderers, one authority" pattern the iPhone app uses. No Swift-in-C#-process embedding of the core.
D2 Control-only. Windows supports ONLY Nucleic Control projects. Every agent session runs inside a WSL container. There is no host-spawned agent path on Windows. (Control will be forced on macOS soon too; plan accordingly.)
D3 Containerization = WSL Container API (Microsoft.WSL.Containers NuGet, WinRT; public preview now, GA fall 2026), driven from a C# broker process. Not full VMs, not Docker, not wsl --import distros.
D4 MacVM subsystem excluded (both macOS-guest and GUI-Linux-VM computer-use paths). MDM (NucleicMDM server role) and NucleicPowerHelper are also not ported (macOS-VM-only / macOS-concept).
D5 Distribution: MSIX packaging + per-channel .appinstaller sideload feeds on Cloudflare R2, mirroring the five-channel Sparkle model (dev/canary/beta/rc/stable). Authenticode via Azure Trusted Signing. Store/winget deferred to post-beta.
D6 On-device AI assists: Windows AI Foundry / Phi Silica (Windows App SDK) where hardware supports it; graceful degradation to the existing heuristic fallbacks elsewhere (the same behavior as a Mac without Apple Intelligence).
D7 Renderer protocol client = Swift DLL. NucleicProtocol (+ transcript projections) compiled into NucleicProtocolC.dll exposing a thin flat C ABI; C# P/Invokes it. No C# reimplementation of CBOR/Noise/envelope.
D8 Repos on NTFS. Control clones + worktrees live at %USERPROFILE%\.nucleic\control, bind-mounted into containers via ContainerVolume. Host git ops keep using GitRunner/WorktreeManager with Windows git.exe (bundled MinGit), unchanged.
D9 Full remote parity: DNS-SD advertise + QR pairing for the iPhone remote over LAN, the relay transport, AND tailnet (libtailscale built for Windows as a DLL).
D10 x64 + ARM64 from day one. The naros-agent OCI image gains a multi-arch (amd64+arm64) build.
D11 Self-hosted Windows CI runners (one x64 first, ARM64 later) for the container e2e suite; hosted runners lack nested virtualization.
D12 The Windows app is also a remote client. It can pair with and render any Nucleic host (its own hostd, or a Mac) — host-picker UI, reusing the same pairing/connection code.

1. Background: what exists today (verified 2026-07-28)

1.1 The codebase already has the port seams

  • Package.swift (swift-tools-version 6.2) computes onLinux at manifest-eval time (Package.swift:62-64), drops darwinOnlyTargets (:70, :402), conditionalizes dependency lists (:219, :266), and excludes Apple-framework sources from NucleicCore via a per-target exclude: list (:352-381). The manifest is also channel-aware (NUCLEIC_CHANNEL env → product name + NUCLEIC_DEV/CANARY/BETA/RC/STABLE compile define).
  • Source-level gating is almost entirely #if canImport(...) (only 3 #if os(...) in all of Sources/: Claude/ClaudeCredentialBroker.swift:360, Codex/CodexCredentialBroker.swift:166, Project.swift:465).
  • Sources/NucleicCore/LinuxSupport.swift (~443 LOC) provides stub actors (ContainerManager, MacVMManager mirrors that throw LinuxUnsupported) plus an os.Logger shim, so shared call sites compile unchanged on non-Darwin. This is the template for WindowsSupport.swift.
  • Sources/nucleicd/ (645 LOC: Nucleicd.swift, ControlEndpoint.swift) is a proven headless host over NucleicCore for Linux (the Covalence Cloud Runner): one AppStore graph, startSyncServer(), pairing mint, signal-driven shutdown. This is the template for nucleic-hostd.
  • NucleicProtocol builds for macOS, iOS, and Linux already. On non-Apple platforms it swaps CryptoKit→swift-crypto and URLSession-WebSocket→SwiftNIO via #if canImport.

1.2 Target portability inventory

Target LOC (≈) Verdict for Windows
NucleicProtocol (57 files) 13,200 Portable as-is. AgentEvent model, ClientMsg/HostMsg envelope (Sync/MessageEnvelope.swift ~1,079 LOC; Sync/WireMessages.swift ~831), custom CBOR codec (CBOR/), Noise (XXpsk0 pairing + IK reconnect, Noise/), SyncClient.swift (generic over FrameChannel), STUN/UDP punch (Sync/Direct/), relay WebSocket (NIO off-Apple).
NucleicCore (177 files) 79,600 The battleground. Exclude Container/ engine files + all MacVM/ engine files (~15.7k LOC, mirroring the Linux exclude list); shim the rest (see §4).
NucleicApp (76 files) 30,000 Do not port. SwiftUI/AppKit cockpit — this is the WinUI 3 rewrite target. Includes SwiftTerm terminal, Sparkle updater, Dock-bounce/NSSound hooks, AppleIntelligence UI.
NucleicTailnet 640 Portable Swift over #if canImport(TailscaleKit); Windows needs a libtailscale DLL leg (D9, §9.1).
NucleicMDM 2,540 Not ported (macOS-VM-only role, D4). Crypto/ASN.1 parts are portable if ever needed.
nucleicd, nucleic-smoke, nucleic-punch-harness ~1,200 Portable; nucleic-smoke becomes the Windows e2e driver (§14).
NucleicPowerHelper / NucleicPowerProtocol ~215 Not ported; replaced by SetThreadExecutionState in hostd (§4.3).
ios/NucleicRemote ~18,300 Not ported, but its Models/RemoteStore.swift (3,396 LOC, pure host-state projection) + Models/HostConnection.swift (1,294 LOC, owns SyncClient over a FrameChannel) are the structural template for the C# renderer (§7). Its Views/Transcript/ projection files are compiled into the host-testable NucleicRemoteProjection SPM target — reused by the DLL (§6).

External dependencies (from Package.resolved): GRDB 7.11 (Windows support since 7.10), swift-crypto, swift-nio(+ssl), swift-certificates/asn1, swift-log, swift-system, swift-collections etc. — all portable. SwiftTerm and Sparkle are App-target-only (replaced on Windows). third_party/containerization (Apple's framework, vendored/patched) and TailscaleKit.xcframework are Apple-only binaries.

1.3 NucleicCore internals: what needs what

Area Files (representative) Windows disposition
App engine / state AppStore.swift (14k LOC, @MainActor @Observable, conforms to SyncHostBridge) Runs headless inside hostd as the sync authority (as nucleicd proves).
Session engine SessionController.swift (2,071), Transcript.swift, TranscriptProjection Portable.
Agent backends Claude/ (9.5k), Codex/ (2.4k), Grok/ (1.2k), Backend.swift, ACP/ Portable; container-exec path only (D2).
Approvals / control plane Claude/MCPApprovalServer.swift (~3.9k), Claude/ApprovalServerRegistry.swift Portable logic; needs a NIO ByteConn (§5).
Git Git/GitRunner.swift, Git/WorktreeManager.swift (1,079), Git/GitHubCredentials.swift, Git/ProjectCloner Shells out to git/gh CLI — portable (D8). Credentials → SecretStore (§4.2).
Process spawning ProcessHost.swift (578: ProcessSpec/ProcessHandle/ChildProcess, plain pipes, no PTY), LoginShellPATH/Env Foundation Process works on Windows; signals + login-shell probing need shims (§4.3).
Persistence Persistence/GRDBMetadataStore.swift (978, WAL DatabasePool under Application Support), SessionMetadataStore protocol Portable; path mapping to %APPDATA% (§4.4).
Sync host Sync/SyncHost.swift (706), ConnectionHandler.swift (1,146), SyncHostBridge.swift (473) Portable; LAN transport files are Network.framework and already Linux-excluded — NIO rewrite (§4.3).
Containers Container/{ContainerEngine,ContainerEngine+Rootfs,ContainerManager,ContainerizedProcessHandle,MemoryBalloon,OCIArtifact,CommandInterceptor}.swift Engine = Apple-only, replaced by wslc broker (§3). Manager = policy, ported (§3.1). CommandInterceptor.swift is pure values — keep.
MacVM MacVM/ (28 files, 12.8k) Excluded (D4); keep pure value files (MacVMSpec, MacVMSurface) compiling, stub MacVMManager.
Crypto CryptoKit sites Free via existing #if canImport(CryptoKit) → swift-crypto seams.
Keychain 24 Security sites in ~14 files SecretStore protocol + DPAPI backend (§4.2).
Logging 12 os/OSLog sites Lift the Linux Logger shim into a shared file (§4.3).
On-device AI Intelligence.swift (1,295, provider seam + heuristic fallbacks), AFMRequestQueue.swift Windows Foundry provider (§9.2).

1.4 The Control/container subsystem (what wslc replaces)

Authoritative docs: docs/VSOCK_CONTROL_PLANE.md, docs/CONTAINER_ISOLATION.md, docs/NAROS.md, docs/NASH.md, docs/RUNTIME_ARCHITECTURE.md.

Two orthogonal virtualization stacks — do not conflate:

  1. The agent sandbox (Container/): Apple containerization framework, headless Linux microVMs. → This maps to wslc.
  2. Computer-use VMs (MacVM/): full Virtualization.framework GUI VMs. → Excluded.

Key mechanics that carry over unchanged (all guest-side / protocol-level, OS-agnostic):

  • Control plane protocol: host MCPApprovalServer serves HTTP JSON-RPC at POST /mcp (Streamable-HTTP MCP: initialize, ping, tools/list, tools/call with SSE for gated calls), bearer-token multiplexed per session, plus plain JSON POST /git-event, /gh-event, /command-event, /shell-event (dispatch at MCPApprovalServer.swift:1903-1916). Agent-side MCP config (mcpConfigJSON, :1611) always points at http://127.0.0.1:9099/mcp.
  • Guest bridge: containers/nucleic-sandbox/control-bridge.js listens on guest loopback 9099 (ContainerSpec.controlBridgePort) and forwards byte-for-byte to the host endpoint. On macOS that endpoint is a per-container host AF_UNIX socket relayed into the guest over virtio-vsock via Apple UnixSocketConfiguration(.into)the only macOS-specific piece of the entire control plane. A second optional forwarder (proxyBridgePort 9098) serves the opt-in Claude token proxy (Claude/ClaudeTokenProxy.swift).
  • Interceptors: Node git/gh shims installed at /usr/local/bin/{git,gh} (sources are Swift string literals: ContainerEngine+Rootfs.swift:394 and :466), exec the real binary then POST {argv, cwd, exitCode, subcommand, sessionId} to NUCLEIC_GIT_HOOK_URL/NUCLEIC_GH_HOOK_URL with a bearer token. Optional bash command tracer + batch poster in Container/CommandInterceptor.swift (also home of CommandInterceptor.hookEnv(...), the single source of interceptor endpoint env, and agentShellArgv/hostShellArgv for nash selection). The nash shell (shell/, Rust) reports /shell-event.
  • Agent run flow (Claude/ClaudeCodeBackend.swift runLoop, :766-1087; Codex/Grok backends identical shape): resolve approval server via ApprovalServerRegistrycontainerManager.ensureRunning(cspec) → mint per-session bearer token → server.start(unixSocketPath: run.container.controlSocketHostPath) → register handlers keyed by token (register, registerGitReport/GhReport/CommandReport/ShellReport, registerNucleicTools) → argv claude -p --output-format stream-json --input-format stream-json --verbose --permission-prompt-tool mcp__nucleic__approve --mcp-config <mcpConfigJSON>sandbox.exec(name:workdir:env:argv:uid:gid:) returning a ProcessHandle (Container/ContainerizedProcessHandle.swift adapts framework stdio onto the same NDJSON AsyncThrowingStream interface a host ChildProcess provides).
  • ContainerSpec (Backend.swift:54): name, image, mounts: [Mount] (host/container/readOnly), workdir, env, idleTimeout, claudeHomeStaging/claudeHomeWritable, installGitInterceptor, cpus, memoryGiB, runAsUID(=narosAgentUID 501)/runAsGID, controlSocketHostPath, proxySocketHostPath. Constants: controlSocketGuestPath = "/run/nucleic/control.sock", controlBridgePort = 9099, proxyBridgePort = 9098. Built by SessionController.containerSpec() (SessionController.swift:643) from Project.sandbox (ProjectSandbox, Project.swift:77).
  • Images: agent rootfs ghcr.io/abkslm/naros-agent:26.07 (narOS, built from os/ by .github/workflows/naros.yml; contract in docs/NAROS.md: naros-init PID 1, agent uid 501, nash as /bin/sh, NAROS_BRIDGE=1 supervises control-bridge); legacy ghcr.io/abkslm/nucleic-sandbox:v7 (containers/nucleic-sandbox/Dockerfile). The kernel OCI artifact and patched vminitd initfs are microVM concepts — NOT needed on WSL (WSL supplies kernel + init). GHCR auth: ContainerEngine.registryAuth(for:) pattern (NUCLEIC_REGISTRY_TOKEN or app GitHub token with read:packages).
  • Lifecycle policy (Container/ContainerManager.swift, 687 LOC): busy ref-counting, idle timers, shared-container naming (sharedControlContainerName, per-backend claude/codex/xai splits, channelSuffix), teardown/GC, reconcile(activeSessions:), updateAgentCLIs, checkImageUpdate, controlContainerStatus/Usage/DownloadProgress, probeControlPlane, requestControlPlaneRecovery. Its public surface is mirrored byte-for-byte by the LinuxSupport.swift stubs — it IS the port contract.

1.5 The WSL Container API (wslc) — facts

Source: learn.microsoft.com/windows/wsl/wsl-container, wsl.dev/api-reference, samples at aka.ms/wslc-samples.

  • NuGet Microsoft.WSL.Containers (C# projection + C++/WinRT headers). Namespace Microsoft.WSL.Containers. Public preview; breaking changes possible; GA fall 2026. CLI twin: wslc.exe (docker-like: wslc run / image ls / container ps / stop, -p, -v).
  • Object model: WslcService (static entry: GetMissingComponents() → ComponentFlags, component install, service version) → Session (a WSL-backed VM host; SessionSettings(name, dataDir) + CpuCount, MemoryMB, timeout, VHD via VhdOptions; manages images: PullImageAsync(PullImageOptions) with progress, import/load/push/tag/delete; session.CreateContainer(...); session.Terminate()) → Container (ContainerSettings(image) + Name, InitProcess, networking mode (ContainerNetworkingMode), hostname/domain, volumes (ContainerVolume, ContainerNamedVolume), port mappings (ContainerPortMapping); Start(), Stop(Signal, TimeSpan), Delete(DeleteContainerFlags), state queries, run additional processes) → Process (ProcessSettings: CmdLine, env, workdir, OutputMode = ProcessOutputMode.Event; OutputReceived events, stdin writes, signals (SIGTERM/SIGKILL/...), exit events (ProcessExitHandler), crash info (ProcessCrashInformation)).
  • OCI images pulled directly from registries (docker.io, GHCR); multi-arch manifests resolve per host arch. GPU access supported. Interactive stdin/stdout streaming supported.
  • Session-level termination handler (SessionTerminationHandler); wslc objects are service-backed (state survives a client-process crash — verify in M1 spike).

2. Architecture

2.1 Process topology

┌─────────────────────────── MSIX package (per channel) ───────────────────────────┐
│                                                                                  │
│  Nucleic.exe (WinUI 3, C#)              nucleic-hostd.exe (Swift, full trust)    │
│  ┌───────────────────────┐   Noise/CBOR ┌──────────────────────────────────┐     │
│  │ RendererStore (C#)    │◄────────────►│ AppStore (headless) + SyncHost   │     │
│  │  ▲ P/Invoke callbacks │  loopback    │ ApprovalServerRegistry (NIO)     │     │
│  │ NucleicProtocolC.dll  │  TCP frames  │ GitRunner / WorktreeManager      │     │
│  │ (Swift, C ABI)        │              │ GRDB @ %APPDATA%\Nucleic\<chan>  │     │
│  └───────────────────────┘              │ ContainerManager (policy, Swift) │     │
│                                         │   └─ WslcBrokerClient (JSON-RPC) │     │
│                                         └───────────────┬──────────────────┘     │
│                                          NDJSON JSON-RPC │ stdio                 │
│                                         ┌────────────────▼─────────────────┐     │
│                                         │ nucleic-brokerd.exe (C#/.NET 9)  │     │
│                                         │ Microsoft.WSL.Containers (WinRT) │     │
│                                         └────────────────┬─────────────────┘     │
└──────────────────────────────────────────────────────────┼───────────────────────┘
                                                WinRT/COM  │
                                    ┌──────────────────────▼──────────────────────┐
                                    │ wslc Session "nucleic-<channel>" (WSL VM)   │
                                    │  ┌────────────────────────────────────────┐ │
                                    │  │ naros-agent containers (shared/split)  │ │
                                    │  │  control-bridge.js :9099 ──TCP──► host │ │
                                    │  │  git/gh shims, nash, claude/codex/grok │ │
                                    │  │  /work ◄─ContainerVolume─ NTFS worktree│ │
                                    │  └────────────────────────────────────────┘ │
                                    └─────────────────────────────────────────────┘

iPhone (NucleicRemote) / other renderers ──► LAN (DNS-SD + QR) / relay / tailnet ──► SyncHost
Windows app in remote-cockpit mode (D12) ──► same transports ──► any Nucleic host

2.2 Division of labor

Concern Owner Why
Sessions, transcripts, approvals, risk classification, Autoship + merge queue, LockManager, nvrsion, quotas, idea inbox, activity/streaks Swift hostd (existing NucleicCore, logic unchanged) Maximum Swift reuse (D1).
Container lifecycle policy (refcounts, idle, shared naming, reconcile, CLI updates) Swift hostd (ContainerManager, retargeted onto SandboxEngine) Policy stays single-sourced across macOS/Windows.
Container mechanism (wslc Session, image pull, create/exec/stdio/signals) C# nucleic-brokerd wslc API is WinRT; Microsoft recommends C# (D3).
Wire protocol (CBOR, Noise, envelope, transcript projection) Swift NucleicProtocolC.dll Single-sourced wire format (D7).
UI, toasts, taskbar flash, theming, Phi Silica calls C# Nucleic.exe WinUI 3 mandate.

2.3 Broker process model

Separate exe, child of hostd, NDJSON JSON-RPC 2.0 over stdio. Rationale:

  • Containers must outlive the UI window; broker lifetime must match hostd, not Nucleic.exe.
  • hostd already has everything needed to run it: ChildProcess (Sources/NucleicCore/ProcessHost.swift) for spawn + NDJSON line streams, and Sources/NucleicCore/JSONRPCConnection.swift for framing. Zero new IPC surface — no named-pipe ACLs, no COM registration.
  • Crash containment: wslc is preview software. hostd supervises brokerd with exponential-backoff restart. On reattach, brokerd re-opens the named wslc Session and re-enumerates containers (wslc state is service-backed); hostd then runs ContainerManager.reconcile(activeSessions:) against the fresh listing. In-flight execs fail with a distinguishable brokerLost error, mapped to the same recovery path ContainerizedProcessHandle.forceCloseStreams() serves today.
  • Broker sends a capabilities hello (wslc/NuGet version, supported features) so hostd can degrade gracefully across preview→GA churn.

2.4 Lifetime & single-instance

  • Nucleic.exe launches hostd at startup if not running. Single-instance guard: named mutex Local\nucleic-hostd-<channel> (hostd also self-guards).
  • hostd writes a rendezvous file %LOCALAPPDATA%\Nucleic\<channel>\hostd.json = {pid, port, localPSK} with an owner-only DACL. The renderer (and nucleic-smoke) reads it to connect. Stale detection: pid liveness + connect probe.
  • hostd outlives the renderer window while any session is active or the Autoship queue is non-empty; the renderer exposes explicit "Quit host". Clean shutdown via SetConsoleCtrlHandler (CTRL_CLOSE/LOGOFF/SHUTDOWN) routed into the same shutdown path nucleicd drives from SIGTERM (Nucleicd.swift:255-263 gets a Windows leg).
  • MSIX declares runFullTrust; hostd + brokerd + DLL + Swift runtime + MinGit ship in the package payload.

3. Container subsystem port

3.1 The SandboxEngine seam (Darwin-first refactor)

New file Sources/NucleicCore/Container/SandboxEngine.swift — a platform-neutral protocol capturing exactly the mechanism surface ContainerManager and the backends use today (extracted from ContainerEngine.swift:513, :660, :697, :727-:924):

public protocol SandboxEngine: Sendable {
    /// Idempotent create-or-start; returns the container name and the address at which
    /// the guest can reach the host (macOS: vmnet gateway; Windows: WSL vEthernet gateway).
    func ensureRunning(_ spec: ContainerSpec) async throws -> (name: String, hostGateway: String)
    func exec(name: String, workdir: String, env: [String: String],
              argv: [String], uid: UInt32, gid: UInt32) async throws -> any ProcessHandle
    func runCapturing(name: String, workdir: String, env: [String: String],
                      argv: [String]) async -> (exitCode: Int32, output: String)?
    func stop(name: String) async
    func restart(name: String) async -> Bool
    func remove(name: String) async -> Bool
    func isRunning(name: String) async -> Bool
    func containerExists(name: String) async -> Bool
    func list() async -> [ControlContainerEntry]
    func sampleResourceUsage(name: String) async -> ContainerResourceSample?
    func probeControlPlane(name: String) async -> ControlPlaneProbe
    func diagnoseKill(name: String) async -> String?
    func reclaimMemoryNow() async
    func pullImage(_ ref: String) async throws     // progress via existing download-progress plumbing
    func checkImageUpdate(_ ref: String) async -> Bool
    // NOTE: match the real ContainerEngine signatures exactly when implementing — the
    // list above is the shape; lift precise signatures from ContainerEngine.swift.
}

Steps (this lands Darwin-first and is verified by the existing macOS test suite before any Windows code exists):

  1. Add the protocol; conform the Apple ContainerEngine (mechanical).
  2. Retype ContainerManager's engine reference (and the few direct engine references in ClaudeCodeBackend / CodexAppServerBackend / CodexExecBackend / GrokACPBackend) to any SandboxEngine.
  3. Keep LinuxSupport.swift stubs compiling (Linux/nucleicd behavior unchanged — the stub ContainerManager remains the Linux story).
  4. On Windows, ContainerManager.swift moves off the exclude list and compiles against the protocol. Engine files (ContainerEngine*.swift, ContainerizedProcessHandle.swift, MemoryBalloon.swift) stay excluded. Audit OCIArtifact.swift: pure ref-parsing/version helpers move to a shared file if WslcContainerEngine wants them.

3.2 WslcContainerEngine mapping

New files in Sources/NucleicCore/Container/Windows/ (compiled only on Windows):

  • WslcBrokerClient.swift — spawns nucleic-brokerd.exe via ChildProcess, speaks NDJSON JSON-RPC over its stdio, multiplexes requests, supervises (backoff restart, capabilities hello, reattach → notify ContainerManager.reconcile).
  • WslcContainerEngine.swiftSandboxEngine conformance over the broker RPC.
  • WslcProcessHandle.swift — mirrors ContainerizedProcessHandle.swift: adapts proc.stdout/stderr/exit notifications onto the ProcessHandle protocol (ProcessHost.swift:44-70): stdoutLines/stderrLines as AsyncThrowingStream<Data, Error> through the shared LineSplitter, writeLine framing NDJSON to stdin, wait() → exit code, sendSignal → broker proc.signal, forceCloseStreams() on broker loss, lastActivityNanos bumping, supportsHostCPUSampling = false.

Concept mapping:

Nucleic concept (macOS) wslc equivalent
One microVM per container (Apple containerization) One wslc Session per channel (SessionSettings("nucleic-<channel>", dataDir: %LOCALAPPDATA%\Nucleic\<channel>\wslc), CpuCount/MemoryMB from settings) hosting all Nucleic containers as siblings.
ContainerSpec.name/image ContainerSettings(image) + .Name (keep existing naming statics incl. channelSuffix so dev/canary/stable coexist).
spec.mounts (host worktree → guest path) ContainerVolume { hostPath: NTFS path, guestPath: mount.container, readOnly }.
spec.cpus/memoryGiB Session-level CpuCount/MemoryMB (wslc resources are per-session). Per-container limits: cgroup exec inside the container if needed later.
Memory balloon (MemoryBalloon.swift) No per-container balloon. reclaimMemoryNow() → WSL autoreclaim / drop-caches exec; MemoryMB resize applies after sandbox restart — surface in Settings exactly like the existing restart-shared flow.
runAsUID: 501 Pass through exec settings; M1 spike verifies uid semantics. If wslc runs container processes as root only, fall back to a setpriv/su agent -c wrapper argv — interceptors and nash don't care about the numeric uid.
Rootfs pull + ext4 clone (+Rootfs.swift) session.PullImageAsync(PullImageOptions("ghcr.io/abkslm/naros-agent:<tag>")) with progress → controlDownloadProgress plumbing. No kernel/vminitd artifacts.
Shim seeding (seed(_:in:), base64 install) Prefer baking into the multi-arch narOS image (already the case); keep idempotent re-seed via runCapturing for legacy-image support.
ensureRunning returns hostGateway Broker reports the session's WSL vEthernet gateway address (see §5).

3.3 Broker RPC surface (windows/NucleicBroker)

NDJSON JSON-RPC 2.0 over stdio; binary payloads base64. Versioned hello first.

Requests (hostd → broker):

  • hello() → {brokerVersion, wslcVersion, capabilities: [..]}
  • components.missing() → {flags} / components.install() (+ progress notifications; wraps WslcService.GetMissingComponents / install — feeds onboarding, §8)
  • session.ensure({name, dataDir, cpu, memoryMB}) → {gateway} / session.terminate()
  • image.pull({ref, auth}) (+ image.pullProgress {ref, status, current, total}), image.list / image.delete / image.inspect
  • container.create({name, image, volumes:[{host,guest,ro}], networkingMode, hostname, env, initArgv})
  • container.start / container.stop({name, signal, graceMs}) / container.delete / container.list / container.state / container.stats({name})
  • proc.exec({container, argv, env, cwd, uid, gid, tty}) → {procId}
  • proc.stdin({procId, b64}) / proc.closeStdin({procId}) / proc.signal({procId, sig})
  • proc.resize({procId, cols, rows}) (tty mode, for the Terminal panel)
  • ai.generate({prompt, schema}) (§9.2 — Phi Silica sidecar duty)

Notifications (broker → hostd):

  • proc.stdout {procId, b64} / proc.stderr {procId, b64} / proc.exit {procId, code}
  • session.down {reason} (from SessionTerminationHandler)
  • image.pullProgress, components.installProgress

Broker implementation notes: .NET 9, single-file publish per arch; all wslc calls behind an internal IWslc interface so unit tests run against a fake; version-pin the NuGet; map ProcessOutputMode.Eventproc.stdout notifications; batch/flush stdout notifications (agents emit high-rate NDJSON — coalesce writes, never block the WinRT event thread).


4. NucleicCore on Windows: shims & seams

4.1 Package.swift changes

  • Add onWindows (mirror the onLinux detection at Package.swift:62-64); introduce nonDarwin = onLinux || onWindows and use it wherever onLinux currently gates darwinOnlyTargets, Sparkle, and product lists (:70, :219, :266, :402).
  • Widen NucleicProtocol's swift-nio / swift-crypto dependency conditions from .linux to [.linux, .windows].
  • NucleicCore exclude: for Windows = the Linux list (:352-381) minus Container/ContainerManager.swift (now protocol-typed, compiles on Windows), keeping excluded: Container/ContainerEngine.swift, Container/ContainerEngine+Rootfs.swift, Container/ContainerizedProcessHandle.swift, Container/MemoryBalloon.swift, all MacVM/*Engine*/MacVMManager/MacVMExecChannel/MacVMAgentClient, and the Network.framework Sync files. Container/CommandInterceptor.swift, MacVMSpec.swift, MacVMSurface.swift stay in (pure values).
  • Add Sources/NucleicCore/Windows/** and Sources/NucleicCore/Container/Windows/** (excluded on non-Windows), the NucleicProtocolC dynamic-library target/product, and the nucleic-hostd executable target.
  • SwiftPM remains the build system, including the DLL (.library(type: .dynamic)). If @_cdecl symbol auto-export proves unreliable, pass a checked-in Sources/NucleicProtocolC/exports.def via -Xlinker /DEF:. CMake is the documented fallback, not the plan.

4.2 SecretStore (Keychain replacement)

Introduce protocol SecretStore (get/set/delete/list by service+account, Data values) in NucleicCore; macOS backend wraps the existing Keychain calls; Windows backend Sources/NucleicCore/Windows/CredentialStore.swift uses DPAPI (CryptProtectData/CryptUnprotectData, user scope, CRYPTPROTECT_UI_FORBIDDEN) encrypting per-item files under %LOCALAPPDATA%\Nucleic\secrets\<service>\<account> with owner-only DACLs. DPAPI chosen over Windows Credential Manager because of Credential Manager's ~2.5 KB blob limit vs. Nucleic's larger OAuth token sets.

Retarget the ~24 Security call sites (~14 files) onto the protocol; macOS behavior unchanged. Files: KeychainOwnedAccess.swift, Claude/ClaudeLoginKeychain.swift, Claude/ClaudeCredentialBroker.swift, Codex/CodexAuthFile.swift, Codex/CodexCredentialBroker.swift, Carbon/CarbonKeyCustody.swift, Carbon/CarbonCrypto.swift, Git/GitHubCredentials.swift, HeartbeatReporter.swift, ControlAuth.swift, SubscriptionUsage.swift, Sync/HostIdentityStore.swift, Sync/CredentialProvider.swift, Sync/RunnerCredentialVault.swift, Sync/PushRelayClient.swift, Sync/RelayAccess.swift, Sync/RunnerPoolClient.swift (grep canImport(Security) for the authoritative list; MacVM/MacVMEngine+MDM.swift is excluded anyway). Land Darwin-first like the SandboxEngine seam.

4.3 Windows platform shims (Sources/NucleicCore/Windows/)

File Replaces Implementation
WindowsSupport.swift LinuxSupport.swift role Stub MacVMManager (and anything else the Linux file stubs that Windows also lacks). Lift the os.Logger shim out of LinuxSupport.swift:13-54 into a shared PortableLogging.swift compiled on both Linux and Windows (swift-log backend; rolling files at %LOCALAPPDATA%\Nucleic\logs, ETW optional later).
NIOByteConn.swift NWByteConn (Network.framework) in MCPApprovalServer SwiftNIO TCP listener behind the existing ByteConn abstraction; start(host:port:) keeps its signature; AF_UNIX leg goes #if !os(Windows). Also usable on Linux later.
NIOLANTransport.swift / NIOLANListener.swift Sync/LANTransport.swift, LANBrowser.swift, PeerDialer.swift, MeshPathMonitor.swift (Network.framework files, already Linux-excluded) FrameChannel + listener over NIO; path monitoring degraded to periodic reachability probes (no NWPathMonitor equivalent — acceptable).
WindowsDNSSD.swift Bonjour advertise/browse dnsapi.dll DnsServiceRegister/DnsServiceBrowse/DnsServiceResolve (WinSDK module) for _nucleic._tcp with the pairing TXT payload. Fallback documented: in-process NIO mDNS responder if dnsapi TXT limits bite.
ProcessTree.swift POSIX signal semantics in ChildProcess.sendSignal Assign every host child to a Job Object at spawn (CreateJobObjectW + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE). SIGKILL → TerminateJobObject; SIGTERM → GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT) then job-terminate after grace. (Container processes get real POSIX signals via wslc — no shim needed there.)
FilePermissions.swift chmod 0600 / posixPermissions hardening (~10 sites: RiskClassifier.swift, Carbon/CarbonKeyCustody.swift, Claude/{ClaudeLoginKeychain,ClaudeTokenProxy,MCPApprovalServer}.swift, SessionController.swift, Codex/{CodexCredentialBroker,CodexAuthFile}.swift, KeychainOwnedAccess.swift, CommandSummary.swift) SetNamedSecurityInfoW owner-only DACL helper; a small SecretFile.write(_:at:) utility both platforms call.
FileWatcher.swift DispatchSource file watches (Claude/ClaudeTokenProxy.swift, Claude/MCPApprovalServer.swift) ReadDirectoryChangesW wrapper with the same callback shape.
PowerBlocker.swift NucleicPowerHelper + SleepBlocker SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED) held while sessions are active; released on idle.
LoginShellEnv / LoginShellPATH Windows legs POSIX login-shell probing Read process env + registry (HKCU\Environment, HKLM\...\Session Manager\Environment); no shell probe.

4.4 Storage & paths

  • corelibs-foundation maps .applicationSupportDirectory%APPDATA%. Data root: %APPDATA%\Nucleic\<channel>\ (channel-suffixed so all five channels coexist — mirrors the container channelSuffix scheme). GRDB WAL DatabasePool (Persistence/GRDBMetadataStore.swift:13 and the other Application-Support call sites: KeychainOwnedAccess.swift:177, TailnetTransport.swift, Claude/ApprovalServerRegistry.swift, Cast/MeshDebugLog.swift) get a single NucleicPaths helper rather than N inline lookups.
  • Control repos: %USERPROFILE%\.nucleic\control\<project>\ with worktrees at <repo>\.nucleic\worktrees\<slug> (unchanged layout).
  • Approval-server socket paths (/tmp/nucleic/<name>.sock) become (gatewayIP, port) tuples behind the existing ApprovalServerRegistry API on Windows (§5).
  • Git: bundle MinGit (x64/arm64) in the MSIX; configure managed clones with core.longpaths=true and core.autocrlf=false (agents run in Linux containers — the repo must stay LF; .gitattributes-driven projects behave as configured). GitRunner/WorktreeManager code unchanged.

5. Control plane on Windows

Everything above the transport is reused: MCPApprovalServer routes + SSE + bearer-token multiplexing, ApprovalServerRegistry, interceptor shims, control-bridge.js, CommandInterceptor.hookEnv, mcpConfigJSON — all unchanged. Only the host↔guest byte path is replaced.

Primary: TCP to the WSL vEthernet gateway (smallest change that works).

  1. MCPApprovalServer.start(host:port:) gains the NIOByteConn transport (§4.3), bound only to the WSL-facing interface address (never 0.0.0.0).
  2. WslcContainerEngine.ensureRunning returns the gateway address the broker reports — this is exactly the hostGateway value the macOS engine already returns, so the contract doesn't change.
  3. control-bridge.js gets a small additive branch: when NUCLEIC_CONTROL_HOST / NUCLEIC_CONTROL_PORT env are present, forward guest-loopback 9099 to that TCP endpoint instead of the relayed UDS (keep the UDS branch for macOS). Same for the proxy bridge (9098NUCLEIC_PROXY_HOST/PORT).
  4. Networking-mode handling: NAT (default) → gateway routing as above. Mirrored mode (Win11 22H2+) → broker detects and reports the reachable host address instead. Auto-detect; prefer NAT for determinism.
  5. Firewall: MSIX can't install rules; rely on Windows 11's Hyper-V firewall defaults (guest→host on the WSL vSwitch) and ship an onboarding connectivity self-test surfaced through the existing probeControlPlane seam, with an elevated one-shot New-NetFirewallRule remediation script if the default-deny case is hit.

Spike + fallback: Hyper-V sockets (true vsock parity). Host listens on AF_HYPERV bound to the WSL VM's VMID + a service GUID derived from the logical port; guest dials AF_VSOCK CID 2. No firewall exposure, no routing variance. Unknown: whether wslc containers pass vsock through their namespaces (likely — vsock isn't network-namespaced). M1 answers this; promote to primary if gateway-TCP shows friction in the wild (enterprise firewall policy, DNS tunneling interactions).

Rejected: named-pipe-via-WSL-interop (pipes aren't visible inside wslc containers without brittle 9P mounts).

Security note: the bearer token — not the transport — is the auth boundary today (the vsock relay was defense-in-depth). Interface-scoped binding restores equivalent posture; tokens remain per-session, minted in runLoop.


6. NucleicProtocolC.dll (renderer protocol client)

New target Sources/NucleicProtocolC/, depending on NucleicProtocol + NucleicRemoteProjection (the SPM target that already compiles ios/.../Views/Transcript/{TranscriptProjection,IncrementalTranscriptProjection}.swift for host testing — reuse it, don't duplicate).

Flat C ABI via @_cdecl (UTF-8 JSON strings in/out; caller frees with np_free; header at Sources/NucleicProtocolC/include/nucleic_protocol.h):

typedef void (*np_event_cb)(void* ctx, const char* host_msg_json);
typedef void (*np_state_cb)(void* ctx, const char* state_json); // connect/disconnect/transport/errors

np_handle*  np_client_create(const char* config_json);   // identity dir, host key pins
void        np_client_set_callbacks(np_handle*, np_event_cb, np_state_cb, void* ctx);
int         np_client_connect_local(np_handle*, const char* rendezvous_json); // {host,port,psk}
int         np_client_connect_remote(np_handle*, const char* endpoint_json);  // D12: LAN/relay/tailnet
int         np_client_pair(np_handle*, const char* pairing_code);
int         np_client_send_intent(np_handle*, const char* client_msg_json);
void        np_client_close(np_handle*);
void        np_free(const char*);

np_proj*    np_projection_create(void);
const char* np_projection_apply(np_proj*, const char* host_msg_json); // incremental render-diff JSON
void        np_projection_free(np_proj*);

Internals: a NIO loopback/TCP FrameChannel implementation feeding the unchanged SyncClient (NucleicProtocol/Sync/SyncClient.swift is generic over FrameChannel — no changes). Noise runs even locally (one code path for local + remote peers; the rendezvous PSK auto-approves local pairing, §2.4). Callbacks fire from one dedicated DLL thread; C# marshals to its dispatcher.

The one real work item: JSON encode/decode for ClientMsg/HostMsg at the ABI boundary (they are CBOR-coded on the wire). Implement in the DLL layer only. Lock it with golden fixtures: fixtures/protocol-abi/ holds paired CBOR↔JSON vectors for every message kind, tested from Swift (Tests/NucleicProtocolCTests) and from C# (xUnit in windows/NucleicApp.Tests) on every CI run, so the two views cannot drift. The envelope already carries protocol versioning — bump fixtures with it.


7. WinUI 3 renderer (windows/NucleicApp)

Stack: .NET 9, Windows App SDK 1.7+, WinUI 3, CommunityToolkit.Mvvm + CommunityToolkit.WinUI (SettingsControls; Markdown via Labs MarkdownTextBlock with RichTextBlock fallback), WebView2 (Monaco) for diff/editor, Windows Terminal control (Microsoft.Terminal.Control) for the terminal panel.

Layering mirrors ios/NucleicRemote exactly — study Models/RemoteStore.swift (a pure projection of host state; the wire types ARE the models) and Models/HostConnection.swift (owns the SyncClient, handles pairing modes and transport failover) before writing C#:

  • Interop/ — P/Invoke bindings for nucleic_protocol.h; HostMsg/ClientMsg DTOs via System.Text.Json source generators (AOT-friendly, fast).
  • Services/HostConnection.cs — DLL handle lifecycle, reconnect loop, rendezvous-file watch; multi-host (D12): one instance per paired host, local host auto-added.
  • Services/RendererStore.cs — the RemoteStore analog: ObservableObject projection (sessions, projects, approvals, dashboard, quotas, activity, todos, mesh state), fed exclusively by HostMsg events; all mutations are intents (np_client_send_intent). No canonical state invented in the renderer, ever.
  • Services/HostdLauncher.cs — mutex check, spawn hostd, health probe.
  • Services/OnboardingService.cs — wslc component flow (renderer → hostd intent → broker components.*), progress UI, connectivity self-test display.

Screens (parity targets from Sources/NucleicApp and ios/.../Views):

Screen macOS source (reference) Notes
Shell / navigation RootView.swift (1,681) NavigationView split layout; host picker at top (D12).
Home HomeView.swift, activity grid, streaks, idea inbox (TodoView.swift) Pure projections — data comes from host.
Project view / Control panel ProjectView.swift, ControlPanelView.swift Container status/usage cards use controlContainerStatus/Usage wire data.
Session detail + transcript SessionDetailView.swift (2,407), TranscriptRow.swift (2,343), HostExecCard.swift ItemsRepeater with incremental virtualization driven by np_projection_apply diffs; tool rows color-coded by risk; collapsible thinking.
Composer NewChatComposer.swift, ChatInputField.swift, ComposerAttachments.swift Model/effort/Orchestra picker (SKU-routed backends), attachments.
Approvals approval cards in session view; ApprovalCardView (iOS) Allow / deny / modify-and-allow / always-allow-in-session; first-responder-wins collapse on approvalResolved.
Terminal panel Panels/TerminalPanel.swift (SwiftTerm) In-container shell (D2): hostd intent → broker proc.exec(tty:true) of agentShellArgv (nash) in the session container; bytes ride a per-terminal loopback side-channel vended by hostd; rendered in the Windows Terminal control; proc.resize on layout. No host ConPTY needed.
Editor / diff Panels/EditorPanel.swift, SyntaxHighlighting.swift, diff tab Monaco in WebView2 (read-only diff-first); native later if desired.
Build & Run BuildRunPanel.swift Host process spawn via hostd intent (ChildProcess + Job Objects).
Settings SettingsView.swift (2,995), RemoteAccessView.swift, ModelCatalog.swift, QuotaIndicator.swift Sections that apply: agents/accounts (OAuth via loopback listener), sandbox (CPU/MemoryMB, restart-shared), remote access (QR pairing display, transports), updates (channel display; App Installer drives updates).
Attention DockBounce.swift, sound hooks (AppStore.swift:668-681) AppNotificationManager toasts + FlashWindowEx + optional sound, wired to the same AppStore hook events over the wire.
Accessibility/theming 4 color-vision palettes + 5 text sizes (app-wide) Resource dictionaries keyed off host-synced settings (SyncedSettings.swift); honor Windows high-contrast.

8. Onboarding flow (first run)

  1. Renderer launches → HostdLauncher starts hostd → hostd starts broker → broker components.missing().
  2. If WSL/wslc components missing → guided install page (elevated wsl --install / component install via broker), progress streamed, reboot handling.
  3. Broker session.ensureimage.pull naros-agent (progress → existing controlDownloadProgress UI path).
  4. Control-plane connectivity self-test (probeControlPlane) with actionable remediation (firewall script) on failure.
  5. Agent CLI auth: Claude/Codex/Grok OAuth flows (loopback listener, DPAPI custody) — same UX as macOS Settings.
  6. Add or clone a Control project → first session.

9. Parity gap plans

9.1 Tailnet (D9)

  • Build libtailscale as tailscale.dll (Go -buildmode=c-shared) for windows/amd64 + windows/arm64; new windows/scripts/build-libtailscale-windows.ps1 pinned to the same commit as scripts/build-tailscalekit.sh.
  • Sources/NucleicTailnet gains a Windows leg over the same libtailscale C header that TailscaleKit wraps (#if canImport(TailscaleKit) stays Apple-only; add #if os(Windows) + dlopen-or-link of the DLL). The fd-backed FrameChannel transport works over Winsock fds.
  • Absent DLL → existing isBuiltIn == false path (feature shows "not built in").

9.2 On-device AI (D6)

  • Intelligence.swift already has a provider seam with heuristic fallbacks. Add a WindowsFoundryIntelligenceProvider: Phi Silica is WinRT-only, so hostd routes requests to a C# sidecar — reuse brokerd with the ai.generate RPC (keeps one helper process). Capability-probe at startup; fall back to heuristics when unavailable (non-Copilot+ hardware) — identical UX to a Mac without Apple Intelligence.

9.3 Free-by-construction (verify, don't rebuild)

LockManager, ConflictCoordinator, nvrsion, Autoship + merge queue, idea inbox, activity/ streaks, quota gauges (SubscriptionUsage.swift logic), Cast/Mirror, transfer — all pure core + GRDB; they work once NucleicCore compiles and DPAPI custody lands. Cover via the existing cross-platform test suites plus the M2 e2e.

9.4 Explicitly out of scope (D4)

MacVM/ computer-use (mac_vm_* and GUI linux_vm_* tools stay unadvertised — they're per-token-registered, so simply don't register handlers), NucleicMDM server role, NucleicPowerHelper, Sparkle, SwiftTerm, shell/ nash host-side use (nash still ships inside the containers and is fully used there).


10. Repo layout for new code

windows/                          # sibling of ios/ and cloud/
  Nucleic.sln
  NucleicApp/                     # WinUI 3 app: Views/ Services/ Interop/ Assets/
  NucleicApp.Package/             # MSIX packaging project (per-channel manifests templated)
  NucleicBroker/                  # nucleic-brokerd (Microsoft.WSL.Containers + ai.generate)
  NucleicProtocol.Interop/        # shared P/Invoke + DTO library (app + tests)
  NucleicApp.Tests/               # xUnit incl. protocol-abi golden fixtures
  NucleicBroker.Tests/            # broker contract tests against fake IWslc
  spikes/                         # M1 spike programs (kept, runnable)
  appinstaller/                   # nucleic-<channel>.appinstaller.template
  props/Directory.Build.props     # TFM, versions, channel from VERSION/NUCLEIC_CHANNEL
  scripts/{build-hostd.ps1, package-msix.ps1, release-windows.ps1,
           build-libtailscale-windows.ps1}
Sources/
  nucleic-hostd/                  # Windows headless host (shares bring-up with nucleicd)
  NucleicProtocolC/               # C-ABI DLL target (+ include/nucleic_protocol.h, exports.def)
  NucleicCore/Container/SandboxEngine.swift
  NucleicCore/Container/Windows/{WslcContainerEngine,WslcBrokerClient,WslcProcessHandle}.swift
  NucleicCore/Windows/{WindowsSupport,CredentialStore,FilePermissions,ProcessTree,
                       FileWatcher,PowerBlocker,WindowsDNSSD,NIOLANTransport,
                       NIOLANListener,NIOByteConn}.swift
  NucleicCore/PortableLogging.swift          # lifted from LinuxSupport.swift
Tests/NucleicProtocolCTests/
fixtures/protocol-abi/
.github/workflows/windows.yml

11. Windows hostd design (Sources/nucleic-hostd)

Factor the shared headless bring-up out of Sources/nucleicd/Nucleicd.swift (makeStore, blank-API-key scrub, startup choreography ~lines 142-171) into an internal NucleicHeadless helper used by both nucleicd and hostd. hostd-specific duties:

  • Single-instance mutex; rendezvous file write (owner-only DACL); SetConsoleCtrlHandler.
  • Start order: GRDB store → AppStore graph → broker spawn + session.ensurestartSyncServer() → local renderer listener (loopback TCP, port 0, Noise XXpsk0 with PSK auto-approval — the local analog of meshAutoApprovePairing, Nucleicd.swift:101) → LAN listener + DNS-SD advertise → relay/tailnet transports per settings.
  • PowerBlocker held while sessions active. Windows Job Object on brokerd so it dies with hostd.
  • Transports env default: NUCLEIC_SYNC_TRANSPORTS=lan,relay (+tailnet when configured) vs nucleicd's relay-only default.

12. Work breakdown

Ordered; items 14 are Darwin-first refactors verifiable by the existing macOS suite and safe to start immediately on macOS hardware.

  1. Manifest & gatingPackage.swift only: onWindows/nonDarwin, widened platform conditions, Windows exclude list, new targets.
  2. PortableLogging — lift the Logger shim from LinuxSupport.swift; WindowsSupport.swift stubs.
  3. SecretStore seam — protocol + Keychain backend + retarget ~14 files (§4.2); DPAPI backend can land later with the Windows build.
  4. SandboxEngine seam — protocol + ContainerEngine conformance + retype ContainerManager and backend references (§3.1). macOS tests must stay green.
  5. Brokerwindows/NucleicBroker full RPC surface (§3.3) + fake-IWslc tests.
  6. Swift wslc clientWslcBrokerClient / WslcContainerEngine / WslcProcessHandle + tests against a scripted fake broker exe (mirror the fake-claude adapter-contract pattern).
  7. Control planeNIOByteConn, gateway plumbing through ensureRunning, control-bridge.js TCP branch (guest contract unchanged).
  8. hostdNucleicHeadless factor-out; Sources/nucleic-hostd/Hostd.swift (§11); NIO LAN transport + WindowsDNSSD.
  9. Windows shimsProcessTree, FilePermissions, FileWatcher, LoginShellEnv/PATH legs, NucleicPaths.
  10. Protocol DLL — target + C header + JSON bridge + golden fixtures + Swift tests.
  11. Interop + renderer coreNucleicProtocol.Interop, HostConnection.cs, RendererStore.cs, HostdLauncher, onboarding.
  12. Screens — Home → SessionDetail/Transcript → Composer → Approvals → Projects/ Settings → Panels (Terminal, Diff, Build&Run, Todos) → RemoteAccess/host picker.
  13. Remote parity — DNS-SD advertise + QR (reuse the mintPairingCode() path), relay (portable already), tailnet DLL + NucleicTailnet Windows leg.
  14. Packaging/CI/releasewindows.yml, MSIX projects, appinstaller templates, cloud/nucleic-updates /win/<channel>/latest routes, release-windows.ps1, Trusted Signing.
  15. naros multi-arch — extend os/ + .github/workflows/naros.yml to push an amd64+arm64 manifest list to GHCR (D10).
  16. Phi Silica provider (§9.2), accessibility/theming parity, polish.

Packaging details (item 14): MSIX Identities ABKSLM.Nucleic[.Dev/.Canary/.Beta/.RC] mirroring the bundle-ID scheme (Package.swift:27-35); runFullTrust; payload = three exes + NucleicProtocolC.dll + Swift runtime DLLs + MinGit; per-arch MSIX with .appinstaller <MainPackage ProcessorArchitecture> selection; feeds nucleic-<channel>.appinstaller on R2 beside the Sparkle appcasts with in-app update check reading the same feed (App Installer has no delta updates); publisher CN must match the Trusted Signing cert.


13. Milestones & exit criteria

M0 — Toolchain proof (riskiest first). Swift 6.2-windows (pinned in windows/props/swift-version.txt, x64 + arm64 toolchains) builds NucleicProtocol; NucleicProtocolTests + NucleicCarbonTests (the designated cross-platform suites, Package.swift:391-401) green on hosted windows-2025 (x64) and windows-11-arm runners, including GRDB/SQLite linkage. Exit: windows.yml green on both arches; corelibs/toolchain gap list filed as issues.

M1 — De-risking spikes (parallel, checked into windows/spikes/). (a) wslc: C# console — Session create → GHCR pull of naros-agent → container with an NTFS ContainerVolume → exec git status in the bind-mounted worktree → stdio round-trip → SIGTERM; measure git status and npm install latency on a real repo vs. native; verify uid semantics and broker-crash/service-state survival. (b) hvsocket: AF_HYPERV host listener ↔ AF_VSOCK dial from inside a wslc container; also verify gateway-TCP reachability + default-firewall behavior in NAT and mirrored modes. (c) DLL: SwiftPM-built NucleicProtocolC.dll, C# P/Invoke connect-pair-echo round-trip against a Swift test host; settle symbol-export strategy. Exit: written go/no-go on control-plane primary (TCP vs hvsocket); 9P perf numbers with mitigation decision; wslc API gaps filed upstream while preview feedback still lands.

M2 — Headless host runs an agent. NucleicCore compiles on Windows (work items 19); hostd boots, GRDB at %APPDATA%, broker supervises the wslc session; nucleic-smoke (already cross-platform) pairs over loopback and drives a full Claude session: prompt → MCP approval round-trip → git-interceptor event → worktree commit on NTFS. Exit: e2e session with approval + interceptor events asserted from the transcript; containers survive hostd kill/restart and reconcile; runs on the self-hosted x64 runner (D11).

M3 — Renderer alpha. DLL interop + golden fixtures; HostConnection/RendererStore; Home, Sessions, SessionDetail transcript, Composer, Approvals; hostd auto-launch + rendezvous. Exit: dogfood-usable real coding session end-to-end from the UI; transcript-parity harness (replay recorded HostMsg streams through np_projection_apply vs. the Swift projector, diff the JSON) passes on a recorded fixture session.

M4 — Remote + panels + packaging. DNS-SD advertise + QR pairing (iPhone pairs to the PC over LAN), relay listener, remote-cockpit host picker (D12), Terminal/Diff/Build&Run/Todos/Settings panels, toasts + taskbar attention, MSIX per-channel + .appinstaller on R2 (dev + canary), Trusted Signing, ARM64 package. Exit: iPhone full remote session against a Windows host over LAN + relay; canary installs and auto-updates via the App Installer feed on clean x64 + ARM64 VMs.

M5 — Parity beta. Tailnet DLL + Windows leg; Phi Silica provider + fallback; accessibility/theming parity (4 palettes, 5 sizes); quota/OAuth flows; idle/memory policy tuning; wslc GA migration (fall 2026) absorbed behind the broker; beta channel live; winget evaluation. Exit: parity checklist (every §7 screen row + every §9 gap row) green; 14-day dogfood with no P0s.


14. Verification strategy

  • Every CI run (from M0): NucleicProtocolTests + NucleicCarbonTests on Windows x64 + ARM64; the full macOS suite must stay green through the Darwin-first SandboxEngine/SecretStore refactors; DLL golden fixtures from Swift and C#.
  • Broker: C# unit tests against a fake IWslc; Swift WslcBrokerClient tests against a scripted fake broker exe (mirrors the fake-claude adapter-contract pattern).
  • M2 gate (self-hosted runner): nucleic-smoke e2e — full Claude session with approval + git-interceptor assertions from the transcript; hostd kill → restart → reconcile test; container idle-teardown test.
  • M3 gate: transcript-parity harness (recorded HostMsg replay, Swift vs. DLL projection JSON diff); renderer store unit tests over fixture event streams.
  • M4/M5 manual matrix: iPhone QR pairing over LAN / relay-only / tailnet; MSIX install → update → rollback across channels on clean x64 + ARM64 VMs with default firewall; accessibility spot-checks (high-contrast, palettes, text sizes); a longpath + LF-sensitive fixture repo through a full session + Autoship.

15. Risks & mitigations

Risk Mitigation
wslc preview churn (API breaks before GA) All wslc calls isolated in brokerd behind our own RPC; NuGet version-pinned per release; broker capabilities hello lets hostd degrade; M1 files gaps early while Microsoft is taking preview feedback.
Swift-Windows toolchain gaps (corelibs Foundation holes, DLL export quirks, NIO/GRDB linkage, FileHandle.bytes-class issues) M0 front-loads discovery; the Linux build's known-gap map is the checklist; CMake fallback documented for the DLL; a WindowsFoundationCompat.swift for point fixes.
9P NTFS bind-mount performance (git/npm in mounted worktrees) M1 measures on real workloads. Mitigations in order: per-directory metadata caching; hot caches (node_modules, build dirs) on ContainerNamedVolume; repo itself stays NTFS per D8. If numbers are catastrophic, surface to the user for a decision — do not silently move repos into ext4.
hvsocket-in-container reachability unknown Gateway-TCP is primary precisely because it's provable in M1; hvsocket is upside, not a dependency.
Firewall / networking-mode variance (NAT vs mirrored, DNS tunneling, enterprise policy) Broker auto-detects and reports the reachable host address; interface-scoped bind; onboarding connectivity self-test through probeControlPlane with actionable remediation.
DLL↔C# JSON drift Golden fixtures in fixtures/protocol-abi/ run from both languages on every CI run; DTOs reviewed against WireMessages.swift on protocol version bumps.
App Installer UX limits (no delta updates, prompt fatigue) In-app update check reads the same feed; keep MSIX payload lean (Swift runtime + MinGit are the big items); winget post-beta.
ARM64 tail risks (Go c-shared for windows/arm64, wslc-on-ARM less trodden) ARM64 is in CI from M0 so breakage is visible immediately; tailnet DLL is M5 scope.
CRLF/longpath git edge cases MinGit pinned + core.longpaths/core.autocrlf enforced on managed clones; e2e includes a longpath + LF-sensitive fixture repo.

16. Key file index (for implementing agents)

Purpose Path
Manifest gating to extend Package.swift (:59-70, :219, :266, :352-381, :402)
Stub/shim template Sources/NucleicCore/LinuxSupport.swift
Headless bring-up to factor Sources/nucleicd/Nucleicd.swift
Container policy (port contract) Sources/NucleicCore/Container/ContainerManager.swift
Engine surface to protocolize Sources/NucleicCore/Container/ContainerEngine.swift (:513,:660,:697,:727-:924)
Process handle pattern to mirror Sources/NucleicCore/Container/ContainerizedProcessHandle.swift, Sources/NucleicCore/ProcessHost.swift
ContainerSpec + constants Sources/NucleicCore/Backend.swift:54
Spec construction Sources/NucleicCore/SessionController.swift:643; Sources/NucleicCore/Project.swift:77
Control plane server + ByteConn seam Sources/NucleicCore/Claude/MCPApprovalServer.swift; Sources/NucleicCore/Claude/ApprovalServerRegistry.swift
Guest bridge containers/nucleic-sandbox/control-bridge.js
Interceptors + hook env Sources/NucleicCore/Container/ContainerEngine+Rootfs.swift:394,:466; Sources/NucleicCore/Container/CommandInterceptor.swift
Backend run loop (container exec path) Sources/NucleicCore/Claude/ClaudeCodeBackend.swift:766-1087
Sync authority + bridge Sources/NucleicCore/Sync/SyncHost.swift, ConnectionHandler.swift, SyncHostBridge.swift; Sources/NucleicCore/AppStore.swift (conforms; wires SyncHost ~:9410)
Wire protocol + client Sources/NucleicProtocol/Sync/{MessageEnvelope,WireMessages,SyncClient}.swift, CBOR/, Noise/, FrameChannel
Renderer template ios/NucleicRemote/NucleicRemote/Models/{RemoteStore,HostConnection}.swift
Transcript projections (shared) ios/NucleicRemote/.../Views/Transcript/{TranscriptProjection,IncrementalTranscriptProjection}.swift (SPM target NucleicRemoteProjection)
Git layer Sources/NucleicCore/Git/{GitRunner,WorktreeManager,GitHubCredentials}.swift
Persistence Sources/NucleicCore/Persistence/GRDBMetadataStore.swift
Intelligence seam Sources/NucleicCore/Intelligence.swift, AFMRequestQueue.swift
Contracts docs docs/VSOCK_CONTROL_PLANE.md, docs/CONTAINER_ISOLATION.md, docs/NAROS.md, docs/NASH.md, docs/RUNTIME_ARCHITECTURE.md, docs/COVALENCE_RUNNER.md
Release pipeline references scripts/package-app.sh, scripts/release-macos.sh, cloud/nucleic-updates, .github/workflows/{naros,sandbox-image}.yml

External references: WSL container overview · wslc API reference · wslc samples · Swift Windows install · GRDB releases (Windows since 7.10)