Add portal playground mode: anonymous browser-pinned sessions - #38
Merged
Conversation
PORTAL_PLAYGROUND=1 lets one deployment serve as a public try-it instance. An unauthenticated browser navigation mints an anonymous playground-<random> principal into the ordinary portal_session cookie, so every visitor gets their own scoped sessions, files, memory, and sandbox through the same personal-scope isolation real teammates use. Non-HTML requests without a session still get 401, so only real page loads mint. /auth/login keeps the full OIDC flow for the one admin, /admin refuses anonymous sessions outright, and signing out simply starts a fresh identity on the next visit. Minting is rate-limited per client IP through the core's durable single-use claim store, failing closed to a 429 page when the claim cannot be recorded, so restarts and blue-green deploys cannot reset the budget. The claim-store helper moves from plugins/auth to the shared chassis package now that both the auth broker and the portal consume it.
Refuse to boot when playground is combined with a domain-wide cookie, an apps domain, or deployment proxying, since those surfaces never see the anon flag and would take an anonymous session at face value. Refuse out-of-range mint knobs instead of silently serving 429 to everyone: the core grants at most 64 claim slots per request and a 24-hour claim horizon, so values outside those bounds brick minting. Bucket IPv6 minting per /64 so a routed prefix cannot rotate through fresh budgets, and warn at boot when the socket address would make every visitor share one bucket behind a reverse proxy. Refuse anonymous sessions the connect and secret-drop flows so real OAuth tokens and dropped secrets cannot be attached to a throwaway principal that a cleared cookie orphans.
Contributor
Author
|
Pushed a hardening commit after two independent adversarial reviews (fresh-context review agent + Codex challenge):
Documented residual (by design, now stated plainly in the README): the |
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.
What
PORTAL_PLAYGROUND=1turns a deployment into a public try-it instance with one real admin. An unauthenticated browser navigation (aGETthat accepts HTML) mints an anonymousplayground-<random>principal, seals it into the ordinaryportal_sessioncookie, and continues. Each visitor's sessions, files, memory, keychain view, and sandbox are pinned to their browser through the samepersonal:<principal>scoping that isolates real teammates — no new isolation machinery.What playground mode deliberately does not change:
/auth/loginstill runs the full OIDC flow; production still demands the usual OIDC config. That is how the one admin signs in, and admin identity remains the core'sADMIN_GRANTS, derived per request./adminrefuses anonymous sessions outright (HTML card or JSON 403).401— the SPA's API calls ride the cookie from the first page load, and barecurlnever mints.Abuse containment
Minting is rate-limited per client IP (
clientIpOf, Fly-header aware) through the core's Postgres-backed single-use claim store — the same/v1/auth/broker/claimslot pattern the sign-in broker uses — so restarts, blue-green deploys, and multiple portal instances share one budget. If the core can't record the claim, the portal fails closed with a 429 card. Tunables:PORTAL_PLAYGROUND_MINTS_PER_IP(default 30) perPORTAL_PLAYGROUND_MINT_WINDOW_S(default 3600).A cleared cookie is a fresh principal, so the per-principal core brakes are soft; the README directs operators to the real ones —
ORG_BUDGET_USD_PER_WINDOW,BUDGET_USD_PER_WINDOW,RATE_LIMIT_PER_WINDOW, and pinning a single model via the adminbase-model/webui-modelsresources — and to run a playground as its own deployment. Known gap, stated in the README: nothing garbage-collects an abandoned visitor's scope yet.Moved
plugins/auth/src/claims.ts→plugins/chassis/src/claims.ts(the claim-store client +withinRateLimitslot math), now that both the auth broker and the portal consume it. Chassis is the sanctioned home for plugin↔core plumbing; the log prefix became a parameter.New surface
The only new rendered page is the 429 "playground is busy" card, built from the existing portal card system. Rendered from the real
playgroundBusyHtml()output (the live path needs an exhausted IP budget, so this is a direct render — same bytes):Testing
plugins/portal/test/playground.test.tsboots the real portal server against a stub core + upstream and covers: mint-on-first-visit with cookie pinning (no re-mint on return), 401 for sessionless API calls,/adminrefusal for anonymous sessions,/auth/loginstill reaching the IdP, per-IP budget exhaustion → 429 with no cookie, and fail-closed when the claim store refuses.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.