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
@@ -0,0 +1,142 @@
|
||||
using ClawdDotNet.Core.Accounting;
|
||||
using ClawdDotNet.Core.Engine;
|
||||
using ClawdDotNet.Core.Mcp;
|
||||
using ClawdDotNet.Core.Runtimes;
|
||||
using ClawdDotNet.Core.Sandbox;
|
||||
using ClawdDotNet.Core.Storage;
|
||||
using ClawdDotNet.Core.Tests.Infrastructure;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Core.Tests.Runtimes;
|
||||
|
||||
/// <summary>
|
||||
/// #712: A foreign agent in the engine — it goes to the runner instead of the model, and
|
||||
/// everything around a run (usage, chat, the refusal without a runner) stays the same.
|
||||
/// </summary>
|
||||
public sealed class ForeignRunEngineTests : IDisposable
|
||||
{
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), "clawd-tests", "foreign-engine-" + Guid.NewGuid().ToString("N")[..8]);
|
||||
private readonly SqliteUsageRepository _usage;
|
||||
private readonly EngineFixture _fixture;
|
||||
|
||||
public ForeignRunEngineTests()
|
||||
{
|
||||
_usage = new SqliteUsageRepository(new SqliteStorage(Path.Combine(_root, "state.db")));
|
||||
_fixture = new EngineFixture(usage: _usage);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SqliteStorage.ReleasePooledConnections(_root);
|
||||
try { Directory.Delete(_root, recursive: true); }
|
||||
catch { /* cleanup is secondary */ }
|
||||
}
|
||||
|
||||
private sealed class ScriptedSandbox(string output) : ISandboxRunner
|
||||
{
|
||||
public SandboxRequest? Request { get; private set; }
|
||||
public bool IsAvailable => true;
|
||||
public string? UnavailableReason => null;
|
||||
|
||||
public Task<SandboxResult> RunAsync(SandboxRequest request, CancellationToken ct)
|
||||
{
|
||||
Request = request;
|
||||
return Task.FromResult(new SandboxResult { ExitCode = 0, StandardOutput = output });
|
||||
}
|
||||
}
|
||||
|
||||
private Core.Config.AgentConfig ForeignAgent(string kind = RuntimeKinds.Command)
|
||||
{
|
||||
var agent = _fixture.AddAgent("coder");
|
||||
agent.AgentDir = Path.Combine(_root, "Agents", "Agent-coder");
|
||||
agent.WorkspacePath = Path.Combine(agent.AgentDir, "Workspace");
|
||||
Directory.CreateDirectory(agent.WorkspacePath);
|
||||
agent.Runtime = new AgentRuntimeConfig { Kind = kind, Command = "/usr/bin/answer" };
|
||||
return agent;
|
||||
}
|
||||
|
||||
private ScriptedSandbox UseSandbox(string output, ClaudeCodeSettings? claude = null)
|
||||
{
|
||||
var sandbox = new ScriptedSandbox(output);
|
||||
_fixture.Engine.ForeignRunner = new ForeignRunner(sandbox,
|
||||
[new CommandRuntime(), new ClaudeCodeRuntime(() => claude ?? new ClaudeCodeSettings())],
|
||||
new McpGrants(), null, null, NullLogger.Instance);
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_foreign_agent_does_not_reach_the_model()
|
||||
{
|
||||
var agent = ForeignAgent();
|
||||
var sandbox = UseSandbox("answer from the command");
|
||||
|
||||
var result = await _fixture.Engine.RunAsync(agent, "Do it.", "test-instance", CancellationToken.None);
|
||||
|
||||
result.Status.ShouldBe(AgentRunStatus.Completed);
|
||||
result.FinalMessage.ShouldBe("answer from the command");
|
||||
_fixture.Client.ReceivedRequests.ShouldBeEmpty();
|
||||
sandbox.Request!.StandardInput!.ShouldContain("Do it.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_briefing_is_the_one_a_native_run_gets()
|
||||
{
|
||||
var agent = ForeignAgent();
|
||||
var sandbox = UseSandbox("ok");
|
||||
|
||||
await _fixture.Engine.RunAsync(agent, "Do it.", "test-instance", CancellationToken.None);
|
||||
|
||||
// The runner removed the run's files; the briefing went to the sandbox through them.
|
||||
sandbox.Request!.Environment["CLAWD_BRIEFING_FILE"].ShouldBe("/run/agent/briefing.md");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Without_a_runner_the_agent_does_not_fall_back_to_the_native_engine()
|
||||
{
|
||||
var agent = ForeignAgent();
|
||||
|
||||
var result = await _fixture.Engine.RunAsync(agent, "Do it.", "test-instance", CancellationToken.None);
|
||||
|
||||
result.Status.ShouldBe(AgentRunStatus.Failed);
|
||||
result.FinalMessage!.ShouldContain("not set up");
|
||||
_fixture.Client.ReceivedRequests.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task A_subscription_run_is_recorded_with_its_tokens_and_marked()
|
||||
{
|
||||
var agent = ForeignAgent(RuntimeKinds.ClaudeCode);
|
||||
var claude = Path.Combine(_root, "claude");
|
||||
File.WriteAllText(claude, "");
|
||||
UseSandbox("""
|
||||
{"type":"system","subtype":"init","model":"claude-sonnet-5"}
|
||||
{"type":"result","subtype":"success","is_error":false,"num_turns":3,"result":"done","total_cost_usd":0.9,"usage":{"input_tokens":100,"cache_read_input_tokens":900,"output_tokens":50}}
|
||||
""", new ClaudeCodeSettings { Executable = claude, Auth = ClaudeCodeSettings.SubscriptionAuth, OAuthToken = "oat" });
|
||||
|
||||
await _fixture.Engine.RunAsync(agent, "Do it.", "test-instance", CancellationToken.None);
|
||||
|
||||
var usage = (await _usage.GetRecentAsync(5, CancellationToken.None)).Single();
|
||||
usage.Runtime.ShouldBe(RuntimeKinds.ClaudeCode);
|
||||
usage.OnSubscription.ShouldBeTrue();
|
||||
usage.CostUsd.ShouldBe(0m, "a subscription does not bill the run");
|
||||
usage.CostIsKnown.ShouldBeTrue();
|
||||
usage.PromptTokens.ShouldBe(1000);
|
||||
usage.CompletionTokens.ShouldBe(50);
|
||||
usage.Model.ShouldBe("claude-sonnet-5");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task In_a_chat_each_message_is_a_run_and_both_sides_are_shown()
|
||||
{
|
||||
var agent = ForeignAgent();
|
||||
UseSandbox("the answer");
|
||||
|
||||
await _fixture.Engine.ChatAsync(agent, "a question", "test-instance", CancellationToken.None);
|
||||
|
||||
_fixture.Engine.GetChatHistory("coder").Select(e => (e.Role, e.Content)).ShouldBe([
|
||||
("user", "a question"),
|
||||
("assistant", "the answer")
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user