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
@@ -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")]
|
||||
|
||||
Reference in New Issue
Block a user