Skip to content

Implement: P2 MediaSanitizer decode/validate/attach + vision-tier routing for MCP image passthrough #6240

Description

@bug-ops

Spec

See specs/072-multimodal-mcp-passthrough/plan.md §"P2 — Decode, Validate, Attach (PR 3)". Parent issue: #6229. P0 landed in PR #6238. Depends on P1 (#6239) merging first — plan.md orders the persistence strip before any code path can produce an Image part, so P2's own tests can rely on P1's strip instead of re-verifying it.

Summary

Add MediaSanitizer (magic-byte sniff, format allowlist, size/dimension/pixel caps), wire the MCP-side opt-in decode path, and add the sibling-MessagePart::Image emission + vision-tier routing gate in process_one_tool_result. This is the phase where Image parts can first actually be produced from MCP tool results.

Scope corrections vs. the original plan.md text

plan.md's P0 section originally listed ToolResultClassification.media (field + population in classify_tool_result) as a P0 deliverable. During P0's review (PR #6238), the critic found that adding a write-only media field to the private, exhaustively-destructured ToolResultClassification struct with no reader yet would fail cargo clippy -D warnings (dead-code/unused-field). P0 deferred this entirely — field and reader must land together, in this phase (P2), not split across phases. Concretely, this issue's scope now includes, as an explicit first deliverable:

  1. crates/zeph-core/src/agent/tool_execution/mod.rs — add media: Vec<zeph_llm::ImageData> to ToolResultClassification. crates/zeph-core/src/agent/tool_execution/tool_result.rs — populate it in classify_tool_result's Ok(Some(out)) arm (media: out.media) and empty in the Ok(None)/Err arms. Land this together with deliverable 6 below (the reader) in the same PR — do not split.

Scope (from plan.md, full deliverable list has exact file/line references)

  1. crates/zeph-sanitizer — new MediaSanitizer (mirrors ContentSanitizer's policy-object shape): magic-byte sniff vs. declared MIME, format allowlist, byte-size cap before decode, spawn_blocking decode via the image crate, dimension/pixel caps enforced post-decode, MediaRejected (thiserror) enum.
  2. crates/zeph-config/src/channels.rsMcpServerConfig.media_passthrough: bool (default false), McpMediaConfig with the pinned defaults (5 MiB / 8192px / 64MP / 4-per-result / 8-per-turn).
  3. crates/zeph-mcp/src/executor.rs — opt-in gate (hard-blocked when server trust is Sandboxed), sanitize each ContentBlock::Image, collect into ToolOutput.media, log accept/reject via the existing tool audit path.
  4. crates/zeph-core/src/agent/tool_execution/tool_result.rsprocess_one_tool_result: push sibling MessagePart::Image per validated entry when the turn's tier is (or will become) vision-capable; respect max_images_per_turn; drop with a warning otherwise (never a runtime 400/422 — AC-6).
  5. crates/zeph-llm/src/router/triage.rs — vision-capability signal for provider/tier selection when the pending request has media parts. Must use the existing provider-registry pattern (no hardcoded model/provider) per CLAUDE.md's Multi-Model Design Principle.
  6. System-prompt caveat: static line added when any server has media_passthrough = true, marking tool-sourced images as untrusted data.

Additional finding to fold in (from P0's security validation, PR #6238)

The redacting Debug impl for ImageData (landed in P0) does not cover the debug-dump JSON export path, which serializes via serde_json/Serialize rather than Debug and therefore bypasses the redaction. Confirmed sites: crates/zeph-core/src/debug_dump/mod.rs:402 (serde_json::to_value(request.messages)) and the custom image handler at :622-627 (base64::...encode(&img.data)), both writing raw image bytes into the dump. This is pre-existing (already reachable via the shipped user-upload MessagePart::Image path) and not exercised by P0 (P0 never populates media), but must be fixed as part of this phase since P2 is where MCP-sourced images first actually flow — AC-3 ("no raw bytes in logs/debug-dumps") requires it. Redact or omit ImageData.data in both debug_dump paths.

Mandatory gate

Per the LLM Serialization Gate (.claude/rules/continuous-improvement.md), a live cascade + MCP-image session test is required before this PR merges: run the agent with cargo run --features full -- --config .local/config/testing.toml, exercise a mock or real MCP server with media_passthrough = true behind a cascade/triage provider pool with mixed vision capability, and verify no 400/422 in the debug dump. Document the result in the PR description.

Acceptance criteria

AC-1 through AC-4, AC-6 through AC-9 (full), AC-13, AC-14, AC-15. Full test list: specs/072-multimodal-mcp-passthrough/plan.md §P2.

Suggested next step

Run /rust-agents:team-develop new-feature on this issue after #6239 (P1) merges. The architect should re-verify the ToolResultClassification.media deferral note above against HEAD before the developer starts, since P1/main will have moved since this issue was filed.

Metadata

Metadata

Assignees

Labels

P2High value, medium complexityllmzeph-llm crate (Ollama, Claude)mcpMCP client/serversecuritySecurity-related issue

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions