Fremde Laufzeiten: Claude Code und Befehl in der Sandbox, Egress-Proxy, Vorschlag statt Einsatz (#712)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7d6c2c9495
commit
367ea387f6
@@ -34,6 +34,75 @@ public sealed record SandboxRequest
|
||||
public SandboxLimits Limits { get; init; } = new();
|
||||
|
||||
public string? StandardInput { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// #712: Further host paths at a place of their own — a runtime's home, the MCP socket.
|
||||
/// Applied after the workspace; a target inside <c>/work</c> is refused.
|
||||
/// </summary>
|
||||
public IReadOnlyList<SandboxMount> Mounts { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// #712/#655: The hosts the run may reach, as <c>host</c> or <c>host:port</c> (443 if
|
||||
/// none), <c>*.example.com</c> for subdomains. Empty: no network at all. Otherwise the
|
||||
/// sandbox still has no network of its own; an HTTPS proxy in the main process opens
|
||||
/// only these, and only to public addresses (<see cref="EgressProxy"/>).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Egress { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// #712: Environment the program needs but that must not stand on a command line — an
|
||||
/// API key. Handed over in a file only the sandbox's launcher reads, never through argv.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> SecretEnvironment { get; init; } = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>Whether the run needs the launcher inside the sandbox (<see cref="SandboxLauncher"/>).</summary>
|
||||
public bool NeedsLauncher => Egress.Count > 0 || SecretEnvironment.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>#712: A host path mounted at <paramref name="Target"/> in the sandbox.</summary>
|
||||
public sealed record SandboxMount(string HostPath, string Target, bool Writable = false);
|
||||
|
||||
/// <summary>
|
||||
/// #712: The program that starts the work inside the sandbox when it needs more than an
|
||||
/// <c>exec</c> — the application itself, called with <see cref="SandboxLauncher.Argument"/>.
|
||||
/// It is mounted read-only with what it needs to start.
|
||||
/// </summary>
|
||||
public sealed record SandboxHelper(
|
||||
string Executable,
|
||||
IReadOnlyList<string> ReadOnlyPaths,
|
||||
IReadOnlyDictionary<string, string> Environment)
|
||||
{
|
||||
/// <summary>
|
||||
/// This application as the helper: its directory, and the .NET it runs on when that is
|
||||
/// not under <c>/usr</c>. <c>null</c> when there is no executable to call (a test host).
|
||||
/// </summary>
|
||||
public static SandboxHelper? ForThisProcess()
|
||||
{
|
||||
var executable = System.Environment.ProcessPath;
|
||||
if (string.IsNullOrEmpty(executable) || !File.Exists(executable))
|
||||
return null;
|
||||
var name = Path.GetFileNameWithoutExtension(executable);
|
||||
return name is "dotnet" or "testhost" ? null : For(executable);
|
||||
}
|
||||
|
||||
/// <summary>The helper at <paramref name="executable"/>, running on the same .NET as this process.</summary>
|
||||
public static SandboxHelper For(string executable)
|
||||
{
|
||||
var paths = new List<string> { Path.GetDirectoryName(Path.GetFullPath(executable))! };
|
||||
var environment = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
// .NET reserves address space by the gigabyte; under a memory limit it would not start.
|
||||
["DOTNET_GCHeapHardLimit"] = "0x20000000",
|
||||
["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1"
|
||||
};
|
||||
// <root>/shared/Microsoft.NETCore.App/<version>/ — the root is three levels up.
|
||||
var runtime = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();
|
||||
var root = Path.GetFullPath(Path.Combine(runtime, "..", "..", ".."));
|
||||
if (!root.StartsWith("/usr/", StringComparison.Ordinal) && Directory.Exists(root))
|
||||
paths.Add(root);
|
||||
environment["DOTNET_ROOT"] = root;
|
||||
return new SandboxHelper(Path.GetFullPath(executable), paths, environment);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>#654: The limits of one run.</summary>
|
||||
@@ -41,7 +110,10 @@ public sealed record SandboxLimits
|
||||
{
|
||||
public TimeSpan Timeout { get; init; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>Address space per process.</summary>
|
||||
/// <summary>
|
||||
/// Address space per process; 0 for none. A JavaScript runtime (Claude Code) reserves
|
||||
/// tens of gigabytes it never uses and does not start under any sensible limit.
|
||||
/// </summary>
|
||||
public long MaxMemoryBytes { get; init; } = 2L * 1024 * 1024 * 1024;
|
||||
|
||||
/// <summary>Processes and threads the run may have at once — a fork bomb stops here.</summary>
|
||||
@@ -109,6 +181,14 @@ public static class SandboxPaths
|
||||
{
|
||||
public const string Work = "/work";
|
||||
|
||||
/// <summary>#712: Where the launcher finds what the runner hands it.</summary>
|
||||
public const string Run = "/run/clawd";
|
||||
public const string EgressSocket = Run + "/egress.sock";
|
||||
public const string SecretFile = Run + "/env.json";
|
||||
|
||||
/// <summary>#712: The proxy's port on the sandbox's own loopback.</summary>
|
||||
public const int ProxyPort = 3128;
|
||||
|
||||
/// <summary>
|
||||
/// In the workspace, read-only by default: <c>scripts/</c> holds approved scripts and their
|
||||
/// manifests (#695) — what is checked must stay what runs.
|
||||
|
||||
Reference in New Issue
Block a user