1use axum::{
7 Json,
8 response::{IntoResponse, Redirect},
9};
10
11pub fn openapi_spec() -> serde_json::Value {
16 serde_json::json!({
17 "openapi": "3.1.0",
18 "info": {
19 "title": "Roboticus API",
20 "version": env!("CARGO_PKG_VERSION"),
21 "description": "Autonomous agent runtime API — sessions, messages, tools, routing, and operator controls."
22 },
23 "servers": [
24 { "url": "/", "description": "Local instance" }
25 ],
26 "paths": {
27 "/api/agent/message": {
28 "post": {
29 "summary": "Send a message to the agent",
30 "tags": ["Agent"],
31 "requestBody": {
32 "required": true,
33 "content": {
34 "application/json": {
35 "schema": {
36 "type": "object",
37 "properties": {
38 "message": { "type": "string" },
39 "session_id": { "type": "string" }
40 },
41 "required": ["message"]
42 }
43 }
44 }
45 },
46 "responses": {
47 "200": { "description": "Agent response" }
48 }
49 }
50 },
51 "/api/agent/message/stream": {
52 "post": {
53 "summary": "Stream a message response via SSE",
54 "tags": ["Agent"],
55 "responses": {
56 "200": { "description": "SSE token stream" }
57 }
58 }
59 },
60 "/api/sessions": {
61 "get": {
62 "summary": "List sessions",
63 "tags": ["Sessions"],
64 "responses": {
65 "200": { "description": "Array of sessions" }
66 }
67 }
68 },
69 "/api/health": {
70 "get": {
71 "summary": "Health check",
72 "tags": ["System"],
73 "responses": {
74 "200": { "description": "Service healthy" }
75 }
76 }
77 },
78 "/api/config": {
79 "get": {
80 "summary": "Get current configuration",
81 "tags": ["Config"],
82 "responses": {
83 "200": { "description": "Current config as JSON" }
84 }
85 },
86 "post": {
87 "summary": "Apply configuration changes",
88 "tags": ["Config"],
89 "responses": {
90 "200": { "description": "Config applied" }
91 }
92 }
93 },
94 "/api/models/routing-diagnostics": {
95 "get": {
96 "summary": "Get routing diagnostics",
97 "tags": ["Models"],
98 "responses": {
99 "200": { "description": "Routing decision trace" }
100 }
101 }
102 },
103 "/api/approvals": {
104 "get": {
105 "summary": "List pending approvals",
106 "tags": ["Approvals"],
107 "responses": {
108 "200": { "description": "Array of pending approval requests" }
109 }
110 }
111 },
112 "/api/stats/efficiency": {
113 "get": {
114 "summary": "Context efficiency analytics",
115 "tags": ["Stats"],
116 "responses": {
117 "200": { "description": "Efficiency metrics" }
118 }
119 }
120 },
121 "/api/channels/{platform}/test": {
122 "post": {
123 "summary": "Test channel connectivity",
124 "tags": ["Channels"],
125 "parameters": [{
126 "name": "platform",
127 "in": "path",
128 "required": true,
129 "schema": { "type": "string" }
130 }],
131 "responses": {
132 "200": { "description": "Channel test result" }
133 }
134 }
135 }
136 },
137 "components": {
138 "securitySchemes": {
139 "apiKey": {
140 "type": "apiKey",
141 "in": "header",
142 "name": "X-API-Key"
143 },
144 "bearer": {
145 "type": "http",
146 "scheme": "bearer"
147 }
148 }
149 }
150 })
151}
152
153pub async fn get_openapi_spec() -> impl IntoResponse {
155 Json(openapi_spec())
156}
157
158pub async fn get_docs_redirect() -> impl IntoResponse {
160 Redirect::temporary("/openapi.json")
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn spec_is_valid_json() {
170 let spec = openapi_spec();
171 assert_eq!(spec["openapi"], "3.1.0");
172 assert!(
173 spec["info"]["title"]
174 .as_str()
175 .unwrap()
176 .contains("Roboticus")
177 );
178 assert!(spec["paths"].as_object().unwrap().len() >= 5);
179 }
180
181 #[test]
182 fn spec_version_from_cargo() {
183 let spec = openapi_spec();
184 let version = spec["info"]["version"].as_str().unwrap();
185 assert!(!version.is_empty());
186 }
187}