Skip to content

ACP mode should send session/request_permission for tool calls instead of auto-approving #845

@jflam

Description

@jflam

Summary

When running Copilot CLI in --acp mode, the agent does not send session/request_permission requests to the client for tool calls (file writes, shell commands, etc.). Instead, operations are auto-approved internally. This prevents ACP clients from implementing their own permission UIs or policies.

Expected Behavior

Per the Agent Client Protocol specification, session/request_permission is a baseline method that agents should send to request user authorization for tool calls:

"Permissioned tool calls: A first-class handshake for sensitive operations where the agent asks the client to prompt the user before proceeding."

The expected protocol flow is:

Agent → Client: session/request_permission (with tool call details and permission options)
Client → Agent: response (user's decision: allow_once, allow_always, reject, etc.)

This allows the client (IDE, editor, CLI wrapper) to be the permission gatekeeper.

Actual Behavior

In --acp mode, Copilot CLI auto-approves tool operations without sending session/request_permission to the client. The client never receives permission requests and has no opportunity to prompt the user or apply its own permission policies.

Test Results

We wrote integration tests that verify whether each ACP agent sends session/request_permission for write operations. Results:

Agent Sends session/request_permission ACP Compliant
Gemini CLI (--experimental-acp) ✅ Yes ✅ Yes
Claude Code (via claude-code-acp) ✅ Yes ✅ Yes
Codex (via codex-acp) ✅ Yes ✅ Yes
Copilot CLI (--acp) ❌ No ❌ No

Copilot is the only agent that does not send permission requests.

Test Code

The test uses a TrackingApproveHandler that records all incoming session/request_permission calls:

/// A permission handler that tracks whether permission was requested and auto-approves.
pub struct TrackingApproveHandler {
    pub request_count: AtomicUsize,
    pub permission_requested: AtomicBool,
}

#[async_trait]
impl PermissionHandler for TrackingApproveHandler {
    async fn handle_permission(
        &self,
        request: &PermissionRequest,
    ) -> Result<PermissionOutcome, String> {
        self.request_count.fetch_add(1, Ordering::SeqCst);
        self.permission_requested.store(true, Ordering::SeqCst);

        // Auto-approve: find allow_always > allow_once > first allow > first option
        let selected = request
            .options
            .iter()
            .find(|o| o.kind == "allow_always")
            .or_else(|| request.options.iter().find(|o| o.kind == "allow_once"))
            .or_else(|| request.options.iter().find(|o| o.kind.contains("allow")))
            .or_else(|| request.options.first());

        match selected {
            Some(option) => Ok(PermissionOutcome::Selected {
                option_id: option.option_id.clone(),
            }),
            None => Err("No permission options available".to_string()),
        }
    }
}

The test for Copilot:

/// Test whether Copilot sends permission requests for write operations.
///
/// KNOWN LIMITATION: Copilot CLI does NOT send `session/request_permission` in
/// `--acp` mode. Instead, it auto-approves operations internally.
#[tokio::test]
async fn test_permission_requested_for_write() {
    let config = AgentConfig::copilot();
    let handler = TrackingApproveHandler::new();

    let client = create_client_with_permission_handler(config, handler.clone())
        .await
        .expect("Failed to create client");

    client.connect().await.expect("Failed to connect");

    // Send a prompt that triggers a file write
    let write_prompt = "Create a file called /tmp/test.txt with content 'test'";
    let result = run_write_prompt_with_tracking(&client, &handler).await;

    // Result: permission_requested=false, request_count=0, completed=true
    // Copilot auto-approves without sending session/request_permission
    assert!(!result.permission_requested);
    assert!(result.completed);
}

Output:

Copilot permission test: permission_requested=false, request_count=0, completed=true

Compare with Gemini CLI which correctly sends permission requests:

Gemini CLI permission test: permission_requested=true, request_count=1, completed=true

Reproduction Steps

  1. Create an ACP client that implements PermissionHandler and logs incoming requests

  2. Start Copilot CLI in ACP mode:

    copilot --acp
  3. Send a prompt that triggers a file write:

    {"jsonrpc":"2.0","id":1,"method":"session/prompt","params":{
      "sessionId":"...",
      "prompt":[{"type":"text","text":"Create a file called /tmp/test.txt with content 'hello'"}]
    }}
  4. Observe the ACP message stream - no session/request_permission request is sent before the file is written

  5. Compare with Gemini CLI (gemini --experimental-acp) which correctly sends permission requests

Why This Matters

ACP clients like Zed, VS Code extensions, Neovim plugins, and CLI tools rely on session/request_permission to:

  1. Present permission prompts to users in their native UI
  2. Implement "always allow" / "always deny" policies per tool or path
  3. Provide audit logs of what operations were approved
  4. Enforce security policies in enterprise environments

Without this, ACP clients cannot provide a consistent permission experience when using Copilot as a backend agent, and users have no control over what operations Copilot performs.

References

Environment

  • Copilot CLI version: 0.0.372
  • OS: macOS (also reproducible on Linux)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions