mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-10 13:48:44 +02:00
feat: add tool-specific approval overrides
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -30,6 +30,7 @@ public sealed class PatternDefinitionDto
|
||||
public sealed class ApprovalPolicyDto
|
||||
{
|
||||
public IReadOnlyList<ApprovalCheckpointRuleDto> Rules { get; init; } = [];
|
||||
public IReadOnlyList<string> AutoApprovedToolNames { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class ApprovalCheckpointRuleDto
|
||||
|
||||
@@ -242,7 +242,9 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
Func<ApprovalRequestedEventDto, Task> onApproval,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!RequiresApproval(command.Pattern.ApprovalPolicy, "tool-call", agent.Id))
|
||||
TryGetApprovalToolName(request, out string? toolName);
|
||||
|
||||
if (!RequiresToolCallApproval(command.Pattern.ApprovalPolicy, agent.Id, toolName))
|
||||
{
|
||||
return new PermissionRequestResult
|
||||
{
|
||||
@@ -266,7 +268,7 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
|
||||
try
|
||||
{
|
||||
await onApproval(BuildPermissionApprovalEvent(command, agent, request, invocation, approvalId))
|
||||
await onApproval(BuildPermissionApprovalEvent(command, agent, request, invocation, approvalId, toolName))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
using CancellationTokenRegistration registration = cancellationToken.Register(
|
||||
@@ -383,12 +385,13 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ApprovalRequestedEventDto BuildPermissionApprovalEvent(
|
||||
internal static ApprovalRequestedEventDto BuildPermissionApprovalEvent(
|
||||
RunTurnCommandDto command,
|
||||
PatternAgentDefinitionDto agent,
|
||||
PermissionRequest request,
|
||||
PermissionInvocation invocation,
|
||||
string approvalId)
|
||||
string approvalId,
|
||||
string? toolName)
|
||||
{
|
||||
string permissionKind = string.IsNullOrWhiteSpace(request.Kind)
|
||||
? "tool access"
|
||||
@@ -397,6 +400,19 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
string? sessionId = string.IsNullOrWhiteSpace(invocation.SessionId)
|
||||
? null
|
||||
: invocation.SessionId.Trim();
|
||||
string? normalizedToolName = string.IsNullOrWhiteSpace(toolName)
|
||||
? null
|
||||
: toolName.Trim();
|
||||
string title = normalizedToolName is null
|
||||
? $"Approve {permissionKind}"
|
||||
: $"Approve {normalizedToolName}";
|
||||
string detail = normalizedToolName is null
|
||||
? sessionId is null
|
||||
? $"{agentName} requested {permissionKind} permission."
|
||||
: $"{agentName} requested {permissionKind} permission for Copilot session {sessionId}."
|
||||
: sessionId is null
|
||||
? $"{agentName} requested {permissionKind} permission for tool \"{normalizedToolName}\"."
|
||||
: $"{agentName} requested {permissionKind} permission for tool \"{normalizedToolName}\" in Copilot session {sessionId}.";
|
||||
|
||||
return new ApprovalRequestedEventDto
|
||||
{
|
||||
@@ -407,44 +423,69 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner
|
||||
ApprovalKind = "tool-call",
|
||||
AgentId = string.IsNullOrWhiteSpace(agent.Id) ? null : agent.Id,
|
||||
AgentName = string.IsNullOrWhiteSpace(agentName) ? null : agentName,
|
||||
ToolName = normalizedToolName,
|
||||
PermissionKind = permissionKind,
|
||||
Title = $"Approve {permissionKind}",
|
||||
Detail = sessionId is null
|
||||
? $"{agentName} requested {permissionKind} permission."
|
||||
: $"{agentName} requested {permissionKind} permission for Copilot session {sessionId}.",
|
||||
Title = title,
|
||||
Detail = detail,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool RequiresApproval(
|
||||
internal static bool RequiresToolCallApproval(
|
||||
ApprovalPolicyDto? approvalPolicy,
|
||||
string checkpointKind,
|
||||
string agentId)
|
||||
string agentId,
|
||||
string? toolName)
|
||||
{
|
||||
if (approvalPolicy?.Rules is null || approvalPolicy.Rules.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool matchesCheckpoint = false;
|
||||
foreach (ApprovalCheckpointRuleDto rule in approvalPolicy.Rules)
|
||||
{
|
||||
if (!string.Equals(rule.Kind, checkpointKind, StringComparison.OrdinalIgnoreCase))
|
||||
if (!string.Equals(rule.Kind, "tool-call", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rule.AgentIds.Count == 0)
|
||||
{
|
||||
return true;
|
||||
matchesCheckpoint = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (rule.AgentIds.Any(candidate =>
|
||||
string.Equals(candidate, agentId, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return true;
|
||||
matchesCheckpoint = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
if (!matchesCheckpoint)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(toolName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return !approvalPolicy.AutoApprovedToolNames.Any(candidate =>
|
||||
string.Equals(candidate, toolName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
internal static bool TryGetApprovalToolName(PermissionRequest request, out string? toolName)
|
||||
{
|
||||
toolName = request switch
|
||||
{
|
||||
PermissionRequestMcp mcp when !string.IsNullOrWhiteSpace(mcp.ToolName) => mcp.ToolName.Trim(),
|
||||
PermissionRequestCustomTool customTool when !string.IsNullOrWhiteSpace(customTool.ToolName) => customTool.ToolName.Trim(),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
return !string.IsNullOrWhiteSpace(toolName);
|
||||
}
|
||||
|
||||
private static string CreateApprovalRequestId()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Eryx.AgentHost.Contracts;
|
||||
using Eryx.AgentHost.Services;
|
||||
using GitHub.Copilot.SDK;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Eryx.AgentHost.Tests;
|
||||
@@ -233,6 +234,100 @@ public sealed class CopilotWorkflowRunnerTests
|
||||
Assert.Equal("Real content", message.Content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequiresToolCallApproval_HonorsAutoApprovedToolNames()
|
||||
{
|
||||
ApprovalPolicyDto policy = new()
|
||||
{
|
||||
Rules =
|
||||
[
|
||||
new ApprovalCheckpointRuleDto
|
||||
{
|
||||
Kind = "tool-call",
|
||||
AgentIds = ["agent-1"],
|
||||
},
|
||||
],
|
||||
AutoApprovedToolNames = ["lsp_ts_hover"],
|
||||
};
|
||||
|
||||
Assert.False(CopilotWorkflowRunner.RequiresToolCallApproval(policy, "agent-1", "lsp_ts_hover"));
|
||||
Assert.True(CopilotWorkflowRunner.RequiresToolCallApproval(policy, "agent-1", "lsp_ts_definition"));
|
||||
Assert.True(CopilotWorkflowRunner.RequiresToolCallApproval(policy, "agent-1", null));
|
||||
Assert.False(CopilotWorkflowRunner.RequiresToolCallApproval(policy, "agent-2", "lsp_ts_definition"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetApprovalToolName_ReadsMcpAndCustomToolRequests()
|
||||
{
|
||||
Assert.True(
|
||||
CopilotWorkflowRunner.TryGetApprovalToolName(
|
||||
new PermissionRequestMcp
|
||||
{
|
||||
Kind = "mcp",
|
||||
ServerName = "Git MCP",
|
||||
ToolName = "git.status",
|
||||
ToolTitle = "Git Status",
|
||||
ReadOnly = true,
|
||||
},
|
||||
out string? mcpToolName));
|
||||
Assert.Equal("git.status", mcpToolName);
|
||||
|
||||
Assert.True(
|
||||
CopilotWorkflowRunner.TryGetApprovalToolName(
|
||||
new PermissionRequestCustomTool
|
||||
{
|
||||
Kind = "custom tool",
|
||||
ToolName = "lsp_ts_hover",
|
||||
ToolDescription = "Hover information",
|
||||
},
|
||||
out string? customToolName));
|
||||
Assert.Equal("lsp_ts_hover", customToolName);
|
||||
|
||||
Assert.False(
|
||||
CopilotWorkflowRunner.TryGetApprovalToolName(
|
||||
new PermissionRequestShell
|
||||
{
|
||||
Kind = "shell",
|
||||
FullCommandText = "git status",
|
||||
Intention = "Inspect repository state",
|
||||
Commands = [],
|
||||
PossiblePaths = [],
|
||||
PossibleUrls = [],
|
||||
HasWriteFileRedirection = false,
|
||||
CanOfferSessionApproval = false,
|
||||
},
|
||||
out string? shellToolName));
|
||||
Assert.Null(shellToolName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPermissionApprovalEvent_IncludesToolContextWhenKnown()
|
||||
{
|
||||
ApprovalRequestedEventDto approvalEvent = CopilotWorkflowRunner.BuildPermissionApprovalEvent(
|
||||
new RunTurnCommandDto
|
||||
{
|
||||
RequestId = "turn-1",
|
||||
SessionId = "session-1",
|
||||
},
|
||||
CreateAgent("agent-1", "Primary"),
|
||||
new PermissionRequestCustomTool
|
||||
{
|
||||
Kind = "custom tool",
|
||||
ToolName = "lsp_ts_hover",
|
||||
ToolDescription = "Hover information",
|
||||
},
|
||||
new PermissionInvocation
|
||||
{
|
||||
SessionId = "copilot-session-1",
|
||||
},
|
||||
"approval-1",
|
||||
"lsp_ts_hover");
|
||||
|
||||
Assert.Equal("lsp_ts_hover", approvalEvent.ToolName);
|
||||
Assert.Equal("Approve lsp_ts_hover", approvalEvent.Title);
|
||||
Assert.Contains("tool \"lsp_ts_hover\"", approvalEvent.Detail);
|
||||
}
|
||||
|
||||
private static PatternAgentDefinitionDto CreateAgent(string id, string name)
|
||||
{
|
||||
return new PatternAgentDefinitionDto
|
||||
|
||||
Reference in New Issue
Block a user