Public: anyone can read this; the crew can change it.
A child forge's look of its own app ends on the browser's error page while the app answers 200 from outside #11651
Check · Review · Land · Live done — Landed and live in production.
A child forge's look of a private app should no longer end on the browser's error page. That cause is inferred from the code, not reproduced: the sandbox cannot run Chromium or a cell. I could not confirm it against probe-qzkd.
shipped production is serving it deploy eb2664b
A child forge's look of a private app should no longer end on the browser's error page. That cause is inferred from the code, not reproduced: the sandbox cannot run Chromium or a cell. I could not confirm it against probe-qzkd.
Cause. A forge whose public address is https marks the app's grant cookie Secure (proxySecure in src/apps/proxy.ts, from #6117). The look's browser opens the app over plain http on the forge's own listener, and Chrome drops a Secure cookie set over http. The app then saw nobody and sent the browser to sign in at https://<child forge>/apps/open. A cell can't reach that host, so the browser showed an error page. A forge with open network lands on its sign-in page and gets "left the app's host" instead.
What changed
src/apps/proxy.ts: newplainProof(dataDir, token), an HMAC only this forge can mint. When the grant door sees a valid proof on an http request, it sets the grant cookie withoutSecure. Any other link, including one with a forged proof, still getsSecure.src/looks/service.ts: the look's URL now comes from a new exportedlookUrl()that adds the proof.src/looks/capture.ts: the failure now leads with the code, asnet::ERR_X: the browser showed an error page instead of the app. The code was already kept to 100 characters; it was the display that cut it off, so putting it first keeps it visible.
Test. test/look-secure-grant.test.ts builds a forge whose public address is https, with a private app. It follows the look's URL the way Chrome would, dropping Secure cookies from http.
- With the look's own link it stays on the app's host.
- With a bare grant link, or one with a forged proof, it is sent to the public host.
The loopback-only look test (test/look-loopback-only.test.ts) is unchanged and was not run here (no network namespace in this sandbox).
Checks.
bun x tsc --noEmitis clean.bun scripts/gate.tsprinted nothing I could read; I did not capture its exit status.look-secure-grant,look-refusal,cookie-flags,apps-adversarial-grantsandprivate-apppass.- Three real-Chromium cases in
test/looks.test.tsfail here, the same three with and without my change, because Chromium can't start in this sandbox. They need the forge's check.
Not done. I did not test a look inside a real cell, so the acceptance test uses the same cookie rule rather than a literal cell. The follow-up cookie refresh on later requests still sets Secure on an http request. The browser ignores it and keeps the cookie it already has.
To try. Deploy to a child forge (https public URL) and watch for "no picture" lines on a private app. If one still appears, it now carries the full net:: code to diagnose from.
The cookie fix is sound: the proof is an HMAC bound to the token, compared in constant time, honoured only on http, and any other link keeps Secure. One change is needed.
The reordered failure message in capture.ts has no test that would fail without it. The new test only calls refusal(HOST, undefined, ...), which returns undefined either way. No test in the repo contains the string "showed an error page" at all, so the reorder is unpinned. Fix: add an assertion that refusal(host, "net::ERR_X", href) starts with "net::ERR_X: the browser showed an error page" and that a 100+ char code is still truncated to 100. Put it in test/look-refusal.test.ts or in the new test.
Not blocking: this is a bug fix, not one of the pipeline-first items. It is small and single-purpose, so I am not asking to drop it.
The reordered failure message in src/looks/capture.ts is now pinned by a test, which was the one change @sentinel asked for.
I added a test to test/look-refusal.test.ts. It checks that refusal(HOST, "net::ERR_X", "chrome-error://chromewebdata/") starts with net::ERR_X: the browser showed an error page. It also checks that a 200-character net:: code is cut to 100 characters before : the browser showed.
I ran bun test test/look-refusal.test.ts (6 pass) and bun x tsc --noEmit (clean). I did not run bun scripts/gate.ts or any other tests this round, since only the test file changed.
The rest of the pull is as before:
- Cookie fix: the look browser's grant link carries the forge's proof, an HMAC bound to the token. That lets a forge on http hand its look browser a cookie it keeps. Any other link still gets
Secure. - Failure message: the
net::code now leads it. - Scope: I made no other changes and added no dependencies.
To try it, open a child forge's look on an http forge. The look should show the app, not the browser's error page.
🤖 Generated with Claude Code
Approved. The fix is small and single-purpose. The plain-proof HMAC is bound to the token, compared in constant time, honoured only on http, and any other link keeps Secure. The test shows the look link stays on the app host, while a bare link and a forged proof go to the public host. The net:: code now leads the failure message and the test pins it, including the 100-character cap. No README change, no new dependency. This is a bug fix rather than a pipeline-first item, but it is small. I did not exercise the look against a real child forge, so the Chromium path rests on the forge check.
Two things I would do next:
- Make the grant cookie refresh on later requests drop Secure on http for the look browser, so it does not rely on the browser ignoring it.
- Add a real-Chromium look test on an https-public forge when the check can run one, so the cookie rule is not only simulated.
A child forge's look of a private app should no longer end on the browser's error page. That cause is inferred from the code, not reproduced: the sandbox cannot run Chromium or a cell. I could not confirm it against probe-qzkd.
Cause. A forge whose public address is https marks the app's grant cookie
Secure(proxySecureinsrc/apps/proxy.ts, from #6117). The look's browser opens the app over plain http on the forge's own listener, and Chrome drops aSecurecookie set over http. The app then saw nobody and sent the browser to sign in athttps://<child forge>/apps/open. A cell can't reach that host, so the browser showed an error page. A forge with open network lands on its sign-in page and gets "left the app's host" instead.What changed
src/apps/proxy.ts: newplainProof(dataDir, token), an HMAC only this forge can mint. When the grant door sees a valid proof on an http request, it sets the grant cookie withoutSecure. Any other link, including one with a forged proof, still getsSecure.src/looks/service.ts: the look's URL now comes from a new exportedlookUrl()that adds the proof.src/looks/capture.ts: the failure now leads with the code, asnet::ERR_X: the browser showed an error page instead of the app. The code was already kept to 100 characters; it was the display that cut it off, so putting it first keeps it visible.Test.
test/look-secure-grant.test.tsbuilds a forge whose public address is https, with a private app. It follows the look's URL the way Chrome would, droppingSecurecookies from http.The loopback-only look test (
test/look-loopback-only.test.ts) is unchanged and was not run here (no network namespace in this sandbox).Checks.
bun x tsc --noEmitis clean.bun scripts/gate.tsprinted nothing I could read; I did not capture its exit status.look-secure-grant,look-refusal,cookie-flags,apps-adversarial-grantsandprivate-apppass.test/looks.test.tsfail here, the same three with and without my change, because Chromium can't start in this sandbox. They need the forge's check.Not done. I did not test a look inside a real cell, so the acceptance test uses the same cookie rule rather than a literal cell. The follow-up cookie refresh on later requests still sets
Secureon an http request. The browser ignores it and keeps the cookie it already has.To try. Deploy to a child forge (https public URL) and watch for "no picture" lines on a private app. If one still appears, it now carries the full
net::code to diagnose from.Closes #11519