child/EngineContainer.cs
|
#nullable enable using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace DeskPilot.Child { /// <summary>Owns the separate credentialless, network-disabled Engine container.</summary> public sealed class EngineContainer : IDisposable { private readonly ToolContainer _owner; private readonly string _name; private readonly string _image; private readonly object _sync = new object(); private readonly CancellationTokenSource _cancel = new CancellationTokenSource(); private ControlProcess? _attach; private Task<byte[]>? _errors; private HostBridge? _bridge; private string _containerId = string.Empty; private volatile bool _starting = true; private volatile bool _stopped; /// <summary>Records ownership, creates and inspects the immutable image, then starts its IPC.</summary> public EngineContainer(ToolContainer owner, string image) { _owner = owner ?? throw new ArgumentNullException(nameof(owner)); if (owner.HostProcess == null || owner.Policy.GetProperty("profile").GetString() != "single-child-v3" || !System.Text.RegularExpressions.Regex.IsMatch(image, "^sha256:[a-f0-9]{64}$")) { throw new InvalidOperationException("The complete child owner or Engine image is unavailable."); } _image = image; _name = "deskpilot-child-engine-" + owner.RunId; owner.AttachEngine(this); try { WriteRecord("engine-ownership.json", "starting", true); WriteRecord("engine.json", "starting", true); JsonElement policy = owner.Policy; long memory = policy.GetProperty("memoryBytes").GetInt64() / 4; var arguments = new List<string> { "create", "--pull", "never", "--name", _name, "--label", "io.deskpilot.child=1", "--label", "io.deskpilot.child.component=engine", "--label", "io.deskpilot.child.run=" + owner.RunId, "--network", "none", "--read-only", "--cap-drop", "ALL", "--security-opt", "no-new-privileges", "--ipc", "none", "--log-driver", "none", "--user", "65534:65534", "--interactive", "--workdir", "/", "--memory", memory.ToString(CultureInfo.InvariantCulture), "--memory-swap", memory.ToString(CultureInfo.InvariantCulture), "--cpus", (policy.GetProperty("cpuCount").GetDouble() / 4).ToString(CultureInfo.InvariantCulture), "--pids-limit", "16", "--ulimit", "core=0:0", "--env", "HOME=/opt/deskpilot-child/home", "--env", "TMPDIR=/opt/deskpilot-child/home/tmp", "--env", "XDG_CACHE_HOME=/opt/deskpilot-child/home/.cache", "--env", "XDG_CONFIG_HOME=/opt/deskpilot-child/home/.config", "--env", "XDG_DATA_HOME=/opt/deskpilot-child/home/.local/share", "--env", "DOTNET_PROCESSOR_COUNT=1", "--env", "DOTNET_EnableDiagnostics=0", "--env", "POWERSHELL_TELEMETRY_OPTOUT=1", "--env", "PSModuleAnalysisCachePath=/dev/null", "--env", "PSModulePath=/opt/microsoft/powershell/7/Modules", "--entrypoint", "/opt/microsoft/powershell/7/pwsh", image, "-NoLogo", "-NoProfile", "-NonInteractive", "-File", "/opt/deskpilot-child/Start-DpChildEngine.ps1", "-EngineModulePath", "/opt/deskpilot-child/engine/ShellPilot.psd1", "-LeaseSeconds", policy.GetProperty("leaseSeconds").GetInt32().ToString(CultureInfo.InvariantCulture) }; EnsureStarting(); _containerId = owner.Control(arguments, Math.Min(30000, owner.RemainingMilliseconds())).Trim(); if (!System.Text.RegularExpressions.Regex.IsMatch(_containerId, "^[a-f0-9]{64}$")) { throw new IOException("The Engine container identity is invalid."); } WriteRecord("engine.json", "created", false); _ = Inspect(); EnsureStarting(); _attach = owner.Start(new[] { "start", "--attach", "--interactive", _containerId }); _bridge = new HostBridge(_attach.Output, _attach.Input, policy.GetProperty("leaseSeconds").GetInt32(), 4194304); _errors = ToolContainer.DrainAsync(_attach.Error, new ToolContainer.OutputBudget(16384), _cancel, null); WriteRecord("engine.json", "running", false); _starting = false; if (_stopped || owner.IsStopped) { throw new OperationCanceledException("Child admission closed during Engine startup."); } } catch { _starting = false; Stop(); throw; } } /// <summary>Immutable daemon identity recorded before execution.</summary> public string ContainerId => _containerId; /// <summary>True only after positive owned-resource removal.</summary> public bool CleanupSucceeded { get; private set; } private void EnsureStarting() { if (_stopped || _owner.IsStopped) { throw new OperationCanceledException("Child Engine startup was cancelled."); } _ = _owner.RemainingMilliseconds(); } /// <summary>Supplies only the Host Server approved bounded configuration.</summary> public void Configure(string configuration) { EnsureStarting(); if (Encoding.UTF8.GetByteCount(configuration) > _owner.Policy.GetProperty("requestBytes").GetInt32()) { throw new InvalidDataException("Child configuration exceeds its request limit."); } _bridge!.Configure(configuration); } /// <summary>Receives untrusted data through the authenticated per-run channel.</summary> public Task<string> ReceiveAsync(CancellationToken cancellationToken) => _bridge!.ReceiveAsync(cancellationToken); /// <summary>Returns one bounded Host Server result to the waiting Engine request.</summary> public void Reply(string id, string payload) { EnsureStarting(); _bridge!.Reply(id, payload); } /// <summary>Stops renewal independently of the Engine Runspace.</summary> public void WithdrawLease() => _bridge?.WithdrawLease(); /// <summary>Validates effective daemon settings and returns only profile facts.</summary> public string Inspect() { using JsonDocument document = JsonDocument.Parse(_owner.Control(new[] { "inspect", _containerId }, 5000)); JsonElement container = document.RootElement[0]; JsonElement host = container.GetProperty("HostConfig"); JsonElement configuration = container.GetProperty("Config"); long memory = _owner.Policy.GetProperty("memoryBytes").GetInt64() / 4; long cpu = (long)(_owner.Policy.GetProperty("cpuCount").GetDouble() / 4 * 1000000000); if (container.GetProperty("Id").GetString() != _containerId || container.GetProperty("Image").GetString() != _image || container.GetProperty("Name").GetString() != "/" + _name || configuration.GetProperty("Labels").GetProperty("io.deskpilot.child.run").GetString() != _owner.RunId || configuration.GetProperty("User").GetString() != "65534:65534" || !host.GetProperty("ReadonlyRootfs").GetBoolean() || host.GetProperty("Privileged").GetBoolean() || host.GetProperty("NetworkMode").GetString() != "none" || host.GetProperty("IpcMode").GetString() != "none" || host.GetProperty("Memory").GetInt64() != memory || host.GetProperty("MemorySwap").GetInt64() != memory || host.GetProperty("NanoCpus").GetInt64() != cpu || host.GetProperty("PidsLimit").GetInt64() != 16 || container.GetProperty("Mounts").GetArrayLength() != 0 || host.GetProperty("CapAdd").ValueKind != JsonValueKind.Null || host.GetProperty("CapDrop").GetArrayLength() != 1 || host.GetProperty("CapDrop")[0].GetString() != "ALL" || host.GetProperty("SecurityOpt").GetArrayLength() != 1 || host.GetProperty("SecurityOpt")[0].GetString() != "no-new-privileges") { throw new InvalidDataException("The effective Engine isolation profile does not match its authority."); } return JsonSerializer.Serialize(new { image = _image, network = "none", hostMounts = 0, readOnly = true, memoryBytes = memory, cpuNano = cpu, pids = 16 }); } private void WriteRecord(string file, string state, bool create) { byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(new { schemaVersion = 1, runId = _owner.RunId, name = _name, image = _image, containerId = _containerId, state, cleanupSucceeded = CleanupSucceeded }); if (bytes.Length > 2048) { throw new InvalidDataException("Engine ownership exceeds its byte limit."); } using var record = new FileStream(Path.Combine(_owner.DirectoryPath, file), create ? FileMode.CreateNew : FileMode.Create, FileAccess.Write, FileShare.None); record.Write(bytes); record.Flush(true); } /// <summary>Closes admission, removes only the owned Engine, and verifies absence.</summary> public void Stop() { lock (_sync) { if (_stopped && CleanupSucceeded) { return; } _stopped = true; _bridge?.WithdrawLease(); _cancel.Cancel(); if (_starting) { CleanupSucceeded = false; return; } try { string found = _owner.Control(new[] { "ps", "--all", "--no-trunc", "--filter", "name=^/" + _name + "$", "--filter", "label=io.deskpilot.child.run=" + _owner.RunId, "--format", "{{.ID}}" }, 5000).Trim(); if (found.Length > 0) { if (!System.Text.RegularExpressions.Regex.IsMatch(found, "^[a-f0-9]{64}$") || (_containerId.Length > 0 && found != _containerId)) { throw new InvalidDataException("Ambiguous Engine cleanup identity."); } _owner.Control(new[] { "rm", "--force", found }, 5000); } string remaining = _owner.Control(new[] { "ps", "--all", "--filter", "name=^/" + _name + "$", "--format", "{{.ID}}" }, 5000).Trim(); CleanupSucceeded = remaining.Length == 0; WriteRecord("engine.json", CleanupSucceeded ? "stopped" : "cleanup-failed", false); } catch { CleanupSucceeded = false; } finally { _attach?.Dispose(); _bridge?.Dispose(); } } } /// <summary>Terminates the owned component; the complete owner retains transport admission.</summary> public void Dispose() => Stop(); } } |