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:
Richard
2026-09-25 22:46:49 +02:00
co-authored by Claude Opus 5.5
parent 7d6c2c9495
commit 367ea387f6
40 changed files with 3428 additions and 29 deletions
@@ -173,6 +173,79 @@ public sealed class SandboxDefinitionTests
line.ShouldNotContain("--clearenv");
}
// ═══════════════════ #712: launcher, network, secrets, mounts ═══════════════════
private static readonly SandboxHelper Helper = new(
"/opt/clawd/ClawdDotNet", ["/opt/clawd"], new Dictionary<string, string> { ["DOTNET_ROOT"] = "/usr/lib/dotnet" });
private static IReadOnlyList<string> LaunchLine(SandboxRequest request)
=> BwrapSandbox.CommandLine("/usr/bin/bwrap", "/tmp/f", request, HostLayout.MergedUsr, BwrapFeatures.All,
Helper, new LaunchFiles("/tmp/p/egress.sock", "/tmp/p/env.json"));
private static SandboxRequest ForeignRequest() => new()
{
Executable = "/opt/claude/claude",
Arguments = ["-p"],
WorkspacePath = Workspace,
Egress = ["api.anthropic.com"],
SecretEnvironment = new Dictionary<string, string> { ["ANTHROPIC_API_KEY"] = "sk-ant-secret" },
Mounts = [new SandboxMount("/srv/agent/home", "/home/agent", Writable: true), new SandboxMount("/srv/run/private", "/run/agent")]
};
[Fact]
public void A_run_with_network_or_secrets_starts_behind_the_launcher()
{
var line = LaunchLine(ForeignRequest());
line.TakeLast(9).ShouldBe(["/opt/clawd/ClawdDotNet", "--sandbox-launch",
"--egress", SandboxPaths.EgressSocket, "--secrets", SandboxPaths.SecretFile, "--", "/opt/claude/claude", "-p"]);
HasSequence(line, "--bind", "/tmp/p/egress.sock", SandboxPaths.EgressSocket).ShouldBeTrue();
HasSequence(line, "--ro-bind", "/tmp/p/env.json", SandboxPaths.SecretFile).ShouldBeTrue();
HasSequence(line, "--ro-bind", "/opt/clawd", "/opt/clawd").ShouldBeTrue();
HasSequence(line, "--setenv", "HTTPS_PROXY", $"http://127.0.0.1:{SandboxPaths.ProxyPort}").ShouldBeTrue();
}
[Fact]
public void The_sandbox_keeps_its_own_network_even_with_egress()
{
var line = LaunchLine(ForeignRequest());
line.ShouldContain("--unshare-all");
line.ShouldNotContain("--share-net");
}
[Fact]
public void A_secret_is_never_on_the_command_line()
=> LaunchLine(ForeignRequest()).ShouldNotContain(a => a.Contains("sk-ant-secret"));
[Fact]
public void Mounts_come_at_their_own_places_and_only_there()
{
var line = LaunchLine(ForeignRequest());
HasSequence(line, "--bind", Path.GetFullPath("/srv/agent/home"), "/home/agent").ShouldBeTrue();
HasSequence(line, "--ro-bind", Path.GetFullPath("/srv/run/private"), "/run/agent").ShouldBeTrue();
foreach (var target in new[] { "/work/.claude", "/work", "relative", "/home/../etc" })
Should.Throw<ArgumentException>(() => LaunchLine(ForeignRequest() with { Mounts = [new SandboxMount("/srv/x", target)] }), target);
}
[Fact]
public void Without_the_launcher_such_a_run_is_refused_not_run_bare()
=> Should.Throw<ArgumentException>(() => BwrapSandbox.CommandLine("/usr/bin/bwrap", "/tmp/f", ForeignRequest(),
HostLayout.MergedUsr, BwrapFeatures.All));
[Fact]
public void No_memory_limit_is_written_as_unlimited()
{
var line = Line(new SandboxRequest
{
Executable = "true", WorkspacePath = Workspace, Limits = new SandboxLimits { MaxMemoryBytes = 0 }
});
line[4].ShouldBe("unlimited");
}
[Theory]
[InlineData("in.mp3", "/work/in.mp3")]
[InlineData("YTTranscript/a b.m4a", "/work/YTTranscript/a b.m4a")]