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}