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
-
+