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:
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,