Files
RichardandClaude Opus 4.8 bb103a578a 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>
2026-07-23 20:29:47 +02:00

84 lines
2.9 KiB
C#

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" }));
}
}
}