Skip to content

Bug: RequestPermissionResponse missing outcome wrapper — Claude Code tool calls universally rejected #130

@chengli

Description

@chengli

Summary

openab's auto-reply for session/request_permission (in src/acp/connection.rs) sends a JSON-RPC response body with the wrong shape — missing the required outcome wrapper. The response works for Kiro CLI (loose parsing) but is rejected by claude-agent-acp (strict ACP SDK validation), causing every tool call from Claude Code to fail with "User refused permission to run tool", even though openab logs auto-allow permission.

This is also the upstream root cause for the symptom reported in #127 (Cursor's Cannot read properties of undefined (reading 'outcome') error) — the Cursor SDK is trying to read response.outcome.optionId and crashing because response.outcome is undefined.

Current vs spec response shape

src/acp/connection.rs line 99:

let reply = JsonRpcResponse::new(id, json!({"optionId": "allow_always"}));

Per the official ACP TypeScript SDK schema (@agentclientprotocol/sdk types.gen.d.ts):

export type RequestPermissionResponse = {
    _meta?: { ... } | null;
    /** The user's decision on the permission request. */
    outcome: RequestPermissionOutcome;
};

export type RequestPermissionOutcome =
    | { outcome: "cancelled"; }
    | (SelectedPermissionOutcome & { outcome: "selected"; });

The valid response shape is:

{
  "outcome": {
    "outcome": "selected",
    "optionId": "allow_always"
  }
}

openab is sending the inner object's optionId field at the top level, with no outcome wrapper.

Repro

  1. Run openab configured with claude-agent-acp backend (any Claude Code agent)
  2. From Discord, ask the agent to do anything that requires a tool call (e.g. git pull, read a file, run ls)
  3. openab log shows: INFO openab::acp::connection: auto-allow permission title="..."
  4. claude-agent-acp log shows: tool_call_update status=failed rawOutput="User refused permission to run tool"
  5. The agent gives up or works around it without ever executing the tool

Trace from a real session (agent-broker:claude running claude-agent-acp@0.25.0 + Claude Code):

18:54:32.071  request_permission for git -C ~/work/trips pull --ff-only
18:54:32.071  auto-allow permission title="git -C ~/work/trips pull --ff-only"
18:54:32.079  tool_call_update status=failed rawOutput="User refused permission to run tool"

8 ms between auto-allow and the failure — claude-agent-acp is parsing openab's reply, finding no outcome field, and treating it as a refusal.

Why Kiro doesn't see this

Kiro CLI's ACP implementation parses the response loosely and accepts {"optionId": "allow_always"} as if it were already inside the outcome wrapper. So this bug is invisible when running the kiro-cli backend, but every Claude Code or Cursor user is hitting it on every tool call.

Suggested fix

Wrap the response correctly:

let reply = JsonRpcResponse::new(id, json!({
    "outcome": {
        "outcome": "selected",
        "optionId": "allow_always"
    }
}));

I've patched my local fork with this change and all Claude Code tool calls now succeed end-to-end: Discord mention → claude-agent-acp spawned → request_permission → openab auto-allow → tool executes → result streams back. Verified with a git pull + file edit + git commit -m ... + git push flow.

Also fixes / relates to

Happy to send a PR for the wrap fix alone, or a combined wrap + optionId-selection fix that also closes #111. Just say which scope you prefer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingp1High — address this sprint

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions