mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-29 07:58:47 +02:00
fix: honor MCP server auto-approvals in hook flow
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -171,6 +171,40 @@ public sealed class CopilotSessionHooksTests
|
||||
Assert.Equal("allow", decision?.PermissionDecision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_PreToolUseAutoAllowsWhenMcpServerIsApproved()
|
||||
{
|
||||
RunTurnCommandDto command = CreateCommandWithConfiguredMcpServers(
|
||||
["icm-mcp"],
|
||||
["mcp_server:icm-mcp"]);
|
||||
SessionHooks hooks = CopilotSessionHooks.Create(command, command.Pattern.Agents[0], ResolvedHookSet.Empty, new RecordingHookCommandRunner());
|
||||
|
||||
PreToolUseHookOutput? decision = await hooks.OnPreToolUse!(
|
||||
new PreToolUseHookInput
|
||||
{
|
||||
ToolName = "icm-mcp-get_incident_details_by_id",
|
||||
},
|
||||
null!);
|
||||
|
||||
Assert.Equal("allow", decision?.PermissionDecision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_PreToolUseRequiresApprovalWhenMcpServerIsNotApproved()
|
||||
{
|
||||
RunTurnCommandDto command = CreateCommandWithConfiguredMcpServers(["icm-mcp"]);
|
||||
SessionHooks hooks = CopilotSessionHooks.Create(command, command.Pattern.Agents[0], ResolvedHookSet.Empty, new RecordingHookCommandRunner());
|
||||
|
||||
PreToolUseHookOutput? decision = await hooks.OnPreToolUse!(
|
||||
new PreToolUseHookInput
|
||||
{
|
||||
ToolName = "icm-mcp-get_incident_details_by_id",
|
||||
},
|
||||
null!);
|
||||
|
||||
Assert.Equal("ask", decision?.PermissionDecision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_RunsConfiguredNonPreToolHooks()
|
||||
{
|
||||
@@ -368,6 +402,43 @@ public sealed class CopilotSessionHooksTests
|
||||
};
|
||||
}
|
||||
|
||||
private static RunTurnCommandDto CreateCommandWithConfiguredMcpServers(
|
||||
IReadOnlyList<string> serverNames,
|
||||
IReadOnlyList<string>? autoApprovedToolNames = null)
|
||||
{
|
||||
RunTurnCommandDto command = CreateCommandWithToolApproval();
|
||||
return new RunTurnCommandDto
|
||||
{
|
||||
RequestId = command.RequestId,
|
||||
SessionId = command.SessionId,
|
||||
ProjectPath = command.ProjectPath,
|
||||
Tooling = new RunTurnToolingConfigDto
|
||||
{
|
||||
McpServers = [.. serverNames.Select(CreateMcpServerConfig)],
|
||||
},
|
||||
Pattern = new PatternDefinitionDto
|
||||
{
|
||||
Id = command.Pattern.Id,
|
||||
Name = command.Pattern.Name,
|
||||
Mode = command.Pattern.Mode,
|
||||
Availability = command.Pattern.Availability,
|
||||
ApprovalPolicy = new ApprovalPolicyDto
|
||||
{
|
||||
Rules = command.Pattern.ApprovalPolicy?.Rules ?? [],
|
||||
AutoApprovedToolNames = autoApprovedToolNames ?? [],
|
||||
},
|
||||
Agents = command.Pattern.Agents,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static RunTurnMcpServerConfigDto CreateMcpServerConfig(string serverName)
|
||||
=> new()
|
||||
{
|
||||
Id = serverName,
|
||||
Name = serverName,
|
||||
};
|
||||
|
||||
private static HookCommandDefinition CreateHookCommand(string name)
|
||||
=> new()
|
||||
{
|
||||
|
||||
@@ -1325,6 +1325,29 @@ public sealed class CopilotWorkflowRunnerTests
|
||||
Assert.Equal("https://example.com", args["url"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPermissionDetail_MapsConfiguredMcpHookToMcpDetail()
|
||||
{
|
||||
PermissionDetailDto detail = CopilotApprovalCoordinator.BuildPermissionDetail(
|
||||
new PermissionRequestHook
|
||||
{
|
||||
Kind = "hook",
|
||||
ToolName = "icm-mcp-get_incident_details_by_id",
|
||||
ToolArgs = new Dictionary<string, object?>
|
||||
{
|
||||
["incidentId"] = 769904783,
|
||||
},
|
||||
},
|
||||
[CreateMcpServerConfig("icm-mcp")]);
|
||||
|
||||
Assert.Equal("mcp", detail.Kind);
|
||||
Assert.Equal("icm-mcp", detail.ServerName);
|
||||
Assert.Equal("get_incident_details_by_id", detail.ToolTitle);
|
||||
|
||||
Dictionary<string, object?> args = Assert.IsType<Dictionary<string, object?>>(detail.Args);
|
||||
Assert.Equal(769904783, args["incidentId"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("view", "read")]
|
||||
[InlineData("glob", "read")]
|
||||
@@ -1362,6 +1385,16 @@ public sealed class CopilotWorkflowRunnerTests
|
||||
Assert.Null(CopilotApprovalCoordinator.ResolveHookToolCategory(" "));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveHookMcpServerApprovalKey_PrefersLongestConfiguredServerName()
|
||||
{
|
||||
string? approvalKey = CopilotApprovalCoordinator.ResolveHookMcpServerApprovalKey(
|
||||
"icm-mcp-get_on_call_schedule",
|
||||
[CreateMcpServerConfig("icm"), CreateMcpServerConfig("icm-mcp")]);
|
||||
|
||||
Assert.Equal("mcp_server:icm-mcp", approvalKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGetApprovalToolName_ResolvesHookToolToCategory()
|
||||
{
|
||||
@@ -1448,6 +1481,41 @@ public sealed class CopilotWorkflowRunnerTests
|
||||
Assert.Contains("read permission", approvalEvent.Detail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPermissionApprovalEvent_UsesMcpKindForConfiguredMcpHookTools()
|
||||
{
|
||||
ApprovalRequestedEventDto approvalEvent = CopilotApprovalCoordinator.BuildPermissionApprovalEvent(
|
||||
new RunTurnCommandDto
|
||||
{
|
||||
RequestId = "turn-1",
|
||||
SessionId = "session-1",
|
||||
Tooling = new RunTurnToolingConfigDto
|
||||
{
|
||||
McpServers = [CreateMcpServerConfig("icm-mcp")],
|
||||
},
|
||||
},
|
||||
CreateAgent("agent-1", "Primary"),
|
||||
new PermissionRequestHook
|
||||
{
|
||||
Kind = "hook",
|
||||
ToolName = "icm-mcp-get_schedule",
|
||||
ToolArgs = """{"teamIds":[91982]}""",
|
||||
},
|
||||
new PermissionInvocation
|
||||
{
|
||||
SessionId = "copilot-session-1",
|
||||
},
|
||||
"approval-1",
|
||||
"icm-mcp-get_schedule");
|
||||
|
||||
Assert.Equal("mcp", approvalEvent.PermissionKind);
|
||||
Assert.Contains("mcp permission", approvalEvent.Detail);
|
||||
Assert.NotNull(approvalEvent.PermissionDetail);
|
||||
Assert.Equal("mcp", approvalEvent.PermissionDetail!.Kind);
|
||||
Assert.Equal("icm-mcp", approvalEvent.PermissionDetail.ServerName);
|
||||
Assert.Equal("get_schedule", approvalEvent.PermissionDetail.ToolTitle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPermissionApprovalEvent_KeepsHookKindForUnknownHookTools()
|
||||
{
|
||||
@@ -1613,6 +1681,43 @@ public sealed class CopilotWorkflowRunnerTests
|
||||
Assert.Equal(PermissionRequestResultKind.Approved, result.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RequestApprovalAsync_AutoApprovesHookRequestsForApprovedMcpServer()
|
||||
{
|
||||
CopilotApprovalCoordinator coordinator = new();
|
||||
bool sawApproval = false;
|
||||
RunTurnCommandDto command = CreateApprovalCommand(
|
||||
autoApprovedToolNames: ["mcp_server:icm-mcp"],
|
||||
mcpServers: [CreateMcpServerConfig("icm-mcp")]);
|
||||
|
||||
PermissionRequestResult result = await coordinator.RequestApprovalAsync(
|
||||
command,
|
||||
command.Pattern.Agents[0],
|
||||
new PermissionRequestHook
|
||||
{
|
||||
Kind = "hook",
|
||||
ToolName = "icm-mcp-get_incident_details_by_id",
|
||||
ToolArgs = new Dictionary<string, object?>
|
||||
{
|
||||
["incidentId"] = 769904783,
|
||||
},
|
||||
},
|
||||
new PermissionInvocation
|
||||
{
|
||||
SessionId = "copilot-session-1",
|
||||
},
|
||||
new Dictionary<string, string>(StringComparer.Ordinal),
|
||||
approval =>
|
||||
{
|
||||
sawApproval = true;
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
CancellationToken.None);
|
||||
|
||||
Assert.False(sawApproval);
|
||||
Assert.Equal(PermissionRequestResultKind.Approved, result.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RequestApprovalAsync_AlwaysApproveCachesRuntimeApprovalForCurrentTurn()
|
||||
{
|
||||
@@ -1853,12 +1958,21 @@ public sealed class CopilotWorkflowRunnerTests
|
||||
null!);
|
||||
}
|
||||
|
||||
private static RunTurnCommandDto CreateApprovalCommand(string requestId = "turn-1")
|
||||
private static RunTurnCommandDto CreateApprovalCommand(
|
||||
string requestId = "turn-1",
|
||||
IReadOnlyList<string>? autoApprovedToolNames = null,
|
||||
IReadOnlyList<RunTurnMcpServerConfigDto>? mcpServers = null)
|
||||
{
|
||||
return new RunTurnCommandDto
|
||||
{
|
||||
RequestId = requestId,
|
||||
SessionId = "session-1",
|
||||
Tooling = mcpServers is null
|
||||
? null
|
||||
: new RunTurnToolingConfigDto
|
||||
{
|
||||
McpServers = [.. mcpServers],
|
||||
},
|
||||
Pattern = new PatternDefinitionDto
|
||||
{
|
||||
Id = "pattern-1",
|
||||
@@ -1875,7 +1989,9 @@ public sealed class CopilotWorkflowRunnerTests
|
||||
AgentIds = ["agent-1"],
|
||||
},
|
||||
],
|
||||
AutoApprovedToolNames = ["web_fetch"],
|
||||
AutoApprovedToolNames = autoApprovedToolNames is null
|
||||
? ["web_fetch"]
|
||||
: [.. autoApprovedToolNames],
|
||||
},
|
||||
Agents =
|
||||
[
|
||||
@@ -1885,6 +2001,13 @@ public sealed class CopilotWorkflowRunnerTests
|
||||
};
|
||||
}
|
||||
|
||||
private static RunTurnMcpServerConfigDto CreateMcpServerConfig(string serverName)
|
||||
=> new()
|
||||
{
|
||||
Id = serverName,
|
||||
Name = serverName,
|
||||
};
|
||||
|
||||
private sealed class StubChatClient : IChatClient
|
||||
{
|
||||
public Task<ChatResponse> GetResponseAsync(
|
||||
|
||||
Reference in New Issue
Block a user