62 KiB
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) computesonLinuxat manifest-eval time (Package.swift:62-64), dropsdarwinOnlyTargets(:70,:402), conditionalizes dependency lists (:219,:266), and excludes Apple-framework sources fromNucleicCorevia a per-targetexclude:list (:352-381). The manifest is also channel-aware (NUCLEIC_CHANNELenv → product name +NUCLEIC_DEV/CANARY/BETA/RC/STABLEcompile define).- Source-level gating is almost entirely
#if canImport(...)(only 3#if os(...)in all ofSources/:Claude/ClaudeCredentialBroker.swift:360,Codex/CodexCredentialBroker.swift:166,Project.swift:465). Sources/NucleicCore/LinuxSupport.swift(~443 LOC) provides stub actors (ContainerManager,MacVMManagermirrors that throwLinuxUnsupported) plus anos.Loggershim, so shared call sites compile unchanged on non-Darwin. This is the template forWindowsSupport.swift.Sources/nucleicd/(645 LOC:Nucleicd.swift,ControlEndpoint.swift) is a proven headless host overNucleicCorefor Linux (the Covalence Cloud Runner): oneAppStoregraph,startSyncServer(), pairing mint, signal-driven shutdown. This is the template fornucleic-hostd.NucleicProtocolbuilds 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:
- The agent sandbox (
Container/): Applecontainerizationframework, headless Linux microVMs. → This maps to wslc. - Computer-use VMs (
MacVM/): fullVirtualization.frameworkGUI VMs. → Excluded.
Key mechanics that carry over unchanged (all guest-side / protocol-level, OS-agnostic):
- Control plane protocol: host
MCPApprovalServerserves HTTP JSON-RPC atPOST /mcp(Streamable-HTTP MCP:initialize,ping,tools/list,tools/callwith SSE for gated calls), bearer-token multiplexed per session, plus plain JSONPOST /git-event,/gh-event,/command-event,/shell-event(dispatch atMCPApprovalServer.swift:1903-1916). Agent-side MCP config (mcpConfigJSON,:1611) always points athttp://127.0.0.1:9099/mcp. - Guest bridge:
containers/nucleic-sandbox/control-bridge.jslistens on guest loopback9099(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 AppleUnixSocketConfiguration(.into)— the only macOS-specific piece of the entire control plane. A second optional forwarder (proxyBridgePort9098) 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:394and:466), exec the real binary then POST{argv, cwd, exitCode, subcommand, sessionId}toNUCLEIC_GIT_HOOK_URL/NUCLEIC_GH_HOOK_URLwith a bearer token. Optional bash command tracer + batch poster inContainer/CommandInterceptor.swift(also home ofCommandInterceptor.hookEnv(...), the single source of interceptor endpoint env, andagentShellArgv/hostShellArgvfor nash selection). The nash shell (shell/, Rust) reports/shell-event. - Agent run flow (
Claude/ClaudeCodeBackend.swiftrunLoop,:766-1087; Codex/Grok backends identical shape): resolve approval server viaApprovalServerRegistry→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) → argvclaude -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 aProcessHandle(Container/ContainerizedProcessHandle.swiftadapts framework stdio onto the same NDJSONAsyncThrowingStreaminterface a hostChildProcessprovides). - ContainerSpec (
Backend.swift:54):name,image,mounts: [Mount](host/container/readOnly),workdir,env,idleTimeout,claudeHomeStaging/claudeHomeWritable,installGitInterceptor,cpus,memoryGiB,runAsUID(=narosAgentUID501)/runAsGID,controlSocketHostPath,proxySocketHostPath. Constants:controlSocketGuestPath = "/run/nucleic/control.sock",controlBridgePort = 9099,proxyBridgePort = 9098. Built bySessionController.containerSpec()(SessionController.swift:643) fromProject.sandbox(ProjectSandbox,Project.swift:77). - Images: agent rootfs
ghcr.io/abkslm/naros-agent:26.07(narOS, built fromos/by.github/workflows/naros.yml; contract indocs/NAROS.md:naros-initPID 1,agentuid 501, nash as/bin/sh,NAROS_BRIDGE=1supervises control-bridge); legacyghcr.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_TOKENor app GitHub token withread: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 theLinuxSupport.swiftstubs — 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). NamespaceMicrosoft.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 viaVhdOptions; 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;OutputReceivedevents, 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, andSources/NucleicCore/JSONRPCConnection.swiftfor 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
Sessionand re-enumerates containers (wslc state is service-backed); hostd then runsContainerManager.reconcile(activeSessions:)against the fresh listing. In-flight execs fail with a distinguishablebrokerLosterror, mapped to the same recovery pathContainerizedProcessHandle.forceCloseStreams()serves today. - Broker sends a
capabilitieshello (wslc/NuGet version, supported features) so hostd can degrade gracefully across preview→GA churn.
2.4 Lifetime & single-instance
Nucleic.exelaunches hostd at startup if not running. Single-instance guard: named mutexLocal\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 (andnucleic-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-263gets 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):
- Add the protocol; conform the Apple
ContainerEngine(mechanical). - Retype
ContainerManager's engine reference (and the few direct engine references inClaudeCodeBackend/CodexAppServerBackend/CodexExecBackend/GrokACPBackend) toany SandboxEngine. - Keep
LinuxSupport.swiftstubs compiling (Linux/nucleicd behavior unchanged — the stubContainerManagerremains the Linux story). - On Windows,
ContainerManager.swiftmoves off the exclude list and compiles against the protocol. Engine files (ContainerEngine*.swift,ContainerizedProcessHandle.swift,MemoryBalloon.swift) stay excluded. AuditOCIArtifact.swift: pure ref-parsing/version helpers move to a shared file ifWslcContainerEnginewants them.
3.2 WslcContainerEngine mapping
New files in Sources/NucleicCore/Container/Windows/ (compiled only on Windows):
WslcBrokerClient.swift— spawnsnucleic-brokerd.exeviaChildProcess, speaks NDJSON JSON-RPC over its stdio, multiplexes requests, supervises (backoff restart, capabilities hello, reattach → notifyContainerManager.reconcile).WslcContainerEngine.swift—SandboxEngineconformance over the broker RPC.WslcProcessHandle.swift— mirrorsContainerizedProcessHandle.swift: adaptsproc.stdout/stderr/exitnotifications onto theProcessHandleprotocol (ProcessHost.swift:44-70):stdoutLines/stderrLinesasAsyncThrowingStream<Data, Error>through the sharedLineSplitter,writeLineframing NDJSON to stdin,wait()→ exit code,sendSignal→ brokerproc.signal,forceCloseStreams()on broker loss,lastActivityNanosbumping,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; wrapsWslcService.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.inspectcontainer.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}(fromSessionTerminationHandler)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 theonLinuxdetection atPackage.swift:62-64); introducenonDarwin = onLinux || onWindowsand use it whereveronLinuxcurrently gatesdarwinOnlyTargets, Sparkle, and product lists (:70,:219,:266,:402). - Widen
NucleicProtocol's swift-nio / swift-crypto dependency conditions from.linuxto[.linux, .windows]. - NucleicCore
exclude:for Windows = the Linux list (:352-381) minusContainer/ContainerManager.swift(now protocol-typed, compiles on Windows), keeping excluded:Container/ContainerEngine.swift,Container/ContainerEngine+Rootfs.swift,Container/ContainerizedProcessHandle.swift,Container/MemoryBalloon.swift, allMacVM/*Engine*/MacVMManager/MacVMExecChannel/MacVMAgentClient, and the Network.framework Sync files.Container/CommandInterceptor.swift,MacVMSpec.swift,MacVMSurface.swiftstay in (pure values). - Add
Sources/NucleicCore/Windows/**andSources/NucleicCore/Container/Windows/**(excluded on non-Windows), theNucleicProtocolCdynamic-library target/product, and thenucleic-hostdexecutable target. - SwiftPM remains the build system, including the DLL (
.library(type: .dynamic)). If@_cdeclsymbol auto-export proves unreliable, pass a checked-inSources/NucleicProtocolC/exports.defvia-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 containerchannelSuffixscheme). GRDB WALDatabasePool(Persistence/GRDBMetadataStore.swift:13and the other Application-Support call sites:KeychainOwnedAccess.swift:177,TailnetTransport.swift,Claude/ApprovalServerRegistry.swift,Cast/MeshDebugLog.swift) get a singleNucleicPathshelper 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 existingApprovalServerRegistryAPI on Windows (§5). - Git: bundle MinGit (x64/arm64) in the MSIX; configure managed clones with
core.longpaths=trueandcore.autocrlf=false(agents run in Linux containers — the repo must stay LF;.gitattributes-driven projects behave as configured).GitRunner/WorktreeManagercode 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).
MCPApprovalServer.start(host:port:)gains theNIOByteConntransport (§4.3), bound only to the WSL-facing interface address (never 0.0.0.0).WslcContainerEngine.ensureRunningreturns the gateway address the broker reports — this is exactly thehostGatewayvalue the macOS engine already returns, so the contract doesn't change.control-bridge.jsgets a small additive branch: whenNUCLEIC_CONTROL_HOST/NUCLEIC_CONTROL_PORTenv are present, forward guest-loopback9099to that TCP endpoint instead of the relayed UDS (keep the UDS branch for macOS). Same for the proxy bridge (9098→NUCLEIC_PROXY_HOST/PORT).- 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.
- 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
probeControlPlaneseam, with an elevated one-shotNew-NetFirewallRuleremediation 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 fornucleic_protocol.h;HostMsg/ClientMsgDTOs 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— theRemoteStoreanalog:ObservableObjectprojection (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 → brokercomponents.*), 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)
- Renderer launches →
HostdLauncherstarts hostd → hostd starts broker → brokercomponents.missing(). - If WSL/wslc components missing → guided install page (elevated
wsl --install/ component install via broker), progress streamed, reboot handling. - Broker
session.ensure→image.pullnaros-agent (progress → existingcontrolDownloadProgressUI path). - Control-plane connectivity self-test (
probeControlPlane) with actionable remediation (firewall script) on failure. - Agent CLI auth: Claude/Codex/Grok OAuth flows (loopback listener, DPAPI custody) — same UX as macOS Settings.
- 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; newwindows/scripts/build-libtailscale-windows.ps1pinned to the same commit asscripts/build-tailscalekit.sh. Sources/NucleicTailnetgains 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-backedFrameChanneltransport works over Winsock fds.- Absent DLL → existing
isBuiltIn == falsepath (feature shows "not built in").
9.2 On-device AI (D6)
Intelligence.swiftalready has a provider seam with heuristic fallbacks. Add aWindowsFoundryIntelligenceProvider: Phi Silica is WinRT-only, so hostd routes requests to a C# sidecar — reuse brokerd with theai.generateRPC (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.ensure→startSyncServer()→ local renderer listener (loopback TCP, port 0, Noise XXpsk0 with PSK auto-approval — the local analog ofmeshAutoApprovePairing,Nucleicd.swift:101) → LAN listener + DNS-SD advertise → relay/tailnet transports per settings. PowerBlockerheld while sessions active. Windows Job Object on brokerd so it dies with hostd.- Transports env default:
NUCLEIC_SYNC_TRANSPORTS=lan,relay(+tailnetwhen 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.
- Manifest & gating —
Package.swiftonly:onWindows/nonDarwin, widened platform conditions, Windows exclude list, new targets. - PortableLogging — lift the Logger shim from
LinuxSupport.swift;WindowsSupport.swiftstubs. - SecretStore seam — protocol + Keychain backend + retarget ~14 files (§4.2); DPAPI backend can land later with the Windows build.
- SandboxEngine seam — protocol +
ContainerEngineconformance + retypeContainerManagerand backend references (§3.1). macOS tests must stay green. - Broker —
windows/NucleicBrokerfull RPC surface (§3.3) + fake-IWslctests. - Swift wslc client —
WslcBrokerClient/WslcContainerEngine/WslcProcessHandle+ tests against a scripted fake broker exe (mirror thefake-claudeadapter-contract pattern). - Control plane —
NIOByteConn, gateway plumbing throughensureRunning,control-bridge.jsTCP branch (guest contract unchanged). - hostd —
NucleicHeadlessfactor-out;Sources/nucleic-hostd/Hostd.swift(§11); NIO LAN transport +WindowsDNSSD. - Windows shims —
ProcessTree,FilePermissions,FileWatcher,LoginShellEnv/PATHlegs,NucleicPaths. - Protocol DLL — target + C header + JSON bridge + golden fixtures + Swift tests.
- Interop + renderer core —
NucleicProtocol.Interop,HostConnection.cs,RendererStore.cs,HostdLauncher, onboarding. - Screens — Home → SessionDetail/Transcript → Composer → Approvals → Projects/ Settings → Panels (Terminal, Diff, Build&Run, Todos) → RemoteAccess/host picker.
- Remote parity — DNS-SD advertise + QR (reuse the
mintPairingCode()path), relay (portable already), tailnet DLL +NucleicTailnetWindows leg. - Packaging/CI/release —
windows.yml, MSIX projects, appinstaller templates,cloud/nucleic-updates/win/<channel>/latestroutes,release-windows.ps1, Trusted Signing. - naros multi-arch — extend
os/+.github/workflows/naros.ymlto push an amd64+arm64 manifest list to GHCR (D10). - 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 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+NucleicCarbonTestson 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; SwiftWslcBrokerClienttests against a scripted fake broker exe (mirrors thefake-claudeadapter-contract pattern). - M2 gate (self-hosted runner):
nucleic-smokee2e — 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)