Public: anyone can read this; the crew can change it.
A preview of the forge can be looked at as its owner: looks of chosen pages are taken signed in, over seeded data #8830
Check · Review · Land · Live done — Landed and live in production.
A preview of the forge can now be pictured signed in as its owner. Pages that `forge.json` lists under the new `app.looksAsOwner` get their phone and desktop pictures taken as an admin. The preview seeds a preview-only admin and a small fixture so those pages have something to show. Production refuses both.
shipped production is serving it deploy 18f765b
A preview of the forge can now be pictured signed in as its owner. Pages that forge.json lists under the new app.looksAsOwner get their phone and desktop pictures taken as an admin. The preview seeds a preview-only admin and a small fixture so those pages have something to show. Production refuses both.
What changed
src/apps/manifest.ts: new fieldapp.looksAsOwner. Each entry is a page path checked like the ones inapp.looks, and each must also appear inapp.looks, or the manifest is refused. Leaving it out means no pages. I folded the path checks shared bylooksandlooksAsOwnerinto onepathListhelper; the error messages forapp.looksread the same as before.src/apps/runtime.ts: the environment status now carrieslooksAsOwner.src/looks/service.ts: for a listed page, phone and desktop are taken with the owner's admin grant and recorded astakenAs: "owner", so only writers can fetch them (mayFetchLook). Pages not listed, and every thumb, are still taken as nobody on a public app.src/spawn/preview-seed.ts(new): whenFORGE_APP_ENV=preview, it createspreview-adminonce. The account has no password and no session. It also seeds a public repopreview-admin/samplewith two issues and an open pull, three agent runs that spent money this week, a valve turned to 60% (the flow week), and two board panels pinned to home. Anywhere else it answersnot_previewand creates nothing.src/spawn/preview-owner.ts(new), wired intoresolveAuthinsrc/http/server.tsand set up insrc/main.ts:
- In a preview only, a request with an x-forge-identity header naming an admin, signed with FORGE_APP_IDENTITY_KEY and not expired, is treated as the preview admin.
- It counts as an edge identity, so changes are still held to the same-origin check.
- Production never reads the key, so the headers are ignored there.
- Anyone the parent forge marks as admin of the repo is a writer, and can already push code that runs over a preview's data, so this gives them nothing new.
forge.json:"looksAsOwner": ["/", "/flow", "/talk"].- Docs:
docs/features/preview-owner-looks.md(new) and a paragraph indocs/APPS.mdunder "The look".
What to look at
The asNobody(kind, page) rule in src/looks/service.ts and the ordering in resolveAuth. Also the choice to reuse via: "edge": it gives the same-origin check without adding a new kind of sign-in. The catch is that the sign-out link in a preview points at the platform's logout.
Not done here
The desk and phone looks of home and /flow that #8698 and #8726 asked for will not appear on this pull. Looks are taken by the parent forge, which runs main and ignores the new field until this merges. The first pull after merge will carry them.
How it was checked
bun scripts/gate.tsandbun x tsc --noEmitpass.test/looks-owner.test.ts(new) passes 8 of 8, covering:
- the manifest field;
- the seed refused in production and with no environment set, and running once in a preview;
- a signed admin seeing /flow, while a reader, a forged signature or an expired one gets nothing;
- the same-origin check on changes;
- production ignoring the headers;
- a page listed as owner getting the admin grant while the others stay nobody.
scripts/fast.tscalled the diff too broad becauseforge.jsonchanged, so I picked the covering suites myself: looks, forge-looks, refresh-meta, edge-auth, signin-edge, apps-adversarial, here-manifest, board-pins. 174 pass, 1 skipped, 3 fail. The 3 are the real-Chromium capture tests intest/looks.test.ts, and they fail the same way on unchangedmainin this sandbox, so they are not caused by this change.- I booted it with
FORGE_APP_ENV=previewand a signed admin identity: the log says "preview seeded",/answers 200,/flowanswers 200 in 18 ms showingpreview-admin/sample, home's pin slot shows both pinned panels, and an anonymous/flowis redirected to sign in.
Try it
Run FORGE_APP_ENV=preview FORGE_APP_IDENTITY_KEY=k FORGE_DATA=$(mktemp -d) bun run src/main.ts. Then request /flow with x-forge-identity: u1|you|admin|<expiry ms> and x-forge-signature set to the base64url HMAC-SHA256 of that identity string under k.
🤖 Generated with Claude Code
A preview of the forge can now be pictured signed in as its owner. Pages that forge.json lists under the new app.looksAsOwner get their phone and desktop pictures taken as an admin, and a preview seeds a preview-only admin and a small fixture so those pages have something to show. Production refuses both. main has been merged in: the forge.json conflict is resolved with main's page list (/repos replaced /migrate) and this branch's "looksAsOwner": ["/", "/flow", "/talk"].
What changed
src/apps/manifest.ts: new fieldapp.looksAsOwner. Each entry is a page path checked like those inapp.looks, and each must also appear inapp.looks, or the manifest is refused. Leaving it out means no pages. The path checks shared bylooksandlooksAsOwnerare now onepathListhelper; the error messages forapp.looksread as before.src/apps/runtime.ts: the environment status now carrieslooksAsOwner.src/looks/service.ts: for a listed page, phone and desktop are taken with the owner's admin grant and recorded astakenAs: "owner", so only writers can fetch them (mayFetchLook). Pages not listed, and every thumb, are still taken as nobody on a public app.src/spawn/preview-seed.ts(new): whenFORGE_APP_ENV=preview, it createspreview-adminonce. The account has no password and no session. It also seeds a public repopreview-admin/samplewith two issues and an open pull, three agent runs that spent this week, a valve turned to 60% (the flow week), and two board panels pinned to home. Anywhere else it answersnot_previewand creates nothing.src/spawn/preview-owner.ts(new), wired intoresolveAuthinsrc/http/server.tsand set up insrc/main.ts:
- In a preview only, a request whose x-forge-identity names an admin, is signed with FORGE_APP_IDENTITY_KEY and has not expired, is treated as the preview admin.
- It counts as an edge identity, so changes are still held to the same-origin check.
- Production never reads the key, so the headers are ignored there.
- Anyone the parent forge marks as admin of the repo is a writer, and can already push code that runs over a preview's data, so this gives them nothing new.
forge.json:"looksAsOwner": ["/", "/flow", "/talk"].- Docs:
docs/features/preview-owner-looks.md(new) and a paragraph under "The look" indocs/APPS.md.
What to look at
The asNobody(kind, page) rule in src/looks/service.ts and the ordering in resolveAuth. Also the choice to reuse via: "edge": it gives the same-origin check without adding a new kind of sign-in. The catch is that the sign-out link in a preview points at the platform's logout.
Not done here
The desk and phone looks of home and /flow that #8698 and #8726 asked for will not appear on this pull. Looks are taken by the parent forge, which runs main and ignores the new field until this merges. The first pull after merge will carry them.
How it was checked
- After the merge,
bun scripts/gate.tsandbun x tsc --noEmitpass.test/looks-owner.test.ts(new), forge-looks, refresh-meta, edge-auth and here-manifest also pass: 82 tests, none failing. - The new test covers:
- the manifest field;
- the seed being refused in production and with no environment set, and running once in a preview;
- a signed admin seeing /flow, while a reader, a forged signature or an expired one gets nothing;
- the same-origin check on changes;
- production ignoring the headers;
- a page listed as owner getting the admin grant while the others stay nobody.
- Before the merge, the wider set also covered
test/looks.test.ts, signin-edge, apps-adversarial and board-pins. In that run, three real-Chromium capture tests intest/looks.test.tsfailed; they fail the same way on unchangedmainin this sandbox. - Before the merge I also booted it with
FORGE_APP_ENV=previewand a signed admin identity. The seed ran,/answered 200,/flowanswered 200 in 18 ms as the preview admin, home showed both pins, and an anonymous/flowwas redirected to sign in.
Try it
Run FORGE_APP_ENV=preview FORGE_APP_IDENTITY_KEY=k FORGE_DATA=$(mktemp -d) bun run src/main.ts. Then request /flow with x-forge-identity: u1|you|admin|<expiry ms> and x-forge-signature set to the base64url HMAC-SHA256 of that identity string under k.
🤖 Generated with Claude Code
A preview of the forge can now be pictured signed in as its owner. Pages that forge.json lists under the new app.looksAsOwner get their phone and desktop pictures taken as an admin, and a preview seeds a preview-only admin and a small fixture so those pages have something to show. Production refuses both.
What changed
src/apps/manifest.ts: new fieldapp.looksAsOwner. Each entry is a page path checked like those inapp.looks, and each must also appear inapp.looks, or the manifest is refused. Leaving it out means no pages. The path checks shared bylooksandlooksAsOwnerare now onepathListhelper; the error messages forapp.looksread as before.src/apps/runtime.ts: the environment status now carrieslooksAsOwner.src/looks/service.ts: for a listed page, phone and desktop are taken with the owner's admin grant and recorded astakenAs: "owner", so only writers can fetch them (mayFetchLook). Pages not listed, and every thumb, are still taken as nobody on a public app.src/spawn/preview-seed.ts(new): whenFORGE_APP_ENV=preview, it createspreview-adminonce. The account has no password and no session. It also seeds a public repopreview-admin/samplewith two issues and an open pull, three agent runs that spent this week, a valve turned to 60% (the flow week), and two board panels pinned to home. Anywhere else it answersnot_previewand creates nothing.src/spawn/preview-owner.ts(new), wired intoresolveAuthinsrc/http/server.tsand set up insrc/main.ts:
- In a preview only, a request whose x-forge-identity names an admin, is signed with FORGE_APP_IDENTITY_KEY and has not expired, is treated as the preview admin.
- It counts as an edge identity, so changes are still held to the same-origin check.
- Production never reads the key, so the headers are ignored there.
- Anyone the parent forge marks as admin of the repo is a writer, and can already push code that runs over a preview's data, so this gives them nothing new.
forge.json:"looksAsOwner": ["/", "/flow", "/talk"], next tomain's current page list.test/signup-gate-parent.test.ts: addssrc/spawn/preview-seed.ts: ["seed"]to its list of every place an account is made. The list is updated by hand whenever a new caller is added, as its comment says. The test was right; this is the deliberate update it asks for, and it fixes the one failure in the last check.- Docs:
docs/features/preview-owner-looks.md(new) and a paragraph under "The look" indocs/APPS.md.
What to look at
The asNobody(kind, page) rule in src/looks/service.ts and the ordering in resolveAuth. Also the choice to reuse via: "edge": it gives the same-origin check without adding a new kind of sign-in. The catch is that the sign-out link in a preview points at the platform's logout.
Not done here
The desk and phone looks of home and /flow that #8698 and #8726 asked for will not appear on this pull. Looks are taken by the parent forge, which runs main and ignores the new field until this merges. The first pull after merge will carry them.
How it was checked
bun scripts/gate.tsandbun x tsc --noEmitpass.test/signup-gate-parent.test.tsandtest/looks-owner.test.tspass: 12 tests, none failing.- The new
test/looks-owner.test.tscovers:
- the manifest field;
- the seed being refused in production and with no environment set, and running once in a preview;
- a signed admin seeing /flow, while a reader, a forged signature or an expired one gets nothing;
- the same-origin check on changes;
- production ignoring the headers;
- a page listed as owner getting the admin grant while the others stay nobody.
- After merging
main, forge-looks, refresh-meta, edge-auth and here-manifest also passed. - Before the merge I also booted it with
FORGE_APP_ENV=previewand a signed admin identity. The seed ran,/answered 200,/flowanswered 200 in 18 ms as the preview admin, home showed both pins, and an anonymous/flowwas redirected to sign in.
Try it
Run FORGE_APP_ENV=preview FORGE_APP_IDENTITY_KEY=k FORGE_DATA=$(mktemp -d) bun run src/main.ts. Then request /flow with x-forge-identity: u1|you|admin|<expiry ms> and x-forge-signature set to the base64url HMAC-SHA256 of that identity string under k.
🤖 Generated with Claude Code
Clean on security: identity is HMAC-verified (constant-time, expiry checked), only read when FORGE_APP_ENV=preview, rides the edge path so mutations keep the same-origin rule, and the seeded admin has no password or session. Tests cover each case and the docs are in docs/features. One non-blocking note: seedWorld is not atomic, so a failure after createRepo leaves a half-made sample repo and every later boot fails on the name. Consider checking for an existing repo or cleaning up on error. The home and /flow looks (box 3) cannot appear until this lands, as the pull says.
Left #8812 open: 1 unticked item remain (The first pull carries desk and phone looks of home with pins and of /flow. Those are the boxes #8698 and #8726 left.).
A preview of the forge can now be pictured signed in as its owner. Pages that
forge.jsonlists under the newapp.looksAsOwnerget their phone and desktop pictures taken as an admin. The preview seeds a preview-only admin and a small fixture so those pages have something to show. Production refuses both.What changed
src/apps/manifest.ts: new fieldapp.looksAsOwner. Each entry is a page path checked like the ones inapp.looks, and each must also appear inapp.looks, or the manifest is refused. Leaving it out means no pages. I folded the path checks shared bylooksandlooksAsOwnerinto onepathListhelper; the error messages forapp.looksread the same as before.src/apps/runtime.ts: the environment status now carrieslooksAsOwner.src/looks/service.ts: for a listed page, phone and desktop are taken with the owner's admin grant and recorded astakenAs: "owner", so only writers can fetch them (mayFetchLook). Pages not listed, and every thumb, are still taken as nobody on a public app.src/spawn/preview-seed.ts(new): whenFORGE_APP_ENV=preview, it createspreview-adminonce. The account has no password and no session. It also seeds a public repopreview-admin/samplewith two issues and an open pull, three agent runs that spent money this week, a valve turned to 60% (the flow week), and two board panels pinned to home. Anywhere else it answersnot_previewand creates nothing.src/spawn/preview-owner.ts(new), wired intoresolveAuthinsrc/http/server.tsand set up insrc/main.ts:- In a preview only, a request with an
x-forge-identityheader naming an admin, signed withFORGE_APP_IDENTITY_KEYand not expired, is treated as the preview admin. - It counts as anedgeidentity, so changes are still held to the same-origin check. - Production never reads the key, so the headers are ignored there. - Anyone the parent forge marks as admin of the repo is a writer, and can already push code that runs over a preview's data, so this gives them nothing new.forge.json:"looksAsOwner": ["/", "/flow", "/talk"].docs/features/preview-owner-looks.md(new) and a paragraph indocs/APPS.mdunder "The look".What to look at The
asNobody(kind, page)rule insrc/looks/service.tsand the ordering inresolveAuth. Also the choice to reusevia: "edge": it gives the same-origin check without adding a new kind of sign-in. The catch is that the sign-out link in a preview points at the platform's logout.Not done here The desk and phone looks of home and /flow that #8698 and #8726 asked for will not appear on this pull. Looks are taken by the parent forge, which runs
mainand ignores the new field until this merges. The first pull after merge will carry them.How it was checked
bun scripts/gate.tsandbun x tsc --noEmitpass.test/looks-owner.test.ts(new) passes 8 of 8, covering:- the manifest field; - the seed refused in production and with no environment set, and running once in a preview; - a signed admin seeing
/flow, while a reader, a forged signature or an expired one gets nothing; - the same-origin check on changes; - production ignoring the headers; - a page listed as owner getting the admin grant while the others stay nobody.scripts/fast.tscalled the diff too broad becauseforge.jsonchanged, so I picked the covering suites myself: looks, forge-looks, refresh-meta, edge-auth, signin-edge, apps-adversarial, here-manifest, board-pins. 174 pass, 1 skipped, 3 fail. The 3 are the real-Chromium capture tests intest/looks.test.ts, and they fail the same way on unchangedmainin this sandbox, so they are not caused by this change.FORGE_APP_ENV=previewand a signed admin identity: the log says "preview seeded",/answers 200,/flowanswers 200 in 18 ms showingpreview-admin/sample, home's pin slot shows both pinned panels, and an anonymous/flowis redirected to sign in.Try it Run
FORGE_APP_ENV=preview FORGE_APP_IDENTITY_KEY=k FORGE_DATA=$(mktemp -d) bun run src/main.ts. Then request/flowwithx-forge-identity: u1|you|admin|<expiry ms>andx-forge-signatureset to the base64url HMAC-SHA256 of that identity string underk.🤖 Generated with Claude Code
Closes #8812