Web chat: image/file uploads from the composer, reusing the existing files service (reference-not-payload); ordering depends on #19 then #18 #34

Open
opened 2026-11-01 14:49:46 +00:00 by rosa · 0 comments
Member

Felix asked (2026-11-01 12:41Z) for image/file uploads from the web chat, "using our internal
file sharing/storage system". Filed as the human-side twin of #18. The good news, verified from
source at main 8d5964ba7e6: the storage half is already built. This is a wiring job.

What already exists (read today from services/files/)

  • Upload endpoint is implemented. POST /files takes an UploadFile, enforces a
    100 MB cap ("file too large (100 MB cap in v1)"), validates the name against
    paths.SEGMENT_RE, defaults the target to the caller's home /home/<username>, and refuses
    writes outside the caller's subtree (_can_create: own home, or a non-personal tree whose top
    folder is theirs or unclaimed; /home/* is a reserved personal namespace).
  • The reference object I was going to propose already exists. fpub() returns
    {id, name, size, content_type, owner, shared_with, created_at, updated_at} — that is the
    "file reference, not payload" shape, already the service's public form.
  • Bytes are out of Mongo already. storage.py: S3 backend in prod (config from
    TG_FILES_S3_*, R41 user-provided host), in-memory dict under TG_DEV=1; Mongo holds
    metadata only
    . So the retention/cost question is already delegated to an object store, and
    it is a different bucket from the cm8b corpus bucket — no risk of colliding with ops#20.
  • Download without moving bytes through chat: presign_get(key, expires_s=900) returns a
    time-boxed URL (dev mode deliberately returns a non-URL tg-memory:// sentinel).
  • Sharing/visibility is modeled (shares_col with path_prefix, folders first-class,
    listing = own-scope R40 where superiors are not implicitly included).
  • services/web/imports/ui/FilesPane.jsx exists, and there is a deprecated flat listing kept
    "for old web clients until they browse by path" — so the web tier already speaks to this API.

The actual gap

One hop: the chat composer has no path into this. Per #18, services/chat/app/routers/messages.py
treats the body as a plain str field (20k cap) with no image content type — so an upload has
nowhere to ride. (That line reference is #18's, filed 10-26; I re-read the files service today,
not the chat router, so treat the message-schema claim as #18's record.)

Proposed shape, as a ladder — each rung ships alone

  1. Composer attaches by calling the existing POST /files. No new storage tier, no new
    bucket, no second permission model. Web forwards with the same caller-derived auth the service
    already requires (CLAIM_CALLER is explicitly ignored — identity is derived, never claimed, so
    a browser cannot upload into someone else's home).
  2. Messages carry the reference, not the bytes — an object shaped like fpub() (id/name/size/
    content_type) on the message doc. Keeps #6's length rule intact and prevents the 20k cap being
    reinterpreted as a place to shove base64. presign_get serves the bytes on demand.
  3. Preview + download in the chat pane. Images inline, everything else a card with name/size
    linking into the existing FilesPane view.
  4. Deliver the reference to agents as a workspace path — the payoff rung. mail.save_attachment
    already lands bytes in an agent workspace; one mechanism, two front doors. That is what makes
    "here is the screenshot" actionable for me instead of decorative.
  5. Then #18/#19 for the model edge (content-parts in the runtime wake).

Ordering constraint, load-bearing rather than cosmetic

#19 before #18, and #18 no later than rung 5. #19 is measured, not theoretical: gateways that
never send architecture.input_modalities fall back to [text] silently — I found 130/130
opencode rows stored text-only while models.dev carries real modality data for 87 of them
(comment 2102). Its warning is precisely this feature: once real multipart image traffic exists,
misclassified models produce all-candidates-failed proxy errors that look like provider outages
but are catalog defects. Uploads are harmless; image traffic against a text-only catalog is the
misleading-failure mode. Fix classification first.

Decisions I am deliberately not making (Felix's, per my role)

  • Size cap for chat specifically. The service says 100 MB in v1; a phone photo is fine, a video
    is not. Whether chat inherits the service ceiling or sets a lower one is a product call.
  • Where chat files live. The service defaults to the caller's /home/<username> and supports
    path= + shares. Options: per-conversation folder under a shared tree, or the sender's home with
    an explicit share. The mechanism exists either way; the ownership rule does not.
  • Retention/quota. Bytes already sit in S3, so cost is real and proportional. This deployment
    just learned that lesson from the cm8b buckets (ops#20). A chat that accepts files needs a TTL.
  • Is an uploaded file trusted input? Anything a model reads can carry instructions; the
    prompt-injection bar should be stated before rung 5, not discovered after.
  • MIME policy. The service stores content_type but I saw no allow/deny list in the upload
    path. Whether chat restricts to images/documents is a policy choice to make deliberately.

Cross-refs

#18 (image inputs — model-side twin, unassigned, blocked on #19) · #19 (modality classification —
prerequisite) · #14 (chat UX umbrella, where attachments was one bullet; this expands it) ·
#7 (mobile — a file card must survive a narrow screen) · #6 (message length — reference-not-payload
preserves it) · ops#20 (bucket economics, the retention precedent).

— rosa (filing, not deciding)

Felix asked (2026-11-01 12:41Z) for image/file uploads from the web chat, "using our internal file sharing/storage system". Filed as the human-side twin of #18. The good news, verified from source at main `8d5964ba7e6`: **the storage half is already built.** This is a wiring job. ## What already exists (read today from `services/files/`) - **Upload endpoint is implemented.** `POST /files` takes an `UploadFile`, enforces a **100 MB cap** ("file too large (100 MB cap in v1)"), validates the name against `paths.SEGMENT_RE`, defaults the target to the caller's home `/home/<username>`, and refuses writes outside the caller's subtree (`_can_create`: own home, or a non-personal tree whose top folder is theirs or unclaimed; `/home/*` is a reserved personal namespace). - **The reference object I was going to propose already exists.** `fpub()` returns `{id, name, size, content_type, owner, shared_with, created_at, updated_at}` — that is the "file reference, not payload" shape, already the service's public form. - **Bytes are out of Mongo already.** `storage.py`: S3 backend in prod (config from `TG_FILES_S3_*`, R41 user-provided host), in-memory dict under `TG_DEV=1`; **Mongo holds metadata only**. So the retention/cost question is already delegated to an object store, and it is a *different* bucket from the cm8b corpus bucket — no risk of colliding with ops#20. - **Download without moving bytes through chat:** `presign_get(key, expires_s=900)` returns a time-boxed URL (dev mode deliberately returns a non-URL `tg-memory://` sentinel). - **Sharing/visibility is modeled** (`shares_col` with `path_prefix`, folders first-class, listing = own-scope R40 where superiors are *not* implicitly included). - `services/web/imports/ui/FilesPane.jsx` exists, and there is a deprecated flat listing kept "for old web clients until they browse by path" — so the web tier already speaks to this API. ## The actual gap One hop: the **chat composer** has no path into this. Per #18, `services/chat/app/routers/messages.py` treats the body as a plain `str` field (20k cap) with no image content type — so an upload has nowhere to ride. (That line reference is #18's, filed 10-26; I re-read the files service today, not the chat router, so treat the message-schema claim as #18's record.) ## Proposed shape, as a ladder — each rung ships alone 1. **Composer attaches by calling the existing `POST /files`.** No new storage tier, no new bucket, no second permission model. Web forwards with the same caller-derived auth the service already requires (`CLAIM_CALLER` is explicitly ignored — identity is derived, never claimed, so a browser cannot upload into someone else's home). 2. **Messages carry the reference, not the bytes** — an object shaped like `fpub()` (id/name/size/ content_type) on the message doc. Keeps #6's length rule intact and prevents the 20k cap being reinterpreted as a place to shove base64. `presign_get` serves the bytes on demand. 3. **Preview + download in the chat pane.** Images inline, everything else a card with name/size linking into the existing FilesPane view. 4. **Deliver the reference to agents as a workspace path** — the payoff rung. `mail.save_attachment` already lands bytes in an agent workspace; one mechanism, two front doors. That is what makes "here is the screenshot" actionable for me instead of decorative. 5. **Then #18/#19 for the model edge** (content-parts in the runtime wake). ## Ordering constraint, load-bearing rather than cosmetic **#19 before #18, and #18 no later than rung 5.** #19 is measured, not theoretical: gateways that never send `architecture.input_modalities` fall back to `[text]` silently — I found **130/130** opencode rows stored text-only while models.dev carries real modality data for 87 of them (comment 2102). Its warning is precisely this feature: once real multipart image traffic exists, misclassified models produce all-candidates-failed proxy errors that *look like provider outages* but are catalog defects. Uploads are harmless; image traffic against a text-only catalog is the misleading-failure mode. Fix classification first. ## Decisions I am deliberately not making (Felix's, per my role) - **Size cap for chat specifically.** The service says 100 MB in v1; a phone photo is fine, a video is not. Whether chat inherits the service ceiling or sets a lower one is a product call. - **Where chat files live.** The service defaults to the caller's `/home/<username>` and supports `path=` + shares. Options: per-conversation folder under a shared tree, or the sender's home with an explicit share. The mechanism exists either way; the ownership rule does not. - **Retention/quota.** Bytes already sit in S3, so cost is real and proportional. This deployment just learned that lesson from the cm8b buckets (ops#20). A chat that accepts files needs a TTL. - **Is an uploaded file trusted input?** Anything a model reads can carry instructions; the prompt-injection bar should be stated *before* rung 5, not discovered after. - **MIME policy.** The service stores `content_type` but I saw no allow/deny list in the upload path. Whether chat restricts to images/documents is a policy choice to make deliberately. ## Cross-refs #18 (image inputs — model-side twin, unassigned, blocked on #19) · #19 (modality classification — prerequisite) · #14 (chat UX umbrella, where attachments was one bullet; this expands it) · #7 (mobile — a file card must survive a narrow screen) · #6 (message length — reference-not-payload preserves it) · ops#20 (bucket economics, the retention precedent). — rosa (filing, not deciding)
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: NSTERN/TreeGent#34
No description provided.