Merge nucleic/nimble-umber-toad-20h7 into dev

This commit is contained in:
2026-08-11 20:11:26 -07:00
parent 50e2b9030b
commit 05d993d83b
5 changed files with 24 additions and 24 deletions
+2 -2
View File
@@ -194,10 +194,10 @@ public sealed class BrokerServiceTests
public async Task ImagePull_EmitsProgressNotificationsBeforeResult() public async Task ImagePull_EmitsProgressNotificationsBeforeResult()
{ {
var lines = await RoundTrip( var lines = await RoundTrip(
"""{"jsonrpc":"2.0","id":3,"method":"image.pull","params":{"ref":"ghcr.io/abkslm/naros-agent:26.07"}}"""); """{"jsonrpc":"2.0","id":3,"method":"image.pull","params":{"ref":"ghcr.io/abkslm/hydrangeaos-agent:26.07"}}""");
Assert.Equal(3, lines.Count); Assert.Equal(3, lines.Count);
Assert.Equal("image.pullProgress", lines[0].GetProperty("method").GetString()); Assert.Equal("image.pullProgress", lines[0].GetProperty("method").GetString());
Assert.Equal("ghcr.io/abkslm/naros-agent:26.07", Assert.Equal("ghcr.io/abkslm/hydrangeaos-agent:26.07",
lines[0].GetProperty("params").GetProperty("ref").GetString()); lines[0].GetProperty("params").GetProperty("ref").GetString());
Assert.Equal("done", lines[1].GetProperty("params").GetProperty("status").GetString()); Assert.Equal("done", lines[1].GetProperty("params").GetProperty("status").GetString());
Result(lines[2]); // the pull's own success response arrives after its progress Result(lines[2]); // the pull's own success response arrives after its progress
+4 -4
View File
@@ -340,7 +340,7 @@ public sealed class WslcFacade : IWslc
} }
} }
/// <summary>`ghcr.io/abkslm/naros-agent:26.07` → `https://ghcr.io`. A ref whose first segment /// <summary>`ghcr.io/abkslm/hydrangeaos-agent:26.07` → `https://ghcr.io`. A ref whose first segment
/// carries no dot or port has no registry host at all (`ubuntu:24.04`), which means Docker /// carries no dot or port has no registry host at all (`ubuntu:24.04`), which means Docker
/// Hub.</summary> /// Hub.</summary>
private static Uri RegistryUri(string reference) private static Uri RegistryUri(string reference)
@@ -503,7 +503,7 @@ public sealed class WslcFacade : IWslc
/// the guest's own cgroup v2 files, read the way a Linux-native engine would (§13.1 finding 2). /// the guest's own cgroup v2 files, read the way a Linux-native engine would (§13.1 finding 2).
/// ///
/// Deliberately `cat` and `echo` only: no awk, no sed. This runs in whatever image the user /// Deliberately `cat` and `echo` only: no awk, no sed. This runs in whatever image the user
/// configured, and nash is `/bin/sh` in narOS — the smaller the tool surface, the fewer images /// configured, and hydrashell is `/bin/sh` in hydrangeaOS — the smaller the tool surface, the fewer images
/// this silently fails in. /// this silently fails in.
/// </summary> /// </summary>
public async Task<ContainerStatsInfo?> ContainerStatsAsync(string name, CancellationToken ct) public async Task<ContainerStatsInfo?> ContainerStatsAsync(string name, CancellationToken ct)
@@ -596,7 +596,7 @@ public sealed class WslcFacade : IWslc
/// <summary> /// <summary>
/// <c>ProcessSettings</c> has no <c>UserId</c>/<c>GroupId</c>, so dropping to the agent uid is /// <c>ProcessSettings</c> has no <c>UserId</c>/<c>GroupId</c>, so dropping to the agent uid is
/// done in-guest by wrapping argv — the fallback §3.2 always named, which costs nothing /// done in-guest by wrapping argv — the fallback §3.2 always named, which costs nothing
/// because the interceptors and nash never look at the numeric uid. /// because the interceptors and hydrashell never look at the numeric uid.
/// ///
/// argv is passed to `setpriv` directly rather than through a shell, so nothing here can be /// argv is passed to `setpriv` directly rather than through a shell, so nothing here can be
/// quoted wrong or injected into. /// quoted wrong or injected into.
@@ -615,7 +615,7 @@ public sealed class WslcFacade : IWslc
WslcError.Unsupported, WslcError.Unsupported,
$"cannot drop to uid {uid} in this image: it has no util-linux `setpriv` " $"cannot drop to uid {uid} in this image: it has no util-linux `setpriv` "
+ "(BusyBox ships a `setpriv` that does not support --reuid). Use an image with " + "(BusyBox ships a `setpriv` that does not support --reuid). Use an image with "
+ "util-linux, as the narOS agent image does — refusing to run as root instead."); + "util-linux, as the hydrangeaOS agent image does — refusing to run as root instead.");
return return
[ [
+3 -3
View File
@@ -168,7 +168,7 @@ for, and nothing above it should move. These three are different:
| `Container` has no `Name`, `Session` has no `GetContainers()` | Sharper than "no enumeration": a container is reachable ONLY through the handle `CreateContainer` returned, so the broker keeps its own name→handle roster — which dies with the process. A restarted broker sees an empty sandbox (§13.2). | | `Container` has no `Name`, `Session` has no `GetContainers()` | Sharper than "no enumeration": a container is reachable ONLY through the handle `CreateContainer` returned, so the broker keeps its own name→handle roster — which dies with the process. A restarted broker sees an empty sandbox (§13.2). |
| No create-or-attach on `Session` | `IWSLCSessionManager::OpenSessionByName` / `EnterSession` — same gate as above. Until then `session.ensure` fails `session_exists` and names `wsl --shutdown` as the remedy. | | No create-or-attach on `Session` | `IWSLCSessionManager::OpenSessionByName` / `EnterSession` — same gate as above. Until then `session.ensure` fails `session_exists` and names `wsl --shutdown` as the remedy. |
| No gateway address anywhere on the API | **Gateway TCP stays primary.** The hvsocket alternative needed a VMID from `IWSLCVirtualMachine::GetId`, and that interface is unreachable from a client — retracted in §13.1. The address comes from `GetAdaptersAddresses` over `vEthernet (WSL)`. | | No gateway address anywhere on the API | **Gateway TCP stays primary.** The hvsocket alternative needed a VMID from `IWSLCVirtualMachine::GetId`, and that interface is unreachable from a client — retracted in §13.1. The address comes from `GetAdaptersAddresses` over `vEthernet (WSL)`. |
| No uid on `ProcessSettings` | Recoverable, and already planned for: exec wraps argv in `setpriv`/`su agent -c`. Interceptors and nash don't care about the numeric uid (§3.2). | | No uid on `ProcessSettings` | Recoverable, and already planned for: exec wraps argv in `setpriv`/`su agent -c`. Interceptors and hydrashell don't care about the numeric uid (§3.2). |
--- ---
@@ -190,7 +190,7 @@ dotnet build windows\NucleicBroker\NucleicBroker.csproj -p:UseWslc=true # the
dotnet run --project windows\spikes\WslcSpike -- --repo C:\src\nucleic dotnet run --project windows\spikes\WslcSpike -- --repo C:\src\nucleic
``` ```
Options: `--image` (default `ghcr.io/abkslm/naros-agent:26.07`), `--container`, `--session-name`, Options: `--image` (default `ghcr.io/abkslm/hydrangeaos-agent:26.07`), `--container`, `--session-name`,
`--iterations` (timing runs, default 5), `--broker <path>` if autodiscovery fails, `--iterations` (timing runs, default 5), `--broker <path>` if autodiscovery fails,
`--no-recovery` to skip the crash test. `--no-recovery` to skip the crash test.
@@ -213,7 +213,7 @@ It answers four open questions:
4. **Can the guest reach the host at the gateway? (M1 b)** The last thing M2 waits on. It binds a 4. **Can the guest reach the host at the gateway? (M1 b)** The last thing M2 waits on. It binds a
listener on the WSL-facing address only — never `0.0.0.0`, which is the posture §5 requires — listener on the WSL-facing address only — never `0.0.0.0`, which is the posture §5 requires —
and has the container open a TCP round trip to it. Every agent session rides this: approvals, and has the container open a TCP round trip to it. Every agent session rides this: approvals,
the git/gh interceptors, nash's shell reports. On failure it prints the `New-NetFirewallRule` the git/gh interceptors, hydrashell's shell reports. On failure it prints the `New-NetFirewallRule`
remediation §5 step 5 calls for, and notes that the AF_HYPERV fallback needs a VM GUID that remediation §5 step 5 calls for, and notes that the AF_HYPERV fallback needs a VM GUID that
§13.2 found no client route to — so gateway TCP is not merely preferred, it is the only path §13.2 found no client route to — so gateway TCP is not merely preferred, it is the only path
currently available. currently available.
+2 -2
View File
@@ -61,7 +61,7 @@ internal static class FacadeAssumptions
// ---- Images ---- // ---- Images ----
new("Session", "PullImageAsync", Kind.Method, new("Session", "PullImageAsync", Kind.Method,
"image.pull RPC (naros-agent from GHCR). The async form specifically: the sync " "image.pull RPC (hydrangeaos-agent from GHCR). The async form specifically: the sync "
+ "PullImage reports no progress at all"), + "PullImage reports no progress at all"),
new("PullImageOptions", ".ctor", Kind.Constructor, "PullImageOptions(uri)"), new("PullImageOptions", ".ctor", Kind.Constructor, "PullImageOptions(uri)"),
new("PullImageOptions", "RegistryAuth", Kind.Property, new("PullImageOptions", "RegistryAuth", Kind.Property,
@@ -88,7 +88,7 @@ internal static class FacadeAssumptions
new("ContainerSettings", "NetworkingMode", Kind.Property, new("ContainerSettings", "NetworkingMode", Kind.Property,
"None | Bridged — NOT the NAT/mirrored pair §5 assumed"), "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, "hydrangeaos-init as PID 1 (docs/HYDRANGEAOS.md)"),
new("ContainerVolume", ".ctor", Kind.Constructor, new("ContainerVolume", ".ctor", Kind.Constructor,
"ContainerVolume(windowsPath, containerPath, readOnly)"), "ContainerVolume(windowsPath, containerPath, readOnly)"),
new("ContainerNetworkingMode", "Bridged", Kind.EnumValue, new("ContainerNetworkingMode", "Bridged", Kind.EnumValue,
+13 -13
View File
@@ -30,13 +30,13 @@ internal static class Program
private static async Task<int> Main(string[] args) private static async Task<int> Main(string[] args)
{ {
var repo = Arg(args, "--repo") ?? Directory.GetCurrentDirectory(); var repo = Arg(args, "--repo") ?? Directory.GetCurrentDirectory();
var image = Arg(args, "--image") ?? "ghcr.io/abkslm/naros-agent:26.07"; var image = Arg(args, "--image") ?? "ghcr.io/abkslm/hydrangeaos-agent:26.07";
var container = Arg(args, "--container") ?? "nucleic-spike-c1"; var container = Arg(args, "--container") ?? "nucleic-spike-c1";
var sessionName = Arg(args, "--session-name") ?? "nucleic-spike"; var sessionName = Arg(args, "--session-name") ?? "nucleic-spike";
var iterations = int.TryParse(Arg(args, "--iterations"), out var n) ? n : 5; var iterations = int.TryParse(Arg(args, "--iterations"), out var n) ? n : 5;
var broker = Arg(args, "--broker") ?? FindBroker(); var broker = Arg(args, "--broker") ?? FindBroker();
var skipRecovery = args.Contains("--no-recovery"); var skipRecovery = args.Contains("--no-recovery");
// narOS runs `naros-init` as PID 1 and stays up (docs/NAROS.md). A stock image usually // hydrangeaOS runs `hydrangeaos-init` as PID 1 and stays up (docs/HYDRANGEAOS.md). A stock image usually
// does not: alpine's PID 1 is /bin/sh, which exits immediately with no tty, so the // does not: alpine's PID 1 is /bin/sh, which exits immediately with no tty, so the
// container is `Exited` before the first exec and every later step fails `not_running`. // container is `Exited` before the first exec and every later step fails `not_running`.
var sleepInit = args.Contains("--sleep-init"); var sleepInit = args.Contains("--sleep-init");
@@ -146,14 +146,14 @@ internal static class Program
["hostname"] = "nucleic-spike", ["hostname"] = "nucleic-spike",
["env"] = new Dictionary<string, string> { ["NUCLEIC_SPIKE"] = "1" }, ["env"] = new Dictionary<string, string> { ["NUCLEIC_SPIKE"] = "1" },
}; };
// Mirror WslcContainerEngine: narOS images carry no default CMD (wslc answers "no command // Mirror WslcContainerEngine: hydrangeaOS images carry no default CMD (wslc answers "no command
// specified"), so the engine always names init explicitly — `naros-init` is PID 1 per // specified"), so the engine always names init explicitly — `hydrangeaos-init` is PID 1 per
// docs/NAROS.md §5, doing zombie reaping, signal forwarding and, with NAROS_BRIDGE=1, // docs/HYDRANGEAOS.md §5, doing zombie reaping, signal forwarding and, with HYDRANGEAOS_BRIDGE=1,
// supervising control-bridge.js. `--sleep-init` swaps in a keepalive for stock images that // supervising control-bridge.js. `--sleep-init` swaps in a keepalive for stock images that
// have no naros-init, which is the case the engine handles with a probe (see §13.3). // have no hydrangeaos-init, which is the case the engine handles with a probe (see §13.3).
create["initArgv"] = sleepInit create["initArgv"] = sleepInit
? new[] { "/bin/sh", "-c", "sleep 3600" } ? new[] { "/bin/sh", "-c", "sleep 3600" }
: new[] { "/usr/sbin/naros-init" }; : new[] { "/usr/sbin/hydrangeaos-init" };
await broker.CallAsync("container.create", create); await broker.CallAsync("container.create", create);
await broker.CallAsync("container.start", new { name = containerName }); await broker.CallAsync("container.start", new { name = containerName });
var state = (await broker.CallAsync("container.state", new { name = containerName })) var state = (await broker.CallAsync("container.state", new { name = containerName }))
@@ -164,8 +164,8 @@ internal static class Program
// (supply a long-running init) is nothing like the remedy for a real start failure. // (supply a long-running init) is nothing like the remedy for a real start failure.
if (state != "running") if (state != "running")
throw new InvalidOperationException( throw new InvalidOperationException(
$"container is '{state}' immediately after start — its PID 1 exited. narOS runs " $"container is '{state}' immediately after start — its PID 1 exited. hydrangeaOS runs "
+ "naros-init and stays up; a stock image (alpine's PID 1 is /bin/sh) does not. " + "hydrangeaos-init and stays up; a stock image (alpine's PID 1 is /bin/sh) does not. "
+ "Re-run with --sleep-init to give it a long-running init process."); + "Re-run with --sleep-init to give it a long-running init process.");
Step("exec: stdio round-trip"); Step("exec: stdio round-trip");
@@ -188,7 +188,7 @@ internal static class Program
catch (BrokerError e) when (e.Kind == "unsupported") catch (BrokerError e) when (e.Kind == "unsupported")
{ {
Console.WriteLine($" REFUSED (correctly): {e.Message}"); Console.WriteLine($" REFUSED (correctly): {e.Message}");
Console.WriteLine(" → expected on a BusyBox image; narOS ships util-linux."); Console.WriteLine(" → expected on a BusyBox image; hydrangeaOS ships util-linux.");
} }
Step("the mounted worktree"); Step("the mounted worktree");
@@ -218,7 +218,7 @@ internal static class Program
/// ///
/// Every agent session depends on this. `control-bridge.js` forwards guest loopback 9099 to /// Every agent session depends on this. `control-bridge.js` forwards guest loopback 9099 to
/// `NUCLEIC_CONTROL_HOST/PORT`, which is where `MCPApprovalServer` serves approvals, the git /// `NUCLEIC_CONTROL_HOST/PORT`, which is where `MCPApprovalServer` serves approvals, the git
/// and gh interceptor endpoints, and the nash shell reports (§5, §1.4). If the guest cannot /// and gh interceptor endpoints, and the hydrashell shell reports (§5, §1.4). If the guest cannot
/// open a TCP connection to the host on that address, none of it works and no agent can run. /// open a TCP connection to the host on that address, none of it works and no agent can run.
/// ///
/// This is the reachability question in isolation: a bare TCP round trip, bound **only** to /// This is the reachability question in isolation: a bare TCP round trip, bound **only** to
@@ -270,7 +270,7 @@ internal static class Program
try try
{ {
// narOS has neither `nc` nor `wget` — it has **node**, since control-bridge.js is the // hydrangeaOS has neither `nc` nor `wget` — it has **node**, since control-bridge.js is the
// real client on this path. Passed as argv with no shell, so nothing here needs // real client on this path. Passed as argv with no shell, so nothing here needs
// quoting, and the payload is a bare "PING" because the host side replies to whatever // quoting, and the payload is a bare "PING" because the host side replies to whatever
// it reads (no CRLF handling to get wrong). // it reads (no CRLF handling to get wrong).
@@ -287,7 +287,7 @@ internal static class Program
// A missing client tool is NOT a blocked connection, and reporting it as one sends the // A missing client tool is NOT a blocked connection, and reporting it as one sends the
// reader off to inspect firewall rules for no reason. An earlier version of this probe // reader off to inspect firewall rules for no reason. An earlier version of this probe
// did exactly that on narOS (§13.3). // did exactly that on hydrangeaOS (§13.3).
if (code == 127 || output.Contains("command not found") || output.Contains("not found")) if (code == 127 || output.Contains("command not found") || output.Contains("not found"))
{ {
Console.WriteLine($" INCONCLUSIVE — no usable client in this image (exit {code}): " Console.WriteLine($" INCONCLUSIVE — no usable client in this image (exit {code}): "