using ClawdDotNet.Core.Config; using ClawdDotNet.Core.Sandbox; namespace ClawdDotNet.Core.Runtimes; /// #712: Everything a runtime needs to lay out one run. public sealed record ForeignRunContext { public required AgentConfig Agent { get; init; } public required string RunId { get; init; } public string? TaskId { get; init; } /// The system prompt as the native engine would send it — rules, organisation. public required string Briefing { get; init; } /// The message of this run. public required string Message { get; init; } /// The run's copy of the workspace, on the host; /work in the sandbox. public required string WorkCopy { get; init; } /// The runtime's home, kept between runs — on the host; in the sandbox. public required string HomeDirectory { get; init; } /// Files of this run only (token, briefing, configuration) — on the host; in the sandbox. public required string PrivateDirectory { get; init; } /// The instance's MCP socket on the host, when it has one. public string? McpSocket { get; init; } /// The application, for the MCP bridge inside the sandbox. public SandboxHelper? Helper { get; init; } public const string Home = "/home/agent"; public const string Private = "/run/agent"; public const string McpSocketInSandbox = "/run/mcp.sock"; public const string TokenFileName = "mcp-token"; public const string BriefingFileName = "briefing.md"; /// /// The sandbox's side of the MCP server: the socket, the token file, the bridge. Without /// a socket or a helper there is none — the run works without taskboard access. /// public bool HasMcp => McpSocket is not null && Helper is not null; /// The mounts every foreign run gets: home (writable), the run's own files, the MCP socket. public IReadOnlyList Mounts() { var mounts = new List { new(HomeDirectory, Home, Writable: true), new(PrivateDirectory, Private) }; if (HasMcp) mounts.Add(new SandboxMount(McpSocket!, McpSocketInSandbox, Writable: true)); return mounts; } /// /// What must be visible read-only for the MCP bridge: the application. Mounted whenever /// there is MCP — not only when the launcher runs, which needs network or secrets. /// public IReadOnlyList HelperPaths() => HasMcp ? Helper!.ReadOnlyPaths : []; /// What a program in the sandbox needs to reach the MCP server itself. public IReadOnlyDictionary McpEnvironment() { var environment = new Dictionary(StringComparer.Ordinal) { ["HOME"] = Home, ["CLAWD_RUN_ID"] = RunId, ["CLAWD_BRIEFING_FILE"] = Private + "/" + BriefingFileName }; if (TaskId is not null) environment["CLAWD_TASK_ID"] = TaskId; if (HasMcp) { environment["CLAWD_MCP_SOCKET"] = McpSocketInSandbox; environment[Mcp.McpStdioBridge.TokenFileVariable] = Private + "/" + TokenFileName; environment["CLAWD_HELPER"] = Helper!.Executable; foreach (var (key, value) in Helper.Environment) environment[key] = value; } return environment; } } /// #712: What came out of a run, in the engine's terms. public sealed record ForeignRunOutcome { public Engine.AgentRunStatus Status { get; init; } = Engine.AgentRunStatus.Completed; public string? Text { get; init; } public int Turns { get; init; } public int PromptTokens { get; init; } public int CompletionTokens { get; init; } public int CachedTokens { get; init; } /// What the provider billed, if it said so; on a subscription 0. public decimal? CostUsd { get; init; } /// Paid by a subscription's quota — costs nothing, uses something (#699). public bool OnSubscription { get; init; } /// The model the runtime reported, if any. public string? Model { get; init; } } /// /// #712: One runtime — how a run is laid out for the sandbox, and how its output is read. /// The runner around it does everything that is the same for all: the copy, the token, /// the proposal, the clean-up. /// public interface IForeignRuntime { string Kind { get; } /// Writes what the run needs into the private directory and returns the sandbox request. SandboxRequest Prepare(ForeignRunContext context); /// Reads the result. takes lines for the run's transcript. ForeignRunOutcome Interpret(SandboxResult result, ForeignRunContext context, Action note); }