mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 01:47:47 +08:00
197 lines
5.7 KiB
Rust
197 lines
5.7 KiB
Rust
use async_trait::async_trait;
|
|
|
|
#[async_trait]
|
|
pub trait AiAuthenticatedDecisionInputPort: Send + Sync {
|
|
type AuthContext: Send + Sync;
|
|
type AuthSnapshot: Send;
|
|
type RequiredCapabilities: Send + Sync;
|
|
type ResolvedInput: Send;
|
|
type Error: Send;
|
|
|
|
async fn read_auth_snapshot(
|
|
&self,
|
|
auth_context: &Self::AuthContext,
|
|
) -> Result<Option<Self::AuthSnapshot>, Self::Error>;
|
|
|
|
async fn resolve_required_capabilities(
|
|
&self,
|
|
auth_context: &Self::AuthContext,
|
|
requested_model: Option<&str>,
|
|
explicit_required_capabilities: Option<&Self::RequiredCapabilities>,
|
|
) -> Result<Option<Self::RequiredCapabilities>, Self::Error>;
|
|
|
|
fn build_resolved_input(
|
|
&self,
|
|
auth_context: Self::AuthContext,
|
|
auth_snapshot: Self::AuthSnapshot,
|
|
required_capabilities: Option<Self::RequiredCapabilities>,
|
|
) -> Self::ResolvedInput;
|
|
}
|
|
|
|
pub async fn run_ai_authenticated_decision_input<Port>(
|
|
port: &Port,
|
|
auth_context: Port::AuthContext,
|
|
requested_model: Option<&str>,
|
|
explicit_required_capabilities: Option<&Port::RequiredCapabilities>,
|
|
) -> Result<Option<Port::ResolvedInput>, Port::Error>
|
|
where
|
|
Port: AiAuthenticatedDecisionInputPort,
|
|
{
|
|
let auth_snapshot = match port.read_auth_snapshot(&auth_context).await? {
|
|
Some(snapshot) => snapshot,
|
|
None => return Ok(None),
|
|
};
|
|
|
|
let required_capabilities = port
|
|
.resolve_required_capabilities(
|
|
&auth_context,
|
|
requested_model,
|
|
explicit_required_capabilities,
|
|
)
|
|
.await?;
|
|
|
|
Ok(Some(port.build_resolved_input(
|
|
auth_context,
|
|
auth_snapshot,
|
|
required_capabilities,
|
|
)))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::sync::Mutex;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
struct TestAuthContext {
|
|
user_id: &'static str,
|
|
api_key_id: &'static str,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
struct TestResolvedInput {
|
|
auth_context: TestAuthContext,
|
|
auth_snapshot: &'static str,
|
|
required_capabilities: Option<String>,
|
|
}
|
|
|
|
struct TestPort {
|
|
auth_snapshot: Option<&'static str>,
|
|
calls: Mutex<Vec<String>>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl AiAuthenticatedDecisionInputPort for TestPort {
|
|
type AuthContext = TestAuthContext;
|
|
type AuthSnapshot = &'static str;
|
|
type RequiredCapabilities = String;
|
|
type ResolvedInput = TestResolvedInput;
|
|
type Error = std::convert::Infallible;
|
|
|
|
async fn read_auth_snapshot(
|
|
&self,
|
|
auth_context: &Self::AuthContext,
|
|
) -> Result<Option<Self::AuthSnapshot>, Self::Error> {
|
|
self.calls.lock().unwrap().push(format!(
|
|
"snapshot:{}:{}",
|
|
auth_context.user_id, auth_context.api_key_id
|
|
));
|
|
Ok(self.auth_snapshot)
|
|
}
|
|
|
|
async fn resolve_required_capabilities(
|
|
&self,
|
|
auth_context: &Self::AuthContext,
|
|
requested_model: Option<&str>,
|
|
explicit_required_capabilities: Option<&Self::RequiredCapabilities>,
|
|
) -> Result<Option<Self::RequiredCapabilities>, Self::Error> {
|
|
self.calls.lock().unwrap().push(format!(
|
|
"capabilities:{}:{}:{}",
|
|
auth_context.user_id,
|
|
requested_model.unwrap_or_default(),
|
|
explicit_required_capabilities
|
|
.map(String::as_str)
|
|
.unwrap_or_default()
|
|
));
|
|
Ok(Some("merged-capabilities".to_string()))
|
|
}
|
|
|
|
fn build_resolved_input(
|
|
&self,
|
|
auth_context: Self::AuthContext,
|
|
auth_snapshot: Self::AuthSnapshot,
|
|
required_capabilities: Option<Self::RequiredCapabilities>,
|
|
) -> Self::ResolvedInput {
|
|
TestResolvedInput {
|
|
auth_context,
|
|
auth_snapshot,
|
|
required_capabilities,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn authenticated_decision_input_resolves_snapshot_and_capabilities() {
|
|
let port = TestPort {
|
|
auth_snapshot: Some("snapshot-a"),
|
|
calls: Mutex::new(Vec::new()),
|
|
};
|
|
let auth_context = TestAuthContext {
|
|
user_id: "user-a",
|
|
api_key_id: "key-a",
|
|
};
|
|
let explicit = "explicit-capability".to_string();
|
|
|
|
let resolved = run_ai_authenticated_decision_input(
|
|
&port,
|
|
auth_context.clone(),
|
|
Some("model-a"),
|
|
Some(&explicit),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
resolved,
|
|
Some(TestResolvedInput {
|
|
auth_context,
|
|
auth_snapshot: "snapshot-a",
|
|
required_capabilities: Some("merged-capabilities".to_string()),
|
|
})
|
|
);
|
|
assert_eq!(
|
|
port.calls.lock().unwrap().as_slice(),
|
|
[
|
|
"snapshot:user-a:key-a",
|
|
"capabilities:user-a:model-a:explicit-capability",
|
|
]
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn authenticated_decision_input_stops_when_snapshot_is_missing() {
|
|
let port = TestPort {
|
|
auth_snapshot: None,
|
|
calls: Mutex::new(Vec::new()),
|
|
};
|
|
|
|
let resolved = run_ai_authenticated_decision_input(
|
|
&port,
|
|
TestAuthContext {
|
|
user_id: "user-a",
|
|
api_key_id: "key-a",
|
|
},
|
|
Some("model-a"),
|
|
None,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(resolved, None);
|
|
assert_eq!(
|
|
port.calls.lock().unwrap().as_slice(),
|
|
["snapshot:user-a:key-a"]
|
|
);
|
|
}
|
|
}
|