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
- Log the offending optionId shown in the error and compare it against the optionIds ZeroClaw offered in the request.
- Fix the client to echo one of the optionId values from the request's options array verbatim.
- In the caller, map this error to Deny (fail closed) so an ambiguous answer never approves a tool.
- 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
- In client implementations, echo optionIds verbatim from the request options
- Never map an unknown approval answer to approve
- Correlate responses by JSON-RPC request id when multiple prompts are in flight
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
- ACP returned unexpected permission outcome: {other}
- ACP returned unexpected outcome: {other}
- ACP request_permission timed out after {:?}
- ACP elicitation/create failed: {} ({})
- ACP elicitation/create timed out after {timeout:?}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/db944a1d5404958a.
Report an issue: GitHub.