Skip to content

feat(uploads): prompt uploads take documents - #621

Merged
Ark0N merged 1 commit into
Ark0N:masterfrom
JDProfresh:feat/prompt-upload-documents
Oct 16, 2026
Merged

Ark0N merged 1 commit into
Ark0N:masterfrom
JDProfresh:feat/prompt-upload-documents

Conversation

@JDProfresh

Copy link
Copy Markdown
Contributor

A user can hand the agent an image or a video but not a document: a PDF to read, a spreadsheet to summarise, a log to scan. This widens the same paste-image route to take them, the last of the three parts from #553, now that the Attach and video part is in (#606).

What it does

The Attach button, the Compose and paste-dialog pickers, terminal drop and the clipboard now take documents beside images and videos: the office formats pdf, docx, pptx, xlsx and zip, and the text family txt, md, csv, tsv, json, log, yaml, yml and xml. The Compose and paste dialogs' picker button reads 📎 Attach instead of 🖼 Image, since it now takes more than images.

A document is admitted by its name's extension, not its Content-Type (the MIME a phone attaches to a .md or .csv is anyone's guess). It is buffered under the same 50 MB image cap and judged on its bytes by the same fail-closed sniffer: a magic-number signature for the binary formats (%PDF-, PK\x03\x04 for the OOXML trio and zip), and valid UTF-8 with no NUL byte for text. It is saved under a sanitized stem of its name, so Q3 report.pdf is findable as paste-<ts>-<hex>-Q3-report.pdf.

From your #553 notes

  • Refusing sh/py/js is worded as a scope choice, not a security property, in the code comments and the docs: file-raw already serves non-media text inert, so this only keeps the server from storing what it would serve or an agent could run. html and svg are the ones that really stay out. A named type that is neither an image nor a document is answered 400 Unsupported file type: .sh, which the agent and the user can read, where master answers a .sh with a 415 about .png (an unknown extension falls back to .png and then fails the PNG sniff).
  • paste-image is described in docs/api-reference.md, extending the section feat(uploads): write prompt uploads to a hidden, self-ignoring .codeman-uploads/ folder #574 and feat(mobile): phone Attach button and camera-roll video uploads #606 added, with the document types, the size cap, the by-name admission and the scope refusal.

Design notes

  • Dispatch mirrors the client's _promptAttachKind, which now returns image, video or document: an image/* MIME takes the image path whatever the name says (a GIF named animation.txt stays a GIF), a document extension takes the document branch, and a foreign extension with no image MIME is refused only after the image sniff fails, so nothing the image path ever accepted by its bytes (a HEIC under a generic name) is lost.
  • The document branch buffers the whole body before any refusal, so none of its early answers can leave the browser on "Uploading" the way the video refusals did before your feat(mobile): phone Attach button and camera-roll video uploads #606 merge fix.
  • The text-mismatch log names the failure (nul-byte or not-utf8) and never prints the bytes, since the head of a text file is user content.
  • The curated list is its own ALLOWED_DOCUMENT_EXTS, never SUPPORTED_ATTACHMENT_EXTENSIONS or EDITABLE_EXTENSIONS (those carry sh/py/js/html): a prompt upload is a thing to read, not a thing to run or serve.
  • zh-CN: the refusal toast now reuses the existing Unsupported file type key (the tile grid's), the old Only image or video files are supported key goes with its last use, and the one new key is 📎 Attach for the dialog button. I checked it against the existing Attach key after your feat(mobile): phone Attach button and camera-roll video uploads #606 fix; it is a different string and overrides nothing.

Tests

Guard tests, each shown to fail with its guard removed: the document bytes-vs-extension check (force it to accept and the mismatch test fails), and the by-name Unsupported file type 400 (remove it and the script/HTML refusal test fails). Plus: a PDF, an OOXML document and a text file accepted with the name kept; the sanitized-stem rule; the sh/py/html refusal; a mismatch per type (PNG bytes under a PDF name, a NUL byte in text, non-UTF-8 in a .csv); the log naming the reason without the bytes; and the classifier and pickers in toolbar-attach.test.ts and mobile-prompt-composer.test.ts.

Verified

Observed on this branch, rebased onto today's master (1.41.1, a8a8bb71):

  • The full npm test gate passes: 11,266 passed, 12 skipped, 0 failed. Static checks and prettier on the touched tests pass.
  • On a throwaway instance built from this branch (removed after): Q3 report.pdf, a docx, a zip, a .md, a .csv and a .tsv were accepted into <ws>/.codeman-uploads/ with their names kept; run.sh and page.html answered 400 Unsupported file type; a .txt with a NUL byte, a Latin-1 .csv and PNG bytes named .pdf answered 415 and wrote nothing. A PNG still lands as before, and an mkv on ?kind=video is still refused at once. A headless iPhone-profile run through the real Attach picker put the PDF and CSV paths on the prompt as unsent text, and picking run.sh showed the Unsupported file type toast without uploading anything.

On my iPhone, a PDF picked from the Files app through Attach landed on the prompt and Claude read it. Earlier builds of this have been running on my own installs since 2026-10-07.

Link pattern

You flagged on #553 that some upload extensions were not in FILE_PATH_LINK_PATTERN. m4v and ogv came in with #606; this adds the last two the uploads produce, zip and tsv, to both the rooted and the relative pattern (the parity test #604 added pins that they match), with a positive case in link-provider-regex.test.ts.

The README's upload line (both languages) now says documents too. No changeset is included, matching #574.

The Attach button, the Compose and paste-dialog pickers, terminal drop and the
clipboard now take documents beside images and videos: the office formats
pdf/docx/pptx/xlsx/zip and the text family txt/md/csv/tsv/json/log/yaml/yml/xml.

A document is admitted by its name's extension (the MIME a phone attaches to a
.md is anyone's guess), buffered under the same 50MB image cap, and judged on
its bytes by the same fail-closed sniffer: a magic-number signature for the
binary formats, UTF-8 with no NUL byte for text. It is saved under a sanitized
stem of its name, so Q3 report.pdf is findable as paste-<ts>-<hex>-Q3-report.pdf.

sh/py/js/html are refused by name with a clear 400. That is a scope choice, not
a security property: file-raw already serves non-media text inert, so this keeps
the server from storing what it would serve or an agent could run, and html and
svg are the ones deliberately kept out. One client classifier (_promptAttachKind)
now returns image, video or document, and the server's dispatch mirrors it: an
image MIME wins whatever the name says, a document extension takes the document
branch, and a foreign extension with no image MIME is refused only after the
image sniff fails, so nothing the image path accepted by its bytes is lost.
@Ark0N
Ark0N merged commit 244d9a4 into Ark0N:master Oct 16, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Oct 16, 2026
The wiki said .codeman-uploads/ is the one path "a local agent, a container
and a remote host all see the same way", two sentences after saying uploads
are refused for remote (SSH) sessions. Only a local agent and a Docker case
(workspace bind-mounted at the same absolute path) share it, which is why the
route refuses remote sessions; say that.

The same paragraph said every image goes in after a re-encode on the device,
but _normalizeImageForUpload passes a GIF through untouched to keep its
frames.

CLAUDE.md and a test comment described the image fallback as behaving
"as before 1.36", a release that never existed (1.35.0 was followed by
1.40.0). It means the image path as it was before this change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 16, 2026
feat(uploads): prompt uploads take documents

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 16, 2026
fix(uploads): refuse an unsupported video before uploading it

Conflict resolved against #621 in src/web/public/image-input.js (the
_promptAttachKind doc comment only): #621's comment is kept with #622's
clause that a video MIME counts only for the containers the server
admits; #622's video MIME check and #621's document branch both stay.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 16, 2026
One release changeset for #616, #617, #621, #622, #623, #624, #625, #628, #632, #634, #635 and #642. The changesets carried by #616, #617 and #642 are folded into it and removed (#617's was minor; 1.41.2 is a patch release).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Ark0N

Ark0N commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Merged and shipped in 1.42.0. Thanks @JDProfresh!

The fail-closed byte checks (a signature for office files, UTF-8 with no NUL byte for text) and the log line that never prints a text file's contents are exactly right for this route. Thanks also for working through every point of the #553 decision, down to the api-reference entry and the zip/tsv link patterns. I tested it live: a PDF, a CSV, a markdown file with no MIME and a docx went through, and a renamed binary pretending to be a PDF was refused.

Three doc statements were corrected at merge time: only a local agent and a Docker case share .codeman-uploads/ (remote sessions are refused), GIFs keep their frames instead of being re-encoded, and a note no longer mentions a 1.36 release that never existed.

Follow-ups we'd welcome, none blocking: a clearer "not UTF-8 text" refusal (or UTF-16 with a BOM) for Excel and Notepad CSV/TXT exports, which today read like a corrupt file, and a parity test pinning the five copies of the document list against ALLOWED_DOCUMENT_EXTS. I'm closing #553 by hand since the PR body has no Closes line.

@JDProfresh
JDProfresh deleted the feat/prompt-upload-documents branch October 16, 2026 15:01
Ark0N pushed a commit that referenced this pull request Oct 18, 2026
Master's #621 added tsv and zip to both file-link extension groups in
constants.js while this branch added diff|patch to the same lines. The
resolution keeps both, in master's order with diff|patch after sql where the
PR put it; the rooted and relative groups stay identical, and every upload
document extension plus .diff/.patch still links in both patterns.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants