1use crate::llm::actions::{
14 protocol_trait::{ActionResult, Protocol, Server},
15 ActionDefinition, Parameter,
16};
17use crate::protocol::EventType;
18use crate::state::app_state::AppState;
19use anyhow::{Context, Result};
20use serde_json::json;
21use std::sync::LazyLock;
22
23use super::protocol::{self, ValueItem};
24
25pub struct MemcachedProtocol;
26
27impl Default for MemcachedProtocol {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl MemcachedProtocol {
34 pub fn new() -> Self {
35 Self
36 }
37}
38
39fn p(name: &str, type_hint: &str, description: &str) -> Parameter {
40 Parameter {
41 name: name.to_string(),
42 type_hint: type_hint.to_string(),
43 description: description.to_string(),
44 required: false,
45 }
46}
47
48fn required(name: &str, type_hint: &str, description: &str) -> Parameter {
49 Parameter {
50 name: name.to_string(),
51 type_hint: type_hint.to_string(),
52 description: description.to_string(),
53 required: true,
54 }
55}
56
57fn decode_value(entry: &serde_json::Value, key: &str, encoding_key: &str) -> Result<Vec<u8>> {
64 let raw = entry
65 .get(key)
66 .and_then(|v| v.as_str())
67 .with_context(|| format!("missing '{}'", key))?;
68 match entry
69 .get(encoding_key)
70 .and_then(|v| v.as_str())
71 .unwrap_or("utf8")
72 {
73 "utf8" => Ok(raw.as_bytes().to_vec()),
74 "hex" => Ok(hex::decode(raw)
75 .with_context(|| format!("'{}' is declared hex but is not valid hex", key))?),
76 other => Err(anyhow::anyhow!(
77 "Unknown {} '{}': use \"utf8\" or \"hex\"",
78 encoding_key,
79 other
80 )),
81 }
82}
83
84pub static MEMCACHED_GET_EVENT: LazyLock<EventType> = LazyLock::new(|| {
89 EventType::new(
90 "memcached_get",
91 "A client asked for one or more keys. Answer each requested key your instruction gives \
92 a value; a key it does not is absent and is left out. Returning an empty values list \
93 is a cache miss. answer_with says which keys may appear.",
94 json!({
95 "type": "send_memcached_values",
96 "values": [{"key": "example-key", "value": "example-value"}]
97 }),
98 )
99 .with_parameters(vec![
100 p(
101 "command",
102 "string",
103 "'get' or 'gets'; 'gets' also wants a cas_unique per value",
104 ),
105 p("keys", "array", "Keys requested, in order"),
106 p(
107 "answer_with",
108 "string",
109 "The action, and the only keys the answer may carry",
110 ),
111 ])
112 .with_actions(vec![
113 send_values_action(),
114 send_error_action(),
115 close_connection_action(),
116 ])
117});
118
119pub static MEMCACHED_STORE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
120 EventType::new(
121 "memcached_store",
122 "A client wants to store an item. Decide whether the store succeeds. This server \
123 keeps nothing, so 'succeeds' means whatever your instruction says it should mean.",
124 json!({ "type": "send_memcached_status", "status": "STORED" }),
125 )
126 .with_parameters(vec![
127 p(
128 "command",
129 "string",
130 "'set', 'add', 'replace', 'append', 'prepend' or 'cas'",
131 ),
132 p("key", "string", "Key being stored"),
133 p(
134 "flags",
135 "number",
136 "Opaque 32-bit flags the client asked to be stored with the item",
137 ),
138 p(
139 "exptime",
140 "number",
141 "Requested expiry: 0 = never, <0 = immediately expired",
142 ),
143 p(
144 "bytes",
145 "number",
146 "Length of the data block, as counted off the wire",
147 ),
148 p("cas_unique", "number", "Present only for 'cas'"),
149 p("value", "string", "The data block"),
150 p(
151 "value_encoding",
152 "string",
153 "'utf8' if the data block was valid UTF-8, else 'hex'",
154 ),
155 ])
156 .with_actions(vec![
157 send_status_action(),
158 send_error_action(),
159 close_connection_action(),
160 ])
161});
162
163pub static MEMCACHED_DELETE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
164 EventType::new(
165 "memcached_delete",
166 "A client asked to delete a key. Answer DELETED or NOT_FOUND.",
167 json!({ "type": "send_memcached_status", "status": "DELETED" }),
168 )
169 .with_parameters(vec![p("key", "string", "Key to delete")])
170 .with_actions(vec![
171 send_status_action(),
172 send_error_action(),
173 close_connection_action(),
174 ])
175});
176
177pub static MEMCACHED_ARITHMETIC_EVENT: LazyLock<EventType> = LazyLock::new(|| {
178 EventType::new(
179 "memcached_arithmetic",
180 "A client asked to increment or decrement a counter. Answer with the new value, \
181 NOT_FOUND, or a CLIENT_ERROR if the key does not hold a number.",
182 json!({ "type": "send_memcached_number", "value": 43 }),
183 )
184 .with_parameters(vec![
185 p("command", "string", "'incr' or 'decr'"),
186 p("key", "string", "Counter key"),
187 p("delta", "number", "Amount to add or subtract"),
188 ])
189 .with_actions(vec![
190 send_number_action(),
191 send_status_action(),
192 send_error_action(),
193 close_connection_action(),
194 ])
195});
196
197pub static MEMCACHED_TOUCH_EVENT: LazyLock<EventType> = LazyLock::new(|| {
198 EventType::new(
199 "memcached_touch",
200 "A client asked to update a key's expiry without fetching it. Answer TOUCHED or \
201 NOT_FOUND.",
202 json!({ "type": "send_memcached_status", "status": "TOUCHED" }),
203 )
204 .with_parameters(vec![
205 p("key", "string", "Key to touch"),
206 p("exptime", "number", "New expiry"),
207 ])
208 .with_actions(vec![
209 send_status_action(),
210 send_error_action(),
211 close_connection_action(),
212 ])
213});
214
215pub static MEMCACHED_STATS_EVENT: LazyLock<EventType> = LazyLock::new(|| {
216 EventType::new(
217 "memcached_stats",
218 "A client asked for server statistics. Report what your instruction says about this \
219 server - its version, how many items it holds - under memcached's stat names, and \
220 fill the rest plausibly.",
221 json!({
222 "type": "send_memcached_stats",
223 "stats": {"version": "1.6.0", "curr_items": "0", "uptime": "0"}
224 }),
225 )
226 .with_parameters(vec![
227 p(
228 "argument",
229 "string",
230 "Sub-command such as 'items', 'slabs', 'sizes', or null for the general stats",
231 ),
232 p(
233 "answer_with",
234 "string",
235 "The action, and which stat names carry what the instruction says",
236 ),
237 ])
238 .with_actions(vec![
239 send_stats_action(),
240 send_error_action(),
241 close_connection_action(),
242 ])
243});
244
245pub static MEMCACHED_VERSION_EVENT: LazyLock<EventType> = LazyLock::new(|| {
246 EventType::new(
247 "memcached_version",
248 "A client asked which memcached version this is. Answer with whatever version you \
249 want to claim to be.",
250 json!({ "type": "send_memcached_version", "version": "1.6.45" }),
251 )
252 .with_parameters(vec![])
253 .with_actions(vec![
254 send_version_action(),
255 send_error_action(),
256 close_connection_action(),
257 ])
258});
259
260pub static MEMCACHED_FLUSH_ALL_EVENT: LazyLock<EventType> = LazyLock::new(|| {
261 EventType::new(
262 "memcached_flush_all",
263 "A client asked to invalidate everything. Answer OK, or refuse with an error.",
264 json!({ "type": "send_memcached_status", "status": "OK" }),
265 )
266 .with_parameters(vec![p(
267 "delay",
268 "number",
269 "Seconds until the flush takes effect; 0 means immediately",
270 )])
271 .with_actions(vec![
272 send_status_action(),
273 send_error_action(),
274 close_connection_action(),
275 ])
276});
277
278pub static MEMCACHED_UNKNOWN_COMMAND_EVENT: LazyLock<EventType> = LazyLock::new(|| {
279 EventType::new(
280 "memcached_unknown_command",
281 "A command line this server does not recognise. Real memcached answers ERROR; you \
282 may instead answer as though the verb existed.",
283 json!({ "type": "send_memcached_error", "kind": "ERROR" }),
284 )
285 .with_parameters(vec![p("line", "string", "The command line as received")])
286 .with_actions(vec![
287 send_error_action(),
288 send_status_action(),
289 send_values_action(),
290 close_connection_action(),
291 ])
292});
293
294fn send_values_action() -> ActionDefinition {
299 ActionDefinition {
300 name: "send_memcached_values".to_string(),
301 description: "Answer a get/gets. Each entry becomes a VALUE block; an empty list is \
302 a cache miss (END with no values). Only keys the client asked for may \
303 appear - NetGet drops any other. The byte count in each VALUE header is \
304 computed from the payload — do not supply one. The example's keys and \
305 values are placeholders."
306 .to_string(),
307 parameters: vec![required(
308 "values",
309 "array",
310 "[{key, value, value_encoding ('utf8' default or 'hex'), flags (default 0), \
311 cas_unique (gets only)}]. Omit keys you want to report as missing.",
312 )],
313 example: json!({
314 "type": "send_memcached_values",
315 "values": [
316 {"key": "example-key", "value": "example-value", "flags": 0},
317 {"key": "example-blob", "value": "0001ff", "value_encoding": "hex"}
318 ]
319 }),
320 log_template: None,
321 }
322}
323
324fn send_status_action() -> ActionDefinition {
325 ActionDefinition {
326 name: "send_memcached_status".to_string(),
327 description: "Answer with one of the protocol's fixed status lines.".to_string(),
328 parameters: vec![required(
329 "status",
330 "string",
331 "One of STORED, NOT_STORED, EXISTS, NOT_FOUND, DELETED, TOUCHED, OK, ERROR",
332 )],
333 example: json!({ "type": "send_memcached_status", "status": "STORED" }),
334 log_template: None,
335 }
336}
337
338fn send_number_action() -> ActionDefinition {
339 ActionDefinition {
340 name: "send_memcached_number".to_string(),
341 description: "Answer an incr/decr with the counter's new value.".to_string(),
342 parameters: vec![required(
343 "value",
344 "number",
345 "New counter value (64-bit unsigned)",
346 )],
347 example: json!({ "type": "send_memcached_number", "value": 43 }),
348 log_template: None,
349 }
350}
351
352fn send_stats_action() -> ActionDefinition {
353 ActionDefinition {
354 name: "send_memcached_stats".to_string(),
355 description: "Answer a stats command. Each entry becomes a STAT line, terminated by \
356 END. Use memcached's own stat names (version, curr_items, total_items, \
357 bytes, uptime, pid); the example's values are placeholders for the ones \
358 your instruction gives."
359 .to_string(),
360 parameters: vec![required(
361 "stats",
362 "object",
363 "Flat map of stat name to value; values are rendered as text",
364 )],
365 example: json!({
366 "type": "send_memcached_stats",
367 "stats": {"version": "1.6.0", "curr_items": "0", "uptime": "0"}
368 }),
369 log_template: None,
370 }
371}
372
373fn send_version_action() -> ActionDefinition {
374 ActionDefinition {
375 name: "send_memcached_version".to_string(),
376 description: "Answer a version command.".to_string(),
377 parameters: vec![required("version", "string", "Version string to claim")],
378 example: json!({ "type": "send_memcached_version", "version": "1.6.45" }),
379 log_template: None,
380 }
381}
382
383fn send_error_action() -> ActionDefinition {
384 ActionDefinition {
385 name: "send_memcached_error".to_string(),
386 description: "Answer with a protocol error. 'ERROR' means the command is not \
387 understood; 'CLIENT_ERROR' means the client got the request wrong; \
388 'SERVER_ERROR' means this server failed."
389 .to_string(),
390 parameters: vec![
391 required("kind", "string", "ERROR, CLIENT_ERROR or SERVER_ERROR"),
392 p(
393 "message",
394 "string",
395 "Explanation; required for CLIENT_ERROR and SERVER_ERROR, ignored for ERROR",
396 ),
397 ],
398 example: json!({
399 "type": "send_memcached_error",
400 "kind": "CLIENT_ERROR",
401 "message": "cannot increment or decrement non-numeric value"
402 }),
403 log_template: None,
404 }
405}
406
407fn close_connection_action() -> ActionDefinition {
408 ActionDefinition {
409 name: "close_memcached_connection".to_string(),
410 description: "Close the client connection without sending anything further.".to_string(),
411 parameters: vec![],
412 example: json!({ "type": "close_memcached_connection" }),
413 log_template: None,
414 }
415}
416
417impl Protocol for MemcachedProtocol {
422 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
423 Vec::new()
424 }
425
426 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
427 vec![
428 send_values_action(),
429 send_status_action(),
430 send_number_action(),
431 send_stats_action(),
432 send_version_action(),
433 send_error_action(),
434 close_connection_action(),
435 ]
436 }
437
438 fn protocol_name(&self) -> &'static str {
439 "Memcached"
440 }
441
442 fn get_event_types(&self) -> Vec<EventType> {
443 vec![
444 MEMCACHED_GET_EVENT.clone(),
445 MEMCACHED_STORE_EVENT.clone(),
446 MEMCACHED_DELETE_EVENT.clone(),
447 MEMCACHED_ARITHMETIC_EVENT.clone(),
448 MEMCACHED_TOUCH_EVENT.clone(),
449 MEMCACHED_STATS_EVENT.clone(),
450 MEMCACHED_VERSION_EVENT.clone(),
451 MEMCACHED_FLUSH_ALL_EVENT.clone(),
452 MEMCACHED_UNKNOWN_COMMAND_EVENT.clone(),
453 ]
454 }
455
456 fn stack_name(&self) -> &'static str {
457 "ETH>IP>TCP>Memcached"
458 }
459
460 fn keywords(&self) -> Vec<&'static str> {
461 vec!["memcached", "memcache", "memcached server"]
465 }
466
467 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
468 use crate::protocol::metadata::{
469 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
470 };
471
472 ProtocolMetadataV2::builder()
473 .state(DevelopmentState::Beta)
474 .privilege_requirement(PrivilegeRequirement::None)
476 .well_known_port(11211)
477 .max_inbound_bytes(crate::server::memcached::protocol::MAX_VALUE_LEN)
481 .implementation(
482 "Hand-rolled memcached *text* protocol (src/server/memcached/protocol.rs) \
483 over TCP. get/gets/set/add/replace/append/prepend/cas/delete/incr/decr/\
484 touch/stats/version/flush_all/quit. Storage-command data blocks are read by \
485 the declared byte count, never by scanning for CRLF, so a payload \
486 containing CRLF frames correctly. The binary protocol is not implemented \
487 (deprecated upstream in 1.6) and neither are the meta commands (mg/ms/md).",
488 )
489 .llm_control(
490 "The model IS the cache. It decides what every key holds, whether a store \
491 succeeds, what the counters read and what stats to report. No value is \
492 stored anywhere in this server.",
493 )
494 .e2e_testing(
495 "libmemcached 1.0.18's C tools are the independent peer, in \
496 tests/server/memcached/real_client_test.rs: memcat reads a model-invented \
497 value (it parses the VALUE header and reads exactly that many bytes, so a \
498 wrong count shows up as an empty or truncated result rather than a pass), \
499 and memstat and memping accept our STAT/END and VERSION replies. They are a \
500 separate C implementation invoked as subprocesses, never linked. Neither \
501 test is #[ignore]d and both now FAIL rather than skip when the binaries are \
502 absent; until September 2026 they printed SKIPPED and returned Ok(()), a \
503 silent pass, which is what held this at Experimental. The always-running \
504 raw-socket tests remain the byte-level check - exact VALUE/END framing, byte \
505 counts, a payload containing CRLF, and a rejected storage header consuming \
506 its data block instead of leaving it to be parsed as commands. Untested \
507 against any client for the binary protocol or the meta commands, neither of \
508 which is implemented.",
509 )
510 .notes(
511 "STORES NOTHING BY DESIGN: there is no map or table in the Rust code, so \
512 two successive gets of the same key are two independent questions to the \
513 model and may legitimately differ. Use a script handler or the generic \
514 SQLite facility if you need a key to keep its value. Every command costs \
515 one LLM call; `noreply` is honoured, so a noreply store still costs a call \
516 but sends nothing.",
517 )
518 .build()
519 }
520
521 fn description(&self) -> &'static str {
522 "Memcached text-protocol server where the model answers every lookup"
523 }
524
525 fn example_prompt(&self) -> &'static str {
526 "pretend to be a memcached server on port 11211 holding session data for a web app"
527 }
528
529 fn group_name(&self) -> &'static str {
530 "Database"
531 }
532
533 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
534 use crate::llm::actions::StartupExamples;
535
536 StartupExamples::new(
537 json!({
538 "type": "open_server",
539 "port": 11211,
540 "base_stack": "memcached",
541 "instruction": "Act as a session cache. Keys look like session:<id> and hold \
542 a JSON user object. Report STORED for any set."
543 }),
544 json!({
545 "type": "open_server",
546 "port": 11211,
547 "base_stack": "memcached",
548 "event_handlers": [{
549 "event_pattern": "memcached_get",
550 "handler": {
551 "type": "script",
552 "language": "python",
553 "code": "respond([{'type': 'send_memcached_values', 'values': [{'key': k, 'value': 'v-' + k} for k in event['keys']]}])"
554 }
555 }]
556 }),
557 json!({
558 "type": "open_server",
559 "port": 11211,
560 "base_stack": "memcached",
561 "event_handlers": [
562 {
563 "event_pattern": "memcached_get",
564 "handler": {
565 "type": "static",
566 "actions": [{"type": "send_memcached_values", "values": []}]
567 }
568 },
569 {
570 "event_pattern": "memcached_store",
571 "handler": {
572 "type": "static",
573 "actions": [{"type": "send_memcached_status", "status": "STORED"}]
574 }
575 }
576 ]
577 }),
578 )
579 }
580}
581
582impl Server for MemcachedProtocol {
583 fn spawn(
584 &self,
585 ctx: crate::protocol::SpawnContext,
586 ) -> std::pin::Pin<
587 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
588 > {
589 Box::pin(async move { super::MemcachedServer::spawn_with_llm_actions(ctx).await })
590 }
591
592 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
593 let action_type = action
594 .get("type")
595 .and_then(|v| v.as_str())
596 .context("Missing 'type' field in action")?;
597
598 match action_type {
599 "send_memcached_values" => {
600 let entries = action
601 .get("values")
602 .and_then(|v| v.as_array())
603 .context("send_memcached_values requires a 'values' array")?;
604
605 let mut items = Vec::with_capacity(entries.len());
606 let mut any_cas = false;
607 for entry in entries {
608 let key = entry
609 .get("key")
610 .and_then(|v| v.as_str())
611 .context("each value needs a 'key'")?
612 .to_string();
613 if key.len() > protocol::MAX_KEY_LEN {
614 return Err(anyhow::anyhow!(
615 "key '{}' is {} bytes; memcached allows at most {}",
616 key,
617 key.len(),
618 protocol::MAX_KEY_LEN
619 ));
620 }
621 if key.bytes().any(|b| b <= b' ' || b == 127) {
622 return Err(anyhow::anyhow!(
623 "key '{}' contains whitespace or a control character, which the \
624 text protocol cannot express",
625 key
626 ));
627 }
628 let data = decode_value(entry, "value", "value_encoding")?;
629 if data.len() > protocol::MAX_VALUE_LEN {
630 return Err(anyhow::anyhow!(
631 "value for '{}' is {} bytes; memcached's item limit is {}",
632 key,
633 data.len(),
634 protocol::MAX_VALUE_LEN
635 ));
636 }
637 let flags = entry.get("flags").and_then(|v| v.as_u64()).unwrap_or(0);
638 let cas_unique = entry.get("cas_unique").and_then(|v| v.as_u64());
639 any_cas |= cas_unique.is_some();
640 items.push(ValueItem {
641 key,
642 flags: u32::try_from(flags).context("flags exceed 32 bits")?,
643 data,
644 cas_unique,
645 });
646 }
647
648 Ok(ActionResult::Output(protocol::encode_values(
649 &items, any_cas,
650 )))
651 }
652
653 "send_memcached_status" => {
654 let status = action
655 .get("status")
656 .and_then(|v| v.as_str())
657 .context("send_memcached_status requires 'status'")?;
658 let line =
659 protocol::status_line(&status.to_ascii_uppercase()).ok_or_else(|| {
660 anyhow::anyhow!(
661 "Unknown memcached status '{}'. Use one of STORED, NOT_STORED, \
662 EXISTS, NOT_FOUND, DELETED, TOUCHED, OK, ERROR",
663 status
664 )
665 })?;
666 Ok(ActionResult::Output(line.as_bytes().to_vec()))
667 }
668
669 "send_memcached_number" => {
670 let value = action
671 .get("value")
672 .and_then(|v| v.as_u64())
673 .context("send_memcached_number requires a non-negative integer 'value'")?;
674 Ok(ActionResult::Output(format!("{}\r\n", value).into_bytes()))
675 }
676
677 "send_memcached_stats" => {
678 let map = action
679 .get("stats")
680 .and_then(|v| v.as_object())
681 .context("send_memcached_stats requires a 'stats' object")?;
682 let entries: Vec<(String, String)> = map
683 .iter()
684 .map(|(k, v)| {
685 let rendered = match v {
686 serde_json::Value::String(s) => s.clone(),
687 other => other.to_string(),
688 };
689 (k.clone(), rendered)
690 })
691 .collect();
692 Ok(ActionResult::Output(protocol::encode_stats(&entries)))
693 }
694
695 "send_memcached_version" => {
696 let version = action
697 .get("version")
698 .and_then(|v| v.as_str())
699 .context("send_memcached_version requires 'version'")?;
700 if version.contains(['\r', '\n']) {
701 return Err(anyhow::anyhow!("version string must not contain CR or LF"));
702 }
703 Ok(ActionResult::Output(
704 format!("VERSION {}\r\n", version).into_bytes(),
705 ))
706 }
707
708 "send_memcached_error" => {
709 let kind = action
710 .get("kind")
711 .and_then(|v| v.as_str())
712 .context("send_memcached_error requires 'kind'")?
713 .to_ascii_uppercase();
714 let message = action
715 .get("message")
716 .and_then(|v| v.as_str())
717 .unwrap_or("")
718 .replace(['\r', '\n'], " ");
719 let line = match kind.as_str() {
720 "ERROR" => "ERROR\r\n".to_string(),
721 "CLIENT_ERROR" | "SERVER_ERROR" => {
722 if message.is_empty() {
723 return Err(anyhow::anyhow!("{} requires a 'message'", kind));
724 }
725 format!("{} {}\r\n", kind, message)
726 }
727 other => {
728 return Err(anyhow::anyhow!(
729 "Unknown error kind '{}': use ERROR, CLIENT_ERROR or SERVER_ERROR",
730 other
731 ))
732 }
733 };
734 Ok(ActionResult::Output(line.into_bytes()))
735 }
736
737 "close_memcached_connection" => Ok(ActionResult::CloseConnection),
738
739 "close_connection" => Ok(ActionResult::CloseConnection),
742
743 _ => Err(anyhow::anyhow!("Unknown Memcached action: {}", action_type)),
744 }
745 }
746}