From 9f8c2ebc1e3748a55cd7736d312c24cd8fb963a2 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Mon, 21 Sep 2026 20:39:23 -0500 Subject: [PATCH 01/21] [Feat] Auto mode for tool approvals: a deployment setting that lets the decision model answer Ask first calls --- .../tool-approval-enforcement.test.ts | 12 +- .../handlers/mcp/tool-approval-enforcement.ts | 3 +- .../IntegrationToolApprovalControls.tsx | 15 +- ...rationToolApprovalsExperimentalSetting.tsx | 8 +- ...grationToolAutoModeSetting.client.test.tsx | 84 +++++++++++ .../IntegrationToolAutoModeSetting.tsx | 139 +++++++++++++++++ .../McpToolManagementDialog.client.test.tsx | 2 - .../integration-tool-policies/index.ts | 50 ++++++- apps/web/src/trpc/routers/_app.ts | 11 ++ .../opencode-server-tool-approvals.test.ts | 2 + .../lib/harnesses/opencode-server/harness.ts | 4 + .../opencode-server/tool-approvals.ts | 4 + .../integration-tool-auto-evaluation.test.ts | 57 ++++++- .../fast-agent-tool-approvals.test.ts | 114 ++++++++------ .../server/fast-agent/fast-agent-service.ts | 1 - .../fast-agent/fast-agent-tool-approvals.ts | 65 ++++---- .../integration-tool-auto-evaluation.ts | 54 ++++++- .../integration-tool-approvals.test.ts | 87 ++++++----- .../db/src/lib/integration-tool-approvals.ts | 38 +++-- .../src/lib/integration-tool-auto-settings.ts | 54 +++++++ packages/db/src/schema.ts | 22 ++- packages/db/src/server.ts | 1 + .../lib/__tests__/task-tool-approvals.test.ts | 141 +++++++----------- .../sdk/src/server/lib/task-tool-approvals.ts | 118 ++++++--------- .../sdk/src/server/routers/mcp-connections.ts | 8 +- .../sdk/src/server/routers/tool-approvals.ts | 12 +- packages/sdk/src/tool-approvals.ts | 1 + ...integration-tool-policy-strictness.test.ts | 26 +--- .../task-integration-tool-approvals.test.ts | 19 +-- .../types/src/integration-tool-approvals.ts | 51 ++++--- 30 files changed, 784 insertions(+), 419 deletions(-) create mode 100644 apps/web/src/components/settings/IntegrationToolAutoModeSetting.client.test.tsx create mode 100644 apps/web/src/components/settings/IntegrationToolAutoModeSetting.tsx create mode 100644 packages/db/src/lib/integration-tool-auto-settings.ts diff --git a/apps/api/src/handlers/mcp/__tests__/tool-approval-enforcement.test.ts b/apps/api/src/handlers/mcp/__tests__/tool-approval-enforcement.test.ts index d5838dd2ba..d1f3048daf 100644 --- a/apps/api/src/handlers/mcp/__tests__/tool-approval-enforcement.test.ts +++ b/apps/api/src/handlers/mcp/__tests__/tool-approval-enforcement.test.ts @@ -33,7 +33,7 @@ import { const policy = ( integrationId: string, toolName: string, - mode: 'auto' | 'ask' | 'reject', + mode: 'ask' | 'reject', ) => ({ integrationId, toolName, mode }); describe('resolveProxyToolApprovalBlocks', () => { @@ -129,16 +129,6 @@ describe('resolveProxyToolApprovalBlocks', () => { expect(mockUser).not.toHaveBeenCalled(); }); - it('holds an auto tool for a task run exactly like an ask tool', async () => { - mockDeployment.mockResolvedValue([policy('linear', 'save_issue', 'auto')]); - const task = await resolveProxyToolApprovalBlocks({ - integrationId: 'linear', - tokenType: 'run', - resolveActingUserId: async () => 'user-1', - }); - expect(Object.fromEntries(task)).toEqual({ save_issue: 'needs_approval' }); - }); - it("applies the task's session overrides to a task run only", async () => { mockSessionForTask.mockResolvedValue({ id: 'session-1' }); mockOverrides.mockResolvedValue([ diff --git a/apps/api/src/handlers/mcp/tool-approval-enforcement.ts b/apps/api/src/handlers/mcp/tool-approval-enforcement.ts index b74f7e6fd2..a5df7d7e61 100644 --- a/apps/api/src/handlers/mcp/tool-approval-enforcement.ts +++ b/apps/api/src/handlers/mcp/tool-approval-enforcement.ts @@ -9,7 +9,6 @@ import { listIntegrationToolUserPolicies, } from '@roomote/db/server'; import { - integrationToolModeAsks, resolveEffectiveIntegrationToolMode, resolveGoverningIntegrationToolPolicies, type IntegrationToolPolicyMode, @@ -99,7 +98,7 @@ export async function resolveProxyToolApprovalBlocks(input: { }); if (mode === 'reject') { blocks.set(toolName, 'reject'); - } else if (integrationToolModeAsks(mode) && input.tokenType === 'run') { + } else if (mode === 'ask' && input.tokenType === 'run') { blocks.set(toolName, 'needs_approval'); } } diff --git a/apps/web/src/components/settings/IntegrationToolApprovalControls.tsx b/apps/web/src/components/settings/IntegrationToolApprovalControls.tsx index 5778760dbf..e2b2b672c8 100644 --- a/apps/web/src/components/settings/IntegrationToolApprovalControls.tsx +++ b/apps/web/src/components/settings/IntegrationToolApprovalControls.tsx @@ -25,26 +25,15 @@ import { SelectItem, SelectTrigger, SelectValue, - Sparkles, type LucideIcon, } from '@/components/system'; const APPROVAL_MODES: { mode: IntegrationToolPolicyMode; label: string; - hint?: string; icon: LucideIcon; }[] = [ { mode: 'allow', label: 'Always allow', icon: CircleCheck }, - // A preview: it asks exactly like Ask first, and records what a decision - // model made of each call so its judgment can be checked before it is - // trusted to skip the ask. - { - mode: 'auto', - label: 'Auto (preview)', - hint: 'Auto (preview): asks first, and records what Roomote would decide', - icon: Sparkles, - }, { mode: 'ask', label: 'Ask first', icon: Hand }, { mode: 'reject', label: 'Reject', icon: Ban }, ]; @@ -75,7 +64,7 @@ function IntegrationToolApprovalModeControl({ aria-label={`Approval mode for ${toolName}`} className="flex shrink-0 items-center gap-0.5 rounded-md bg-muted/50 p-0.5" > - {APPROVAL_MODES.map(({ mode, label, hint, icon: Icon }) => { + {APPROVAL_MODES.map(({ mode, label, icon: Icon }) => { const checked = mode === value; return ( + ); + })} + +
+ +