Nucleic: Windows Port 1
This commit is contained in:
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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
|
||||
BIN
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
|
||||
+15
@@ -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;
|
||||
Binary file not shown.
@@ -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/*"}}
|
||||
Executable
BIN
Binary file not shown.
Binary file not shown.
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" />
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": []
|
||||
}
|
||||
Reference in New Issue
Block a user