diff --git a/sidecar/src/Eryx.AgentHost/Services/CopilotWorkflowRunner.cs b/sidecar/src/Eryx.AgentHost/Services/CopilotWorkflowRunner.cs index c52ae50..d96568d 100644 --- a/sidecar/src/Eryx.AgentHost/Services/CopilotWorkflowRunner.cs +++ b/sidecar/src/Eryx.AgentHost/Services/CopilotWorkflowRunner.cs @@ -403,16 +403,29 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner string? normalizedToolName = string.IsNullOrWhiteSpace(toolName) ? null : toolName.Trim(); + string? requestedUrl = request is PermissionRequestUrl urlRequest && !string.IsNullOrWhiteSpace(urlRequest.Url) + ? urlRequest.Url.Trim() + : null; 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}."; + ? $"{agentName} requested {permissionKind} permission" + : $"{agentName} requested {permissionKind} permission for tool \"{normalizedToolName}\""; + + if (requestedUrl is not null) + { + detail = $"{detail} to access \"{requestedUrl}\""; + } + + if (sessionId is not null) + { + detail = normalizedToolName is null + ? $"{detail} for Copilot session {sessionId}" + : $"{detail} in Copilot session {sessionId}"; + } + + detail = $"{detail}."; return new ApprovalRequestedEventDto { @@ -482,6 +495,8 @@ public sealed class CopilotWorkflowRunner : ITurnWorkflowRunner { PermissionRequestMcp mcp when !string.IsNullOrWhiteSpace(mcp.ToolName) => mcp.ToolName.Trim(), PermissionRequestCustomTool customTool when !string.IsNullOrWhiteSpace(customTool.ToolName) => customTool.ToolName.Trim(), + PermissionRequestHook hook when !string.IsNullOrWhiteSpace(hook.ToolName) => hook.ToolName.Trim(), + PermissionRequestUrl => "web_fetch", _ => null, }; diff --git a/sidecar/tests/Eryx.AgentHost.Tests/CopilotWorkflowRunnerTests.cs b/sidecar/tests/Eryx.AgentHost.Tests/CopilotWorkflowRunnerTests.cs index fa05ef9..31726cb 100644 --- a/sidecar/tests/Eryx.AgentHost.Tests/CopilotWorkflowRunnerTests.cs +++ b/sidecar/tests/Eryx.AgentHost.Tests/CopilotWorkflowRunnerTests.cs @@ -247,17 +247,18 @@ public sealed class CopilotWorkflowRunnerTests AgentIds = ["agent-1"], }, ], - AutoApprovedToolNames = ["lsp_ts_hover"], + AutoApprovedToolNames = ["lsp_ts_hover", "web_fetch"], }; Assert.False(CopilotWorkflowRunner.RequiresToolCallApproval(policy, "agent-1", "lsp_ts_hover")); + Assert.False(CopilotWorkflowRunner.RequiresToolCallApproval(policy, "agent-1", "web_fetch")); 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() + public void TryGetApprovalToolName_ReadsMcpCustomHookAndUrlRequests() { Assert.True( CopilotWorkflowRunner.TryGetApprovalToolName( @@ -283,6 +284,30 @@ public sealed class CopilotWorkflowRunnerTests out string? customToolName)); Assert.Equal("lsp_ts_hover", customToolName); + Assert.True( + CopilotWorkflowRunner.TryGetApprovalToolName( + new PermissionRequestHook + { + Kind = "hook", + ToolName = "web_fetch", + ToolArgs = """{"url":"https://example.com"}""", + HookMessage = "Review required before fetch", + }, + out string? hookToolName)); + Assert.Equal("web_fetch", hookToolName); + + Assert.True( + CopilotWorkflowRunner.TryGetApprovalToolName( + new PermissionRequestUrl + { + Kind = "url", + ToolCallId = "tool-call-1", + Intention = "Fetch the requested page", + Url = "https://example.com/docs", + }, + out string? urlToolName)); + Assert.Equal("web_fetch", urlToolName); + Assert.False( CopilotWorkflowRunner.TryGetApprovalToolName( new PermissionRequestShell @@ -328,6 +353,37 @@ public sealed class CopilotWorkflowRunnerTests Assert.Contains("tool \"lsp_ts_hover\"", approvalEvent.Detail); } + [Fact] + public void BuildPermissionApprovalEvent_IncludesRequestedUrlForUrlPermissions() + { + ApprovalRequestedEventDto approvalEvent = CopilotWorkflowRunner.BuildPermissionApprovalEvent( + new RunTurnCommandDto + { + RequestId = "turn-1", + SessionId = "session-1", + }, + CreateAgent("agent-1", "Analyst"), + new PermissionRequestUrl + { + Kind = "url", + ToolCallId = "tool-call-1", + Intention = "Fetch the requested page", + Url = "https://example.com/docs", + }, + new PermissionInvocation + { + SessionId = "copilot-session-1", + }, + "approval-1", + "web_fetch"); + + Assert.Equal("web_fetch", approvalEvent.ToolName); + Assert.Equal("Approve web_fetch", approvalEvent.Title); + Assert.Contains("url permission", approvalEvent.Detail); + Assert.Contains("tool \"web_fetch\"", approvalEvent.Detail); + Assert.Contains("https://example.com/docs", approvalEvent.Detail); + } + private static PatternAgentDefinitionDto CreateAgent(string id, string name) { return new PatternAgentDefinitionDto