github / github/copilot-cli

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

Open
#4,537 1 comment 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area:permissions
Dominant language
Shell
Stars
11.2k
Forks
1.9k
Avg merge
14h 16m
Merged PRs (30d)
6

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the acp-perm-repro.js client and run it against the known good and affected builds using --acp and the documented COPILOT_CLI_DIST_DIR settings. Trace the ACP session/prompt path around session/request_permission and compare it with the TUI permission flow; done means a client refusal prevents the shell command and permission events are recorded as in 1.0.81-0.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
cli, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.