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

tenant_access_token failed: {msg}

Error message

tenant_access_token failed: {msg}

What it means

The tenant_access_token endpoint returned HTTP 2xx, but the JSON body carried a non-zero Lark business code. Lark reports application errors as {"code": ..., "msg": ...} even with a 200 status; this variant almost always means the credentials themselves are wrong or the app is gone. The msg from the body is included ("unknown error" if the field is missing), and code=-1 appears when no code field could be parsed.

Source

Thrown at crates/zeroclaw-channels/src/lark.rs:1506

            "app_id": self.app_id,
            "app_secret": self.app_secret,
        });

        let resp = self.http_client().post(&url).json(&body).send().await?;
        let status = resp.status();
        let data: serde_json::Value = resp.json().await?;

        if !status.is_success() {
            anyhow::bail!("tenant_access_token request failed: status={status}, body={data}");
        }

        let code = data.get("code").and_then(|c| c.as_i64()).unwrap_or(-1);
        if code != 0 {
            let msg = data
                .get("msg")
                .and_then(|m| m.as_str())
                .unwrap_or("unknown error");
            anyhow::bail!("tenant_access_token failed: {msg}");
        }

        let token = data
            .get("tenant_access_token")
            .and_then(|t| t.as_str())
            .ok_or_else(|| {
                ::zeroclaw_log::record!(
                    WARN,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
                        .with_outcome(::zeroclaw_log::EventOutcome::Failure),
                    "missing tenant_access_token in response"
                );
                anyhow::Error::msg("missing tenant_access_token in response")
            })?
            .to_string();

        let ttl_seconds = extract_lark_token_ttl_seconds(&data);
        let refresh_after = next_token_refresh_deadline(Instant::now(), ttl_seconds);

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Re-copy both App ID and App Secret from the Lark developer console (Credentials page) and update [channels.lark.<alias>] exactly.
  2. Inspect the secret for stray whitespace, quotes, or newlines and strip them.
  3. Confirm the app still exists, is enabled, and has not been deleted.
  4. After fixing credentials, restart the channel so it drops any cached token state.

Example fix

# before (TOML config)
[channels.lark.main]
app_id = "cli_a1b2c3"
app_secret = "\"xYz...secret...\"\n"   # pasted with quotes and trailing newline

# after
[channels.lark.main]
app_id = "cli_a1b2c3"
app_secret = "xYz...secret..."
Defensive patterns

Strategy: validation

Validate before calling

// Fail fast at startup on bad credentials instead of mid-conversation
let token = lark.get_tenant_access_token().await
    .context("Lark credentials rejected at startup; check app_id/app_secret in [channels.lark.<alias>]")?;

Try / catch

if let Err(e) = lark.get_tenant_access_token().await {
    if e.to_string().contains("tenant_access_token failed") {
        // business-code rejection: do NOT retry; credentials are wrong
        return Err(e.context("fix Lark app credentials before retrying"));
    }
    return Err(e);
}

Prevention

When it happens

Trigger: Invalid or revoked app_id/app_secret, an app that was deleted or disabled in the Lark developer console, a secret with trailing whitespace/newline/quotes copied from the console, or credentials from a different app pasted into config.

Common situations: The app secret was rotated in the Lark console but the config was never updated; the App ID was pasted into the app_secret field; environment-specific credentials (dev vs prod app) got mixed; secret stored with surrounding quotes or a newline.

Related errors


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