Skip to main content

aonyx_api/
state.rs

1//! Shared application state and its value types.
2//!
3//! [`ApiState`] is cloned into every handler by axum, so its fields are
4//! cheap-to-clone `Arc`s. The session store and the turn-runner are trait
5//! objects so the binary injects its real implementations while tests use
6//! an in-memory store + a stub agent.
7
8use std::sync::Arc;
9
10use aonyx_memory::{Palace, SessionStore};
11use serde::Serialize;
12
13use crate::agent::ApiAgent;
14
15/// Authentication + authorization policy for the API.
16#[derive(Debug, Clone)]
17pub struct AuthConfig {
18    /// Bearer token required on protected routes. `None` disables auth
19    /// (only safe on a loopback bind — the binary enforces that in V4.5).
20    pub token: Option<String>,
21    /// Whether [`aonyx_core::SafetyClass::Destructive`] tools may be invoked
22    /// through the direct tool endpoint. Defaults to `false`.
23    pub allow_destructive: bool,
24}
25
26impl AuthConfig {
27    /// Build a policy from an optional token and the destructive-tool flag.
28    pub fn new(token: Option<String>, allow_destructive: bool) -> Self {
29        Self {
30            token,
31            allow_destructive,
32        }
33    }
34
35    /// Returns `true` when the request is authorized: either no token is
36    /// configured, or `auth_header` carries the matching
37    /// `Authorization: Bearer <token>` (the `Bearer ` prefix is optional).
38    pub fn check(&self, auth_header: Option<&str>) -> bool {
39        match &self.token {
40            None => true,
41            Some(expected) => auth_header
42                .map(|h| h.strip_prefix("Bearer ").unwrap_or(h) == expected)
43                .unwrap_or(false),
44        }
45    }
46}
47
48/// Server identity + capabilities, returned by `GET /v1/info`.
49#[derive(Debug, Clone, Serialize)]
50pub struct ServerInfo {
51    /// Product name (`"aonyx-agent"`).
52    pub name: &'static str,
53    /// Crate version (`CARGO_PKG_VERSION`).
54    pub version: &'static str,
55    /// Active LLM provider id (e.g. `"anthropic"`).
56    pub provider: String,
57    /// Active default model id.
58    pub model: String,
59    /// Enabled capability flags (e.g. `"streaming"`, `"tools"`).
60    pub features: Vec<String>,
61}
62
63impl ServerInfo {
64    /// Build server info for the active provider/model and capability set.
65    pub fn new(
66        provider: impl Into<String>,
67        model: impl Into<String>,
68        features: Vec<String>,
69    ) -> Self {
70        Self {
71            name: "aonyx-agent",
72            version: env!("CARGO_PKG_VERSION"),
73            provider: provider.into(),
74            model: model.into(),
75            features,
76        }
77    }
78}
79
80/// State shared with every request handler.
81#[derive(Clone)]
82pub struct ApiState {
83    /// Auth + authorization policy.
84    pub auth: Arc<AuthConfig>,
85    /// Static server/capability info for `GET /v1/info`.
86    pub info: Arc<ServerInfo>,
87    /// Persistent session store (typically `~/.aonyx/sessions.db`).
88    pub sessions: Arc<dyn SessionStore>,
89    /// The memory palace (KG + diary + chunks) for the memory endpoints.
90    pub palace: Arc<Palace>,
91    /// The injected agent loop used to run a turn.
92    pub agent: Arc<dyn ApiAgent>,
93    /// Project slug used when a request does not specify one.
94    pub default_project: Arc<String>,
95}
96
97impl ApiState {
98    /// Assemble the state from its parts.
99    pub fn new(
100        auth: AuthConfig,
101        info: ServerInfo,
102        sessions: Arc<dyn SessionStore>,
103        palace: Arc<Palace>,
104        agent: Arc<dyn ApiAgent>,
105        default_project: impl Into<String>,
106    ) -> Self {
107        Self {
108            auth: Arc::new(auth),
109            info: Arc::new(info),
110            sessions,
111            palace,
112            agent,
113            default_project: Arc::new(default_project.into()),
114        }
115    }
116
117    /// The given project, or the server default when `None`/empty.
118    pub(crate) fn project_or_default(&self, project: Option<String>) -> String {
119        project
120            .filter(|s| !s.is_empty())
121            .unwrap_or_else(|| self.default_project.as_ref().clone())
122    }
123}