Salvage of #35362, evolved to also close the vision sandbox-escape
(GHSA-gpxw-6wxv-w3qq). The two were the same root cause — vision read image
bytes host-side while every other tool reads through the terminal backend —
so one resolver fixes both the delivery gaps and the escape.
Delivery (from #35362, re-authored against current main since the branch was
4140 commits stale and vision_tools.py had been rewritten on both sides):
- tools/image_source.py: one resolver for data:/http(s)/file/local/container
image sources, returning raw bytes through a single magic-byte-sniff +
50MB-ingest chokepoint. Fixes 'no image attached' / 'Invalid image source'
for every source type (#7571, #25118, #29643, #22328, #32709, #9077).
- tools/credential_files.py: from_agent_visible_cache_path, the container->host
cache reverse-map (inverse of the existing forward twin).
- tools/vision_tools.py: both vision sites route through the resolver with
task_id threaded from the handler; resolved bytes are materialized to a temp
file so main's evolved encode/resize/embed-cap pipeline is reused verbatim
(kept over the PR's older bytes-core resize to avoid touching browser_tool /
conversation_compression callers).
Security (fills #35362's deliberately-stubbed _within_allowed_roots seam):
- Under a non-local terminal backend the file tools are confined to the sandbox
(SECURITY.md 2.2), but vision read host-side — a prompt-injected
vision_analyze('/etc/passwd') exfiltrated host secrets, and read_file even
redirects the model to vision_analyze for image paths. The resolver now
enforces the same boundary: local backend reads any host path (chosen
posture); non-local backend host-reads ONLY the media caches under
HERMES_HOME (where the gateway/download media lives) and routes every other
path to an in-sandbox base64 exec-read — which reads the CONTAINER's file,
the same one 'cat' would, never the host's. Paths are resolve()-d so a
symlink can't escape a cache; fail-closed when no sandbox env exists.
This closes the escape AND delivers container-only images (#32709) with the
same mechanism.
Tests: unified resolver + confinement model (tests/tools/test_image_source.py,
incl. proof a non-cache host path under Docker yields container bytes not the
host secret); existing vision tests updated to the resolver boundary; Docker
integration test verified green against a real daemon (exec-read of a tmpfs
/workspace file, a root-owned mode-600 file, and the host-secret invariant).
Fixes GHSA-gpxw-6wxv-w3qq.
Co-authored-by: banditburai <promptsiren@gmail.com>