Skip to main content

nexara_server/
lib.rs

1use axum::extract::{Path, Query, State};
2use axum::http::{HeaderMap, StatusCode};
3use axum::response::{IntoResponse, Response};
4use axum::{Json, Router, routing::get, routing::post};
5use nexara_core::{ToolCallRequest, ToolCallResult};
6use nexara_registry::installed::InstalledSkillStore;
7use nexara_registry::{
8    InstalledSkillRecord, InstalledSkillState, JsonFileInstalledSkillStore,
9    parse_skill_manifest_str,
10};
11use nexara_runtime::NexaraRuntime;
12use nexara_secrets::{MemorySecretStore, SecretStore, SecretValue};
13use serde::Deserialize;
14use serde_json::json;
15use std::path::PathBuf;
16use std::sync::Arc;
17use std::sync::atomic::{AtomicU64, Ordering};
18use subtle::ConstantTimeEq;
19
20#[derive(Clone)]
21pub struct NexaraServerState {
22    pub runtime: Arc<NexaraRuntime<()>>,
23    pub auth: ServerAuth,
24    pub installed_skills: JsonFileInstalledSkillStore,
25    pub secrets: Arc<dyn SecretStore>,
26    pub metrics: Arc<ServerMetrics>,
27}
28
29#[derive(Debug, Default)]
30pub struct ServerMetrics {
31    pub tool_list_requests: AtomicU64,
32    pub tool_call_requests: AtomicU64,
33    pub admin_requests: AtomicU64,
34    pub unauthorized_requests: AtomicU64,
35}
36
37#[derive(Clone, Default)]
38pub struct ServerAuth {
39    pub bearer_token: Option<String>,
40    pub allow_dev_no_auth: bool,
41    pub authorizer: Option<Arc<dyn Authorizer>>,
42}
43
44impl std::fmt::Debug for ServerAuth {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        f.debug_struct("ServerAuth")
47            .field(
48                "bearer_token",
49                &self.bearer_token.as_ref().map(|_| "[redacted]"),
50            )
51            .field("allow_dev_no_auth", &self.allow_dev_no_auth)
52            .field("authorizer", &self.authorizer.as_ref().map(|_| "custom"))
53            .finish()
54    }
55}
56
57pub trait Authorizer: Send + Sync {
58    fn authorize(&self, headers: &HeaderMap) -> bool;
59}
60
61impl ServerAuth {
62    pub fn require(token: impl Into<String>) -> Self {
63        Self {
64            bearer_token: Some(token.into()),
65            allow_dev_no_auth: false,
66            authorizer: None,
67        }
68    }
69
70    pub fn with_authorizer(authorizer: Arc<dyn Authorizer>) -> Self {
71        Self {
72            bearer_token: None,
73            allow_dev_no_auth: false,
74            authorizer: Some(authorizer),
75        }
76    }
77
78    pub fn authorize(&self, headers: &HeaderMap) -> bool {
79        if let Some(authorizer) = &self.authorizer {
80            return authorizer.authorize(headers);
81        }
82        if self.allow_dev_no_auth {
83            return true;
84        }
85        let Some(expected) = &self.bearer_token else {
86            return false;
87        };
88        let Some(raw) = headers.get(axum::http::header::AUTHORIZATION) else {
89            return false;
90        };
91        let Ok(raw) = raw.to_str() else {
92            return false;
93        };
94        let actual = raw.strip_prefix("Bearer ").unwrap_or(raw);
95        actual.as_bytes().ct_eq(expected.as_bytes()).into()
96    }
97}
98
99pub fn router(state: NexaraServerState) -> Router {
100    Router::new()
101        .route("/health", get(health))
102        .route("/v1/nexara/tools", get(list_tools))
103        .route("/v1/nexara/tools/call", post(call_tool))
104        .route("/admin/nexara/skills", get(admin_list_skills))
105        .route("/admin/nexara/skills/install", post(admin_install_skill))
106        .route(
107            "/admin/nexara/skills/:skill_id/enable",
108            post(admin_enable_skill),
109        )
110        .route(
111            "/admin/nexara/skills/:skill_id/disable",
112            post(admin_disable_skill),
113        )
114        .route(
115            "/admin/nexara/skills/:skill_id/readiness",
116            get(admin_skill_readiness),
117        )
118        .route(
119            "/admin/nexara/skills/:skill_id/secrets",
120            get(admin_skill_secrets),
121        )
122        .route(
123            "/admin/nexara/skills/:skill_id/secrets/:secret_name",
124            post(admin_put_skill_secret).delete(admin_delete_skill_secret),
125        )
126        .route("/metrics", get(metrics))
127        .with_state(Arc::new(state))
128}
129
130async fn health() -> Json<serde_json::Value> {
131    Json(json!({ "status": "ok" }))
132}
133
134#[derive(Debug, Deserialize, Default)]
135struct ToolsQuery {
136    prompt: Option<String>,
137}
138
139async fn list_tools(
140    State(state): State<Arc<NexaraServerState>>,
141    headers: HeaderMap,
142    Query(query): Query<ToolsQuery>,
143) -> Response {
144    if !state.auth.authorize(&headers) {
145        return unauthorized(&state);
146    }
147    state
148        .metrics
149        .tool_list_requests
150        .fetch_add(1, Ordering::Relaxed);
151    let scopes = vec!["read".to_string()];
152    let tools = state.runtime.list_tools(query.prompt.as_deref(), &scopes);
153    (
154        StatusCode::OK,
155        Json(json!({ "status": "ok", "tools": tools })),
156    )
157        .into_response()
158}
159
160async fn call_tool(
161    State(state): State<Arc<NexaraServerState>>,
162    headers: HeaderMap,
163    Json(request): Json<ToolCallRequest>,
164) -> Response {
165    if !state.auth.authorize(&headers) {
166        return unauthorized(&state);
167    }
168    state
169        .metrics
170        .tool_call_requests
171        .fetch_add(1, Ordering::Relaxed);
172    match state.runtime.call_tool(request, ()).await {
173        Ok(ToolCallResult { result }) => (
174            StatusCode::OK,
175            Json(json!({ "status": "ok", "result": result })),
176        )
177            .into_response(),
178        Err(err) => error_response(err),
179    }
180}
181
182async fn admin_list_skills(
183    State(state): State<Arc<NexaraServerState>>,
184    headers: HeaderMap,
185) -> Response {
186    if !state.auth.authorize(&headers) {
187        return unauthorized(&state);
188    }
189    state.metrics.admin_requests.fetch_add(1, Ordering::Relaxed);
190    match state.installed_skills.list() {
191        Ok(skills) => (
192            StatusCode::OK,
193            Json(json!({ "status": "ok", "skills": skills })),
194        )
195            .into_response(),
196        Err(err) => (
197            StatusCode::INTERNAL_SERVER_ERROR,
198            Json(json!({ "error": err.to_string() })),
199        )
200            .into_response(),
201    }
202}
203
204#[derive(Debug, Deserialize)]
205struct InstallSkillRequest {
206    manifest_toml: String,
207    #[serde(default)]
208    enabled: bool,
209}
210
211async fn admin_install_skill(
212    State(state): State<Arc<NexaraServerState>>,
213    headers: HeaderMap,
214    Json(request): Json<InstallSkillRequest>,
215) -> Response {
216    if !state.auth.authorize(&headers) {
217        return unauthorized(&state);
218    }
219    state.metrics.admin_requests.fetch_add(1, Ordering::Relaxed);
220    let metadata = match parse_skill_manifest_str(&request.manifest_toml) {
221        Ok(metadata) => metadata,
222        Err(err) => {
223            return (
224                StatusCode::BAD_REQUEST,
225                Json(json!({ "error": err.to_string() })),
226            )
227                .into_response();
228        }
229    };
230    let record = InstalledSkillRecord {
231        metadata,
232        state: InstalledSkillState {
233            installed: true,
234            enabled: request.enabled,
235        },
236    };
237    if let Err(err) = state.installed_skills.put(record) {
238        return (
239            StatusCode::INTERNAL_SERVER_ERROR,
240            Json(json!({ "error": err.to_string() })),
241        )
242            .into_response();
243    }
244    (StatusCode::OK, Json(json!({ "status": "ok" }))).into_response()
245}
246
247async fn admin_enable_skill(
248    State(state): State<Arc<NexaraServerState>>,
249    headers: HeaderMap,
250    Path(skill_id): Path<String>,
251) -> Response {
252    update_skill_enabled(state, headers, skill_id, true)
253}
254
255async fn admin_disable_skill(
256    State(state): State<Arc<NexaraServerState>>,
257    headers: HeaderMap,
258    Path(skill_id): Path<String>,
259) -> Response {
260    update_skill_enabled(state, headers, skill_id, false)
261}
262
263fn update_skill_enabled(
264    state: Arc<NexaraServerState>,
265    headers: HeaderMap,
266    skill_id: String,
267    enabled: bool,
268) -> Response {
269    if !state.auth.authorize(&headers) {
270        return unauthorized(&state);
271    }
272    state.metrics.admin_requests.fetch_add(1, Ordering::Relaxed);
273    let Some(mut record) = (match state.installed_skills.get(&skill_id) {
274        Ok(record) => record,
275        Err(err) => {
276            return (
277                StatusCode::INTERNAL_SERVER_ERROR,
278                Json(json!({ "error": err.to_string() })),
279            )
280                .into_response();
281        }
282    }) else {
283        return (
284            StatusCode::NOT_FOUND,
285            Json(json!({ "error": "skill not found" })),
286        )
287            .into_response();
288    };
289    record.state.enabled = enabled;
290    if let Err(err) = state.installed_skills.put(record) {
291        return (
292            StatusCode::INTERNAL_SERVER_ERROR,
293            Json(json!({ "error": err.to_string() })),
294        )
295            .into_response();
296    }
297    (
298        StatusCode::OK,
299        Json(json!({ "status": "ok", "enabled": enabled })),
300    )
301        .into_response()
302}
303
304async fn admin_skill_readiness(
305    State(state): State<Arc<NexaraServerState>>,
306    headers: HeaderMap,
307    Path(skill_id): Path<String>,
308) -> Response {
309    if !state.auth.authorize(&headers) {
310        return unauthorized(&state);
311    }
312    state.metrics.admin_requests.fetch_add(1, Ordering::Relaxed);
313    let Some(record) = (match state.installed_skills.get(&skill_id) {
314        Ok(record) => record,
315        Err(err) => {
316            return (
317                StatusCode::INTERNAL_SERVER_ERROR,
318                Json(json!({ "error": err.to_string() })),
319            )
320                .into_response();
321        }
322    }) else {
323        return (
324            StatusCode::NOT_FOUND,
325            Json(json!({ "error": "skill not found" })),
326        )
327            .into_response();
328    };
329    let mut missing = Vec::new();
330    let mut present = Vec::new();
331    for secret in &record.metadata.requires_secrets {
332        match state.secrets.get(secret) {
333            Ok(Some(_)) => present.push(secret.clone()),
334            Ok(None) => missing.push(secret.clone()),
335            Err(err) => {
336                return (
337                    StatusCode::INTERNAL_SERVER_ERROR,
338                    Json(json!({ "error": err.to_string() })),
339                )
340                    .into_response();
341            }
342        }
343    }
344    (
345        StatusCode::OK,
346        Json(json!({
347            "status": "ok",
348            "ready": missing.is_empty(),
349            "installed": record.state.installed,
350            "enabled": record.state.enabled,
351            "present_secrets": present,
352            "missing_secrets": missing
353        })),
354    )
355        .into_response()
356}
357
358async fn admin_skill_secrets(
359    State(state): State<Arc<NexaraServerState>>,
360    headers: HeaderMap,
361    Path(skill_id): Path<String>,
362) -> Response {
363    if !state.auth.authorize(&headers) {
364        return unauthorized(&state);
365    }
366    state.metrics.admin_requests.fetch_add(1, Ordering::Relaxed);
367    let Some(record) = (match state.installed_skills.get(&skill_id) {
368        Ok(record) => record,
369        Err(err) => {
370            return (
371                StatusCode::INTERNAL_SERVER_ERROR,
372                Json(json!({ "error": err.to_string() })),
373            )
374                .into_response();
375        }
376    }) else {
377        return (
378            StatusCode::NOT_FOUND,
379            Json(json!({ "error": "skill not found" })),
380        )
381            .into_response();
382    };
383    let statuses = record
384        .metadata
385        .requires_secrets
386        .iter()
387        .map(|secret| {
388            let configured = state
389                .secrets
390                .get(secret)
391                .map(|value| value.is_some())
392                .unwrap_or(false);
393            json!({ "name": secret, "configured": configured })
394        })
395        .collect::<Vec<_>>();
396    (
397        StatusCode::OK,
398        Json(json!({ "status": "ok", "secrets": statuses })),
399    )
400        .into_response()
401}
402
403#[derive(Debug, Deserialize)]
404struct PutSecretRequest {
405    value: String,
406}
407
408async fn admin_put_skill_secret(
409    State(state): State<Arc<NexaraServerState>>,
410    headers: HeaderMap,
411    Path((skill_id, secret_name)): Path<(String, String)>,
412    Json(request): Json<PutSecretRequest>,
413) -> Response {
414    if !state.auth.authorize(&headers) {
415        return unauthorized(&state);
416    }
417    state.metrics.admin_requests.fetch_add(1, Ordering::Relaxed);
418    let Some(record) = (match state.installed_skills.get(&skill_id) {
419        Ok(record) => record,
420        Err(err) => {
421            return (
422                StatusCode::INTERNAL_SERVER_ERROR,
423                Json(json!({ "error": err.to_string() })),
424            )
425                .into_response();
426        }
427    }) else {
428        return (
429            StatusCode::NOT_FOUND,
430            Json(json!({ "error": "skill not found" })),
431        )
432            .into_response();
433    };
434    if !record.metadata.requires_secrets.contains(&secret_name) {
435        return (
436            StatusCode::BAD_REQUEST,
437            Json(json!({ "error": "secret is not declared by this skill" })),
438        )
439            .into_response();
440    }
441    if let Err(err) = state
442        .secrets
443        .put(&secret_name, SecretValue::new(request.value))
444    {
445        return (
446            StatusCode::INTERNAL_SERVER_ERROR,
447            Json(json!({ "error": err.to_string() })),
448        )
449            .into_response();
450    }
451    (StatusCode::OK, Json(json!({ "status": "ok" }))).into_response()
452}
453
454async fn admin_delete_skill_secret(
455    State(state): State<Arc<NexaraServerState>>,
456    headers: HeaderMap,
457    Path((skill_id, secret_name)): Path<(String, String)>,
458) -> Response {
459    if !state.auth.authorize(&headers) {
460        return unauthorized(&state);
461    }
462    state.metrics.admin_requests.fetch_add(1, Ordering::Relaxed);
463    if matches!(state.installed_skills.get(&skill_id), Ok(None)) {
464        return (
465            StatusCode::NOT_FOUND,
466            Json(json!({ "error": "skill not found" })),
467        )
468            .into_response();
469    }
470    if let Err(err) = state.secrets.delete(&secret_name) {
471        return (
472            StatusCode::INTERNAL_SERVER_ERROR,
473            Json(json!({ "error": err.to_string() })),
474        )
475            .into_response();
476    }
477    (StatusCode::OK, Json(json!({ "status": "ok" }))).into_response()
478}
479
480async fn metrics(State(state): State<Arc<NexaraServerState>>) -> Json<serde_json::Value> {
481    Json(json!({
482        "tool_list_requests": state.metrics.tool_list_requests.load(Ordering::Relaxed),
483        "tool_call_requests": state.metrics.tool_call_requests.load(Ordering::Relaxed),
484        "admin_requests": state.metrics.admin_requests.load(Ordering::Relaxed),
485        "unauthorized_requests": state.metrics.unauthorized_requests.load(Ordering::Relaxed)
486    }))
487}
488
489fn unauthorized(state: &NexaraServerState) -> Response {
490    state
491        .metrics
492        .unauthorized_requests
493        .fetch_add(1, Ordering::Relaxed);
494    (
495        StatusCode::UNAUTHORIZED,
496        Json(json!({ "error": "unauthorized" })),
497    )
498        .into_response()
499}
500
501fn error_response(err: nexara_core::NexaraError) -> Response {
502    let status = match err {
503        nexara_core::NexaraError::ToolNotFound => StatusCode::NOT_FOUND,
504        nexara_core::NexaraError::ToolNotAllowed => StatusCode::FORBIDDEN,
505        nexara_core::NexaraError::TrustPolicyDenied(_) => StatusCode::FORBIDDEN,
506        nexara_core::NexaraError::ConfirmationRequired(_) => StatusCode::CONFLICT,
507        nexara_core::NexaraError::InvalidParams(_) => StatusCode::BAD_REQUEST,
508        nexara_core::NexaraError::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
509        nexara_core::NexaraError::ExecutionFailed(_) => StatusCode::INTERNAL_SERVER_ERROR,
510        nexara_core::NexaraError::PayloadTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE,
511        nexara_core::NexaraError::ConcurrencyLimitExceeded => StatusCode::TOO_MANY_REQUESTS,
512        nexara_core::NexaraError::InvalidDescriptor(_) => StatusCode::BAD_REQUEST,
513    };
514    (status, Json(json!({ "error": err.to_string() }))).into_response()
515}
516
517pub fn default_installed_store(path: impl Into<PathBuf>) -> JsonFileInstalledSkillStore {
518    JsonFileInstalledSkillStore::new(path)
519}
520
521pub fn memory_secret_store() -> Arc<dyn SecretStore> {
522    Arc::new(MemorySecretStore::default())
523}