1use crate::llm::actions::{
6 protocol_trait::{ActionResult, Protocol, Server},
7 ActionDefinition, Parameter, ParameterDefinition,
8};
9use crate::protocol::log_template::LogTemplate;
10use crate::protocol::EventType;
11use crate::state::app_state::AppState;
12use anyhow::Result;
13use serde_json::{json, Value};
14use std::sync::LazyLock;
15
16pub struct SqsProtocol {
18 }
20
21impl SqsProtocol {
22 pub fn new() -> Self {
23 Self {}
24 }
25}
26
27pub static SQS_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
29 EventType::new(
30 "sqs_request",
31 "An SQS API request arrived. Answer in the **AWS JSON protocol**: this server replies \
32 with Content-Type application/x-amz-json-1.0, so the body is a JSON object \
33 (`{\"MessageId\": \"...\"}`). SQS also has an older Query API that answers with XML \
34 (<SendMessageResponse xmlns=\"http://queue.amazonaws.com/doc/2012-11-05/\">…) — that \
35 is a different protocol and a client here cannot parse it, however well-formed it is.",
36 json!({
37 "type": "send_sqs_response",
38 "status_code": 200,
39 "body": "{\"MessageId\": \"59c5d728-17e6-4355-a298-1a9364790137\", \"MD5OfMessageBody\": \"5d41402abc4b2a76b9719d911017c592\"}"
40 }),
41 )
42 .with_parameters(vec![
43 Parameter {
44 name: "operation".to_string(),
45 type_hint: "string".to_string(),
46 description: "SQS operation (SendMessage, ReceiveMessage, CreateQueue, etc.)"
47 .to_string(),
48 required: true,
49 },
50 Parameter {
51 name: "queue_url".to_string(),
52 type_hint: "string".to_string(),
53 description: "Target queue URL (if available)".to_string(),
54 required: false,
55 },
56 Parameter {
57 name: "request_body".to_string(),
58 type_hint: "string".to_string(),
59 description: "JSON request body".to_string(),
60 required: true,
61 },
62 ])
63 .with_actions(vec![send_sqs_response_action()])
64 .with_log_template(
69 LogTemplate::new()
70 .with_info("SQS {operation} {queue_url}")
71 .with_debug("SQS {operation} queue={queue_url}")
72 .with_trace("SQS request: {json_pretty(.)}"),
73 )
74});
75
76fn send_sqs_response_action() -> ActionDefinition {
77 ActionDefinition {
78 name: "send_sqs_response".to_string(),
79 description: "Send an SQS response. The body is sent as \
80 Content-Type: application/x-amz-json-1.0, so it must be a JSON object — \
81 never the legacy Query API's XML, which a client on this endpoint cannot \
82 parse."
83 .to_string(),
84 parameters: vec![
85 Parameter {
86 name: "status_code".to_string(),
87 type_hint: "number".to_string(),
88 description: "HTTP status code (200, 400, 500, etc.)".to_string(),
89 required: true,
90 },
91 Parameter {
92 name: "body".to_string(),
93 type_hint: "string".to_string(),
94 description: "Response body as a JSON object, serialized to a string. This is \
95 the AWS JSON protocol (application/x-amz-json-1.0): a SendMessage \
96 reply is {\"MessageId\": \"...\", \"MD5OfMessageBody\": \"...\"}, \
97 not an <SendMessageResponse> XML document."
98 .to_string(),
99 required: true,
100 },
101 ],
102 example: serde_json::json!({
103 "type": "send_sqs_response",
104 "status_code": 200,
105 "body": "{\"MessageId\": \"msg-123\", \"MD5OfMessageBody\": \"d41d8cd98f00b204e9800998ecf8427e\"}"
106 }),
107 log_template: Some(
108 LogTemplate::new()
109 .with_info("-> {status_code} ({body_len}B)")
110 .with_debug("SQS response: status={status_code}")
111 .with_trace("SQS response body: {body}"),
112 ),
113 }
114}
115
116pub(crate) fn parse_status_code(action: &Value) -> Result<u16> {
123 let raw = action
124 .get("status_code")
125 .and_then(|v| v.as_u64())
126 .ok_or_else(|| anyhow::anyhow!("Missing or invalid status_code: expected a number"))?;
127
128 if !(100..=599).contains(&raw) {
129 return Err(anyhow::anyhow!(
130 "Invalid status_code {raw}: must be an HTTP status between 100 and 599. \
131 SQS uses 200 for success and 400 with an {{\"__type\": ..., \"message\": ...}} \
132 body for client errors."
133 ));
134 }
135
136 Ok(raw as u16)
137}
138
139pub fn get_sqs_event_types() -> Vec<EventType> {
140 vec![SQS_REQUEST_EVENT.clone()]
141}
142
143impl Protocol for SqsProtocol {
145 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
146 vec![]
152 }
153 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
154 vec![]
156 }
157 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
158 vec![send_sqs_response_action()]
159 }
160 fn protocol_name(&self) -> &'static str {
161 "SQS"
162 }
163 fn get_event_types(&self) -> Vec<EventType> {
164 get_sqs_event_types()
165 }
166 fn stack_name(&self) -> &'static str {
167 "ETH>IP>TCP>HTTP>SQS"
168 }
169 fn keywords(&self) -> Vec<&'static str> {
170 vec!["sqs", "message queue", "sqs queue", "aws sqs"]
173 }
174 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
175 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
176
177 ProtocolMetadataV2::builder()
178 .state(DevelopmentState::Beta)
182 .implementation("hyper v1.5 HTTP with AWS JSON protocol")
183 .llm_control("All SQS operations (SendMessage, ReceiveMessage, DeleteMessage)")
184 .e2e_testing(
185 "TWO independent clients, neither #[ignore]d and neither able to skip. \
186 (1) aws-sdk-sqs, the official Rust SDK, in tests/server/sqs/e2e_test.rs. \
187 (2) the real `aws` CLI (botocore, Python -- a different SDK generation and a \
188 different serialiser) in tests/server/sqs/real_client_test.rs, completing \
189 CreateQueue, SendMessage and ReceiveMessage and asserting on what it PRINTED. \
190 Rendering is what the second client adds: a field name or JSON shape the Rust \
191 SDK tolerates through its generated deserialiser shows up there as a missing \
192 column. Verified by renaming MessageId to messageId in the reply, which the \
193 test then reports. \
194 THE TEST CANNOT REACH REAL AWS, and that is enforced rather than assumed: \
195 --endpoint-url is passed on every invocation, the port is asserted non-zero \
196 before the CLI is spawned, and credentials, region, profile and the EC2 \
197 metadata service are overridden in the child's environment. The reason is in \
198 the project CLAUDE.md -- the DynamoDB CLIENT once dropped its target, let the \
199 SDK resolve the production endpoint, and signed with ambient credentials. \
200 UNPROVEN, and worth saying plainly: NO SIGNATURE IS VALIDATED. This server \
201 serves every request unconditionally and does not put Authorization or \
202 X-Amz-Date into the event, so the model cannot make that decision either. \
203 Also unproven: long polling, batch operations, dead-letter queues, FIFO \
204 queues, and the query-protocol wire format older SDKs use.",
205 )
206 .notes("Virtual queues (no persistence); no auth; visibility timeouts are the LLM's job, the server tracks nothing")
207 .max_inbound_bytes(crate::server::sqs::MAX_REQUEST_BYTES)
208 .request_only("SQS is HTTP request/response; a server cannot send a peer anything unprompted")
209 .build()
210 }
211 fn description(&self) -> &'static str {
212 "AWS SQS-compatible message queue server"
213 }
214 fn example_prompt(&self) -> &'static str {
215 "Start an AWS SQS-compatible queue server on port 9324"
216 }
217 fn group_name(&self) -> &'static str {
218 "Database"
219 }
220
221 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
222 use crate::llm::actions::StartupExamples;
223
224 let script = r#"import json, sys
227data = json.load(sys.stdin)
228event = data["event"]
229if data["event_type_id"] == "sqs_request":
230 op = event.get("operation", "")
231 if op == "SendMessage":
232 body = json.dumps({"MessageId": "00000000-0000-0000-0000-000000000001",
233 "MD5OfMessageBody": "d41d8cd98f00b204e9800998ecf8427e"})
234 elif op == "ReceiveMessage":
235 body = json.dumps({"Messages": []})
236 else:
237 body = json.dumps({})
238 actions = [{"type": "send_sqs_response", "status_code": 200, "body": body}]
239else:
240 actions = []
241print(json.dumps({"actions": actions}))"#;
242
243 StartupExamples::new(
244 json!({
246 "type": "open_server",
247 "port": 9324,
248 "base_stack": "sqs",
249 "instruction": "Act as an SQS queue named 'jobs'. Accept SendMessage requests and remember each message body in order; on ReceiveMessage return the oldest not-yet-received message with a fresh receipt handle, and an empty response once the queue is drained."
250 }),
251 json!({
253 "type": "open_server",
254 "port": 9324,
255 "base_stack": "sqs",
256 "event_handlers": [{
257 "event_pattern": "sqs_request",
258 "handler": {
259 "type": "script",
260 "language": "python",
261 "code": script
262 }
263 }]
264 }),
265 json!({
267 "type": "open_server",
268 "port": 9324,
269 "base_stack": "sqs",
270 "event_handlers": [{
271 "event_pattern": "sqs_request",
272 "handler": {
273 "type": "static",
274 "actions": [{
275 "type": "send_sqs_response",
276 "status_code": 200,
277 "body": "{}"
278 }]
279 }
280 }]
281 }),
282 )
283 }
284}
285
286impl Server for SqsProtocol {
288 fn spawn(
289 &self,
290 ctx: crate::protocol::SpawnContext,
291 ) -> std::pin::Pin<
292 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
293 > {
294 Box::pin(async move {
295 use crate::server::sqs::SqsServer;
296 SqsServer::spawn_with_llm_actions(
297 ctx.legacy_listen_addr(),
298 ctx.llm_client,
299 ctx.state,
300 ctx.status_tx,
301 false,
302 ctx.server_id,
303 )
304 .await
305 })
306 }
307 fn execute_action(&self, action: Value) -> Result<ActionResult> {
308 let action_type = action
309 .get("type")
310 .and_then(|v| v.as_str())
311 .ok_or_else(|| anyhow::anyhow!("Missing action type"))?;
312
313 match action_type {
314 "send_sqs_response" => {
315 let status_code = parse_status_code(&action)?;
316
317 let body = action
318 .get("body")
319 .and_then(|v| v.as_str())
320 .ok_or_else(|| anyhow::anyhow!("Missing body"))?
321 .to_string();
322
323 Ok(ActionResult::Custom {
324 name: "sqs_response".to_string(),
325 data: json!({
326 "status": status_code,
327 "body": body
328 }),
329 })
330 }
331 _ => Err(anyhow::anyhow!("Unknown action type: {}", action_type)),
332 }
333 }
334}