app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit cb2cea5bbc8be802faaeae25d922852237c3bb9a
parent bec8462a0ab530659aa068e76369fcac21e6a470
Author: triesap <tyson@radroots.org>
Date:   Tue,  4 Aug 2026 00:05:28 +0000

profile: bind refresh tasks to signer sessions

- include the local signer binding in task correlation
- validate request account revision and session generation
- reject completions after foreground binding replacement
- cover the complete correlation tuple in actor tests

Diffstat:
Mcore/crates/application/src/actor.rs | 21+++++++++++++++++++++
Mcore/crates/storage/src/runtime_actor.rs | 33+++++++++++++++++++++++++++++++++
2 files changed, 54 insertions(+), 0 deletions(-)

diff --git a/core/crates/application/src/actor.rs b/core/crates/application/src/actor.rs @@ -95,6 +95,7 @@ const fn invalid_foreground_session() -> SafeError { pub struct TaskCorrelation { request_id: RequestId, account: PublicKey, + binding: LocalSignerBinding, expected_revision: SnapshotRevision, session_generation: SessionGeneration, } @@ -104,12 +105,14 @@ impl TaskCorrelation { pub const fn new( request_id: RequestId, account: PublicKey, + binding: LocalSignerBinding, expected_revision: SnapshotRevision, session_generation: SessionGeneration, ) -> Self { Self { request_id, account, + binding, expected_revision, session_generation, } @@ -126,6 +129,11 @@ impl TaskCorrelation { } #[must_use] + pub const fn binding(self) -> LocalSignerBinding { + self.binding + } + + #[must_use] pub const fn expected_revision(self) -> SnapshotRevision { self.expected_revision } @@ -597,6 +605,19 @@ mod tests { assert_eq!(session.signer().account(), public_key); assert_eq!(session.generation(), generation); + let correlation = super::TaskCorrelation::new( + RequestId::new(8).expect("request"), + public_key, + session.signer(), + crate::SnapshotRevision::from_value(9), + generation, + ); + assert_eq!(correlation.request_id().get(), 8); + assert_eq!(correlation.account(), public_key); + assert_eq!(correlation.binding(), session.signer()); + assert_eq!(correlation.expected_revision().value(), 9); + assert_eq!(correlation.session_generation(), generation); + assert!( ForegroundSessionBinding::new( identity.clone(), diff --git a/core/crates/storage/src/runtime_actor.rs b/core/crates/storage/src/runtime_actor.rs @@ -848,6 +848,11 @@ impl RuntimeActor { reply: oneshot::Sender<CommandReceipt<RuntimeCommandValue>>, completion_sender: mpsc::Sender<ProfileCompletion>, ) { + let foreground = self + .published_foreground_session + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone(); let plan = match self.adapter.core().begin_profile_refresh() { Ok(Some(plan)) => plan, Ok(None) => { @@ -867,9 +872,20 @@ impl RuntimeActor { return; } }; + let Some(foreground) = foreground.filter(|binding| { + binding.identity().public_key() == plan.public_key() + && binding.generation() == self.session_generation + }) else { + let _ = reply.send(CommandReceipt::new( + context.request_id(), + CommandResult::Failed(stale_profile_binding()), + )); + return; + }; let correlation = TaskCorrelation::new( context.request_id(), plan.public_key(), + foreground.signer(), plan.expected_revision(), self.session_generation, ); @@ -923,8 +939,18 @@ impl RuntimeActor { return; }; let current = self.adapter.core().snapshot(); + let foreground = self + .published_foreground_session + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone(); let correlated = task.correlation.session_generation() == self.session_generation && task.correlation.expected_revision() == current.revision() + && foreground.is_some_and(|binding| { + binding.generation() == task.correlation.session_generation() + && binding.identity().public_key() == task.correlation.account() + && binding.signer() == task.correlation.binding() + }) && current .active_account() .is_some_and(|active| active.account().public_key() == task.correlation.account()); @@ -1019,6 +1045,13 @@ const fn request_space_exhausted() -> SafeError { ) } +const fn stale_profile_binding() -> SafeError { + SafeError::new( + SafeErrorCode::InvalidApplicationState, + SafeMessage::new("The active account binding changed before profile refresh."), + ) +} + const fn command_conflicted() -> SafeError { SafeError::new( SafeErrorCode::InvalidApplicationState,