Add a browser operator console to the durable-agent demo - #718
Open
7nohe wants to merge 4 commits into
Open
Conversation
examples/agents was API-only: the RFC 0017 approval loop could only be driven with curl and a bearer token. It now also serves an Inertia console showing the tickets, the pending approvals and the agent's own report on one page, with Approve, Reject and Run sweep. The JSON surface is unchanged. Routes carrying .agent() metadata keep returning JSON, since guren check warns about an agent route answering with an Inertia response, and tool:list output is byte-identical. The two operator surfaces answer approvals through one shared module, so a second copy of "which rows are answerable" cannot hand the agent a grant a human gave once; only presentation differs. Login spends the same bearer token the API takes: users carries no password column, and scrypt in a request path does not fit the Workers Free CPU budget. Sessions are database-backed because a Worker answers the login redirect and the page it lands on from different isolates.
The console shipped its own 141-line stylesheet with invented colours and a hardcoded dark scheme. It now imports the same guren.css token sheet a scaffolded app gets, styles with the g-* Tailwind utilities over it, and follows the reader's light/dark preference. The system's rules are the point: one crimson fill per screen, spent on Run sweep, with Sign out, Approve and Reject as outlines; signal tokens for the status chips; machine-issued values in mono; the ember tick once, on the page title. Login reuses the class spellings the generators emit. The token sheet now has three copies in the repo, so the byte-identity test covers all of them. An inline row no longer wraps, which kept a long ticket title's status chip off its own line.
`.guren/pages.gen.ts` is gitignored, and the demo's controllers and Inertia entry import it now that the app renders pages. CI's only codegen step ran the blog example, so `typecheck:example-agents` failed on a fresh checkout with TS2307 while passing in any tree where the demo had been run. `build:routes` now covers both examples, which is also what makes a clean clone's `bun run typecheck` reproducible.
The approval title fragment was spelled out in both approval panels and the disabled tail on all four buttons; both are now one name. The alert tone map moves beside the chip one at module scope rather than being rebuilt per render, and a single-use outline-button constant that named only one of the screen's three outline buttons is inlined. Rendered markup is byte-identical across populated, empty and no-sweep shapes, which is what keeps console.png accurate.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
examples/agentsdemonstrates the RFC 0017 durable agent runtime, but it wasAPI-only: the approval loop could only be driven with
curland a bearer token.It now also serves an Inertia console that shows the tickets, the pending
approvals and the agent's own report on one page, with Approve, Reject and
Run sweep.
The JSON surface is unchanged
Routes carrying
.agent()metadata keep returning JSON —guren checkwarnsabout an agent route answering with an Inertia response, because a rendered page
is not a tool result.
tool:listoutput is byte-identical tomain.The console gets its own actions under
/console/*, so the bare/approvals/...paths the README's
curlwalkthrough uses keep working. Both surfaces answerapprovals through one shared module (
app/Services/approvals.ts): a second copyof "which rows are answerable" is the copy that hands the agent a grant a human
gave once. Only presentation differs — the console redirects and flashes the
refusal, the API returns a status code.
Login and sessions
Login spends the same bearer token the API takes, verified with
verifyApiToken.userscarries no password column, and scrypt in a request path does not fit theWorkers Free CPU budget (10 ms per invocation against a measured 20-31 ms cold
boot).
SessionGuard.login()rotates the session id, so a pre-auth sessioncannot be fixated.
Sessions use
DatabaseSessionStoreover a newsessionstable: on Workers thelogin redirect and the page it lands on are answered by different isolates, so
per-isolate memory would drop the session.
cookieSecurecannot be decided byNODE_ENVhere —wrangler.jsoncdefines itto
"production"at bundle time, local or not. The app reads its ownTRIAGER_INSECURE_COOKIES, through getters, because wranglervarsland afterthe module evaluates.
Route wiring and the CLI flags
routes/web.tsis now the app's single entry registrar and callsregisterApiRoutes(router), which is what letsguren checkreachroutes/api.tsfrom the entry
createApp({ routes })names. Consequently:auditdropped its--routes routes/api.tsflag and now covers 31 checksinstead of 23 — the console's six new routes are the new attack surface.
codegenwas repointed toroutes/web.ts; it needs the whole graph.checkandtool:listcarried no flag and still need none.CSRF
No configuration was needed in either direction.
createApp({ auth })mounts CSRFover the whole app; the console carries a session cookie and is verified, while a
bearer request that carries no cookies is exempt on its own terms, as is a request
carrying the agent principal the pipeline installed. The one visible change: a
mutating request with neither a bearer header nor a CSRF token now answers
403rather than
401, because CSRF runs ahead of the auth middleware.Free-plan budget
One console render costs 6 D1 queries (7 when the session is written back rather
than touched), against the 50-per-invocation Free-plan ceiling. Ticket and
approval listings are bounded at 40 / 50 / 20 rows.
Also fixed
resolveApprovalderived its refusal status without the pending-only guard thatpresentApprovalhas, so an approval that was answered and then passed its windowwas refused as "expired before it was answered". Both now share one
statusAt().The HTTP status was, and stays, 409.
Scope
examples/agents/**anddocs/**only — nothing underpackages/, so nochangeset and nothing to release.
Verification
build,typecheck,lint,audit:core-first,audit:docs— all green.examples/agents:test27 pass,check15 passed / 6 warnings / 0 failures,audit31 passed / 1 pre-existing advisory warning / 0 failures.wrangler dev --local, full loop: sign in, Run sweep, two approvals appear,Approve one and Reject the other, wait past the ledger's first backoff without
sweeping, then the approved ticket closes by alarm and the rejected one appears
under
declined.examples/agents/console.pngis that state.bun run dev: sign in and console render re-confirmed after the finalsimplification pass, which was separately checked to leave the rendered markup
unchanged across populated, empty and no-sweep prop shapes.