use serde::{Deserialize, Serialize};
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use crate::error::SdkError;
pub type PortId = String;
pub type PortValue = serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct OAuthToken {
pub access_token: String,
pub refresh_token: Option<String>,
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
pub token_type: Option<String>,
pub scope: Option<String>,
}
impl OAuthToken {
pub fn bearer(access_token: impl Into<String>) -> Self {
Self {
access_token: access_token.into(),
refresh_token: None,
expires_at: None,
token_type: Some("Bearer".to_string()),
scope: None,
}
}
}
pub trait StateStore: Send + Sync + 'static {
fn append(
&self,
entry: PortValue,
) -> Pin<Box<dyn Future<Output = Result<PortId, SdkError>> + Send + '_>>;
fn load(
&self,
id: &PortId,
) -> Pin<Box<dyn Future<Output = Result<Option<PortValue>, SdkError>> + Send + '_>>;
fn list(
&self,
prefix: &str,
) -> Pin<Box<dyn Future<Output = Result<Vec<PortId>, SdkError>> + Send + '_>>;
fn delete(
&self,
id: &PortId,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>>;
#[allow(clippy::type_complexity)]
fn load_all(
&self,
_prefix: &str,
) -> Pin<Box<dyn Future<Output = Result<Vec<(PortId, PortValue)>, SdkError>> + Send + '_>> {
Box::pin(async { Ok(Vec::new()) })
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopStateStore;
impl StateStore for NoopStateStore {
fn append(
&self,
_entry: PortValue,
) -> Pin<Box<dyn Future<Output = Result<PortId, SdkError>> + Send + '_>> {
Box::pin(async { Err(SdkError::PortNotConfigured { port: "StateStore" }) })
}
fn load(
&self,
_id: &PortId,
) -> Pin<Box<dyn Future<Output = Result<Option<PortValue>, SdkError>> + Send + '_>> {
Box::pin(async { Ok(None) })
}
fn list(
&self,
_prefix: &str,
) -> Pin<Box<dyn Future<Output = Result<Vec<PortId>, SdkError>> + Send + '_>> {
Box::pin(async { Ok(Vec::new()) })
}
fn delete(
&self,
_id: &PortId,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
Box::pin(async { Ok(()) })
}
}
pub trait ConfigStore: Send + Sync + 'static {
fn get(&self, key: &str) -> Result<Option<PortValue>, SdkError>;
fn set(&self, key: &str, value: PortValue) -> Result<(), SdkError>;
fn list(&self) -> Result<Vec<(String, PortValue)>, SdkError>;
fn source(&self, _key: &str) -> Option<String> {
None
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopConfigStore;
impl ConfigStore for NoopConfigStore {
fn get(&self, _key: &str) -> Result<Option<PortValue>, SdkError> {
Ok(None)
}
fn set(&self, _key: &str, _value: PortValue) -> Result<(), SdkError> {
Ok(())
}
fn list(&self) -> Result<Vec<(String, PortValue)>, SdkError> {
Ok(Vec::new())
}
}
pub trait AuthProvider: Send + Sync + 'static {
fn get_api_key(
&self,
provider: &str,
) -> Pin<Box<dyn Future<Output = Result<Option<String>, SdkError>> + Send + '_>>;
fn set_api_key(
&self,
provider: &str,
key: &str,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>>;
fn delete_api_key(
&self,
provider: &str,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>>;
fn get_oauth(
&self,
provider: &str,
) -> Pin<Box<dyn Future<Output = Result<Option<OAuthToken>, SdkError>> + Send + '_>>;
fn set_oauth(
&self,
provider: &str,
token: OAuthToken,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>>;
fn list_providers(
&self,
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, SdkError>> + Send + '_>>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopAuthProvider;
impl AuthProvider for NoopAuthProvider {
fn get_api_key(
&self,
_provider: &str,
) -> Pin<Box<dyn Future<Output = Result<Option<String>, SdkError>> + Send + '_>> {
Box::pin(async { Ok(None) })
}
fn set_api_key(
&self,
_provider: &str,
_key: &str,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
Box::pin(async {
Err(SdkError::PortNotConfigured {
port: "AuthProvider",
})
})
}
fn delete_api_key(
&self,
_provider: &str,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
Box::pin(async { Ok(()) })
}
fn get_oauth(
&self,
_provider: &str,
) -> Pin<Box<dyn Future<Output = Result<Option<OAuthToken>, SdkError>> + Send + '_>> {
Box::pin(async { Ok(None) })
}
fn set_oauth(
&self,
_provider: &str,
_token: OAuthToken,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
Box::pin(async {
Err(SdkError::PortNotConfigured {
port: "AuthProvider",
})
})
}
fn list_providers(
&self,
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, SdkError>> + Send + '_>> {
Box::pin(async { Ok(Vec::new()) })
}
}
pub type EventTopic = String;
pub type EventPayload = serde_json::Value;
pub trait EventBus: Send + Sync + 'static {
fn publish(
&self,
topic: &EventTopic,
payload: EventPayload,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>>;
fn subscribe(
&self,
topic: &EventTopic,
) -> Pin<Box<dyn Future<Output = Result<SubscriptionHandle, SdkError>> + Send + '_>>;
}
pub struct SubscriptionHandle {
_unsubscribe: Option<Box<dyn FnOnce() + Send + Sync>>,
receiver: Option<tokio::sync::mpsc::Receiver<(EventTopic, EventPayload)>>,
}
impl SubscriptionHandle {
pub async fn recv(&mut self) -> Option<(EventTopic, EventPayload)> {
match &mut self.receiver {
Some(rx) => rx.recv().await,
None => None,
}
}
}
impl std::fmt::Debug for SubscriptionHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SubscriptionHandle")
.field("active", &self.receiver.is_some())
.finish()
}
}
impl SubscriptionHandle {
pub fn from_receiver(rx: tokio::sync::mpsc::Receiver<(EventTopic, EventPayload)>) -> Self {
Self {
_unsubscribe: None,
receiver: Some(rx),
}
}
}
pub struct InMemoryEventBus {
tx: tokio::sync::broadcast::Sender<(EventTopic, EventPayload)>,
}
impl InMemoryEventBus {
pub fn new(capacity: usize) -> Arc<Self> {
let (tx, _) = tokio::sync::broadcast::channel(capacity);
Arc::new(Self { tx })
}
}
impl EventBus for InMemoryEventBus {
fn publish(
&self,
topic: &EventTopic,
payload: EventPayload,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
let _ = self.tx.send((topic.clone(), payload));
Box::pin(async { Ok(()) })
}
fn subscribe(
&self,
_topic: &EventTopic,
) -> Pin<Box<dyn Future<Output = Result<SubscriptionHandle, SdkError>> + Send + '_>> {
let mut rx = self.tx.subscribe();
let (tx, rx2) = tokio::sync::mpsc::channel(64);
drop(tokio::spawn(async move {
while let Ok(event) = rx.recv().await {
if tx.send(event).await.is_err() {
break;
}
}
}));
Box::pin(async {
Ok(SubscriptionHandle {
_unsubscribe: None,
receiver: Some(rx2),
})
})
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopEventBus;
impl EventBus for NoopEventBus {
fn publish(
&self,
_topic: &EventTopic,
_payload: EventPayload,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
Box::pin(async { Ok(()) })
}
fn subscribe(
&self,
_topic: &EventTopic,
) -> Pin<Box<dyn Future<Output = Result<SubscriptionHandle, SdkError>> + Send + '_>> {
Box::pin(async {
Ok(SubscriptionHandle {
_unsubscribe: None,
receiver: None,
})
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkillMeta {
pub name: String,
pub description: String,
pub path: PathBuf,
pub version: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Skill {
pub meta: SkillMeta,
pub body: String,
}
pub trait SkillLoader: Send + Sync + 'static {
fn list(&self) -> Pin<Box<dyn Future<Output = Result<Vec<SkillMeta>, SdkError>> + Send + '_>>;
fn load(
&self,
name: &str,
) -> Pin<Box<dyn Future<Output = Result<Option<Skill>, SdkError>> + Send + '_>>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopSkillLoader;
impl SkillLoader for NoopSkillLoader {
fn list(&self) -> Pin<Box<dyn Future<Output = Result<Vec<SkillMeta>, SdkError>> + Send + '_>> {
Box::pin(async { Ok(Vec::new()) })
}
fn load(
&self,
_name: &str,
) -> Pin<Box<dyn Future<Output = Result<Option<Skill>, SdkError>> + Send + '_>> {
Box::pin(async { Ok(None) })
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Persona {
pub name: String,
pub system_prompt: String,
pub preferred_model: Option<String>,
pub allowed_tools: Option<Vec<String>>,
}
pub trait PersonaProvider: Send + Sync + 'static {
fn list(&self) -> Pin<Box<dyn Future<Output = Result<Vec<Persona>, SdkError>> + Send + '_>>;
fn get(
&self,
name: &str,
) -> Pin<Box<dyn Future<Output = Result<Option<Persona>, SdkError>> + Send + '_>>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopPersonaProvider;
impl PersonaProvider for NoopPersonaProvider {
fn list(&self) -> Pin<Box<dyn Future<Output = Result<Vec<Persona>, SdkError>> + Send + '_>> {
Box::pin(async { Ok(Vec::new()) })
}
fn get(
&self,
_name: &str,
) -> Pin<Box<dyn Future<Output = Result<Option<Persona>, SdkError>> + Send + '_>> {
Box::pin(async { Ok(None) })
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallRequest {
pub tool: String,
pub action: String,
pub cwd: PathBuf,
pub subject: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum AccessDecision {
Allow,
AllowWithAudit,
Deny { reason: String },
RequireApproval { reason: String },
}
pub trait AccessGate: Send + Sync + 'static {
fn check(
&self,
request: &ToolCallRequest,
) -> Pin<Box<dyn Future<Output = Result<AccessDecision, SdkError>> + Send + '_>>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct AllowAllAccessGate;
impl AccessGate for AllowAllAccessGate {
fn check(
&self,
_request: &ToolCallRequest,
) -> Pin<Box<dyn Future<Output = Result<AccessDecision, SdkError>> + Send + '_>> {
Box::pin(async { Ok(AccessDecision::Allow) })
}
}
pub trait CapabilityResolver: Send + Sync + 'static {
fn visible_tools(
&self,
subject: &str,
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, SdkError>> + Send + '_>>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct EmptyCapabilityResolver;
impl CapabilityResolver for EmptyCapabilityResolver {
fn visible_tools(
&self,
_subject: &str,
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, SdkError>> + Send + '_>> {
Box::pin(async { Ok(Vec::new()) })
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryEntry {
pub id: String,
pub subject: String,
pub kind: String,
pub embedding: Option<Vec<f32>>,
pub content: PortValue,
pub created_at: chrono::DateTime<chrono::Utc>,
}
pub trait MemoryStore: Send + Sync + 'static {
fn put(
&self,
entry: MemoryEntry,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>>;
fn search(
&self,
_query: &[f32],
_k: usize,
) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryEntry>, SdkError>> + Send + '_>> {
Box::pin(async { Ok(Vec::new()) })
}
fn list(
&self,
subject: &str,
) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryEntry>, SdkError>> + Send + '_>>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopMemoryStore;
impl MemoryStore for NoopMemoryStore {
fn put(
&self,
_entry: MemoryEntry,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
Box::pin(async {
Err(SdkError::PortNotConfigured {
port: "MemoryStore",
})
})
}
fn list(
&self,
_subject: &str,
) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryEntry>, SdkError>> + Send + '_>> {
Box::pin(async { Ok(Vec::new()) })
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CronJob {
pub id: String,
pub schedule: String,
pub action: String,
pub payload: Option<PortValue>,
}
pub trait CronScheduler: Send + Sync + 'static {
fn register(
&self,
job: CronJob,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>>;
fn unregister(
&self,
id: &str,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>>;
fn list(&self) -> Pin<Box<dyn Future<Output = Result<Vec<CronJob>, SdkError>> + Send + '_>>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopCronScheduler;
impl CronScheduler for NoopCronScheduler {
fn register(
&self,
_job: CronJob,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
Box::pin(async {
Err(SdkError::PortNotConfigured {
port: "CronScheduler",
})
})
}
fn unregister(
&self,
_id: &str,
) -> Pin<Box<dyn Future<Output = Result<(), SdkError>> + Send + '_>> {
Box::pin(async { Ok(()) })
}
fn list(&self) -> Pin<Box<dyn Future<Output = Result<Vec<CronJob>, SdkError>> + Send + '_>> {
Box::pin(async { Ok(Vec::new()) })
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ResourceUsage {
pub cpu_percent: f32,
pub memory_bytes: u64,
pub disk_bytes: u64,
pub active_agents: usize,
pub tokens_consumed: u64,
}
pub trait ResourceMonitor: Send + Sync + 'static {
fn snapshot(
&self,
) -> Pin<Box<dyn Future<Output = Result<ResourceUsage, SdkError>> + Send + '_>>;
fn is_over_budget(&self) -> Pin<Box<dyn Future<Output = Result<bool, SdkError>> + Send + '_>> {
Box::pin(async { Ok(false) })
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopResourceMonitor;
impl ResourceMonitor for NoopResourceMonitor {
fn snapshot(
&self,
) -> Pin<Box<dyn Future<Output = Result<ResourceUsage, SdkError>> + Send + '_>> {
Box::pin(async { Ok(ResourceUsage::default()) })
}
}
#[derive(Clone)]
pub struct PortRegistry {
pub state: Arc<dyn StateStore>,
pub config: Arc<dyn ConfigStore>,
pub auth: Arc<dyn AuthProvider>,
pub event_bus: Arc<dyn EventBus>,
pub skills: Arc<dyn SkillLoader>,
pub personas: Arc<dyn PersonaProvider>,
pub access: Arc<dyn AccessGate>,
pub capabilities: Arc<dyn CapabilityResolver>,
pub memory: Arc<dyn MemoryStore>,
pub cron: Arc<dyn CronScheduler>,
pub resources: Arc<dyn ResourceMonitor>,
}
impl std::fmt::Debug for PortRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PortRegistry")
.field("state", &"<dyn StateStore>")
.field("config", &"<dyn ConfigStore>")
.field("auth", &"<dyn AuthProvider>")
.field("event_bus", &"<dyn EventBus>")
.field("skills", &"<dyn SkillLoader>")
.field("personas", &"<dyn PersonaProvider>")
.field("access", &"<dyn AccessGate>")
.field("capabilities", &"<dyn CapabilityResolver>")
.field("memory", &"<dyn MemoryStore>")
.field("cron", &"<dyn CronScheduler>")
.field("resources", &"<dyn ResourceMonitor>")
.finish()
}
}
impl Default for PortRegistry {
fn default() -> Self {
Self::noop()
}
}
impl PortRegistry {
pub fn noop() -> Self {
Self {
state: Arc::new(NoopStateStore),
config: Arc::new(NoopConfigStore),
auth: Arc::new(NoopAuthProvider),
event_bus: Arc::new(NoopEventBus),
skills: Arc::new(NoopSkillLoader),
personas: Arc::new(NoopPersonaProvider),
access: Arc::new(AllowAllAccessGate),
capabilities: Arc::new(EmptyCapabilityResolver),
memory: Arc::new(NoopMemoryStore),
cron: Arc::new(NoopCronScheduler),
resources: Arc::new(NoopResourceMonitor),
}
}
pub async fn from_directory(_dir: &Path) -> Self {
Self::noop()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[tokio::test]
async fn noop_state_store_load_returns_none() {
let s = NoopStateStore;
assert!(s.load(&"x".into()).await.unwrap().is_none());
assert!(s.list("").await.unwrap().is_empty());
}
#[tokio::test]
async fn noop_state_store_append_errors() {
let s = NoopStateStore;
let err = s.append(json!({})).await.unwrap_err();
assert!(matches!(
err,
SdkError::PortNotConfigured { port: "StateStore" }
));
}
#[test]
fn noop_config_get_returns_none() {
let c = NoopConfigStore;
assert!(c.get("any").unwrap().is_none());
assert!(c.list().unwrap().is_empty());
}
#[tokio::test]
async fn noop_auth_get_api_key_returns_none() {
let a = NoopAuthProvider;
assert!(a.get_api_key("anthropic").await.unwrap().is_none());
assert!(a.list_providers().await.unwrap().is_empty());
}
#[tokio::test]
async fn in_memory_event_bus_round_trip() {
let bus = InMemoryEventBus::new(8);
bus.publish(&"test".to_string(), json!({"hello": "world"}))
.await
.unwrap();
let mut sub = bus.subscribe(&"test".to_string()).await.unwrap();
bus.publish(&"test".to_string(), json!({"k": 1}))
.await
.unwrap();
let (topic, payload) = sub.recv().await.unwrap();
assert_eq!(topic, "test");
assert_eq!(payload, json!({"k": 1}));
}
#[tokio::test]
async fn noop_event_bus_publish_succeeds_but_subscribes_return_none() {
let bus = NoopEventBus;
bus.publish(&"x".to_string(), json!({})).await.unwrap();
let mut sub = bus.subscribe(&"x".to_string()).await.unwrap();
assert!(sub.recv().await.is_none());
}
#[test]
fn default_registry_is_noop() {
let reg = PortRegistry::default();
assert!(Arc::strong_count(®.state) >= 1);
}
#[test]
fn oauth_token_bearer_constructor() {
let t = OAuthToken::bearer("abc");
assert_eq!(t.access_token, "abc");
assert_eq!(t.token_type.as_deref(), Some("Bearer"));
}
}
pub mod fs;
pub mod inmem;