""" Shared helpers for materializing multimodal attachment blocks to disk. Both native executors (Claude Code, Codex) receive user messages whose image/file content blocks carry resolved base64 data URIs. Inlining that base64 into the text sent to the native CLI is wrong: Claude Code cannot view it, and the Codex app-server rejects any turn whose input text exceeds 1 MiB (``input_too_large``). Instead each executor decodes the data URI to a file on disk and references it by path — Claude Code via its Read tool, Codex via a ``localImage`` input item. This module owns that shared decode-and-write step. """ from __future__ import annotations import base64 import logging import uuid from dataclasses import dataclass from pathlib import Path from typing import Any _logger = logging.getLogger(__name__) # Maps a data-URI MIME type to the file extension used when no filename # is supplied, e.g. ``"image/png"`` -> ``".png"``. MIME_TO_EXT: dict[str, str] = { "image/png": ".png", "image/jpeg": ".jpg", "image/gif": ".gif", "image/webp": ".webp", "image/svg+xml": ".svg", "application/pdf": ".pdf", "text/plain": ".txt", } @dataclass(frozen=True) class DataUri: """ Decoded components of a ``data:`` URI. :param mime_type: The MIME type, e.g. ``"image/png"``. :param base64_payload: The base64-encoded payload following the comma, e.g. ``"iVBORw0KGgo..."``. """ mime_type: str base64_payload: str def parse_data_uri(uri: str) -> DataUri: """ Split a ``data:`` URI into its MIME type and base64 payload. :param uri: Data URI string, e.g. ``"data:image/png;base64,iVBOR..."``. :returns: A :class:`DataUri` with the MIME type and base64 payload. :raises ValueError: If the URI has no comma separating header from payload. """ # "data:image/png;base64,iVBOR..." header, _, payload = uri.partition(",") if not payload: raise ValueError(f"Malformed data URI: no comma separator in {uri[:80]}") # header = "data:image/png;base64" mime_part = header.removeprefix("data:").removesuffix(";base64") return DataUri(mime_type=mime_part, base64_payload=payload) def materialize_attachment(block: dict[str, Any], bridge_dir: Path) -> Path | None: """ Decode a base64 data URI from a content block and write it to disk. :param block: A content block dict with ``type`` of ``"input_image"`` or ``"input_file"``. Expected to carry a resolved data URI in ``image_url`` or ``file_data``, e.g. ``"data:image/png;base64,iVBOR..."``. May also carry a ``filename``, e.g. ``"diagram.png"``. :param bridge_dir: Bridge directory path. Files are written to an ``uploads/`` subdirectory underneath it, e.g. ``Path("/tmp/omnigent/codex-native/")``. :returns: Path to the written file, or ``None`` if the block could not be materialized (missing data URI, decode error). """ data_uri = block.get("image_url") or block.get("file_data") if not isinstance(data_uri, str) or not data_uri.startswith("data:"): if block.get("file_id"): _logger.warning( "Native executor received unresolved file_id %s — " "content resolver may not have run", block["file_id"], ) return None try: parsed = parse_data_uri(data_uri) raw_bytes = base64.b64decode(parsed.base64_payload) except (ValueError, base64.binascii.Error): _logger.warning("Failed to decode data URI for attachment", exc_info=True) return None ext = MIME_TO_EXT.get(parsed.mime_type, "") filename = block.get("filename") if not filename: filename = f"attachment_{uuid.uuid4().hex[:8]}{ext}" else: filename = Path(filename).name or f"attachment_{uuid.uuid4().hex[:8]}{ext}" uploads_dir = bridge_dir / "uploads" uploads_dir.mkdir(parents=True, exist_ok=True) dest = uploads_dir / filename if dest.exists(): stem = dest.stem dest = uploads_dir / f"{stem}_{uuid.uuid4().hex[:6]}{dest.suffix}" dest.write_bytes(raw_bytes) return dest