Nucleic: Windows Port 1

This commit is contained in:
2026-07-27 22:02:34 -07:00
commit 049ab9bc78
155 changed files with 3568 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
using System.Text.Json;
namespace NucleicBroker;
/// <summary>
/// The `ai.generate` seam (docs/WINDOWS_PORT.md §9.2): Phi Silica via Windows AI Foundry is
/// WinRT-only, so hostd routes on-device-AI requests through this broker rather than a
/// second helper process. Capability-probed: on non-Copilot+ hardware (or off-Windows) the
/// provider is unavailable and hostd's Intelligence seam falls back to its heuristics —
/// identical UX to a Mac without Apple Intelligence.
/// </summary>
public interface IAiProvider
{
bool IsAvailable { get; }
/// <summary>Generate text for `prompt`; when `schema` is present the result must be JSON
/// conforming to it (guided generation). Throws WslcError(AiUnavailable) when absent.</summary>
Task<string> GenerateAsync(string prompt, JsonElement? schema, CancellationToken ct);
}
/// <summary>Default provider: no on-device model. The real Foundry-backed provider lands
/// with work item 16 behind the same interface.</summary>
public sealed class UnavailableAiProvider : IAiProvider
{
public bool IsAvailable => false;
public Task<string> GenerateAsync(string prompt, JsonElement? schema, CancellationToken ct) =>
throw new WslcError(WslcError.AiUnavailable, "no on-device AI provider on this machine");
}
+254
View File
@@ -0,0 +1,254 @@
using System.Reflection;
using System.Text.Json;
namespace NucleicBroker;
/// <summary>
/// The broker's request dispatcher (docs/WINDOWS_PORT.md §3.3): one instance per process,
/// fed NDJSON lines from hostd, answering through an <see cref="OutboundWriter"/>. Requests
/// run concurrently (an image pull must not block a container.list), responses/notifications
/// serialize through the writer's single drain loop. Also the <see cref="IBrokerEvents"/>
/// sink the facade raises async events into.
/// </summary>
public sealed class BrokerService : IBrokerEvents
{
private readonly IWslc wslc;
private readonly IAiProvider ai;
private readonly OutboundWriter outbound;
private readonly Dictionary<long, IWslcProcess> procs = new();
private readonly Lock procsLock = new();
private long nextProcId;
public BrokerService(IWslc wslc, IAiProvider ai, OutboundWriter outbound)
{
this.wslc = wslc;
this.ai = ai;
this.outbound = outbound;
wslc.SetEvents(this);
}
/// <summary>Broker protocol revision, bumped with any wire change; hostd degrades from
/// the `hello` capabilities rather than parsing versions (docs/WINDOWS_PORT.md §2.3).</summary>
public const int ProtocolVersion = 1;
/// <summary>Handle one inbound NDJSON line. The returned task completes once the
/// response has been ENQUEUED (tests await it; the stdin loop fires and forgets).</summary>
public async Task HandleLineAsync(string line, CancellationToken ct = default)
{
JsonElement? id = null;
string method;
JsonElement? @params = null;
try
{
using var doc = JsonDocument.Parse(line);
var root = doc.RootElement;
// Clone: the id/params outlive this JsonDocument (responses are built after
// arbitrary awaits).
if (root.TryGetProperty("id", out var rawId)) id = rawId.Clone();
if (root.TryGetProperty("params", out var rawParams)) @params = rawParams.Clone();
method = root.TryGetProperty("method", out var m) && m.ValueKind == JsonValueKind.String
? m.GetString()!
: throw new JsonException("missing method");
}
catch (JsonException e)
{
outbound.EnqueueJson(Rpc.Error(id, Rpc.ParseError, $"unparseable request: {e.Message}"));
return;
}
try
{
var result = await DispatchAsync(method, @params, ct).ConfigureAwait(false);
if (id is { } requestId)
outbound.EnqueueJson(Rpc.Response(requestId, result));
}
catch (JsonException e)
{
outbound.EnqueueJson(Rpc.Error(id, Rpc.InvalidParams, e.Message));
}
catch (WslcError e)
{
outbound.EnqueueJson(Rpc.Error(id, Rpc.FacadeError, e.Message, new { kind = e.Kind }));
}
catch (NotSupportedException)
{
outbound.EnqueueJson(Rpc.Error(id, Rpc.MethodNotFound, $"unknown method: {method}"));
}
catch (Exception e)
{
outbound.EnqueueJson(Rpc.Error(id, Rpc.InternalError, e.Message));
}
}
private async Task<object> DispatchAsync(string method, JsonElement? p, CancellationToken ct)
{
switch (method)
{
case "hello":
{
var caps = new List<string> { "components", "session", "image", "container", "proc" };
if (ai.IsAvailable) caps.Add("ai");
return new
{
protocol = ProtocolVersion,
brokerVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "0.0.0",
wslcVersion = wslc.WslcVersion,
capabilities = caps,
};
}
case "components.missing":
return new { flags = await wslc.MissingComponentsAsync(ct).ConfigureAwait(false) };
case "components.install":
await wslc.InstallComponentsAsync(ct).ConfigureAwait(false);
return new { };
case "session.ensure":
{
var spec = Rpc.Params<SessionSpec>(p);
var gateway = await wslc.EnsureSessionAsync(spec, ct).ConfigureAwait(false);
return new { gateway };
}
case "session.terminate":
await wslc.TerminateSessionAsync(ct).ConfigureAwait(false);
return new { };
case "image.pull":
{
var args = Rpc.Params<ImagePullParams>(p);
await wslc.PullImageAsync(args.Ref, args.Auth, ct).ConfigureAwait(false);
return new { };
}
case "image.list":
return new { images = await wslc.ListImagesAsync(ct).ConfigureAwait(false) };
case "image.delete":
await wslc.DeleteImageAsync(Rpc.Params<ImageRefParams>(p).Ref, ct).ConfigureAwait(false);
return new { };
case "image.inspect":
{
var image = await wslc.InspectImageAsync(Rpc.Params<ImageRefParams>(p).Ref, ct)
.ConfigureAwait(false);
return new { image };
}
case "container.create":
await wslc.CreateContainerAsync(Rpc.Params<ContainerCreateSpec>(p), ct).ConfigureAwait(false);
return new { };
case "container.start":
await wslc.StartContainerAsync(Rpc.Params<ContainerNameParams>(p).Name, ct).ConfigureAwait(false);
return new { };
case "container.stop":
{
var args = Rpc.Params<ContainerStopParams>(p);
await wslc.StopContainerAsync(args.Name, args.Signal ?? 15, args.GraceMs ?? 5000, ct)
.ConfigureAwait(false);
return new { };
}
case "container.delete":
{
var args = Rpc.Params<ContainerDeleteParams>(p);
await wslc.DeleteContainerAsync(args.Name, args.Force ?? false, ct).ConfigureAwait(false);
return new { };
}
case "container.list":
return new { containers = await wslc.ListContainersAsync(ct).ConfigureAwait(false) };
case "container.state":
{
var state = await wslc.ContainerStateAsync(Rpc.Params<ContainerNameParams>(p).Name, ct)
.ConfigureAwait(false);
return new { state };
}
case "container.stats":
{
var stats = await wslc.ContainerStatsAsync(Rpc.Params<ContainerNameParams>(p).Name, ct)
.ConfigureAwait(false);
return new { stats };
}
case "proc.exec":
{
var spec = Rpc.Params<ProcSpec>(p);
var procId = Interlocked.Increment(ref nextProcId);
var proc = await wslc.ExecAsync(procId, spec, ct).ConfigureAwait(false);
lock (procsLock) procs[procId] = proc;
return new { procId };
}
case "proc.stdin":
{
var args = Rpc.Params<ProcStdinParams>(p);
await Proc(args.ProcId).WriteStdinAsync(Convert.FromBase64String(args.B64), ct)
.ConfigureAwait(false);
return new { };
}
case "proc.closeStdin":
await Proc(Rpc.Params<ProcIdParams>(p).ProcId).CloseStdinAsync(ct).ConfigureAwait(false);
return new { };
case "proc.signal":
{
var args = Rpc.Params<ProcSignalParams>(p);
await Proc(args.ProcId).SignalAsync(args.Sig, ct).ConfigureAwait(false);
return new { };
}
case "proc.resize":
{
var args = Rpc.Params<ProcResizeParams>(p);
await Proc(args.ProcId).ResizeAsync(args.Cols, args.Rows, ct).ConfigureAwait(false);
return new { };
}
case "ai.generate":
{
var args = Rpc.Params<AiGenerateParams>(p);
var text = await ai.GenerateAsync(args.Prompt, args.Schema, ct).ConfigureAwait(false);
return new { text };
}
default:
throw new NotSupportedException(method);
}
}
private IWslcProcess Proc(long procId)
{
lock (procsLock)
{
return procs.TryGetValue(procId, out var proc)
? proc
: throw new WslcError(WslcError.NotFound, $"unknown procId {procId}");
}
}
// MARK: IBrokerEvents — facade threads enqueue and return immediately.
public void ProcOutput(long procId, bool stderr, ReadOnlySpan<byte> chunk) =>
outbound.EnqueueProcOutput(procId, stderr, chunk);
public void ProcExited(long procId, int code)
{
lock (procsLock) procs.Remove(procId);
outbound.EnqueueJson(Rpc.Notification("proc.exit", new { procId, code }));
}
public void SessionDown(string reason) =>
outbound.EnqueueJson(Rpc.Notification("session.down", new { reason }));
public void PullProgress(string reference, string status, long current, long total) =>
outbound.EnqueueJson(Rpc.Notification(
"image.pullProgress", new { @ref = reference, status, current, total }));
public void InstallProgress(string status, double percent) =>
outbound.EnqueueJson(Rpc.Notification(
"components.installProgress", new { status, percent }));
// Param records for methods whose shapes aren't shared DTOs.
private sealed record ImagePullParams(string Ref, RegistryAuth? Auth);
private sealed record ImageRefParams(string Ref);
private sealed record ContainerNameParams(string Name);
private sealed record ContainerStopParams(string Name, int? Signal, int? GraceMs);
private sealed record ContainerDeleteParams(string Name, bool? Force);
private sealed record ProcIdParams(long ProcId);
private sealed record ProcStdinParams(long ProcId, string B64);
private sealed record ProcSignalParams(long ProcId, int Sig);
private sealed record ProcResizeParams(long ProcId, int Cols, int Rows);
private sealed record AiGenerateParams(string Prompt, JsonElement? Schema);
}
+115
View File
@@ -0,0 +1,115 @@
using System.Text.Json.Serialization;
namespace NucleicBroker;
/// <summary>
/// The one seam between broker logic and Microsoft.WSL.Containers (docs/WINDOWS_PORT.md
/// §3.3): everything the RPC surface needs from wslc, and nothing WinRT. The real facade
/// (Wslc/WslcFacade.cs, compiled with -p:UseWslc=true) adapts the preview API; unit tests
/// substitute FakeWslc; non-Windows builds get UnavailableWslc. Async events (process stdio,
/// pull progress, session death) flow out through <see cref="IBrokerEvents"/> so no wslc
/// event thread ever blocks on the stdio pipe.
/// </summary>
public interface IWslc
{
/// <summary>Reported in the `hello` capabilities exchange; null when wslc is absent.</summary>
string? WslcVersion { get; }
/// <summary>Install the sink BEFORE any operation that can emit events.</summary>
void SetEvents(IBrokerEvents events);
/// <summary>Missing OS components (WSL, the container service, …), empty when ready.</summary>
Task<IReadOnlyList<string>> MissingComponentsAsync(CancellationToken ct);
/// <summary>Install missing components; progress via <see cref="IBrokerEvents.InstallProgress"/>.</summary>
Task InstallComponentsAsync(CancellationToken ct);
/// <summary>Create-or-attach the per-channel wslc session; returns the WSL-facing host
/// gateway address guests reach the host on (docs/WINDOWS_PORT.md §5).</summary>
Task<string> EnsureSessionAsync(SessionSpec spec, CancellationToken ct);
Task TerminateSessionAsync(CancellationToken ct);
/// <summary>Pull an OCI image; progress via <see cref="IBrokerEvents.PullProgress"/>.</summary>
Task PullImageAsync(string reference, RegistryAuth? auth, CancellationToken ct);
Task<IReadOnlyList<ImageInfo>> ListImagesAsync(CancellationToken ct);
Task DeleteImageAsync(string reference, CancellationToken ct);
/// <summary>Digest/size for a local image, or null when not present.</summary>
Task<ImageInfo?> InspectImageAsync(string reference, CancellationToken ct);
Task CreateContainerAsync(ContainerCreateSpec spec, CancellationToken ct);
Task StartContainerAsync(string name, CancellationToken ct);
Task StopContainerAsync(string name, int signal, int graceMs, CancellationToken ct);
Task DeleteContainerAsync(string name, bool force, CancellationToken ct);
Task<IReadOnlyList<ContainerInfo>> ListContainersAsync(CancellationToken ct);
/// <summary>One of "running" | "stopped" | "absent" (kept coarse on purpose — the Swift
/// policy layer only distinguishes these three).</summary>
Task<string> ContainerStateAsync(string name, CancellationToken ct);
/// <summary>cgroup counters for the resource monitor; null when not running.</summary>
Task<ContainerStatsInfo?> ContainerStatsAsync(string name, CancellationToken ct);
/// <summary>Start a process in a running container. `procId` is minted by the broker and
/// keys every event this process emits through <see cref="IBrokerEvents"/>.</summary>
Task<IWslcProcess> ExecAsync(long procId, ProcSpec spec, CancellationToken ct);
}
/// <summary>Control half of a running in-container process (output arrives via events).</summary>
public interface IWslcProcess
{
Task WriteStdinAsync(ReadOnlyMemory<byte> data, CancellationToken ct);
Task CloseStdinAsync(CancellationToken ct);
Task SignalAsync(int signal, CancellationToken ct);
/// <summary>tty mode only (the Terminal panel); no-op for pipe-mode processes.</summary>
Task ResizeAsync(int cols, int rows, CancellationToken ct);
}
/// <summary>Event sink the broker hands to the facade; implementations must be
/// non-blocking (they enqueue onto the outbound writer).</summary>
public interface IBrokerEvents
{
void ProcOutput(long procId, bool stderr, ReadOnlySpan<byte> chunk);
void ProcExited(long procId, int code);
void SessionDown(string reason);
void PullProgress(string reference, string status, long current, long total);
void InstallProgress(string status, double percent);
}
/// <summary>A structured facade failure, surfaced to hostd as JSON-RPC error -32000 with
/// `data.kind` so the Swift side can branch (e.g. `brokerLost` vs `imagePullFailed`).</summary>
public sealed class WslcError(string kind, string message) : Exception(message)
{
public string Kind { get; } = kind;
public const string Unavailable = "wslc_unavailable";
public const string NotFound = "not_found";
public const string NotRunning = "not_running";
public const string PullFailed = "image_pull_failed";
public const string StartFailed = "start_failed";
public const string AiUnavailable = "ai_unavailable";
}
// DTOs — property names (after camel-casing) match the §3.3 wire keys exactly.
public sealed record SessionSpec(
string Name, string DataDir, int? Cpu, long? MemoryMB);
public sealed record RegistryAuth(string? Username, string? Password);
public sealed record ImageInfo(
[property: JsonPropertyName("ref")] string Ref, string? Digest, long? SizeBytes);
public sealed record VolumeSpec(
string Host, string Guest, [property: JsonPropertyName("ro")] bool ReadOnly);
public sealed record ContainerCreateSpec(
string Name, string Image, IReadOnlyList<VolumeSpec>? Volumes,
string? NetworkingMode, string? Hostname,
IReadOnlyDictionary<string, string>? Env, IReadOnlyList<string>? InitArgv);
public sealed record ContainerInfo(string Name, string Image, string State);
public sealed record ContainerStatsInfo(
long CpuUsageUsec, long MemoryUsedBytes, long MemoryLimitBytes, long? OomKills);
public sealed record ProcSpec(
string Container, IReadOnlyList<string> Argv,
IReadOnlyDictionary<string, string>? Env, string? Cwd,
int? Uid, int? Gid, bool Tty);
+33
View File
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- nucleic-brokerd (docs/WINDOWS_PORT.md §2.3, §3.3): the C# child process of
nucleic-hostd that owns every Microsoft.WSL.Containers (wslc) call, exposed to Swift
as NDJSON JSON-RPC 2.0 over stdio.
The wslc WinRT surface is isolated behind IWslc, and the REAL facade is compiled only
when -p:UseWslc=true (a Windows machine with the preview NuGet reachable): the API is
public preview with breaking changes expected, and the M1 spike validates it. Every
other build — Linux CI, the fake-backed unit tests — compiles the portable core plus
an "unavailable" facade, so broker logic and RPC surface are testable everywhere. -->
<PropertyGroup>
<OutputType>Exe</OutputType>
<AssemblyName>nucleic-brokerd</AssemblyName>
<RootNamespace>NucleicBroker</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition="'$(UseWslc)' == 'true'">
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
<DefineConstants>$(DefineConstants);USE_WSLC</DefineConstants>
</PropertyGroup>
<ItemGroup Condition="'$(UseWslc)' != 'true'">
<Compile Remove="Wslc/**/*.cs" />
</ItemGroup>
<ItemGroup Condition="'$(UseWslc)' == 'true'">
<!-- 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. -->
<PackageReference Include="Microsoft.WSL.Containers" Version="[0.1.0-preview.1]" />
</ItemGroup>
</Project>
+79
View File
@@ -0,0 +1,79 @@
using System.Threading.Channels;
namespace NucleicBroker;
/// <summary>
/// The single writer of the broker's stdout: responses and notifications from any thread are
/// enqueued (never blocking the caller — wslc raises stdio events on WinRT threads that must
/// not stall on the pipe, docs/WINDOWS_PORT.md §3.3) and drained by one loop that also
/// coalesces bursts: consecutive stdio chunks for the same (proc, stream) queued at drain
/// time merge into one `proc.stdout`/`proc.stderr` notification, cutting per-line overhead
/// for agents' high-rate NDJSON without adding any timer or latency (only already-queued
/// items merge). Ordering is preserved end-to-end — one channel, one drain loop — so
/// `proc.exit` can never overtake the output that preceded it.
/// </summary>
public sealed class OutboundWriter
{
private abstract record Item;
private sealed record JsonLine(string Line) : Item;
private sealed record ProcChunk(long ProcId, bool Stderr, byte[] Bytes) : Item;
private readonly Channel<Item> channel = Channel.CreateUnbounded<Item>(
new UnboundedChannelOptions { SingleReader = true });
private readonly TextWriter output;
private readonly Task loop;
public OutboundWriter(TextWriter output)
{
this.output = output;
loop = Task.Run(DrainAsync);
}
public void EnqueueJson(string line) => channel.Writer.TryWrite(new JsonLine(line));
public void EnqueueProcOutput(long procId, bool stderr, ReadOnlySpan<byte> chunk) =>
channel.Writer.TryWrite(new ProcChunk(procId, stderr, chunk.ToArray()));
/// <summary>Close the queue, drain everything, and flush. Idempotent.</summary>
public async Task CompleteAsync()
{
channel.Writer.TryComplete();
await loop.ConfigureAwait(false);
}
private async Task DrainAsync()
{
var reader = channel.Reader;
var batch = new List<Item>(64);
while (await reader.WaitToReadAsync().ConfigureAwait(false))
{
batch.Clear();
while (batch.Count < 256 && reader.TryRead(out var item)) batch.Add(item);
for (var i = 0; i < batch.Count; i++)
{
if (batch[i] is not ProcChunk head)
{
await output.WriteLineAsync(((JsonLine)batch[i]).Line).ConfigureAwait(false);
continue;
}
// Merge the run of chunks for the same (proc, stream) into one notification.
using var merged = new MemoryStream();
merged.Write(head.Bytes);
while (i + 1 < batch.Count
&& batch[i + 1] is ProcChunk next
&& next.ProcId == head.ProcId && next.Stderr == head.Stderr)
{
merged.Write(next.Bytes);
i++;
}
var line = Rpc.Notification(
head.Stderr ? "proc.stderr" : "proc.stdout",
new { procId = head.ProcId, b64 = Convert.ToBase64String(merged.ToArray()) });
await output.WriteLineAsync(line).ConfigureAwait(false);
}
await output.FlushAsync().ConfigureAwait(false);
}
await output.FlushAsync().ConfigureAwait(false);
}
}
+37
View File
@@ -0,0 +1,37 @@
using System.Text;
using NucleicBroker;
// nucleic-brokerd (docs/WINDOWS_PORT.md §2.3): child of nucleic-hostd, NDJSON JSON-RPC 2.0
// over stdio. Exits when stdin closes (hostd died or shut down); on Windows the Job Object
// hostd assigns kills us anyway — this is the graceful path.
//
// stdout carries ONLY protocol lines (the OutboundWriter is its single writer); every
// diagnostic goes to stderr, which hostd logs.
Console.OutputEncoding = Encoding.UTF8;
IWslc wslc =
#if USE_WSLC
new NucleicBroker.Wslc.WslcFacade();
#else
new UnavailableWslc();
#endif
IAiProvider ai = new UnavailableAiProvider();
var stdout = new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false)) { AutoFlush = false };
var outbound = new OutboundWriter(stdout);
var service = new BrokerService(wslc, ai, outbound);
using var stdin = new StreamReader(Console.OpenStandardInput(), Encoding.UTF8);
var inFlight = new List<Task>();
for (string? line; (line = await stdin.ReadLineAsync()) is not null;)
{
if (line.Length == 0) continue;
// Requests run concurrently (a long image.pull must not block container.list); the
// outbound writer serializes their responses. Track them so shutdown can drain.
inFlight.Add(service.HandleLineAsync(line));
inFlight.RemoveAll(t => t.IsCompleted);
}
await Task.WhenAll(inFlight).ConfigureAwait(false);
await outbound.CompleteAsync().ConfigureAwait(false);
+54
View File
@@ -0,0 +1,54 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace NucleicBroker;
/// <summary>
/// NDJSON JSON-RPC 2.0 plumbing (docs/WINDOWS_PORT.md §3.3): one JSON object per line,
/// requests flow hostd → broker only, notifications broker → hostd only. Binary payloads
/// ride base64 in `b64` fields.
/// </summary>
public static class Rpc
{
public static readonly JsonSerializerOptions Json = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
/// <summary>A JSON `null`, used as the response id for unidentifiable requests — the
/// spec requires `"id": null` to be PRESENT, which the ignore-null policy would drop if
/// we passed a bare null.</summary>
private static readonly JsonElement NullId = JsonDocument.Parse("null").RootElement;
// JSON-RPC 2.0 error codes (+ -32000 for structured facade errors, kind in data).
public const int ParseError = -32700;
public const int InvalidRequest = -32600;
public const int MethodNotFound = -32601;
public const int InvalidParams = -32602;
public const int InternalError = -32603;
public const int FacadeError = -32000;
public static string Response(JsonElement id, object result) =>
JsonSerializer.Serialize(new { jsonrpc = "2.0", id, result }, Json);
public static string Error(JsonElement? id, int code, string message, object? data = null) =>
JsonSerializer.Serialize(
new { jsonrpc = "2.0", id = id ?? NullId, error = new { code, message, data } },
Json);
public static string Notification(string method, object @params) =>
JsonSerializer.Serialize(new { jsonrpc = "2.0", method, @params }, Json);
/// <summary>Deserialize a request's params object; throws JsonException on a missing or
/// mismatched shape (the dispatcher maps that to InvalidParams). Positional records bind
/// through their constructor, case-insensitively.</summary>
public static T Params<T>(JsonElement? @params)
{
if (@params is not { ValueKind: JsonValueKind.Object } p)
throw new JsonException("missing params object");
return JsonSerializer.Deserialize<T>(p.GetRawText(), Json)
?? throw new JsonException("params decoded to null");
}
}
+48
View File
@@ -0,0 +1,48 @@
namespace NucleicBroker;
/// <summary>
/// The facade used when Microsoft.WSL.Containers isn't compiled in (any non-Windows build,
/// or a build without -p:UseWslc=true): every operation fails with a structured
/// `wslc_unavailable` error so hostd sees a clean capability gap, never a crash. The `hello`
/// exchange reports `wslcVersion: null`, which is the real capability signal.
/// </summary>
public sealed class UnavailableWslc : IWslc
{
public string? WslcVersion => null;
public void SetEvents(IBrokerEvents events) { }
private static WslcError Unavailable() =>
new(WslcError.Unavailable, "Microsoft.WSL.Containers is not available in this broker build");
public Task<IReadOnlyList<string>> MissingComponentsAsync(CancellationToken ct) =>
Task.FromException<IReadOnlyList<string>>(Unavailable());
public Task InstallComponentsAsync(CancellationToken ct) => Task.FromException(Unavailable());
public Task<string> EnsureSessionAsync(SessionSpec spec, CancellationToken ct) =>
Task.FromException<string>(Unavailable());
public Task TerminateSessionAsync(CancellationToken ct) => Task.FromException(Unavailable());
public Task PullImageAsync(string reference, RegistryAuth? auth, CancellationToken ct) =>
Task.FromException(Unavailable());
public Task<IReadOnlyList<ImageInfo>> ListImagesAsync(CancellationToken ct) =>
Task.FromException<IReadOnlyList<ImageInfo>>(Unavailable());
public Task DeleteImageAsync(string reference, CancellationToken ct) =>
Task.FromException(Unavailable());
public Task<ImageInfo?> InspectImageAsync(string reference, CancellationToken ct) =>
Task.FromException<ImageInfo?>(Unavailable());
public Task CreateContainerAsync(ContainerCreateSpec spec, CancellationToken ct) =>
Task.FromException(Unavailable());
public Task StartContainerAsync(string name, CancellationToken ct) =>
Task.FromException(Unavailable());
public Task StopContainerAsync(string name, int signal, int graceMs, CancellationToken ct) =>
Task.FromException(Unavailable());
public Task DeleteContainerAsync(string name, bool force, CancellationToken ct) =>
Task.FromException(Unavailable());
public Task<IReadOnlyList<ContainerInfo>> ListContainersAsync(CancellationToken ct) =>
Task.FromException<IReadOnlyList<ContainerInfo>>(Unavailable());
public Task<string> ContainerStateAsync(string name, CancellationToken ct) =>
Task.FromException<string>(Unavailable());
public Task<ContainerStatsInfo?> ContainerStatsAsync(string name, CancellationToken ct) =>
Task.FromException<ContainerStatsInfo?>(Unavailable());
public Task<IWslcProcess> ExecAsync(long procId, ProcSpec spec, CancellationToken ct) =>
Task.FromException<IWslcProcess>(Unavailable());
}
+232
View File
@@ -0,0 +1,232 @@
#if USE_WSLC
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:
// WslcService (components) → Session (VM host, images) → Container → Process.
public sealed class WslcFacade : IWslc
{
private IBrokerEvents? events;
private Session? session;
private readonly SemaphoreSlim sessionGate = new(1, 1);
public string? WslcVersion => WslcService.GetServiceVersion()?.ToString();
public void SetEvents(IBrokerEvents events) => this.events = events;
public Task<IReadOnlyList<string>> MissingComponentsAsync(CancellationToken ct)
{
var flags = WslcService.GetMissingComponents();
var missing = new List<string>();
foreach (var flag in Enum.GetValues<ComponentFlags>())
if (flag != ComponentFlags.None && flags.HasFlag(flag))
missing.Add(flag.ToString().ToLowerInvariant());
return Task.FromResult<IReadOnlyList<string>>(missing);
}
public async Task InstallComponentsAsync(CancellationToken ct)
{
// M1: wire the component-install progress callback into events.InstallProgress.
await WslcService.InstallComponentsAsync(WslcService.GetMissingComponents())
.AsTask(ct).ConfigureAwait(false);
events?.InstallProgress("done", 100);
}
public async Task<string> EnsureSessionAsync(SessionSpec spec, CancellationToken ct)
{
await sessionGate.WaitAsync(ct).ConfigureAwait(false);
try
{
if (session is null)
{
var settings = new SessionSettings(spec.Name, spec.DataDir);
if (spec.Cpu is { } cpu) settings.CpuCount = cpu;
if (spec.MemoryMB is { } mem) settings.MemoryMB = mem;
session = Session.CreateOrOpen(settings);
session.SessionTerminationHandler = reason =>
events?.SessionDown(reason?.ToString() ?? "unknown");
}
// M1: confirm how the WSL vEthernet gateway address is surfaced (session
// property vs. querying the vNIC); NAT mode is preferred for determinism
// (docs/WINDOWS_PORT.md §5). Mirrored mode reports the reachable host address.
return session.HostGatewayAddress?.ToString()
?? throw new WslcError(WslcError.StartFailed, "wslc session has no gateway address");
}
finally
{
sessionGate.Release();
}
}
public Task TerminateSessionAsync(CancellationToken ct)
{
session?.Terminate();
session = null;
return Task.CompletedTask;
}
private Session RequireSession() =>
session ?? throw new WslcError(WslcError.NotRunning, "no wslc session (call session.ensure first)");
public async Task PullImageAsync(string reference, RegistryAuth? auth, CancellationToken ct)
{
var options = new PullImageOptions(reference);
if (auth is { Username: { } user, Password: { } pass })
options.Credentials = new RegistryCredentials(user, pass);
options.Progress += (status, current, total) =>
events?.PullProgress(reference, status, current, total);
try
{
await RequireSession().PullImageAsync(options).AsTask(ct).ConfigureAwait(false);
}
catch (Exception e) when (e is not WslcError)
{
throw new WslcError(WslcError.PullFailed, e.Message);
}
}
public Task<IReadOnlyList<ImageInfo>> ListImagesAsync(CancellationToken ct) =>
Task.FromResult<IReadOnlyList<ImageInfo>>(
RequireSession().GetImages()
.Select(i => new ImageInfo(i.Reference, i.Digest, i.Size))
.ToList());
public Task DeleteImageAsync(string reference, CancellationToken ct)
{
RequireSession().DeleteImage(reference);
return Task.CompletedTask;
}
public Task<ImageInfo?> InspectImageAsync(string reference, CancellationToken ct)
{
var image = RequireSession().GetImages().FirstOrDefault(i => i.Reference == reference);
return Task.FromResult(image is null ? null : new ImageInfo(image.Reference, image.Digest, image.Size));
}
public Task CreateContainerAsync(ContainerCreateSpec spec, CancellationToken ct)
{
var settings = new ContainerSettings(spec.Image) { Name = spec.Name };
if (spec.Hostname is { } hostname) settings.Hostname = hostname;
if (spec.NetworkingMode is { } mode)
settings.NetworkingMode = Enum.Parse<ContainerNetworkingMode>(mode, ignoreCase: true);
foreach (var volume in spec.Volumes ?? [])
settings.Volumes.Add(new ContainerVolume(volume.Host, volume.Guest, volume.ReadOnly));
if (spec.InitArgv is { Count: > 0 } argv)
settings.InitProcess = new ProcessSettings { CmdLine = argv.ToList() };
if (spec.Env is { } env)
foreach (var (key, value) in env)
settings.InitProcess?.Environment.Add(key, value);
try
{
RequireSession().CreateContainer(settings);
}
catch (Exception e) when (e is not WslcError)
{
throw new WslcError(WslcError.StartFailed, e.Message);
}
return Task.CompletedTask;
}
private Container RequireContainer(string name) =>
RequireSession().GetContainers().FirstOrDefault(c => c.Name == name)
?? throw new WslcError(WslcError.NotFound, $"no container named {name}");
public Task StartContainerAsync(string name, CancellationToken ct)
{
RequireContainer(name).Start();
return Task.CompletedTask;
}
public Task StopContainerAsync(string name, int signal, int graceMs, CancellationToken ct)
{
RequireContainer(name).Stop((Signal)signal, TimeSpan.FromMilliseconds(graceMs));
return Task.CompletedTask;
}
public Task DeleteContainerAsync(string name, bool force, CancellationToken ct)
{
RequireContainer(name).Delete(force ? DeleteContainerFlags.Force : DeleteContainerFlags.None);
return Task.CompletedTask;
}
public Task<IReadOnlyList<ContainerInfo>> ListContainersAsync(CancellationToken ct) =>
Task.FromResult<IReadOnlyList<ContainerInfo>>(
RequireSession().GetContainers()
.Select(c => new ContainerInfo(c.Name, c.Image, c.State.ToString().ToLowerInvariant()))
.ToList());
public Task<string> ContainerStateAsync(string name, CancellationToken ct)
{
var container = RequireSession().GetContainers().FirstOrDefault(c => c.Name == name);
return Task.FromResult(container?.State switch
{
null => "absent",
ContainerState.Running => "running",
_ => "stopped",
});
}
public Task<ContainerStatsInfo?> ContainerStatsAsync(string name, CancellationToken ct)
{
// M1: confirm the stats surface (cgroup v2 counters are what the Swift resource
// monitor folds into ContainerResourceSample).
var stats = RequireContainer(name).GetStatistics();
return Task.FromResult<ContainerStatsInfo?>(stats is null
? null
: new ContainerStatsInfo(stats.CpuUsageUsec, stats.MemoryUsedBytes, stats.MemoryLimitBytes, stats.OomKills));
}
public Task<IWslcProcess> ExecAsync(long procId, ProcSpec spec, CancellationToken ct)
{
var container = RequireContainer(spec.Container);
var settings = new ProcessSettings
{
CmdLine = spec.Argv.ToList(),
WorkingDirectory = spec.Cwd,
OutputMode = ProcessOutputMode.Event,
Terminal = spec.Tty,
};
if (spec.Env is { } env)
foreach (var (key, value) in env) settings.Environment.Add(key, value);
if (spec.Uid is { } uid) settings.UserId = uid; // M1: verify uid semantics (§3.2);
if (spec.Gid is { } gid) settings.GroupId = gid; // fall back to a setpriv wrapper argv.
var process = container.RunProcess(settings);
process.OutputReceived += (stderr, data) => events?.ProcOutput(procId, stderr, data);
process.Exited += code => events?.ProcExited(procId, code);
return Task.FromResult<IWslcProcess>(new WslcProcess(process));
}
private sealed class WslcProcess(Process process) : IWslcProcess
{
public Task WriteStdinAsync(ReadOnlyMemory<byte> data, CancellationToken ct)
{
process.WriteStdin(data.Span);
return Task.CompletedTask;
}
public Task CloseStdinAsync(CancellationToken ct)
{
process.CloseStdin();
return Task.CompletedTask;
}
public Task SignalAsync(int signal, CancellationToken ct)
{
process.Signal((Signal)signal);
return Task.CompletedTask;
}
public Task ResizeAsync(int cols, int rows, CancellationToken ct)
{
process.ResizeTerminal(cols, rows);
return Task.CompletedTask;
}
}
}
#endif
Binary file not shown.
@@ -0,0 +1,23 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v9.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v9.0": {
"nucleic-brokerd/1.0.0": {
"runtime": {
"nucleic-brokerd.dll": {}
}
}
}
},
"libraries": {
"nucleic-brokerd/1.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
}
}
}
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,14 @@
{
"runtimeOptions": {
"tfm": "net9.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "9.0.0"
},
"configProperties": {
"System.Globalization.Invariant": true,
"System.Globalization.PredefinedCulturesOnly": true,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}
@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0")]
@@ -0,0 +1,22 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("nucleic-brokerd")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+20c2c31cf39c17bf53c32ecdaf353dd361151b0c")]
[assembly: System.Reflection.AssemblyProductAttribute("nucleic-brokerd")]
[assembly: System.Reflection.AssemblyTitleAttribute("nucleic-brokerd")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
// Generated by the MSBuild WriteCodeFragment class.
@@ -0,0 +1 @@
b47bbe50f1e268ae59fc2e387aaadfe56feaf26d8852247f940ca82af44828dc
@@ -0,0 +1,15 @@
is_global = true
build_property.TargetFramework = net9.0
build_property.TargetPlatformMinVersion =
build_property.UsingMicrosoftNETSdkWeb =
build_property.ProjectTypeGuids =
build_property.InvariantGlobalization = true
build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = NucleicBroker
build_property.ProjectDir = /Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.EffectiveAnalysisLevelStyle = 9.0
build_property.EnableCodeStyleSeverity =
@@ -0,0 +1,8 @@
// <auto-generated/>
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;
@@ -0,0 +1 @@
b7fd0fc35ef857f78f5ddebcbbb4bc594b8574b1cd56521a4ecbb615c5be3243
@@ -0,0 +1,15 @@
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/bin/Debug/net9.0/nucleic-brokerd
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/bin/Debug/net9.0/nucleic-brokerd.deps.json
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/bin/Debug/net9.0/nucleic-brokerd.runtimeconfig.json
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/bin/Debug/net9.0/nucleic-brokerd.dll
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/bin/Debug/net9.0/nucleic-brokerd.pdb
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/obj/Debug/net9.0/NucleicBroker.GeneratedMSBuildEditorConfig.editorconfig
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/obj/Debug/net9.0/NucleicBroker.AssemblyInfoInputs.cache
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/obj/Debug/net9.0/NucleicBroker.AssemblyInfo.cs
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/obj/Debug/net9.0/NucleicBroker.csproj.CoreCompileInputs.cache
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/obj/Debug/net9.0/NucleicBroker.sourcelink.json
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/obj/Debug/net9.0/nucleic-brokerd.dll
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/obj/Debug/net9.0/refint/nucleic-brokerd.dll
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/obj/Debug/net9.0/nucleic-brokerd.pdb
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/obj/Debug/net9.0/NucleicBroker.genruntimeconfig.cache
/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/obj/Debug/net9.0/ref/nucleic-brokerd.dll
@@ -0,0 +1 @@
de89b2563b4e65c8127d700361a8ea13c111ac14ac03a902ffc90bddcf2ba77c
@@ -0,0 +1 @@
{"documents":{"/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/*":"https://raw.githubusercontent.com/abkslm/nucleic/20c2c31cf39c17bf53c32ecdaf353dd361151b0c/*"}}
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,67 @@
{
"format": 1,
"restore": {
"/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/NucleicBroker.csproj": {}
},
"projects": {
"/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/NucleicBroker.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/NucleicBroker.csproj",
"projectName": "nucleic-brokerd",
"projectPath": "/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/NucleicBroker.csproj",
"packagesPath": "/Users/blakeslee/Library/Application Support/Nucleic/sessions/704B73C5-6D1C-460E-869C-21B3914B2CE4/claude-home/.nuget/packages/",
"outputPath": "/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/obj/",
"projectStyle": "PackageReference",
"configFilePaths": [
"/Users/blakeslee/Library/Application Support/Nucleic/sessions/704B73C5-6D1C-460E-869C-21B3914B2CE4/claude-home/.nuget/NuGet/NuGet.Config"
],
"originalTargetFrameworks": [
"net9.0"
],
"sources": {
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net9.0": {
"targetAlias": "net9.0",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "9.0.300"
},
"frameworks": {
"net9.0": {
"targetAlias": "net9.0",
"imports": [
"net461",
"net462",
"net47",
"net471",
"net472",
"net48",
"net481"
],
"assetTargetFallback": true,
"warn": true,
"frameworkReferences": {
"Microsoft.NETCore.App": {
"privateAssets": "all"
}
},
"runtimeIdentifierGraphPath": "/Users/blakeslee/Library/Application Support/Nucleic/sessions/704B73C5-6D1C-460E-869C-21B3914B2CE4/claude-home/.dotnet/sdk/9.0.316/PortableRuntimeIdentifierGraph.json"
}
}
}
}
}
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/Users/blakeslee/Library/Application Support/Nucleic/sessions/704B73C5-6D1C-460E-869C-21B3914B2CE4/claude-home/.nuget/packages/</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/Users/blakeslee/Library/Application Support/Nucleic/sessions/704B73C5-6D1C-460E-869C-21B3914B2CE4/claude-home/.nuget/packages/</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.3</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="/Users/blakeslee/Library/Application Support/Nucleic/sessions/704B73C5-6D1C-460E-869C-21B3914B2CE4/claude-home/.nuget/packages/" />
</ItemGroup>
</Project>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
+72
View File
@@ -0,0 +1,72 @@
{
"version": 3,
"targets": {
"net9.0": {}
},
"libraries": {},
"projectFileDependencyGroups": {
"net9.0": []
},
"packageFolders": {
"/Users/blakeslee/Library/Application Support/Nucleic/sessions/704B73C5-6D1C-460E-869C-21B3914B2CE4/claude-home/.nuget/packages/": {}
},
"project": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/NucleicBroker.csproj",
"projectName": "nucleic-brokerd",
"projectPath": "/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/NucleicBroker.csproj",
"packagesPath": "/Users/blakeslee/Library/Application Support/Nucleic/sessions/704B73C5-6D1C-460E-869C-21B3914B2CE4/claude-home/.nuget/packages/",
"outputPath": "/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/obj/",
"projectStyle": "PackageReference",
"configFilePaths": [
"/Users/blakeslee/Library/Application Support/Nucleic/sessions/704B73C5-6D1C-460E-869C-21B3914B2CE4/claude-home/.nuget/NuGet/NuGet.Config"
],
"originalTargetFrameworks": [
"net9.0"
],
"sources": {
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net9.0": {
"targetAlias": "net9.0",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "9.0.300"
},
"frameworks": {
"net9.0": {
"targetAlias": "net9.0",
"imports": [
"net461",
"net462",
"net47",
"net471",
"net472",
"net48",
"net481"
],
"assetTargetFallback": true,
"warn": true,
"frameworkReferences": {
"Microsoft.NETCore.App": {
"privateAssets": "all"
}
},
"runtimeIdentifierGraphPath": "/Users/blakeslee/Library/Application Support/Nucleic/sessions/704B73C5-6D1C-460E-869C-21B3914B2CE4/claude-home/.dotnet/sdk/9.0.316/PortableRuntimeIdentifierGraph.json"
}
}
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"version": 2,
"dgSpecHash": "/9BJzGlDuxs=",
"success": true,
"projectFilePath": "/Users/blakeslee/.nucleic/control/nucleic/.nucleic/worktrees/keen-river-gecko-yexx/windows/NucleicBroker/NucleicBroker.csproj",
"expectedPackageFiles": [],
"logs": []
}