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

Lark/Feishu marker target uses a disallowed scheme

Error message

Lark/Feishu marker target uses a disallowed scheme

What it means

validate_lark_marker_target lowercases the target and rejects any string starting with http://, https://, data: or file:, or containing '://' at all — media markers must reference local files, never URLs or inline data. The guard exists because the resolved path is later uploaded to Lark, and URL targets would either break path resolution or smuggle remote content; the WARN log records reason=disallowed_scheme.

Source

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

    let trimmed = target.trim();
    if trimmed.is_empty() {
        anyhow::bail!("Lark/Feishu marker target is empty");
    }

    let lower = trimmed.to_ascii_lowercase();
    if lower.starts_with("http://")
        || lower.starts_with("https://")
        || lower.starts_with("data:")
        || lower.starts_with("file:")
        || lower.contains("://")
    {
        ::zeroclaw_log::record!(
            WARN,
            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
                .with_attrs(::serde_json::json!({"reason": "disallowed_scheme"})),
            "lark: marker target uses disallowed scheme"
        );
        anyhow::bail!("Lark/Feishu marker target uses a disallowed scheme");
    }

    let workspace = workspace_dir.ok_or_else(|| {
        ::zeroclaw_log::record!(
            WARN,
            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
                .with_attrs(::serde_json::json!({"reason": "no_workspace_dir"})),
            "lark: local marker target has no workspace_dir"
        );
        anyhow::Error::msg("Lark/Feishu channel was started without a workspace_dir")
    })?;

    let workspace = std::fs::canonicalize(workspace).map_err(|err| {
        anyhow::Error::msg(format!(
            "canonicalize Lark/Feishu workspace_dir failed: {err}"
        ))
    })?;
    let candidate = Path::new(trimmed);

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Download the URL into workspace_dir first (with your own fetch step), then emit a marker with the workspace-relative path
  2. For data: URIs, decode and write the bytes to a file inside the workspace, then reference that file
  3. Adjust the agent instructions so media markers always carry a path under the workspace, never a link

Example fix

// before — marker target is a URL
{ "kind": "image", "target": "https://cdn.example.com/cat.png" }

// after — fetch to workspace, then reference the local file
let path = fetch_to_workspace("https://cdn.example.com/cat.png").await?;
{ "kind": "image", "target": path.to_string_lossy() }
Defensive patterns

Strategy: validation

Validate before calling

// Reject URL/scheme targets before sending:
fn is_local_marker_target(t: &str) -> bool {
    let l = t.trim().to_ascii_lowercase();
    !l.is_empty() && !l.contains("://") && !l.starts_with("data:") && !l.starts_with("file:")
}

Try / catch

if let Err(e) = channel.send(&msg).await {
    if e.to_string().contains("disallowed scheme") {
        // fetch the URL into workspace_dir yourself, replace target with the local path, retry once
    }
}

Prevention

When it happens

Trigger: An outgoing media marker carries target = "https://cdn.example.com/img.png", "data:image/png;base64,...", "file:///Users/x/a.png", or any scheme-bearing string; resolve_lark_media_marker -> validate_lark_marker_target rejects it before workspace resolution. The tests lark_marker_target_rejects_url_schemes pin this behaviour.

Common situations: The model pastes a URL instead of producing a local file; a tool returns links rather than writing artifacts into the workspace; prompt examples demonstrate URL-based attachments.

Related errors


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