From e7558e37e75f05e793d7b6fb53689444632daf7c Mon Sep 17 00:00:00 2001 From: can1357 Date: Tue, 28 Jul 2026 03:41:04 +0200 Subject: [PATCH] fix(coding-agent): corrected tool resolution and conditional auto-qa session config - Replaced getTool with getExecutableTool in CursorExecBridgeOptions to prioritize mounted-device permission wrappers over canonical tools. - Updated createAgentSession to check isAutoQaEnabled against restricted tool filtering when configuring system prompts. - Added test coverage verifying execution overrides preserve approval gates and restricted sessions omit auto-qa guidance. --- packages/coding-agent/CHANGELOG.md | 2 +- packages/coding-agent/src/cursor.ts | 7 +++--- packages/coding-agent/src/sdk.ts | 4 +++- .../coding-agent/test/cursor-exec.test.ts | 10 +++++---- .../test/sdk-tool-activation.test.ts | 22 +++++++++++++++++++ 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index af7a7e28b..9d0b46fe8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,7 +4,7 @@ ### Changed -- Direct and `xd://` dispatch now share one canonical tool map: `write xd://` executes any enabled top-level or mounted tool, and `read xd://` returns its docs, instead of failing when the name was exposed through the other layer. Mounted names are presentation metadata only, so tool replacement and disconnection cannot leave stale device instances; disabled tools remain unreachable and the dispatched tool's approval tier still applies. +- Direct and `xd://` dispatch now share one canonical tool map: `write xd://` executes any enabled top-level or mounted tool, and `read xd://` returns its docs, instead of failing when the name was exposed through the other layer. Mounted names are presentation metadata only, so tool replacement and disconnection cannot leave stale device instances; disabled tools remain unreachable, and both `xd://` and Cursor/top-level fallback execution retain the tool's approval and ACP permission gates. ## [17.1.7] - 2026-07-27 diff --git a/packages/coding-agent/src/cursor.ts b/packages/coding-agent/src/cursor.ts index caf3e4908..7c30755a6 100644 --- a/packages/coding-agent/src/cursor.ts +++ b/packages/coding-agent/src/cursor.ts @@ -25,7 +25,8 @@ interface CursorExecBridgeOptions { cwd: string; getCwd?: () => string; tools: Map; - getTool?: (name: string) => AgentTool | undefined; + /** Resolves execution overrides (mounted-device permission wrappers) before the canonical map. */ + getExecutableTool?: (name: string) => AgentTool | undefined; getToolContext?: () => AgentToolContext | undefined; emitEvent?: (event: AgentEvent) => void; /** @@ -81,7 +82,7 @@ async function executeTool( toolCallId: string, args: Record, ): Promise { - const tool = options.tools.get(toolName) ?? options.getTool?.(toolName); + const tool = options.getExecutableTool?.(toolName) ?? options.tools.get(toolName); if (!tool) { const result = buildToolErrorResult(`Tool "${toolName}" not available`); return createToolResultMessage(toolCallId, toolName, result, true); @@ -497,7 +498,7 @@ export class CursorExecHandlers implements ICursorExecHandlers { async mcp(call: CursorMcpCall) { const toolName = call.toolName || call.name; const toolCallId = decodeToolCallId(call.toolCallId); - const tool = this.options.tools.get(toolName) ?? this.options.getTool?.(toolName); + const tool = this.options.getExecutableTool?.(toolName) ?? this.options.tools.get(toolName); if (!tool) { const availableTools = Array.from(this.options.tools.keys()).filter(name => name.startsWith("mcp__")); const message = formatMcpToolErrorMessage(toolName, availableTools); diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 67ba04735..afe318051 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -207,6 +207,7 @@ import { ToolContextStore } from "./tools/context"; import { isIrcEnabled } from "./tools/hub"; import { getImageGenTools } from "./tools/image-gen"; import { wrapToolWithMetaNotice } from "./tools/output-meta"; +import { isAutoQaEnabled } from "./tools/report-tool-issue"; import { queueResolveHandler } from "./tools/resolve"; import { USER_TODO_EDIT_CUSTOM_TYPE } from "./tools/todo"; import { ttsTool } from "./tools/tts"; @@ -2626,7 +2627,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} const cursorExecHandlers = new CursorExecHandlers({ cwd, tools: toolRegistry, - getTool: resolveDeviceTool, + getExecutableTool: resolveDeviceTool, getToolContext: () => toolContextStore.getContext(), emitEvent: event => cursorEventEmitter?.(event), getTodoPhases: () => session.getTodoPhases(), @@ -2740,6 +2741,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} taskBatch: settings.get("task.batch"), taskMaxConcurrency: settings.get("task.maxConcurrency"), taskIrcEnabled: !restrictToolNames && isIrcEnabled(settings, options.taskDepth ?? 0), + autoQaEnabled: !restrictToolNames && isAutoQaEnabled(settings), secretsEnabled, workspaceTree: workspaceTreePromise, includeWorkspaceTree, diff --git a/packages/coding-agent/test/cursor-exec.test.ts b/packages/coding-agent/test/cursor-exec.test.ts index 08e6edd1d..2827cb782 100644 --- a/packages/coding-agent/test/cursor-exec.test.ts +++ b/packages/coding-agent/test/cursor-exec.test.ts @@ -149,8 +149,8 @@ describe("CursorExecHandlers mounted tool bridge", () => { }; const handlers = new CursorExecHandlers({ cwd: ".", - tools: new Map(), - getTool: name => (name === mountedTool.name ? mountedTool : undefined), + tools: new Map([[mountedTool.name, mountedTool]]), + getExecutableTool: name => (name === mountedTool.name ? mountedTool : undefined), }); const result = await handlers.mcp({ @@ -187,8 +187,10 @@ describe("CursorExecHandlers mounted tool bridge", () => { const settings = Settings.isolated({ "tools.approval": { ast_edit: "deny" } }); const handlers = new CursorExecHandlers({ cwd: ".", - tools: new Map(), - getTool: name => (name === device.name ? (wrapped as unknown as AgentTool) : undefined), + // The canonical map contains the undecorated mounted tool. The execution + // override must win or Cursor bypasses the approval gate. + tools: new Map([[device.name, device]]), + getExecutableTool: name => (name === device.name ? (wrapped as unknown as AgentTool) : undefined), getToolContext: () => ({ settings }) as AgentToolContext, }); diff --git a/packages/coding-agent/test/sdk-tool-activation.test.ts b/packages/coding-agent/test/sdk-tool-activation.test.ts index 6d583a805..87bc493ab 100644 --- a/packages/coding-agent/test/sdk-tool-activation.test.ts +++ b/packages/coding-agent/test/sdk-tool-activation.test.ts @@ -494,6 +494,28 @@ describe("createAgentSession defaultInactive tool activation", () => { } }); + it("renders report-issue guidance only for unrestricted sessions", async () => { + const normalDir = makeTempDir(); + const restrictedDir = makeTempDir(); + const { session: normal } = await createAgentSession({ + ...baseOptions(normalDir), + settings: Settings.isolated({ "dev.autoqa": true }), + }); + const { session: restricted } = await createAgentSession({ + ...baseOptions(restrictedDir), + settings: Settings.isolated({ "dev.autoqa": true }), + toolNames: ["read"], + restrictToolNames: true, + }); + + try { + expect(normal.systemPrompt.join("\n")).toContain("xd://report_issue"); + expect(restricted.systemPrompt.join("\n")).not.toContain("xd://report_issue"); + } finally { + await Promise.all([normal.dispose(), restricted.dispose()]); + } + }); + it("ignores an inherited MCP manager when MCP is disabled", async () => { const tempDir = makeTempDir(); const inheritedManager = {