mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-30 08:28:48 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0eac1da70c | ||
|
|
b8553c6593 | ||
|
|
26847cf00d | ||
|
|
fce0430091 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aryx",
|
"name": "aryx",
|
||||||
"version": "0.0.29",
|
"version": "0.0.30",
|
||||||
"description": "Orchestrator for Copilot-powered agent workflows across multiple projects.",
|
"description": "Orchestrator for Copilot-powered agent workflows across multiple projects.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "dist-electron/main/index.js",
|
"main": "dist-electron/main/index.js",
|
||||||
|
|||||||
@@ -678,6 +678,7 @@ public sealed class ApprovalRequestedEventDto : SidecarEventDto
|
|||||||
public string? AgentName { get; init; }
|
public string? AgentName { get; init; }
|
||||||
public string? ToolName { get; init; }
|
public string? ToolName { get; init; }
|
||||||
public string? PermissionKind { get; init; }
|
public string? PermissionKind { get; init; }
|
||||||
|
public string? ApprovalToolKey { get; init; }
|
||||||
public string Title { get; init; } = string.Empty;
|
public string Title { get; init; } = string.Empty;
|
||||||
public string? Detail { get; init; }
|
public string? Detail { get; init; }
|
||||||
public PermissionDetailDto? PermissionDetail { get; init; }
|
public PermissionDetailDto? PermissionDetail { get; init; }
|
||||||
|
|||||||
+171
-59
@@ -21,22 +21,33 @@ internal sealed class CopilotApprovalCoordinator
|
|||||||
private const string HookPermissionKind = "hook";
|
private const string HookPermissionKind = "hook";
|
||||||
private const string ToolCallingActivityType = "tool-calling";
|
private const string ToolCallingActivityType = "tool-calling";
|
||||||
|
|
||||||
private static readonly Dictionary<string, string> HookToolCategories = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly Dictionary<string, HookToolApprovalMapping> HookToolApprovals = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
["view"] = ReadPermissionKind,
|
["view"] = new(ReadPermissionKind, ReadPermissionKind),
|
||||||
["glob"] = ReadPermissionKind,
|
["show_file"] = new(ReadPermissionKind, ReadPermissionKind),
|
||||||
["grep"] = ReadPermissionKind,
|
["read_file"] = new(ReadPermissionKind, ReadPermissionKind),
|
||||||
["lsp"] = ReadPermissionKind,
|
["glob"] = new(ReadPermissionKind, ReadPermissionKind),
|
||||||
["edit"] = WritePermissionKind,
|
["grep"] = new(ReadPermissionKind, ReadPermissionKind),
|
||||||
["create"] = WritePermissionKind,
|
["rg"] = new(ReadPermissionKind, ReadPermissionKind),
|
||||||
["powershell"] = ShellPermissionKind,
|
["lsp"] = new(ReadPermissionKind, ReadPermissionKind),
|
||||||
["read_powershell"] = ShellPermissionKind,
|
["edit"] = new(WritePermissionKind, WritePermissionKind),
|
||||||
["write_powershell"] = ShellPermissionKind,
|
["create"] = new(WritePermissionKind, WritePermissionKind),
|
||||||
["stop_powershell"] = ShellPermissionKind,
|
["write_file"] = new(WritePermissionKind, WritePermissionKind),
|
||||||
["list_powershell"] = ShellPermissionKind,
|
["apply_patch"] = new(WritePermissionKind, WritePermissionKind),
|
||||||
["web_fetch"] = UrlPermissionKind,
|
["bash"] = new(ShellPermissionKind, ShellPermissionKind),
|
||||||
["web_search"] = UrlPermissionKind,
|
["read_bash"] = new(ShellPermissionKind, ShellPermissionKind),
|
||||||
["store_memory"] = MemoryPermissionKind,
|
["write_bash"] = new(ShellPermissionKind, ShellPermissionKind),
|
||||||
|
["stop_bash"] = new(ShellPermissionKind, ShellPermissionKind),
|
||||||
|
["list_bash"] = new(ShellPermissionKind, ShellPermissionKind),
|
||||||
|
["powershell"] = new(ShellPermissionKind, ShellPermissionKind),
|
||||||
|
["read_powershell"] = new(ShellPermissionKind, ShellPermissionKind),
|
||||||
|
["write_powershell"] = new(ShellPermissionKind, ShellPermissionKind),
|
||||||
|
["stop_powershell"] = new(ShellPermissionKind, ShellPermissionKind),
|
||||||
|
["list_powershell"] = new(ShellPermissionKind, ShellPermissionKind),
|
||||||
|
["web_fetch"] = new(UrlPermissionKind, WebFetchToolName),
|
||||||
|
["web_search"] = new(UrlPermissionKind, WebFetchToolName),
|
||||||
|
["store_memory"] = new(MemoryPermissionKind, StoreMemoryToolName),
|
||||||
|
["remember_fact"] = new(MemoryPermissionKind, StoreMemoryToolName),
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, PendingApprovalRequest> _pendingApprovals = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, PendingApprovalRequest> _pendingApprovals = new(StringComparer.Ordinal);
|
||||||
@@ -96,19 +107,22 @@ internal sealed class CopilotApprovalCoordinator
|
|||||||
Func<ApprovalRequestedEventDto, Task> onApproval,
|
Func<ApprovalRequestedEventDto, Task> onApproval,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
string? toolName = ResolveApprovalToolName(request, toolCalls);
|
ResolvedApprovalContext approval = ResolveApprovalContext(request, toolCalls, command.Tooling?.McpServers);
|
||||||
string? autoApprovedToolName = ResolveAutoApprovedToolName(request);
|
string? approvalCacheKey = ResolveApprovalCacheKey(approval.ToolName, approval.ApprovalToolKey);
|
||||||
string? mcpServerApprovalKey = ResolveMcpServerApprovalKey(request, command.Tooling?.McpServers);
|
|
||||||
string? approvalCacheKey = ResolveApprovalCacheKey(toolName, autoApprovedToolName);
|
|
||||||
|
|
||||||
AgentActivityEventDto? fileChangeActivity = BuildToolCallFileChangeActivity(command, agent, request, toolName);
|
AgentActivityEventDto? fileChangeActivity = BuildToolCallFileChangeActivity(command, agent, request, approval.ToolName);
|
||||||
if (fileChangeActivity is not null && onActivity is not null)
|
if (fileChangeActivity is not null && onActivity is not null)
|
||||||
{
|
{
|
||||||
await onActivity(fileChangeActivity).ConfigureAwait(false);
|
await onActivity(fileChangeActivity).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsToolApprovedForRequest(command.RequestId, approvalCacheKey)
|
if (IsToolApprovedForRequest(command.RequestId, approvalCacheKey)
|
||||||
|| !RequiresToolCallApproval(command.Workflow.Settings.ApprovalPolicy, agent.GetAgentId(), toolName, autoApprovedToolName, mcpServerApprovalKey))
|
|| !RequiresToolCallApproval(
|
||||||
|
command.Workflow.Settings.ApprovalPolicy,
|
||||||
|
agent.GetAgentId(),
|
||||||
|
approval.ToolName,
|
||||||
|
approval.ApprovalToolKey,
|
||||||
|
approval.McpServerApprovalKey))
|
||||||
{
|
{
|
||||||
return CreateApprovalResult(PermissionRequestResultKind.Approved);
|
return CreateApprovalResult(PermissionRequestResultKind.Approved);
|
||||||
}
|
}
|
||||||
@@ -127,7 +141,7 @@ internal sealed class CopilotApprovalCoordinator
|
|||||||
request,
|
request,
|
||||||
invocation,
|
invocation,
|
||||||
pending.ApprovalId,
|
pending.ApprovalId,
|
||||||
toolName))
|
approval.ToolName))
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
using CancellationTokenRegistration registration = cancellationToken.Register(
|
using CancellationTokenRegistration registration = cancellationToken.Register(
|
||||||
@@ -155,21 +169,22 @@ internal sealed class CopilotApprovalCoordinator
|
|||||||
string approvalId,
|
string approvalId,
|
||||||
string? toolName)
|
string? toolName)
|
||||||
{
|
{
|
||||||
string permissionKind = ResolvePermissionKind(request, command.Tooling?.McpServers);
|
ResolvedApprovalContext approval = ResolveApprovalContext(request, toolName, command.Tooling?.McpServers);
|
||||||
|
|
||||||
string agentId = agent.GetAgentId();
|
string agentId = agent.GetAgentId();
|
||||||
string agentName = agent.GetAgentName();
|
string agentName = agent.GetAgentName();
|
||||||
string? sessionId = NormalizeOptionalString(invocation.SessionId);
|
string? sessionId = NormalizeOptionalString(invocation.SessionId);
|
||||||
string? normalizedToolName = NormalizeOptionalString(toolName);
|
string? normalizedToolName = approval.ToolName;
|
||||||
|
string? displayToolName = normalizedToolName ?? NormalizeOptionalString(approval.ApprovalToolKey);
|
||||||
string? requestedUrl = request is PermissionRequestUrl urlRequest
|
string? requestedUrl = request is PermissionRequestUrl urlRequest
|
||||||
? NormalizeOptionalString(urlRequest.Url)
|
? NormalizeOptionalString(urlRequest.Url)
|
||||||
: null;
|
: null;
|
||||||
string title = normalizedToolName is null
|
string title = displayToolName is null
|
||||||
? $"Approve {permissionKind}"
|
? $"Approve {approval.PermissionKind}"
|
||||||
: $"Approve {normalizedToolName}";
|
: $"Approve {displayToolName}";
|
||||||
string detail = normalizedToolName is null
|
string detail = displayToolName is null
|
||||||
? $"{agentName} requested {permissionKind} permission"
|
? $"{agentName} requested {approval.PermissionKind} permission"
|
||||||
: $"{agentName} requested {permissionKind} permission for tool \"{normalizedToolName}\"";
|
: $"{agentName} requested {approval.PermissionKind} permission for tool \"{displayToolName}\"";
|
||||||
|
|
||||||
if (requestedUrl is not null)
|
if (requestedUrl is not null)
|
||||||
{
|
{
|
||||||
@@ -195,7 +210,8 @@ internal sealed class CopilotApprovalCoordinator
|
|||||||
AgentId = NormalizeOptionalString(agentId),
|
AgentId = NormalizeOptionalString(agentId),
|
||||||
AgentName = NormalizeOptionalString(agentName),
|
AgentName = NormalizeOptionalString(agentName),
|
||||||
ToolName = normalizedToolName,
|
ToolName = normalizedToolName,
|
||||||
PermissionKind = permissionKind,
|
PermissionKind = approval.PermissionKind,
|
||||||
|
ApprovalToolKey = NormalizeOptionalString(approval.ApprovalToolKey),
|
||||||
Title = title,
|
Title = title,
|
||||||
Detail = detail,
|
Detail = detail,
|
||||||
PermissionDetail = BuildPermissionDetail(request, command.Tooling?.McpServers),
|
PermissionDetail = BuildPermissionDetail(request, command.Tooling?.McpServers),
|
||||||
@@ -314,7 +330,7 @@ internal sealed class CopilotApprovalCoordinator
|
|||||||
ApprovalPolicyDto? approvalPolicy,
|
ApprovalPolicyDto? approvalPolicy,
|
||||||
string agentId,
|
string agentId,
|
||||||
string? toolName,
|
string? toolName,
|
||||||
string? autoApprovedToolName = null,
|
string? approvalToolKey = null,
|
||||||
string? mcpServerApprovalKey = null)
|
string? mcpServerApprovalKey = null)
|
||||||
{
|
{
|
||||||
if (approvalPolicy?.Rules is null || approvalPolicy.Rules.Count == 0)
|
if (approvalPolicy?.Rules is null || approvalPolicy.Rules.Count == 0)
|
||||||
@@ -333,7 +349,7 @@ internal sealed class CopilotApprovalCoordinator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !MatchesAutoApprovedTool(autoApprovedToolNames, toolName, autoApprovedToolName)
|
return !MatchesAutoApprovedTool(autoApprovedToolNames, toolName, approvalToolKey)
|
||||||
&& !MatchesAutoApprovedToolName(autoApprovedToolNames, mcpServerApprovalKey);
|
&& !MatchesAutoApprovedToolName(autoApprovedToolNames, mcpServerApprovalKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,9 +427,67 @@ internal sealed class CopilotApprovalCoordinator
|
|||||||
?? GetFallbackToolName(request);
|
?? GetFallbackToolName(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? ResolveAutoApprovedToolName(PermissionRequest request)
|
private static ResolvedApprovalContext ResolveApprovalContext(
|
||||||
|
PermissionRequest request,
|
||||||
|
ToolCallRegistry? toolCalls,
|
||||||
|
IReadOnlyList<RunTurnMcpServerConfigDto>? configuredMcpServers)
|
||||||
{
|
{
|
||||||
return GetFallbackToolName(request);
|
return ResolveApprovalContext(
|
||||||
|
request,
|
||||||
|
ResolveApprovalToolName(request, toolCalls),
|
||||||
|
configuredMcpServers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResolvedApprovalContext ResolveApprovalContext(
|
||||||
|
PermissionRequest request,
|
||||||
|
string? toolName,
|
||||||
|
IReadOnlyList<RunTurnMcpServerConfigDto>? configuredMcpServers)
|
||||||
|
{
|
||||||
|
string? normalizedToolName = NormalizeOptionalString(toolName);
|
||||||
|
return request switch
|
||||||
|
{
|
||||||
|
PermissionRequestShell => new(
|
||||||
|
normalizedToolName,
|
||||||
|
ShellPermissionKind,
|
||||||
|
ShellPermissionKind,
|
||||||
|
null),
|
||||||
|
PermissionRequestWrite => new(
|
||||||
|
normalizedToolName,
|
||||||
|
WritePermissionKind,
|
||||||
|
WritePermissionKind,
|
||||||
|
null),
|
||||||
|
PermissionRequestRead => new(
|
||||||
|
normalizedToolName,
|
||||||
|
ReadPermissionKind,
|
||||||
|
ReadPermissionKind,
|
||||||
|
null),
|
||||||
|
PermissionRequestUrl => new(
|
||||||
|
normalizedToolName,
|
||||||
|
UrlPermissionKind,
|
||||||
|
WebFetchToolName,
|
||||||
|
null),
|
||||||
|
PermissionRequestMemory => new(
|
||||||
|
normalizedToolName,
|
||||||
|
MemoryPermissionKind,
|
||||||
|
StoreMemoryToolName,
|
||||||
|
null),
|
||||||
|
PermissionRequestMcp => new(
|
||||||
|
normalizedToolName,
|
||||||
|
McpPermissionKind,
|
||||||
|
normalizedToolName,
|
||||||
|
ResolveMcpServerApprovalKey(request, configuredMcpServers)),
|
||||||
|
PermissionRequestCustomTool => new(
|
||||||
|
normalizedToolName,
|
||||||
|
CustomToolPermissionKind,
|
||||||
|
normalizedToolName,
|
||||||
|
null),
|
||||||
|
PermissionRequestHook => ResolveHookApprovalContext(normalizedToolName, configuredMcpServers),
|
||||||
|
_ => new(
|
||||||
|
normalizedToolName,
|
||||||
|
NormalizeOptionalString(request.Kind) ?? "tool access",
|
||||||
|
normalizedToolName,
|
||||||
|
ResolveMcpServerApprovalKey(request, configuredMcpServers)),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string McpServerApprovalPrefix = "mcp_server:";
|
private const string McpServerApprovalPrefix = "mcp_server:";
|
||||||
@@ -522,44 +596,56 @@ internal sealed class CopilotApprovalCoordinator
|
|||||||
PermissionRequestWrite => WritePermissionKind,
|
PermissionRequestWrite => WritePermissionKind,
|
||||||
PermissionRequestRead => ReadPermissionKind,
|
PermissionRequestRead => ReadPermissionKind,
|
||||||
PermissionRequestMemory => StoreMemoryToolName,
|
PermissionRequestMemory => StoreMemoryToolName,
|
||||||
PermissionRequestHook hook => ResolveHookToolCategory(hook.ToolName),
|
PermissionRequestHook hook => ResolveHookApprovalToolKey(hook.ToolName),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static string? ResolveHookToolCategory(string? toolName)
|
internal static string? ResolveHookToolCategory(string? toolName)
|
||||||
{
|
{
|
||||||
string? normalized = NormalizeOptionalString(toolName);
|
return TryResolveHookToolApproval(toolName, out HookToolApprovalMapping? mapping) && mapping is not null
|
||||||
if (normalized is null)
|
? mapping.PermissionKind
|
||||||
{
|
: null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return HookToolCategories.TryGetValue(normalized, out string? category) ? category : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolvePermissionKind(
|
internal static string? ResolveHookApprovalToolKey(string? toolName)
|
||||||
PermissionRequest request,
|
{
|
||||||
|
return TryResolveHookToolApproval(toolName, out HookToolApprovalMapping? mapping) && mapping is not null
|
||||||
|
? mapping.ApprovalToolKey
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ResolvedApprovalContext ResolveHookApprovalContext(
|
||||||
|
string? toolName,
|
||||||
IReadOnlyList<RunTurnMcpServerConfigDto>? configuredMcpServers)
|
IReadOnlyList<RunTurnMcpServerConfigDto>? configuredMcpServers)
|
||||||
{
|
{
|
||||||
string permissionKind = string.IsNullOrWhiteSpace(request.Kind)
|
string? normalizedToolName = NormalizeOptionalString(toolName);
|
||||||
? "tool access"
|
if (normalizedToolName is null)
|
||||||
: request.Kind.Trim();
|
|
||||||
|
|
||||||
if (request is not PermissionRequestHook hook)
|
|
||||||
{
|
{
|
||||||
return permissionKind;
|
return new ResolvedApprovalContext(null, HookPermissionKind, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
string? resolvedCategory = ResolveHookToolCategory(hook.ToolName);
|
if (TryResolveHookToolApproval(normalizedToolName, out HookToolApprovalMapping? mapping) && mapping is not null)
|
||||||
if (resolvedCategory is not null)
|
|
||||||
{
|
{
|
||||||
return resolvedCategory;
|
return new ResolvedApprovalContext(
|
||||||
|
normalizedToolName,
|
||||||
|
mapping.PermissionKind,
|
||||||
|
mapping.ApprovalToolKey,
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResolveHookMcpServerName(hook.ToolName, configuredMcpServers) is not null
|
string? mcpServerApprovalKey = ResolveHookMcpServerApprovalKey(normalizedToolName, configuredMcpServers);
|
||||||
? McpPermissionKind
|
return mcpServerApprovalKey is not null
|
||||||
: permissionKind;
|
? new ResolvedApprovalContext(
|
||||||
|
normalizedToolName,
|
||||||
|
McpPermissionKind,
|
||||||
|
normalizedToolName,
|
||||||
|
mcpServerApprovalKey)
|
||||||
|
: new ResolvedApprovalContext(
|
||||||
|
normalizedToolName,
|
||||||
|
HookPermissionKind,
|
||||||
|
normalizedToolName,
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PermissionDetailDto BuildHookPermissionDetail(
|
private static PermissionDetailDto BuildHookPermissionDetail(
|
||||||
@@ -619,13 +705,29 @@ internal sealed class CopilotApprovalCoordinator
|
|||||||
: strippedToolName;
|
: strippedToolName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryResolveHookToolApproval(
|
||||||
|
string? toolName,
|
||||||
|
out HookToolApprovalMapping? mapping)
|
||||||
|
{
|
||||||
|
string? normalizedToolName = NormalizeOptionalString(toolName);
|
||||||
|
if (normalizedToolName is not null
|
||||||
|
&& HookToolApprovals.TryGetValue(normalizedToolName, out HookToolApprovalMapping? resolvedMapping))
|
||||||
|
{
|
||||||
|
mapping = resolvedMapping;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool MatchesAutoApprovedTool(
|
private static bool MatchesAutoApprovedTool(
|
||||||
IReadOnlyList<string> autoApprovedToolNames,
|
IReadOnlyList<string> autoApprovedToolNames,
|
||||||
string? toolName,
|
string? toolName,
|
||||||
string? autoApprovedToolName)
|
string? approvalToolKey)
|
||||||
{
|
{
|
||||||
return MatchesAutoApprovedToolName(autoApprovedToolNames, toolName)
|
return MatchesAutoApprovedToolName(autoApprovedToolNames, toolName)
|
||||||
|| MatchesAutoApprovedToolName(autoApprovedToolNames, autoApprovedToolName);
|
|| MatchesAutoApprovedToolName(autoApprovedToolNames, approvalToolKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool MatchesAutoApprovedToolName(
|
private static bool MatchesAutoApprovedToolName(
|
||||||
@@ -720,6 +822,16 @@ internal sealed class CopilotApprovalCoordinator
|
|||||||
return normalized.Count > 0 ? normalized : null;
|
return normalized.Count > 0 ? normalized : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed record ResolvedApprovalContext(
|
||||||
|
string? ToolName,
|
||||||
|
string PermissionKind,
|
||||||
|
string? ApprovalToolKey,
|
||||||
|
string? McpServerApprovalKey);
|
||||||
|
|
||||||
|
private sealed record HookToolApprovalMapping(
|
||||||
|
string PermissionKind,
|
||||||
|
string ApprovalToolKey);
|
||||||
|
|
||||||
private sealed record PendingApprovalRequest(
|
private sealed record PendingApprovalRequest(
|
||||||
string RequestId,
|
string RequestId,
|
||||||
string SessionId,
|
string SessionId,
|
||||||
|
|||||||
@@ -248,17 +248,16 @@ internal static class CopilotSessionHooks
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
string? autoApprovedToolName = CopilotApprovalCoordinator.ResolveHookToolCategory(toolName) ?? toolName;
|
CopilotApprovalCoordinator.ResolvedApprovalContext approval = CopilotApprovalCoordinator.ResolveHookApprovalContext(
|
||||||
string? mcpServerApprovalKey = CopilotApprovalCoordinator.ResolveHookMcpServerApprovalKey(
|
|
||||||
toolName,
|
toolName,
|
||||||
command.Tooling?.McpServers);
|
command.Tooling?.McpServers);
|
||||||
|
|
||||||
bool requiresApproval = CopilotApprovalCoordinator.RequiresToolCallApproval(
|
bool requiresApproval = CopilotApprovalCoordinator.RequiresToolCallApproval(
|
||||||
command.Workflow.Settings.ApprovalPolicy,
|
command.Workflow.Settings.ApprovalPolicy,
|
||||||
agentDefinition.GetAgentId(),
|
agentDefinition.GetAgentId(),
|
||||||
toolName,
|
approval.ToolName,
|
||||||
autoApprovedToolName,
|
approval.ApprovalToolKey,
|
||||||
mcpServerApprovalKey);
|
approval.McpServerApprovalKey);
|
||||||
|
|
||||||
return new PreToolUseHookOutput
|
return new PreToolUseHookOutput
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -154,11 +154,15 @@ public sealed class CopilotSessionHooksTests
|
|||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("view", "read")]
|
[InlineData("view", "read")]
|
||||||
[InlineData("grep", "read")]
|
[InlineData("grep", "read")]
|
||||||
|
[InlineData("rg", "read")]
|
||||||
[InlineData("edit", "write")]
|
[InlineData("edit", "write")]
|
||||||
|
[InlineData("apply_patch", "write")]
|
||||||
[InlineData("powershell", "shell")]
|
[InlineData("powershell", "shell")]
|
||||||
public async Task Create_PreToolUseAutoAllowsWhenCategoryIsApproved(string toolName, string category)
|
[InlineData("web_search", "web_fetch")]
|
||||||
|
[InlineData("store_memory", "store_memory")]
|
||||||
|
public async Task Create_PreToolUseAutoAllowsWhenApprovalToolKeyIsApproved(string toolName, string approvalToolKey)
|
||||||
{
|
{
|
||||||
RunTurnCommandDto command = CreateCommandWithAutoApprovedCategory(category);
|
RunTurnCommandDto command = CreateCommandWithAutoApprovedCategory(approvalToolKey);
|
||||||
SessionHooks hooks = CopilotSessionHooks.Create(command, command.Workflow.GetAgentNodes()[0], ResolvedHookSet.Empty, new RecordingHookCommandRunner());
|
SessionHooks hooks = CopilotSessionHooks.Create(command, command.Workflow.GetAgentNodes()[0], ResolvedHookSet.Empty, new RecordingHookCommandRunner());
|
||||||
|
|
||||||
PreToolUseHookOutput? decision = await hooks.OnPreToolUse!(
|
PreToolUseHookOutput? decision = await hooks.OnPreToolUse!(
|
||||||
|
|||||||
@@ -1487,6 +1487,28 @@ public sealed class CopilotWorkflowRunnerTests
|
|||||||
Assert.True(CopilotApprovalCoordinator.RequiresToolCallApproval(policy, "agent-1", "git.status"));
|
Assert.True(CopilotApprovalCoordinator.RequiresToolCallApproval(policy, "agent-1", "git.status"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RequiresToolCallApproval_HonorsCanonicalHookApprovalKeysForModernToolNames()
|
||||||
|
{
|
||||||
|
ApprovalPolicyDto policy = new()
|
||||||
|
{
|
||||||
|
Rules =
|
||||||
|
[
|
||||||
|
new ApprovalCheckpointRuleDto
|
||||||
|
{
|
||||||
|
Kind = "tool-call",
|
||||||
|
AgentIds = ["agent-1"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
AutoApprovedToolNames = ["read", "write", "web_fetch"],
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.False(CopilotApprovalCoordinator.RequiresToolCallApproval(policy, "agent-1", "rg", "read"));
|
||||||
|
Assert.False(CopilotApprovalCoordinator.RequiresToolCallApproval(policy, "agent-1", "apply_patch", "write"));
|
||||||
|
Assert.False(CopilotApprovalCoordinator.RequiresToolCallApproval(policy, "agent-1", "web_search", "web_fetch"));
|
||||||
|
Assert.True(CopilotApprovalCoordinator.RequiresToolCallApproval(policy, "agent-1", "powershell", "shell"));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RequiresToolCallApproval_HonorsMcpServerLevelApprovalKey()
|
public void RequiresToolCallApproval_HonorsMcpServerLevelApprovalKey()
|
||||||
{
|
{
|
||||||
@@ -1738,6 +1760,7 @@ public sealed class CopilotWorkflowRunnerTests
|
|||||||
"lsp_ts_hover");
|
"lsp_ts_hover");
|
||||||
|
|
||||||
Assert.Equal("lsp_ts_hover", approvalEvent.ToolName);
|
Assert.Equal("lsp_ts_hover", approvalEvent.ToolName);
|
||||||
|
Assert.Equal("lsp_ts_hover", approvalEvent.ApprovalToolKey);
|
||||||
Assert.Equal("Approve lsp_ts_hover", approvalEvent.Title);
|
Assert.Equal("Approve lsp_ts_hover", approvalEvent.Title);
|
||||||
Assert.Contains("tool \"lsp_ts_hover\"", approvalEvent.Detail);
|
Assert.Contains("tool \"lsp_ts_hover\"", approvalEvent.Detail);
|
||||||
Assert.NotNull(approvalEvent.PermissionDetail);
|
Assert.NotNull(approvalEvent.PermissionDetail);
|
||||||
@@ -1770,6 +1793,7 @@ public sealed class CopilotWorkflowRunnerTests
|
|||||||
"web_fetch");
|
"web_fetch");
|
||||||
|
|
||||||
Assert.Equal("web_fetch", approvalEvent.ToolName);
|
Assert.Equal("web_fetch", approvalEvent.ToolName);
|
||||||
|
Assert.Equal("web_fetch", approvalEvent.ApprovalToolKey);
|
||||||
Assert.Equal("Approve web_fetch", approvalEvent.Title);
|
Assert.Equal("Approve web_fetch", approvalEvent.Title);
|
||||||
Assert.Contains("url permission", approvalEvent.Detail);
|
Assert.Contains("url permission", approvalEvent.Detail);
|
||||||
Assert.Contains("tool \"web_fetch\"", approvalEvent.Detail);
|
Assert.Contains("tool \"web_fetch\"", approvalEvent.Detail);
|
||||||
@@ -1992,11 +2016,21 @@ public sealed class CopilotWorkflowRunnerTests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("view", "read")]
|
[InlineData("view", "read")]
|
||||||
|
[InlineData("show_file", "read")]
|
||||||
|
[InlineData("read_file", "read")]
|
||||||
[InlineData("glob", "read")]
|
[InlineData("glob", "read")]
|
||||||
[InlineData("grep", "read")]
|
[InlineData("grep", "read")]
|
||||||
|
[InlineData("rg", "read")]
|
||||||
[InlineData("lsp", "read")]
|
[InlineData("lsp", "read")]
|
||||||
[InlineData("edit", "write")]
|
[InlineData("edit", "write")]
|
||||||
[InlineData("create", "write")]
|
[InlineData("create", "write")]
|
||||||
|
[InlineData("write_file", "write")]
|
||||||
|
[InlineData("apply_patch", "write")]
|
||||||
|
[InlineData("bash", "shell")]
|
||||||
|
[InlineData("read_bash", "shell")]
|
||||||
|
[InlineData("write_bash", "shell")]
|
||||||
|
[InlineData("stop_bash", "shell")]
|
||||||
|
[InlineData("list_bash", "shell")]
|
||||||
[InlineData("powershell", "shell")]
|
[InlineData("powershell", "shell")]
|
||||||
[InlineData("read_powershell", "shell")]
|
[InlineData("read_powershell", "shell")]
|
||||||
[InlineData("write_powershell", "shell")]
|
[InlineData("write_powershell", "shell")]
|
||||||
@@ -2005,6 +2039,7 @@ public sealed class CopilotWorkflowRunnerTests
|
|||||||
[InlineData("web_fetch", "url")]
|
[InlineData("web_fetch", "url")]
|
||||||
[InlineData("web_search", "url")]
|
[InlineData("web_search", "url")]
|
||||||
[InlineData("store_memory", "memory")]
|
[InlineData("store_memory", "memory")]
|
||||||
|
[InlineData("remember_fact", "memory")]
|
||||||
public void ResolveHookToolCategory_ReturnsExpectedCategoryForKnownTools(string toolName, string expectedCategory)
|
public void ResolveHookToolCategory_ReturnsExpectedCategoryForKnownTools(string toolName, string expectedCategory)
|
||||||
{
|
{
|
||||||
Assert.Equal(expectedCategory, CopilotApprovalCoordinator.ResolveHookToolCategory(toolName));
|
Assert.Equal(expectedCategory, CopilotApprovalCoordinator.ResolveHookToolCategory(toolName));
|
||||||
@@ -2027,6 +2062,29 @@ public sealed class CopilotWorkflowRunnerTests
|
|||||||
Assert.Null(CopilotApprovalCoordinator.ResolveHookToolCategory(" "));
|
Assert.Null(CopilotApprovalCoordinator.ResolveHookToolCategory(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("view", "read")]
|
||||||
|
[InlineData("rg", "read")]
|
||||||
|
[InlineData("apply_patch", "write")]
|
||||||
|
[InlineData("bash", "shell")]
|
||||||
|
[InlineData("web_fetch", "web_fetch")]
|
||||||
|
[InlineData("web_search", "web_fetch")]
|
||||||
|
[InlineData("store_memory", "store_memory")]
|
||||||
|
[InlineData("remember_fact", "store_memory")]
|
||||||
|
public void ResolveHookApprovalToolKey_ReturnsExpectedKeyForKnownTools(string toolName, string expectedKey)
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedKey, CopilotApprovalCoordinator.ResolveHookApprovalToolKey(toolName));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveHookApprovalToolKey_ReturnsNullForUnknownTools()
|
||||||
|
{
|
||||||
|
Assert.Null(CopilotApprovalCoordinator.ResolveHookApprovalToolKey("custom_tool"));
|
||||||
|
Assert.Null(CopilotApprovalCoordinator.ResolveHookApprovalToolKey(null));
|
||||||
|
Assert.Null(CopilotApprovalCoordinator.ResolveHookApprovalToolKey(""));
|
||||||
|
Assert.Null(CopilotApprovalCoordinator.ResolveHookApprovalToolKey(" "));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResolveHookMcpServerApprovalKey_PrefersLongestConfiguredServerName()
|
public void ResolveHookMcpServerApprovalKey_PrefersLongestConfiguredServerName()
|
||||||
{
|
{
|
||||||
@@ -2119,6 +2177,7 @@ public sealed class CopilotWorkflowRunnerTests
|
|||||||
"view");
|
"view");
|
||||||
|
|
||||||
Assert.Equal("view", approvalEvent.ToolName);
|
Assert.Equal("view", approvalEvent.ToolName);
|
||||||
|
Assert.Equal("read", approvalEvent.ApprovalToolKey);
|
||||||
Assert.Equal("read", approvalEvent.PermissionKind);
|
Assert.Equal("read", approvalEvent.PermissionKind);
|
||||||
Assert.Contains("read permission", approvalEvent.Detail);
|
Assert.Contains("read permission", approvalEvent.Detail);
|
||||||
}
|
}
|
||||||
@@ -2151,6 +2210,7 @@ public sealed class CopilotWorkflowRunnerTests
|
|||||||
"icm-mcp-get_schedule");
|
"icm-mcp-get_schedule");
|
||||||
|
|
||||||
Assert.Equal("mcp", approvalEvent.PermissionKind);
|
Assert.Equal("mcp", approvalEvent.PermissionKind);
|
||||||
|
Assert.Equal("icm-mcp-get_schedule", approvalEvent.ApprovalToolKey);
|
||||||
Assert.Contains("mcp permission", approvalEvent.Detail);
|
Assert.Contains("mcp permission", approvalEvent.Detail);
|
||||||
Assert.NotNull(approvalEvent.PermissionDetail);
|
Assert.NotNull(approvalEvent.PermissionDetail);
|
||||||
Assert.Equal("mcp", approvalEvent.PermissionDetail!.Kind);
|
Assert.Equal("mcp", approvalEvent.PermissionDetail!.Kind);
|
||||||
@@ -2182,6 +2242,7 @@ public sealed class CopilotWorkflowRunnerTests
|
|||||||
"icm-mcp-get_schedule");
|
"icm-mcp-get_schedule");
|
||||||
|
|
||||||
Assert.Equal("hook", approvalEvent.PermissionKind);
|
Assert.Equal("hook", approvalEvent.PermissionKind);
|
||||||
|
Assert.Equal("icm-mcp-get_schedule", approvalEvent.ApprovalToolKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -2214,6 +2275,7 @@ public sealed class CopilotWorkflowRunnerTests
|
|||||||
|
|
||||||
Assert.False(pending.IsCompleted);
|
Assert.False(pending.IsCompleted);
|
||||||
Assert.NotNull(observedApproval);
|
Assert.NotNull(observedApproval);
|
||||||
|
Assert.Equal("lsp_ts_definition", observedApproval!.ApprovalToolKey);
|
||||||
|
|
||||||
await coordinator.ResolveApprovalAsync(
|
await coordinator.ResolveApprovalAsync(
|
||||||
new ResolveApprovalCommandDto
|
new ResolveApprovalCommandDto
|
||||||
@@ -2270,6 +2332,8 @@ public sealed class CopilotWorkflowRunnerTests
|
|||||||
Assert.Equal("tool-calling", observedActivity!.ActivityType);
|
Assert.Equal("tool-calling", observedActivity!.ActivityType);
|
||||||
Assert.Equal("apply_patch", observedActivity.ToolName);
|
Assert.Equal("apply_patch", observedActivity.ToolName);
|
||||||
Assert.Equal("tool-call-write-1", observedActivity.ToolCallId);
|
Assert.Equal("tool-call-write-1", observedActivity.ToolCallId);
|
||||||
|
Assert.Equal("apply_patch", observedApproval!.ToolName);
|
||||||
|
Assert.Equal("write", observedApproval.ApprovalToolKey);
|
||||||
|
|
||||||
ToolCallFileChangeDto preview = Assert.Single(observedActivity.FileChanges!);
|
ToolCallFileChangeDto preview = Assert.Single(observedActivity.FileChanges!);
|
||||||
Assert.Equal("README.md", preview.Path);
|
Assert.Equal("README.md", preview.Path);
|
||||||
@@ -2388,6 +2452,7 @@ public sealed class CopilotWorkflowRunnerTests
|
|||||||
|
|
||||||
Assert.False(firstPending.IsCompleted);
|
Assert.False(firstPending.IsCompleted);
|
||||||
Assert.NotNull(firstApproval);
|
Assert.NotNull(firstApproval);
|
||||||
|
Assert.Equal("read", firstApproval!.ApprovalToolKey);
|
||||||
|
|
||||||
await coordinator.ResolveApprovalAsync(
|
await coordinator.ResolveApprovalAsync(
|
||||||
new ResolveApprovalCommandDto
|
new ResolveApprovalCommandDto
|
||||||
@@ -2428,6 +2493,82 @@ public sealed class CopilotWorkflowRunnerTests
|
|||||||
Assert.Equal(PermissionRequestResultKind.Approved, secondResult.Kind);
|
Assert.Equal(PermissionRequestResultKind.Approved, secondResult.Kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestApprovalAsync_AlwaysApproveCachesCanonicalWriteApprovalForModernHookTools()
|
||||||
|
{
|
||||||
|
CopilotApprovalCoordinator coordinator = new();
|
||||||
|
ApprovalRequestedEventDto? firstApproval = null;
|
||||||
|
RunTurnCommandDto command = CreateApprovalCommand();
|
||||||
|
|
||||||
|
Task<PermissionRequestResult> firstPending = coordinator.RequestApprovalAsync(
|
||||||
|
command,
|
||||||
|
command.Workflow.GetAgentNodes()[0],
|
||||||
|
new PermissionRequestWrite
|
||||||
|
{
|
||||||
|
Kind = "write",
|
||||||
|
ToolCallId = "tool-call-write-1",
|
||||||
|
Intention = "Update the README",
|
||||||
|
FileName = "README.md",
|
||||||
|
Diff = "@@ -1 +1 @@",
|
||||||
|
NewFileContents = "# Aryx\n",
|
||||||
|
},
|
||||||
|
new PermissionInvocation
|
||||||
|
{
|
||||||
|
SessionId = "copilot-session-1",
|
||||||
|
},
|
||||||
|
CreateToolCallRegistry(("tool-call-write-1", "apply_patch")),
|
||||||
|
approval =>
|
||||||
|
{
|
||||||
|
firstApproval = approval;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.False(firstPending.IsCompleted);
|
||||||
|
Assert.NotNull(firstApproval);
|
||||||
|
Assert.Equal("write", firstApproval!.ApprovalToolKey);
|
||||||
|
|
||||||
|
await coordinator.ResolveApprovalAsync(
|
||||||
|
new ResolveApprovalCommandDto
|
||||||
|
{
|
||||||
|
ApprovalId = firstApproval.ApprovalId,
|
||||||
|
Decision = "approved",
|
||||||
|
AlwaysApprove = true,
|
||||||
|
},
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
PermissionRequestResult firstResult = await firstPending;
|
||||||
|
Assert.Equal(PermissionRequestResultKind.Approved, firstResult.Kind);
|
||||||
|
|
||||||
|
bool sawSecondApproval = false;
|
||||||
|
PermissionRequestResult secondResult = await coordinator.RequestApprovalAsync(
|
||||||
|
command,
|
||||||
|
command.Workflow.GetAgentNodes()[0],
|
||||||
|
new PermissionRequestWrite
|
||||||
|
{
|
||||||
|
Kind = "write",
|
||||||
|
ToolCallId = "tool-call-write-2",
|
||||||
|
Intention = "Create docs draft",
|
||||||
|
FileName = "docs\\guide.md",
|
||||||
|
Diff = "@@ -0,0 +1 @@",
|
||||||
|
NewFileContents = "# Guide\n",
|
||||||
|
},
|
||||||
|
new PermissionInvocation
|
||||||
|
{
|
||||||
|
SessionId = "copilot-session-1",
|
||||||
|
},
|
||||||
|
CreateToolCallRegistry(("tool-call-write-2", "write_file")),
|
||||||
|
approval =>
|
||||||
|
{
|
||||||
|
sawSecondApproval = true;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.False(sawSecondApproval);
|
||||||
|
Assert.Equal(PermissionRequestResultKind.Approved, secondResult.Kind);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RequestApprovalAsync_AlwaysApproveCacheDoesNotCarryAcrossTurnRequests()
|
public async Task RequestApprovalAsync_AlwaysApproveCacheDoesNotCarryAcrossTurnRequests()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -412,6 +412,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
AgentId = "agent-1",
|
AgentId = "agent-1",
|
||||||
AgentName = "Primary",
|
AgentName = "Primary",
|
||||||
PermissionKind = "tool access",
|
PermissionKind = "tool access",
|
||||||
|
ApprovalToolKey = "shell",
|
||||||
Title = "Approve tool access",
|
Title = "Approve tool access",
|
||||||
PermissionDetail = new PermissionDetailDto
|
PermissionDetail = new PermissionDetailDto
|
||||||
{
|
{
|
||||||
@@ -437,6 +438,7 @@ public sealed class SidecarProtocolHostTests
|
|||||||
Assert.Equal("turn-approval", approvalEvent.GetProperty("requestId").GetString());
|
Assert.Equal("turn-approval", approvalEvent.GetProperty("requestId").GetString());
|
||||||
Assert.Equal("approval-1", approvalEvent.GetProperty("approvalId").GetString());
|
Assert.Equal("approval-1", approvalEvent.GetProperty("approvalId").GetString());
|
||||||
Assert.Equal("tool-call", approvalEvent.GetProperty("approvalKind").GetString());
|
Assert.Equal("tool-call", approvalEvent.GetProperty("approvalKind").GetString());
|
||||||
|
Assert.Equal("shell", approvalEvent.GetProperty("approvalToolKey").GetString());
|
||||||
Assert.Equal("Approve tool access", approvalEvent.GetProperty("title").GetString());
|
Assert.Equal("Approve tool access", approvalEvent.GetProperty("title").GetString());
|
||||||
JsonElement permissionDetail = approvalEvent.GetProperty("permissionDetail");
|
JsonElement permissionDetail = approvalEvent.GetProperty("permissionDetail");
|
||||||
Assert.Equal("shell", permissionDetail.GetProperty("kind").GetString());
|
Assert.Equal("shell", permissionDetail.GetProperty("kind").GetString());
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import type {
|
|||||||
import {
|
import {
|
||||||
dequeuePendingApprovalState,
|
dequeuePendingApprovalState,
|
||||||
enqueuePendingApprovalState,
|
enqueuePendingApprovalState,
|
||||||
|
getPendingApprovalToolKey,
|
||||||
listPendingApprovals,
|
listPendingApprovals,
|
||||||
resolvePendingApproval,
|
resolvePendingApproval,
|
||||||
resolveApprovalToolKey,
|
|
||||||
type ApprovalDecision,
|
type ApprovalDecision,
|
||||||
type PendingApprovalRecord,
|
type PendingApprovalRecord,
|
||||||
} from '@shared/domain/approval';
|
} from '@shared/domain/approval';
|
||||||
@@ -109,7 +109,7 @@ export class ApprovalCoordinator {
|
|||||||
this.setSessionPendingApprovalState(session, dequeuePendingApprovalState(session, approvalId));
|
this.setSessionPendingApprovalState(session, dequeuePendingApprovalState(session, approvalId));
|
||||||
session.updatedAt = resolvedAt;
|
session.updatedAt = resolvedAt;
|
||||||
|
|
||||||
const approvalKey = resolveApprovalToolKey(approval.toolName, approval.permissionKind);
|
const approvalKey = getPendingApprovalToolKey(approval);
|
||||||
if (decision === 'approved' && alwaysApprove && approvalKey) {
|
if (decision === 'approved' && alwaysApprove && approvalKey) {
|
||||||
const existing = session.approvalSettings?.autoApprovedToolNames ?? [];
|
const existing = session.approvalSettings?.autoApprovedToolNames ?? [];
|
||||||
if (!existing.includes(approvalKey)) {
|
if (!existing.includes(approvalKey)) {
|
||||||
@@ -127,7 +127,7 @@ export class ApprovalCoordinator {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const queuedKey = resolveApprovalToolKey(queued.toolName, queued.permissionKind);
|
const queuedKey = getPendingApprovalToolKey(queued);
|
||||||
if (queuedKey !== approvalKey) {
|
if (queuedKey !== approvalKey) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -361,6 +361,7 @@ export class ApprovalCoordinator {
|
|||||||
agentName: event.agentName,
|
agentName: event.agentName,
|
||||||
toolName: event.toolName,
|
toolName: event.toolName,
|
||||||
permissionKind: event.permissionKind,
|
permissionKind: event.permissionKind,
|
||||||
|
approvalToolKey: event.approvalToolKey,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
detail: event.detail,
|
detail: event.detail,
|
||||||
permissionDetail: event.permissionDetail,
|
permissionDetail: event.permissionDetail,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Bot, Check, ChevronDown, Loader2, ShieldAlert, ShieldBan, ShieldCheck,
|
|||||||
|
|
||||||
import { MarkdownContent } from '@renderer/components/MarkdownContent';
|
import { MarkdownContent } from '@renderer/components/MarkdownContent';
|
||||||
import { permissionDetailSummary, PermissionDetailView } from '@renderer/components/chat/PermissionDetailView';
|
import { permissionDetailSummary, PermissionDetailView } from '@renderer/components/chat/PermissionDetailView';
|
||||||
import { resolveApprovalToolKey } from '@shared/domain/approval';
|
import { getPendingApprovalToolKey } from '@shared/domain/approval';
|
||||||
import type { ApprovalDecision, PendingApprovalRecord } from '@shared/domain/approval';
|
import type { ApprovalDecision, PendingApprovalRecord } from '@shared/domain/approval';
|
||||||
import { resolveToolLabel } from '@shared/domain/tooling';
|
import { resolveToolLabel } from '@shared/domain/tooling';
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export function ApprovalBanner({
|
|||||||
const kindLabel = approval.kind === 'final-response' ? 'Final response review' : 'Tool call approval';
|
const kindLabel = approval.kind === 'final-response' ? 'Final response review' : 'Tool call approval';
|
||||||
const hasMessages = approval.messages && approval.messages.length > 0;
|
const hasMessages = approval.messages && approval.messages.length > 0;
|
||||||
const showPosition = position !== undefined && total !== undefined && total > 1;
|
const showPosition = position !== undefined && total !== undefined && total > 1;
|
||||||
const approvalToolKey = resolveApprovalToolKey(approval.toolName, approval.permissionKind);
|
const approvalToolKey = getPendingApprovalToolKey(approval);
|
||||||
const canAlwaysApprove = approval.kind === 'tool-call' && !!approvalToolKey;
|
const canAlwaysApprove = approval.kind === 'tool-call' && !!approvalToolKey;
|
||||||
const approvalToolLabel = approvalToolKey ? resolveToolLabel(approvalToolKey) : undefined;
|
const approvalToolLabel = approvalToolKey ? resolveToolLabel(approvalToolKey) : undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -518,6 +518,7 @@ export interface ApprovalRequestedEvent {
|
|||||||
agentName?: string;
|
agentName?: string;
|
||||||
toolName?: string;
|
toolName?: string;
|
||||||
permissionKind?: string;
|
permissionKind?: string;
|
||||||
|
approvalToolKey?: string;
|
||||||
title: string;
|
title: string;
|
||||||
detail?: string;
|
detail?: string;
|
||||||
permissionDetail?: PermissionDetail;
|
permissionDetail?: PermissionDetail;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export interface PendingApprovalRecord {
|
|||||||
agentName?: string;
|
agentName?: string;
|
||||||
toolName?: string;
|
toolName?: string;
|
||||||
permissionKind?: string;
|
permissionKind?: string;
|
||||||
|
approvalToolKey?: string;
|
||||||
title: string;
|
title: string;
|
||||||
detail?: string;
|
detail?: string;
|
||||||
messages?: PendingApprovalMessageRecord[];
|
messages?: PendingApprovalMessageRecord[];
|
||||||
@@ -215,6 +216,22 @@ export function resolveApprovalToolKey(
|
|||||||
return toolName;
|
return toolName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the canonical approval key for a pending approval.
|
||||||
|
*
|
||||||
|
* Prefers the explicit `approvalToolKey` supplied by the sidecar. Falls back to
|
||||||
|
* re-deriving the key from `toolName` and `permissionKind` to support legacy
|
||||||
|
* records persisted before the sidecar contract change.
|
||||||
|
*/
|
||||||
|
export function getPendingApprovalToolKey(
|
||||||
|
record: Pick<PendingApprovalRecord, 'approvalToolKey' | 'toolName' | 'permissionKind'>,
|
||||||
|
): string | undefined {
|
||||||
|
return (
|
||||||
|
normalizeOptionalString(record.approvalToolKey)
|
||||||
|
?? resolveApprovalToolKey(record.toolName, record.permissionKind)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function approvalPolicyAutoApprovesTool(
|
export function approvalPolicyAutoApprovesTool(
|
||||||
policy: ApprovalPolicy | undefined,
|
policy: ApprovalPolicy | undefined,
|
||||||
toolName?: string,
|
toolName?: string,
|
||||||
@@ -312,6 +329,7 @@ export function normalizePendingApproval(
|
|||||||
agentName: normalizeOptionalString(approval?.agentName),
|
agentName: normalizeOptionalString(approval?.agentName),
|
||||||
toolName: normalizeOptionalString(approval?.toolName),
|
toolName: normalizeOptionalString(approval?.toolName),
|
||||||
permissionKind: normalizeOptionalString(approval?.permissionKind),
|
permissionKind: normalizeOptionalString(approval?.permissionKind),
|
||||||
|
approvalToolKey: normalizeOptionalString(approval?.approvalToolKey),
|
||||||
title,
|
title,
|
||||||
detail: normalizeOptionalString(approval?.detail),
|
detail: normalizeOptionalString(approval?.detail),
|
||||||
messages: normalizePendingApprovalMessages(approval?.messages),
|
messages: normalizePendingApprovalMessages(approval?.messages),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
normalizeSessionApprovalSettings,
|
normalizeSessionApprovalSettings,
|
||||||
pruneSessionApprovalSettings,
|
pruneSessionApprovalSettings,
|
||||||
resolveApprovalToolKey,
|
resolveApprovalToolKey,
|
||||||
|
getPendingApprovalToolKey,
|
||||||
dequeuePendingApprovalState,
|
dequeuePendingApprovalState,
|
||||||
enqueuePendingApprovalState,
|
enqueuePendingApprovalState,
|
||||||
listPendingApprovals,
|
listPendingApprovals,
|
||||||
@@ -261,4 +262,48 @@ describe('approval helpers', () => {
|
|||||||
expect(resolveApprovalToolKey(undefined, undefined)).toBeUndefined();
|
expect(resolveApprovalToolKey(undefined, undefined)).toBeUndefined();
|
||||||
expect(resolveApprovalToolKey(undefined, 'mcp')).toBeUndefined();
|
expect(resolveApprovalToolKey(undefined, 'mcp')).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('getPendingApprovalToolKey prefers the explicit approval tool key from the sidecar', () => {
|
||||||
|
expect(
|
||||||
|
getPendingApprovalToolKey({
|
||||||
|
approvalToolKey: 'write',
|
||||||
|
toolName: 'apply_patch',
|
||||||
|
permissionKind: 'hook',
|
||||||
|
}),
|
||||||
|
).toBe('write');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getPendingApprovalToolKey({
|
||||||
|
approvalToolKey: 'web_fetch',
|
||||||
|
toolName: 'web_search',
|
||||||
|
permissionKind: 'hook',
|
||||||
|
}),
|
||||||
|
).toBe('web_fetch');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getPendingApprovalToolKey({
|
||||||
|
approvalToolKey: 'mcp_server:icm-mcp',
|
||||||
|
toolName: 'icm-mcp-get_schedule',
|
||||||
|
permissionKind: 'mcp',
|
||||||
|
}),
|
||||||
|
).toBe('mcp_server:icm-mcp');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getPendingApprovalToolKey falls back to derivation for legacy approvals without the explicit key', () => {
|
||||||
|
expect(
|
||||||
|
getPendingApprovalToolKey({
|
||||||
|
toolName: 'view',
|
||||||
|
permissionKind: 'read',
|
||||||
|
}),
|
||||||
|
).toBe('read');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getPendingApprovalToolKey({
|
||||||
|
toolName: 'git.status',
|
||||||
|
permissionKind: 'mcp',
|
||||||
|
}),
|
||||||
|
).toBe('git.status');
|
||||||
|
|
||||||
|
expect(getPendingApprovalToolKey({})).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user