From 148c66e0f777ddf97d8d08b12b5c299229a7109b Mon Sep 17 00:00:00 2001 From: Nucleic Date: Mon, 27 Jul 2026 17:56:49 -0700 Subject: [PATCH] Merge nucleic/zesty-opal-robin-p6sk into dev --- docs/WINDOWS_PORT.md | 846 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 846 insertions(+) create mode 100644 docs/WINDOWS_PORT.md diff --git a/docs/WINDOWS_PORT.md b/docs/WINDOWS_PORT.md new file mode 100644 index 00000000..dfdd46d0 --- /dev/null +++ b/docs/WINDOWS_PORT.md @@ -0,0 +1,846 @@ +# 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 `ApprovalServerRegistry` → + `containerManager.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 ` → `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\ │ │ +│ └───────────────────────┘ │ 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-" (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-` (hostd also self-guards). +- hostd writes a rendezvous file `%LOCALAPPDATA%\Nucleic\\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`): + +```swift +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.swift`** — `SandboxEngine` 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` 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-", dataDir: %LOCALAPPDATA%\Nucleic\\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:"))` 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.Event` → `proc.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\\` 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-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\\` with worktrees at + `\.nucleic\worktrees\` (unchanged layout). +- Approval-server socket paths (`/tmp/nucleic/.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 (`9098` → `NUCLEIC_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`): + +```c +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.ensure` → `image.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-.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.ensure` → + `startSyncServer()` → 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 1–4 are **Darwin-first refactors** verifiable by the existing macOS suite +and safe to start immediately on macOS hardware. + +1. **Manifest & gating** — `Package.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. **Broker** — `windows/NucleicBroker` full RPC surface (§3.3) + fake-`IWslc` tests. +6. **Swift wslc client** — `WslcBrokerClient` / `WslcContainerEngine` / + `WslcProcessHandle` + tests against a scripted fake broker exe (mirror the + `fake-claude` adapter-contract pattern). +7. **Control plane** — `NIOByteConn`, gateway plumbing through `ensureRunning`, + `control-bridge.js` TCP branch (guest contract unchanged). +8. **hostd** — `NucleicHeadless` factor-out; `Sources/nucleic-hostd/Hostd.swift` (§11); + NIO LAN transport + `WindowsDNSSD`. +9. **Windows shims** — `ProcessTree`, `FilePermissions`, `FileWatcher`, + `LoginShellEnv/PATH` legs, `NucleicPaths`. +10. **Protocol DLL** — target + C header + JSON bridge + golden fixtures + Swift tests. +11. **Interop + renderer core** — `NucleicProtocol.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/release** — `windows.yml`, MSIX projects, appinstaller templates, + `cloud/nucleic-updates` `/win//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` `` selection; feeds +`nucleic-.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 1–9); 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](https://learn.microsoft.com/en-us/windows/wsl/wsl-container) · +[wslc API reference](https://wsl.dev/api-reference/) · [wslc samples](https://aka.ms/wslc-samples) · +[Swift Windows install](https://www.swift.org/install/windows/) · +[GRDB releases (Windows since 7.10)](https://github.com/groue/GRDB.swift/releases)