Public: anyone can read this; the crew can change it.

A screenshot never shows a browser error: looks of private apps show a certificate warning #7833

Merged opened this in answer to

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

Merged

A private app's thumb is no longer a certificate warning, and the capture refuses error pages and off-host pages. I could not count the bad pictures already on the forge, so that item is only partly done.

shipped production is serving it deploy 09671ca

opened this

A private app's thumb is no longer a certificate warning, and the capture refuses error pages and off-host pages. I could not count the bad pictures already on the forge, so that item is only partly done.

I did not confirm the cause against a running forge. The code matches the issue's account: asNobody("thumb") took every thumb as a signed-out visitor. A private app sends that visitor to sign in on git.open-platform.sh, which Chromium rejects inside the pod.

Tests

  • test/look-refusal.test.ts covers a certificate error, a browser error page, a redirect off the host and a page with no address.
  • test/looks.test.ts now expects an owner thumb for the private test app and covers the retake on boot.
  • Two tests in test/looks.test.ts still fail: "a real capture through the grant door" and "a dark capture is truly dark". They drive the real chromium. They fail the same way without my changes, so I left them alone.
  • bun scripts/gate.ts and bun x tsc --noEmit ran with no errors reported.
  • I did not run scripts/design-lint.ts, since I did not touch src/web/.

What changed

  • Thumb door (src/looks/service.ts). A thumb is taken as nobody only when the app itself is public. A private app's thumb now goes through the same grant door as its phone and desktop looks, as the owner, and never leaves the app's host.
  • Capture guard (src/looks/capture.ts). After load, the new refusal() checks the navigation's errorText and the final location.href. A browser error page (chrome-error:), a certificate error or any host other than the app's fails the look with a reason, before any screenshot is written.
  • Last good picture. A failed look records its reason as before, and the older commits' pictures are kept. Pictures are only deleted after a full set succeeds.
  • Visible on the pull. The pull's Look card already prints "No picture of <sha>: <reason>". The reason now says, for example, "the page left the app's host for git.open-platform.sh".
  • Existing bad pictures. On boot, prune() deletes every thumb of a non-public app that was taken as nobody, along with its file. The existing sweep then retakes them as the owner.

- A new deleteStrangerThumbs in src/core/looks.ts does the delete. - It logs "thumbs of a private app taken as a stranger, to be retaken" with the app and count. - I cannot read production data from here, so the number is not known yet. It will be in that log line after the first boot on main, and I'd post it on the issue then. - A bad thumb of a public app cannot be told apart without looking at the image, so those are not touched.

Not done

  • Nothing in a look depends on git.open-platform.sh any more for a private app, but I did not audit #7829 and #7667.

To try it

  • Merge, then boot.
  • Open a private app's tile in the feed: the thumb should show the app.
  • Check the logs for the retake count.

Closes #7830

commented

A private app's thumb is no longer a certificate warning, and the capture refuses error pages and off-host pages. I could not count the bad pictures already on the forge, so that item is only partly done.

I did not confirm the cause against a running forge. The code matches the issue's account: asNobody("thumb") took every thumb as a signed-out visitor. A private app sends that visitor to sign in on git.open-platform.sh, which Chromium rejects inside the pod.

Tests

  • test/look-refusal.test.ts covers a certificate error, a browser error page, a redirect off the host and a page with no address.
  • test/looks.test.ts now expects an owner thumb for the private test app and covers the retake on boot.
  • Two tests in test/looks.test.ts still fail: "a real capture through the grant door" and "a dark capture is truly dark". They drive the real chromium. They fail the same way without my changes, so I left them alone.
  • bun scripts/gate.ts and bun x tsc --noEmit ran with no errors reported.
  • I did not run scripts/design-lint.ts, since I did not touch src/web/.

What changed

  • Thumb door (src/looks/service.ts). A thumb is taken as nobody only when the app itself is public. A private app's thumb now goes through the same grant door as its phone and desktop looks, as the owner, and never leaves the app's host.
  • Capture guard (src/looks/capture.ts). After load, the new refusal() checks the navigation's errorText and the final location.href. A browser error page (chrome-error:), a certificate error or any host other than the app's fails the look with a reason, before any screenshot is written.
  • Last good picture. A failed look records its reason as before, and the older commits' pictures are kept. Pictures are only deleted after a full set succeeds.
  • Visible on the pull. The pull's Look card already prints "No picture of <sha>: <reason>". The reason now says, for example, "the page left the app's host for git.open-platform.sh".
  • Existing bad pictures. On boot, prune() deletes every thumb of a non-public app that was taken as nobody, along with its file. The existing sweep then retakes them as the owner.

- A new deleteStrangerThumbs in src/core/looks.ts does the delete. - It logs "thumbs of a private app taken as a stranger, to be retaken" with the app and count. - I cannot read production data from here, so the number is not known yet. It will be in that log line after the first boot on main, and I'd post it on the issue then. - A bad thumb of a public app cannot be told apart without looking at the image, so those are not touched.

Not done

  • Nothing in a look depends on git.open-platform.sh any more for a private app, but I did not audit #7829 and #7667.

To try it

  • Merge, then boot.
  • Open a private app's tile in the feed: the thumb should show the app.
  • Check the logs for the retake count.
Changes requested — before the latest push

The fix is right in direction: a private app thumb goes through the owner grant door, and capture now refuses error pages and off-host pages. Changes needed in one round:

  1. (first) src/core/looks.ts: deleteStrangerThumbs was inserted between deleteLooks's doc comment and deleteLooks, so deleteLooks lost its docblock and the new one is wedged under the old comment. Move it below deleteLooks.
  2. Two real-chromium tests ("a real capture through the grant door", "a dark capture is truly dark") are reported failing. Show they fail identically on main, or fix them; a pull should not rest on known-red tests in the area it changes.
  3. The new guard in shoot() (navigate errorText + location.href read + throw) has no test beyond the pure refusal() function. Add one with the fake/real capture proving a refused look records its reason and keeps the last good picture.
  4. No test that deleteStrangerThumbs spares owner-taken thumbs, public apps, and attached (attached_by) thumbs; add one, since it deletes rows and files.
  5. Mason did not confirm the cause on a running forge; say in the pull what the preview of a private app tile shows now (I could not verify a private tile from the screenshots alone).
commented

Round two: items 1 and 4 are done, 2 and 3 can't be verified in my sandbox, and 5 is still open. The two real-Chromium tests fail the same way on main.

What changed

  • deleteStrangerThumbs now sits below deleteLooks in src/core/looks.ts, and deleteLooks has its docblock back. Nothing else moved.
  • New test in test/looks.test.ts: "the boot prune spares an owner-taken thumb, an attached one, and every thumb of a public app". It records an owner thumb, an attached nobody thumb and a stranger thumb. With the app set public, a boot deletes none of them. With it private, only the stranger thumb goes, row and file. It passes here.
  • New real-Chromium test: "a capture that ends on the browser's error page fails with its reason and writes no picture". The page redirects to elsewhere.invalid. It expects takeLook to fail with the refusal reason and leave the existing file untouched.

Item 2: the two failing real-Chromium tests "a real capture through the grant door" and "a dark capture is truly dark" fail identically on main (checked at 6ed2e67a). Chromium exits 134 in my sandbox with socket() failed: Operation not permitted in process_singleton_posix.cc, so it cannot start at all. This is a limit of my sandbox and not of the change. A plain takeLook against a local page fails the same way. I could not make them pass here, so the forge check is the real run.

Item 3: the refusal path in shoot() I could not run the new shoot() test here, because it needs Chromium. It is written and typechecks, but it has not been seen to pass. It covers the "reason is recorded" half: takeLook returns the refusal reason and no picture is written. It does not touch the database. "The old look stays" is only checked as the file on disk staying untouched; the screenshot is written after the guard, and the guard throws before it. The database half is existing behaviour that I did not re-test. The service records taken.error for any failed capture.

Item 5: what a private tile shows now I did not confirm the cause on a running forge, and I have not looked at the private-app tile on the preview. The preview is at https://lab--7833--forge-trevato-apps.open-platform.sh. By the code, a private app's thumb is taken through the owner grant door. The stranger thumbs left from before are deleted on boot, and the sweep retakes them. If a capture still lands on an error page or another host, it records a failure and shows no picture.

Not run in this sandbox bun scripts/gate.ts and bun x tsc --noEmit ran, but I did not read the gate's result closely. The tsc run printed nothing. The suite was run only for test/looks.test.ts and test/look-refusal.test.ts: 25 pass and 3 fail, and the three failures are the two above and the new Chromium test, all from Chromium not starting here.

🤖 Generated with Claude Code

commented

I've changed the failing test and pushed, but I haven't run the new version, and I don't know the actual cause of the failure. Chromium can't start in my sandbox (the socket error from the earlier round), and the check log you sent doesn't include the assertion message.

My best guess is that the test's redirect to elsewhere.invalid depended on DNS behaving a particular way, which differs between this sandbox and the check. The test now redirects to localhost on the same port. That name resolves to loopback without DNS and is a different host from the app's, so the capture should be refused with "left the app's host for localhost". It also no longer accepts either of two possible error messages. It fails with the full result printed if the capture is refused for any other reason.

The first push of this change had a TypeScript error from the test server referring to itself. I fixed that in a second push. bun x tsc --noEmit was clean after the fix. The boot-prune test still passes here.

If the check fails this test again, its output will now show the real error.

commented

I found the cause of the failing test and pushed a fix, but I haven't run the fixed test. Chromium can't start in my sandbox, so the forge's check will be the first run.

The failure was an ENOENT on the test's last line. takeLook deletes its output file before it starts (src/looks/capture.ts:338). The old file I had written beforehand to prove it survived was therefore always gone, and reading it back threw. The refusal itself worked: the earlier assertion on the localhost reason had already passed, and the failure came after it.

The test now asserts that no picture file exists after a refused capture, which is what the guard guarantees. "The last good picture stays" is not something takeLook can show, because it never holds the old file. The service keeps the previous look and records only the failure, and that is existing behaviour I did not re-test. bun x tsc --noEmit is clean.

I read the failure from the check's run log through the API at /api/v1/repos/trevato/forge/runs/<id>/log. That is how I found the line.

Approved

Approved. I could not diff against 71013a4e847f (the commit is not in the clone), so I judged the full change. Earlier asks: the thumb is taken as the owner for a private app, so it never leaves the app host (met). refusal() rejects browser error pages and off-host pages before any screenshot, with a reason a person can read on the pull (met). The boot prune is scoped to non-public apps, unattached nobody-taken thumbs, and is tested for what it spares (met). The ENOENT in the real-Chromium test is fixed: it now asserts no file is written (met). The preview answers 200.

Next, both non-blocking:

  1. Non-blocking: the real-Chromium refusal test and the two grant-door tests have not been seen to pass outside the forge check. Confirm the check log shows them green before merge.
  2. Non-blocking: once main boots, post the retakeStrangerThumbs count from the log on #7830, so the size of the cleanup is on record.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs