Files
Skald-Circle/dev-docs/users-auth-and-boot.md
T
Daniele 902f47ecd8
Nightly Build / build (push) Successful in 10s
docs: split CLAUDE.md into an always-loaded core plus dev-docs/
CLAUDE.md had grown to 152 KB (~21k words, ~40k tokens) and is loaded into
every coding-agent session. The cost is not the cache read, it is attention:
the rules that are genuinely invariant were drowning in the mechanics of
subsystems that most tasks never touch.

The split criterion is blast radius, not importance. A rule a change anywhere
could violate stays in CLAUDE.md — the commit rule, the production/schema
constraint, domain neutrality, the event-bus rule, the crate boundaries, and
the module map. The mechanism of one subsystem moves to dev-docs/, opened on
entry to that subsystem via a routing table at the top of CLAUDE.md.

Nothing was rewritten: every section was moved verbatim by line range and
verified line-by-line against the original. The only edits are cross-reference
repairs ("see the DB section" -> a link), the promotion of headings in the
extracted files, and a condensed "Current state" whose full text now lives in
dev-docs/users-auth-and-boot.md.

CLAUDE.md: 152 KB -> 31 KB. Twelve subsystem files plus an index under
dev-docs/, which now carries the same standing rule as docs/ and CHANGELOG.md:
a change to a subsystem updates its dev-doc in the same change.

No CHANGELOG entry: this is documentation for coding agents with no observable
effect on the application.
2026-08-24 18:04:43 +01:00

4.8 KiB

Skald dev-docs — architectural reference for coding agents. Index: README.md · Entry point: ../CLAUDE.md

Read this when: you touch login, sessions, UserManager, UserContext, encryption of a user DB, or what boot unlocks and spawns.


Users, auth, crypto & boot

crates/skald-core/src/users/

UserManager (§11): user directory CRUD on system.db, credential check, and the map userid → SqlitePool of unlocked databases. The pool is the unlock token — its connect options carry the DEK as SQLCipher's raw key, so an open pool means the key is in RAM (§9) and dropping it re-locks. Knows nothing about cookies: whatever maps an HTTP session to a user id sits above it. A login is what unlocks an encrypted file only — see the boot-unlock section below

crates/skald-core/src/crypto/

Envelope encryption (§4/§5.1). A random 256-bit DEK encrypts {userid}.db; users.database_password holds it sealed with AES-256-GCM under Argon2id(password, salt). The AEAD tag is the password verifier — one derivation both authenticates and yields the key, and no second hash sits in the admin-readable DB. Cleartext users store the Argon2id output directly, compared constant-time. Argon2 runs in spawn_blocking behind a 2-permit semaphore (256 MiB per derivation)

Runtime state

UserManager (§11) is now consumed. Login exists (crates/skald-core/src/auth/mod.rs: SessionStorelogin/user_of/logout plus revoke_user, the admin-side "drop every session of this user" used by Skald::revoke_user_runtime; the deny-by-default middleware is src/frontend/api/guard.rs, whose require_auth maps token → id and does not re-read the row, which is exactly why revocation must be pushed rather than polled; first admin created by skald-setup), and the per-user owner-bound runtime is UserContext (crates/skald-core/src/skald/user_context.rs) — resolved by Skald::user_context / the frontend's require_context, keyed off UserManager::pool_of, and carrying its own CancellationToken (a child of the instance one) so a single user's cron/hub/MCP loops can be stopped without touching anyone else's. The frontend owner call-sites (WS, sessions, inbox, approval-pending, projects, uploads, run-context, cron) route through the per-user pool; dev/stats read llm_requests — a registry table — from system.db, which is correct. The "owner-without-a-user" question resolved to there isn't one: every owner content belongs to a logged-in user (the admin included). The global owner-bound bundles (Conversation/Tasks: the "ownerless" ChatSessionManager, ChatHub, cron TaskManager) are still constructed but inert — their loops never spawn and nothing consumes their accessors; removing them is pending follow-on work (kept for now because RunContextManager shares the Conversation bundle and is used, being registry-backed). See blueprint §19.

Boot unlocks the databases that have no key, and starts their runtimes. §9 ties readability to a login, and for an encrypted file that is the mechanism — the key only exists once the password has been typed. For an unencrypted one it was a rule with nothing behind it: the data is already readable by anything in this process, so the only thing the login gated was the runtime. The cost was user-visible and looked like a bug — after every restart the Telegram bot answered "your account is locked, log in via the web app", cron fired nothing and no background agent ran, until a human opened the SPA. So Skald::new calls UserManager::unlock_all_unencrypted (which registers the pools exactly as a login would, refusing an encrypted or inactive user), and wiring::spawn_unlocked_user_runtimes then builds a UserContext for each — unlocking only makes the data readable; cron, the notify queue, the hub and the per-user MCP runtime all hang off the context, so an instance is working only once those exist. That build is a background supervisor task, not part of new(): it starts every member's MCP servers inside their container, and the HTTP listener must not wait behind that. The same two steps run per user off the lifecycle bus (UserCreated, UserActiveChanged{active:true}, after the container ensure) so a member created at runtime does not wait for the next restart. Two boundaries are untouched and worth stating: authentication is unaffected (SessionStore sits above UserManager; no HTTP request authenticates as anyone because of this), and open_unencrypted still exists for the supervision path, still deliberately not registering its pool. The auto-unlock is deliberately not on a lazy path (e.g. inside Skald::user_context): revoke_user_runtime locks a pool synchronously and expects nothing to re-open it, so the writers of that map stay boot, login, and the lifecycle bus.