zeroclaw-labs/zeroclaw · error · anyhow::Error

ACP returned unexpected permission outcome: {other}

Error message

ACP returned unexpected permission outcome: {other}

What it means

The approval response's outcome.outcome was neither "selected" nor "cancelled" — the only two outcomes request_approval_attributed understands. As with the choice path, a response missing the nested outcome field parses as an empty string and lands here too. It signals a malformed or version-drifted client response, not an operator decision.

Source

Thrown at crates/zeroclaw-channels/src/acp_channel.rs:542

                            .unwrap_or("")
                            .to_string();
                        ChannelApprovalResponse::DenyWithEdit { replacement }
                    }
                    other => anyhow::bail!("ACP returned unknown permission optionId: {other}"),
                };
                Ok(Some(
                    zeroclaw_api::channel::AttributedApprovalResponse::operator(response),
                ))
            }
            // The client withdrew the prompt; nobody chose anything. Still a
            // deny, but the runtime's, not the operator's.
            "cancelled" => Ok(Some(
                zeroclaw_api::channel::AttributedApprovalResponse::from_runtime(
                    ChannelApprovalResponse::Deny,
                    zeroclaw_api::channel::ApprovalSource::Unreachable,
                ),
            )),
            other => anyhow::bail!("ACP returned unexpected permission outcome: {other}"),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tokio::sync::mpsc;
    use zeroclaw_api::elicitation::single_select_schema_with_property_name;
    use zeroclaw_api::jsonrpc::JSONRPC_VERSION;

    fn make_rpc() -> (Arc<RpcOutbound>, mpsc::Receiver<String>) {
        let (tx, rx) = mpsc::channel::<String>(16);
        (Arc::new(RpcOutbound::new(tx)), rx)
    }

    #[tokio::test]
    async fn name_returns_provided_name() {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Capture and log the raw outcome object to see the exact value the client sent.
  2. Align the client with the ACP permission response schema: {outcome: {outcome: selected|cancelled, ...}}.
  3. Map this error to a runtime Deny in the caller so the gated tool does not run.
  4. Add a client-side regression test that replies with only the two legal outcome kinds.
Defensive patterns

Strategy: fallback

Try / catch

// Any unparseable approval outcome is a runtime Deny (like 'cancelled'),
// attributed to the runtime rather than the operator.
let resp = ch.request_approval(recipient, &req).await
    .unwrap_or(Some(ChannelApprovalResponse::Deny))
    .unwrap_or(ChannelApprovalResponse::Deny);

Prevention

When it happens

Trigger: session/request_permission for an approval returns an outcome object with an unknown kind string or without the nested outcome/outcome field — e.g. a client sending a flat or differently-named shape.

Common situations: Protocol revision drift after a client update; incomplete client implementations; response shapes copied from a different JSON-RPC method.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/b41f668492523b55. Report an issue: GitHub.