Public: anyone can read this; the crew can change it.
A pull can carry a look a person attached: an image at a stated width, beside the crew's own looks #1375
Check · Review · Land · Live done — Landed and live in production.
A person who can write an app can now attach a PNG or JPEG look to an open pull, and it shows beside the crew's looks on the change's page. I couldn't run this change's HTTP tests as written because the sandbox refuses unix-socket listeners. I ran the new `test/attach-look.test.ts` and the updated `test/mcp.test.ts` with a local preload that swaps the unix listener for a TCP port, and both pass. `…
shipped production is serving it deploy 12e5a4a
I merged main into this pull. Both main and this pull had added a migration numbered 73 in src/core/schema.ts, so that file conflicted.
- Fix: I kept
main'soauth_codetable as migration 73. Theattached_byandnotecolumns onlookare now migration 74. - Tests: The schema tests pass. In the sandbox, 525 of the 1,869 tests fail, including both tests in
test/attach-look.test.tsand the other tests that start a server. The ones I opened fail onTypeError: undefined is not an object (evaluating 'servers.stop'), after the server fails to start. I assume the sandbox is blocking the listening ports and haven't confirmed it. So the attach-a-look route and MCP tool were not exercised here. The forge's check is the real test of those. - Left out: I didn't compare against a run of
mainin this sandbox, so I can't say how many of those failures would also fail without this change.
To try it, open the preview at https://lab--1375--forge-trevato-apps.open-platform.sh and attach a look to a pull. The note appears beside the crew's looks.
🤖 Generated with Claude Code
Approved. This does what #1066 asks: a person who can write the app attaches a phone or desktop look through the API (POST …/pulls/:number/looks) or the MCP tool attach_look, and it shows in the Look card for writers only.
Security, checked line by line:
- Identity comes from the token. A read-only scope is refused. A stranger gets 401, a reader 403, and an agent 403 because of
actor.kind !== "user". readBoundedcaps the body by the bytes it actually reads, at 16 MB. The base64 length is also checked before decoding.- The image is accepted only if it parses as PNG or JPEG, and each side must be at most 8192 px.
- The note is one line, escaped in the caption and the
altattribute. The test asserts the escaping. - The served file stays behind
mayFetchLook: attached looks aretakenAs: owner, so readers and strangers get 404. The test covers both. content-typefollows the file extension, and nosniff stays on.- Every validation runs before anything is written, and the tests assert nothing reaches the disk or the database on a refusal.
- The row lives under the preview env, so
forgetremoves the row and its folder when the pull closes or merges, and the sweep catches strays. The newattached_by IS NULLstops a retake from deleting attachments. - No SQL or shell is built from user text, and nothing new is added as a dependency.
Tests: on head 31ec12f I ran bun test test/attach-look.test.ts test/mcp.test.ts test/schema.test.ts and got 18 pass, 0 fail.
I could not see the preview or the forge's pictures from my sandbox (the preview host is refused by egress and the pictures folder is not readable), so this verdict rests on the code and the test that renders the page.
The terminal has no attach door. That is consistent with the page, which has none either: attaching is API and MCP, as the issue asks, and docs/APPS.md says how. The two line comments below are follow-ups and do not block.
The full gate on land was red; the pull is open again. Red tests:
- the gate's list > every test/*.test.ts is in exactly one list, and every entry exists
- the gate's list > every test/*.test.ts is in exactly one list, and every entry exists [0.66ms] [shard 182]
A person who can write an app can now attach a PNG or JPEG look to an open pull, and it shows beside the crew's looks on the change's page. I couldn't run this change's HTTP tests as written because the sandbox refuses unix-socket listeners. I ran the new
test/attach-look.test.tsand the updatedtest/mcp.test.tswith a local preload that swaps the unix listener for a TCP port, and both pass.looks.test.tsandparity.test.tsfail 13 tests the same way with this change stashed (checked with the same preload), so those are environmental.tscis clean. The forge's check is the real verdict.What changed
POST /api/v1/repos/:owner/:name/pulls/:number/lookstakes JSON{width: "phone"|"desktop", says, image}, with the image as base64. It returns 201 with the look's URL.attach_lookdoes the same for a person's token and plans first, likehold. An agent is refused, so the crew's looks keep their existing path.attached_byandnotecolumns tolook. It emits alook.attachedevent.image/jpeg.Choices and limits
no_app, because looks hang off the app's preview.docs/APPS.md.To try it
curl -X POST -H "authorization: Bearer $TOKEN" -H "content-type: application/json" -d '{"width":"phone","says":"the empty state","image":"'$(base64 -w0 shot.png)'"}' $FORGE_URL/api/v1/repos/OWNER/NAME/pulls/N/looks, then open the pull as a writer.🤖 Generated with Claude Code
Closes #1066