child/ToolContainer.cs

#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Formats.Tar;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

namespace DeskPilot.Child
{
    /// <summary>Owns one quota-backed Tool container and its independent control path.</summary>
    public sealed class ToolContainer : IDisposable
    {
        private readonly string _docker;
        private readonly string _directory;
        private readonly string _name = "deskpilot-child-" + Guid.NewGuid().ToString("N");
        private readonly string _runId = Guid.NewGuid().ToString("N");
        private readonly JsonDocument _policy;
        private readonly CancellationTokenSource _cancel = new CancellationTokenSource();
        private readonly Stopwatch _clock = Stopwatch.StartNew();
        private readonly Stopwatch _cleanupClock = new Stopwatch();
        private readonly object _stopLock = new object();
        private readonly ManualResetEventSlim _toolOutput = new ManualResetEventSlim(false);
        private readonly FileStream? _owner;
        private readonly ProjectBaseline _controlRoot;
        private readonly ProjectBaseline? _runRoot;
        private readonly DateTime _createdUtc = DateTime.UtcNow;
        private readonly SortedDictionary<string, string> _baseline = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        private readonly OwnedProcess? _hostProcess;
        private readonly bool _ownsHostProcess;
        private EngineContainer? _engine;
        private readonly CancellationTokenRegistration _externalCancellation;
        private ControlProcess? _attach;
        private AuthenticatedChannel? _channel;
        private Timer? _renewal;
        private Timer? _deadline;
        private bool _sealed;
        private bool _frozen;
        private bool _stopped;
        private long _seedBytes;
        private int _seedFiles;
        private string _containerId = string.Empty;

        /// <summary>Creates ownership before launch and validates the effective container profile.</summary>
        public ToolContainer(string docker, string image, string directory, string policyJson)
            : this(docker, image, directory, policyJson, null, null) { }

        /// <summary>Creates a Tool container whose control processes share the trusted host budget.</summary>
        public ToolContainer(string docker, string image, string directory, string policyJson, OwnedProcess? hostProcess)
            : this(docker, image, directory, policyJson, hostProcess, null) { }

        /// <summary>Creates and records a suspended provider within the complete-run resource budget.</summary>
        public ToolContainer(string docker, string image, string directory, string policyJson, ProcessStartInfo providerStart)
            : this(docker, image, directory, policyJson, null, providerStart) { }

        /// <summary>Creates complete-run ownership under an external admission deadline.</summary>
        public ToolContainer(string docker, string image, string directory, string policyJson,
            ProcessStartInfo providerStart, string runId, CancellationToken cancellationToken)
            : this(docker, image, directory, policyJson, null, providerStart, runId, cancellationToken) { }

        private ToolContainer(string docker, string image, string directory, string policyJson,
            OwnedProcess? hostProcess, ProcessStartInfo? providerStart, string? runId = null, CancellationToken cancellationToken = default)
        {
            if (runId != null)
            {
                if (!System.Text.RegularExpressions.Regex.IsMatch(runId, "^[a-f0-9]{32}$")) { throw new ArgumentException("Invalid child identity."); }
                _runId = runId;
            }
            cancellationToken.ThrowIfCancellationRequested();
            _externalCancellation = cancellationToken.Register(() => _cancel.Cancel());
            _docker = docker;
            _hostProcess = hostProcess;
            _policy = JsonDocument.Parse(policyJson);
            string installation = Path.Combine(Path.GetFullPath(directory), "child-runs");
            _controlRoot = ProjectBaseline.CreateControlDirectory(installation);
            try
            {
                _owner = new FileStream(Path.Combine(installation, "active.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
                long retained = VerifyPreviousCleanup(installation);
                if (retained + _policy.RootElement.GetProperty("storageBytes").GetInt64() >
                    _policy.RootElement.GetProperty("retentionBytes").GetInt64())
                {
                    throw new InvalidOperationException("The installation child retention limit cannot reserve another run.");
                }
            }
            catch { _owner?.Dispose(); _controlRoot.Dispose(); throw; }
            _directory = Path.Combine(installation, _runId);
            ClaimPath = Path.Combine(_directory, "claim.json");
            try
            {
                _runRoot = ProjectBaseline.CreateControlDirectory(_directory);
                File.WriteAllText(Path.Combine(_directory, "config.json"), "{\"auths\":{},\"proxies\":{}}", new UTF8Encoding(false));
                SaveClaim("starting");
                JsonElement policy = _policy.RootElement;
                if (providerStart != null)
                {
                    using (var profileBarrier = new FileStream(Path.Combine(_directory, "AppData"), FileMode.CreateNew, FileAccess.Write, FileShare.None))
                    {
                        profileBarrier.Flush(true);
                    }
                    var environment = new Dictionary<string, string>
                    {
                        ["SystemRoot"] = Environment.GetEnvironmentVariable("SystemRoot") ?? @"C:\Windows",
                        ["TEMP"] = _directory, ["TMP"] = _directory, ["USERPROFILE"] = _directory,
                        ["APPDATA"] = _directory, ["LOCALAPPDATA"] = _directory,
                        ["PSModulePath"] = Path.Combine(Path.GetDirectoryName(providerStart.FileName)!, "Modules"),
                        ["HOME"] = _directory, ["DOTNET_EnableDiagnostics"] = "0",
                        ["DOTNET_PROCESSOR_COUNT"] = "1", ["POWERSHELL_TELEMETRY_OPTOUT"] = "1",
                        ["PSModuleAnalysisCachePath"] = "NUL"
                    };
                    _ownsHostProcess = true;
                    _hostProcess = new OwnedProcess(providerStart.FileName, new List<string>(providerStart.ArgumentList).ToArray(),
                        _directory, environment, policy.GetProperty("memoryBytes").GetInt64() / 4,
                        policy.GetProperty("cpuCount").GetDouble() / 4, 8);
                    byte[] identity = JsonSerializer.SerializeToUtf8Bytes(new
                    {
                        schemaVersion = 1, runId = _runId, processId = _hostProcess.Id,
                        startTimeUtcTicks = _hostProcess.StartTimeUtcTicks
                    });
                    using var record = new FileStream(Path.Combine(_directory, "provider.json"), FileMode.CreateNew, FileAccess.Write, FileShare.None);
                    record.Write(identity);
                    record.Flush(true);
                }
                string access = policy.GetProperty("projectAccess").GetString()!;
                var arguments = new List<string>
                {
                    "create", "--pull", "never", "--name", _name,
                    "--label", "io.deskpilot.child=1", "--label", "io.deskpilot.child.run=" + _runId,
                    "--network", "none", "--read-only", "--cap-drop", "ALL",
                    "--security-opt", "no-new-privileges", "--ipc", "none", "--log-driver", "none",
                    "--user", "0:0", "--interactive", "--workdir", "/",
                    "--memory", (policy.GetProperty("memoryBytes").GetInt64() / 2).ToString(CultureInfo.InvariantCulture),
                    "--memory-swap", (policy.GetProperty("memoryBytes").GetInt64() / 2).ToString(CultureInfo.InvariantCulture),
                    "--cpus", (policy.GetProperty("cpuCount").GetDouble() / 2).ToString(CultureInfo.InvariantCulture),
                    "--pids-limit", "40", "--ulimit", "core=0:0",
                    "--tmpfs", "/work:rw,nosuid,nodev,noexec,mode=0755,size=" + policy.GetProperty("toolStorageBytes").GetInt64() +
                        ",nr_inodes=" + policy.GetProperty("inodeLimit").GetInt64(),
                    "--env", "HOME=/work/home", "--env", "TMPDIR=/work/tmp", "--env", "DOTNET_PROCESSOR_COUNT=1",
                    "--env", "DOTNET_EnableDiagnostics=0", "--env", "POWERSHELL_TELEMETRY_OPTOUT=1",
                    "--env", "PSModuleAnalysisCachePath=/dev/null"
                };
                foreach (string capability in new[] { "CHOWN", "DAC_OVERRIDE", "SETUID", "SETGID", "SETPCAP", "KILL" })
                {
                    arguments.AddRange(new[] { "--cap-add", capability });
                }
                arguments.AddRange(new[] { "--entrypoint", "/opt/microsoft/powershell/7/pwsh", image,
                    "-NoLogo", "-NoProfile", "-NonInteractive", "-File", "/opt/deskpilot-child/Start-DpChildSupervisor.ps1",
                    "-LeaseSeconds", policy.GetProperty("leaseSeconds").GetInt32().ToString(CultureInfo.InvariantCulture),
                    "-ProjectAccess", access });
                _containerId = Control(arguments, 30000).Trim();
                if (!System.Text.RegularExpressions.Regex.IsMatch(_containerId, "^[a-f0-9]{64}$"))
                {
                    throw new IOException("The child container identity could not be verified.");
                }
                SaveClaim("created");
                _attach = Start(new[] { "start", "--attach", "--interactive", _containerId });
                byte[] key = RandomNumberGenerator.GetBytes(32);
                _channel = new AuthenticatedChannel(_attach.Output, _attach.Input, key, true, 16384);
                _attach.Input.Write(key);
                _attach.Input.Flush();
                CryptographicOperations.ZeroMemory(key);
                _ = DrainAsync(_attach.Error, new OutputBudget(16384), _cancel, null);
                ReceiveControl("ready");
                int renewalMs = Math.Max(100, policy.GetProperty("leaseSeconds").GetInt32() * 1000 / 3);
                _renewal = new Timer(_ =>
                {
                    try { _channel.Send("{\"type\":\"renew\"}"); }
                    catch { _cancel.Cancel(); }
                }, null, 0, renewalMs);
                int remaining = Math.Max(1, policy.GetProperty("durationSeconds").GetInt32() * 1000 - (int)_clock.ElapsedMilliseconds);
                _deadline = new Timer(_ => Stop(), null, remaining, Timeout.Infinite);
                SaveClaim("seeding");
            }
            catch
            {
                Stop();
                if (_ownsHostProcess) { _hostProcess?.Dispose(); }
                _owner?.Dispose();
                _runRoot?.Dispose();
                _controlRoot.Dispose();
                throw;
            }
        }

        /// <summary>Durable ownership claim written before creation.</summary>
        public string ClaimPath { get; }
        /// <summary>Immutable Docker identity, not a child-provided identity.</summary>
        public string ContainerId => _containerId;
        /// <summary>Host-owned run identity shared by both containers and the transport.</summary>
        public string RunId => _runId;
        /// <summary>Private bounded control storage, never sent to the child.</summary>
        public string DirectoryPath => _directory;
        /// <summary>The trusted aggregate job; provider code remains suspended until Host Server resume.</summary>
        public OwnedProcess? HostProcess => _hostProcess;
        internal long HostStorageLimit => _policy.RootElement.GetProperty("storageBytes").GetInt64() - _policy.RootElement.GetProperty("toolStorageBytes").GetInt64();
        internal JsonElement Policy => _policy.RootElement;
        internal bool IsStopped => _stopped;

        internal void AttachEngine(EngineContainer engine)
        {
            lock (_stopLock)
            {
                if (_stopped || _engine != null) { throw new InvalidOperationException("Child Engine ownership is unavailable."); }
                _engine = engine;
            }
        }
        /// <summary>True only after verified removal.</summary>
        public bool CleanupSucceeded { get; private set; }

        /// <summary>Seeds one selected file before any Tool operation is admitted.</summary>
        public void Seed(string path, byte[] bytes)
        {
            if (_sealed || _stopped) { throw new InvalidOperationException("Child input is already sealed or stopped."); }
            ProjectBaseline.ValidateRelativePath(path);
            if (_seedFiles + 1 > _policy.RootElement.GetProperty("baselineFiles").GetInt32() ||
                _seedBytes + bytes.LongLength > _policy.RootElement.GetProperty("baselineBytes").GetInt64())
            {
                throw new InvalidOperationException("The selected baseline limit was exceeded.");
            }
            string result = FileOperation("seed", path, bytes, 0, 8192, true);
            using JsonDocument record = JsonDocument.Parse(result);
            if (!record.RootElement.GetProperty("ok").GetBoolean()) { throw new IOException("The selected baseline could not be seeded."); }
            _seedBytes += bytes.LongLength;
            _seedFiles++;
            _baseline.Add(path.Replace('\\', '/'), Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant());
        }

        /// <summary>Freezes baseline ownership before enabling File or Terminal.</summary>
        public void Seal()
        {
            if (_sealed || _stopped) { throw new InvalidOperationException("Child input is already sealed or stopped."); }
            _channel!.Send("{\"type\":\"seal\"}");
            ReceiveControl("sealed");
            _sealed = true;
            SaveClaim("running");
        }

        /// <summary>Reads only through the contained File implementation.</summary>
        public string Read(string path, int offset, int count)
        {
            EnsureRunning();
            return FileOperation("read", path, Array.Empty<byte>(), offset, count, false);
        }

        /// <summary>Writes only through the contained File implementation.</summary>
        public string Write(string path, byte[] bytes)
        {
            EnsureRunning();
            if (_policy.RootElement.GetProperty("projectAccess").GetString() != "read-write")
            {
                return "{\"ok\":false,\"code\":\"read_only\"}";
            }
            return FileOperation("write", path, bytes, 0, 8192, false);
        }

        /// <summary>Executes a Host Server-authorized command as the unprivileged Tool user.</summary>
        public string Execute(string command)
        {
            EnsureRunning();
            _toolOutput.Reset();
            if (string.IsNullOrWhiteSpace(command) || command.Length > 2000) { throw new InvalidDataException("The command limit was exceeded."); }
            string script = "$ErrorActionPreference = 'Stop'\n" + command;
            var arguments = ToolArguments(false);
            arguments.AddRange(new[] { "-EncodedCommand", Convert.ToBase64String(Encoding.Unicode.GetBytes(script)) });
            NativeResult result;
            try { result = Call(arguments, Array.Empty<byte>(), RemainingMilliseconds(), _policy.RootElement.GetProperty("outputBytes").GetInt32(), _cancel.Token, _toolOutput); }
            catch { Stop(); throw; }
            using JsonDocument quota = JsonDocument.Parse(Quota());
            bool exhausted = quota.RootElement.GetProperty("freeBytes").GetUInt64() == 0 || quota.RootElement.GetProperty("freeInodes").GetUInt64() == 0;
            return JsonSerializer.Serialize(new { exitCode = result.ExitCode, stdout = result.Output, stderr = result.Error, quotaExceeded = exhausted });
        }

        /// <summary>Runs a command without blocking the Host Server control path.</summary>
        public Task<string> ExecuteAsync(string command) => Task.Run(() => Execute(command));

        /// <summary>Waits for actual command output, not merely client process creation.</summary>
        public bool WaitForToolOutput(int timeoutMilliseconds) => _toolOutput.Wait(timeoutMilliseconds);

        /// <summary>Withdraws renewals without relying on a busy command or closed pipe.</summary>
        public void WithdrawLease()
        {
            _renewal?.Dispose();
            _renewal = null;
        }

        /// <summary>Exports bounded proposed bytes after terminating every Tool descendant.</summary>
        public string Export()
        {
            EnsureRunning();
            _frozen = true;
            try
            {
                _channel!.Send("{\"type\":\"freeze\"}");
                ReceiveControl("frozen");
                SaveClaim("exporting");
                var validate = ToolArguments(true);
                validate.AddRange(new[] { "-File", "/opt/deskpilot-child/Invoke-DpChildFile.ps1", "-Operation", "validate-export",
                    "-Count", _policy.RootElement.GetProperty("inodeLimit").GetInt32().ToString(CultureInfo.InvariantCulture) });
                NativeResult validation = Call(validate, Array.Empty<byte>(), 10000, 16384, _cancel.Token);
                if (validation.ExitCode != 0) { throw new InvalidDataException("Unsafe export file, link or mount was refused."); }
                using ControlProcess copy = Start(new[] { "exec", "--user", "0:0", _containerId,
                    "/usr/bin/tar", "--format=pax", "--one-file-system", "-C", "/work", "-cf", "-", "--", "project" });
                copy.Input.Close();
                using var deadline = CancellationTokenSource.CreateLinkedTokenSource(_cancel.Token);
                deadline.CancelAfter(RemainingMilliseconds());
                using CancellationTokenRegistration kill = deadline.Token.Register(() =>
                {
                    try { if (!copy.HasExited) { copy.Kill(true); } }
                    catch (InvalidOperationException) { }
                });
                Task<byte[]> errors = DrainAsync(copy.Error, new OutputBudget(16384), deadline, null);
                long archiveLimit = _policy.RootElement.GetProperty("toolStorageBytes").GetInt64() +
                    _policy.RootElement.GetProperty("inodeLimit").GetInt64() * 4096;
                using var bounded = new BoundedReadStream(copy.Output, archiveLimit);
                using var archive = new TarReader(bounded, leaveOpen: true);
                var files = new List<object>();
                var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                long proposedBytes = 0;
                long scannedBytes = 0;
                int entries = 0;
                try
                {
                    TarEntry? entry;
                    while ((entry = archive.GetNextEntryAsync(false, deadline.Token).AsTask().GetAwaiter().GetResult()) != null)
                    {
                        if (++entries > _policy.RootElement.GetProperty("inodeLimit").GetInt32())
                        {
                            throw new InvalidDataException("The export entry limit was exceeded.");
                        }
                        string name = entry.Name.TrimEnd('/');
                        if (name == "project" && entry.EntryType == TarEntryType.Directory) { continue; }
                        if (!name.StartsWith("project/", StringComparison.Ordinal)) { throw new InvalidDataException("Unsafe export archive path."); }
                        string relative = ProjectBaseline.ValidateRelativePath(name.Substring(8));
                        if (!seen.Add(relative)) { throw new InvalidDataException("Duplicate export paths or case aliases were refused."); }
                        if (entry.EntryType == TarEntryType.Directory) { continue; }
                        if (entry.EntryType != TarEntryType.RegularFile || entry.Length < 0 ||
                            entry.Length > _policy.RootElement.GetProperty("toolStorageBytes").GetInt64())
                        {
                            throw new InvalidDataException("Unsafe export file type or size was refused.");
                        }
                        scannedBytes += entry.Length;
                        if (scannedBytes > _policy.RootElement.GetProperty("toolStorageBytes").GetInt64())
                        {
                            throw new InvalidDataException("The export byte limit was exceeded.");
                        }
                        var bytes = new byte[(int)entry.Length];
                        if (bytes.Length > 0) { entry.DataStream!.ReadExactlyAsync(bytes, deadline.Token).AsTask().GetAwaiter().GetResult(); }
                        string digest = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
                        _baseline.TryGetValue(relative, out string? baselineDigest);
                        if (digest == baselineDigest) { continue; }
                        proposedBytes += bytes.LongLength;
                        CheckProposalLimit(files.Count + 1, proposedBytes);
                        files.Add(new
                        {
                            path = relative, operation = baselineDigest == null ? "add" : "modify", size = bytes.LongLength,
                            baselineSha256 = baselineDigest, resultSha256 = digest, contentBase64 = Convert.ToBase64String(bytes)
                        });
                    }
                    foreach (KeyValuePair<string, string> baseline in _baseline)
                    {
                        if (seen.Contains(baseline.Key)) { continue; }
                        CheckProposalLimit(files.Count + 1, proposedBytes);
                        files.Add(new { path = baseline.Key, operation = "delete", size = 0, baselineSha256 = baseline.Value,
                            resultSha256 = (string?)null, contentBase64 = (string?)null });
                    }
                    bounded.CopyToAsync(Stream.Null, 8192, deadline.Token).GetAwaiter().GetResult();
                    copy.WaitForExitAsync(deadline.Token).GetAwaiter().GetResult();
                    errors.GetAwaiter().GetResult();
                    if (copy.ExitCode != 0) { throw new IOException("The contained export did not complete."); }
                }
                catch { deadline.Cancel(); throw; }
                string proposal = JsonSerializer.Serialize(new
                {
                    schemaVersion = 1, runId = _runId, containerId = _containerId,
                    files, totalBytes = proposedBytes, realProjectFilesWritten = Array.Empty<string>()
                });
                long hostLimit = _policy.RootElement.GetProperty("storageBytes").GetInt64() -
                    _policy.RootElement.GetProperty("toolStorageBytes").GetInt64();
                long reserved = _engine == null ? 8192 : 65536 + _policy.RootElement.GetProperty("resultBytes").GetInt64() +
                    _policy.RootElement.GetProperty("eventBytes").GetInt64() * _policy.RootElement.GetProperty("eventLimit").GetInt64() +
                    _policy.RootElement.GetProperty("requestBytes").GetInt64() * 12 + _policy.RootElement.GetProperty("outputBytes").GetInt64() * 8;
                if (Encoding.UTF8.GetByteCount(proposal) > hostLimit - reserved)
                {
                    throw new InvalidDataException("The retained proposal storage limit was exceeded.");
                }
                using (var destination = new FileStream(Path.Combine(_directory, "proposal.json"), FileMode.CreateNew, FileAccess.Write, FileShare.None))
                {
                    byte[] bytes = Encoding.UTF8.GetBytes(proposal);
                    destination.Write(bytes);
                    destination.Flush(true);
                }
                SaveClaim("exported");
                return proposal;
            }
            catch { Stop(); throw; }
        }

        private void CheckProposalLimit(int count, long bytes)
        {
            if (count > _policy.RootElement.GetProperty("proposalFiles").GetInt32() ||
                bytes > _policy.RootElement.GetProperty("proposalBytes").GetInt64())
            {
                throw new InvalidDataException("The proposal file or byte limit was exceeded.");
            }
        }

        /// <summary>Reports kernel quotas and daemon-observed network and mounts.</summary>
        public string Inspect()
        {
            EnsureRunning();
            using JsonDocument quota = JsonDocument.Parse(Quota());
            using JsonDocument inspection = JsonDocument.Parse(Control(new[] { "inspect", _containerId }, 5000));
            JsonElement state = inspection.RootElement[0];
            int hostMounts = 0;
            foreach (JsonElement mount in state.GetProperty("Mounts").EnumerateArray())
            {
                if (mount.GetProperty("Type").GetString() != "tmpfs") { hostMounts++; }
            }
            JsonElement host = state.GetProperty("HostConfig");
            long memory = _policy.RootElement.GetProperty("memoryBytes").GetInt64() / 2;
            long cpu = (long)(_policy.RootElement.GetProperty("cpuCount").GetDouble() / 2 * 1000000000);
            if (state.GetProperty("Id").GetString() != _containerId || hostMounts != 0 ||
                state.GetProperty("Config").GetProperty("Labels").GetProperty("io.deskpilot.child.run").GetString() != _runId ||
                !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() != 40 ||
                quota.RootElement.GetProperty("bytes").GetUInt64() != (ulong)_policy.RootElement.GetProperty("toolStorageBytes").GetInt64() ||
                quota.RootElement.GetProperty("inodes").GetUInt64() != (ulong)_policy.RootElement.GetProperty("inodeLimit").GetInt64())
            { throw new InvalidDataException("The effective child Tool profile does not match its authority."); }
            return JsonSerializer.Serialize(new
            {
                bytes = quota.RootElement.GetProperty("bytes").GetUInt64(),
                inodes = quota.RootElement.GetProperty("inodes").GetUInt64(),
                network = host.GetProperty("NetworkMode").GetString(), hostMounts,
                memoryBytes = memory, cpuNano = cpu, pids = 40, readOnly = true
            });
        }

        private string Quota()
        {
            var arguments = ToolArguments(false);
            arguments.AddRange(new[] { "-File", "/opt/deskpilot-child/Invoke-DpChildFile.ps1", "-Operation", "inspect" });
            NativeResult result = Call(arguments, Array.Empty<byte>(), 5000, 16384, _cancel.Token);
            if (result.ExitCode != 0) { throw new IOException("The effective Tool quota could not be inspected."); }
            return result.Output.Trim();
        }

        private string FileOperation(string operation, string path, byte[] bytes, int offset, int count, bool seed)
        {
            ProjectBaseline.ValidateRelativePath(path);
            if (bytes.LongLength > 67108864) { throw new InvalidDataException("The File input limit was exceeded."); }
            var arguments = ToolArguments(seed);
            arguments.AddRange(new[] { "-File", "/opt/deskpilot-child/Invoke-DpChildFile.ps1", "-Operation", operation,
                "-PathBase64", Convert.ToBase64String(Encoding.UTF8.GetBytes(path)),
                "-Offset", offset.ToString(CultureInfo.InvariantCulture), "-Count", count.ToString(CultureInfo.InvariantCulture),
                "-InputLength", bytes.LongLength.ToString(CultureInfo.InvariantCulture) });
            NativeResult result = Call(arguments, bytes, RemainingMilliseconds(), 65536, _cancel.Token);
            if (result.ExitCode != 0 || string.IsNullOrWhiteSpace(result.Output)) { throw new IOException("The contained File operation failed: " + result.Error); }
            return result.Output.Trim();
        }

        private List<string> ToolArguments(bool seed)
        {
            var arguments = new List<string> { "exec", "--interactive", "--user", "0:0", "--workdir", "/work/project", _containerId };
            if (!seed)
            {
                arguments.AddRange(new[] { "/usr/bin/setpriv", "--reuid=10001", "--regid=10001", "--clear-groups",
                    "--bounding-set=-all", "--inh-caps=-all", "--ambient-caps=-all", "--" });
            }
            arguments.AddRange(new[] { "/opt/microsoft/powershell/7/pwsh", "-NoLogo", "-NoProfile", "-NonInteractive" });
            return arguments;
        }

        private void ReceiveControl(string expected)
        {
            using var timeout = new CancellationTokenSource(Math.Min(30000, RemainingMilliseconds()));
            using JsonDocument record = JsonDocument.Parse(_channel!.ReceiveAsync(timeout.Token).GetAwaiter().GetResult());
            if (record.RootElement.GetProperty("type").GetString() != expected) { throw new InvalidDataException("Unexpected child control state."); }
        }

        private void EnsureRunning()
        {
            if (!_sealed || _frozen || _stopped || _cancel.IsCancellationRequested) { throw new InvalidOperationException("The child Tool container is not running."); }
        }

        internal int RemainingMilliseconds()
        {
            long remaining = _policy.RootElement.GetProperty("durationSeconds").GetInt32() * 1000L - _clock.ElapsedMilliseconds;
            if (remaining <= 0) { throw new TimeoutException("The complete child deadline expired."); }
            return (int)remaining;
        }

        private void SaveClaim(string state)
        {
            string record = JsonSerializer.Serialize(new
            {
                schemaVersion = 1, runId = _runId, name = _name, containerId = _containerId, state,
                ownerPid = Environment.ProcessId, ownerStartUtc = Process.GetCurrentProcess().StartTime.ToUniversalTime(),
                policyDigest = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(_policy.RootElement.GetRawText()))).ToLowerInvariant(),
                retainedBytes = _policy.RootElement.GetProperty("storageBytes").GetInt64() - _policy.RootElement.GetProperty("toolStorageBytes").GetInt64(),
                expiresUtc = _createdUtc.AddHours(_policy.RootElement.GetProperty("retentionHours").GetInt32()),
                cleanupSucceeded = CleanupSucceeded
            });
            byte[] bytes = Encoding.UTF8.GetBytes(record);
            if (bytes.Length > 2048) { throw new InvalidDataException("The child ownership record limit was exceeded."); }
            string ownership = Path.Combine(_directory, "ownership.json");
            if (!File.Exists(ownership))
            {
                using var identity = new FileStream(ownership, FileMode.CreateNew, FileAccess.Write, FileShare.None);
                identity.Write(bytes);
                identity.Flush(true);
            }
            using var stateFile = new FileStream(ClaimPath, FileMode.Create, FileAccess.Write, FileShare.None);
            stateFile.Write(bytes);
            stateFile.Flush(true);
        }

        private static long VerifyPreviousCleanup(string installation)
        {
            int count = 0;
            long retained = 0;
            foreach (string directory in Directory.EnumerateDirectories(installation))
            {
                if (++count > 4096 || (File.GetAttributes(directory) & FileAttributes.ReparsePoint) != 0)
                {
                    throw new IOException("Child cleanup records exceed their limit or require reconciliation.");
                }
                string claim = Path.Combine(directory, "claim.json");
                try
                {
                    if (!File.Exists(claim) || new FileInfo(claim).Length > 4096)
                    {
                        throw new InvalidDataException("Missing or oversized ownership claim.");
                    }
                    using JsonDocument record = JsonDocument.Parse(File.ReadAllText(claim));
                    if (!record.RootElement.GetProperty("cleanupSucceeded").GetBoolean() ||
                        record.RootElement.GetProperty("state").GetString() != "stopped")
                    {
                        throw new InvalidDataException("Unfinished ownership claim.");
                    }
                    long reservation = record.RootElement.GetProperty("retainedBytes").GetInt64();
                    if (reservation < 8192 || reservation > 67108864 ||
                        record.RootElement.GetProperty("runId").GetString() != Path.GetFileName(directory))
                    {
                        throw new InvalidDataException("Invalid retention reservation.");
                    }
                    long actual = 0;
                    int fileCount = 0;
                    foreach (string file in Directory.EnumerateFileSystemEntries(directory))
                    {
                        if (++fileCount > 9 || (File.GetAttributes(file) & (FileAttributes.ReparsePoint | FileAttributes.Directory)) != 0)
                        {
                            throw new InvalidDataException("Unsupported retained child data.");
                        }
                        actual += new FileInfo(file).Length;
                        if (actual > reservation) { throw new InvalidDataException("Retained child data exceeds its reservation."); }
                    }
                    retained = checked(retained + reservation);
                }
                catch (Exception error) when (error is IOException || error is InvalidDataException || error is JsonException ||
                    error is KeyNotFoundException || error is InvalidOperationException)
                {
                    throw new IOException("Child cleanup must be reconciled before another run.", error);
                }
            }
            return retained;
        }

        internal ControlProcess Start(IEnumerable<string> arguments)
        {
            var start = new ProcessStartInfo(_docker)
            {
                UseShellExecute = false, CreateNoWindow = true, RedirectStandardInput = true,
                RedirectStandardOutput = true, RedirectStandardError = true, WorkingDirectory = _directory
            };
            start.Environment.Clear();
            start.Environment["SystemRoot"] = Environment.GetEnvironmentVariable("SystemRoot") ?? @"C:\Windows";
            start.Environment["PATH"] = Path.GetDirectoryName(_docker)!;
            start.Environment["USERPROFILE"] = _directory;
            start.Environment["TEMP"] = _directory;
            start.Environment["TMP"] = _directory;
            start.ArgumentList.Add("--host");
            start.ArgumentList.Add("npipe:////./pipe/dockerDesktopLinuxEngine");
            start.ArgumentList.Add("--config");
            start.ArgumentList.Add(_directory);
            foreach (string argument in arguments) { start.ArgumentList.Add(argument); }
            return new ControlProcess(start, _hostProcess);
        }

        internal string Control(IEnumerable<string> arguments, int timeout)
        {
            if (_stopped)
            {
                int remaining = _policy.RootElement.GetProperty("cleanupSeconds").GetInt32() * 1000 - (int)_cleanupClock.ElapsedMilliseconds;
                if (remaining <= 0) { throw new TimeoutException("The complete child cleanup grace expired."); }
                timeout = Math.Min(timeout, remaining);
            }
            else { timeout = Math.Min(timeout, RemainingMilliseconds()); }
            NativeResult result = Call(arguments, Array.Empty<byte>(), timeout, 1048576, _stopped ? CancellationToken.None : _cancel.Token);
            if (result.ExitCode != 0) { throw new IOException("Docker child control failed: " + result.Error); }
            return result.Output;
        }

        private NativeResult Call(IEnumerable<string> arguments, byte[] input, int timeout, int limit,
            CancellationToken cancellation, ManualResetEventSlim? observed = null)
        {
            cancellation.ThrowIfCancellationRequested();
            using ControlProcess process = Start(arguments);
            using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
            deadline.CancelAfter(timeout);
            using CancellationTokenRegistration kill = deadline.Token.Register(() =>
            {
                try { if (!process.HasExited) { process.Kill(true); } }
                catch (InvalidOperationException) { }
            });
            var budget = new OutputBudget(limit);
            Task<byte[]> output = DrainAsync(process.Output, budget, deadline, observed);
            Task<byte[]> error = DrainAsync(process.Error, budget, deadline, observed);
            Task write = WriteInputAsync(process.Input, input, deadline.Token);
            try
            {
                Task.WhenAll(output, error, write, process.WaitForExitAsync(deadline.Token)).GetAwaiter().GetResult();
                return new NativeResult { ExitCode = process.ExitCode, Output = Encoding.UTF8.GetString(output.Result), Error = Encoding.UTF8.GetString(error.Result) };
            }
            catch
            {
                deadline.Cancel();
                if (budget.Exceeded) { throw new InvalidDataException("The combined child output limit was exceeded."); }
                throw;
            }
        }

        private static async Task WriteInputAsync(Stream stream, byte[] bytes, CancellationToken cancellation)
        {
            try { await stream.WriteAsync(bytes.AsMemory(), cancellation).ConfigureAwait(false); }
            catch (IOException) { }
            finally { stream.Close(); }
        }

        internal static async Task<byte[]> DrainAsync(Stream stream, OutputBudget budget,
            CancellationTokenSource cancellation, ManualResetEventSlim? observed)
        {
            using var result = new MemoryStream();
            var buffer = new byte[4096];
            while (true)
            {
                int count = await stream.ReadAsync(buffer.AsMemory(), cancellation.Token).ConfigureAwait(false);
                if (count == 0) { return result.ToArray(); }
                observed?.Set();
                if (!budget.Reserve(count))
                {
                    cancellation.Cancel();
                    throw new InvalidDataException("The combined child output limit was exceeded.");
                }
                result.Write(buffer, 0, count);
            }
        }

        /// <summary>Stops through a control path independent of the busy Tool process.</summary>
        public void Stop()
        {
            lock (_stopLock)
            {
                if (_stopped) { return; }
                _stopped = true;
                _cleanupClock.Start();
                _hostProcess?.BeginCleanup(Math.Max(1, _policy.RootElement.GetProperty("cleanupSeconds").GetInt32() * 1000 - (int)_cleanupClock.ElapsedMilliseconds));
                _renewal?.Dispose();
                _deadline?.Dispose();
                _cancel.Cancel();
                try
                {
                    _engine?.Stop();
                    string found = Control(new[] { "ps", "--all", "--filter", "name=^/" + _name + "$", "--filter", "label=io.deskpilot.child.run=" + _runId, "--format", "{{.ID}}" }, 5000).Trim();
                    if (found.Length > 0) { Control(new[] { "rm", "--force", found }, 15000); }
                    string remaining = Control(new[] { "ps", "--all", "--filter", "name=^/" + _name + "$", "--format", "{{.ID}}" }, 5000).Trim();
                    CleanupSucceeded = remaining.Length == 0 && (_engine == null || _engine.CleanupSucceeded);
                }
                catch { CleanupSucceeded = false; }
                finally
                {
                    try { if (_ownsHostProcess) { _hostProcess?.Stop(); } }
                    catch { CleanupSucceeded = false; }
                    try { SaveClaim(CleanupSucceeded ? "stopped" : "cleanup-failed"); }
                    catch { CleanupSucceeded = false; }
                    _channel?.Dispose();
                    if (_attach != null)
                    {
                        if (!_attach.HasExited) { _attach.Kill(true); }
                        _attach.Dispose();
                    }
                }
            }
        }

        /// <summary>Stops owned resources and releases installation admission.</summary>
        public void Dispose()
        {
            Stop();
            if (_ownsHostProcess) { _hostProcess?.Dispose(); }
            _externalCancellation.Dispose();
            _owner?.Dispose();
            _runRoot?.Dispose();
            _controlRoot.Dispose();
        }

        private sealed class NativeResult
        {
            public int ExitCode { get; set; }
            public string Output { get; set; } = string.Empty;
            public string Error { get; set; } = string.Empty;
        }

        internal sealed class OutputBudget
        {
            private readonly int _limit;
            private int _bytes;
            public OutputBudget(int limit) { _limit = limit; }
            public bool Exceeded => Volatile.Read(ref _bytes) > _limit;
            public bool Reserve(int bytes) => Interlocked.Add(ref _bytes, bytes) <= _limit;
        }

        private sealed class BoundedReadStream : Stream
        {
            private readonly Stream _source;
            private readonly long _limit;
            private long _read;
            public BoundedReadStream(Stream source, long limit) { _source = source; _limit = limit; }
            public override bool CanRead => true;
            public override bool CanSeek => false;
            public override bool CanWrite => false;
            public override long Length => throw new NotSupportedException();
            public override long Position { get => _read; set => throw new NotSupportedException(); }
            public override void Flush() => throw new NotSupportedException();
            public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
            public override void SetLength(long value) => throw new NotSupportedException();
            public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
            public override int Read(byte[] buffer, int offset, int count)
            {
                int read = _source.Read(buffer, offset, (int)Math.Min(count, Math.Max(1, _limit - _read + 1)));
                _read += read;
                if (_read > _limit) { throw new InvalidDataException("The export archive limit was exceeded."); }
                return read;
            }
            public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
            {
                int count = (int)Math.Min(buffer.Length, Math.Max(1, _limit - _read + 1));
                int read = await _source.ReadAsync(buffer.Slice(0, count), cancellationToken).ConfigureAwait(false);
                _read += read;
                if (_read > _limit) { throw new InvalidDataException("The export archive limit was exceeded."); }
                return read;
            }
        }
    }
}