lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 9cb20b77ddcba28bb166836e2aed621bfe4cd717
parent 6fb1e4394d8a2dbaddc8a84a52741b3ad3bfc23e
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:
Mcrates/studio_application/src/actor.rs | 21+++++++++++++++++++++
Mcrates/studio_storage/src/runtime_actor.rs | 33+++++++++++++++++++++++++++++++++
2 files changed, 54 insertions(+), 0 deletions(-)

diff --git a/crates/studio_application/src/actor.rs b/crates/studio_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/crates/studio_storage/src/runtime_actor.rs b/crates/studio_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,