fix: use runtime tools for approval auto-approval

- load runtime tools dynamically from Copilot CLI capabilities via tools.list
- merge runtime tools with configured MCP and LSP tools in the approval catalog
- keep a fallback builtin runtime tool list when capabilities are unavailable
- move approval-tool pruning to the app service so dynamic tools are not dropped on load
- update approval UI and docs to use the corrected runtime-tool model

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-24 20:16:11 +01:00
co-authored by Copilot
parent 4b7409368f
commit 130b114906
16 changed files with 255 additions and 87 deletions
@@ -69,6 +69,13 @@ public sealed class SidecarModelCapabilityDto
public string? DefaultReasoningEffort { get; init; }
}
public sealed class SidecarRuntimeToolDto
{
public string Id { get; init; } = string.Empty;
public string Label { get; init; } = string.Empty;
public string? Description { get; init; }
}
public sealed class SidecarConnectionDiagnosticsDto
{
public string Status { get; init; } = "copilot-error";
@@ -103,6 +110,7 @@ public sealed class SidecarCapabilitiesDto
public string Runtime { get; init; } = "dotnet-maf";
public Dictionary<string, SidecarModeCapabilityDto> Modes { get; init; } = new(StringComparer.OrdinalIgnoreCase);
public IReadOnlyList<SidecarModelCapabilityDto> Models { get; init; } = [];
public IReadOnlyList<SidecarRuntimeToolDto> RuntimeTools { get; init; } = [];
public SidecarConnectionDiagnosticsDto Connection { get; init; } = new();
}
@@ -2,6 +2,7 @@ using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
using GitHub.Copilot.SDK;
using GitHub.Copilot.SDK.Rpc;
using Eryx.AgentHost.Contracts;
namespace Eryx.AgentHost.Services;
@@ -187,6 +188,7 @@ public sealed class SidecarProtocolHost
private static async Task<SidecarCapabilitiesDto> BuildCapabilitiesAsync(CancellationToken cancellationToken)
{
IReadOnlyList<SidecarModelCapabilityDto> models = [];
IReadOnlyList<SidecarRuntimeToolDto> runtimeTools = [];
CopilotCliContext cliContext;
SidecarConnectionDiagnosticsDto connection;
SidecarCopilotCliVersionDiagnosticsDto? cliVersion = null;
@@ -227,6 +229,14 @@ public sealed class SidecarProtocolHost
cancellationToken).ConfigureAwait(false);
models = await ListAvailableModelsAsync(client, cancellationToken).ConfigureAwait(false);
try
{
runtimeTools = await ListAvailableRuntimeToolsAsync(client, cancellationToken).ConfigureAwait(false);
}
catch (Exception exception)
{
Console.Error.WriteLine($"[eryx sidecar] Failed to list available Copilot runtime tools: {exception.Message}");
}
cliVersion = await cliVersionTask.ConfigureAwait(false);
connection = CreateReadyConnectionDiagnostics(cliContext.CliPath, models.Count, cliVersion, account);
}
@@ -241,6 +251,7 @@ public sealed class SidecarProtocolHost
{
Modes = BuildModeCapabilities(),
Models = models,
RuntimeTools = runtimeTools,
Connection = connection,
};
}
@@ -284,6 +295,24 @@ public sealed class SidecarProtocolHost
.ToList();
}
private static async Task<IReadOnlyList<SidecarRuntimeToolDto>> ListAvailableRuntimeToolsAsync(
CopilotClient client,
CancellationToken cancellationToken)
{
ToolsListResult result = await client.Rpc.Tools.ListAsync(null!, cancellationToken).ConfigureAwait(false);
return result.Tools
.Where(tool => !string.IsNullOrWhiteSpace(tool.Name))
.Select(tool => new SidecarRuntimeToolDto
{
Id = tool.Name.Trim(),
Label = tool.Name.Trim(),
Description = string.IsNullOrWhiteSpace(tool.Description) ? null : tool.Description.Trim(),
})
.DistinctBy(tool => tool.Id, StringComparer.OrdinalIgnoreCase)
.OrderBy(tool => tool.Label, StringComparer.OrdinalIgnoreCase)
.ToList();
}
private static bool IsReasoningEffort(string? value)
{
return value is "low" or "medium" or "high" or "xhigh";
@@ -37,6 +37,10 @@ public sealed class SidecarProtocolHostTests
JsonElement model = Assert.Single(models);
Assert.Equal("gpt-5.4", model.GetProperty("id").GetString());
Assert.Equal("medium", model.GetProperty("defaultReasoningEffort").GetString());
JsonElement[] runtimeTools = capabilities.GetProperty("runtimeTools").EnumerateArray().ToArray();
JsonElement runtimeTool = Assert.Single(runtimeTools);
Assert.Equal("web_fetch", runtimeTool.GetProperty("id").GetString());
Assert.Equal("web_fetch", runtimeTool.GetProperty("label").GetString());
JsonElement connection = capabilities.GetProperty("connection");
Assert.Equal("ready", connection.GetProperty("status").GetString());
Assert.Equal(@"C:\tools\copilot\copilot.exe", connection.GetProperty("copilotCliPath").GetString());
@@ -403,6 +407,15 @@ public sealed class SidecarProtocolHostTests
DefaultReasoningEffort = "medium",
},
],
RuntimeTools =
[
new SidecarRuntimeToolDto
{
Id = "web_fetch",
Label = "web_fetch",
Description = "Fetch content from the web.",
},
],
Connection = new SidecarConnectionDiagnosticsDto
{
Status = "ready",