Second-pass findings on the security fix:
- Mime sanitized ONCE and passed to BOTH StageInputFile and the descriptor (was
passing raw f.MimeType to the host store while only the descriptor sanitized) —
3 models.
- sanitizeField now also strips Unicode format chars (category Cf, incl. the bidi
overrides U+202A–U+202E that can reorder how the descriptor renders); IsControl
already covers \n\r\t so the explicit checks are dropped.
- fileID is sanitized before inlining + an empty file_id drops the file (defense
vs a misbehaving stager).
- humanizeBytes clamps the prefix index so an absurd size (≥1024^6) can't index
past "KMGTPE" and panic — a no-panic guarantee independent of the per-file cap.
- Docs sync: README Ports list gains InputFiles; tool.InputFile.Name doc now says
the executor reduces an untrusted name to a safe base name (was claiming the
field is already safe).
Tests: bidi/control stripping; mime sanitized in staged value + descriptor; empty
file_id drop; humanizeBytes no-panic across sizes up to 1<<62. Suite green (-race).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The full swarm (5-6 models) flagged that stageInputFiles passed the untrusted
attachment filename straight to StageInputFile and inlined it into the
[ATTACHED FILES]/`/workspace/<name>` descriptor with no sanitization — a path
the byte-cap already treats as a trust boundary. A name like ../../etc/passwd or
an absolute/drive path could escape the host store or the sandbox workspace, and
newlines in the name/mime could inject text into the prompt block.
- sanitizeName: strips control chars/newlines, then reduces to a base name
(path.Base after backslash-normalization) so ../, nested dirs, and absolute /
drive paths all collapse to their last element; "attachment" fallback for
empty/"."/"..". Applied BEFORE staging AND inlining.
- sanitizeField: strips control chars from MimeType (also inlined verbatim).
- maxInputFiles (32) count cap — defense-in-depth vs a flood of tiny files,
independent of the per-file byte cap.
Tests: sanitizeName table (traversal/absolute/backslash/control/fallback, +
no-separator invariant); traversal staged+described under the base name only;
oversize skip; count-cap truncation. Full suite green (-race).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
executus's tool.Invocation already carried InputFiles (audio/PDF/binary), but the
executor never staged them — only Images were folded into the run. This adds the
host seam mort's chat/chatbot surfaces need for audio-input parity with agentexec.
- run.Ports gains InputFiles InputFileStager (nil-safe; nil = input files silently
ignored, run still proceeds text-only). The interface mirrors mort's skill
FileStorage: StageInputFile(ctx, runID, agentID, name, mime, content) → file_id.
- run/input_files.go (ported from mort agentexec/input_files.go): stageInputFiles
persists each file under run scope and appends an [ATTACHED FILES] descriptor
block to the prompt so the agent can reach them by file_id (e.g. code_exec
files_in → /workspace/<name>). Bytes are NEVER inlined into model context.
Best-effort: empty/oversized(>50MB)/save-error files are skipped; colliding
base names are disambiguated (name-2, name-3) so they don't clobber at
/workspace/<name>.
- Executor.Run calls it after the model/toolbox build, before the loop, so the
descriptor rides the first user turn (alongside the existing Images folding).
Tests: stages + builds the block; nil stager / no files leave the prompt intact;
dedup; empty/save-error skipping. Full suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>