diff --git a/assistant/src/daemon/conversation-surfaces.ts b/assistant/src/daemon/conversation-surfaces.ts index 6df0eaa781a..962e0990bba 100644 --- a/assistant/src/daemon/conversation-surfaces.ts +++ b/assistant/src/daemon/conversation-surfaces.ts @@ -1780,6 +1780,10 @@ export async function surfaceProxyResolver( // Record the action and proxy to the connected desktop client const reasoning = typeof input.reasoning === "string" ? input.reasoning : undefined; + const targetClientId = + typeof input.target_client_id === "string" && input.target_client_id !== "" + ? input.target_client_id + : undefined; ctx.hostCuProxy.recordAction(toolName, input, reasoning); return ctx.hostCuProxy.request( toolName, @@ -1788,6 +1792,7 @@ export async function surfaceProxyResolver( ctx.hostCuProxy.stepCount, reasoning, signal, + targetClientId, ); } diff --git a/assistant/src/tools/computer-use/definitions.ts b/assistant/src/tools/computer-use/definitions.ts index 0d237092761..7bbd9db4da0 100644 --- a/assistant/src/tools/computer-use/definitions.ts +++ b/assistant/src/tools/computer-use/definitions.ts @@ -63,6 +63,11 @@ export const computerUseClickTool: Tool = { description: "Explanation of what you see and why you are clicking here", }, + target_client_id: { + type: "string", + description: + "ID of the specific client to target. Required when multiple clients support host_cu; omit when only one is connected. Obtain IDs from `assistant clients list --capability host_cu`.", + }, }, required: ["reasoning"], }, @@ -99,6 +104,11 @@ export const computerUseTypeTextTool: Tool = { type: "string", description: "Explanation of what you are typing and why", }, + target_client_id: { + type: "string", + description: + "ID of the specific client to target. Required when multiple clients support host_cu; omit when only one is connected. Obtain IDs from `assistant clients list --capability host_cu`.", + }, }, required: ["text", "reasoning"], }, @@ -136,6 +146,11 @@ export const computerUseKeyTool: Tool = { type: "string", description: "Explanation of why you are pressing this key", }, + target_client_id: { + type: "string", + description: + "ID of the specific client to target. Required when multiple clients support host_cu; omit when only one is connected. Obtain IDs from `assistant clients list --capability host_cu`.", + }, }, required: ["key", "reasoning"], }, @@ -190,6 +205,11 @@ export const computerUseScrollTool: Tool = { type: "string", description: "Explanation of why you are scrolling", }, + target_client_id: { + type: "string", + description: + "ID of the specific client to target. Required when multiple clients support host_cu; omit when only one is connected. Obtain IDs from `assistant clients list --capability host_cu`.", + }, }, required: ["direction", "amount", "reasoning"], }, @@ -250,6 +270,11 @@ export const computerUseDragTool: Tool = { type: "string", description: "Explanation of what you are dragging and why", }, + target_client_id: { + type: "string", + description: + "ID of the specific client to target. Required when multiple clients support host_cu; omit when only one is connected. Obtain IDs from `assistant clients list --capability host_cu`.", + }, }, required: ["reasoning"], }, @@ -285,6 +310,11 @@ export const computerUseWaitTool: Tool = { type: "string", description: "Explanation of what you are waiting for", }, + target_client_id: { + type: "string", + description: + "ID of the specific client to target. Required when multiple clients support host_cu; omit when only one is connected. Obtain IDs from `assistant clients list --capability host_cu`.", + }, }, required: ["duration_ms", "reasoning"], }, @@ -323,6 +353,11 @@ export const computerUseOpenAppTool: Tool = { description: "Explanation of why you need to open or switch to this app", }, + target_client_id: { + type: "string", + description: + "ID of the specific client to target. Required when multiple clients support host_cu; omit when only one is connected. Obtain IDs from `assistant clients list --capability host_cu`.", + }, }, required: ["app_name", "reasoning"], }, @@ -360,6 +395,11 @@ export const computerUseRunAppleScriptTool: Tool = { description: "Explanation of what this script does and why AppleScript is better than UI interaction for this step", }, + target_client_id: { + type: "string", + description: + "ID of the specific client to target. Required when multiple clients support host_cu; omit when only one is connected. Obtain IDs from `assistant clients list --capability host_cu`.", + }, }, required: ["script", "reasoning"], }, diff --git a/assistant/src/tools/host-filesystem/edit.ts b/assistant/src/tools/host-filesystem/edit.ts index 4100ff260e1..4c606d4eb3a 100644 --- a/assistant/src/tools/host-filesystem/edit.ts +++ b/assistant/src/tools/host-filesystem/edit.ts @@ -1,6 +1,8 @@ +import { supportsHostProxy } from "../../channels/types.js"; import { HostFileProxy } from "../../daemon/host-file-proxy.js"; import { RiskLevel } from "../../permissions/types.js"; import type { ToolDefinition } from "../../providers/types.js"; +import { assistantEventHub } from "../../runtime/assistant-event-hub.js"; import { FileSystemOps } from "../shared/filesystem/file-ops-service.js"; import { formatEditDiff } from "../shared/filesystem/format-diff.js"; import { hostPolicy } from "../shared/filesystem/path-policy.js"; @@ -37,6 +39,11 @@ class HostFileEditTool implements Tool { description: "Replace all occurrences instead of requiring a unique match (default: false)", }, + target_client_id: { + type: "string", + description: + "ID of the specific client to execute this on. Required when multiple clients support host_file; omit when only one is connected. Obtain IDs from `assistant clients list --capability host_file`.", + }, }, required: ["path", "old_string", "new_string"], }, @@ -84,6 +91,24 @@ class HostFileEditTool implements Tool { const replaceAll = input.replace_all === true; + const targetClientId = + typeof input.target_client_id === "string" && input.target_client_id !== "" + ? input.target_client_id + : undefined; + + const transportInterface = context.transportInterface; + if ( + targetClientId == null && + transportInterface != null && + !supportsHostProxy(transportInterface) && + assistantEventHub.listClientsByCapability("host_file").length > 1 + ) { + return { + content: `Error: multiple clients support host_file. Specify which client to use with \`target_client_id\`. Run \`assistant clients list --capability host_file\` to see client IDs and labels.`, + isError: true, + }; + } + // Proxy to connected client for execution on the user's machine // when a capable client is available (managed/cloud-hosted mode). if (HostFileProxy.instance.isAvailable()) { @@ -94,6 +119,7 @@ class HostFileEditTool implements Tool { old_string: oldString as string, new_string: newString as string, replace_all: replaceAll, + targetClientId, }, context.conversationId, context.signal, diff --git a/assistant/src/tools/host-filesystem/read.ts b/assistant/src/tools/host-filesystem/read.ts index a41220255a8..bfe219701d0 100644 --- a/assistant/src/tools/host-filesystem/read.ts +++ b/assistant/src/tools/host-filesystem/read.ts @@ -1,8 +1,10 @@ import { extname } from "node:path"; +import { supportsHostProxy } from "../../channels/types.js"; import { HostFileProxy } from "../../daemon/host-file-proxy.js"; import { RiskLevel } from "../../permissions/types.js"; import type { ToolDefinition } from "../../providers/types.js"; +import { assistantEventHub } from "../../runtime/assistant-event-hub.js"; import { FileSystemOps } from "../shared/filesystem/file-ops-service.js"; import { IMAGE_EXTENSIONS, @@ -37,6 +39,11 @@ class HostFileReadTool implements Tool { type: "number", description: "Maximum number of lines to read", }, + target_client_id: { + type: "string", + description: + "ID of the specific client to execute this on. Required when multiple clients support host_file; omit when only one is connected. Obtain IDs from `assistant clients list --capability host_file`.", + }, }, required: ["path"], }, @@ -55,6 +62,24 @@ class HostFileReadTool implements Tool { }; } + const targetClientId = + typeof input.target_client_id === "string" && input.target_client_id !== "" + ? input.target_client_id + : undefined; + + const transportInterface = context.transportInterface; + if ( + targetClientId == null && + transportInterface != null && + !supportsHostProxy(transportInterface) && + assistantEventHub.listClientsByCapability("host_file").length > 1 + ) { + return { + content: `Error: multiple clients support host_file. Specify which client to use with \`target_client_id\`. Run \`assistant clients list --capability host_file\` to see client IDs and labels.`, + isError: true, + }; + } + // Proxy to connected client for execution on the user's machine // when a capable client is available (managed/cloud-hosted mode), // including image reads that need the host filesystem view. @@ -65,6 +90,7 @@ class HostFileReadTool implements Tool { path: rawPath, offset: typeof input.offset === "number" ? input.offset : undefined, limit: typeof input.limit === "number" ? input.limit : undefined, + targetClientId, }, context.conversationId, context.signal, diff --git a/assistant/src/tools/host-filesystem/write.ts b/assistant/src/tools/host-filesystem/write.ts index 3113545ef61..be23354bd7c 100644 --- a/assistant/src/tools/host-filesystem/write.ts +++ b/assistant/src/tools/host-filesystem/write.ts @@ -1,6 +1,8 @@ +import { supportsHostProxy } from "../../channels/types.js"; import { HostFileProxy } from "../../daemon/host-file-proxy.js"; import { RiskLevel } from "../../permissions/types.js"; import type { ToolDefinition } from "../../providers/types.js"; +import { assistantEventHub } from "../../runtime/assistant-event-hub.js"; import { FileSystemOps } from "../shared/filesystem/file-ops-service.js"; import { formatWriteSummary } from "../shared/filesystem/format-diff.js"; import { hostPolicy } from "../shared/filesystem/path-policy.js"; @@ -28,6 +30,11 @@ class HostFileWriteTool implements Tool { type: "string", description: "The content to write to the file", }, + target_client_id: { + type: "string", + description: + "ID of the specific client to execute this on. Required when multiple clients support host_file; omit when only one is connected. Obtain IDs from `assistant clients list --capability host_file`.", + }, }, required: ["path", "content"], }, @@ -54,6 +61,24 @@ class HostFileWriteTool implements Tool { }; } + const targetClientId = + typeof input.target_client_id === "string" && input.target_client_id !== "" + ? input.target_client_id + : undefined; + + const transportInterface = context.transportInterface; + if ( + targetClientId == null && + transportInterface != null && + !supportsHostProxy(transportInterface) && + assistantEventHub.listClientsByCapability("host_file").length > 1 + ) { + return { + content: `Error: multiple clients support host_file. Specify which client to use with \`target_client_id\`. Run \`assistant clients list --capability host_file\` to see client IDs and labels.`, + isError: true, + }; + } + // Proxy to connected client for execution on the user's machine // when a capable client is available (managed/cloud-hosted mode). if (HostFileProxy.instance.isAvailable()) { @@ -62,6 +87,7 @@ class HostFileWriteTool implements Tool { operation: "write", path: rawPath, content: fileContent, + targetClientId, }, context.conversationId, context.signal,