From 51691224f530a4d5737d3260d800a70f82a725bd Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 24 Sep 2026 15:57:25 -0700 Subject: [PATCH 1/2] fix(mothership): strip presentation activity before desktop browser dispatch --- .../client/browser-tool-execution.test.ts | 21 +++++++++++++++++++ .../tools/client/browser-tool-execution.ts | 6 ++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/mothership/tools/client/browser-tool-execution.test.ts b/apps/sim/lib/mothership/tools/client/browser-tool-execution.test.ts index f9dfa10fa2e..7670c4b4da5 100644 --- a/apps/sim/lib/mothership/tools/client/browser-tool-execution.test.ts +++ b/apps/sim/lib/mothership/tools/client/browser-tool-execution.test.ts @@ -110,6 +110,27 @@ describe('executeBrowserToolOnClient', () => { expect(mockExecuteBrowserTool).toHaveBeenCalledTimes(1) }) + it('never forwards presentation activity to the desktop driver', async () => { + mockExecuteBrowserTool.mockResolvedValue({ completed: true, results: [] }) + const toolCallId = nextToolCallId() + const fields = [{ elementId: 1, kind: 'text', text: 'a' }] + executeBrowserToolOnClient( + toolCallId, + 'browser_fill_form', + { activity: { description: 'Filling the form' }, fields }, + CHAT_SCOPE + ) + await flush() + expect(mockExecuteBrowserTool).toHaveBeenCalledWith( + toolCallId, + 'browser_fill_form', + { fields }, + expect.anything(), + CHAT_SCOPE, + expect.any(Function) + ) + }) + it('reports a batch as failed only when one of its actions failed', async () => { const actions = [ { tool: 'browser_click', args: { elementId: 1 } }, diff --git a/apps/sim/lib/mothership/tools/client/browser-tool-execution.ts b/apps/sim/lib/mothership/tools/client/browser-tool-execution.ts index eb65edd1e11..aa5712056d7 100644 --- a/apps/sim/lib/mothership/tools/client/browser-tool-execution.ts +++ b/apps/sim/lib/mothership/tools/client/browser-tool-execution.ts @@ -898,14 +898,16 @@ async function doExecuteBrowserTool( logger.info('Executing browser tool via the desktop agent browser', { toolCallId, toolName }) + /** `activity` is presentation metadata for the chat row; desktop actions reject unknown keys. */ + const { activity: _activity, ...actionParams } = params let result: unknown try { nativeDispatchStarted = true result = await executeBrowserTool( toolCallId, toolName, - params, - timeoutForTool(toolName, params), + actionParams, + timeoutForTool(toolName, actionParams), scopeId, () => { cancelled = true From 25fb5e4bc945817c4e4126803a8b8577d6bb6dc0 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 24 Sep 2026 16:04:14 -0700 Subject: [PATCH 2/2] fix(mothership): strip activity from desktop-authorized tool args --- .../api/desktop/tool/authorize/route.test.ts | 19 +++++++++++++++++ .../app/api/desktop/tool/authorize/route.ts | 7 ++++--- .../client/browser-tool-execution.test.ts | 21 ------------------- .../tools/client/browser-tool-execution.ts | 6 ++---- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/apps/sim/app/api/desktop/tool/authorize/route.test.ts b/apps/sim/app/api/desktop/tool/authorize/route.test.ts index 0b833983df9..6d965335655 100644 --- a/apps/sim/app/api/desktop/tool/authorize/route.test.ts +++ b/apps/sim/app/api/desktop/tool/authorize/route.test.ts @@ -87,6 +87,25 @@ describe('desktop tool authorization', () => { expect(claimPendingAsyncToolCall).toHaveBeenCalledWith('browser-tool', 'desktop-browser') }) + it('never returns presentation activity as an executable browser argument', async () => { + const fields = [{ elementId: 1, kind: 'text', text: 'a' }] + getAsyncToolCall.mockResolvedValueOnce({ + toolCallId: 'form-tool', + runId: 'run-1', + status: 'pending', + toolName: 'browser_fill_form', + args: { activity: { description: 'Filling the form' }, fields }, + }) + + const response = await POST(request('form-tool')) + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ + chatId: 'chat-1', + toolName: 'browser_fill_form', + args: { fields }, + }) + }) + it('rejects retired browser tools retained only for history', async () => { getAsyncToolCall.mockResolvedValueOnce({ toolCallId: 'retired-browser-tool', diff --git a/apps/sim/app/api/desktop/tool/authorize/route.ts b/apps/sim/app/api/desktop/tool/authorize/route.ts index fb735203a7a..9a657073b1e 100644 --- a/apps/sim/app/api/desktop/tool/authorize/route.ts +++ b/apps/sim/app/api/desktop/tool/authorize/route.ts @@ -1,6 +1,6 @@ import { isCurrentBrowserToolName } from '@sim/browser-protocol' import { isTerminalToolName } from '@sim/terminal-protocol' -import { isRecordLike } from '@sim/utils/object' +import { isRecordLike, omit } from '@sim/utils/object' import { type NextRequest, NextResponse } from 'next/server' import { authorizeDesktopToolContract } from '@/lib/api/contracts/desktop-tool-authorization' import { parseRequest } from '@/lib/api/server' @@ -24,7 +24,8 @@ import { isUserLocalVfsToolCall } from '@/lib/mothership/tools/local-filesystem' * Electron calls this endpoint from the main process before every privileged * native model action. It returns only server-persisted canonical tool args; * Electron validates local-file requests against them and uses them directly - * for browser and terminal tools. + * for browser and terminal tools. The presentation-only `activity` field is + * dropped: desktop actions reject arguments they do not declare. */ export const POST = withRouteHandler(async (request: NextRequest) => { const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly() @@ -117,7 +118,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return NextResponse.json({ toolName: toolCall.toolName, - args, + args: omit(args, ['activity']), chatId: run.chatId, }) }) diff --git a/apps/sim/lib/mothership/tools/client/browser-tool-execution.test.ts b/apps/sim/lib/mothership/tools/client/browser-tool-execution.test.ts index 7670c4b4da5..f9dfa10fa2e 100644 --- a/apps/sim/lib/mothership/tools/client/browser-tool-execution.test.ts +++ b/apps/sim/lib/mothership/tools/client/browser-tool-execution.test.ts @@ -110,27 +110,6 @@ describe('executeBrowserToolOnClient', () => { expect(mockExecuteBrowserTool).toHaveBeenCalledTimes(1) }) - it('never forwards presentation activity to the desktop driver', async () => { - mockExecuteBrowserTool.mockResolvedValue({ completed: true, results: [] }) - const toolCallId = nextToolCallId() - const fields = [{ elementId: 1, kind: 'text', text: 'a' }] - executeBrowserToolOnClient( - toolCallId, - 'browser_fill_form', - { activity: { description: 'Filling the form' }, fields }, - CHAT_SCOPE - ) - await flush() - expect(mockExecuteBrowserTool).toHaveBeenCalledWith( - toolCallId, - 'browser_fill_form', - { fields }, - expect.anything(), - CHAT_SCOPE, - expect.any(Function) - ) - }) - it('reports a batch as failed only when one of its actions failed', async () => { const actions = [ { tool: 'browser_click', args: { elementId: 1 } }, diff --git a/apps/sim/lib/mothership/tools/client/browser-tool-execution.ts b/apps/sim/lib/mothership/tools/client/browser-tool-execution.ts index aa5712056d7..eb65edd1e11 100644 --- a/apps/sim/lib/mothership/tools/client/browser-tool-execution.ts +++ b/apps/sim/lib/mothership/tools/client/browser-tool-execution.ts @@ -898,16 +898,14 @@ async function doExecuteBrowserTool( logger.info('Executing browser tool via the desktop agent browser', { toolCallId, toolName }) - /** `activity` is presentation metadata for the chat row; desktop actions reject unknown keys. */ - const { activity: _activity, ...actionParams } = params let result: unknown try { nativeDispatchStarted = true result = await executeBrowserTool( toolCallId, toolName, - actionParams, - timeoutForTool(toolName, actionParams), + params, + timeoutForTool(toolName, params), scopeId, () => { cancelled = true