Summary:
When cursor-agent runs in ACP mode (cursor-agent acp), the built-in web search tool (toolCall.kind: "search") unconditionally issues a session/request_permission for every call. No configuration — allowlist token, approvalMode, or otherwise — auto-approves it. Every other tool type has an approval path; web search has none.
Environment:
cursor-agentversion2026.07.09-a3815c0- macOS (Darwin), running
cursor-agent acpas a JSON-RPC ACP server over stdio - ACP session mode
agent, modelgrok-4.5
Repro:
- Drive
cursor-agent acpwith the minimal ACP client below:initialize→session/new→session/promptwith a prompt that forces a web search (e.g. “Search the web for the latest Node.js LTS version. Use your web search tool.”). - Observe the client receives
session/request_permissionwithtoolCall.kind: "search". - Attempt to pre-authorize via
~/.cursor/cli-config.json/ project.cursor/cli.json. Tried allow tokens:Web(search),WebSearch(*),Search(*),Search(),Web(*),WebFetch(*). - Also set
approvalMode: "unrestricted".
Captured IDs (live run):
sessionId: 630bcd3f-271f-4fca-9517-a510bc9809ba
session/request_permission: jsonrpc id=0
toolCall.toolCallId: web_search_0
toolCall.kind: search
toolCall.title: "Web search: latest Node.js LTS version 2026"
Expected: Some config should let the web search tool run without a per-call prompt (as Shell(...) allowlist entries and unrestricted do for shell commands).
Actual: Web search prompts in every case. Notably, under approvalMode: unrestricted, a non-allowlisted shell command (date) runs with no prompt — proving unrestricted is in effect — yet the web search request (id: 0, toolCallId: web_search_0) still fires. The documented permission tool types (Shell, Read, Write, WebFetch(domain), Mcp) include no web-search token, and WebFetch(domain) governs URL fetching, not the search tool.
Impact: Headless/ACP integrations (custom clients, IDE integrations) cannot run web-search-using agents unattended — every search blocks on a manual approval with no way to allowlist it.
Suggested fix: Add a web-search permission token (e.g. Search(*) / WebSearch) honored in ACP, and/or have approvalMode: unrestricted cover the search tool like it covers shell/read/write.
Attachment — minimal ACP repro client (acp-repro.mjs)
Run with node acp-repro.mjs <cwd> "<prompt>". It auto-approves any permission request (so the run completes) and logs the sessionId + every session/request_permission with its ids.
import { spawn } from "node:child_process";
const cwd = process.argv[2] ?? process.cwd();
const text = process.argv[3] ?? "Search the web for the latest Node.js LTS version. Use your web search tool.";
const child = spawn("cursor-agent", ["acp"], { cwd, stdio: ["pipe", "pipe", "pipe"] });
let buf = "";
let id = 0;
const pending = new Map();
const send = (method, params) => {
const msg = { jsonrpc: "2.0", id: ++id, method, params };
child.stdin.write(JSON.stringify(msg) + "\n");
return new Promise((r) => pending.set(msg.id, r));
};
const respond = (rid, result) =>
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: rid, result }) + "\n");
child.stdout.on("data", (d) => {
buf += d.toString();
let i;
while ((i = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, i);
buf = buf.slice(i + 1);
if (!line.trim()) continue;
let m;
try { m = JSON.parse(line); } catch { continue; }
if (m.method === "session/request_permission") {
const tc = m.params.toolCall ?? {};
console.log(
`PERMISSION_REQUEST jsonrpc.id=${m.id} toolCallId=${tc.toolCallId} ` +
`kind=${tc.kind} title=${JSON.stringify(tc.title)}`,
);
// auto-approve so the turn completes
respond(m.id, { outcome: { outcome: "selected", optionId: m.params.options?.[0]?.optionId } });
} else if (m.id && pending.has(m.id)) {
pending.get(m.id)(m);
pending.delete(m.id);
}
}
});
child.stderr.on("data", (d) => process.stderr.write("[stderr] " + d));
await send("initialize", {
protocolVersion: 1,
clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } },
});
const sess = await send("session/new", { cwd, mcpServers: [] });
console.log(
`sessionId=${sess.result.sessionId} currentMode=${sess.result.modes?.currentModeId} ` +
`model=${sess.result.models?.currentModelId}`,
);
await send("session/prompt", { sessionId: sess.result.sessionId, prompt: [{ type: "text", text }] });
child.kill();
process.exit(0);