Multi-token prefix-scoped access for the Control Plane (per-user iframe embedding) - #1
Closed
logical-and wants to merge 4 commits into
Closed
Multi-token prefix-scoped access for the Control Plane (per-user iframe embedding)#1logical-and wants to merge 4 commits into
logical-and wants to merge 4 commits into
Conversation
added 4 commits
July 13, 2026 18:15
- Add src/lib/auth/tokens.ts: resolveToken (admin key -> "" prefix, HINDSIGHT_CP_TOKENS entries constant-time matched) and bankAllowed(prefix, bankId) predicate
- Rework session cookie to 3-part `<issuedAt>.<prefixB64url>.<hmac>`; verifySessionToken now returns {valid, prefix}; add getSessionPrefix; legacy 2-part tokens verify invalid
- sessionCookieOptions emits SameSite=None; Secure; Partitioned when HINDSIGHT_CP_COOKIE_SAMESITE=none, else Lax
- Update middleware verifySessionToken call sites to use .valid
- Login route resolves via resolveToken and bakes the prefix into the cookie
- Add public embed-login route: form/JSON token POST, force-recreates session, 302 to sanitized returnTo (default /dashboard)
- Update session tests for the 3-part token, prefix round-trip, and legacy-token rejection
- Add src/lib/auth/bank-guard.ts (assertBankAllowed) for body-based bank_id routes; add labelForPrefix + reuse bankAllowed from tokens.ts
- Filter GET /api/banks by session prefix; guard POST /api/banks create
- Middleware: 403 scoped sessions on path (banks/operations/stats/profile) and ?bank_id=/?agent_id= mismatches (URL-decoded); redirect foreign /banks/<id> page nav to dashboard
- Apply assertBankAllowed to recall, reflect, extract, memories/retain, memories/retain_async, files/retain, memories/[memoryId] PATCH
- Add GET /api/auth/whoami returning {isAdmin, prefix, label?}; gate create-bank + GitHub link behind isAdmin in bank-selector
- next.config: frame-ancestors CSP from HINDSIGHT_CP_FRAME_ANCESTORS (space/comma list, defaults to 'self')
- Add api.errors.auth.forbidden key; document HINDSIGHT_CP_TOKENS/COOKIE_SAMESITE/FRAME_ANCESTORS in .env.example
- Move frame-ancestors CSP out of next.config headers() (baked at build time) into middleware, stamped per-request from HINDSIGHT_CP_FRAME_ANCESTORS so the container-time env controls embedding without a rebuild - Apply the CSP header to every matched response (pages, api, redirects, 401/403, intl) via a single DRY helper; falls back to 'self' when unset - Add api.errors.auth.forbidden to all 9 non-en locale catalogs for messages parity
- embed-login: emit a relative (path-only) Location instead of an absolute URL built from request.url, so the browser resolves it against the public origin (fixes redirect to https://0.0.0.0:9999/dashboard behind nginx and inside the cross-site iframe) - middleware: build login/dashboard redirects from x-forwarded-proto/x-forwarded-host (fallback host) instead of request.url, since middleware redirects must be absolute - sanitizeReturnTo still blocks off-origin targets; relative path prevents open redirect
Owner
Author
|
Superseded by the upstream PR against vectorize-io/hindsight: vectorize-io#2698 |
logical-and
pushed a commit
that referenced
this pull request
Aug 3, 2026
…ain deadlock (vectorize-io#2948) * ci(oracle): free runner disk space before Oracle jobs The three Oracle jobs run the Oracle 23ai `free` service image, which together with the Python ML deps (torch) exhausts the runner's ~14 GB root disk. Two symptoms, one cause: - uv fails to extract a wheel with "No space left on device (os error 28)" (fast ~2 min failure), and - a near-full disk starves I/O badly enough to trip the 30-minute job timeout. test-python-client-oracle and test-typescript-client-oracle have been red on every open PR (vectorize-io#2941, vectorize-io#2942, vectorize-io#2943) from this, independent of the code under test. Reclaim ~20 GB of preinstalled tooling (the same jlumbroso action the Docker build job already uses) before the Oracle setup step. docker-images stays false here: unlike the Docker build job, the Oracle service container is already running by the time steps execute, so pruning images could disrupt it. The savings come from the tool cache, Android SDK, .NET, Haskell, large apt packages and swap. * ci(oracle): trim disk reclaim to the fast, high-yield options The first pass enabled every reclaim, which cost ~4 minutes of job time — counterproductive on jobs that are already fighting a 30-minute limit. android + dotnet + haskell + swap are a few rm -rf's worth ~16-21 GB, which is ample headroom for the Oracle image plus torch. Dropped: - large-packages: apt-get remove, costs minutes for little extra space; - tool-cache: deletes the preinstalled Python that actions/setup-python then re-downloads, making the job slower rather than faster. * fix(retain): flush entity stats after releasing the connection (Oracle hang) Retain hung forever on the Oracle backend: every retain test burned its 120s client timeout while the server sat idle, so test-python-client-oracle and test-typescript-client-oracle only ever reached ~5% of the suite before the 30-minute job limit. The server was not slow — it was deadlocked. flush_pending_stats() acquires its own connection, but it was being called while the enclosing acquire_with_retry(...) block still held one: async with acquire_with_retry(pool) as conn: # conn checked out async with conn.transaction(): # SAVEPOINT only ...write facts/entities... await entity_resolver.flush_pending_stats() # takes a 2nd connection oracledb does not autocommit and OracleConnection.transaction() is only a SAVEPOINT, so the write is committed by OracleBackend.acquire() when its block exits. Connection vectorize-io#2's `UPDATE entities ...` therefore waits on row locks held by the still-open connection #1, which cannot commit until the call returns — a circular wait. Oracle never reports ORA-00060 because session #1 is blocked in Python, not on the database, so it hangs indefinitely instead of erroring. Move the flush after the acquire block in all three call sites (streaming retain, delta retain, transfer importer), which is what its own docstring already required ("must be called AFTER the retain transaction commits") and which PostgreSQL satisfied only by accident via asyncpg autocommit. Guarded with an AST lint test rather than a behavioural one: the deadlock cannot be reproduced against PostgreSQL, which is what the suite runs on. * test(repair): retry the concurrent index drop on deadlock test_dry_run_creates_nothing still flaked in test-api shard 3. CONCURRENTLY avoids ACCESS EXCLUSIVE but still takes ShareUpdateExclusive, which conflicts with the ShareLock a fresh bank's plain CREATE INDEX holds — and that one cannot be made concurrent, since it runs inside the bank-create transaction. So _drop_bank_indexes can still be picked as the deadlock victim while another xdist worker seeds a bank: Process A waits for ShareUpdateExclusiveLock on memory_units; blocked by B. Process B waits for ShareLock on virtual transaction; blocked by A. The bank-create side already retries (vectorize-io#2943); give the drop the same treatment. The drop is idempotent, so retrying is safe.
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.
Summary
Adds multi-token, prefix-scoped access to the Control Plane so it can be embedded per-user via an iframe (each token is limited to its own bank namespace), while the existing admin access key keeps seeing every bank. A scoped token maps to a bank-id prefix (e.g.
u2), and the CP now enforces that scope at the API level, not just the UI, across every route that takes abank_id.This is built for a self-hosted deployment fronted by a reverse proxy (nginx) where the CP is embedded cross-origin from a separate app.
What changed
Token resolver and session
HINDSIGHT_CP_TOKENSJSON env ([{"token","prefix","label?"}]) parsed defensively; the adminHINDSIGHT_CP_ACCESS_KEYresolves to the empty prefix (all banks).resolveToken()does a constant-time compare across all entries; a singlebankAllowed(prefix, bankId)predicate (empty prefix = all; else exact match or<prefix>--*, sou2never matchesu20) is reused everywhere so the layers cannot drift.<issuedAt>.<prefixB64url>.<hmac>signed with the admin key.verifySessionTokenreturns{valid, prefix}; addedgetSessionPrefix. Legacy 2-part cookies simply verify as invalid (re-login).sessionCookieOptionsemitsSameSite=None; Secure; PartitionedwhenHINDSIGHT_CP_COOKIE_SAMESITE=none(for the cross-site iframe / CHIPS), otherwise the previousSameSite=Laxbehavior.Auth routes
POST /api/auth/embed-login(form-encoded or JSONtoken+ optionalreturnTo) force-recreates the session and 302s to a sanitized, relativereturnTo(defaults to/dashboard). The relative Location resolves against the public origin, so it works behind a reverse proxy and inside the cross-site iframe.GET /api/auth/whoamireturns{isAdmin, prefix, label?}for the client UI.API-level prefix isolation (three entry points)
/api/*requests get a 403 when the target bank (from/api/{banks,operations,stats,profile}/<id>path segments, URL-decoded, or?bank_id=/?agent_id=) is outside the prefix. Page navigation to a foreign/banks/<id>redirects to the dashboard.assertBankAllowedapplied to the routes that readbank_idfrom the JSON/form body: recall, reflect, extract, memories/retain, memories/retain_async, files/retain, memories/[memoryId] PATCH, banks POST.GET /api/banksfilters the returned list to the session prefix (admin passes through).UI and framing
whoami.Content-Security-Policy: frame-ancestorsdriven byHINDSIGHT_CP_FRAME_ANCESTORS, stamped in middleware per-request (not baked into the build) so the embedding origin is configurable at container runtime. Falls back to'self'when unset (never*).New env vars
HINDSIGHT_CP_TOKENS- JSON array of{ "token": string, "prefix": string, "label"?: string }. Unset/empty/invalid -> no scoped tokens. Entries missing token/prefix are ignored.HINDSIGHT_CP_COOKIE_SAMESITE- set tononefor cross-site iframe embedding (addsSecure+Partitioned; requires HTTPS). Unset keepsSameSite=Laxfor local http dev.HINDSIGHT_CP_FRAME_ANCESTORS- space- or comma-separated origins allowed to embed the CP. Falls back to'self'.All three are documented in
.env.example.Backwards compatibility
HINDSIGHT_CP_ACCESS_KEYworks unchanged: it resolves to prefix""(all banks), and admin behavior short-circuits every enforcement layer.HINDSIGHT_CP_TOKENSbehave exactly as before (single shared key).Testing and verification
tsc --noEmitclean; 45 unit tests pass (session round-trip incl. prefix, tampered/expired/legacy rejection, plus i18n message-catalog parity across all 10 locales).u2,u3,u5) see only their ownu<id>[--*]banks and no create/foreign chrome.GET /api/banks/u5--x/memories,?bank_id=u5, and a body{"bank_id":"u5"}.embed-loginlands on the real public dashboard (no internal upstream host leaking into the redirect) and the cross-site cookie survives.Build note
Built against the standalone Docker image (
docker/standalone/Dockerfile) withINCLUDE_LOCAL_MODELS=true(the deployment uses a local cross-encoder reranker) andPRELOAD_ML_MODELS=false(the model is pulled at runtime into a persistent cache volume).