Merge nucleic/olive-jade-civet-rznt into dev

This commit is contained in:
2026-07-29 00:14:32 -07:00
parent b342860779
commit 140b358da6
6 changed files with 127 additions and 80 deletions
+8 -3
View File
@@ -16,7 +16,10 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(UseWslc)' == 'true'"> <PropertyGroup Condition="'$(UseWslc)' == 'true'">
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework> <!-- Windows SDK 19041, matching the package's own lib TFM
(lib/net8.0-windows10.0.19041.0/wslcsdkcs.dll). Targeting a higher SDK revision
(26100) only adds a targeting-pack requirement the package does not need. -->
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
<DefineConstants>$(DefineConstants);USE_WSLC</DefineConstants> <DefineConstants>$(DefineConstants);USE_WSLC</DefineConstants>
</PropertyGroup> </PropertyGroup>
@@ -26,8 +29,10 @@
<ItemGroup Condition="'$(UseWslc)' == 'true'"> <ItemGroup Condition="'$(UseWslc)' == 'true'">
<!-- Version-pinned (docs/WINDOWS_PORT.md §15): bump deliberately per release, never <!-- Version-pinned (docs/WINDOWS_PORT.md §15): bump deliberately per release, never
float — the API is preview and breaking changes must be absorbed behind IWslc. --> float — the API is preview and breaking changes must be absorbed behind IWslc.
<PackageReference Include="Microsoft.WSL.Containers" Version="[0.1.0-preview.1]" /> 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. -->
<PackageReference Include="Microsoft.WSL.Containers" Version="[2.9.3]" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+23 -4
View File
@@ -4,10 +4,29 @@ using Microsoft.WSL.Containers;
namespace NucleicBroker.Wslc; namespace NucleicBroker.Wslc;
// The REAL Microsoft.WSL.Containers adapter (docs/WINDOWS_PORT.md §1.5, §3.2), compiled // 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 // only with -p:UseWslc=true on Windows.
// 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 // !! KNOWN WRONG AS WRITTEN — DO NOT BUILD ON IT. !!
// this file as the best-effort transcription of the documented object model: //
// 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. // WslcService (components) → Session (VM host, images) → Container → Process.
public sealed class WslcFacade : IWslc public sealed class WslcFacade : IWslc
{ {
+18 -9
View File
@@ -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? ## `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 **Status: it has served its original purpose (docs/WINDOWS_PORT.md §13.1).** The real API is
control plane — were all built against Microsoft's *documentation*, on machines with no WSL. known, and it was obtained without a Windows machine at all — the package is public, so the
`NucleicBroker/Wslc/WslcFacade.cs` is therefore a careful transcription that has never been `.nupkg` was downloaded, its projection assembly extracted, and its metadata read with
compiled against the real assembly, and `-p:UseWslc=true` is the only thing standing between it `MetadataLoadContext`. What this tool is *for* has therefore changed: its assumption list now
and the build. Every line in it might be right. We have no idea which. 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 **Why it is still reflection.** Same reason as before: a typed check fails to compile on the
to **compile**, which teaches us one name and stops. So this tool is written entirely in first rename and reports one problem, where this reports all of them at once. That property is
reflection. It cannot fail to build, and a single run prints the complete list of what is wrong. 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 ```powershell
cd windows/spikes/WslcApiDump 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 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. 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 ### Why `--session` earns its risk
+72 -61
View File
@@ -1,18 +1,19 @@
namespace WslcApiDump; namespace WslcApiDump;
/// <summary> /// <summary>
/// Every member <c>NucleicBroker/Wslc/WslcFacade.cs</c> calls on the preview /// The <c>Microsoft.WSL.Containers</c> surface `NucleicBroker/Wslc/WslcFacade.cs` depends on.
/// <c>Microsoft.WSL.Containers</c> API, as plain strings.
/// ///
/// The facade was written from Microsoft's documentation without a machine to run it on /// **This list changed meaning on 2026-07-29.** It began as a list of *guesses* — the facade was
/// (docs/WINDOWS_PORT.md §1.5), so each line here is a *claim* — and this spike's job is to say, /// transcribed from documentation on machines with no WSL — and this tool existed to find out how
/// for each one, whether the shipped assembly agrees. Strings rather than typed references is the /// many were wrong. That question is answered (docs/WINDOWS_PORT.md §13.1: many of them, including
/// entire trick: a typed spike that names <c>Session.CreateOrOpen</c> fails to COMPILE if that /// three that were missing capability rather than a wrong name). The entries below are now the
/// method has a different name, which teaches us nothing. Reflection turns every wrong guess into /// surface **actually observed in package 2.9.3**, so this tool's job has changed from discovery
/// a printed line instead of a build error, so one run produces the complete worklist. /// 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; /// Still strings rather than typed references, for the same reason as before — a typed check fails
/// a newly-used member should join it, so the next run of this tool still covers the real surface. /// to compile on the first rename and reports one problem; this reports all of them.
///
/// Keep in lockstep with WslcFacade.cs.
/// </summary> /// </summary>
internal static class FacadeAssumptions internal static class FacadeAssumptions
{ {
@@ -23,83 +24,93 @@ internal static class FacadeAssumptions
internal static readonly Assumption[] All = internal static readonly Assumption[] All =
[ [
// ---- Service entry point: onboarding (§8 step 2) ---- // ---- Service entry point: onboarding (§8 step 2) ----
new("WslcService", "GetServiceVersion", Kind.Method, new("WslcService", "GetVersion", Kind.Method,
"hello capabilities — hostd degrades across preview→GA churn on this string"), "hello capabilities — hostd degrades across preview→GA churn on this"),
new("WslcService", "GetMissingComponents", Kind.Method, new("WslcService", "GetMissingComponents", Kind.Method,
"components.missing RPC; drives the guided-install onboarding page"), "components.missing RPC; returns IReadOnlyList<Component>, NOT a flags enum"),
new("WslcService", "InstallComponentsAsync", Kind.Method, new("WslcService", "InstallWithDependencies", Kind.Method, "components.install RPC"),
"components.install RPC"), new("Component", "WslPackage", Kind.EnumValue,
new("ComponentFlags", "None", Kind.EnumValue, "one of the three components onboarding can report missing"),
"the 'nothing missing' sentinel the facade filters on"), new("ServiceVersion", "Major", Kind.Property, "the version triple reported in hello"),
// ---- Session: one per channel, hosts every container (§3.2) ---- // ---- Session: one per channel, hosts every container (§3.2) ----
new("SessionSettings", ".ctor", Kind.Constructor, new("SessionSettings", ".ctor", Kind.Constructor, "SessionSettings(name, storagePath)"),
"SessionSettings(name, dataDir) — the two-arg shape the facade assumes"),
new("SessionSettings", "CpuCount", Kind.Property, "session.ensure cpu"), new("SessionSettings", "CpuCount", Kind.Property, "session.ensure cpu"),
new("SessionSettings", "MemoryMB", Kind.Property, new("SessionSettings", "MemorySizeInMB", Kind.Property,
"session.ensure memoryMB; §3.2 notes a resize needs a sandbox restart"), "session.ensure memoryMB — NOT MemoryMB; a resize needs a sandbox restart (§3.2)"),
new("Session", "CreateOrOpen", Kind.Method, new("SessionSettings", "Timeout", Kind.Property, "idle timeout for the whole session VM"),
"THE create-or-attach primitive. If this is absent, broker reattach after a crash " new("Session", ".ctor", Kind.Constructor,
+ "(§2.3) has no mechanism and the whole supervision design changes"), "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", "Terminate", Kind.Method, "session.terminate RPC"),
new("Session", "SessionTerminationHandler", Kind.Property, new("Session", "Terminated", Kind.Event,
"session.down notification → hostd's reconcile sweep"), "session.down notification (SessionTerminationReason) → hostd's reconcile sweep"),
new("Session", "HostGatewayAddress", Kind.Property, new("SessionTerminationReason", "Crashed", Kind.EnumValue,
"THE control-plane address (§5). ensureRunning returns it to the Swift engine and " "distinguishes a crash from an orderly shutdown in the session.down reason"),
+ "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"),
// ---- Images ---- // ---- Images ----
new("Session", "PullImageAsync", Kind.Method, "image.pull RPC (naros-agent from GHCR)"), new("Session", "PullImage", Kind.Method, "image.pull RPC (naros-agent from GHCR), sync form"),
new("PullImageOptions", ".ctor", Kind.Constructor, "PullImageOptions(reference)"), new("Session", "PullImageAsync", Kind.Method,
new("PullImageOptions", "Credentials", Kind.Property, "GHCR auth (registryAuth)"), "the async form, which is where pull PROGRESS comes from (ImageProgress)"),
new("PullImageOptions", "Progress", Kind.Event, new("PullImageOptions", ".ctor", Kind.Constructor, "PullImageOptions(uri)"),
"image.pullProgress → the existing controlDownloadProgress UI surface"), new("PullImageOptions", "RegistryAuth", Kind.Property,
new("RegistryCredentials", ".ctor", Kind.Constructor, "RegistryCredentials(user, password)"), "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("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"), new("Session", "DeleteImage", Kind.Method, "image.delete"),
// ---- Containers ---- // ---- Containers ----
new("Session", "CreateContainer", Kind.Method, "container.create"), new("Session", "CreateContainer", Kind.Method, "container.create"),
new("Session", "GetContainers", Kind.Method, "container.list; reattach re-enumeration"), new("ContainerSettings", ".ctor", Kind.Constructor, "ContainerSettings(imageName)"),
new("ContainerSettings", ".ctor", Kind.Constructor, "ContainerSettings(image)"),
new("ContainerSettings", "Name", Kind.Property, "channel-suffixed container naming"), 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, 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", "Volumes", Kind.Property, "the NTFS worktree bind mount (D8)"),
new("ContainerSettings", "InitProcess", Kind.Property, "naros-init as PID 1 (docs/NAROS.md)"), new("ContainerSettings", "InitProcess", Kind.Property, "naros-init as PID 1 (docs/NAROS.md)"),
new("ContainerVolume", ".ctor", Kind.Constructor, new("ContainerVolume", ".ctor", Kind.Constructor,
"ContainerVolume(hostPath, guestPath, readOnly) — the 3-arg shape"), "ContainerVolume(windowsPath, containerPath, readOnly)"),
new("ContainerNetworkingMode", "", Kind.Type, "enum parsed from the RPC's networkingMode"), 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", "Start", Kind.Method, "container.start"),
new("Container", "Stop", Kind.Method, "container.stop(signal, grace)"), new("Container", "Stop", Kind.Method, "container.stop(Signal, TimeSpan)"),
new("Container", "Delete", Kind.Method, "container.delete"), new("Container", "Delete", Kind.Method, "container.delete(DeleteContainerOption)"),
new("Container", "State", Kind.Property, "container.state → running/stopped/absent"), new("Container", "State", Kind.Property, "container.state → running/stopped/absent"),
new("Container", "GetStatistics", Kind.Method, new("Container", "Inspect", Kind.Method,
"container.stats → ContainerResourceSample; the Swift engine folds deltas from it"), "the ONLY per-container introspection there is — there is no GetStatistics(), so "
new("Container", "RunProcess", Kind.Method, "proc.exec — the agent's own exec path"), + "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("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) ---- // ---- Processes: the agent stdio path (§3.2 WslcProcessHandle) ----
new("ProcessSettings", "CmdLine", Kind.Property, "argv"), new("ProcessSettings", "CommandLine", Kind.Property, "argv — NOT CmdLine"),
new("ProcessSettings", "Environment", Kind.Property, "env"), new("ProcessSettings", "EnvironmentVariables", Kind.Property, "env — NOT Environment"),
new("ProcessSettings", "WorkingDirectory", Kind.Property, "cwd"), new("ProcessSettings", "WorkingDirectory", Kind.Property, "cwd"),
new("ProcessSettings", "OutputMode", Kind.Property, "event-mode stdio, not polling"), 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, new("ProcessOutputMode", "Event", Kind.EnumValue,
"the mode that makes stdio push-based; polling would change the whole broker design"), "the mode that makes stdio push-based; polling would change the broker design"),
new("Signal", "", Kind.Type, "the enum Stop/Signal take; the RPC carries POSIX ints"), new("Process", "Start", Kind.Method,
new("Process", "OutputReceived", Kind.Event, "→ proc.stdout / proc.stderr notifications"), "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", "Exited", Kind.Event, "→ proc.exit; must never overtake output (OutboundWriter)"),
new("Process", "WriteStdin", Kind.Method, "proc.stdin (NDJSON to the agent)"), new("Process", "GetInputStream", Kind.Method,
new("Process", "CloseStdin", Kind.Method, "proc.closeStdin"), "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", "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"),
]; ];
} }
+4 -1
View File
@@ -20,7 +20,10 @@ namespace WslcApiDump;
/// </summary> /// </summary>
internal static class Program 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) private static int Main(string[] args)
{ {
+2 -2
View File
@@ -11,14 +11,14 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<AssemblyName>wslc-api-dump</AssemblyName> <AssemblyName>wslc-api-dump</AssemblyName>
<RootNamespace>WslcApiDump</RootNamespace> <RootNamespace>WslcApiDump</RootNamespace>
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework> <TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<!-- Same pin as the broker (docs/WINDOWS_PORT.md §15). If restore fails because the <!-- Same pin as the broker (docs/WINDOWS_PORT.md §15). If restore fails because the
version moved, change it HERE first, find out what the new API looks like, and only version moved, change it HERE first, find out what the new API looks like, and only
then bump the broker — that ordering is the whole point of this spike. --> then bump the broker — that ordering is the whole point of this spike. -->
<PackageReference Include="Microsoft.WSL.Containers" Version="[0.1.0-preview.1]" /> <PackageReference Include="Microsoft.WSL.Containers" Version="[2.9.3]" />
</ItemGroup> </ItemGroup>
</Project> </Project>