tinyhumansai/openhuman · error · anyhow::Error

composio.execute_tool_once: tool slug must not be empty

Error message

composio.execute_tool_once: tool slug must not be empty

What it means

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.

Source

Thrown at src/openhuman/integrations/composio/client.rs:237

            }
        }
        Ok(resp)
    }

    /// `POST /agent-integrations/composio/execute` — single, non-retrying
    /// HTTP round-trip. Use this when the caller owns the retry loop
    /// (e.g. `auth_retry`) to avoid double-retry. In particular,
    /// [`super::auth_retry::execute_with_auth_retry`] uses this entry
    /// point so its `must retry exactly once` contract still holds
    /// after PR #1707 introduced the inner retry.
    pub(crate) async fn execute_tool_once(
        &self,
        tool: &str,
        arguments: Option<serde_json::Value>,
    ) -> Result<ComposioExecuteResponse> {
        let tool = tool.trim();
        if tool.is_empty() {
            anyhow::bail!("composio.execute_tool_once: tool slug must not be empty");
        }
        // Egress spine (privacy epic S2, #4436): see `execute_tool`. This is the
        // caller-owns-retry entry point (e.g. `auth_retry`), disjoint from
        // `execute_tool`, so each logical tool call emits exactly once.
        let egress = crate::openhuman::security::egress::EgressDescriptor::composio(tool);
        // Local-only enforcement (privacy epic S7, #4441): same gate as
        // `execute_tool` — this disjoint entry point must block too.
        crate::openhuman::security::egress::enforce_egress(&egress)?;
        crate::openhuman::security::egress::emit_external_transfer(egress);
        let arguments = super::execute_prepare::prepare_execute_arguments(tool, arguments)
            .map_err(anyhow::Error::msg)?;
        tracing::debug!(tool = %tool, "[composio] execute_tool_once (no built-in retry)");
        let body = json!({ "tool": tool, "arguments": arguments });
        let result = self.post_execute_tool(&body).await;
        match &result {
            Ok(resp) => tracing::debug!(
                tool = %tool,
                successful = resp.successful,

View on GitHub (pinned to 7491200858)

Solutions

  1. Validate the slug before entering your retry wrapper — this entry point exists for caller-owned control flow, so own the validation too
  2. Re-prompt or skip when the model produces a blank action name
  3. Assert non-empty where tool calls are deserialized from model output

Example fix

// before
let resp = client.execute_tool_once(slug, args).await?;

// after
let slug = slug.trim();
if slug.is_empty() {
    return Err(anyhow::anyhow!("refusing to execute composio action with empty slug"));
}
let resp = client.execute_tool_once(slug, args).await?;
Defensive patterns

Strategy: validation

Validate before calling

let tool = tool.trim();
if tool.is_empty() {
    anyhow::bail!("cannot execute composio tool with an empty slug");
}
let resp = client.execute_tool_once(tool, arguments).await?;

Type guard

fn is_non_empty_slug(s: &str) -> bool {
    !s.trim().is_empty()
}

Prevention

When it happens

Trigger: 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.

Common situations: Custom retry wrappers that call execute_tool_once directly with unvalidated input; agent harnesses forwarding model output verbatim; templated action names that rendered empty.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/a6604d558be324aa. Report an issue: GitHub.