diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-08-10 01:39:40 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-08-10 01:39:40 +0900 |
| commit | 60d558e9a7eb1944fc58ee88e121a5db5248a332 (patch) | |
| tree | a459d0789e177247af68fbf65e65d326633280dd /src/Agent.Core | |
| parent | 6394dfcc50fb1804ab07be309d940017eb11ef7d (diff) | |
fix(agent): fail fast on sandbox policy denial
Treat Policy Engine denials and UnauthorizedAccessException results as terminal tool failures by adding RetryAllowed metadata to ToolExecutionResult.
Stop the Agent loop immediately after recording the denied tool result, transition the Task to failed, and run normal Task resource cleanup instead of returning the same failure to the LLM for another iteration. Correctable validation and process failures retain the existing bounded retry behavior.
Expose RetryAllowed in llm.jsonl and document the fail-fast security invariant in Specifications, Design, and ImplementationPlan.
Tests: Agent.Core.Tests 50/50 passed with skip 0; Release build passed with 0 warnings and 0 errors. The full Windows suite remained environment-blocked outside an elevated token: 14 tests failed while creating/deriving the fixed AppContainer or creating symlinks; 11 passed and none were skipped.
Release: refreshed the win-x64 self-contained folder binaries, publish manifest, ZIP, and checksum while preserving the local agent.yaml. The clean ZIP contains no runtime logs or Task state.
Diffstat (limited to 'src/Agent.Core')
| -rw-r--r-- | src/Agent.Core/Runtime/AgentRuntime.cs | 8 | ||||
| -rw-r--r-- | src/Agent.Core/Tools/InternalToolDispatcher.cs | 2 | ||||
| -rw-r--r-- | src/Agent.Core/Tools/ToolPolicy.cs | 7 |
3 files changed, 14 insertions, 3 deletions
diff --git a/src/Agent.Core/Runtime/AgentRuntime.cs b/src/Agent.Core/Runtime/AgentRuntime.cs index c2fa568..0fcf7d0 100644 --- a/src/Agent.Core/Runtime/AgentRuntime.cs +++ b/src/Agent.Core/Runtime/AgentRuntime.cs @@ -63,7 +63,11 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag { toolResult = await _tools.DispatchAsync(taskId, call.Function.Name, call.Function.Arguments, cancellationToken).ConfigureAwait(false); } - catch (Exception ex) when (ex is ToolValidationException or ArgumentException or UnauthorizedAccessException) + catch (UnauthorizedAccessException ex) + { + toolResult = new(false, ex.Message, false, $"{call.Function.Name}:{ex.GetType().Name}:{ex.Message}", RetryAllowed: false); + } + catch (Exception ex) when (ex is ToolValidationException or ArgumentException) { toolResult = new(false, ex.Message, false, $"{call.Function.Name}:{ex.GetType().Name}:{ex.Message}"); } @@ -72,6 +76,8 @@ public sealed class AgentRuntime(TaskRegistry registry, IOllamaClient ollama, Ag else unresolvedToolFailures[call.Function.Name] = toolResult.FailureFingerprint ?? toolResult.Output; if (toolResult.Success) successfulToolCalls[call.Function.Name] = successfulToolCalls.GetValueOrDefault(call.Function.Name) + 1; _observer.ToolResult(taskId, call.Function.Name, toolResult); + if (!toolResult.Success && !toolResult.RetryAllowed) + throw new InvalidOperationException($"Tool '{call.Function.Name}' was denied by sandbox policy and will not be retried: {toolResult.Output}"); failureFingerprint = toolResult.FailureFingerprint ?? failureFingerprint; messages.Add(new OllamaMessage("tool", toolResult.Output, ToolName: call.Function.Name)); } diff --git a/src/Agent.Core/Tools/InternalToolDispatcher.cs b/src/Agent.Core/Tools/InternalToolDispatcher.cs index 7302c1f..0dca5df 100644 --- a/src/Agent.Core/Tools/InternalToolDispatcher.cs +++ b/src/Agent.Core/Tools/InternalToolDispatcher.cs @@ -11,7 +11,7 @@ public sealed class InternalToolDispatcher(IToolPolicyEngine policy, IInternalTo { InternalToolValidator.Validate(toolName, arguments); ToolPolicyDecision decision = policy.Evaluate(toolName, arguments); - if (!decision.Allowed) return new(false, decision.Reason, false, $"policy:{toolName}:{decision.Reason}"); + if (!decision.Allowed) return new(false, decision.Reason, false, $"policy:{toolName}:{decision.Reason}", RetryAllowed: false); return await backend.ExecuteAsync(taskId, toolName, arguments, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Agent.Core/Tools/ToolPolicy.cs b/src/Agent.Core/Tools/ToolPolicy.cs index c9f216a..d478a2c 100644 --- a/src/Agent.Core/Tools/ToolPolicy.cs +++ b/src/Agent.Core/Tools/ToolPolicy.cs @@ -20,7 +20,12 @@ public interface IInternalToolBackend Task EndTaskAsync(Guid taskId, bool success, CancellationToken cancellationToken) => Task.CompletedTask; } -public sealed record ToolExecutionResult(bool Success, string Output, bool ExternalEffect, string? FailureFingerprint = null); +public sealed record ToolExecutionResult( + bool Success, + string Output, + bool ExternalEffect, + string? FailureFingerprint = null, + bool RetryAllowed = true); public sealed class DenyByDefaultPolicy(IReadOnlySet<string>? allowedTools = null) : IToolPolicyEngine { |
