Files
ClawdDotNet/src/ClawdDotNet.Core/Runtimes/ForeignRuntime.cs
T

123 lines
5.0 KiB
C#

using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Sandbox;
namespace ClawdDotNet.Core.Runtimes;
/// <summary>#712: Everything a runtime needs to lay out one run.</summary>
public sealed record ForeignRunContext
{
public required AgentConfig Agent { get; init; }
public required string RunId { get; init; }
public string? TaskId { get; init; }
/// <summary>The system prompt as the native engine would send it — rules, organisation.</summary>
public required string Briefing { get; init; }
/// <summary>The message of this run.</summary>
public required string Message { get; init; }
/// <summary>The run's copy of the workspace, on the host; <c>/work</c> in the sandbox.</summary>
public required string WorkCopy { get; init; }
/// <summary>The runtime's home, kept between runs — on the host; <see cref="Home"/> in the sandbox.</summary>
public required string HomeDirectory { get; init; }
/// <summary>Files of this run only (token, briefing, configuration) — on the host; <see cref="Private"/> in the sandbox.</summary>
public required string PrivateDirectory { get; init; }
/// <summary>The instance's MCP socket on the host, when it has one.</summary>
public string? McpSocket { get; init; }
/// <summary>The application, for the MCP bridge inside the sandbox.</summary>
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";
/// <summary>
/// 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.
/// </summary>
public bool HasMcp => McpSocket is not null && Helper is not null;
/// <summary>The mounts every foreign run gets: home (writable), the run's own files, the MCP socket.</summary>
public IReadOnlyList<SandboxMount> Mounts()
{
var mounts = new List<SandboxMount>
{
new(HomeDirectory, Home, Writable: true),
new(PrivateDirectory, Private)
};
if (HasMcp)
mounts.Add(new SandboxMount(McpSocket!, McpSocketInSandbox, Writable: true));
return mounts;
}
/// <summary>
/// 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.
/// </summary>
public IReadOnlyList<string> HelperPaths() => HasMcp ? Helper!.ReadOnlyPaths : [];
/// <summary>What a program in the sandbox needs to reach the MCP server itself.</summary>
public IReadOnlyDictionary<string, string> McpEnvironment()
{
var environment = new Dictionary<string, string>(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;
}
}
/// <summary>#712: What came out of a run, in the engine's terms.</summary>
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; }
/// <summary>What the provider billed, if it said so; on a subscription 0.</summary>
public decimal? CostUsd { get; init; }
/// <summary>Paid by a subscription's quota — costs nothing, uses something (#699).</summary>
public bool OnSubscription { get; init; }
/// <summary>The model the runtime reported, if any.</summary>
public string? Model { get; init; }
}
/// <summary>
/// #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.
/// </summary>
public interface IForeignRuntime
{
string Kind { get; }
/// <summary>Writes what the run needs into the private directory and returns the sandbox request.</summary>
SandboxRequest Prepare(ForeignRunContext context);
/// <summary>Reads the result. <paramref name="note"/> takes lines for the run's transcript.</summary>
ForeignRunOutcome Interpret(SandboxResult result, ForeignRunContext context, Action<string> note);
}