feat: enable 'always approve for session' for runtime tools

Runtime tool permission requests (read, write, shell, store_memory) now
resolve stable approval names via fallback in the sidecar approval
coordinator, enabling the 'Always approve' button and session-level
auto-approval for built-in tools.

Backend:
- Add fallback tool names for PermissionRequestRead (read),
  PermissionRequestWrite (write), PermissionRequestShell (shell),
  and PermissionRequestMemory (store_memory)
- Add autoApprovedToolName parameter to RequiresToolCallApproval
  for permission-kind-based session approval matching
- Track tool.execution_start events in ToolNamesByCallId for
  supplementary exact-name resolution

Frontend:
- Use approval.toolName ?? approval.permissionKind as fallback key
  in resolveSessionApproval and ApprovalBanner
- Add shell, read, write permission-kind entries to builtin approval
  tool definitions so pruning preserves session overrides

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-27 21:20:14 +01:00
co-authored by Copilot
parent 154787c336
commit 3ec69d990b
9 changed files with 199 additions and 15 deletions
+31
View File
@@ -8,6 +8,7 @@ import {
normalizePendingApproval,
normalizePendingApprovalState,
normalizeSessionApprovalSettings,
pruneSessionApprovalSettings,
dequeuePendingApprovalState,
enqueuePendingApprovalState,
listPendingApprovals,
@@ -209,4 +210,34 @@ describe('approval helpers', () => {
pendingApprovalQueue: undefined,
});
});
test('prune preserves permission-kind approval entries when they are known tools', () => {
const settings = normalizeSessionApprovalSettings({
autoApprovedToolNames: ['read', 'write', 'shell', 'git.status', 'unknown_tool'],
});
const knownToolNames = ['read', 'write', 'shell', 'git.status', 'bash', 'view'];
const pruned = pruneSessionApprovalSettings(settings, knownToolNames);
expect(pruned?.autoApprovedToolNames).toEqual(['read', 'write', 'shell', 'git.status']);
});
test('session approval with permission-kind entries overrides pattern defaults', () => {
const effective = resolveEffectiveApprovalPolicy(
{
rules: [{ kind: 'tool-call' }],
autoApprovedToolNames: ['git.status'],
},
normalizeSessionApprovalSettings({
autoApprovedToolNames: ['read', 'shell'],
}),
);
expect(effective).toEqual({
rules: [{ kind: 'tool-call' }],
autoApprovedToolNames: ['read', 'shell'],
});
expect(approvalPolicyRequiresToolCallApproval(effective, 'agent-1', 'read')).toBe(false);
expect(approvalPolicyRequiresToolCallApproval(effective, 'agent-1', 'shell')).toBe(false);
expect(approvalPolicyRequiresToolCallApproval(effective, 'agent-1', 'write')).toBe(true);
});
});
+15 -1
View File
@@ -258,6 +258,9 @@ describe('tooling settings helpers', () => {
expect(resolveToolLabel('fetch_copilot_cli_documentation')).toBe('Fetch CLI docs');
expect(resolveToolLabel('glob')).toBe('Find files by pattern');
expect(resolveToolLabel('lsp')).toBe('Language server');
expect(resolveToolLabel('shell')).toBe('Shell commands');
expect(resolveToolLabel('read')).toBe('Read files');
expect(resolveToolLabel('write')).toBe('Write files');
});
test('passes through unknown tool IDs as labels', () => {
@@ -269,15 +272,26 @@ describe('tooling settings helpers', () => {
const tools = listApprovalToolDefinitions({ mcpServers: [], lspProfiles: [] });
const builtinTools = tools.filter((t) => t.kind === 'builtin');
expect(builtinTools.length).toBeGreaterThanOrEqual(20);
expect(builtinTools.length).toBeGreaterThanOrEqual(23);
expect(builtinTools.some((t) => t.id === 'bash')).toBe(true);
expect(builtinTools.some((t) => t.id === 'web_fetch')).toBe(true);
expect(builtinTools.some((t) => t.id === 'task')).toBe(true);
expect(builtinTools.some((t) => t.id === 'store_memory')).toBe(true);
// Permission-kind approval categories should be included
expect(builtinTools.some((t) => t.id === 'shell')).toBe(true);
expect(builtinTools.some((t) => t.id === 'read')).toBe(true);
expect(builtinTools.some((t) => t.id === 'write')).toBe(true);
// Labels should be human-readable, not raw IDs
const bashTool = builtinTools.find((t) => t.id === 'bash');
expect(bashTool?.label).toBe('Execute shell commands');
const shellTool = builtinTools.find((t) => t.id === 'shell');
expect(shellTool?.label).toBe('Shell commands');
const readTool = builtinTools.find((t) => t.id === 'read');
expect(readTool?.label).toBe('Read files');
const writeTool = builtinTools.find((t) => t.id === 'write');
expect(writeTool?.label).toBe('Write files');
// Internal tools should not appear in the fallback
expect(builtinTools.some((t) => t.id === 'ask_user')).toBe(false);