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.
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Direct and `xd://` dispatch now share one canonical tool map: `write xd://<tool>` executes any enabled top-level or mounted tool, and `read xd://<tool>` 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://<tool>` executes any enabled top-level or mounted tool, and `read xd://<tool>` 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
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ interface CursorExecBridgeOptions {
|
||||
cwd: string;
|
||||
getCwd?: () => string;
|
||||
tools: Map<string, AgentTool>;
|
||||
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<string, unknown>,
|
||||
): Promise<ToolResultMessage> {
|
||||
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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user