feat(users): unlock and start unencrypted users at boot
Nightly Build / build (push) Successful in 8m10s
Nightly Build / build (push) Successful in 8m10s
A login is what makes an *encrypted* database readable; for an
unencrypted one it gated nothing but the runtime — the file has no key
and is already readable by this process. The cost was user-visible and
read as 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.
`Skald::new` now calls `UserManager::unlock_all_unencrypted`, which
registers the pools exactly as a login would and refuses an encrypted or
inactive user. Unlocking alone only makes the data readable, so
`wiring::spawn_unlocked_user_runtimes` then builds a `UserContext` for
each — cron, the notify queue, the hub and the per-user MCP runtime all
hang off it. That build is a background supervisor task rather than part
of `new()`: it starts every member's MCP servers inside their container,
and the HTTP listener must not wait behind it. 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 stay where they were. Authentication is untouched:
`SessionStore` sits above `UserManager`, so no HTTP request
authenticates as anyone because of this. And the auto-unlock is
deliberately not on a lazy path such as `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 bus.
`open_db` and the two unencrypted openers now share `register_unlocked`
and `open_unencrypted_file`; `open_unencrypted` (the supervision path)
still does not register its pool.
This commit is contained in:
@@ -32,9 +32,10 @@ const RECONCILE_INTERVAL: Duration = Duration::from_secs(60);
|
||||
|
||||
/// Periodically (re)spawns forwarders for bound + unlocked users.
|
||||
///
|
||||
/// This is load-bearing, not a nicety: at boot every pool is locked (§9), so the
|
||||
/// eager start-time pass spawns nothing. Users unlock later via web/phone login,
|
||||
/// and there is no "user unlocked" system event to hook. Without this loop a user
|
||||
/// This is load-bearing, not a nicety: an encrypted pool is locked at boot (§9),
|
||||
/// so the eager start-time pass skips those users. They unlock later via
|
||||
/// web/phone login, and there is no "user unlocked" system event to hook (an
|
||||
/// unencrypted one is already unlocked by then). Without this loop a user
|
||||
/// whose phone stays backgrounded would never get a forwarder — so no Inbox push
|
||||
/// would ever be armed for them. `ensure_forwarder` dedups, so this is idempotent
|
||||
/// and cheap (locked users resolve to `None` and are skipped without a build).
|
||||
|
||||
@@ -169,9 +169,9 @@ impl MobileConnectorPlugin {
|
||||
}
|
||||
|
||||
// Reconcile loop: (re)spawns forwarders for bound users as they unlock. Its
|
||||
// first tick fires immediately (covering already-unlocked users at start),
|
||||
// then it periodically catches users who log in later — there is no "user
|
||||
// unlocked" event to hook, and at boot every pool is locked (§9).
|
||||
// first tick fires immediately (covering the unencrypted users, unlocked at
|
||||
// boot), then it periodically catches encrypted ones as they log in — there
|
||||
// is no "user unlocked" event to hook (§9).
|
||||
{
|
||||
let app4 = Arc::clone(&app);
|
||||
handles.push(tokio::spawn(events::reconcile_loop(app4)));
|
||||
|
||||
Reference in New Issue
Block a user