use std::hash::{Hash, Hasher};
use tonic::transport::Channel;
#[derive(Clone)]
pub struct CallerId {
identity: String,
credential_sha256: Option<String>,
}
impl std::fmt::Debug for CallerId {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.debug_tuple("CallerId").field(&self.identity).finish()
}
}
impl PartialEq for CallerId {
fn eq(&self, other: &Self) -> bool {
self.identity == other.identity
}
}
impl Eq for CallerId {}
impl Hash for CallerId {
fn hash<H: Hasher>(&self, state: &mut H) {
self.identity.hash(state);
}
}
impl CallerId {
pub fn local_owner() -> Self {
Self {
identity: "local-owner".to_string(),
credential_sha256: None,
}
}
pub fn paired_device(pair_id: &str) -> Self {
Self {
identity: format!("paired-device:{pair_id}"),
credential_sha256: None,
}
}
pub fn authenticated_paired_device(pair_id: &str, credential_sha256: String) -> Self {
Self {
identity: format!("paired-device:{pair_id}"),
credential_sha256: Some(credential_sha256),
}
}
pub fn paired_device_id(&self) -> Option<&str> {
self.identity.strip_prefix("paired-device:")
}
pub fn credential_sha256(&self) -> Option<&str> {
self.credential_sha256.as_deref()
}
pub fn as_str(&self) -> &str {
&self.identity
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RunnerRoute {
pub device_id: Option<String>,
pub run_id: Option<String>,
pub runner_class: String,
}
pub trait OperationPermit: Send {}
impl<T: Send> OperationPermit for T {}
pub struct RoutedOperation {
pub channel: Channel,
pub permit: Box<dyn OperationPermit>,
}
#[derive(Debug, thiserror::Error)]
pub enum ControlError {
#[error("failed to generate control-plane identity: {0}")]
Identity(String),
#[error("invalid control-plane argument: {0}")]
InvalidArgument(&'static str),
#[error("unknown Device: {0}")]
UnknownDevice(String),
#[error("unknown Run: {0}")]
UnknownRun(String),
#[error("unknown Runner: {0}")]
UnknownRunner(String),
#[error("no RunnerProvider is registered for RunnerClass: {0}")]
RunnerProviderUnavailable(String),
#[error("Runner operation failed: {0}")]
RunnerOperation(String),
}
#[derive(Debug, thiserror::Error)]
pub enum PairingError {
#[error("pairing is not configured")]
NotConfigured,
#[error("pairing request is invalid: {0}")]
Invalid(String),
#[error("pairing token is invalid, expired, or has already been consumed")]
InvalidToken,
#[error("paired Device was not found: {0}")]
NotFound(String),
#[error("paired Device selector is ambiguous: {0}")]
Ambiguous(String),
#[error("Device credential is not paired or has been revoked")]
Unauthenticated,
#[error("pairing persistence failed: {0}")]
Persistence(String),
}
pub struct PairingToken {
pub token: String,
}
pub struct Enrollment {
pub device_id: String,
pub credential: String,
}
pub trait Pairing: Send + Sync {
fn authenticate_bearer(&self, credential: &str) -> Result<CallerId, PairingError>;
fn is_active_caller(&self, caller: &CallerId) -> bool;
fn issue_token(&self, lifetime: Option<std::time::Duration>) -> Result<PairingToken, PairingError>;
fn enroll(&self, token: &str, device_id: String, label: String) -> Result<Enrollment, PairingError>;
fn revoke_device_credentials(&self, selector: &str) -> Result<bool, PairingError>;
fn set_enabled(&self, selector: &str, enabled: bool) -> Result<bool, PairingError>;
fn unpair(&self, selector: &str) -> Result<bool, PairingError>;
}
#[cfg(test)]
mod caller_tests {
use super::CallerId;
#[test]
fn bearer_proof_is_redacted_and_does_not_change_stable_caller_identity() {
let digest = "private-digest".to_string();
let authenticated = CallerId::authenticated_paired_device("tablet", digest.clone());
let stable = CallerId::paired_device("tablet");
assert_eq!(authenticated, stable);
assert_eq!(authenticated.as_str(), "paired-device:tablet");
assert!(!format!("{authenticated:?}").contains(&digest));
}
}
#[tonic::async_trait]
pub trait Control: Send + Sync {
fn list_devices(&self) -> Result<Vec<auv::devices::Device>, ControlError>;
fn get_device(&self, device_id: &str) -> Result<Option<auv::devices::Device>, ControlError>;
fn list_user_sessions(&self, caller: &CallerId) -> Result<Vec<auv::devices::UserSession>, auv::devices::DeviceEntryErrorReason>;
fn get_user_session(
&self,
caller: &CallerId,
session_selector: &str,
) -> Result<auv::devices::UserSession, auv::devices::DeviceEntryErrorReason>;
async fn ensure_user_session_unlocked(
&self,
caller: &CallerId,
target: auv::devices::UserSessionTarget,
) -> Result<auv::devices::EnsureUserSessionUnlockedEffect, auv::devices::DeviceEntryErrorReason>;
async fn ensure_user_session_locked(
&self,
caller: &CallerId,
target: auv::devices::UserSessionTarget,
) -> Result<auv::devices::EnsureUserSessionLockedEffect, auv::devices::DeviceEntryErrorReason>;
fn create_run(&self, caller: &CallerId, request: auv::runs::CreateRun) -> Result<auv::runs::Run, ControlError>;
async fn stop_run(&self, caller: &CallerId, run_id: &str, outcome: auv::runs::RunOutcome) -> Result<auv::runs::Run, ControlError>;
fn list_runs(&self, caller: &CallerId) -> Result<Vec<auv::runs::Run>, ControlError>;
fn get_run(&self, caller: &CallerId, run_id: &str) -> Result<auv::runs::Run, ControlError>;
fn list_runners(&self) -> Result<Vec<auv::runners::Runner>, ControlError>;
async fn create_runner(&self, request: auv::runners::CreateRunner) -> Result<auv::runners::Runner, ControlError>;
fn get_runner(&self, runner_id: &str) -> Result<auv::runners::Runner, ControlError>;
fn list_runner_classes(&self, device_id: Option<&str>) -> Result<Vec<auv::runners::RunnerClass>, ControlError>;
fn get_runner_class(&self, device_id: Option<&str>, runner_class: &str) -> Result<auv::runners::RunnerClass, ControlError>;
async fn delete_runner(&self, runner_id: &str, options: auv::runners::StopRunner) -> Result<auv::runners::Runner, ControlError>;
async fn admit_routed_channel(
&self,
caller: &CallerId,
route: RunnerRoute,
service: &str,
method: &str,
) -> Result<RoutedOperation, ControlError>;
async fn shutdown(&self);
fn has_live_runners(&self) -> bool;
}