fix: auto-allow infrastructure tool hooks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-29 22:01:56 +02:00
co-authored by Copilot
parent 4726e2acea
commit 66b2a94977
2 changed files with 50 additions and 2 deletions
@@ -7,9 +7,19 @@ namespace Aryx.AgentHost.Services;
internal static class CopilotSessionHooks
{
private const string AskUserToolName = "ask_user";
private const string AllowDecision = "allow";
private const string AskDecision = "ask";
private const string DenyDecision = "deny";
private const string HandoffToolPrefix = "handoff_to_";
private const string ReportIntentToolName = "report_intent";
private const string TaskCompleteToolName = "task_complete";
private static readonly HashSet<string> AlwaysAllowedToolNames = new(StringComparer.OrdinalIgnoreCase)
{
AskUserToolName,
ReportIntentToolName,
TaskCompleteToolName,
};
private static readonly JsonSerializerOptions HookJsonOptions = new(JsonSerializerDefaults.Web)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
@@ -216,11 +226,20 @@ internal static class CopilotSessionHooks
PatternAgentDefinitionDto agentDefinition,
PreToolUseHookInput input)
{
string? toolName = Normalize(input.ToolName);
if (IsAlwaysAllowedTool(toolName))
{
return new PreToolUseHookOutput
{
PermissionDecision = AllowDecision,
};
}
bool requiresApproval = CopilotApprovalCoordinator.RequiresToolCallApproval(
command.Pattern.ApprovalPolicy,
agentDefinition.Id,
Normalize(input.ToolName),
Normalize(input.ToolName));
toolName,
toolName);
return new PreToolUseHookOutput
{
@@ -262,6 +281,14 @@ internal static class CopilotSessionHooks
private static string SerializeHookValue(object? value)
=> JsonSerializer.Serialize(value, HookJsonOptions);
private static bool IsAlwaysAllowedTool(string? toolName)
{
string? normalizedToolName = Normalize(toolName);
return normalizedToolName is not null
&& (AlwaysAllowedToolNames.Contains(normalizedToolName)
|| normalizedToolName.StartsWith(HandoffToolPrefix, StringComparison.OrdinalIgnoreCase));
}
private static string? Normalize(string? value)
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
@@ -106,6 +106,27 @@ public sealed class CopilotSessionHooksTests
Assert.Single(runner.Invocations);
}
[Theory]
[InlineData("ask_user")]
[InlineData("report_intent")]
[InlineData("task_complete")]
[InlineData("handoff_to_2")]
[InlineData("handoff_to_specialist")]
public async Task Create_PreToolUseAutoAllowsInfrastructureTools(string toolName)
{
RunTurnCommandDto command = CreateCommandWithToolApproval();
SessionHooks hooks = CopilotSessionHooks.Create(command, command.Pattern.Agents[0], ResolvedHookSet.Empty, new RecordingHookCommandRunner());
PreToolUseHookOutput? decision = await hooks.OnPreToolUse!(
new PreToolUseHookInput
{
ToolName = toolName,
},
null!);
Assert.Equal("allow", decision?.PermissionDecision);
}
[Fact]
public async Task Create_RunsConfiguredNonPreToolHooks()
{