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

ACP returned unknown permission optionId: {other}

Error message

ACP returned unknown permission optionId: {other}

What it means

The client answered an approval prompt with an optionId outside the offered set (allow-once, allow-always, reject-once, reject-always, reject-with-edit). ZeroClaw only recognizes the optionIds it sent, so any other id — including an empty string when the response omits optionId — is treated as a client-side protocol violation, not an operator decision.

Source

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

                let option_id = outcome
                    .and_then(|o| o.get("optionId"))
                    .and_then(|s| s.as_str())
                    .unwrap_or("");
                // "selected" means the operator picked one of the options we
                // offered, so every arm here is a genuine operator decision.
                let response = match option_id {
                    "allow-once" => ChannelApprovalResponse::Approve,
                    "allow-always" => ChannelApprovalResponse::AlwaysApprove,
                    "reject-once" | "reject-always" => ChannelApprovalResponse::Deny,
                    "reject-with-edit" => {
                        let replacement = outcome
                            .and_then(|o| o.get("replacementContent"))
                            .and_then(|s| s.as_str())
                            .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}"),
        }
    }
}

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Log the offending optionId shown in the error and compare it against the optionIds ZeroClaw offered in the request.
  2. Fix the client to echo one of the optionId values from the request's options array verbatim.
  3. In the caller, map this error to Deny (fail closed) so an ambiguous answer never approves a tool.
  4. If a legitimate new option kind appeared, upgrade ZeroClaw to a revision that maps it.

Example fix

// before
let resp = ch.request_approval(recipient, &req).await?;

// after
let resp = match ch.request_approval(recipient, &req).await {
    Ok(r) => r,
    Err(e) => {
        tracing::warn!(error = %e, "unrecognized approval answer; denying");
        return Ok(Some(ChannelApprovalResponse::Deny));
    }
};
Defensive patterns

Strategy: fallback

Try / catch

// An unrecognized approval answer is ambiguous: deny rather than guess.
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 replies with outcome "selected" but a made-up or missing optionId: custom clients inventing ids like "yes", response-building bugs, or a client echoing ids from a different concurrent permission prompt.

Common situations: Hand-rolled ACP client implementations; client-side prompt queues mixing up concurrent permission requests; a spec revision renaming option kinds that ZeroClaw does not yet map.

Related errors


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