{"record":{"id":"e749b15c030cc3a9","repo":"astrid-runtime/astrid","slug":"tool-describe-interceptor-failed-e","errorCode":null,"errorMessage":"tool_describe interceptor failed: {e}","messagePattern":"tool_describe interceptor failed: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule/src/tool_discovery.rs","lineNumber":128,"sourceCode":"    )\n    .with_principal(principal.to_string())\n}\n\n/// Pure mapping of a `tool_describe` interceptor outcome to a captured tool\n/// surface. Split out from [`describe_loaded_capsule`] so the capture semantics\n/// — especially the pool-less `NotSupported => None` case that fixes #1198 — are\n/// unit-testable without a live capsule.\nfn interpret_describe_result(\n    outcome: Result<InterceptResult, crate::error::CapsuleError>,\n) -> anyhow::Result<Option<Vec<ToolDescriptor>>> {\n    let result = match outcome {\n        Ok(r) => r,\n        // Pool-less run-loop capsule: the interceptor path isn't available, so\n        // the tool surface is UNKNOWN — not empty. Signal absent (`None`) so the\n        // caller lets the describe fan-out supply it (#1198), rather than\n        // injecting `[]` and suppressing the fan-out.\n        Err(e) if is_unsupported(&e) => return Ok(None),\n        Err(e) => return Err(anyhow::anyhow!(\"tool_describe interceptor failed: {e}\")),\n    };\n\n    let payload = match result {\n        InterceptResult::Continue(bytes) | InterceptResult::Final(bytes) => bytes,\n        // A capsule that CAN run interceptors but has no `#[astrid::tool]` arm\n        // (e.g. the broker) genuinely has zero static tools: captured-empty\n        // (`Some([])`), NOT absent — there is nothing for a fan-out to supply.\n        InterceptResult::Deny { reason } if is_unknown_action(&reason) => {\n            return Ok(Some(Vec::new()));\n        },\n        // Any other deny is a genuine refusal — surface it.\n        InterceptResult::Deny { reason } => {\n            anyhow::bail!(\"tool_describe interceptor denied: {reason}\");\n        },\n    };\n\n    if payload.is_empty() {\n        return Ok(Some(Vec::new()));","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule/src/tool_discovery.rs#L110-L146","documentation":"interpret_describe_result converts a failing tool_describe interceptor invocation into this anyhow error. The interceptor channel itself errored in a way that is NOT an 'unsupported' reply: Err(e) matching is_unsupported returns Ok(None) (tool surface unknown, fan-out allowed), but any other error is fatal and wrapped with this message.","triggerScenarios":"describe_loaded_capsule_status(_for) or the fan-out tests trigger interpret_describe_result; the capsule's tool_describe interceptor returns an error other than a not-supported/unsupported signal — e.g. the guest capsule panicked, the IPC/interceptor transport failed, or the guest returned a malformed error reply.","commonSituations":"Capsule running without the interceptor path but reporting a non-unsupported error; a broken or hostile capsule crashing during tool_describe; version mismatch where the guest's error reply is no longer recognized as 'unsupported'; transport/IPC failures between host and capsule runtime.","solutions":["Inspect the wrapped error `{e}` in the chain — it carries the actual interceptor failure","Update/rebuild the capsule so its tool_describe interceptor works or correctly signals 'unsupported' instead of erroring","Check runtime/version compatibility between host and capsule (unsupported-signal detection may be stale)","If the capsule is pool-less and cannot run interceptors, ensure it returns the unsupported signal so Ok(None) is taken and the describe fan-out runs"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// pre-check: only call describe when the capsule supports interceptors\nif !capsule.supports_interceptor(\"tool_describe\") {\n    return Ok(None); // let the describe fan-out supply the tool surface\n}","typeGuard":"fn is_unsupported_err(e: &anyhow::Error) -> bool {\n    e.chain().any(|c| is_unsupported(c))\n}","tryCatchPattern":"match interpret_describe_result(&capsule) {\n    Ok(None) => fan_out_describe(&capsule), // unsupported: tool surface unknown, not empty\n    Ok(Some(tools)) => apply(tools),\n    Err(e) if e.to_string().contains(\"interceptor failed\") => {\n        log::warn!(\"describe failed, falling back to fan-out: {e}\");\n        fan_out_describe(&capsule)\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Keep host and capsule runtime versions aligned so 'unsupported' signals are recognized","Test capsules' tool_describe interceptor in CI before release","Handle Ok(None) explicitly: an unavailable interceptor means UNKNOWN tools, never inject []"],"tags":["rust","wasm","interceptor","tool-discovery","ipc"],"backgroundTag":"upstream-api-error","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}