Hmbown/CodeWhale · error · io::Error
invalid tool image evidence
Error message
invalid tool image evidence
What it means
`publish_image` turns a tool-result image block into persisted rich media evidence. It base64-decodes the block's data and then re-validates the bytes with the shared `decode_and_guard_image` validator; any failure to decode, decode-guard, or even serialize the identity tuple raises this generic `InvalidData` error rather than trusting metadata or headers.
Solutions
- Fix the tool/provider so the image block contains valid base64 of a supported image format (check for truncation, whitespace, or wrong encoding).
- Re-run the tool call that produced the image to regenerate a clean payload.
- Inspect the payload: `base64 -d < data.b64 > img.bin && file img.bin` to see whether the bytes form a real image.
- If your pipeline transforms tool output, verify it does not re-encode or corrupt the base64 data before it reaches the engine.
Example fix
// before: trusting/forwarding raw tool payload
data: block.data.clone(),
// after (caller side): validate before publishing
let bytes = STANDARD.decode(&payload).map_err(|_| anyhow::anyhow!("invalid base64"))?;
image::load_from_memory(&bytes)?; // ensure decodable before handing to publish_image Defensive patterns
Strategy: validation
Validate before calling
use base64::Engine;
fn tool_image_bytes_ok(data: &str) -> Option<Vec<u8>> {
let bytes = base64::engine::general_purpose::STANDARD.decode(data).ok()?;
image::load_from_memory(&bytes).ok()?;
Some(bytes)
} Type guard
fn is_valid_image_block(block: &ToolResultContentBlock) -> bool {
matches!(block, ToolResultContentBlock::Image { data, .. })
&& tool_image_bytes_ok(data).is_some()
} Try / catch
match publish_image(block, session_id, call_id, tool_name) {
Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {
log::warn!("tool returned invalid image evidence for {call_id}; skipping media render");
// degrade gracefully: render the tool result without the image
}
other => other?,
} Prevention
- Validate tool outputs at the boundary: base64-decode and image-decode before trusting media blocks.
- Keep tool image payloads unmodified end-to-end (no re-encoding in proxies or relays).
- Prefer standard base64 with correct padding from tools emitting images.
- Add a fixture test per supported image format your tools emit.
When it happens
Trigger: `project` -> `publish_image` with a `ToolResultContentBlock::Image` whose `data` is not valid base64, whose decoded bytes are not a supported/valid image (per the shared rich-image validator), or with corrupted internal serialization input.
Common situations: A tool (or a provider relaying tool output) emitted a malformed or truncated image payload; the base64 string contains invalid characters or wrong padding; the image is in an unsupported codec or fails dimension/format guards.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- image has invalid base64
- image base64 is not canonical
- image exceeds the MiB limit
- image MIME does not match its content
- images exceed the 5 MiB total limit
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/d8634b28f56e8b7f.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/core/engine/tool_media.rs:73
})
.await
.unwrap_or_else(|_| {
let mut rich = RichToolResult::plain(fallback);
rich.result
.content
.push_str("\n[Tool image omitted: image preparation failed.]");
rich
})
}
fn publish_image(
block: &ToolResultContentBlock,
session_id: &str,
call_id: &str,
tool_name: &str,
) -> io::Result<Value> {
let ToolResultContentBlock::Image { mime_type, data } = block;
let invalid = || io::Error::new(io::ErrorKind::InvalidData, "invalid tool image evidence");
let bytes = STANDARD.decode(data).map_err(|_| invalid())?;
// The caller passed the shared rich-image validator; retain the shared decode
// guard when deriving dimensions rather than trusting metadata or headers.
let (_, width, height) =
crate::image_attach::decode_and_guard_image(&bytes).map_err(|_| invalid())?;
let identity = serde_json::to_vec(&(session_id, call_id, 0u8)).map_err(|_| invalid())?;
let handle = format!("art_image_{}", crate::hashing::sha256_hex(&identity));
let storage_path =
PathBuf::from(crate::artifacts::ARTIFACTS_DIR_NAME).join(format!("{handle}.image"));
let digest = crate::hashing::sha256_hex(&bytes);
let now = unix_millis_now();
let proposed = EvidenceArtifact {
handle: handle.clone(),
digest: digest.clone(),
size_bytes: bytes.len() as u64,
content_type: mime_type.clone(),
tool_name: tool_name.to_owned(),
call_id: call_id.to_owned(),View on GitHub (pinned to 73e0f67d83)