Agenten-Kostendeckel: LoopGuard + PermissionGate aus ClawdDotNet uebernommen
- Neu PolyTrader.Core/Agents: AgentBudget (Steps/Tokens/Timeout), LoopGuard (thread-safe, AgentBudgetExceededException mit Kind), PermissionGate (Tool-Allow-List, null = alle erlaubt). Aus ClawdDotNet portiert, NICHT als Abhaengigkeit (.NET 10 vs 8). - SupervisorAgent nutzt LoopGuard (Default-Steps = MaxIterations=8, rueckwaerts- kompatibel) + PermissionGate (Allow-List = angebotene Tools; nicht freigegebene Calls liefern Fehlertext statt Ausfuehrung). SupervisorProfile.Budget ueber- schreibt den Deckel. Abbruch graceful mit Grund (Steps/Tokens/Zeit). - Tests: AgentGuardTests (LoopGuard/PermissionGate) + Token-Abbruch am Agenten. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
039bc240f8
commit
bb103a578a
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using PolyTrader.Core.Agents;
|
||||
using Xunit;
|
||||
|
||||
namespace PolyTrader.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Sicherheitsnetz für die aus ClawdDotNet übernommenen Agenten-Bausteine: LoopGuard (Kostendeckel
|
||||
/// Schritte/Tokens/Zeit) und PermissionGate (Tool-Allow-List).
|
||||
/// </summary>
|
||||
public class AgentGuardTests
|
||||
{
|
||||
// ----- LoopGuard -----
|
||||
|
||||
[Fact]
|
||||
public void LoopGuard_throws_when_step_limit_exceeded()
|
||||
{
|
||||
var guard = new LoopGuard(new AgentBudget { MaxSteps = 2, MaxTokens = 1_000_000, Timeout = TimeSpan.FromMinutes(5) });
|
||||
|
||||
guard.RecordStep(); // 1
|
||||
guard.RecordStep(); // 2
|
||||
var ex = Assert.Throws<AgentBudgetExceededException>(() => guard.RecordStep()); // 3 -> Abbruch
|
||||
Assert.Equal(AgentBudgetKind.Steps, ex.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoopGuard_throws_when_token_budget_exceeded()
|
||||
{
|
||||
var guard = new LoopGuard(new AgentBudget { MaxSteps = 100, MaxTokens = 100, Timeout = TimeSpan.FromMinutes(5) });
|
||||
|
||||
guard.RecordTokens(60);
|
||||
var ex = Assert.Throws<AgentBudgetExceededException>(() => guard.RecordTokens(60)); // 120 > 100
|
||||
Assert.Equal(AgentBudgetKind.Tokens, ex.Kind);
|
||||
Assert.Equal(120, guard.Tokens);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoopGuard_ignores_non_positive_token_counts()
|
||||
{
|
||||
var guard = new LoopGuard(new AgentBudget { MaxTokens = 10 });
|
||||
guard.RecordTokens(0);
|
||||
guard.RecordTokens(-5);
|
||||
Assert.Equal(0, guard.Tokens);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoopGuard_throws_when_time_budget_exceeded()
|
||||
{
|
||||
var guard = new LoopGuard(new AgentBudget { MaxSteps = 100, MaxTokens = 1_000_000, Timeout = TimeSpan.FromMilliseconds(10) });
|
||||
|
||||
Thread.Sleep(40);
|
||||
var ex = Assert.Throws<AgentBudgetExceededException>(() => guard.ThrowIfExpired());
|
||||
Assert.Equal(AgentBudgetKind.Time, ex.Kind);
|
||||
}
|
||||
|
||||
// ----- PermissionGate -----
|
||||
|
||||
[Fact]
|
||||
public void PermissionGate_allows_listed_tool_case_insensitive()
|
||||
{
|
||||
var gate = new PermissionGate();
|
||||
var allowed = new[] { "read_logs", "query_trades" };
|
||||
|
||||
Assert.True(gate.IsAllowed("READ_LOGS", allowed));
|
||||
Assert.False(gate.IsAllowed("place_order", allowed));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PermissionGate_null_allowlist_permits_everything()
|
||||
{
|
||||
var gate = new PermissionGate();
|
||||
Assert.True(gate.IsAllowed("anything", null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PermissionGate_enforce_throws_for_denied_tool()
|
||||
{
|
||||
var gate = new PermissionGate();
|
||||
Assert.Throws<ToolAccessDeniedException>(() => gate.Enforce("place_order", new[] { "read_logs" }));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user