feat: serve attachment content via GET /api/v1/attachments/{hash}#463
Conversation
roborev: Combined Review (
|
|
Addressed the Medium finding in 5bdb2fc: |
roborev: Combined Review (
|
|
looking |
5bdb2fc to
70a9a13
Compare
roborev: Combined Review (
|
|
I will rebase this once #464 is merged and then I can get this ready to merge |
Add an authenticated endpoint that streams stored attachments by SHA-256 hash with their own content type and download filename. This gives networked clients access to attachment bytes that were previously available only to co-located CLI/MCP paths. The endpoint validates content-addressed paths, returns not found for missing metadata or files, and supports SQLite, Postgres, DuckDB, mocks, and the remote engine compatibility boundary. Squashed follow-up: - feat: expose attachment content_hash in message detail responses Generated with Codex Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
70a9a13 to
4e31bef
Compare
roborev: Combined Review (
|
The public download endpoint still derived loose-file paths after packed attachment storage became authoritative, so sealed blobs were unreachable even though every other attachment reader could open them. Route content reads through the shared blob store while retaining the existing fallback for embedded servers. Attachment metadata lookup also bypassed dialect rebinding, which left SQLite placeholders in PostgreSQL queries. Use the engine query helper so both backends execute the same lookup contract. VALID (fixed): #1, #2 Generated with Codex Co-authored-by: Codex <noreply@openai.com>
Legacy imports may record a valid local blob beneath a namespaced path rather than the canonical hash shard. Packed reads remain authoritative, but a not-found result must fall back to that recorded location until maintenance canonicalizes it. Resolve the recorded path through the attachments root, reject traversal, absolute paths, URLs, and symlink escapes, and expose no storage-path detail in API responses. VALID (fixed): #2 legacy namespaced-path case Generated with Codex Co-authored-by: Codex <noreply@openai.com>
The packed-attachment endpoint regression test adopts PostgreSQL in CI through NewTestStore, so hard-coding the SQLite query dialect leaves placeholders unrebound and makes the test fail before it can exercise the endpoint. Use the backend-aware engine factory so the same coverage validates both supported stores.\n\nValidation: reproduced the failing job against PostgreSQL 16 and reran the focused endpoint test successfully.\n\nGenerated with Codex\nCo-authored-by: Codex <noreply@openai.com>
roborev: Combined Review (
|
roborev: Combined Review (
|
Attachment downloads can carry arbitrary MIME types and non-JSON bytes, but describing the success response as application/octet-stream makes the client generator select its file-upload type and JSON decoder. Describe the binary response with the wildcard media type the endpoint actually serves so regeneration selects its raw-byte response path. VALID (fixed): #1 Validation: reproduced the decode failure with non-JSON binary content, then verified the generated convenience client returns the exact bytes. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
roborev: Combined Review (
|
A content hash can belong to attachment rows with different metadata and legacy storage paths. Selecting one arbitrary row can hide a valid loose copy behind a stale or non-local path and can send headers unrelated to the bytes that were opened. Preserve stable row order for packed and canonical content, while trying every distinct recorded path and selecting metadata from the row whose loose content succeeds. VALID (fixed): #1 Validation: reproduced the failure with duplicate rows whose first path was missing, then served the later namespaced path with matching headers on SQLite and PostgreSQL 16. Generated with Codex Co-authored-by: Codex <noreply@openai.com>
roborev: Combined Review (
|
What
Adds
GET /api/v1/attachments/{hash}— an authenticated endpoint that streams a stored attachment's raw bytes, addressed by its SHA-256 content hash, with the attachment's ownContent-Typeand a downloadContent-Dispositionfilename.Why
The HTTP API had no way to retrieve file-attachment content.
/messages/{id}/inlineserves only inline images (it requires acidand animage/*content type), and attachment bytes were otherwise reachable only from the co-located CLI/MCP paths that read the on-disk store directly. A networked client — a remote TUI, or any integration without filesystem access to the attachments directory — had no route to fetch them.Usage
200with the file bytes andContent-Type/Content-Disposition/Content-Lengthset.400for a malformed hash (not 64 hex chars; also guards path traversal).404when the attachment metadata row or its on-disk file is absent (e.g. an account removed with its attachments, or an archive built with--no-attachments).Auth is inherited from the existing
/api/v1API-key middleware.content_hashis not unique (content-addressed dedup), so any matching row supplies the header metadata.