{"record":{"id":"a6604d558be324aa","repo":"tinyhumansai/openhuman","slug":"composio-execute-tool-once-tool-slug-must-not-be","errorCode":null,"errorMessage":"composio.execute_tool_once: tool slug must not be empty","messagePattern":"composio\\.execute_tool_once: tool slug must not be empty","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src/openhuman/integrations/composio/client.rs","lineNumber":237,"sourceCode":"            }\n        }\n        Ok(resp)\n    }\n\n    /// `POST /agent-integrations/composio/execute` — single, non-retrying\n    /// HTTP round-trip. Use this when the caller owns the retry loop\n    /// (e.g. `auth_retry`) to avoid double-retry. In particular,\n    /// [`super::auth_retry::execute_with_auth_retry`] uses this entry\n    /// point so its `must retry exactly once` contract still holds\n    /// after PR #1707 introduced the inner retry.\n    pub(crate) async fn execute_tool_once(\n        &self,\n        tool: &str,\n        arguments: Option<serde_json::Value>,\n    ) -> Result<ComposioExecuteResponse> {\n        let tool = tool.trim();\n        if tool.is_empty() {\n            anyhow::bail!(\"composio.execute_tool_once: tool slug must not be empty\");\n        }\n        // Egress spine (privacy epic S2, #4436): see `execute_tool`. This is the\n        // caller-owns-retry entry point (e.g. `auth_retry`), disjoint from\n        // `execute_tool`, so each logical tool call emits exactly once.\n        let egress = crate::openhuman::security::egress::EgressDescriptor::composio(tool);\n        // Local-only enforcement (privacy epic S7, #4441): same gate as\n        // `execute_tool` — this disjoint entry point must block too.\n        crate::openhuman::security::egress::enforce_egress(&egress)?;\n        crate::openhuman::security::egress::emit_external_transfer(egress);\n        let arguments = super::execute_prepare::prepare_execute_arguments(tool, arguments)\n            .map_err(anyhow::Error::msg)?;\n        tracing::debug!(tool = %tool, \"[composio] execute_tool_once (no built-in retry)\");\n        let body = json!({ \"tool\": tool, \"arguments\": arguments });\n        let result = self.post_execute_tool(&body).await;\n        match &result {\n            Ok(resp) => tracing::debug!(\n                tool = %tool,\n                successful = resp.successful,","sourceCodeStart":219,"sourceCodeEnd":255,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/integrations/composio/client.rs#L219-L255","documentation":"execute_tool_once is the caller-owns-retry entry point (used by auth_retry so a logical tool call retries exactly once); like execute_tool it rejects a tool slug that is empty after trimming, before egress disclosure and argument preparation. The guard keeps the disjoint entry points consistent: no path can send a blank slug.","triggerScenarios":"Calling execute_tool_once(\"\", arguments) directly, or auth_retry receiving a blank slug and forwarding it here. Typically the same caller bugs as execute_tool: LLM tool calls with missing action names.","commonSituations":"Custom retry wrappers that call execute_tool_once directly with unvalidated input; agent harnesses forwarding model output verbatim; templated action names that rendered empty.","solutions":["Validate the slug before entering your retry wrapper — this entry point exists for caller-owned control flow, so own the validation too","Re-prompt or skip when the model produces a blank action name","Assert non-empty where tool calls are deserialized from model output"],"exampleFix":"// before\nlet resp = client.execute_tool_once(slug, args).await?;\n\n// after\nlet slug = slug.trim();\nif slug.is_empty() {\n    return Err(anyhow::anyhow!(\"refusing to execute composio action with empty slug\"));\n}\nlet resp = client.execute_tool_once(slug, args).await?;","handlingStrategy":"validation","validationCode":"let tool = tool.trim();\nif tool.is_empty() {\n    anyhow::bail!(\"cannot execute composio tool with an empty slug\");\n}\nlet resp = client.execute_tool_once(tool, arguments).await?;","typeGuard":"fn is_non_empty_slug(s: &str) -> bool {\n    !s.trim().is_empty()\n}","tryCatchPattern":null,"preventionTips":["This entry point exists for caller-owned retry loops — validate input before entering yours","Share one slug validation helper across execute_tool and execute_tool_once call sites","Assert non-empty in tests of your retry wrapper so regressions surface there first"],"tags":["composio","execute","validation","empty-string","argument-validation","retry"],"backgroundTag":"empty-string-argument","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}