{"record":{"id":"e100db6ebf454b82","repo":"zeroclaw-labs/zeroclaw","slug":"acp-request-permission-failed","errorCode":null,"errorMessage":"ACP request_permission failed: {} ({})","messagePattern":"ACP request_permission failed: (.+?) \\((.+?)\\)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/acp_channel.rs","lineNumber":95,"sourceCode":"        let params = json!({\n            \"sessionId\": self.session_id,\n            \"options\": options,\n            // `toolCall` is required by the ACP schema. We use a synthetic\n            // ask_user tool call so the client surfaces the prompt with a\n            // sensible title.\n            \"toolCall\": {\n                \"toolCallId\": format!(\"ask-user-{}\", uuid::Uuid::new_v4()),\n                \"title\": question,\n                \"kind\": \"other\",\n                \"status\": \"pending\",\n            }\n        });\n\n        let call = self.rpc.request(\"session/request_permission\", params);\n        let response = match tokio::time::timeout(timeout, call).await {\n            Ok(Ok(value)) => value,\n            Ok(Err(e)) => {\n                anyhow::bail!(\"ACP request_permission failed: {} ({})\", e.message, e.code)\n            }\n            Err(_) => anyhow::bail!(\"ACP request_permission timed out after {timeout:?}\"),\n        };\n\n        // Response shape: { outcome: { outcome: \"selected\", optionId: \"...\" } | { outcome: \"cancelled\" } }\n        let outcome = response.get(\"outcome\");\n        let kind = outcome\n            .and_then(|o| o.get(\"outcome\"))\n            .and_then(|s| s.as_str())\n            .unwrap_or(\"\");\n        match kind {\n            \"selected\" => {\n                let option_id = outcome\n                    .and_then(|o| o.get(\"optionId\"))\n                    .and_then(|s| s.as_str())\n                    .unwrap_or(\"\");\n                let idx = option_id\n                    .strip_prefix(\"choice-\")","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/acp_channel.rs#L77-L113","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Set resource_name() (the <resource> prefix of <resource>.openai.azure.com) before build().","Validate config at load time: reject azure provider entries missing resource or deployment with a clear error before the builder runs.","Restructure to make missing values unrepresentable: a new(resource, deployment) constructor, keeping Option setters only for genuinely optional fields like api_version.","After touching this code, run the azure_openai builder tests (url_construction_*, auth_header_*, creates_*)."],"exampleFix":"// before\nlet provider = AzureOpenAiModelProvider::builder()\n    .deployment_name(\"gpt-4o\")\n    .build(); // panics: resource_name() is required\n\n// after\nlet provider = AzureOpenAiModelProvider::builder()\n    .resource_name(\"my-resource\")\n    .deployment_name(\"gpt-4o\")\n    .build();","handlingStrategy":"validation","validationCode":"// Validate config before touching the builder:\nfn azure_entry_ok(resource: &Option<String>, deployment: &Option<String>) -> Result<(), String> {\n    if resource.as_deref().unwrap_or(\"\").is_empty() {\n        return Err(\"azure provider: resource_name missing\".into());\n    }\n    if deployment.as_deref().unwrap_or(\"\").is_empty() {\n        return Err(\"azure provider: deployment_name missing\".into());\n    }\n    Ok(())\n}","typeGuard":"// Builder fields are private, so guard at the config layer:\nfn has_azure_required(cfg: &serde_json::Value) -> bool {\n    cfg.get(\"resource_name\").and_then(|v| v.as_str()).map_or(false, |s| !s.is_empty())\n        && cfg.get(\"deployment_name\").and_then(|v| v.as_str()).map_or(false, |s| !s.is_empty())\n}","tryCatchPattern":"// Builders panic rather than return Result; contain it if you must:\nlet p = std::panic::catch_unwind(|| {\n    AzureOpenAiModelProvider::builder()\n        .resource_name(r.clone())\n        .deployment_name(d.clone())\n        .build()\n});\nmatch p {\n    Ok(provider) => { /* use */ }\n    Err(_) => { /* report config error with both required keys */ }\n}","preventionTips":["Map every required config key explicitly and fail at load time with the key names","Prefer constructors that take required parameters over Option setters","Keep the azure builder unit tests (url_construction_*, auth_header_*, creates_*) green","Add a config schema check for azure providers in CI"],"tags":["rust","builder-pattern","azure","openai","config-validation","panic"],"backgroundTag":"builder-missing-required-field","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}