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

ACP request_permission failed: {} ({})

Error message

ACP request_permission failed: {} ({})

What it means

AzureOpenAiBuilder::build() constructs the Azure OpenAI provider and, per its doc comment, requires both resource_name() and deployment_name() because the deployment URL https://{resource}.openai.azure.com/openai/deployments/{deployment} has no sensible default for either. This expect fires when build() runs without resource_name having been set. The tests listed under RAISED IN are the canonical callers that always set both before building.

Source

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

        let params = json!({
            "sessionId": self.session_id,
            "options": options,
            // `toolCall` is required by the ACP schema. We use a synthetic
            // ask_user tool call so the client surfaces the prompt with a
            // sensible title.
            "toolCall": {
                "toolCallId": format!("ask-user-{}", uuid::Uuid::new_v4()),
                "title": question,
                "kind": "other",
                "status": "pending",
            }
        });

        let call = self.rpc.request("session/request_permission", params);
        let response = match tokio::time::timeout(timeout, call).await {
            Ok(Ok(value)) => value,
            Ok(Err(e)) => {
                anyhow::bail!("ACP request_permission failed: {} ({})", e.message, e.code)
            }
            Err(_) => anyhow::bail!("ACP request_permission timed out after {timeout:?}"),
        };

        // Response shape: { outcome: { outcome: "selected", optionId: "..." } | { outcome: "cancelled" } }
        let outcome = response.get("outcome");
        let kind = outcome
            .and_then(|o| o.get("outcome"))
            .and_then(|s| s.as_str())
            .unwrap_or("");
        match kind {
            "selected" => {
                let option_id = outcome
                    .and_then(|o| o.get("optionId"))
                    .and_then(|s| s.as_str())
                    .unwrap_or("");
                let idx = option_id
                    .strip_prefix("choice-")

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Set resource_name() (the <resource> prefix of <resource>.openai.azure.com) before build().
  2. Validate config at load time: reject azure provider entries missing resource or deployment with a clear error before the builder runs.
  3. Restructure to make missing values unrepresentable: a new(resource, deployment) constructor, keeping Option setters only for genuinely optional fields like api_version.
  4. After touching this code, run the azure_openai builder tests (url_construction_*, auth_header_*, creates_*).

Example fix

// before
let provider = AzureOpenAiModelProvider::builder()
    .deployment_name("gpt-4o")
    .build(); // panics: resource_name() is required

// after
let provider = AzureOpenAiModelProvider::builder()
    .resource_name("my-resource")
    .deployment_name("gpt-4o")
    .build();
Defensive patterns

Strategy: validation

Validate before calling

// Validate config before touching the builder:
fn azure_entry_ok(resource: &Option<String>, deployment: &Option<String>) -> Result<(), String> {
    if resource.as_deref().unwrap_or("").is_empty() {
        return Err("azure provider: resource_name missing".into());
    }
    if deployment.as_deref().unwrap_or("").is_empty() {
        return Err("azure provider: deployment_name missing".into());
    }
    Ok(())
}

Type guard

// Builder fields are private, so guard at the config layer:
fn has_azure_required(cfg: &serde_json::Value) -> bool {
    cfg.get("resource_name").and_then(|v| v.as_str()).map_or(false, |s| !s.is_empty())
        && cfg.get("deployment_name").and_then(|v| v.as_str()).map_or(false, |s| !s.is_empty())
}

Try / catch

// Builders panic rather than return Result; contain it if you must:
let p = std::panic::catch_unwind(|| {
    AzureOpenAiModelProvider::builder()
        .resource_name(r.clone())
        .deployment_name(d.clone())
        .build()
});
match p {
    Ok(provider) => { /* use */ }
    Err(_) => { /* report config error with both required keys */ }
}

Prevention

When it happens

Trigger: Calling build() on a builder whose resource_name() setter was skipped — typically provider config parsing that maps an Azure entry missing its resource field straight onto the builder.

Common situations: A config file with an azure provider entry missing the resource name key; a rename of the config field so the mapping silently drops it; a new code path constructing the builder without porting all required setters.

Related errors


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