agentkit · pages · design brief · 2026-08-05

A published page is private property with a verified owner, and the HTML it serves never runs where the account lives

AgentKit Pages went from anonymous publishing to Assay accounts with per-device credentials in v0.7.0. Two decisions carry the security model: every page has one verified owner who grants and revokes access, and the control plane sits on a different origin from the arbitrary HTML agents publish.

shipped v0.7.0 → v0.7.2 origins account · pages device credential 90 days · 2 scopes page capability 10 minutes · 1 page default visibility private
01 · The whole path

From publish command to shared page

Everything below is a zoom into one band of this map. Read it left to right: secrets stay on the publisher's machine, identity and ownership live on the control plane, and the page the world loads is served from an origin that holds no credentials at all.

the publisher's machine control plane · account.agentkit.sbs render origin · pages.agentkit.sbs origin boundary — crossed only by an explicit exchange
your machinefiles, mode 0600
account.agentkit.sbsidentity, ownership, grants, quotas
pages.agentkit.sbsarbitrary published HTML
agent · publish
publish.ts --nameself-contained page, ≤ 5 MB
device credential
pages-tokenmode 0600 · 90 dayspages:writepages:delete
Both secrets stay here. The slug key that derives a page URL is a second file, so rotating the credential never moves a page.
device authorization
/devicebounded flow, Assay sign-in
publish worker
scope · rate · sizeeach operation names the scope it needs60/min
owner dashboard
share · invite · revokelinks, verified emails, devices
d1 · the record
pages · devices · page_access_tokensowner, grants, per-device counters — migrations are forward-only
session exchange
host-only session → capabilityrandom, ten minutes, scoped to one page
rendered page
pages.agentkit.sbs/<slug>private by default · owner is a verified Assay userCSP: no external requests
the page's own javascript
inline onlyno dashboard origin, no dashboard cookie
1 · device code 2 · credential 3 · publish 4 5 6 7 · capability ✕ blocked

Trust map — the seven crossings a page makes between the machine that publishes it, the account that owns it, and the origin that serves it.

02 · Zoom · inside band one

How a machine earns the right to publish

The first publish on a machine has no credential. Rather than failing, it starts the bounded device flow on the account origin, and the credential it stores is scoped and dated. When the service later rejects a stored credential, the publisher runs this same flow again instead of leaving the operator holding an unactionable 401.

Device authorization, first run
publish runs--name auth-flow-design
no token on disk
device flowaccount.agentkit.sbs/device
sign in
Assay verifies youthe page's owner is this identity
issues
credential storedpages-token · mode 0600
every request
scope and expiry checked90 days · pages:write · pages:delete

Device authorization — the one path by which a machine acquires, and later re-acquires, the right to publish.

File on the machineWhat it holdsWhy it is a separate fileIf it is lost
pages-token
~/.config/agentkit/
The 90-day device credential, written mode 0600, carrying pages:write and pages:delete. It is the thing an owner revokes. The dashboard lists every publishing device with its grants and expiry. Re-run the device flow; revoke the old device from the dashboard.
pages-slug-key
~/.config/agentkit/
An auto-generated HMAC key. A page's URL is HMAC(key, name) — cryptic hex, but deterministic, so republishing a name updates the same URL. Deliberately separate from the auth token so credential rotation never changes a URL. Copy it to other machines or the same name derives a different URL there. HMAC-derived pages fall out of --name reach; recover slugs from the pages repo meta.yaml and manage them with --slug.
03 · Zoom · inside the boundary

Why the two origins exist, and what crosses between them

A published page is arbitrary HTML written by an agent. If it were served from the account origin, its inline JavaScript would sit in the same origin as the dashboard and could read the dashboard's cookie. So the session that proves who you are stays host-only on account.agentkit.sbs, and what travels to the render origin is a random capability: ten minutes long, scoped to a single page, and useless anywhere else.

viewer account.agentkit.sbs pages.agentkit.sbs
01opens pages.agentkit.sbs/<slug>
02presents its host-only session
03capability · 10 min · this page only
04capability presented
05rendered HTML · CSP blocks every external request
06✕ page JS cannot reach the dashboard cookie

Reading a private page — the session never leaves its origin; only a short-lived, single-page capability does.

The third origin is not part of this. The site deployment API on agentkit.sbs stays separately credentialed. A device credential that can publish pages cannot deploy the site.
04 · Zoom · inside the control plane

What the owner can actually do

Sharing is not a property baked into the page — it is a row the owner can delete. The dashboard is where a page becomes reachable by someone else, and the only place where that reach is taken back.

account.agentkit.sbs/dashboard signed in · Assay New share link Invite by email

Publishing devices

eda-laptoppages:write · pages:delete
expires 2026-11-03
gondor-runnerpages:write
expires 2026-10-18

Revoking a device stops it publishing; the pages it already published keep their owner.

Your pages

pages.agentkit.sbs/7f3a9c1e4b0dprivate
pages.agentkit.sbs/c40b8e2d61afshared · 1 link, 2 emails
pages.agentkit.sbs/19d7f0a3c85bprivate
pages.agentkit.sbs/a2e6b940df13public · pre-migration
17 / 100 pages on this account

Access · selected page

owneryou · verified
share linkactive
invited2 verified emails
Revoke link Remove email

Every grant here is a row in page_access_tokens; revoking clears it.

Owner dashboard — devices, pages, and grants are the three lists an owner revokes from.

A revocation is only as good as its schema. The accounts migration was squashed during active development after production had already recorded its filename, so D1 skipped the expanded file and invite revocation failed when it tried to clear a page-scoped grant. The fix in v0.7.1 was a forward migration for page_access_tokens — and when v0.7.2 added explicit scopes, existing devices were backfilled through another immutable migration rather than an edit.
05 · The bounds

Five ceilings, and what happens at each

Ownership answers who. These answer how much and for how long — the part of the model that limits the blast radius of a credential nobody has noticed is loose yet.

BoundValueWhere it livesAt the limit
Credential lifetime 90 days issued by the control plane The publisher restarts device authorization rather than surfacing a bare 401.
Operations per device 60 publish or delete per minute D1 counter, configurable ceiling 429 carrying Retry-After. Wait that interval; do not retry in a loop.
Page size 5 MB, self-contained publish path + serving CSP Inline every asset — the CSP blocks external requests, so a page that needs the network is already broken.
Pages per account 100 account record The cap is per account, not per device — every device an owner authorizes draws on the same 100.
Page capability 10 minutes, one page issued on the account origin It expires and a fresh exchange is required; it was never valid for a second page.

Quotas and lifetimes — each one is enforced on the control plane, never by the page itself.

06 · Not covered

What page privacy deliberately does not reach

It is not repository visibility. Archiving a publish into the pages repo happens only with an explicit --git, and that repository has its own visibility — a private page never implies a private archive.
It is not retroactive. Pages published before the accounts migration stay public until someone claims or removes them.
It is not the site credential. The deployment API on agentkit.sbs is credentialed separately from anything a publishing device holds.
It is not anonymous any more. v0.7.0 replaced anonymous publishing outright: every new page has a verified Assay owner, and there is no path that creates one without an account.
It does not protect you from yourself. Republishing the same name overwrites silently, and without the pages repo clone there is no history to recover from.
07 · Checks

What has to stay true for the model to hold

A newly published page is unreadable by anyone but its owner until a share link or a verified-email invite exists for it.
The dashboard session cookie is host-only to account.agentkit.sbs, so JavaScript inside a published page cannot read it.
A page capability stops working ten minutes after issue, and presenting it for a different page fails.
Revoking a share link or removing an invited email clears the corresponding page_access_tokens row — the failure v0.7.1 fixed.
An operation requiring pages:delete is refused for a credential holding only pages:write.
The 61st operation in a minute from one device returns 429 with Retry-After, and honoring that interval succeeds.
A credential the service rejects sends the publisher back through device authorization, not to an unactionable error.
Revoking a device from the dashboard stops that machine publishing while leaving the pages it already owns intact.