use super::{AwaitEventWaitIdentity, ExecutionScope, Resolution, ResolveOutcome};
use crate::RuntimeError;
pub fn derive_key_id(
scope: &ExecutionScope,
wait: &AwaitEventWaitIdentity,
) -> Result<String, RuntimeError> {
scope.validate()?;
wait.validate()?;
crate::stable_hash::stable_json_sha256_hex(&(scope, wait)).map_err(|err| {
RuntimeError::new(
"await_event_key_hash",
format!("failed to hash await-event identity: {err}"),
)
})
}
pub fn sign_material(
scope: &ExecutionScope,
wait: &AwaitEventWaitIdentity,
key_id: &str,
) -> Vec<u8> {
serde_json::to_vec(&(scope, wait, key_id))
.expect("await-event signing material contains only infallible JSON values")
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PromiseState {
Missing,
Pending,
Resolved(Resolution),
Revoked,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PromiseTransition {
Store(Resolution),
AlreadyResolved(Resolution),
UnknownOrRevoked,
Unchanged,
}
impl PromiseTransition {
pub fn resolve_outcome(self) -> Option<ResolveOutcome> {
match self {
Self::Store(_) => Some(ResolveOutcome::Accepted),
Self::AlreadyResolved(terminal) => Some(ResolveOutcome::AlreadyResolved { terminal }),
Self::UnknownOrRevoked => Some(ResolveOutcome::UnknownOrRevoked),
Self::Unchanged => None,
}
}
}
pub fn resolve(state: PromiseState, proposed: Resolution) -> PromiseTransition {
match state {
PromiseState::Missing | PromiseState::Pending => PromiseTransition::Store(proposed),
PromiseState::Resolved(terminal) => PromiseTransition::AlreadyResolved(terminal),
PromiseState::Revoked => PromiseTransition::UnknownOrRevoked,
}
}
pub fn cancel_sweep(wait: &AwaitEventWaitIdentity, state: PromiseState) -> PromiseTransition {
if wait.is_turn_control() {
return PromiseTransition::Unchanged;
}
match state {
PromiseState::Missing => PromiseTransition::Unchanged,
PromiseState::Pending => PromiseTransition::Store(Resolution::Cancelled),
PromiseState::Resolved(terminal) => PromiseTransition::AlreadyResolved(terminal),
PromiseState::Revoked => PromiseTransition::UnknownOrRevoked,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SessionRevocationTransition {
MarkRevoked,
AlreadyRevoked,
}
pub fn revoke_session(already_revoked: bool) -> SessionRevocationTransition {
if already_revoked {
SessionRevocationTransition::AlreadyRevoked
} else {
SessionRevocationTransition::MarkRevoked
}
}
pub fn session_allows_access(revoked: bool) -> bool {
!revoked
}
pub fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
let mut difference = left.len() ^ right.len();
for index in 0..left.len().max(right.len()) {
let left_byte = left.get(index).copied().unwrap_or_default();
let right_byte = right.get(index).copied().unwrap_or_default();
difference |= usize::from(left_byte ^ right_byte);
}
difference == 0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_buffers_before_wait_and_preserves_the_first_terminal() {
let first = Resolution::Ok(serde_json::json!("first"));
assert_eq!(
resolve(PromiseState::Missing, first.clone()),
PromiseTransition::Store(first.clone())
);
assert_eq!(
resolve(
PromiseState::Resolved(first.clone()),
Resolution::Ok(serde_json::json!("second")),
),
PromiseTransition::AlreadyResolved(first)
);
}
#[test]
fn cancel_sweep_excludes_turn_control_and_existing_terminals() {
assert_eq!(
cancel_sweep(
&AwaitEventWaitIdentity::TurnCancelGate,
PromiseState::Pending,
),
PromiseTransition::Unchanged
);
assert_eq!(
cancel_sweep(
&AwaitEventWaitIdentity::tool_completion("call"),
PromiseState::Pending,
),
PromiseTransition::Store(Resolution::Cancelled)
);
assert_eq!(
cancel_sweep(
&AwaitEventWaitIdentity::tool_completion("call"),
PromiseState::Resolved(Resolution::Timeout),
),
PromiseTransition::AlreadyResolved(Resolution::Timeout)
);
}
#[test]
fn revoked_sessions_reject_access_and_revoke_idempotently() {
assert!(session_allows_access(false));
assert!(!session_allows_access(true));
assert_eq!(
revoke_session(false),
SessionRevocationTransition::MarkRevoked
);
assert_eq!(
revoke_session(true),
SessionRevocationTransition::AlreadyRevoked
);
assert_eq!(
resolve(PromiseState::Revoked, Resolution::Cancelled),
PromiseTransition::UnknownOrRevoked
);
}
#[test]
fn authentication_comparison_covers_content_and_length_mismatches() {
assert!(constant_time_eq(b"same", b"same"));
assert!(!constant_time_eq(b"same", b"sale"));
assert!(!constant_time_eq(b"same", b"same-longer"));
}
#[test]
fn signing_material_is_the_canonical_scope_wait_key_tuple() {
let scope = ExecutionScope::turn("session", "turn");
let wait = AwaitEventWaitIdentity::tool_completion("call");
let key_id = derive_key_id(&scope, &wait).expect("derive key id");
assert_eq!(
sign_material(&scope, &wait, &key_id),
serde_json::to_vec(&(scope, wait, key_id)).expect("serialize tuple")
);
}
}