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}