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 = {