Skip to content

ACP mode auto-approves tool calls again — session/request_permission not sent since 1.0.81-1 (regression of #845) #4537

Description

@richardjv-msft

Describe the bug

In --acp mode the agent no longer sends session/request_permission. Shell commands, file edits and deletions execute unattended: the client is given no opportunity to prompt, and nothing is written to the session log to say permission was ever waived.

This is the same defect as #845 (closed 2026-01-02). It returned in 1.0.81-1.

The permission engine itself is healthy — the interactive TUI on the same build still prompts for the identical command. Only the ACP path skips it.

Affected version

1.0.81-2, 1.0.81-3, 1.0.81-5 (current). Last good: 1.0.81-0.

Bisected with COPILOT_CLI_DIST_DIR against builds cached under ~/.copilot/pkg/linux-x64/:

build sends session/request_permission file created despite client refusing
1.0.80 yes no
1.0.81-0 yes no
1.0.81-1 not tested — not cached locally
1.0.81-2 no yes
1.0.81-3 no yes
1.0.81-5 no yes

1.0.81-1 is named because it carries the only permission-related entry in the changelog between the last good and first bad build:

"Turning allow-all off from an ACP client now reaches the permission engine whenever there is a runtime override or auto-approval to revoke… (a baseline granted by --allow-all-* launch flags is still deliberately left intact)" — PR 14283

Steps to reproduce the behavior

Save as acp-perm-repro.js (no dependencies), then mkdir /tmp/acpdemo && node acp-perm-repro.js /tmp/acpdemo:

// Minimal ACP client: initialize -> session/new -> session/prompt.
// Logs every request the agent sends us, and refuses any permission request.
const { spawn } = require('node:child_process');
const dir = process.argv[2] || process.cwd();
const say = (...a) => require('node:fs').writeSync(1, a.join(' ') + '\n');
const child = spawn('copilot', ['--acp'], { cwd: dir, stdio: ['pipe', 'pipe', 'ignore'] });
let id = 0, buf = '';
const pending = new Map();
const send = (o) => child.stdin.write(JSON.stringify(o) + '\n');
const request = (method, params) =>
  new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej }); send({ jsonrpc: '2.0', id: i, method, params }); });

child.stdout.on('data', (d) => {
  buf += d;
  for (let n; (n = buf.indexOf('\n')) >= 0; ) {
    const line = buf.slice(0, n); buf = buf.slice(n + 1);
    let m; try { m = JSON.parse(line); } catch { continue; }
    if (m.method && m.id !== undefined) {                 // agent -> client request
      say('AGENT ASKS:', m.method);
      send({ jsonrpc: '2.0', id: m.id,
             result: m.method === 'session/request_permission'
               ? { outcome: { outcome: 'cancelled' } }     // client refuses; tool must not run
               : {} });
    } else if (m.id !== undefined && pending.has(m.id)) {
      const p = pending.get(m.id); pending.delete(m.id);
      m.error ? p.rej(new Error(JSON.stringify(m.error))) : p.res(m.result);
    }
  }
});

(async () => {
  const init = await request('initialize', {
    protocolVersion: 1,
    clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, terminal: false },
  });
  say('agent version:', init.agentInfo?.version);
  const { sessionId } = await request('session/new', { cwd: dir, mcpServers: [] });
  say('session:', sessionId);
  await request('session/prompt', { sessionId, prompt: [{ type: 'text',
    text: `Run exactly this shell command and nothing else: touch ${dir}/created-without-asking.txt` }] });
  say('file exists:', require('node:fs').existsSync(`${dir}/created-without-asking.txt`));
  child.kill();
})().catch((e) => { say(e.message); child.kill(); process.exit(1); });

On 1.0.81-0 (COPILOT_AUTO_UPDATE=false COPILOT_CLI_DIST_DIR=~/.copilot/pkg/linux-x64/1.0.81-0):

agent version: 1.0.81-0
AGENT ASKS: session/request_permission
file exists: false

On 1.0.81-5:

agent version: 1.0.81-5
file exists: true

The directory is fresh, has never been trusted, and none of these commands appear in ~/.copilot/permissions-config.json.

Expected behavior

As in #845 and the ACP specification, where session/request_permission is a baseline method: the agent asks, the client decides. A client answering cancelled should see the tool not run — which is what 1.0.81-0 does.

Additional context

  • The TUI is unaffected, which localises the fault. Driven under a pty on 1.0.81-5, the identical prompt produces "Do you want to run this command? … touch …", and the file is not created. Only the ACP path skips the ask.

  • Not gated on a client capability. Declaring terminal: true alongside fs changes nothing.

  • Not explained by saved approvals or folder trust. Reproduced in a directory with neither.

  • Reproduced by two independent client implementations, one of them the ~30 lines above, written from scratch.

  • Nothing in the session log distinguishes this from a deliberate waiver. The log records permission decisions, not permission policy — so every path that reaches the engine is recorded however it is answered, and every path that bypasses the engine records nothing at all:

    how the tool got permission permission.requested / .completed recorded kind
    TUI, user answers "Yes" logged approved
    ACP client prompts, user allows once logged approved
    ACP client auto-answers allow_always logged approved-for-session
    ACP client refuses logged denied-interactively-by-user
    --allow-all-tools (launch flag) none
    --yolo (launch flag) none
    /yolo on (runtime toggle) none
    1.0.81-5 over ACP (this bug) none

    session.start carries no permission-posture field either (contextTier, reasoningEffort, copilotVersion… and nothing about approvals), and the affected build's log contains no string matching allow/approve/permission anywhere. So an ACP session that ran unsupervised because the agent stopped asking cannot be told apart afterwards from one where the operator knowingly disabled permissions — and /yolo on leaves no trace even though the TUI announces "All permissions are now enabled" on screen.

    A session here shows 105 tool calls in this state — git checkout --detach, git commit, rm -rf, chmod +x and fourteen file edits — with zero permission.* events.

  • Note for anyone bisecting: the agent sends this request with "id": 0. A client testing if (m.id) will silently drop it and hang, which looks exactly like the bug. Worth ruling out before concluding a build is affected.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:permissionsTool approval, security boundaries, sandbox mode, and directory restrictions

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions