feat(cloud): B2 — per-uid home isolation (AsyncLocalStorage seam) + lazy per-user birth#262
Merged
Conversation
…m, lazy per-user birth
The multi-tenant core (PLAN_ACCOUNTS_BILLING §6.2 / PLAN_IDENTITY I3): a
signed-in cloud request now runs inside homeScope with its home rebound to
$LISA_HOME/users/<uid>, and everything downstream — soul, sessions, memory,
reflections, kb, autonomy, skills — reads and writes that user's subtree.
- paths.ts: path CONSTANTS become path FUNCTIONS. Two homes, on purpose:
lisaGlobalHome() (config.env, accounts.json, devices, session secret —
never per-uid) vs lisaHome() (identity-bearing state; AsyncLocalStorage-
scoped). soul/paths.ts + kb/paths.ts converted the same way, plus every
module-scope constant that froze the home at import time (active-web-session
pointer, autonomy dir, embeddings cache, advisor state, comparisons,
scheduled dispatches, idle lock, screen-advisor config).
- server.ts: after auth, an account session enters homeScope (enterWith — the
scope survives the whole async chain); per-uid ChatCtx {session, history,
chain} so each account has its own conversation, history restore, and turn
queue (one account's long turn no longer blocks another's); prompt snapshot
cache keyed by active home; per-uid lazy birth on first sign-in (background,
deduped; chat before it completes runs on the bare prompt and the soul
arrives mid-conversation); account deletion now removes home + in-memory
contexts via the same helpers.
- Mac edition: zero behavior change — no scope is ever entered, lisaHome()
degrades to the process-global home, and the module-level global chat
context is exactly the old code path. Background schedulers (idle/reflect)
stay on the global context; per-uid autonomy waits for per-uid budgets (B4+).
- Hook env vars keep the LISA_HOME NAME (scripts depend on it); the VALUE now
follows the active scope.
Tests: new tenancy suite — scope resolution, await-survival, two tenants
writing disjoint souls, cross-tenant reads impossible, isBorn scoping. Full
suite: 1038 pass / 0 fail.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner
Author
🤖 Automated review — ✅ MERGE (nits) — isolation empirically verifiedThe filesystem home-isolation boundary (this PR's charter) is correct and verified by running the tests, not just reading:
Nits (non-blocking at reviewer scale):
|
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.
Stacked on #261. The multi-tenant core — the gate before any real cloud user beyond a reviewer.
Design
Two homes, deliberately:
lisaGlobalHome()— operator/process home:config.env,accounts.json, device tokens, session secret. Never per-uid.lisaHome()— the ACTIVE home for identity-bearing state (soul, sessions, memory, reflections, kb, autonomy…). AsyncLocalStorage-scoped: a signed-in cloud request entershomeScopewith$LISA_HOME/users/<uid>and every downstream path — across awaits — resolves there.Path constants became functions (
SESSIONS_DIR→sessionsDir()etc., 65 files) because an import-time constant freezes the home — exactly what made the codebase single-tenant. Module-scope constants that captured the home at import were functionized too (active-session pointer, autonomy dir, embeddings cache, advisor state…).Server
{session, history, chain}: each account gets its own conversation, restore-from-disk, and serialized turn queue — one account's long turn no longer blocks another's.Mac edition: zero change
No scope is ever entered →
lisaHome()degrades to the global home; the global chat context is byte-for-byte the old code path. Background schedulers (idle/reflect) stay global-only; per-uid autonomy is gated on per-uid budgets (B4+).Tests
New tenancy suite: scope resolution, await-survival, two tenants writing disjoint souls, cross-tenant reads impossible, isBorn scoping. Full suite 1038 pass / 0 fail.
Notes / follow-ups
min=max=1, which deploy.sh still pins./dataneeds no change: per-uid is just the/data/users/<uid>subtree.🤖 Generated with Claude Code