Repository navigation
Host plan images in Chopin - #441
Merged
Merged
Conversation
Add an upload_image MCP tool that stores a PNG, JPEG, WebP or GIF of at most 1 MiB for a document under its SHA-256, an authenticated GET /images route that serves it only to readers of a document it belongs to, and dialect support for the same-origin /images/<sha256>.<ext> path.
Upload a real WebP through MCP upload_image, place it with update_document, and check that the signed-in author's browser renders it while a signed-out client, a signed-in user without access, and an unknown hash all receive 404. The fake GitHub gains score-reader- and score-outsider- handles for pull-only and no access to octo-org/score.
|
The preview deployment for chopin failed. 🔴 Open Build Logs | Open Application Logs Last updated at: 2026-10-09 22:09:19 CET |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A Planner or MCP client can now put a generated picture into a Chopin document. Until now the dialect accepted only absolute
https:images and nothing could upload one, so a picture drawn for a plan (for example by pdd-kit'splan_image) could only appear as alt text.What changes
upload_imageMCP tool:{ id, data (base64), mimeType }returns{ path: "/images/<sha256>.<ext>", markdown }.document-unavailable,repository-forbidden,document-archived,too-large,unsupported-type,signature-mismatch.GET /images/:file: serves the image only to a signed-in user with pull access to a document it was uploaded to.no-store404.content-type,nosniff,private, max-age=31536000, immutable, andcontent-security-policy: default-src 'none'; sandbox.018_plan_images(keyed(channel_id, sha256), deleted with its document), plus Postgres and in-memory stores that share one contract test./images/<64 hex>.<png|jpg|jpeg|webp|gif>as well as absolutehttps:URLs. Every other relative image is still rejected, and these images count toward the image limit. The editor keeps such images and lets you edit their alt text.End-to-end evidence (real run)
e2e/hosted-images.e2e.tsruns against the real built server, the e2e Postgres containers, and Chromium. It uses a real picture drawn bygpt-image-2.5-sunburst, 1536×1024 WebP, 50,812 bytes.The screenshot attached below is the document as the author sees it after
update_documentplaced the hosted path. In the same run the browser loaded the image with status 200 andnaturalWidth1536.HTTP trace from the run (written by the test; no tokens or cookies)
Hosted image requests
1. Create the document
POST /mcptools/call create_documentas author (push) (octo-org/score) -> HTTP 200{"id":"ceac76c4-d098-5e14-b771-fbffe19cd0ed","title":"Hosted images 3ab45d24","brief":{"goal":"Show the refund cap","constraints":[],"settledDecisions":[],"openQuestions":[],"repositoryFindings":[]},"source":"# Refund cap\n\nRefunds stop at the cap.\n","revision":0,"url":"/documents/octo-org/score/hosted-images-3ab45d24"}2. Upload the picture twice
POST /mcptools/call upload_imageas author (push) (50812 bytes declared image/webp) -> HTTP 200{"path":"/images/86f452e1abec59f59990fd05e2aa656c2e3b96a92dd6f10b3339925ddcb41fbd.webp","markdown":""}POST /mcptools/call upload_imageas author (push) (50812 bytes declared image/webp) -> HTTP 200{"path":"/images/86f452e1abec59f59990fd05e2aa656c2e3b96a92dd6f10b3339925ddcb41fbd.webp","markdown":""}3. Place the picture in the document
POST /mcptools/call update_documentas author (push) (revision 0) -> HTTP 200{"id":"ceac76c4-d098-5e14-b771-fbffe19cd0ed","title":"Hosted images 3ab45d24","brief":{"goal":"Show the refund cap","constraints":[],"settledDecisions":[],"openQuestions":[],"repositoryFindings":[]},"source":"# Refund cap\n\nRefunds stop at the cap.\n\n\n","revision":1,"url":"/documents/octo-org/score/hosted-images-3ab45d24"}4. Open the document in the browser
GET /images/86f452e1abec59f59990fd05e2aa656c2e3b96a92dd6f10b3339925ddcb41fbd.webpas the author's browser loading thecontent-type: image/webpcontent-length: 50812cache-control: private, max-age=31536000, immutablecontent-security-policy: default-src 'none'; sandboxcontent-disposition: inlinex-content-type-options: nosniff5. Fetch the picture with the author's session
GET /images/86f452e1abec59f59990fd05e2aa656c2e3b96a92dd6f10b3339925ddcb41fbd.webpas author (browser session) -> HTTP 200content-type: image/webpcontent-length: 50812cache-control: private, max-age=31536000, immutablecontent-security-policy: default-src 'none'; sandboxcontent-disposition: inlinex-content-type-options: nosniff6. Fetch the picture without read access
GET /images/86f452e1abec59f59990fd05e2aa656c2e3b96a92dd6f10b3339925ddcb41fbd.webpas a signed-out client (no cookie) -> HTTP 404content-type: text/plain; charset=utf-8content-length: 15cache-control: no-storex-content-type-options: nosniffGET /api/sessionas outsider (signed in, no access to octo-org/score) -> HTTP 200content-type: application/json; charset=utf-8content-length: 316cache-control: no-storex-content-type-options: nosniffGET /images/86f452e1abec59f59990fd05e2aa656c2e3b96a92dd6f10b3339925ddcb41fbd.webpas outsider (signed in, no access to octo-org/score) -> HTTP 404content-type: text/plain; charset=utf-8content-length: 15cache-control: no-storex-content-type-options: nosniffGET /images/0000000000000000000000000000000000000000000000000000000000000000.webpas author (browser session) -> HTTP 404content-type: text/plain; charset=utf-8content-length: 15cache-control: no-storex-content-type-options: nosniff7. Refused uploads
POST /mcptools/call upload_imageas author (push) (71 bytes declared image/svg+xml) -> HTTP 200{"code":"unsupported-type"}POST /mcptools/call upload_imageas author (push) (50812 bytes declared image/png) -> HTTP 200{"code":"signature-mismatch"}POST /mcptools/call upload_imageas reader (pull, no push) (50812 bytes declared image/webp) -> HTTP 200{"code":"repository-forbidden"}The full Playwright
trace.zip(663 KB) from this run was not attached:gh --attachtakes only images and video, and the trace contained the run's throwaway session cookies. It was deleted with the run's scratch files after merge; re-running the command above with--trace onproduces a fresh one.Verification
bun teston the MCP, storage, image route, dialect, editor URL, router and skills tests: 334 passed, 0 failed.TEST_DATABASE_URL=… bun test apps/server/src/storage/postgres) against a throwaway database: 80 passed.bun test: 4424 passed, 4 skipped, 1 failed. The failure isapps/connector/src/workspace.test.ts, which also fails onmainon this machine (global git signing config) and passes withGIT_CONFIG_GLOBAL=/dev/null.document-creationandmcp-update: 18 passed, 0 failed.bun run typesexits 0;bun run ci(dprint, oxlint, token and design checks) passes. oxlint findings are the same as onmain.Notes for review
score-reader-(pull without push) andscore-outsider-(can't seeocto-org/score) handle prefixes for the e2e. Every other handle behaves as before./images/:filematched by pattern, because the router matches whole segments.https:-only, and the hosted Planner's prompt still says images must be absolutehttps:URLs (the Planner's tools were out of scope).Assistant-model: Claude Opus 5.5 (fast)
Assistant-workflow: inline (implementation and e2e delegated to worker subagents on anthropic-api/claude-opus-5-5-fast)
Assistant-duration: 47m converged, estimated 45-90m
Assistant-verification: playwright e2e passed: bun run e2e -- e2e/hosted-images.e2e.ts (upload, idempotency, placement, browser render naturalWidth 1536, reader 200 with headers and identical bytes, signed-out/outsider/unknown 404, three refusals)
Assistant-verification: bun test passed: touched areas 334 pass; Postgres storage 80 pass
Assistant-verification: types and ci passed: bun run types; bun run ci
User-preference: Host plan images in Chopin, visible only to readers of the image's document, stored in Postgres, admin-merged
User-preference: Prove image upload with a real end-to-end scenario and attach its trace to the PR
User-preference: Use fast subagents for everything