1use crate::llm::actions::{
4 protocol_trait::{ActionResult, Protocol, Server},
5 ActionDefinition, Parameter,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{anyhow, Result};
11use serde_json::{json, Value as JsonValue};
12use std::sync::LazyLock;
13use tracing::{debug, error, warn};
14
15pub static OPENAPI_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
17 EventType::new(
18 "openapi_request",
19 "HTTP request received by OpenAPI server",
20 json!({
21 "type": "send_openapi_response",
22 "status_code": 200,
23 "headers": {"content-type": "application/json"},
24 "body": "[{\"id\": 1, \"title\": \"Buy milk\", \"done\": false}]"
25 }),
26 )
27 .with_alternative_example(json!({
28 "type": "send_validation_error",
29 "status_code": 400,
30 "message": "Field 'title' is required"
31 }))
32 .with_actions(OpenApiProtocol.get_sync_actions())
36 .with_parameters(vec![
37 Parameter {
38 name: "method".to_string(),
39 type_hint: "string".to_string(),
40 description: "HTTP method (GET, POST, etc.)".to_string(),
41 required: true,
42 },
43 Parameter {
44 name: "path".to_string(),
45 type_hint: "string".to_string(),
46 description: "Request path".to_string(),
47 required: true,
48 },
49 Parameter {
50 name: "uri".to_string(),
51 type_hint: "string".to_string(),
52 description: "Full request URI".to_string(),
53 required: true,
54 },
55 Parameter {
56 name: "headers".to_string(),
57 type_hint: "object".to_string(),
58 description: "Request headers as key-value pairs".to_string(),
59 required: false,
60 },
61 Parameter {
62 name: "body".to_string(),
63 type_hint: "string".to_string(),
64 description: "Request body".to_string(),
65 required: false,
66 },
67 Parameter {
68 name: "spec_info".to_string(),
69 type_hint: "object".to_string(),
70 description: "Information about loaded OpenAPI spec".to_string(),
71 required: false,
72 },
73 Parameter {
74 name: "matched_route".to_string(),
75 type_hint: "object".to_string(),
76 description: "Matched route information (only present if route matched). Contains: operation_id, path_template, path_params (extracted parameters like {id}), and operation (full OpenAPI operation spec)".to_string(),
77 required: false,
78 },
79 ])
80});
81
82fn required_status(action: &serde_json::Value, action_name: &str) -> Result<u16> {
90 let value = action
91 .get("status_code")
92 .ok_or_else(|| anyhow!("Missing status_code parameter for {action_name}"))?;
93 let raw = value
94 .as_i64()
95 .ok_or_else(|| anyhow!("{action_name} status_code must be a number, got {value}"))?;
96 if !(100..=599).contains(&raw) {
97 return Err(anyhow!(
98 "{action_name} status_code {raw} is not an HTTP status code; use 100-599 \
99 (400 for a bad request, 404 for an unmatched route, 500 for a server fault)"
100 ));
101 }
102 Ok(raw as u16)
103}
104
105#[derive(Clone)]
107pub struct OpenApiProtocol;
108
109impl OpenApiProtocol {
110 pub fn new() -> Self {
111 Self
112 }
113}
114
115impl Default for OpenApiProtocol {
116 fn default() -> Self {
117 Self::new()
118 }
119}
120
121impl Protocol for OpenApiProtocol {
123 fn protocol_name(&self) -> &'static str {
124 "OpenAPI"
125 }
126 fn stack_name(&self) -> &'static str {
127 "ETH>IP>TCP>HTTP>OPENAPI"
128 }
129 fn keywords(&self) -> Vec<&'static str> {
130 vec!["openapi", "rest", "rest api", "api", "swagger"]
131 }
132 fn get_event_types(&self) -> Vec<EventType> {
133 vec![OPENAPI_REQUEST_EVENT.clone()]
134 }
135 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
136 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
137
138 ProtocolMetadataV2::builder()
139 .state(DevelopmentState::Experimental)
140 .implementation("openapi-rs parser, matchit router, hyper HTTP")
141 .llm_control("All API responses, spec compliance/violations")
142 .e2e_testing("reqwest HTTP client")
143 .notes("Dynamic spec loading, intentional violations for testing")
144 .max_inbound_bytes(crate::server::openapi::MAX_REQUEST_BODY_BYTES)
145 .request_only("An OpenAPI service is HTTP request/response; a server cannot send a peer anything unprompted")
146 .build()
147 }
148 fn description(&self) -> &'static str {
149 "OpenAPI specification server"
150 }
151 fn example_prompt(&self) -> &'static str {
152 "Start an OpenAPI server for a TODO API on port 8080"
153 }
154 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
155 vec![
156 ActionDefinition {
157 name: "reload_spec".to_string(),
158 description: "Reload or update the OpenAPI specification at runtime".to_string(),
159 parameters: vec![
160 Parameter {
161 name: "spec".to_string(),
162 type_hint: "string".to_string(),
163 description: "OpenAPI 3.1 specification in YAML or JSON format".to_string(),
164 required: true,
165 },
166 ],
167 example: serde_json::json!({"type": "reload_spec", "spec": "openapi: 3.1.0\ninfo:\n title: My API\n version: 1.0.0\npaths:\n /users:\n get:\n responses:\n '200':\n description: List users"}),
168 log_template: Some(
169 LogTemplate::new()
170 .with_info("-> OpenAPI spec reloaded")
171 .with_debug("OpenAPI reload_spec: spec_len={spec_len}"),
172 ),
173 },
174 ActionDefinition {
175 name: "get_spec_info".to_string(),
176 description: "Get summary information about the loaded OpenAPI specification".to_string(),
177 parameters: vec![],
178 example: serde_json::json!({"type": "get_spec_info"}),
179 log_template: Some(
180 LogTemplate::new()
181 .with_info("-> OpenAPI spec info requested")
182 .with_debug("OpenAPI get_spec_info"),
183 ),
184 },
185 ActionDefinition {
186 name: "configure_error_handling".to_string(),
187 description: "Configure whether to ask LLM for invalid requests (404/405/400). By default, these errors are handled immediately without LLM. Enable this to let LLM customize error responses for honeypot purposes.".to_string(),
188 parameters: vec![
189 Parameter {
190 name: "llm_on_invalid".to_string(),
191 type_hint: "boolean".to_string(),
192 description: "If true, ask LLM for 404/405/400 errors. If false (default), return immediate RFC-compliant error responses.".to_string(),
193 required: true,
194 },
195 ],
196 example: serde_json::json!({"type": "configure_error_handling", "llm_on_invalid": false}),
197 log_template: Some(
198 LogTemplate::new()
199 .with_info("-> OpenAPI error handling: llm_on_invalid={llm_on_invalid}")
200 .with_debug("OpenAPI configure_error_handling: llm_on_invalid={llm_on_invalid}"),
201 ),
202 },
203 ]
204 }
205 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
206 vec![
207 ActionDefinition {
208 name: "provide_openapi_spec".to_string(),
209 description: "Provide the OpenAPI specification (used during server startup)".to_string(),
210 parameters: vec![
211 Parameter {
212 name: "spec".to_string(),
213 type_hint: "string".to_string(),
214 description: "OpenAPI 3.1 specification in YAML or JSON format".to_string(),
215 required: true,
216 },
217 ],
218 example: serde_json::json!({"type": "provide_openapi_spec", "spec": "openapi: 3.1.0\ninfo:\n title: TODO API\n version: 1.0.0\npaths:\n /todos:\n get:\n operationId: listTodos\n responses:\n '200':\n description: List of todos\n content:\n application/json:\n schema:\n type: array\n items:\n type: object\n properties:\n id:\n type: integer\n title:\n type: string\n done:\n type: boolean"}),
219 log_template: Some(
220 LogTemplate::new()
221 .with_info("-> OpenAPI spec provided")
222 .with_debug("OpenAPI provide_openapi_spec: spec_len={spec_len}"),
223 ),
224 },
225 ActionDefinition {
226 name: "send_openapi_response".to_string(),
227 description: "Send an HTTP response for an OpenAPI request".to_string(),
228 parameters: vec![
229 Parameter {
230 name: "status_code".to_string(),
231 type_hint: "number".to_string(),
232 description: "HTTP status code (e.g., 200, 404, 500)".to_string(),
233 required: true,
234 },
235 Parameter {
236 name: "headers".to_string(),
237 type_hint: "object".to_string(),
238 description: "HTTP response headers as key-value pairs".to_string(),
239 required: false,
240 },
241 Parameter {
242 name: "body".to_string(),
243 type_hint: "string".to_string(),
244 description: "Response body (JSON, XML, plain text, etc.)".to_string(),
245 required: false,
246 },
247 Parameter {
248 name: "spec_compliant".to_string(),
249 type_hint: "boolean".to_string(),
250 description: "Set false to record that this response deliberately violates the loaded spec (for testing how a client handles a misbehaving server). Only affects logging; the response is sent either way. Default true.".to_string(),
251 required: false,
252 },
253 ],
254 example: serde_json::json!({"type": "send_openapi_response", "status_code": 200, "headers": {"content-type": "application/json"}, "body": "[{\"id\": 1, \"title\": \"Buy milk\", \"done\": false}]"}),
255 log_template: Some(
256 LogTemplate::new()
257 .with_info("-> OpenAPI {status_code} ({body_len}B)")
258 .with_debug("OpenAPI send_openapi_response: status={status_code} body_len={body_len}"),
259 ),
260 },
261 ActionDefinition {
262 name: "send_validation_error".to_string(),
263 description: "Send an HTTP error response for invalid requests (400, 405, 415, etc.)".to_string(),
264 parameters: vec![
265 Parameter {
266 name: "status_code".to_string(),
267 type_hint: "number".to_string(),
268 description: "HTTP error status code (400=Bad Request, 405=Method Not Allowed, 415=Unsupported Media Type, etc.)".to_string(),
269 required: true,
270 },
271 Parameter {
272 name: "message".to_string(),
273 type_hint: "string".to_string(),
274 description: "Error message explaining the validation failure".to_string(),
275 required: true,
276 },
277 ],
278 example: serde_json::json!({"type": "send_validation_error", "status_code": 405, "message": "Method GET not allowed for path /users, expected POST"}),
279 log_template: Some(
280 LogTemplate::new()
281 .with_info("-> OpenAPI {status_code}: {message}")
282 .with_debug("OpenAPI send_validation_error: status={status_code} message={message}"),
283 ),
284 },
285 ]
286 }
287 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
288 use crate::llm::actions::ParameterDefinition;
289 vec![ParameterDefinition {
297 name: "spec".to_string(),
298 type_hint: "string".to_string(),
299 description:
300 "OpenAPI 3.x specification in YAML or JSON format. Inline content only — to serve \
301 a spec that lives on disk, read the file and pass its contents here."
302 .to_string(),
303 required: false,
304 example: serde_json::json!("openapi: 3.1.0\ninfo:\n title: My API\n version: 1.0.0"),
305 default: None,
306 }]
307 }
308 fn group_name(&self) -> &'static str {
309 "AI & API"
310 }
311 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
312 use crate::llm::actions::StartupExamples;
313
314 StartupExamples::new(
315 json!({
317 "type": "open_server",
318 "port": 8080,
319 "base_stack": "openapi",
320 "instruction": "OpenAPI server for TODO API. GET /todos returns list, POST /todos creates item, GET /todos/{id} returns single item",
321 "startup_params": {
322 "spec": "openapi: 3.1.0\ninfo:\n title: TODO API\n version: 1.0.0\npaths:\n /todos:\n get:\n operationId: listTodos\n responses:\n '200':\n description: List of todos"
323 }
324 }),
325 json!({
327 "type": "open_server",
328 "port": 8080,
329 "base_stack": "openapi",
330 "startup_params": {
331 "spec": "openapi: 3.1.0\ninfo:\n title: API\n version: 1.0.0\npaths:\n /items:\n get:\n operationId: listItems\n responses:\n '200':\n description: OK"
332 },
333 "event_handlers": [{
334 "event_pattern": "openapi_request",
335 "handler": {
336 "type": "script",
337 "language": "python",
338 "code": "path = event.get('path', '')\nif path == '/items':\n action('send_openapi_response', status_code=200, headers={'content-type': 'application/json'}, body='[{\"id\": 1}]')\nelse:\n action('send_openapi_response', status_code=404, body='{\"error\": \"Not found\"}')"
339 }
340 }]
341 }),
342 json!({
344 "type": "open_server",
345 "port": 8080,
346 "base_stack": "openapi",
347 "startup_params": {
348 "spec": "openapi: 3.1.0\ninfo:\n title: Health API\n version: 1.0.0\npaths:\n /health:\n get:\n operationId: healthCheck\n responses:\n '200':\n description: OK"
349 },
350 "event_handlers": [{
351 "event_pattern": "openapi_request",
352 "handler": {
353 "type": "static",
354 "actions": [{
355 "type": "send_openapi_response",
356 "status_code": 200,
357 "headers": {"content-type": "application/json"},
358 "body": "{\"status\": \"healthy\"}"
359 }]
360 }
361 }]
362 }),
363 )
364 }
365}
366
367impl Server for OpenApiProtocol {
369 fn spawn(
370 &self,
371 ctx: crate::protocol::SpawnContext,
372 ) -> std::pin::Pin<
373 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
374 > {
375 Box::pin(async move {
376 use crate::server::openapi::OpenApiServer;
377 OpenApiServer::spawn_with_llm_actions(
378 ctx.legacy_listen_addr(),
379 ctx.llm_client,
380 ctx.state,
381 ctx.status_tx,
382 ctx.server_id,
383 ctx.startup_params,
384 )
385 .await
386 })
387 }
388 fn execute_action(&self, action: JsonValue) -> Result<ActionResult> {
389 let action_type = action["type"]
390 .as_str()
391 .ok_or_else(|| anyhow!("Missing action type"))?;
392
393 match action_type {
394 "provide_openapi_spec" => {
395 let spec = action["spec"]
396 .as_str()
397 .ok_or_else(|| anyhow!("Missing spec parameter"))?;
398
399 debug!("OpenAPI spec provided: {} bytes", spec.len());
400
401 Ok(ActionResult::Custom {
402 name: "load_openapi_spec".to_string(),
403 data: serde_json::json!({
404 "spec": spec
405 }),
406 })
407 }
408 "send_openapi_response" => {
409 let status_code = required_status(&action, "send_openapi_response")?;
410
411 let headers = action["headers"].as_object().cloned().unwrap_or_default();
412
413 let body = action["body"].as_str().unwrap_or("").to_string();
414
415 let spec_compliant = action["spec_compliant"].as_bool().unwrap_or(true);
416
417 let compliance_status = if spec_compliant {
418 "compliant"
419 } else {
420 "non-compliant (intentional)"
421 };
422 debug!(
423 "OpenAPI response: {} {} bytes, spec: {}",
424 status_code,
425 body.len(),
426 compliance_status
427 );
428
429 Ok(ActionResult::Custom {
430 name: "send_openapi_response".to_string(),
431 data: serde_json::json!({
432 "status_code": status_code,
433 "headers": headers,
434 "body": body,
435 "spec_compliant": spec_compliant
436 }),
437 })
438 }
439 "send_validation_error" => {
440 let status_code = required_status(&action, "send_validation_error")?;
441
442 let message = action["message"]
443 .as_str()
444 .ok_or_else(|| anyhow!("Missing message parameter"))?;
445
446 warn!("OpenAPI validation error: {} - {}", status_code, message);
447
448 Ok(ActionResult::Custom {
449 name: "send_validation_error".to_string(),
450 data: serde_json::json!({
451 "status_code": status_code,
452 "message": message
453 }),
454 })
455 }
456 "reload_spec" => {
457 let spec = action["spec"]
458 .as_str()
459 .ok_or_else(|| anyhow!("Missing spec parameter"))?;
460
461 debug!("OpenAPI spec reload requested: {} bytes", spec.len());
462
463 Ok(ActionResult::Custom {
464 name: "reload_spec".to_string(),
465 data: serde_json::json!({
466 "spec": spec,
467 "reload": true
468 }),
469 })
470 }
471 "get_spec_info" => {
472 debug!("OpenAPI spec info requested");
473 Ok(ActionResult::NoAction)
474 }
475 "configure_error_handling" => {
476 let llm_on_invalid = action["llm_on_invalid"]
477 .as_bool()
478 .ok_or_else(|| anyhow!("Missing llm_on_invalid parameter"))?;
479
480 debug!(
481 "OpenAPI error handling configured: llm_on_invalid={}",
482 llm_on_invalid
483 );
484
485 Ok(ActionResult::Custom {
486 name: "configure_error_handling".to_string(),
487 data: serde_json::json!({
488 "llm_on_invalid": llm_on_invalid
489 }),
490 })
491 }
492 _ => {
493 error!("Unknown OpenAPI action: {}", action_type);
494 Err(anyhow!("Unknown action type: {}", action_type))
495 }
496 }
497 }
498}