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);
}