feat: sync first-run key bootstrap and add key regeneration flow#44
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughBoot sequence changed to initialize the SQLite DB first, derive identity keys from the DB, then initialize/load settings (possibly generating a colocated Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant UI as Mostrix UI
participant Main as main.rs
participant DB as SQLite
participant Settings as settings.rs
participant Backup as Backup Popup
Note over Main,Settings: First-run bootstrap with DB-derived keys
User->>UI: Start app
UI->>Main: launch
Main->>DB: db::init_db()
Main->>DB: User::get_identity_keys()
DB-->>Main: identity keys
Main->>Settings: init_settings(Some(identity keys))
Settings->>Settings: generate settings.toml (derive nsec from DB keys)
Settings-->>Main: InitSettingsResult(did_generate_new_settings_file=true)
Main->>DB: User::get() (mnemonic)
DB-->>Main: mnemonic
Main->>Backup: show BackupNewKeys(mnemonic)
Backup->>User: display 12-word mnemonic (save & dismiss)
sequenceDiagram
actor User
participant UI as Mostrix UI
participant Handler as KeyHandler
participant Tasks as Async Tasks
participant DB as SQLite
participant Settings as settings.rs
participant Main as main.rs
Note over UI,Tasks: Runtime key rotation flow
User->>UI: Select "Generate New Keys"
UI->>Handler: show ConfirmGenerateNewKeys
User->>UI: Confirm (Enter/Y)
UI->>Handler: generate mnemonic & derive nsec
Handler->>Tasks: spawn_key_rotation_task(mnemonic, derived_nsec)
Tasks->>DB: User::replace_all_atomic(mnemonic)
DB-->>Tasks: success
Tasks->>Settings: load_settings_from_disk()
Tasks->>Settings: update nsec_privkey/admin_privkey
Tasks->>Settings: save_settings()
Tasks-->>Handler: send Ok(mnemonic) via key_rotation channel
Handler->>UI: BackupNewKeys(mnemonic) (set backup_requires_restart=true)
User->>UI: dismiss backup -> app.pending_key_reload=true
UI->>Main: main triggers apply_pending_key_reload => reload settings, abort/respawn listeners, reset session
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (3)
src/ui/key_handler/esc_handlers.rs (1)
121-124: Consider confirming ESC dismissal for mnemonic backup.Line 121 lets
BackupNewKeysclose immediately on Esc. A secondary confirmation (e.g., “I saved my mnemonic”) would reduce accidental loss of the only displayed backup.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/key_handler/esc_handlers.rs` around lines 121 - 124, The ESC handler for UiMode::BackupNewKeys currently closes the mnemonic popup immediately; instead require an explicit confirmation before dismissing—modify the branch handling UiMode::BackupNewKeys in esc_handlers.rs to, on first ESC press, transition to a new confirmation state (e.g., UiMode::ConfirmMnemonicDismiss or set a flag on the app like app.awaiting_mnemonic_confirm) and only set app.mode = default_mode when the user explicitly confirms (e.g., via a confirm action or a second ESC mapped to confirm in the new state); update related UI rendering and input handling to show the “I saved my mnemonic” confirmation prompt and to handle the confirm/cancel actions accordingly so accidental dismissal is prevented.src/ui/constants.rs (1)
5-6: Move runtime config defaults out ofui::constants.Line 5 defines a settings/bootstrap default in a UI-focused module. If
src/settings.rsdepends on this, it introduces avoidable cross-layer coupling. Consider relocating it to a shared config/settings constants module.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/constants.rs` around lines 5 - 6, MOSTRO_STAGING_PUBKEY is a runtime/bootstrap default living in the UI module which creates cross-layer coupling; move the constant out of src/ui/constants.rs into a new or existing shared settings/constants module (e.g., src/settings/constants.rs or add to src/settings.rs), export it with the same name (MOSTRO_STAGING_PUBKEY) and update all references/imports (including settings-related code that currently depends on ui::constants::MOSTRO_STAGING_PUBKEY) to import it from the new shared module so UI code still compiles but bootstrap/settings code no longer depends on the UI layer.src/settings.rs (1)
204-227: Remove unusednpubcomputation.The
_npubvariable is computed viato_bech32()in both branches but never used. This wastes CPU cycles and allocations. Simplify to only derivensec.Proposed fix
- // On first launch, derive the user `nsec` from the database identity/index-0 key - // (generated from the mnemonic stored in `users`), so DB keys and settings match. - let (nsec, _npub) = - if let Some(identity) = identity_keys { - let sk = identity.secret_key(); - let nsec = sk.to_bech32().map_err(|e| { - anyhow::anyhow!("Failed to encode identity Nostr secret key: {}", e) - })?; - let npub = identity.public_key().to_bech32().map_err(|e| { - anyhow::anyhow!("Failed to encode identity Nostr public key: {}", e) - })?; - (nsec, npub) - } else { - // Fallback: preserve older behavior if identity keys aren't provided. - let keys = Keys::generate(); - let sk = keys.secret_key(); - let nsec = sk.to_bech32().map_err(|e| { - anyhow::anyhow!("Failed to encode generated Nostr secret key: {}", e) - })?; - let npub = keys.public_key().to_bech32().map_err(|e| { - anyhow::anyhow!("Failed to encode generated Nostr public key: {}", e) - })?; - (nsec, npub) - }; + // On first launch, derive the user `nsec` from the database identity/index-0 key + // (generated from the mnemonic stored in `users`), so DB keys and settings match. + let nsec = if let Some(identity) = identity_keys { + identity.secret_key().to_bech32().map_err(|e| { + anyhow::anyhow!("Failed to encode identity Nostr secret key: {}", e) + })? + } else { + // Fallback: preserve older behavior if identity keys aren't provided. + Keys::generate().secret_key().to_bech32().map_err(|e| { + anyhow::anyhow!("Failed to encode generated Nostr secret key: {}", e) + })? + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/settings.rs` around lines 204 - 227, The code is computing an unused `_npub` (via `to_bech32()` on `identity.public_key()` and `keys.public_key()`) which wastes work; update the initialization that currently binds `(nsec, _npub)` to only derive and return `nsec` and remove all `to_bech32()` calls for public keys. Specifically, in the branches that use `identity_keys` (where `identity.secret_key()` is converted to `nsec`) and the fallback that calls `Keys::generate()` (where `keys.secret_key()` is converted to `nsec`), drop the `npub`/`_npub` computation and simplify the binding to just `let nsec = ...` so only `nsec` is produced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@README.md`:
- Around line 35-43: Clarify the precedence and which file Mostrix reads/writes
after first run by adding an explicit rule: if a settings.toml is present next
to the executable (the "colocated settings.toml"), Mostrix uses and updates that
file; otherwise it uses and updates ~/.mostrix/settings.toml. Reference the two
symbols settings.toml (colocated) and ~/.mostrix/settings.toml in the same
paragraph so operators know which file takes precedence and which file to edit.
In `@src/main.rs`:
- Around line 205-208: The code currently drops errors from User::get(&pool)
when init.did_generate_new_settings_file is true, so the first-run backup flow
can be skipped; change the if-let to explicitly handle the Err case: call
User::get(&pool).await with a match (or try/if let_err), and on Ok(user) set
app.mode = UiMode::BackupNewKeys(user.mnemonic), but on Err log the error and
still transition into the backup flow (e.g., set app.mode to a backup/error UI
or BackupNewKeys with a safe fallback) so the mnemonic backup path is not
silently skipped; update the block around init.did_generate_new_settings_file,
User::get, and app.mode accordingly.
In `@src/models.rs`:
- Around line 67-69: The delete_all call is non-atomic and can leave the users
table empty if the subsequent insert (from User::new in
src/ui/key_handler/enter_handlers.rs) fails; change this to an atomic replace by
performing the DELETE and INSERT inside a single sqlx transaction. Implement
either a new API like User::replace_all_atomic(pool: &SqlitePool, new_user:
&User) that begins a sqlx::Transaction, runs the DELETE and then the INSERT(s)
and commits (rolling back on error), or change delete_all to accept a &mut
sqlx::Transaction so the handler can start a transaction, call delete_all(&mut
tx) and then do the User insert(s) before committing; ensure errors propagate so
failures rollback and identity is not lost.
In `@src/ui/app_state.rs`:
- Around line 40-43: UiMode::BackupNewKeys currently stores mnemonic as an
ordinary String (and UiMode derives Clone), which allows sensitive seed words to
be duplicated in memory; replace the String variant with a zeroizing secret type
(e.g., zeroize::Zeroizing<String> or secrecy::SecretString) in the UiMode enum
(BackupNewKeys) and update any code that constructs or reads this variant to use
the secret wrapper, ensuring the enum's Clone/Copy behavior is adjusted so
secrets are not trivially cloned (either avoid deriving Clone for UiMode or
implement Clone to forbid/clamp cloning of secret variants) and that secrets are
explicitly zeroized/dropped when done.
In `@src/ui/generate_keys_popup.rs`:
- Around line 59-93: The comment says wrapping must be disabled but the mnemonic
Paragraph enables it; remove the .wrap(...) call on the Paragraph created for
mnemonic_lines (the Paragraph::new(mnemonic_lines) rendered to chunks[3]) so it
renders without wrapping, ensuring mnemonic_lines is displayed as a single
space-separated line and preventing clipping artefacts.
In `@src/ui/key_handler/enter_handlers.rs`:
- Around line 197-231: The code currently spawns a background task that deletes
the old user, writes a new User (User::delete_all, User::new) and updates
settings (crate::settings::load_settings_from_disk / save_settings) but
immediately sets app.mode = UiMode::BackupNewKeys(mnemonic) before those
operations complete; change this so the rotation is committed before showing the
backup: perform the DB/settings work synchronously (await the operations) or
await the tokio::spawn JoinHandle and check its result, only setting app.mode =
UiMode::BackupNewKeys(mnemonic) if User::new and save_settings succeeded; also
avoid deleting the existing user before ensuring the new row is created (create
new first or wrap in a transaction) and log/propagate failures so the UI does
not show the mnemonic on failure.
- Around line 234-237: The current branch handling UiMode::BackupNewKeys only
resets app.mode to default_mode but leaves the in-memory Client and admin key
material unchanged, allowing continued operation under stale identity; fix by
enforcing a required restart or replacing in-memory credentials: after closing
the backup popup (UiMode::BackupNewKeys), either (A) set a restart-required flag
on the app (e.g., app.must_restart or app.restart_required) and ensure all
action handlers check this flag and block normal operations until a restart, or
(B) trigger an immediate shutdown/quit flow (e.g., call the app shutdown/restart
routine) so the process exits and restarts, or (C) reload/replace the in-memory
Client/admin key material from disk (refresh the Client instance) before
returning to default_mode; pick one approach and update the
UiMode::BackupNewKeys handling and any central action dispatchers to respect the
restart_required flag or shutdown trigger so the user cannot continue using
stale credentials.
---
Nitpick comments:
In `@src/settings.rs`:
- Around line 204-227: The code is computing an unused `_npub` (via
`to_bech32()` on `identity.public_key()` and `keys.public_key()`) which wastes
work; update the initialization that currently binds `(nsec, _npub)` to only
derive and return `nsec` and remove all `to_bech32()` calls for public keys.
Specifically, in the branches that use `identity_keys` (where
`identity.secret_key()` is converted to `nsec`) and the fallback that calls
`Keys::generate()` (where `keys.secret_key()` is converted to `nsec`), drop the
`npub`/`_npub` computation and simplify the binding to just `let nsec = ...` so
only `nsec` is produced.
In `@src/ui/constants.rs`:
- Around line 5-6: MOSTRO_STAGING_PUBKEY is a runtime/bootstrap default living
in the UI module which creates cross-layer coupling; move the constant out of
src/ui/constants.rs into a new or existing shared settings/constants module
(e.g., src/settings/constants.rs or add to src/settings.rs), export it with the
same name (MOSTRO_STAGING_PUBKEY) and update all references/imports (including
settings-related code that currently depends on
ui::constants::MOSTRO_STAGING_PUBKEY) to import it from the new shared module so
UI code still compiles but bootstrap/settings code no longer depends on the UI
layer.
In `@src/ui/key_handler/esc_handlers.rs`:
- Around line 121-124: The ESC handler for UiMode::BackupNewKeys currently
closes the mnemonic popup immediately; instead require an explicit confirmation
before dismissing—modify the branch handling UiMode::BackupNewKeys in
esc_handlers.rs to, on first ESC press, transition to a new confirmation state
(e.g., UiMode::ConfirmMnemonicDismiss or set a flag on the app like
app.awaiting_mnemonic_confirm) and only set app.mode = default_mode when the
user explicitly confirms (e.g., via a confirm action or a second ESC mapped to
confirm in the new state); update related UI rendering and input handling to
show the “I saved my mnemonic” confirmation prompt and to handle the
confirm/cancel actions accordingly so accidental dismissal is prevented.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 435e4e68-845b-43cb-abfb-4efd024e1be3
📒 Files selected for processing (19)
README.mddocs/KEY_MANAGEMENT.mddocs/SETTINGS_ANALYSIS.mddocs/STARTUP_AND_CONFIG.mddocs/TUI_INTERFACE.mdsrc/main.rssrc/models.rssrc/settings.rssrc/ui/admin_key_confirm.rssrc/ui/app_state.rssrc/ui/constants.rssrc/ui/draw.rssrc/ui/generate_keys_popup.rssrc/ui/key_handler/enter_handlers.rssrc/ui/key_handler/esc_handlers.rssrc/ui/key_handler/mod.rssrc/ui/key_handler/navigation.rssrc/ui/mod.rssrc/ui/tabs/settings_tab.rs
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
src/main.rs (1)
505-535:⚠️ Potential issue | 🟠 MajorRefresh admin key material during the soft reload.
This reload only rebuilds
clientfromnsec_privkey.admin_chat_keysandadmin_chat_pubkeyare computed once before the loop and never refreshed here, so afterGenerate New Keysin admin mode the dispute/chat flows keep using the old admin key until a full process restart.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main.rs` around lines 505 - 535, The reload path only rebuilds client from nsec_privkey but doesn't refresh admin_chat_keys and admin_chat_pubkey, so recompute those values when you perform the soft reload (the same place you create new_client and call PublicKey::from_str on latest_settings.mostro_pubkey); after creating new_client and before spawning message_listener_handle, derive admin_chat_keys and admin_chat_pubkey again from the updated nsec_privkey (and any config used originally) and assign them to the same variables used by the running app so subsequent admin/chat flows use the new keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main.rs`:
- Around line 486-503: The abort of message_listener_handle currently happens
before validating and fully constructing the replacement session, which can
leave the app without a listener on error; change the flow in the pending key
reload path so you first call crate::settings::load_settings_from_disk(), parse
latest_settings.nsec_privkey into Keys, construct
Client::new(new_identity_keys), and await new_client.add_relay(...) for all
relays to validate the new session successfully, and only then call
message_listener_handle.abort() and swap to the new runtime; alternatively, if
you prefer to keep the abort earlier, ensure you restart the previous listener
on every error path (where new_client.add_relay fails) and set app.mode
(UiMode::OperationResult(OperationResult::Error(...))) accordingly to avoid
leaving the app without a listener.
In `@src/ui/generate_keys_popup.rs`:
- Around line 59-92: The mnemonic_lines is rendered as one fixed-length row (via
Layout/Constraint::Length and Paragraph::new) which causes clipping for long
12-word seeds; update the UI code that builds mnemonic_lines and the
Layout/chunks usage in generate_keys_popup.rs to split the mnemonic into
multiple rows (e.g., two rows of 6 words each) or compute the required height
from mnemonic.split_whitespace().len() and use Constraint::Length dynamically
instead of the hard-coded Constraint::Length(2) for the mnemonic chunk; then
render multiple Paragraphs or a single Paragraph with newline-separated word
groups so the full seed is always visible (adjust chunks index references such
as chunks[3] accordingly).
In `@src/ui/key_handler/async_tasks.rs`:
- Around line 167-178: The current flow calls
crate::models::User::replace_all_atomic(...) which commits the new mnemonic to
the DB before crate::settings::save_settings(&s) updates the settings file, so a
file-write failure leaves DB and settings.toml out of sync; change the sequence
to guarantee atomicity across both stores by using a coordinated commit: open a
DB transaction instead of calling replace_all_atomic (or add a new
replace_all_transaction API), write the new settings to a temporary file (not
overwriting settings.toml), then if the temp file write succeeds commit the DB
transaction and atomically rename the temp file to replace settings.toml; if any
step fails rollback the DB transaction and remove the temp file. Update the code
around User::replace_all_atomic, load_settings_from_disk, and save_settings to
implement this transactional pattern (or add helper functions like
replace_all_with_settings_commit) so DB commit only happens after file is safely
written and rename completed.
- Around line 19-30: Background tasks currently call SETTINGS.get() (OnceLock)
and therefore keep using the old snapshot after a soft-reload; instead stop
reading SETTINGS inside the task and use the runtime-provided settings: change
the task signature to accept a &Settings (or a SettingsRef) passed in by the
reloader/context, and propagate that instance into calls like
crate::util::send_new_order and any other calls that currently use
SETTINGS.get(); update code paths in async_tasks.rs (the block that reads
SETTINGS and the similar block at lines 52-66) to remove SETTINGS.get() lookups,
use the passed settings reference, and send errors via result_tx as before so
the task uses the currently swapped settings.
In `@src/ui/key_handler/enter_handlers.rs`:
- Around line 166-171: The ConfirmGenerateNewKeys popup
(UiMode::ConfirmGenerateNewKeys) isn't wired to the global 'y'/'n' shortcuts;
update the enter key handler so that when app.mode is
UiMode::ConfirmGenerateNewKeys you call the same confirmation helpers as
Enter/Esc do—invoke handle_confirm_key(...) for the YES case and
handle_cancel_key(...) for the NO case (or forward the selected_button boolean
into those functions) so 'y' and 'n' trigger the same code path as Enter/Esc; do
the same change for the other occurrence around the UiMode match at the 663-678
region so both instances reference handle_confirm_key and handle_cancel_key from
confirmation.rs.
---
Duplicate comments:
In `@src/main.rs`:
- Around line 505-535: The reload path only rebuilds client from nsec_privkey
but doesn't refresh admin_chat_keys and admin_chat_pubkey, so recompute those
values when you perform the soft reload (the same place you create new_client
and call PublicKey::from_str on latest_settings.mostro_pubkey); after creating
new_client and before spawning message_listener_handle, derive admin_chat_keys
and admin_chat_pubkey again from the updated nsec_privkey (and any config used
originally) and assign them to the same variables used by the running app so
subsequent admin/chat flows use the new keys.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: eec9c88b-1208-4859-8e1a-b40f11fe37b2
📒 Files selected for processing (12)
README.mdsrc/main.rssrc/models.rssrc/ui/app_state.rssrc/ui/draw.rssrc/ui/generate_keys_popup.rssrc/ui/key_handler/async_tasks.rssrc/ui/key_handler/confirmation.rssrc/ui/key_handler/enter_handlers.rssrc/ui/key_handler/esc_handlers.rssrc/ui/key_handler/mod.rssrc/ui/key_handler/user_handlers.rs
✅ Files skipped from review due to trivial changes (2)
- src/ui/draw.rs
- README.md
🚧 Files skipped from review as they are similar to previous changes (2)
- src/models.rs
- src/ui/app_state.rs
There was a problem hiding this comment.
♻️ Duplicate comments (4)
src/main.rs (2)
491-568:⚠️ Potential issue | 🟠 MajorValidate the replacement session before aborting the listener.
Line 493 aborts the only
listen_for_order_messagestask before the new settings, keys, relays, and Mostro pubkey are all known-good. Any failure later in this branch leaves the old runtime without a message listener.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main.rs` around lines 491 - 568, Do not abort message_listener_handle before confirming the replacement session is valid: first call crate::settings::load_settings_from_disk(), parse latest_settings.nsec_privkey into Keys, instantiate Client::new, add relays via new_client.add_relay, parse latest_settings.mostro_pubkey with PublicKey::from_str and (optionally) call new_client.connect(), and spawn a new listen_for_order_messages task to verify it starts successfully; only after all of those succeed should you abort the old message_listener_handle, swap in client, mostro_pubkey, message_listener_handle, and clear_runtime_session_state, leaving the existing listener running on any failure so the old runtime remains operational.
196-207:⚠️ Potential issue | 🟠 MajorAdmin key reload is still incomplete.
This soft-reload path only reparses
latest_settings.nsec_privkeyand swapsclient.admin_chat_keys/admin_chat_pubkeyare computed once at startup and never refreshed, so the admin “Generate New Keys” flow still keeps admin chat send/fetch on stale credentials after the “Keys reloaded” success state.Also applies to: 295-299, 497-541
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main.rs` around lines 196 - 207, The admin key reload path only reparses latest_settings.nsec_privkey and swaps client, but never recomputes admin_keys/admin_chat_pubkey so the admin chat still uses stale credentials; update the reload logic to re-run Keys::parse on settings.admin_privkey and recompute admin_keys and admin_chat_pubkey (using Keys::public_key) whenever settings are reloaded and the client is swapped, and ensure the new admin_keys/admin_chat_pubkey are stored/assigned to the same location the rest of the code reads from (the same variable/field that was initialized at startup).src/ui/key_handler/async_tasks.rs (2)
222-233:⚠️ Potential issue | 🔴 CriticalUser key rotation can still commit only half of the new identity.
Per
src/models.rs:72-103,User::replace_all_atomic()commits the new mnemonic to SQLite before this code reachessave_settings(). If the settings write fails, the DB andsettings.tomldiverge immediately, which breaks the new sync guarantee.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/key_handler/async_tasks.rs` around lines 222 - 233, The current flow may leave DB and settings.toml inconsistent because User::replace_all_atomic(mnemonic, &pool) writes to SQLite before save_settings(&s) completes; make the update atomic by writing settings and DB in a rollback-safe sequence: load current settings with load_settings_from_disk(), keep a copy of the original settings, update s.nsec_privkey/admin_privkey and call save_settings(&s) first, then call User::replace_all_atomic(mnemonic.clone(), &pool); if replace_all_atomic fails, restore the original settings via save_settings(&original_s) to revert the file; use the same local variables (s, original_s, mnemonic, derived_nsec, is_user_mode) and functions (load_settings_from_disk, save_settings, User::replace_all_atomic) to implement this rollback pattern.
66-95:⚠️ Potential issue | 🟠 MajorThese order tasks still read the pre-reload settings snapshot.
The soft-reload path in
src/main.rs:491-541only swaps the localclientand disk-backed settings;SETTINGSnever changes. These spawners still pull from thatOnceLock, so any setting-dependent behavior insend_new_order/take_orderwill keep using the original snapshot after a key reload.Also applies to: 97-137
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/key_handler/async_tasks.rs` around lines 66 - 95, spawn_send_new_order_task (and the sibling spawn for take_order) currently reads the immutable OnceLock SETTINGS inside the spawned task, so tasks keep using the pre-reload snapshot; change this so the task obtains the up-to-date settings at execution time instead of calling SETTINGS.get(): either (A) add a new parameter to spawn_send_new_order_task/spawn_take_order_task that accepts a settings-provider (e.g., Arc<RwLock<Settings>> or a closure) which main updates on soft-reload and use that inside the async block when calling send_new_order/take_order, or (B) remove the SETTINGS.get() call from the task and call the disk-backed loader function that main swaps (e.g., crate::config::load_current_settings or equivalent) inside the async block so send_new_order and take_order receive the current settings; update both spawn_send_new_order_task and the 97-137 sibling accordingly and ensure you pass the obtained settings into crate::util::send_new_order / take_order.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/main.rs`:
- Around line 491-568: Do not abort message_listener_handle before confirming
the replacement session is valid: first call
crate::settings::load_settings_from_disk(), parse latest_settings.nsec_privkey
into Keys, instantiate Client::new, add relays via new_client.add_relay, parse
latest_settings.mostro_pubkey with PublicKey::from_str and (optionally) call
new_client.connect(), and spawn a new listen_for_order_messages task to verify
it starts successfully; only after all of those succeed should you abort the old
message_listener_handle, swap in client, mostro_pubkey, message_listener_handle,
and clear_runtime_session_state, leaving the existing listener running on any
failure so the old runtime remains operational.
- Around line 196-207: The admin key reload path only reparses
latest_settings.nsec_privkey and swaps client, but never recomputes
admin_keys/admin_chat_pubkey so the admin chat still uses stale credentials;
update the reload logic to re-run Keys::parse on settings.admin_privkey and
recompute admin_keys and admin_chat_pubkey (using Keys::public_key) whenever
settings are reloaded and the client is swapped, and ensure the new
admin_keys/admin_chat_pubkey are stored/assigned to the same location the rest
of the code reads from (the same variable/field that was initialized at
startup).
In `@src/ui/key_handler/async_tasks.rs`:
- Around line 222-233: The current flow may leave DB and settings.toml
inconsistent because User::replace_all_atomic(mnemonic, &pool) writes to SQLite
before save_settings(&s) completes; make the update atomic by writing settings
and DB in a rollback-safe sequence: load current settings with
load_settings_from_disk(), keep a copy of the original settings, update
s.nsec_privkey/admin_privkey and call save_settings(&s) first, then call
User::replace_all_atomic(mnemonic.clone(), &pool); if replace_all_atomic fails,
restore the original settings via save_settings(&original_s) to revert the file;
use the same local variables (s, original_s, mnemonic, derived_nsec,
is_user_mode) and functions (load_settings_from_disk, save_settings,
User::replace_all_atomic) to implement this rollback pattern.
- Around line 66-95: spawn_send_new_order_task (and the sibling spawn for
take_order) currently reads the immutable OnceLock SETTINGS inside the spawned
task, so tasks keep using the pre-reload snapshot; change this so the task
obtains the up-to-date settings at execution time instead of calling
SETTINGS.get(): either (A) add a new parameter to
spawn_send_new_order_task/spawn_take_order_task that accepts a settings-provider
(e.g., Arc<RwLock<Settings>> or a closure) which main updates on soft-reload and
use that inside the async block when calling send_new_order/take_order, or (B)
remove the SETTINGS.get() call from the task and call the disk-backed loader
function that main swaps (e.g., crate::config::load_current_settings or
equivalent) inside the async block so send_new_order and take_order receive the
current settings; update both spawn_send_new_order_task and the 97-137 sibling
accordingly and ensure you pass the obtained settings into
crate::util::send_new_order / take_order.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 90bdff55-855e-4941-bad3-cc0513573a6f
📒 Files selected for processing (7)
README.mdsrc/main.rssrc/settings.rssrc/ui/key_handler/async_tasks.rssrc/ui/key_handler/enter_handlers.rssrc/ui/key_handler/mod.rssrc/ui/tabs/settings_tab.rs
🚧 Files skipped from review as they are similar to previous changes (3)
- src/ui/tabs/settings_tab.rs
- src/ui/key_handler/mod.rs
- README.md
…snapshot; they always use current runtime settings loaded from disk at action time.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/ui/key_handler/enter_handlers.rs (1)
405-410:⚠️ Potential issue | 🟡 MinorDon't use the settings helper's "quit app" path for a pubkey parse error.
handle_enter_settings_mode()usesfalseto mean "exit Mostrix". If this parse ever disagrees withvalidate_mostro_pubkey, Line 409 will terminate the app instead of showing an error popup.Suggested fix
let new_pubkey = match PublicKey::from_str(&key_string) { Ok(pk) => pk, Err(e) => { - log::error!("Invalid pubkey after confirmation: {}", e); - return false; + app.mode = UiMode::OperationResult(OperationResult::Error(format!( + "Invalid Mostro pubkey after confirmation: {}", + e + ))); + return true; } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/key_handler/enter_handlers.rs` around lines 405 - 410, The code currently treats a PublicKey::from_str parse error inside handle_enter_settings_mode() as a quit signal by returning false; instead, on Err(e) from PublicKey::from_str(&key_string) you should surface the parse error via the settings UI error popup (the same UX path used by validate_mostro_pubkey) and return true to remain in settings, not false; locate the match on PublicKey::from_str in handle_enter_settings_mode(), replace the log::error + return false branch with logic that logs the error, invokes the settings error-display helper (or the same popup routine used by validate_mostro_pubkey), and returns true so the app does not exit.
♻️ Duplicate comments (1)
src/main.rs (1)
296-300:⚠️ Potential issue | 🟠 MajorReload the in-memory admin signer too.
The new Admin “Generate New Keys” flow persists
admin_privkey, butadmin_chat_keysis still computed once at startup and this reload branch only rebuilds the userClientfromnsec_privkey. After an admin rotation, chat sends and admin-message classification keep using the old admin key until the process is restarted. Reparselatest_settings.admin_privkeyhere and refreshadmin_chat_pubkey, or force a full restart for admin-key rotations.Also applies to: 493-580
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main.rs` around lines 296 - 300, The admin key rotation code currently only rebuilds the user Client from nsec_privkey but leaves admin_chat_keys and admin_chat_pubkey stale; update the reload path to reparse latest_settings.admin_privkey and recreate admin_chat_keys (and derive/admin_chat_pubkey) when app.user_role == UserRole::Admin instead of reusing the old admin_keys, or alternatively trigger a full restart; locate the variables admin_chat_keys, admin_keys, latest_settings.admin_privkey and admin_chat_pubkey in the reload logic and ensure admin_chat_keys is reassigned from the new admin_privkey so chat sends and admin-message classification use the rotated key.
🧹 Nitpick comments (1)
src/ui/key_handler/user_handlers.rs (1)
81-83: TODO: Invoice input for buy orders is pending.The comment indicates that invoice handling for buy orders (taking sell) is not yet implemented.
Would you like me to help design the invoice input flow for buy orders, or open an issue to track this task?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/key_handler/user_handlers.rs` around lines 81 - 83, The code leaves invoice handling unimplemented for buy orders by setting let invoice = None; — implement an input flow so buy (taking sell) orders prompt for an invoice and populate the invoice variable instead of leaving it None: detect the order side in the user handlers in user_handlers.rs (where invoice is declared), if the order is a buy ask the user for invoice text (or select a file) via the existing input/prompt utilities, validate non-empty input, convert it to the expected invoice type and assign it to invoice; if validation fails, return an error or re-prompt. If you prefer tracking rather than implementing now, create an issue referencing this TODO and link it in the comment above the invoice variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/ui/key_handler/enter_handlers.rs`:
- Around line 412-421: The handler updates ctx.current_mostro_pubkey but leaves
a stale by-value ctx.mostro_pubkey used for admin/dispute actions; fix by
removing the by-value mostro_pubkey from the EnterKeyContext (or stop using it)
and make all admin/dispute pathways read the live key from
ctx.current_mostro_pubkey.lock() at dispatch time (update any usages of
ctx.mostro_pubkey to acquire the mutex and clone the key before sending), and
keep spawn_refresh_mostro_info_task unchanged to use the new_pubkey.
In `@src/util/order_utils/fetch_scheduler.rs`:
- Around line 42-45: The scheduler currently captures and pins the original
Client in the spawned order/dispute loops so soft reloads can't replace it;
change start_fetch_scheduler to accept a swappable client (e.g.,
Arc<Mutex<Client>> or a client-factory closure) instead of consuming a concrete
Client and update the internal order/dispute loops to read the client on each
poll from that shared handle (or call the factory) so they use the new runtime
session after reload; alternatively, keep the signature but modify
FetchSchedulerResult to include explicit abort/restart controls (e.g.,
AbortHandle/RestartHandle) that main can call to stop and recreate the loops;
update all references to Client clones in the order/dispute loop spawn sites and
ensure current_mostro_pubkey usage remains Arc<Mutex<PublicKey>> as before.
---
Outside diff comments:
In `@src/ui/key_handler/enter_handlers.rs`:
- Around line 405-410: The code currently treats a PublicKey::from_str parse
error inside handle_enter_settings_mode() as a quit signal by returning false;
instead, on Err(e) from PublicKey::from_str(&key_string) you should surface the
parse error via the settings UI error popup (the same UX path used by
validate_mostro_pubkey) and return true to remain in settings, not false; locate
the match on PublicKey::from_str in handle_enter_settings_mode(), replace the
log::error + return false branch with logic that logs the error, invokes the
settings error-display helper (or the same popup routine used by
validate_mostro_pubkey), and returns true so the app does not exit.
---
Duplicate comments:
In `@src/main.rs`:
- Around line 296-300: The admin key rotation code currently only rebuilds the
user Client from nsec_privkey but leaves admin_chat_keys and admin_chat_pubkey
stale; update the reload path to reparse latest_settings.admin_privkey and
recreate admin_chat_keys (and derive/admin_chat_pubkey) when app.user_role ==
UserRole::Admin instead of reusing the old admin_keys, or alternatively trigger
a full restart; locate the variables admin_chat_keys, admin_keys,
latest_settings.admin_privkey and admin_chat_pubkey in the reload logic and
ensure admin_chat_keys is reassigned from the new admin_privkey so chat sends
and admin-message classification use the rotated key.
---
Nitpick comments:
In `@src/ui/key_handler/user_handlers.rs`:
- Around line 81-83: The code leaves invoice handling unimplemented for buy
orders by setting let invoice = None; — implement an input flow so buy (taking
sell) orders prompt for an invoice and populate the invoice variable instead of
leaving it None: detect the order side in the user handlers in user_handlers.rs
(where invoice is declared), if the order is a buy ask the user for invoice text
(or select a file) via the existing input/prompt utilities, validate non-empty
input, convert it to the expected invoice type and assign it to invoice; if
validation fails, return an error or re-prompt. If you prefer tracking rather
than implementing now, create an issue referencing this TODO and link it in the
comment above the invoice variable.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b9832d55-35b5-4beb-9a4e-26a9809ec88b
📒 Files selected for processing (8)
src/main.rssrc/ui/generate_keys_popup.rssrc/ui/key_handler/async_tasks.rssrc/ui/key_handler/confirmation.rssrc/ui/key_handler/enter_handlers.rssrc/ui/key_handler/mod.rssrc/ui/key_handler/user_handlers.rssrc/util/order_utils/fetch_scheduler.rs
✅ Files skipped from review due to trivial changes (1)
- src/ui/key_handler/async_tasks.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- src/ui/key_handler/confirmation.rs
- src/ui/key_handler/mod.rs
There was a problem hiding this comment.
Code Review ✅
Well-structured implementation for key bootstrap synchronization and key regeneration flow.
Security Positives
| Aspect | Implementation |
|---|---|
| Memory safety | Zeroizing<String> for mnemonics ✅ |
| Atomic DB update | replace_all_in_tx uses SQL transaction ✅ |
| Atomic file write | Write to .tmp-{nanos}, then fs::rename ✅ |
| Rollback on failure | If tx.commit() fails, removes tmp file ✅ |
Architecture
-
First-launch sync —
init_settings()now receives identity keys from DB, ensuring settings.tomlnsec_privkeymatches the DB mnemonic-derived key -
Key rotation flow:
- User mode: Updates both DB (atomic tx) and settings.toml (atomic rename)
- Admin mode: Only updates
admin_privkeyin settings.toml - Shows backup popup with mnemonic before allowing restart
-
Channel-based async tasks — Clean separation with
AppChannelsstruct
Documentation ✅
Updated STARTUP_AND_CONFIG.md, KEY_MANAGEMENT.md, SETTINGS_ANALYSIS.md, TUI_INTERFACE.md to reflect new behaviors.
CI ✅
cargo fmt✅cargo clippy✅cargo test✅cargo build✅- MSRV 1.90.0 ✅
Summary
LGTM. The implementation correctly handles the security-sensitive key regeneration flow with proper atomicity guarantees and memory cleanup.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/ui/key_handler/admin_handlers.rs (1)
25-30: Silent failure on mutex poison leaves UI in waiting state.If
current_mostro_pubkey.lock()fails (poisoned mutex), the function returns early without updatingapp.mode, leaving the UI stuck inWaitingTakeDisputewith no error feedback to the user.Consider setting an error mode before returning:
🛠️ Suggested improvement
let current_mostro_pubkey = if let Ok(active_pubkey) = ctx.current_mostro_pubkey.lock() { *active_pubkey } else { log::warn!("Failed to get runtime Mostro pubkey after confirmation"); + app.mode = UiMode::OperationResult(OperationResult::Error( + "Internal error: failed to access runtime state".to_string(), + )); return; };The same pattern applies to
execute_add_solver_action(lines 74-79) andexecute_finalize_dispute_action(lines 110-115).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/key_handler/admin_handlers.rs` around lines 25 - 30, When the mutex lock on ctx.current_mostro_pubkey fails the code currently logs a warning and returns, leaving the UI stuck in WaitingTakeDispute; change the error path to update the UI state before returning by setting app.mode to an error state (e.g. AppMode::Error or a suitable error variant) and populate any available error message field so the user sees feedback, then return; apply the same change to the mutex-failure branches in execute_add_solver_action and execute_finalize_dispute_action where current_mostro_pubkey.lock() is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/ui/key_handler/admin_handlers.rs`:
- Around line 25-30: When the mutex lock on ctx.current_mostro_pubkey fails the
code currently logs a warning and returns, leaving the UI stuck in
WaitingTakeDispute; change the error path to update the UI state before
returning by setting app.mode to an error state (e.g. AppMode::Error or a
suitable error variant) and populate any available error message field so the
user sees feedback, then return; apply the same change to the mutex-failure
branches in execute_add_solver_action and execute_finalize_dispute_action where
current_mostro_pubkey.lock() is used.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ee891390-03c0-4cad-9f9e-97a1a2034d64
📒 Files selected for processing (10)
src/main.rssrc/models.rssrc/ui/key_handler/admin_handlers.rssrc/ui/key_handler/async_tasks.rssrc/ui/key_handler/confirmation.rssrc/ui/key_handler/enter_handlers.rssrc/ui/key_handler/mod.rssrc/util/order_utils/fetch_scheduler.rssrc/util/order_utils/mod.rssrc/util/order_utils/send_new_order.rs
🚧 Files skipped from review as they are similar to previous changes (4)
- src/util/order_utils/fetch_scheduler.rs
- src/ui/key_handler/confirmation.rs
- src/models.rs
- src/ui/key_handler/mod.rs
Summary
sec_privkey is derived from the DB identity key (mnemonic index 0), keeping DB and settings in sync
sec_privkey for user mode, admin_privkey for admin mode), and simplified popup state handling
Test plan
sec_privkey matches DB identity key
Notes
Summary by CodeRabbit
New Features
Bug Fixes
Documentation