From 419b41ebf097a9b8ab68f7725f2d75eb0d71db1e Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Fri, 9 Oct 2026 20:38:33 -0400 Subject: [PATCH 1/2] Inbox: images in agent messages load (fixes #1813) A message body's ![alt](path) and now load through a per-message route, GET /api/inbox/messages/:id/image?path=, instead of the /api/image the Inbox never served. The path must be an image the message's own body references, inside the message's project (realpath, so ../ and symlink escapes are refused), an image by extension and by magic bytes, at most 10 MB. Answers carry nosniff, a sandbox CSP and a same-origin CORP; cross-site requests are refused. --- AGENTS.md | 7 + .../src/content/docs/reference/inbox.md | 1 + packages/core/inbox-types.ts | 7 + packages/inbox/App.tsx | 3 + packages/inbox/components/Thread.tsx | 2 + packages/inbox/images.test.tsx | 76 +++++++ packages/inbox/images.ts | 43 ++++ packages/server/inbox-mcp.ts | 20 +- packages/server/inbox-message-images.test.ts | 191 ++++++++++++++++++ packages/server/inbox-message-images.ts | 66 ++++++ packages/server/inbox.ts | 6 + packages/shared/inbox/message-images.ts | 179 ++++++++++++++++ packages/shared/inbox/store.ts | 3 + packages/shared/package.json | 1 + tests/e2e/inbox-message-images.spec.ts | 80 ++++++++ tests/e2e/playwright.config.ts | 4 +- 16 files changed, 686 insertions(+), 3 deletions(-) create mode 100644 packages/inbox/images.test.tsx create mode 100644 packages/inbox/images.ts create mode 100644 packages/server/inbox-message-images.test.ts create mode 100644 packages/server/inbox-message-images.ts create mode 100644 packages/shared/inbox/message-images.ts create mode 100644 tests/e2e/inbox-message-images.spec.ts diff --git a/AGENTS.md b/AGENTS.md index 65faf674c4..6c48aa5625 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1975,6 +1975,12 @@ Sorting and the rest: - Comments take no images. - Delete thread rewrites the project's files without the thread's lines. Delete project removes its folder. Both then remove every blob that no remaining record uses. +**Images in a message** (#1813; `packages/shared/inbox/message-images.ts`, `packages/server/inbox-message-images.ts`, `packages/inbox/images.ts`). +- A message body's `![alt](path)` and `` load through `GET /api/inbox/messages/:id/image?path=`. The window draws each body with `imageBaseDir` = `inbox-message:` and installs `inboxImageSrcResolver`, which maps that marker to the route. There is no route that takes a bare path. +- The rules, in order: the path must appear in THAT message's body as an image reference (`inboxMessageImageRefs`: the markdown target as written, the `` value decoded), so the route cannot probe other project files; an image extension (png jpg jpeg gif webp svg avif bmp ico apng) before anything is read, else `415`; a relative path resolves against the message's `base_path` (the realpath of the agent's `project_path` when it is a subfolder of the project, recorded at send time, additive) else the project root; the resolved path AND its realpath must sit inside the project root, so `../` and symlink escapes are `403`; a regular file of at most 10 MB and 50 megapixels (`413`), its type sniffed from magic bytes (`sniffImageContentType`, `415` when not an image). `?query` / `#fragment` are dropped and `%xx` decoded as fallbacks. +- Every answer carries `X-Content-Type-Options: nosniff`, `Content-Security-Policy: sandbox; default-src 'none'; style-src 'unsafe-inline'` and `Cross-Origin-Resource-Policy: same-origin`, no CORS; `Sec-Fetch-Site: cross-site` / `same-site` is refused (`403`). +- Remote `http(s)` images stay as written and do not load: the window's CSP is `img-src 'self' data: blob:`. Images inside an attached markdown file are not served (only message bodies). The iPhone draws message bodies natively and shows no images; the device door has no image route. + **Decisions** (`packages/server/inbox-decisions.ts`; records in `decisions.jsonl`, `InboxDecision` in `inbox-types.ts`). - Fields: `text`, `reason`, `source` `{ kind: answer | agent | person, … }`, `state` `current | replaced | retired`, `version` (a stale one is `409 decision_version_conflict`), `replaces_id`, `replacement_id`, and the dates. - The switch: @@ -2320,6 +2326,7 @@ Not covered, by design: the live-app proxy (`live-proxy-core.ts` has its own Hos | `/mcp` | POST | MCP, stateless; any Origin refused | | `/api/inbox/threads/:id/attachments` | GET | `{ serverSession, attachments: [state], annotations }`: each attachment record with `message_id`, `current: { sha256, size, mtime } \| null`, `changed_since_sent`, `unavailable: { code, message } \| null`, and the thread's annotations waiting for a Send | | `/api/inbox/attachments/:id` | GET | The current file's bytes by id (`?version=sent`: the sent blob), always `text/plain`, CSP `sandbox`. `404 attachment_not_found` / `attachment_missing`, `409 attachment_changed_type`. No route takes a path | +| `/api/inbox/messages/:id/image` | GET | `?path=`: an image the message's body shows, read from its project (see "Images in a message"); sniffed `Content-Type`, nosniff, sandbox CSP, same-origin CORP. `403 image_not_referenced` / `outside_project` / `cross_origin`, `404 message_not_found` / `image_missing`, `413 image_too_large`, `415 not_an_image` | | `/api/inbox/attachments/:id/view` | GET | `{ attachment, version, text, html }`: one version's text (`version` is `current` or the sent sha256, the annotations' key); for HTML, `html` is the page with its `` at its folder's asset route | | `/api/html-assets//` | GET | Annotate's asset route (`resolveHtmlAssetRoute`) for the folder of an attached HTML page | | `/api/inbox/annotations` | POST | `{ serverSession?, attachment_id, version, annotation }`: save a new annotation or edit one still waiting (`409 annotation_closed` once sent or removed) | diff --git a/apps/marketing/src/content/docs/reference/inbox.md b/apps/marketing/src/content/docs/reference/inbox.md index 6799f0a2bd..c86e070c27 100644 --- a/apps/marketing/src/content/docs/reference/inbox.md +++ b/apps/marketing/src/content/docs/reference/inbox.md @@ -38,6 +38,7 @@ You never need to keep the Inbox running yourself. When an agent calls it and it - **A thread.** It reads like email. Questions render as cards, and a click saves your pick at once. **Send** carries your picks, your words and your annotations as one reply. **Mod+Enter** sends. After a Send the reply reads "Saved for " until the agent reads it, then "Delivered to ,