diff --git a/NucleicBroker/NucleicBroker.csproj b/NucleicBroker/NucleicBroker.csproj index 1f03130..e6b57ab 100644 --- a/NucleicBroker/NucleicBroker.csproj +++ b/NucleicBroker/NucleicBroker.csproj @@ -16,7 +16,10 @@ - net9.0-windows10.0.26100.0 + + net9.0-windows10.0.19041.0 $(DefineConstants);USE_WSLC @@ -26,8 +29,10 @@ - + float — the API is preview and breaking changes must be absorbed behind IWslc. + 2.9.3 is the ONLY version on nuget.org and it tracks WSL's own version scheme, not + a `0.1.0-preview.N` one; the invented pin this replaced could never have restored. --> + diff --git a/NucleicBroker/Wslc/WslcFacade.cs b/NucleicBroker/Wslc/WslcFacade.cs index 5318a67..336819a 100644 --- a/NucleicBroker/Wslc/WslcFacade.cs +++ b/NucleicBroker/Wslc/WslcFacade.cs @@ -4,10 +4,29 @@ using Microsoft.WSL.Containers; namespace NucleicBroker.Wslc; // The REAL Microsoft.WSL.Containers adapter (docs/WINDOWS_PORT.md §1.5, §3.2), compiled -// only with -p:UseWslc=true on Windows. The API is public preview (GA fall 2026) and its -// exact shapes are validated by the M1 wslc spike — every mapping below that spikes prove -// wrong gets fixed HERE, never above the IWslc seam. Until M1 runs on real hardware, treat -// this file as the best-effort transcription of the documented object model: +// only with -p:UseWslc=true on Windows. +// +// !! KNOWN WRONG AS WRITTEN — DO NOT BUILD ON IT. !! +// +// M1 spike (a) has run (docs/WINDOWS_PORT.md §13.1) and the real API differs from this +// transcription in roughly twenty places. Most are renames that belong exactly here and +// nowhere else, which is what the IWslc seam is for: GetVersion not GetServiceVersion, +// `new Session(settings)` + Start() not CreateOrOpen, MemorySizeInMB, HostName, ImageName, +// CreateProcess-then-Start rather than RunProcess, DeleteContainerOption, a named Signal +// enum, RegistryAuth as a string, ImageInfo.Name/.Sha256, and two separate output events +// instead of one with a stderr flag. +// +// Three differences are NOT renames and need decisions before this file is finished: +// 1. there is no container enumeration at all, so §2.3 broker reattach and the +// container.list RPC have no API behind them; +// 2. there is no per-container statistics call, so container.stats must exec cgroup +// reads inside the container instead; +// 3. there is no pty and no uid/gid on ProcessSettings, so §7's Terminal panel loses +// tty mode and exec wraps argv in setpriv/su (which §3.2 already anticipated). +// +// Read §13.1 before touching this file. Fixing it is the next step of item 5, and it is +// now a fast loop: the package restores, so `dotnet build -p:UseWslc=true` compiles it. +// // WslcService (components) → Session (VM host, images) → Container → Process. public sealed class WslcFacade : IWslc { diff --git a/spikes/README.md b/spikes/README.md index b76c6cc..c979e14 100644 --- a/spikes/README.md +++ b/spikes/README.md @@ -11,15 +11,23 @@ NuGet and a real WSL stack, so they are built by path. ## `WslcApiDump` — what does the wslc API actually look like? -**The problem it solves.** Items 5, 6 and 7 — the C# broker, the Swift wslc client, and the -control plane — were all built against Microsoft's *documentation*, on machines with no WSL. -`NucleicBroker/Wslc/WslcFacade.cs` is therefore a careful transcription that has never been -compiled against the real assembly, and `-p:UseWslc=true` is the only thing standing between it -and the build. Every line in it might be right. We have no idea which. +**Status: it has served its original purpose (docs/WINDOWS_PORT.md §13.1).** The real API is +known, and it was obtained without a Windows machine at all — the package is public, so the +`.nupkg` was downloaded, its projection assembly extracted, and its metadata read with +`MetadataLoadContext`. What this tool is *for* has therefore changed: its assumption list now +describes the surface actually observed in 2.9.3, so running it says **what the next package +version moved**, not what we guessed wrong. -A typed spike cannot tell us: if `Session.CreateOrOpen` is really `Session.Open`, the spike fails -to **compile**, which teaches us one name and stops. So this tool is written entirely in -reflection. It cannot fail to build, and a single run prints the complete list of what is wrong. +**Why it is still reflection.** Same reason as before: a typed check fails to compile on the +first rename and reports one problem, where this reports all of them at once. That property is +worth keeping for a preview API whose next version can break anything. + +Two facts it discovered that anything referencing this package needs: + +- the assembly is **`wslcsdkcs.dll`**, not `Microsoft.WSL.Containers.dll` — loading it by package + name fails; +- the only published version is **2.9.3**, targeting `net8.0-windows10.0.19041.0`. The + `0.1.0-preview.1` pin this repo carried could never have restored. ```powershell cd windows/spikes/WslcApiDump @@ -33,7 +41,8 @@ It writes the full public object model to `wslc-api-dump.txt` (`--out` to reloca It exits 0 even when assumptions fail — a mismatch is the product, not an error. Only a genuinely broken run (the assembly won't load) exits non-zero. -**What to send back:** the console output, and `wslc-api-dump.txt` if anything is MISSING. +**What to send back:** the console output, and `wslc-api-dump.txt` if anything is MISSING — +which now means the package moved under us, not that we guessed wrong. ### Why `--session` earns its risk diff --git a/spikes/WslcApiDump/FacadeAssumptions.cs b/spikes/WslcApiDump/FacadeAssumptions.cs index 462aba7..8dfd098 100644 --- a/spikes/WslcApiDump/FacadeAssumptions.cs +++ b/spikes/WslcApiDump/FacadeAssumptions.cs @@ -1,18 +1,19 @@ namespace WslcApiDump; /// -/// Every member NucleicBroker/Wslc/WslcFacade.cs calls on the preview -/// Microsoft.WSL.Containers API, as plain strings. +/// The Microsoft.WSL.Containers surface `NucleicBroker/Wslc/WslcFacade.cs` depends on. /// -/// The facade was written from Microsoft's documentation without a machine to run it on -/// (docs/WINDOWS_PORT.md §1.5), so each line here is a *claim* — and this spike's job is to say, -/// for each one, whether the shipped assembly agrees. Strings rather than typed references is the -/// entire trick: a typed spike that names Session.CreateOrOpen fails to COMPILE if that -/// method has a different name, which teaches us nothing. Reflection turns every wrong guess into -/// a printed line instead of a build error, so one run produces the complete worklist. +/// **This list changed meaning on 2026-07-29.** It began as a list of *guesses* — the facade was +/// transcribed from documentation on machines with no WSL — and this tool existed to find out how +/// many were wrong. That question is answered (docs/WINDOWS_PORT.md §13.1: many of them, including +/// three that were missing capability rather than a wrong name). The entries below are now the +/// surface **actually observed in package 2.9.3**, so this tool's job has changed from discovery +/// to **drift detection**: run it after bumping the pin and it says what the new version moved. /// -/// Keep in lockstep with WslcFacade.cs. A member that stops being used should leave this list; -/// a newly-used member should join it, so the next run of this tool still covers the real surface. +/// Still strings rather than typed references, for the same reason as before — a typed check fails +/// to compile on the first rename and reports one problem; this reports all of them. +/// +/// Keep in lockstep with WslcFacade.cs. /// internal static class FacadeAssumptions { @@ -23,83 +24,93 @@ internal static class FacadeAssumptions internal static readonly Assumption[] All = [ // ---- Service entry point: onboarding (§8 step 2) ---- - new("WslcService", "GetServiceVersion", Kind.Method, - "hello capabilities — hostd degrades across preview→GA churn on this string"), + new("WslcService", "GetVersion", Kind.Method, + "hello capabilities — hostd degrades across preview→GA churn on this"), new("WslcService", "GetMissingComponents", Kind.Method, - "components.missing RPC; drives the guided-install onboarding page"), - new("WslcService", "InstallComponentsAsync", Kind.Method, - "components.install RPC"), - new("ComponentFlags", "None", Kind.EnumValue, - "the 'nothing missing' sentinel the facade filters on"), + "components.missing RPC; returns IReadOnlyList, NOT a flags enum"), + new("WslcService", "InstallWithDependencies", Kind.Method, "components.install RPC"), + new("Component", "WslPackage", Kind.EnumValue, + "one of the three components onboarding can report missing"), + new("ServiceVersion", "Major", Kind.Property, "the version triple reported in hello"), // ---- Session: one per channel, hosts every container (§3.2) ---- - new("SessionSettings", ".ctor", Kind.Constructor, - "SessionSettings(name, dataDir) — the two-arg shape the facade assumes"), + new("SessionSettings", ".ctor", Kind.Constructor, "SessionSettings(name, storagePath)"), new("SessionSettings", "CpuCount", Kind.Property, "session.ensure cpu"), - new("SessionSettings", "MemoryMB", Kind.Property, - "session.ensure memoryMB; §3.2 notes a resize needs a sandbox restart"), - new("Session", "CreateOrOpen", Kind.Method, - "THE create-or-attach primitive. If this is absent, broker reattach after a crash " - + "(§2.3) has no mechanism and the whole supervision design changes"), + new("SessionSettings", "MemorySizeInMB", Kind.Property, + "session.ensure memoryMB — NOT MemoryMB; a resize needs a sandbox restart (§3.2)"), + new("SessionSettings", "Timeout", Kind.Property, "idle timeout for the whole session VM"), + new("Session", ".ctor", Kind.Constructor, + "there is NO CreateOrOpen — construction is the only entry point, and whether a second " + + "construction with an existing name attaches or throws Error.SessionReserved is the " + + "open question broker reattach (§2.3) hangs on"), + new("Session", "Start", Kind.Method, "brings the session VM up after construction"), new("Session", "Terminate", Kind.Method, "session.terminate RPC"), - new("Session", "SessionTerminationHandler", Kind.Property, - "session.down notification → hostd's reconcile sweep"), - new("Session", "HostGatewayAddress", Kind.Property, - "THE control-plane address (§5). ensureRunning returns it to the Swift engine and " - + "control-bridge.js dials it. If this member does not exist, §5's primary transport " - + "needs another source (query the vNIC) or the hvsocket fallback gets promoted"), + new("Session", "Terminated", Kind.Event, + "session.down notification (SessionTerminationReason) → hostd's reconcile sweep"), + new("SessionTerminationReason", "Crashed", Kind.EnumValue, + "distinguishes a crash from an orderly shutdown in the session.down reason"), // ---- Images ---- - new("Session", "PullImageAsync", Kind.Method, "image.pull RPC (naros-agent from GHCR)"), - new("PullImageOptions", ".ctor", Kind.Constructor, "PullImageOptions(reference)"), - new("PullImageOptions", "Credentials", Kind.Property, "GHCR auth (registryAuth)"), - new("PullImageOptions", "Progress", Kind.Event, - "image.pullProgress → the existing controlDownloadProgress UI surface"), - new("RegistryCredentials", ".ctor", Kind.Constructor, "RegistryCredentials(user, password)"), + new("Session", "PullImage", Kind.Method, "image.pull RPC (naros-agent from GHCR), sync form"), + new("Session", "PullImageAsync", Kind.Method, + "the async form, which is where pull PROGRESS comes from (ImageProgress)"), + new("PullImageOptions", ".ctor", Kind.Constructor, "PullImageOptions(uri)"), + new("PullImageOptions", "RegistryAuth", Kind.Property, + "GHCR auth — a STRING, not a credentials object"), + new("ImageProgress", "CurrentBytes", Kind.Property, + "→ image.pullProgress → the existing controlDownloadProgress UI surface"), + new("ImageProgressStatus", "Downloading", Kind.EnumValue, "pull progress phase"), new("Session", "GetImages", Kind.Method, "image.list / image.inspect"), + new("ImageInfo", "Name", Kind.Property, "image ref — NOT .Reference"), + new("ImageInfo", "Sha256", Kind.Property, "image digest — NOT .Digest"), new("Session", "DeleteImage", Kind.Method, "image.delete"), // ---- Containers ---- new("Session", "CreateContainer", Kind.Method, "container.create"), - new("Session", "GetContainers", Kind.Method, "container.list; reattach re-enumeration"), - new("ContainerSettings", ".ctor", Kind.Constructor, "ContainerSettings(image)"), + new("ContainerSettings", ".ctor", Kind.Constructor, "ContainerSettings(imageName)"), new("ContainerSettings", "Name", Kind.Property, "channel-suffixed container naming"), - new("ContainerSettings", "Hostname", Kind.Property, "container.create hostname"), + new("ContainerSettings", "HostName", Kind.Property, "container.create hostname — capital N"), new("ContainerSettings", "NetworkingMode", Kind.Property, - "NAT vs mirrored (§5 item 4) — determines how the guest reaches the host"), + "None | Bridged — NOT the NAT/mirrored pair §5 assumed"), new("ContainerSettings", "Volumes", Kind.Property, "the NTFS worktree bind mount (D8)"), new("ContainerSettings", "InitProcess", Kind.Property, "naros-init as PID 1 (docs/NAROS.md)"), new("ContainerVolume", ".ctor", Kind.Constructor, - "ContainerVolume(hostPath, guestPath, readOnly) — the 3-arg shape"), - new("ContainerNetworkingMode", "", Kind.Type, "enum parsed from the RPC's networkingMode"), + "ContainerVolume(windowsPath, containerPath, readOnly)"), + new("ContainerNetworkingMode", "Bridged", Kind.EnumValue, + "the mode a container needs to reach the host at all (§5)"), new("Container", "Start", Kind.Method, "container.start"), - new("Container", "Stop", Kind.Method, "container.stop(signal, grace)"), - new("Container", "Delete", Kind.Method, "container.delete"), + new("Container", "Stop", Kind.Method, "container.stop(Signal, TimeSpan)"), + new("Container", "Delete", Kind.Method, "container.delete(DeleteContainerOption)"), new("Container", "State", Kind.Property, "container.state → running/stopped/absent"), - new("Container", "GetStatistics", Kind.Method, - "container.stats → ContainerResourceSample; the Swift engine folds deltas from it"), - new("Container", "RunProcess", Kind.Method, "proc.exec — the agent's own exec path"), + new("Container", "Inspect", Kind.Method, + "the ONLY per-container introspection there is — there is no GetStatistics(), so " + + "container.stats has to exec cgroup reads instead (§13.1 finding 2)"), + new("Container", "CreateProcess", Kind.Method, + "proc.exec — NOT RunProcess, and it does not start the process"), new("ContainerState", "Running", Kind.EnumValue, "the one state the facade tests by name"), - new("DeleteContainerFlags", "Force", Kind.EnumValue, "container.delete force"), + new("DeleteContainerOption", "Force", Kind.EnumValue, "container.delete force"), + new("Error", "ContainerNotFound", Kind.EnumValue, + "structured failure codes — a better source for the RPC's data.kind than string matching"), // ---- Processes: the agent stdio path (§3.2 WslcProcessHandle) ---- - new("ProcessSettings", "CmdLine", Kind.Property, "argv"), - new("ProcessSettings", "Environment", Kind.Property, "env"), + new("ProcessSettings", "CommandLine", Kind.Property, "argv — NOT CmdLine"), + new("ProcessSettings", "EnvironmentVariables", Kind.Property, "env — NOT Environment"), new("ProcessSettings", "WorkingDirectory", Kind.Property, "cwd"), new("ProcessSettings", "OutputMode", Kind.Property, "event-mode stdio, not polling"), - new("ProcessSettings", "Terminal", Kind.Property, "tty for the Terminal panel (§7)"), - new("ProcessSettings", "UserId", Kind.Property, - "runAsUID 501 (§3.2). If absent, exec falls back to a setpriv/su wrapper argv — " - + "interceptors and nash don't care about the numeric uid, so this is recoverable"), - new("ProcessSettings", "GroupId", Kind.Property, "runAsGID"), new("ProcessOutputMode", "Event", Kind.EnumValue, - "the mode that makes stdio push-based; polling would change the whole broker design"), - new("Signal", "", Kind.Type, "the enum Stop/Signal take; the RPC carries POSIX ints"), - new("Process", "OutputReceived", Kind.Event, "→ proc.stdout / proc.stderr notifications"), + "the mode that makes stdio push-based; polling would change the broker design"), + new("Process", "Start", Kind.Method, + "the second half of exec — handlers are attached between CreateProcess and this, which " + + "is precisely why the API is split in two"), + new("Process", "OutputReceived", Kind.Event, "→ proc.stdout notifications"), + new("Process", "ErrorReceived", Kind.Event, + "→ proc.stderr — a SEPARATE event, not a stderr flag on one handler"), new("Process", "Exited", Kind.Event, "→ proc.exit; must never overtake output (OutboundWriter)"), - new("Process", "WriteStdin", Kind.Method, "proc.stdin (NDJSON to the agent)"), - new("Process", "CloseStdin", Kind.Method, "proc.closeStdin"), + new("Process", "GetInputStream", Kind.Method, + "proc.stdin — a WinRT stream, not a WriteStdin call; closing it is proc.closeStdin"), new("Process", "Signal", Kind.Method, "proc.signal — the agent's real POSIX Stop path"), - new("Process", "ResizeTerminal", Kind.Method, "proc.resize (tty mode)"), + new("Signal", "SIGKILL", Kind.EnumValue, + "signals are a NAMED enum; the RPC carries POSIX ints, so the broker maps them, and " + + "anything outside None/HUP/INT/QUIT/KILL/TERM is unavailable"), ]; } diff --git a/spikes/WslcApiDump/Program.cs b/spikes/WslcApiDump/Program.cs index 1ec99bc..a3700f5 100644 --- a/spikes/WslcApiDump/Program.cs +++ b/spikes/WslcApiDump/Program.cs @@ -20,7 +20,10 @@ namespace WslcApiDump; /// internal static class Program { - private const string AssemblyName = "Microsoft.WSL.Containers"; + /// The C#/WinRT PROJECTION assembly. The NuGet package is "Microsoft.WSL.Containers" but + /// the managed assembly it ships is `lib/net8.0-windows10.0.19041.0/wslcsdkcs.dll`, so + /// loading it by package name fails — which is exactly the first thing this tool found. + private const string AssemblyName = "wslcsdkcs"; private static int Main(string[] args) { diff --git a/spikes/WslcApiDump/WslcApiDump.csproj b/spikes/WslcApiDump/WslcApiDump.csproj index 6d090be..3f07b1b 100644 --- a/spikes/WslcApiDump/WslcApiDump.csproj +++ b/spikes/WslcApiDump/WslcApiDump.csproj @@ -11,14 +11,14 @@ Exe wslc-api-dump WslcApiDump - net9.0-windows10.0.26100.0 + net9.0-windows10.0.19041.0 - +