fix(desktop,service,features): draw bot badges and repository screenshots on the Pull request tab
What was asked
Bot images on an agent's Pull request tab (Overview) were drawn as their alt text with a picture glyph instead of the image. Examples on #3030: Vercel's "Request Review" button, the Vercel project avatar, and Devin Review's badge. A follow-up added screenshots a description links from a branch of the agent's own private repository, such as #3030's raw.githubusercontent.com/ReviewStage/luke/pr-assets/... table. The fix had to keep the security model: the window never loads a remote address, images go through the service, and any non-GitHub host is reached only through camo.
What dropped each image
I checked each step against #3030's real body_html:
- GitHub's HTML. GitHub rewrites every external
srcandsrcsettocamo.githubusercontent.com, the Vercel avatar,ready.svg, and both Devin and Vercel<source>s included. Nothing external is left. - Camo. It answers all of them
200: the avatar asimage/png(457 B), and the badges asimage/svg+xml(138 B to 15.8 KB). The service's owngithubImage, run against them with the productionFetchHttpClient, reads all of them. githubImageOf. It admits every camo address.- The sanitizer (the real cause for themed badges). It keeps
<picture>and<source srcset="https://camo…">. A browser draws a matching<source>in place of the<img>, so thedata:URL the tab draws from the service was ignored. The window then asked camo itself,img-srcrefused it, and the image stood broken, showing its alt text. Vercel's button has both a dark and a light<source>, so it always broke; Devin's has a dark one, so it broke in dark mode. - Repository screenshots. GitHub leaves
raw.githubusercontent.comaddresses as written, with no camo.githubImageOfadmitted none of them, so they were drawn as links.
The fix
packages/ui/src/github-html.ts:githubShapesnow drops every<source>before the sanitizer runs. A<picture>keeps only its<img>, which the tab reads through the service as before. The<img>GitHub keeps is the light variant, so a themed badge now shows its light face in the dark theme.packages/features/src/github-image.ts: adds a third source,FILE, forraw.githubusercontent.com/<owner>/<name>/<ref>/<path>andgithub.com/<owner>/<name>/raw/<ref>/<path>. It is admitted only when the path ends inpng/jpg/jpeg/webp/gif/svg, and the extension decides the type. It refuses./.., encoded/, malformed escapes, and any query (a raw?token=is never used).packages/service/src/github/github-images.ts: reads aFILEimage throughGET /repos/{agent's repo}/contents/{path}?ref=…withAccept: application/vnd.github.raw. It uses the token already minted for the agent's repository (contents: read, cut to that one repository).- A file of any other repository gets an answer of none and is never asked for.
- The contents API serves the bytes as
application/vnd.github.raw, so only that answer is read (not a JSON listing), and only within the 8 MB bound, typed by the extension. - The bytes must also show that type's signature: PNG, JPEG, GIF and WebP magic bytes, or
<svganywhere in an SVG. A path whose escapes spell no UTF-8 names no file and is drawn as its link.
Limitation: a ref is taken as the first segment after the repository, so an image on a branch whose name has a slash still stands as its link.
What I couldn't confirm
The developer also listed the Vercel avatar, a plain <img> with no <picture>. In a jsdom render of #3030's real HTML, both the avatar and ready.svg reach the service, and the service reads both from camo. I found no step that drops them, so if they still fail after this, the cause is outside the code I could exercise (likely the deployment the app was pointed at).
Tests
These fail without the fix and pass with it:
packages/ui/src/github-html.test.ts: a themed picture keeps only its image.apps/desktop/src/renderer/tasks/agent-pull-request-tab.test.ts: the tab holds no<source>and draws a themed badge and a repository screenshot from what the service read.packages/features/src/github-image.test.ts: repository file addresses are admitted or refused.packages/service/src/github/github-images.test.ts: a file is read through contents on the token; another repository is never asked for; a listing or an oversize answer is none.
pnpm check:affected passed lint, types and every other suite. Only apps/desktop/src/main/app-menu.test.ts failed, because the sandbox has no Electron binary; that suite is unrelated and runs in CI.
Made with Luke
Summary by cubic
Fixes bot badges and repository screenshots on the agent's Pull request tab, which were drawn as broken alt text or bare links. Bot badges broke because a browser picks a themed <source> over the <img> and the tab's policy refuses a direct request to camo; screenshots linked from the agent's own repository were never read at all.
- Drops every
<source>from GitHub's rendering, so a<picture>keeps only its<img>and the service reads it as before. A themed badge now shows its light variant in dark mode. - Reads repository file images (
raw.githubusercontent.com/...andgithub.com/<owner>/<name>/raw/...) through the contents API on the token already minted for the agent's repository, bounded at 8 MB. Files of any other repository are never fetched, and a file is read as its extension's type only when its bytes show that type's signature; an SVG is read when its bytes hold<svganywhere, so one with a long comment before its root draws. - A ref is taken as the first path segment, so an image on a branch whose name contains a slash still draws as its link; an address whose path escapes spell no UTF-8 draws as its link rather than throw while the tab renders.