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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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 cc21c84

opened this

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. looks.test.ts and parity.test.ts fail 13 tests the same way with this change stashed (checked with the same preload), so those are environmental. tsc is clean. The forge's check is the real verdict.

What changed

  • API: POST /api/v1/repos/:owner/:name/pulls/:number/looks takes JSON {width: "phone"|"desktop", says, image}, with the image as base64. It returns 201 with the look's URL.
  • MCP: attach_look does the same for a person's token and plans first, like hold. An agent is refused, so the crew's looks keep their existing path.
  • Validation: only a person who can write may attach. The image is checked as bytes, not by its label: it must parse as a PNG or JPEG, be at most 5 MB (413 over that) and at most 8192 px on a side. The line is one line of 1–200 characters with no control characters. Anything refused writes nothing.
  • Storage and event: the picture is saved under the preview's looks folder at the pull's head. Migration 73 adds attached_by and note columns to look. It emits a look.attached event.
  • Page: the Look card shows attached looks in the same strip, each with its line and who attached it. The caption is escaped and the image carries its own width and height, so nothing shifts. JPEGs are served as image/jpeg.
  • Lifetime: an attached look is not aged out when a later head is pictured. It goes with the preview when the pull closes.

Choices and limits

  • Attached looks are shown to writers only, never to other readers. That follows "nothing a stranger sends is ever shown", and I did not extend it to readers.
  • A repository with no app is refused with 409 no_app, because looks hang off the app's preview.
  • I used base64 in JSON rather than multipart, so the API and MCP take the same body and no upload parser is needed.
  • A JPEG whose size header sits past the first 64 KB renders without width and height attributes.
  • There is no web form for attaching, and a person cannot delete an attached look.
  • I added a section to 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

commented

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's oauth_code table as migration 73. The attached_by and note columns on look are 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.ts and the other tests that start a server. The ones I opened fail on TypeError: 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 main in 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

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".
  • readBounded caps 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 alt attribute. The test asserts the escaping.
  • The served file stays behind mayFetchLook: attached looks are takenAs: owner, so readers and strangers get 404. The test covers both.
  • content-type follows 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 forget removes the row and its folder when the pull closes or merges, and the sweep catches strays. The new attached_by IS NULL stops 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.

commented

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]
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs