mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-26 21:03:58 +02:00
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:
+2
-1
@@ -246,8 +246,9 @@ This is a good example of a cross-cutting concern that spans multiple layers wit
|
|||||||
|
|
||||||
Tooling is deliberately split into two levels:
|
Tooling is deliberately split into two levels:
|
||||||
|
|
||||||
|
- **dynamic runtime tools** reported by the Copilot CLI, with a fallback catalog for startup/offline cases
|
||||||
- **global definitions** for MCP servers and LSP profiles
|
- **global definitions** for MCP servers and LSP profiles
|
||||||
- **pattern defaults** for which known tools can bypass manual approval
|
- **pattern defaults** for which known runtime tools can bypass manual approval
|
||||||
- **per-session overrides** for both tool enablement and tool auto-approval
|
- **per-session overrides** for both tool enablement and tool auto-approval
|
||||||
|
|
||||||
This lets the application treat tooling as reusable workspace capability while still preserving session-level control and safety.
|
This lets the application treat tooling as reusable workspace capability while still preserving session-level control and safety.
|
||||||
|
|||||||
+39
-45
@@ -8,7 +8,7 @@ This builds on the existing approval-checkpoint system:
|
|||||||
|
|
||||||
- `final-response` approvals still work the same way
|
- `final-response` approvals still work the same way
|
||||||
- `tool-call` checkpoints still decide **whether a tool call needs approval at all**
|
- `tool-call` checkpoints still decide **whether a tool call needs approval at all**
|
||||||
- the new feature decides **which known tools can be auto-approved instead of surfacing a manual approval**
|
- the new feature decides **which known runtime tools can be auto-approved instead of surfacing a manual approval**
|
||||||
|
|
||||||
The approval queue work from earlier is unchanged and still applies.
|
The approval queue work from earlier is unchanged and still applies.
|
||||||
|
|
||||||
@@ -80,21 +80,39 @@ Unknown or non-tool-specific permission requests still require manual approval w
|
|||||||
Use the shared helper in `src/shared/domain/tooling.ts`:
|
Use the shared helper in `src/shared/domain/tooling.ts`:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
listApprovalToolDefinitions(workspace.settings.tooling)
|
listApprovalToolDefinitions(workspace.settings.tooling, sidecarCapabilities?.runtimeTools)
|
||||||
```
|
```
|
||||||
|
|
||||||
This is the canonical source for UI rendering and validation.
|
This is the canonical source for UI rendering and validation.
|
||||||
|
|
||||||
|
Important:
|
||||||
|
|
||||||
|
- runtime tools should come from `describeSidecarCapabilities().runtimeTools` when available
|
||||||
|
- the shared helper falls back to a conservative built-in catalog until capabilities load or when the CLI cannot report tools
|
||||||
|
- the helper also merges configured MCP server tools and derived LSP tools
|
||||||
|
|
||||||
Each returned item includes:
|
Each returned item includes:
|
||||||
|
|
||||||
- `id` → the runtime tool identifier used by approvals
|
- `id` → the runtime tool identifier used by approvals
|
||||||
- `label` → human-readable label for the UI
|
- `label` → human-readable label for the UI
|
||||||
- `kind` → `mcp`, `lsp`, or `mixed`
|
- `kind` → `builtin`, `mcp`, `lsp`, or `mixed`
|
||||||
- `providerIds`
|
- `providerIds`
|
||||||
- `providerNames`
|
- `providerNames`
|
||||||
|
|
||||||
Do **not** re-derive tool IDs in the renderer.
|
Do **not** re-derive tool IDs in the renderer.
|
||||||
|
|
||||||
|
### Runtime tool IDs
|
||||||
|
|
||||||
|
For Copilot CLI runtime tools, the ID is the tool name returned by the sidecar capability payload.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `web_fetch`
|
||||||
|
- `view`
|
||||||
|
- `glob`
|
||||||
|
|
||||||
|
Descriptions come from the sidecar when available and can be surfaced in the UI.
|
||||||
|
|
||||||
### MCP tool IDs
|
### MCP tool IDs
|
||||||
|
|
||||||
For MCP tools, the runtime ID is the raw tool name from `server.tools`.
|
For MCP tools, the runtime ID is the raw tool name from `server.tools`.
|
||||||
@@ -157,7 +175,7 @@ The backend now enforces and maintains tool references consistently:
|
|||||||
- saving a pattern rejects unknown `autoApprovedToolNames`
|
- saving a pattern rejects unknown `autoApprovedToolNames`
|
||||||
- updating session approval settings rejects unknown tool IDs
|
- updating session approval settings rejects unknown tool IDs
|
||||||
- scratchpad sessions reject non-empty session tool auto-approval overrides
|
- scratchpad sessions reject non-empty session tool auto-approval overrides
|
||||||
- workspace load prunes stale tool IDs from patterns and sessions
|
- workspace load normalizes approval state and the app service prunes stale tool IDs after runtime tools/capabilities are available
|
||||||
- saving or deleting MCP/LSP definitions also prunes stale tool IDs from patterns and sessions
|
- saving or deleting MCP/LSP definitions also prunes stale tool IDs from patterns and sessions
|
||||||
|
|
||||||
This is intentional: stored approval defaults should only reference tools that still exist.
|
This is intentional: stored approval defaults should only reference tools that still exist.
|
||||||
@@ -180,13 +198,17 @@ Existing UI can keep working without changes, but the UX agent can now surface b
|
|||||||
- `src/shared/domain/approval.ts`
|
- `src/shared/domain/approval.ts`
|
||||||
- pattern defaults, session override model, effective-policy helpers, pruning/validation helpers
|
- pattern defaults, session override model, effective-policy helpers, pruning/validation helpers
|
||||||
- `src/shared/domain/tooling.ts`
|
- `src/shared/domain/tooling.ts`
|
||||||
- canonical `listApprovalToolDefinitions(...)` helper
|
- canonical merged runtime/MCP/LSP `listApprovalToolDefinitions(...)` helper
|
||||||
|
- `src/shared/contracts/sidecar.ts`
|
||||||
|
- runtime tool capability payload
|
||||||
- `src/shared/domain/session.ts`
|
- `src/shared/domain/session.ts`
|
||||||
- session approval settings + effective pattern merge helper
|
- session approval settings + effective pattern merge helper
|
||||||
- `src/main/EryxAppService.ts`
|
- `src/main/EryxAppService.ts`
|
||||||
- pattern validation, session override IPC handler, tooling cleanup, effective-pattern merge
|
- pattern validation, session override IPC handler, tooling cleanup, effective-pattern merge
|
||||||
- `src/main/persistence/workspaceRepository.ts`
|
- `src/main/persistence/workspaceRepository.ts`
|
||||||
- load-time normalization + pruning for stale tool IDs
|
- load-time normalization without prematurely pruning dynamic runtime tool IDs
|
||||||
|
- `sidecar/src/Eryx.AgentHost/Services/SidecarProtocolHost.cs`
|
||||||
|
- dynamic Copilot CLI runtime tool discovery via `tools.list`
|
||||||
- `src/shared/contracts/ipc.ts`
|
- `src/shared/contracts/ipc.ts`
|
||||||
- `src/shared/contracts/channels.ts`
|
- `src/shared/contracts/channels.ts`
|
||||||
- `src\preload\index.ts`
|
- `src\preload\index.ts`
|
||||||
@@ -198,55 +220,27 @@ Existing UI can keep working without changes, but the UX agent can now surface b
|
|||||||
|
|
||||||
## UX work for the frontend agent
|
## UX work for the frontend agent
|
||||||
|
|
||||||
### 1. Pattern editor: default tool auto-approval
|
All UX tasks from this section have been implemented in commit `4ff5cbb`.
|
||||||
|
|
||||||
In `PatternEditor`, add a section that renders `listApprovalToolDefinitions(workspace.settings.tooling)`.
|
### 1. Pattern editor: default tool auto-approval — ✅ Done
|
||||||
|
|
||||||
Recommended behavior:
|
`PatternEditor` renders `listApprovalToolDefinitions(workspace.settings.tooling, sidecarCapabilities?.runtimeTools)` with toggles writing to `pattern.approvalPolicy.autoApprovedToolNames`. Runtime tools come from the sidecar when available and fall back to the shared built-in catalog until capabilities load.
|
||||||
|
|
||||||
- show every available approval tool with a toggle
|
### 2. Activity panel: per-session override — ✅ Done
|
||||||
- write the selected tool IDs to `pattern.approvalPolicy.autoApprovedToolNames`
|
|
||||||
- keep this separate from the existing `tool-call` / `final-response` checkpoint toggles
|
|
||||||
- if there are no tools, show an empty state instead of an interactive list
|
|
||||||
|
|
||||||
### 2. Activity panel: per-session override
|
Right-side Activity panel shows "Inheriting pattern defaults" / "Custom for this session" badge with "Reset to pattern" action. Toggle rows for each tool call `updateSessionApprovalSettings(...)`.
|
||||||
|
|
||||||
In the right-side Activity panel, add a per-session auto-approval section for tools.
|
### 3. Disable while running — ✅ Done
|
||||||
|
|
||||||
Recommended behavior:
|
Per-session override controls disabled when `session.status === 'running'`. Scratchpad shows non-interactive explanation.
|
||||||
|
|
||||||
- base the list on `listApprovalToolDefinitions(workspace.settings.tooling)`
|
### 4. Surface tool context in approval UI — ✅ Done
|
||||||
- show the current **effective** state
|
|
||||||
- distinguish inherited vs overridden state in the UI
|
|
||||||
- allow resetting the session to inherit the pattern defaults
|
|
||||||
- call `updateSessionApprovalSettings(...)` when the user changes the override
|
|
||||||
|
|
||||||
Suggested UX:
|
`approval.toolName` displayed in both the active approval banner and queued approval list when present.
|
||||||
|
|
||||||
- `Inheriting pattern defaults` badge/state when `session.approvalSettings` is `undefined`
|
### 5. Avoid ID duplication logic in the renderer — ✅ Done
|
||||||
- `Custom for this session` badge/state when the override object exists
|
|
||||||
- `Reset to pattern` action that calls `updateSessionApprovalSettings({ sessionId })`
|
|
||||||
|
|
||||||
### 3. Disable while running
|
All tool ID resolution uses `listApprovalToolDefinitions()` output exclusively.
|
||||||
|
|
||||||
Match the current tools behavior:
|
|
||||||
|
|
||||||
- disable the per-session override controls while `session.status === 'running'`
|
|
||||||
- scratchpad sessions should show a non-interactive explanation because tool auto-approval does not apply there
|
|
||||||
|
|
||||||
### 4. Surface tool context in approval UI
|
|
||||||
|
|
||||||
Optional but recommended:
|
|
||||||
|
|
||||||
- show `approval.toolName` in the active approval banner / queued approval list when present
|
|
||||||
- this is now available for tool-specific approval requests
|
|
||||||
|
|
||||||
### 5. Avoid ID duplication logic in the renderer
|
|
||||||
|
|
||||||
Important:
|
|
||||||
|
|
||||||
- do not manually rebuild LSP tool IDs in UI code
|
|
||||||
- use the shared helper output and persist the `id` values from it
|
|
||||||
|
|
||||||
## Validation commands
|
## Validation commands
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ Eryx includes connection status in the app so you can quickly tell whether Copil
|
|||||||
Use a simple single-agent setup to begin, or choose a saved multi-agent pattern when you want a more structured workflow.
|
Use a simple single-agent setup to begin, or choose a saved multi-agent pattern when you want a more structured workflow.
|
||||||
|
|
||||||
5. **Configure optional tooling**
|
5. **Configure optional tooling**
|
||||||
If you want MCP or LSP support, add the global definitions in settings and then enable the ones you want for the current session from the Activity panel. You can also set pattern-level tool auto-approval defaults and override them per session.
|
If you want MCP or LSP support, add the global definitions in settings and then enable the ones you want for the current session from the Activity panel. Eryx also surfaces Copilot CLI runtime tools for tool auto-approval, and you can set pattern-level defaults and override them per session.
|
||||||
|
|
||||||
6. **Start working**
|
6. **Start working**
|
||||||
Ask a question, describe a task, or explore a project. As the run progresses, you can watch the participating agents and keep the session for later.
|
Ask a question, describe a task, or explore a project. As the run progresses, you can watch the participating agents and keep the session for later.
|
||||||
|
|||||||
@@ -69,6 +69,13 @@ public sealed class SidecarModelCapabilityDto
|
|||||||
public string? DefaultReasoningEffort { get; init; }
|
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 sealed class SidecarConnectionDiagnosticsDto
|
||||||
{
|
{
|
||||||
public string Status { get; init; } = "copilot-error";
|
public string Status { get; init; } = "copilot-error";
|
||||||
@@ -103,6 +110,7 @@ public sealed class SidecarCapabilitiesDto
|
|||||||
public string Runtime { get; init; } = "dotnet-maf";
|
public string Runtime { get; init; } = "dotnet-maf";
|
||||||
public Dictionary<string, SidecarModeCapabilityDto> Modes { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, SidecarModeCapabilityDto> Modes { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
public IReadOnlyList<SidecarModelCapabilityDto> Models { get; init; } = [];
|
public IReadOnlyList<SidecarModelCapabilityDto> Models { get; init; } = [];
|
||||||
|
public IReadOnlyList<SidecarRuntimeToolDto> RuntimeTools { get; init; } = [];
|
||||||
public SidecarConnectionDiagnosticsDto Connection { get; init; } = new();
|
public SidecarConnectionDiagnosticsDto Connection { get; init; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Collections.Concurrent;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using GitHub.Copilot.SDK;
|
using GitHub.Copilot.SDK;
|
||||||
|
using GitHub.Copilot.SDK.Rpc;
|
||||||
using Eryx.AgentHost.Contracts;
|
using Eryx.AgentHost.Contracts;
|
||||||
|
|
||||||
namespace Eryx.AgentHost.Services;
|
namespace Eryx.AgentHost.Services;
|
||||||
@@ -187,6 +188,7 @@ public sealed class SidecarProtocolHost
|
|||||||
private static async Task<SidecarCapabilitiesDto> BuildCapabilitiesAsync(CancellationToken cancellationToken)
|
private static async Task<SidecarCapabilitiesDto> BuildCapabilitiesAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
IReadOnlyList<SidecarModelCapabilityDto> models = [];
|
IReadOnlyList<SidecarModelCapabilityDto> models = [];
|
||||||
|
IReadOnlyList<SidecarRuntimeToolDto> runtimeTools = [];
|
||||||
CopilotCliContext cliContext;
|
CopilotCliContext cliContext;
|
||||||
SidecarConnectionDiagnosticsDto connection;
|
SidecarConnectionDiagnosticsDto connection;
|
||||||
SidecarCopilotCliVersionDiagnosticsDto? cliVersion = null;
|
SidecarCopilotCliVersionDiagnosticsDto? cliVersion = null;
|
||||||
@@ -227,6 +229,14 @@ public sealed class SidecarProtocolHost
|
|||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
models = await ListAvailableModelsAsync(client, 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);
|
cliVersion = await cliVersionTask.ConfigureAwait(false);
|
||||||
connection = CreateReadyConnectionDiagnostics(cliContext.CliPath, models.Count, cliVersion, account);
|
connection = CreateReadyConnectionDiagnostics(cliContext.CliPath, models.Count, cliVersion, account);
|
||||||
}
|
}
|
||||||
@@ -241,6 +251,7 @@ public sealed class SidecarProtocolHost
|
|||||||
{
|
{
|
||||||
Modes = BuildModeCapabilities(),
|
Modes = BuildModeCapabilities(),
|
||||||
Models = models,
|
Models = models,
|
||||||
|
RuntimeTools = runtimeTools,
|
||||||
Connection = connection,
|
Connection = connection,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -284,6 +295,24 @@ public sealed class SidecarProtocolHost
|
|||||||
.ToList();
|
.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)
|
private static bool IsReasoningEffort(string? value)
|
||||||
{
|
{
|
||||||
return value is "low" or "medium" or "high" or "xhigh";
|
return value is "low" or "medium" or "high" or "xhigh";
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ public sealed class SidecarProtocolHostTests
|
|||||||
JsonElement model = Assert.Single(models);
|
JsonElement model = Assert.Single(models);
|
||||||
Assert.Equal("gpt-5.4", model.GetProperty("id").GetString());
|
Assert.Equal("gpt-5.4", model.GetProperty("id").GetString());
|
||||||
Assert.Equal("medium", model.GetProperty("defaultReasoningEffort").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");
|
JsonElement connection = capabilities.GetProperty("connection");
|
||||||
Assert.Equal("ready", connection.GetProperty("status").GetString());
|
Assert.Equal("ready", connection.GetProperty("status").GetString());
|
||||||
Assert.Equal(@"C:\tools\copilot\copilot.exe", connection.GetProperty("copilotCliPath").GetString());
|
Assert.Equal(@"C:\tools\copilot\copilot.exe", connection.GetProperty("copilotCliPath").GetString());
|
||||||
@@ -403,6 +407,15 @@ public sealed class SidecarProtocolHostTests
|
|||||||
DefaultReasoningEffort = "medium",
|
DefaultReasoningEffort = "medium",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
RuntimeTools =
|
||||||
|
[
|
||||||
|
new SidecarRuntimeToolDto
|
||||||
|
{
|
||||||
|
Id = "web_fetch",
|
||||||
|
Label = "web_fetch",
|
||||||
|
Description = "Fetch content from the web.",
|
||||||
|
},
|
||||||
|
],
|
||||||
Connection = new SidecarConnectionDiagnosticsDto
|
Connection = new SidecarConnectionDiagnosticsDto
|
||||||
{
|
{
|
||||||
Status = "ready",
|
Status = "ready",
|
||||||
|
|||||||
+44
-13
@@ -103,6 +103,16 @@ function isBuiltinPattern(patternId: string): boolean {
|
|||||||
return patternId.startsWith('pattern-');
|
return patternId.startsWith('pattern-');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function equalStringArrays(left?: readonly string[], right?: readonly string[]): boolean {
|
||||||
|
const normalizedLeft = left ?? [];
|
||||||
|
const normalizedRight = right ?? [];
|
||||||
|
if (normalizedLeft.length !== normalizedRight.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
||||||
|
}
|
||||||
|
|
||||||
export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
||||||
private readonly workspaceRepository = new WorkspaceRepository();
|
private readonly workspaceRepository = new WorkspaceRepository();
|
||||||
private readonly sidecar = new SidecarClient();
|
private readonly sidecar = new SidecarClient();
|
||||||
@@ -124,7 +134,8 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
async loadWorkspace(): Promise<WorkspaceState> {
|
async loadWorkspace(): Promise<WorkspaceState> {
|
||||||
if (!this.workspace) {
|
if (!this.workspace) {
|
||||||
this.workspace = await this.workspaceRepository.load();
|
this.workspace = await this.workspaceRepository.load();
|
||||||
if (this.failInterruptedPendingApprovals(this.workspace)) {
|
const didPruneApprovalTools = await this.pruneUnavailableApprovalTools(this.workspace);
|
||||||
|
if (didPruneApprovalTools || this.failInterruptedPendingApprovals(this.workspace)) {
|
||||||
await this.workspaceRepository.save(this.workspace);
|
await this.workspaceRepository.save(this.workspace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,9 +211,10 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
|
|
||||||
async savePattern(pattern: PatternDefinition): Promise<WorkspaceState> {
|
async savePattern(pattern: PatternDefinition): Promise<WorkspaceState> {
|
||||||
const workspace = await this.loadWorkspace();
|
const workspace = await this.loadWorkspace();
|
||||||
|
const knownApprovalToolNames = await this.listKnownApprovalToolNames(workspace);
|
||||||
const issues = validatePatternDefinition(
|
const issues = validatePatternDefinition(
|
||||||
pattern,
|
pattern,
|
||||||
this.listKnownApprovalToolNames(workspace),
|
knownApprovalToolNames,
|
||||||
).filter((issue) => issue.level === 'error');
|
).filter((issue) => issue.level === 'error');
|
||||||
if (issues.length > 0) {
|
if (issues.length > 0) {
|
||||||
throw new Error(issues[0].message);
|
throw new Error(issues[0].message);
|
||||||
@@ -281,7 +293,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
workspace.settings.tooling.mcpServers.push(candidate);
|
workspace.settings.tooling.mcpServers.push(candidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pruneUnavailableApprovalTools(workspace);
|
await this.pruneUnavailableApprovalTools(workspace);
|
||||||
return this.persistAndBroadcast(workspace);
|
return this.persistAndBroadcast(workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +311,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pruneUnavailableApprovalTools(workspace);
|
await this.pruneUnavailableApprovalTools(workspace);
|
||||||
return this.persistAndBroadcast(workspace);
|
return this.persistAndBroadcast(workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +340,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
workspace.settings.tooling.lspProfiles.push(candidate);
|
workspace.settings.tooling.lspProfiles.push(candidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pruneUnavailableApprovalTools(workspace);
|
await this.pruneUnavailableApprovalTools(workspace);
|
||||||
return this.persistAndBroadcast(workspace);
|
return this.persistAndBroadcast(workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +358,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pruneUnavailableApprovalTools(workspace);
|
await this.pruneUnavailableApprovalTools(workspace);
|
||||||
return this.persistAndBroadcast(workspace);
|
return this.persistAndBroadcast(workspace);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,7 +715,7 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
throw new Error('Scratchpad sessions do not support tool auto-approval settings.');
|
throw new Error('Scratchpad sessions do not support tool auto-approval settings.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const knownToolNames = new Set(this.listKnownApprovalToolNames(workspace));
|
const knownToolNames = new Set(await this.listKnownApprovalToolNames(workspace));
|
||||||
const unknownToolName = settings?.autoApprovedToolNames.find((toolName) => !knownToolNames.has(toolName));
|
const unknownToolName = settings?.autoApprovedToolNames.find((toolName) => !knownToolNames.has(toolName));
|
||||||
if (unknownToolName) {
|
if (unknownToolName) {
|
||||||
throw new Error(`Unknown approval tool "${unknownToolName}".`);
|
throw new Error(`Unknown approval tool "${unknownToolName}".`);
|
||||||
@@ -1172,23 +1184,42 @@ export class EryxAppService extends EventEmitter<AppServiceEvents> {
|
|||||||
return normalizePatternModels(patternWithApprovalSettings, modelCatalog);
|
return normalizePatternModels(patternWithApprovalSettings, modelCatalog);
|
||||||
}
|
}
|
||||||
|
|
||||||
private listKnownApprovalToolNames(workspace: WorkspaceState): string[] {
|
private async listKnownApprovalToolNames(workspace: WorkspaceState): Promise<string[]> {
|
||||||
return listApprovalToolNames(workspace.settings.tooling);
|
const capabilities = await this.loadSidecarCapabilities();
|
||||||
|
const runtimeTools = capabilities.runtimeTools.length > 0 ? capabilities.runtimeTools : undefined;
|
||||||
|
return listApprovalToolNames(workspace.settings.tooling, runtimeTools);
|
||||||
}
|
}
|
||||||
|
|
||||||
private pruneUnavailableApprovalTools(workspace: WorkspaceState): void {
|
private async pruneUnavailableApprovalTools(workspace: WorkspaceState): Promise<boolean> {
|
||||||
const knownToolNames = this.listKnownApprovalToolNames(workspace);
|
const knownToolNames = await this.listKnownApprovalToolNames(workspace);
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
for (const pattern of workspace.patterns) {
|
for (const pattern of workspace.patterns) {
|
||||||
pattern.approvalPolicy = pruneApprovalPolicyTools(pattern.approvalPolicy, knownToolNames);
|
const nextPolicy = pruneApprovalPolicyTools(pattern.approvalPolicy, knownToolNames);
|
||||||
|
if (!equalStringArrays(
|
||||||
|
pattern.approvalPolicy?.autoApprovedToolNames,
|
||||||
|
nextPolicy?.autoApprovedToolNames,
|
||||||
|
)) {
|
||||||
|
pattern.approvalPolicy = nextPolicy;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const session of workspace.sessions) {
|
for (const session of workspace.sessions) {
|
||||||
session.approvalSettings = pruneSessionApprovalSettings(
|
const nextSettings = pruneSessionApprovalSettings(
|
||||||
session.approvalSettings,
|
session.approvalSettings,
|
||||||
knownToolNames,
|
knownToolNames,
|
||||||
);
|
);
|
||||||
|
if (!equalStringArrays(
|
||||||
|
session.approvalSettings?.autoApprovedToolNames,
|
||||||
|
nextSettings?.autoApprovedToolNames,
|
||||||
|
)) {
|
||||||
|
session.approvalSettings = nextSettings;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildRunTurnToolingConfig(
|
private buildRunTurnToolingConfig(
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import type { PatternDefinition } from '@shared/domain/pattern';
|
|||||||
import { mergeScratchpadProject } from '@shared/domain/project';
|
import { mergeScratchpadProject } from '@shared/domain/project';
|
||||||
import { normalizeSessionRunRecords } from '@shared/domain/runTimeline';
|
import { normalizeSessionRunRecords } from '@shared/domain/runTimeline';
|
||||||
import {
|
import {
|
||||||
listApprovalToolNames,
|
|
||||||
normalizeSessionToolingSelection,
|
normalizeSessionToolingSelection,
|
||||||
normalizeWorkspaceSettings,
|
normalizeWorkspaceSettings,
|
||||||
} from '@shared/domain/tooling';
|
} from '@shared/domain/tooling';
|
||||||
@@ -13,8 +12,6 @@ import {
|
|||||||
normalizeApprovalPolicy,
|
normalizeApprovalPolicy,
|
||||||
normalizePendingApprovalState,
|
normalizePendingApprovalState,
|
||||||
normalizeSessionApprovalSettings,
|
normalizeSessionApprovalSettings,
|
||||||
pruneApprovalPolicyTools,
|
|
||||||
pruneSessionApprovalSettings,
|
|
||||||
} from '@shared/domain/approval';
|
} from '@shared/domain/approval';
|
||||||
import { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
|
import { createWorkspaceSeed, type WorkspaceState } from '@shared/domain/workspace';
|
||||||
import { nowIso } from '@shared/utils/ids';
|
import { nowIso } from '@shared/utils/ids';
|
||||||
@@ -68,26 +65,19 @@ export class WorkspaceRepository {
|
|||||||
|
|
||||||
const projects = mergeScratchpadProject(stored.projects ?? [], this.scratchpadPath);
|
const projects = mergeScratchpadProject(stored.projects ?? [], this.scratchpadPath);
|
||||||
const settings = normalizeWorkspaceSettings(stored.settings);
|
const settings = normalizeWorkspaceSettings(stored.settings);
|
||||||
const knownToolNames = listApprovalToolNames(settings.tooling);
|
|
||||||
|
|
||||||
const workspace: WorkspaceState = {
|
const workspace: WorkspaceState = {
|
||||||
...stored,
|
...stored,
|
||||||
patterns: mergePatterns(stored.patterns ?? []).map((pattern) => ({
|
patterns: mergePatterns(stored.patterns ?? []).map((pattern) => ({
|
||||||
...pattern,
|
...pattern,
|
||||||
approvalPolicy: pruneApprovalPolicyTools(
|
approvalPolicy: normalizeApprovalPolicy(pattern.approvalPolicy),
|
||||||
normalizeApprovalPolicy(pattern.approvalPolicy),
|
|
||||||
knownToolNames,
|
|
||||||
),
|
|
||||||
})),
|
})),
|
||||||
projects,
|
projects,
|
||||||
sessions: (stored.sessions ?? []).map((session) => ({
|
sessions: (stored.sessions ?? []).map((session) => ({
|
||||||
...session,
|
...session,
|
||||||
runs: normalizeSessionRunRecords(session.runs),
|
runs: normalizeSessionRunRecords(session.runs),
|
||||||
tooling: normalizeSessionToolingSelection(session.tooling),
|
tooling: normalizeSessionToolingSelection(session.tooling),
|
||||||
approvalSettings: pruneSessionApprovalSettings(
|
approvalSettings: normalizeSessionApprovalSettings(session.approvalSettings),
|
||||||
normalizeSessionApprovalSettings(session.approvalSettings),
|
|
||||||
knownToolNames,
|
|
||||||
),
|
|
||||||
...normalizePendingApprovalState({
|
...normalizePendingApprovalState({
|
||||||
pendingApproval: session.pendingApproval,
|
pendingApproval: session.pendingApproval,
|
||||||
pendingApprovalQueue: session.pendingApprovalQueue,
|
pendingApprovalQueue: session.pendingApprovalQueue,
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ export default function App() {
|
|||||||
lspProfiles={workspace.settings.tooling.lspProfiles}
|
lspProfiles={workspace.settings.tooling.lspProfiles}
|
||||||
mcpServers={workspace.settings.tooling.mcpServers}
|
mcpServers={workspace.settings.tooling.mcpServers}
|
||||||
toolingSettings={workspace.settings.tooling}
|
toolingSettings={workspace.settings.tooling}
|
||||||
|
runtimeTools={sidecarCapabilities?.runtimeTools}
|
||||||
onJumpToMessage={jumpToMessage}
|
onJumpToMessage={jumpToMessage}
|
||||||
onUpdateSessionTooling={(selection) => {
|
onUpdateSessionTooling={(selection) => {
|
||||||
void api.updateSessionTooling({
|
void api.updateSessionTooling({
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
LspProfileDefinition,
|
LspProfileDefinition,
|
||||||
McpServerDefinition,
|
McpServerDefinition,
|
||||||
|
RuntimeToolDefinition,
|
||||||
SessionToolingSelection,
|
SessionToolingSelection,
|
||||||
WorkspaceToolingSettings,
|
WorkspaceToolingSettings,
|
||||||
} from '@shared/domain/tooling';
|
} from '@shared/domain/tooling';
|
||||||
@@ -163,6 +164,7 @@ interface ActivityPanelProps {
|
|||||||
lspProfiles: LspProfileDefinition[];
|
lspProfiles: LspProfileDefinition[];
|
||||||
mcpServers: McpServerDefinition[];
|
mcpServers: McpServerDefinition[];
|
||||||
toolingSettings: WorkspaceToolingSettings;
|
toolingSettings: WorkspaceToolingSettings;
|
||||||
|
runtimeTools?: ReadonlyArray<RuntimeToolDefinition>;
|
||||||
onJumpToMessage?: (messageId: string) => void;
|
onJumpToMessage?: (messageId: string) => void;
|
||||||
onUpdateSessionTooling: (selection: SessionToolingSelection) => void;
|
onUpdateSessionTooling: (selection: SessionToolingSelection) => void;
|
||||||
onUpdateSessionApprovalSettings: (settings: { autoApprovedToolNames?: string[] }) => void;
|
onUpdateSessionApprovalSettings: (settings: { autoApprovedToolNames?: string[] }) => void;
|
||||||
@@ -176,6 +178,7 @@ export function ActivityPanel({
|
|||||||
lspProfiles,
|
lspProfiles,
|
||||||
mcpServers,
|
mcpServers,
|
||||||
toolingSettings,
|
toolingSettings,
|
||||||
|
runtimeTools,
|
||||||
onJumpToMessage,
|
onJumpToMessage,
|
||||||
onUpdateSessionTooling,
|
onUpdateSessionTooling,
|
||||||
onUpdateSessionApprovalSettings,
|
onUpdateSessionApprovalSettings,
|
||||||
@@ -188,7 +191,10 @@ export function ActivityPanel({
|
|||||||
[activity, pattern.agents],
|
[activity, pattern.agents],
|
||||||
);
|
);
|
||||||
const selection = useMemo(() => resolveSessionToolingSelection(session), [session]);
|
const selection = useMemo(() => resolveSessionToolingSelection(session), [session]);
|
||||||
const approvalTools = useMemo(() => listApprovalToolDefinitions(toolingSettings), [toolingSettings]);
|
const approvalTools = useMemo(
|
||||||
|
() => listApprovalToolDefinitions(toolingSettings, runtimeTools),
|
||||||
|
[runtimeTools, toolingSettings],
|
||||||
|
);
|
||||||
|
|
||||||
const isOverridden = session.approvalSettings !== undefined;
|
const isOverridden = session.approvalSettings !== undefined;
|
||||||
const effectiveAutoApproved = new Set(
|
const effectiveAutoApproved = new Set(
|
||||||
@@ -353,7 +359,7 @@ export function ActivityPanel({
|
|||||||
</p>
|
</p>
|
||||||
) : approvalTools.length === 0 ? (
|
) : approvalTools.length === 0 ? (
|
||||||
<p className="text-[11px] leading-relaxed text-zinc-600">
|
<p className="text-[11px] leading-relaxed text-zinc-600">
|
||||||
Add MCP servers or LSP profiles in Settings to configure tool auto-approvals.
|
No approval-capable runtime tools are currently available.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -478,7 +484,13 @@ function ApprovalOverrideRow({
|
|||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
}) {
|
}) {
|
||||||
const kindBadge = tool.kind === 'lsp' ? 'LSP' : tool.kind === 'mcp' ? 'MCP' : 'Mixed';
|
const kindBadge = tool.kind === 'builtin'
|
||||||
|
? 'Built-in'
|
||||||
|
: tool.kind === 'lsp'
|
||||||
|
? 'LSP'
|
||||||
|
: tool.kind === 'mcp'
|
||||||
|
? 'MCP'
|
||||||
|
: 'Mixed';
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition ${
|
className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left transition ${
|
||||||
@@ -496,6 +508,11 @@ function ApprovalOverrideRow({
|
|||||||
{kindBadge}
|
{kindBadge}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{(tool.description || tool.providerNames.length > 0) && (
|
||||||
|
<div className="truncate text-[10px] text-zinc-600">
|
||||||
|
{tool.description ?? tool.providerNames.join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch enabled={enabled} />
|
<ToggleSwitch enabled={enabled} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
listApprovalToolDefinitions,
|
listApprovalToolDefinitions,
|
||||||
type ApprovalToolDefinition,
|
type ApprovalToolDefinition,
|
||||||
|
type RuntimeToolDefinition,
|
||||||
type WorkspaceToolingSettings,
|
type WorkspaceToolingSettings,
|
||||||
} from '@shared/domain/tooling';
|
} from '@shared/domain/tooling';
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ interface PatternEditorProps {
|
|||||||
pattern: PatternDefinition;
|
pattern: PatternDefinition;
|
||||||
isBuiltin: boolean;
|
isBuiltin: boolean;
|
||||||
toolingSettings: WorkspaceToolingSettings;
|
toolingSettings: WorkspaceToolingSettings;
|
||||||
|
runtimeTools?: ReadonlyArray<RuntimeToolDefinition>;
|
||||||
onChange: (pattern: PatternDefinition) => void;
|
onChange: (pattern: PatternDefinition) => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
@@ -209,6 +211,7 @@ export function PatternEditor({
|
|||||||
pattern,
|
pattern,
|
||||||
isBuiltin,
|
isBuiltin,
|
||||||
toolingSettings,
|
toolingSettings,
|
||||||
|
runtimeTools,
|
||||||
onChange,
|
onChange,
|
||||||
onDelete,
|
onDelete,
|
||||||
onSave,
|
onSave,
|
||||||
@@ -262,7 +265,7 @@ export function PatternEditor({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const approvalTools = listApprovalToolDefinitions(toolingSettings);
|
const approvalTools = listApprovalToolDefinitions(toolingSettings, runtimeTools);
|
||||||
const autoApprovedSet = new Set(pattern.approvalPolicy?.autoApprovedToolNames ?? []);
|
const autoApprovedSet = new Set(pattern.approvalPolicy?.autoApprovedToolNames ?? []);
|
||||||
|
|
||||||
function toggleToolAutoApproval(toolId: string) {
|
function toggleToolAutoApproval(toolId: string) {
|
||||||
@@ -568,7 +571,7 @@ export function PatternEditor({
|
|||||||
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 px-4 py-3">
|
<div className="rounded-xl border border-zinc-800 bg-zinc-900/50 px-4 py-3">
|
||||||
{approvalTools.length === 0 ? (
|
{approvalTools.length === 0 ? (
|
||||||
<p className="py-2 text-center text-[11px] text-zinc-600">
|
<p className="py-2 text-center text-[11px] text-zinc-600">
|
||||||
No tools available. Add MCP servers or LSP profiles in Settings to configure auto-approvals.
|
No approval-capable runtime tools are currently available.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
@@ -719,7 +722,13 @@ function ToolApprovalToggleRow({
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
}) {
|
}) {
|
||||||
const kindBadge = tool.kind === 'lsp' ? 'LSP' : tool.kind === 'mcp' ? 'MCP' : 'Mixed';
|
const kindBadge = tool.kind === 'builtin'
|
||||||
|
? 'Built-in'
|
||||||
|
: tool.kind === 'lsp'
|
||||||
|
? 'LSP'
|
||||||
|
: tool.kind === 'mcp'
|
||||||
|
? 'MCP'
|
||||||
|
: 'Mixed';
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="flex w-full items-center gap-2.5 rounded-lg px-2 py-1.5 text-left transition hover:bg-zinc-800/60"
|
className="flex w-full items-center gap-2.5 rounded-lg px-2 py-1.5 text-left transition hover:bg-zinc-800/60"
|
||||||
@@ -733,8 +742,10 @@ function ToolApprovalToggleRow({
|
|||||||
{kindBadge}
|
{kindBadge}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{tool.providerNames.length > 0 && (
|
{(tool.description || tool.providerNames.length > 0) && (
|
||||||
<div className="truncate text-[10px] text-zinc-600">{tool.providerNames.join(', ')}</div>
|
<div className="truncate text-[10px] text-zinc-600">
|
||||||
|
{tool.description ?? tool.providerNames.join(', ')}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch enabled={enabled} onToggle={onToggle} />
|
<ToggleSwitch enabled={enabled} onToggle={onToggle} />
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ export function SettingsPanel({
|
|||||||
setEditingPattern(null);
|
setEditingPattern(null);
|
||||||
}}
|
}}
|
||||||
pattern={editingPattern}
|
pattern={editingPattern}
|
||||||
|
runtimeTools={sidecarCapabilities?.runtimeTools}
|
||||||
toolingSettings={toolingSettings}
|
toolingSettings={toolingSettings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { PatternDefinition, PatternValidationIssue, ReasoningEffort } from '@shared/domain/pattern';
|
import type { PatternDefinition, PatternValidationIssue, ReasoningEffort } from '@shared/domain/pattern';
|
||||||
import type { ApprovalCheckpointKind, ApprovalDecision } from '@shared/domain/approval';
|
import type { ApprovalCheckpointKind, ApprovalDecision } from '@shared/domain/approval';
|
||||||
import type { ChatMessageRecord } from '@shared/domain/session';
|
import type { ChatMessageRecord } from '@shared/domain/session';
|
||||||
|
import type { RuntimeToolDefinition } from '@shared/domain/tooling';
|
||||||
|
|
||||||
export interface SidecarModeCapability {
|
export interface SidecarModeCapability {
|
||||||
available: boolean;
|
available: boolean;
|
||||||
@@ -52,6 +53,7 @@ export interface SidecarCapabilities {
|
|||||||
runtime: 'dotnet-maf';
|
runtime: 'dotnet-maf';
|
||||||
modes: Record<PatternDefinition['mode'], SidecarModeCapability>;
|
modes: Record<PatternDefinition['mode'], SidecarModeCapability>;
|
||||||
models: SidecarModelCapability[];
|
models: SidecarModelCapability[];
|
||||||
|
runtimeTools: RuntimeToolDefinition[];
|
||||||
connection: SidecarConnectionDiagnostics;
|
connection: SidecarConnectionDiagnostics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,13 @@ export interface SessionToolingSelection {
|
|||||||
enabledLspProfileIds: string[];
|
enabledLspProfileIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ApprovalToolKind = 'mcp' | 'lsp' | 'mixed';
|
export type ApprovalToolKind = 'builtin' | 'mcp' | 'lsp' | 'mixed';
|
||||||
|
|
||||||
|
export interface RuntimeToolDefinition {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApprovalToolDefinition {
|
export interface ApprovalToolDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -60,6 +66,7 @@ export interface ApprovalToolDefinition {
|
|||||||
kind: ApprovalToolKind;
|
kind: ApprovalToolKind;
|
||||||
providerIds: string[];
|
providerIds: string[];
|
||||||
providerNames: string[];
|
providerNames: string[];
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lspApprovalOperations = [
|
const lspApprovalOperations = [
|
||||||
@@ -70,6 +77,17 @@ const lspApprovalOperations = [
|
|||||||
{ suffix: 'references', label: 'References' },
|
{ suffix: 'references', label: 'References' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
// Fallback runtime tools used before sidecar capabilities are loaded or when the
|
||||||
|
// CLI cannot report its built-in tool catalog dynamically.
|
||||||
|
const fallbackRuntimeApprovalTools: ReadonlyArray<RuntimeToolDefinition> = [
|
||||||
|
{ id: 'glob', label: 'glob', description: 'Match files by glob pattern.' },
|
||||||
|
{ id: 'lsp', label: 'lsp', description: 'Query configured language servers.' },
|
||||||
|
{ id: 'rg', label: 'rg', description: 'Search file contents with ripgrep.' },
|
||||||
|
{ id: 'view', label: 'view', description: 'Read files and list directories.' },
|
||||||
|
{ id: 'web_fetch', label: 'web_fetch', description: 'Fetch content from a URL.' },
|
||||||
|
{ id: 'web_search', label: 'web_search', description: 'Search the web for current information.' },
|
||||||
|
];
|
||||||
|
|
||||||
export function createWorkspaceSettings(): WorkspaceSettings {
|
export function createWorkspaceSettings(): WorkspaceSettings {
|
||||||
return {
|
return {
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
@@ -114,8 +132,21 @@ export function normalizeSessionToolingSelection(
|
|||||||
|
|
||||||
export function listApprovalToolDefinitions(
|
export function listApprovalToolDefinitions(
|
||||||
tooling: WorkspaceToolingSettings,
|
tooling: WorkspaceToolingSettings,
|
||||||
|
runtimeTools: ReadonlyArray<RuntimeToolDefinition> = fallbackRuntimeApprovalTools,
|
||||||
): ApprovalToolDefinition[] {
|
): ApprovalToolDefinition[] {
|
||||||
const toolsById = new Map<string, ApprovalToolDefinition>();
|
const toolsById = new Map<string, ApprovalToolDefinition>();
|
||||||
|
const runtimeApprovalTools = runtimeTools.length > 0 ? runtimeTools : fallbackRuntimeApprovalTools;
|
||||||
|
|
||||||
|
for (const tool of runtimeApprovalTools) {
|
||||||
|
registerApprovalTool(toolsById, {
|
||||||
|
id: tool.id,
|
||||||
|
label: tool.label,
|
||||||
|
description: tool.description,
|
||||||
|
kind: 'builtin',
|
||||||
|
providerId: `builtin:${tool.id}`,
|
||||||
|
providerName: 'Built-in',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (const server of tooling.mcpServers) {
|
for (const server of tooling.mcpServers) {
|
||||||
for (const toolName of normalizeStringArray(server.tools)) {
|
for (const toolName of normalizeStringArray(server.tools)) {
|
||||||
@@ -146,8 +177,11 @@ export function listApprovalToolDefinitions(
|
|||||||
left.label.localeCompare(right.label) || left.id.localeCompare(right.id));
|
left.label.localeCompare(right.label) || left.id.localeCompare(right.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listApprovalToolNames(tooling: WorkspaceToolingSettings): string[] {
|
export function listApprovalToolNames(
|
||||||
return listApprovalToolDefinitions(tooling).map((tool) => tool.id);
|
tooling: WorkspaceToolingSettings,
|
||||||
|
runtimeTools?: ReadonlyArray<RuntimeToolDefinition>,
|
||||||
|
): string[] {
|
||||||
|
return listApprovalToolDefinitions(tooling, runtimeTools).map((tool) => tool.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateMcpServerDefinition(server: McpServerDefinition): string | undefined {
|
export function validateMcpServerDefinition(server: McpServerDefinition): string | undefined {
|
||||||
@@ -276,6 +310,7 @@ function registerApprovalTool(
|
|||||||
tool: {
|
tool: {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
description?: string;
|
||||||
kind: Exclude<ApprovalToolKind, 'mixed'>;
|
kind: Exclude<ApprovalToolKind, 'mixed'>;
|
||||||
providerId: string;
|
providerId: string;
|
||||||
providerName: string;
|
providerName: string;
|
||||||
@@ -286,6 +321,7 @@ function registerApprovalTool(
|
|||||||
toolsById.set(tool.id, {
|
toolsById.set(tool.id, {
|
||||||
id: tool.id,
|
id: tool.id,
|
||||||
label: tool.label,
|
label: tool.label,
|
||||||
|
description: tool.description,
|
||||||
kind: tool.kind,
|
kind: tool.kind,
|
||||||
providerIds: [tool.providerId],
|
providerIds: [tool.providerId],
|
||||||
providerNames: [tool.providerName],
|
providerNames: [tool.providerName],
|
||||||
@@ -299,6 +335,9 @@ function registerApprovalTool(
|
|||||||
if (!existing.providerNames.includes(tool.providerName)) {
|
if (!existing.providerNames.includes(tool.providerName)) {
|
||||||
existing.providerNames.push(tool.providerName);
|
existing.providerNames.push(tool.providerName);
|
||||||
}
|
}
|
||||||
|
if (!existing.description && tool.description) {
|
||||||
|
existing.description = tool.description;
|
||||||
|
}
|
||||||
if (existing.kind !== tool.kind) {
|
if (existing.kind !== tool.kind) {
|
||||||
existing.kind = 'mixed';
|
existing.kind = 'mixed';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,9 +137,9 @@ describe('pattern validation', () => {
|
|||||||
...singlePattern!,
|
...singlePattern!,
|
||||||
approvalPolicy: {
|
approvalPolicy: {
|
||||||
rules: [{ kind: 'tool-call' }],
|
rules: [{ kind: 'tool-call' }],
|
||||||
autoApprovedToolNames: ['git.status', 'unknown.tool'],
|
autoApprovedToolNames: ['web_fetch', 'unknown.tool'],
|
||||||
},
|
},
|
||||||
}, ['git.status']);
|
}, ['web_fetch']);
|
||||||
|
|
||||||
expect(issues.find((issue) => issue.field === 'approvalPolicy')?.message).toBe(
|
expect(issues.find((issue) => issue.field === 'approvalPolicy')?.message).toBe(
|
||||||
'Approval auto-approve references unknown tool "unknown.tool".',
|
'Approval auto-approve references unknown tool "unknown.tool".',
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ describe('tooling settings helpers', () => {
|
|||||||
).toBe('LSP profile "Typescript LSP" needs the "--stdio" argument.');
|
).toBe('LSP profile "Typescript LSP" needs the "--stdio" argument.');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('lists approval tools from MCP and LSP definitions using runtime tool identifiers', () => {
|
test('lists builtin, MCP, and LSP approval tools using runtime tool identifiers', () => {
|
||||||
const tools = listApprovalToolDefinitions({
|
const tools = listApprovalToolDefinitions({
|
||||||
mcpServers: [
|
mcpServers: [
|
||||||
{
|
{
|
||||||
@@ -195,6 +195,14 @@ describe('tooling settings helpers', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(tools).toContainEqual({
|
||||||
|
id: 'web_fetch',
|
||||||
|
label: 'web_fetch',
|
||||||
|
description: 'Fetch content from a URL.',
|
||||||
|
kind: 'builtin',
|
||||||
|
providerIds: ['builtin:web_fetch'],
|
||||||
|
providerNames: ['Built-in'],
|
||||||
|
});
|
||||||
expect(tools).toContainEqual({
|
expect(tools).toContainEqual({
|
||||||
id: 'git.status',
|
id: 'git.status',
|
||||||
label: 'git.status',
|
label: 'git.status',
|
||||||
@@ -210,4 +218,27 @@ describe('tooling settings helpers', () => {
|
|||||||
providerNames: ['TypeScript'],
|
providerNames: ['TypeScript'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('prefers dynamically reported runtime tools over the fallback builtin catalog', () => {
|
||||||
|
const tools = listApprovalToolDefinitions(
|
||||||
|
{ mcpServers: [], lspProfiles: [] },
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 'fetch',
|
||||||
|
label: 'fetch',
|
||||||
|
description: 'Dynamic runtime tool from Copilot CLI.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tools).toContainEqual({
|
||||||
|
id: 'fetch',
|
||||||
|
label: 'fetch',
|
||||||
|
description: 'Dynamic runtime tool from Copilot CLI.',
|
||||||
|
kind: 'builtin',
|
||||||
|
providerIds: ['builtin:fetch'],
|
||||||
|
providerNames: ['Built-in'],
|
||||||
|
});
|
||||||
|
expect(tools.some((tool) => tool.id === 'web_fetch')).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user