1use lex_bytecode::vm::{EffectHandler, Vm};
8use lex_bytecode::{Program, Value};
9use smol_str::SmolStr;
10use std::path::PathBuf;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::{Mutex, OnceLock};
13use std::sync::Arc;
14use std::time::{SystemTime, UNIX_EPOCH};
15
16use crate::builtins::{call_pure_builtin, is_pure_call};
17use crate::policy::Policy;
18
19pub trait IoSink: Send {
22 fn print_line(&mut self, s: &str);
23}
24
25pub struct StdoutSink;
26impl IoSink for StdoutSink {
27 fn print_line(&mut self, s: &str) {
28 use std::io::Write;
29 println!("{s}");
30 let _ = std::io::stdout().flush();
31 }
32}
33
34#[derive(Default)]
35pub struct CapturedSink { pub lines: Vec<String> }
36impl IoSink for CapturedSink {
37 fn print_line(&mut self, s: &str) { self.lines.push(s.to_string()); }
38}
39
40pub type StreamRegistry =
43 std::collections::HashMap<String, Box<dyn Iterator<Item = String> + Send>>;
44
45pub struct DefaultHandler {
46 policy: Policy,
47 pub sink: Box<dyn IoSink>,
48 pub read_root: Option<PathBuf>,
51 pub budget_remaining: Arc<AtomicU64>,
58 pub budget_ceiling: Option<u64>,
62 pub program: Option<Arc<Program>>,
66 pub chat_registry: Option<Arc<crate::ws::ChatRegistry>>,
70 pub mcp_clients: crate::mcp_client::McpClientCache,
76 pub streams: Arc<std::sync::Mutex<StreamRegistry>>,
83 pub next_stream_id: Arc<std::sync::atomic::AtomicU64>,
85 arena_stack: Vec<(u64, crate::arena::Arena)>,
98 next_scope_id: u64,
103 pub program_args: Vec<String>,
106}
107
108impl DefaultHandler {
109 pub fn new(policy: Policy) -> Self {
110 let ceiling = policy.budget;
114 let initial = ceiling.unwrap_or(u64::MAX);
115 Self {
116 policy,
117 sink: Box::new(StdoutSink),
118 read_root: None,
119 budget_remaining: Arc::new(AtomicU64::new(initial)),
120 budget_ceiling: ceiling,
121 program: None,
122 chat_registry: None,
123 mcp_clients: crate::mcp_client::McpClientCache::with_capacity(16),
124 streams: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
125 next_stream_id: Arc::new(std::sync::atomic::AtomicU64::new(0)),
126 arena_stack: Vec::new(),
127 next_scope_id: 1,
128 program_args: Vec::new(),
129 }
130 }
131
132 pub fn active_arena(&self) -> Option<&crate::arena::Arena> {
138 self.arena_stack.last().map(|(_, a)| a)
139 }
140
141 pub fn arena_stack_depth(&self) -> usize {
144 self.arena_stack.len()
145 }
146
147 pub fn with_program(mut self, program: Arc<Program>) -> Self {
148 self.program = Some(program); self
149 }
150
151 pub fn with_chat_registry(mut self, registry: Arc<crate::ws::ChatRegistry>) -> Self {
152 self.chat_registry = Some(registry); self
153 }
154
155 pub fn with_sink(mut self, sink: Box<dyn IoSink>) -> Self {
156 self.sink = sink; self
157 }
158
159 pub fn with_read_root(mut self, root: PathBuf) -> Self {
160 self.read_root = Some(root); self
161 }
162
163 pub fn with_program_args(mut self, args: Vec<String>) -> Self {
164 self.program_args = args; self
165 }
166
167 fn ensure_kind_allowed(&self, kind: &str) -> Result<(), String> {
168 if self.policy.allow_effects.contains(kind) {
169 Ok(())
170 } else {
171 Err(format!("effect `{kind}` not in --allow-effects"))
172 }
173 }
174
175 fn resolve_read_path(&self, p: &str) -> PathBuf {
176 match &self.read_root {
177 Some(root) => root.join(p.trim_start_matches('/')),
178 None => PathBuf::from(p),
179 }
180 }
181
182 fn dispatch_log(&mut self, op: &str, args: Vec<Value>) -> Result<Value, String> {
183 match op {
184 "debug" | "info" | "warn" | "error" => {
185 let msg = expect_str(args.first())?;
186 let level = match op {
187 "debug" => LogLevel::Debug,
188 "info" => LogLevel::Info,
189 "warn" => LogLevel::Warn,
190 _ => LogLevel::Error,
191 };
192 emit_log(level, msg);
193 Ok(Value::Unit)
194 }
195 "set_level" => {
196 let s = expect_str(args.first())?;
197 match parse_log_level(s) {
198 Some(l) => {
199 log_state().lock().unwrap().level = l;
200 Ok(ok(Value::Unit))
201 }
202 None => Ok(err(Value::Str(format!(
203 "log.set_level: unknown level `{s}`; expected debug|info|warn|error").into()))),
204 }
205 }
206 "set_format" => {
207 let s = expect_str(args.first())?;
208 let fmt = match s {
209 "text" => LogFormat::Text,
210 "json" => LogFormat::Json,
211 other => return Ok(err(Value::Str(format!(
212 "log.set_format: unknown format `{other}`; expected text|json").into()))),
213 };
214 log_state().lock().unwrap().format = fmt;
215 Ok(ok(Value::Unit))
216 }
217 "set_sink" => {
218 let path = expect_str(args.first())?;
219 if path == "-" {
220 log_state().lock().unwrap().sink = LogSink::Stderr;
221 return Ok(ok(Value::Unit));
222 }
223 if let Err(e) = self.ensure_fs_write_path(path) {
224 return Ok(err(Value::Str(e.into())));
225 }
226 match std::fs::OpenOptions::new()
227 .create(true).append(true).open(path)
228 {
229 Ok(f) => {
230 log_state().lock().unwrap().sink = LogSink::File(std::sync::Arc::new(Mutex::new(f)));
231 Ok(ok(Value::Unit))
232 }
233 Err(e) => Ok(err(Value::Str(format!("log.set_sink `{path}`: {e}").into()))),
234 }
235 }
236 other => Err(format!("unsupported log.{other}")),
237 }
238 }
239
240 fn dispatch_process(&mut self, op: &str, args: Vec<Value>) -> Result<Value, String> {
241 match op {
242 "spawn" => {
243 let cmd = expect_str(args.first())?.to_string();
244 let raw_args = match args.get(1) {
245 Some(Value::List(items)) => items.clone(),
246 _ => return Err("process.spawn: args must be List[Str]".into()),
247 };
248 let str_args: Result<Vec<String>, String> = raw_args.iter().map(|v| match v {
249 Value::Str(s) => Ok(s.to_string()),
250 other => Err(format!("process.spawn: arg must be Str, got {other:?}")),
251 }).collect();
252 let str_args = str_args?;
253 let opts = match args.get(2) {
254 Some(Value::Record { fields: r, .. }) => r.clone(),
255 _ => return Err("process.spawn: missing or invalid opts record".into()),
256 };
257
258 if !self.policy.allow_proc.is_empty() {
260 let basename = std::path::Path::new(&cmd)
261 .file_name()
262 .and_then(|s| s.to_str())
263 .unwrap_or(&cmd);
264 if !self.policy.allow_proc.iter().any(|a| a == basename) {
265 return Ok(err(Value::Str(format!(
266 "process.spawn: `{cmd}` not in --allow-proc {:?}",
267 self.policy.allow_proc
268 ).into())));
269 }
270 }
271
272 let mut command = std::process::Command::new(&cmd);
273 command.args(&str_args);
274 command.stdin(std::process::Stdio::piped());
275 command.stdout(std::process::Stdio::piped());
276 command.stderr(std::process::Stdio::piped());
277
278 if let Some(Value::Variant { name, args: vargs }) = opts.get("cwd") {
279 if name == "Some" {
280 if let Some(Value::Str(s)) = vargs.first() {
281 command.current_dir(s);
282 }
283 }
284 }
285 if let Some(Value::Map(env)) = opts.get("env") {
286 for (k, v) in env {
287 if let (lex_bytecode::MapKey::Str(ks), Value::Str(vs)) = (k, v) {
288 command.env(ks, vs);
289 }
290 }
291 }
292
293 let stdin_payload: Option<Vec<u8>> = match opts.get("stdin") {
294 Some(Value::Variant { name, args: vargs }) if name == "Some" => {
295 match vargs.first() {
296 Some(Value::Bytes(b)) => Some(b.clone()),
297 _ => None,
298 }
299 }
300 _ => None,
301 };
302
303 let mut child = match command.spawn() {
304 Ok(c) => c,
305 Err(e) => return Ok(err(Value::Str(format!("process.spawn `{cmd}`: {e}").into()))),
306 };
307
308 if let Some(payload) = stdin_payload {
309 if let Some(mut stdin) = child.stdin.take() {
310 use std::io::Write;
311 let _ = stdin.write_all(&payload);
312 }
314 }
315
316 let stdout = child.stdout.take().map(std::io::BufReader::new);
317 let stderr = child.stderr.take().map(std::io::BufReader::new);
318 let handle = next_process_handle();
319 process_registry().lock().unwrap().insert(handle, ProcessState {
320 child,
321 stdout,
322 stderr,
323 });
324 Ok(ok(Value::Int(handle as i64)))
325 }
326 "read_stdout_line" => Self::read_line_op(args, true),
327 "read_stderr_line" => Self::read_line_op(args, false),
328 "wait" => {
329 let h = expect_process_handle(args.first())?;
330 let arc = process_registry().lock().unwrap()
334 .touch_get(h)
335 .ok_or_else(|| "process.wait: closed or unknown ProcessHandle".to_string())?;
336 let status = {
337 let mut state = arc.lock().unwrap();
338 state.child.wait().map_err(|e| format!("process.wait: {e}"))?
339 };
340 process_registry().lock().unwrap().remove(h);
344 let mut rec = indexmap::IndexMap::new();
345 rec.insert("code".into(), Value::Int(status.code().unwrap_or(-1) as i64));
346 #[cfg(unix)]
347 {
348 use std::os::unix::process::ExitStatusExt;
349 rec.insert("signaled".into(), Value::Bool(status.signal().is_some()));
350 }
351 #[cfg(not(unix))]
352 {
353 rec.insert("signaled".into(), Value::Bool(false));
354 }
355 Ok(Value::record_dynamic(rec))
356 }
357 "kill" => {
358 let h = expect_process_handle(args.first())?;
359 let _signal = expect_str(args.get(1))?;
360 let arc = process_registry().lock().unwrap()
361 .touch_get(h)
362 .ok_or_else(|| "process.kill: closed or unknown ProcessHandle".to_string())?;
363 let mut state = arc.lock().unwrap();
364 match state.child.kill() {
367 Ok(_) => Ok(ok(Value::Unit)),
368 Err(e) => Ok(err(Value::Str(format!("process.kill: {e}").into()))),
369 }
370 }
371 "run" => {
372 let cmd = expect_str(args.first())?.to_string();
373 let raw_args = match args.get(1) {
374 Some(Value::List(items)) => items.clone(),
375 _ => return Err("process.run: args must be List[Str]".into()),
376 };
377 let str_args: Result<Vec<String>, String> = raw_args.iter().map(|v| match v {
378 Value::Str(s) => Ok(s.to_string()),
379 other => Err(format!("process.run: arg must be Str, got {other:?}")),
380 }).collect();
381 let str_args = str_args?;
382 if !self.policy.allow_proc.is_empty() {
383 let basename = std::path::Path::new(&cmd)
384 .file_name()
385 .and_then(|s| s.to_str())
386 .unwrap_or(&cmd);
387 if !self.policy.allow_proc.iter().any(|a| a == basename) {
388 return Ok(err(Value::Str(format!(
389 "process.run: `{cmd}` not in --allow-proc {:?}",
390 self.policy.allow_proc
391 ).into())));
392 }
393 }
394 match std::process::Command::new(&cmd).args(&str_args).output() {
395 Ok(o) => {
396 let mut rec = indexmap::IndexMap::new();
397 rec.insert("stdout".into(), Value::Str(
398 String::from_utf8_lossy(&o.stdout).into_owned().into()));
399 rec.insert("stderr".into(), Value::Str(
400 String::from_utf8_lossy(&o.stderr).into_owned().into()));
401 rec.insert("exit_code".into(), Value::Int(
402 o.status.code().unwrap_or(-1) as i64));
403 Ok(ok(Value::record_dynamic(rec)))
404 }
405 Err(e) => Ok(err(Value::Str(format!("process.run `{cmd}`: {e}").into()))),
406 }
407 }
408 other => Err(format!("unsupported process.{other}")),
409 }
410 }
411
412 fn read_line_op(args: Vec<Value>, is_stdout: bool) -> Result<Value, String> {
418 let h = expect_process_handle(args.first())?;
419 let arc = process_registry().lock().unwrap()
420 .touch_get(h)
421 .ok_or_else(|| format!(
422 "process.read_{}_line: closed or unknown ProcessHandle",
423 if is_stdout { "stdout" } else { "stderr" }))?;
424 let mut state = arc.lock().unwrap();
425 let reader_opt = if is_stdout {
426 state.stdout.as_mut().map(|r| -> &mut dyn std::io::BufRead { r })
427 } else {
428 state.stderr.as_mut().map(|r| -> &mut dyn std::io::BufRead { r })
429 };
430 let reader = match reader_opt {
431 Some(r) => r,
432 None => return Ok(none()),
433 };
434 let mut line = String::new();
435 match reader.read_line(&mut line) {
436 Ok(0) => Ok(none()),
437 Ok(_) => {
438 if line.ends_with('\n') { line.pop(); }
439 if line.ends_with('\r') { line.pop(); }
440 Ok(some(Value::Str(line.into())))
441 }
442 Err(e) => Err(format!("process.read_*_line: {e}")),
443 }
444 }
445
446 fn dispatch_fs(&mut self, op: &str, args: Vec<Value>) -> Result<Value, String> {
447 match op {
448 "exists" => {
449 let path = expect_str(args.first())?.to_string();
450 if let Err(e) = self.ensure_fs_walk_path(&path) {
451 return Ok(err(Value::Str(e.into())));
452 }
453 Ok(Value::Bool(std::path::Path::new(&path).exists()))
454 }
455 "is_file" => {
456 let path = expect_str(args.first())?.to_string();
457 if let Err(e) = self.ensure_fs_walk_path(&path) {
458 return Ok(err(Value::Str(e.into())));
459 }
460 Ok(Value::Bool(std::path::Path::new(&path).is_file()))
461 }
462 "is_dir" => {
463 let path = expect_str(args.first())?.to_string();
464 if let Err(e) = self.ensure_fs_walk_path(&path) {
465 return Ok(err(Value::Str(e.into())));
466 }
467 Ok(Value::Bool(std::path::Path::new(&path).is_dir()))
468 }
469 "stat" => {
470 let path = expect_str(args.first())?.to_string();
471 if let Err(e) = self.ensure_fs_walk_path(&path) {
472 return Ok(err(Value::Str(e.into())));
473 }
474 match std::fs::metadata(&path) {
475 Ok(md) => {
476 let mtime = md.modified()
477 .ok()
478 .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
479 .map(|d| d.as_secs() as i64)
480 .unwrap_or(0);
481 let mut rec = indexmap::IndexMap::new();
482 rec.insert("size".into(), Value::Int(md.len() as i64));
483 rec.insert("mtime".into(), Value::Int(mtime));
484 rec.insert("is_dir".into(), Value::Bool(md.is_dir()));
485 rec.insert("is_file".into(), Value::Bool(md.is_file()));
486 Ok(ok(Value::record_dynamic(rec)))
487 }
488 Err(e) => Ok(err(Value::Str(format!("fs.stat `{path}`: {e}").into()))),
489 }
490 }
491 "list_dir" => {
492 let path = expect_str(args.first())?.to_string();
493 if let Err(e) = self.ensure_fs_walk_path(&path) {
494 return Ok(err(Value::Str(e.into())));
495 }
496 match std::fs::read_dir(&path) {
497 Ok(rd) => {
498 let mut entries: Vec<Value> = Vec::new();
499 for ent in rd {
500 match ent {
501 Ok(e) => {
502 let p = e.path();
503 entries.push(Value::Str(p.to_string_lossy().into_owned().into()));
504 }
505 Err(e) => return Ok(err(Value::Str(format!("fs.list_dir: {e}").into()))),
506 }
507 }
508 Ok(ok(Value::List(entries.into())))
509 }
510 Err(e) => Ok(err(Value::Str(format!("fs.list_dir `{path}`: {e}").into()))),
511 }
512 }
513 "walk" => {
514 let path = expect_str(args.first())?.to_string();
515 if let Err(e) = self.ensure_fs_walk_path(&path) {
516 return Ok(err(Value::Str(e.into())));
517 }
518 let mut paths: Vec<Value> = Vec::new();
519 for ent in walkdir::WalkDir::new(&path) {
520 match ent {
521 Ok(e) => paths.push(Value::Str(
522 e.path().to_string_lossy().into_owned().into())),
523 Err(e) => return Ok(err(Value::Str(format!("fs.walk: {e}").into()))),
524 }
525 }
526 Ok(ok(Value::List(paths.into())))
527 }
528 "glob" => {
529 let pattern = expect_str(args.first())?.to_string();
530 let entries = match glob::glob(&pattern) {
535 Ok(e) => e,
536 Err(e) => return Ok(err(Value::Str(format!("fs.glob: {e}").into()))),
537 };
538 let mut paths: Vec<Value> = Vec::new();
539 for ent in entries {
540 match ent {
541 Ok(p) => {
542 let s = p.to_string_lossy().into_owned();
543 if self.policy.allow_fs_read.is_empty()
544 || self.policy.allow_fs_read.iter().any(|root| p.starts_with(root))
545 {
546 paths.push(Value::Str(s.into()));
547 }
548 }
549 Err(e) => return Ok(err(Value::Str(format!("fs.glob: {e}").into()))),
550 }
551 }
552 Ok(ok(Value::List(paths.into())))
553 }
554 "mkdir_p" => {
555 let path = expect_str(args.first())?.to_string();
556 if let Err(e) = self.ensure_fs_write_path(&path) {
557 return Ok(err(Value::Str(e.into())));
558 }
559 match std::fs::create_dir_all(&path) {
560 Ok(_) => Ok(ok(Value::Unit)),
561 Err(e) => Ok(err(Value::Str(format!("fs.mkdir_p `{path}`: {e}").into()))),
562 }
563 }
564 "remove" => {
565 let path = expect_str(args.first())?.to_string();
566 if let Err(e) = self.ensure_fs_write_path(&path) {
567 return Ok(err(Value::Str(e.into())));
568 }
569 let p = std::path::Path::new(&path);
570 let result = if p.is_dir() {
571 std::fs::remove_dir_all(p)
572 } else {
573 std::fs::remove_file(p)
574 };
575 match result {
576 Ok(_) => Ok(ok(Value::Unit)),
577 Err(e) => Ok(err(Value::Str(format!("fs.remove `{path}`: {e}").into()))),
578 }
579 }
580 "copy" => {
581 let src = expect_str(args.first())?.to_string();
582 let dst = expect_str(args.get(1))?.to_string();
583 if let Err(e) = self.ensure_fs_walk_path(&src) {
584 return Ok(err(Value::Str(e.into())));
585 }
586 if let Err(e) = self.ensure_fs_write_path(&dst) {
587 return Ok(err(Value::Str(e.into())));
588 }
589 match std::fs::copy(&src, &dst) {
590 Ok(_) => Ok(ok(Value::Unit)),
591 Err(e) => Ok(err(Value::Str(format!("fs.copy {src} -> {dst}: {e}").into()))),
592 }
593 }
594 other => Err(format!("unsupported fs.{other}")),
595 }
596 }
597
598 fn ensure_fs_walk_path(&self, path: &str) -> Result<(), String> {
603 if self.policy.allow_fs_read.is_empty() {
604 return Ok(());
605 }
606 let p = std::path::Path::new(path);
607 if self.policy.allow_fs_read.iter().any(|a| p.starts_with(a)) {
608 Ok(())
609 } else {
610 Err(format!("fs path `{path}` outside --allow-fs-read"))
611 }
612 }
613
614 fn ensure_fs_write_path(&self, path: &str) -> Result<(), String> {
617 if self.policy.allow_fs_write.is_empty() {
618 return Ok(());
619 }
620 let p = std::path::Path::new(path);
621 if self.policy.allow_fs_write.iter().any(|a| p.starts_with(a)) {
622 Ok(())
623 } else {
624 Err(format!("fs path `{path}` outside --allow-fs-write"))
625 }
626 }
627
628 fn ensure_host_allowed(&self, url: &str) -> Result<(), String> {
632 if self.policy.allow_net_host.is_empty() { return Ok(()); }
633 let host = extract_host(url).unwrap_or("");
634 if self.policy.allow_net_host.iter().any(|h| host == h) {
635 Ok(())
636 } else {
637 Err(format!(
638 "net call to host `{host}` not in --allow-net-host {:?}",
639 self.policy.allow_net_host,
640 ))
641 }
642 }
643}
644
645fn extract_host(url: &str) -> Option<&str> {
646 let rest = url
647 .strip_prefix("http://")
648 .or_else(|| url.strip_prefix("https://"))
649 .or_else(|| url.strip_prefix("redis://"))
650 .or_else(|| url.strip_prefix("rediss://"))
651 .map(|r| r.split_once('@').map(|(_, after)| after).unwrap_or(r))?;
653 let host_port = match rest.find('/') {
654 Some(i) => &rest[..i],
655 None => rest,
656 };
657 Some(match host_port.rsplit_once(':') {
658 Some((h, _)) => h,
659 None => host_port,
660 })
661}
662
663impl EffectHandler for DefaultHandler {
664 fn enter_request_scope(&mut self) -> u64 {
668 let id = self.next_scope_id;
669 self.next_scope_id = self.next_scope_id.wrapping_add(1);
670 self.arena_stack.push((id, crate::arena::Arena::new()));
671 id
672 }
673
674 fn exit_request_scope(&mut self, scope_id: u64) {
680 if let Some(pos) = self.arena_stack.iter().position(|(id, _)| *id == scope_id) {
681 self.arena_stack.truncate(pos);
686 }
687 }
688
689 fn note_call_budget(&mut self, cost: u64) -> Result<(), String> {
694 let Some(ceiling) = self.budget_ceiling else { return Ok(()); };
697 loop {
703 let cur = self.budget_remaining.load(Ordering::SeqCst);
704 if cost > cur {
705 let used = ceiling.saturating_sub(cur);
706 return Err(format!(
707 "budget exceeded: requested {cost}, used so far {used}, ceiling {ceiling}"));
708 }
709 let next = cur - cost;
710 if self.budget_remaining.compare_exchange(cur, next,
713 Ordering::SeqCst, Ordering::SeqCst).is_ok() {
714 return Ok(());
715 }
716 }
717 }
718
719 fn dispatch(&mut self, kind: &str, op: &str, args: Vec<Value>) -> Result<Value, String> {
720 if is_pure_call(kind, op) {
724 return call_pure_builtin(kind, op, args);
725 }
726 if kind == "process" {
730 self.ensure_kind_allowed("proc")?;
731 return self.dispatch_process(op, args);
732 }
733 if kind == "log" {
734 let effect_kind = match op {
737 "debug" | "info" | "warn" | "error" => "log",
738 "set_level" | "set_format" => "io",
739 "set_sink" => {
740 self.ensure_kind_allowed("io")?;
741 self.ensure_kind_allowed("fs_write")?;
742 return self.dispatch_log(op, args);
743 }
744 other => return Err(format!("unsupported log.{other}")),
745 };
746 self.ensure_kind_allowed(effect_kind)?;
747 return self.dispatch_log(op, args);
748 }
749 if kind == "fs" {
750 let effect_kind = match op {
751 "exists" | "is_file" | "is_dir" | "stat"
752 | "list_dir" | "walk" | "glob" => "fs_walk",
753 "mkdir_p" | "remove" => "fs_write",
754 "copy" => {
755 self.ensure_kind_allowed("fs_walk")?;
756 self.ensure_kind_allowed("fs_write")?;
757 return self.dispatch_fs(op, args);
758 }
759 other => return Err(format!("unsupported fs.{other}")),
760 };
761 self.ensure_kind_allowed(effect_kind)?;
762 return self.dispatch_fs(op, args);
763 }
764 if kind == "datetime" && op == "now" {
771 self.ensure_kind_allowed("time")?;
772 if let Ok(s) = std::env::var("LEX_TEST_NOW") {
774 if let Ok(secs) = s.trim().parse::<i64>() {
775 return Ok(Value::Int(secs.saturating_mul(1_000_000_000)));
776 }
777 }
778 let now = chrono::Utc::now();
779 let nanos = now.timestamp_nanos_opt().unwrap_or(i64::MAX);
780 return Ok(Value::Int(nanos));
781 }
782 if kind == "crypto" && op == "random" {
783 self.ensure_kind_allowed("random")?;
784 let n = expect_int(args.first())?;
785 if !(0..=1_048_576).contains(&n) {
786 return Err("crypto.random: n must be in 0..=1048576".into());
787 }
788 use rand::{rngs::SysRng, TryRng};
789 let mut buf = vec![0u8; n as usize];
790 SysRng.try_fill_bytes(&mut buf)
791 .map_err(|e| format!("crypto.random: OS RNG: {e}"))?;
792 return Ok(Value::Bytes(buf));
793 }
794 if kind == "crypto" && op == "random_str_hex" {
799 self.ensure_kind_allowed("random")?;
800 let n = expect_int(args.first())?;
801 if !(0..=1_048_576).contains(&n) {
802 return Err("crypto.random_str_hex: n must be in 0..=1048576".into());
803 }
804 use rand::{rngs::SysRng, TryRng};
805 let mut buf = vec![0u8; n as usize];
806 SysRng.try_fill_bytes(&mut buf)
807 .map_err(|e| format!("crypto.random_str_hex: OS RNG: {e}"))?;
808 return Ok(Value::Str(hex::encode(&buf).into()));
809 }
810 if kind == "crypto" && op == "p256_generate" {
823 self.ensure_kind_allowed("random")?;
824 use p256::ecdsa::SigningKey;
825 use rand::{rngs::SysRng, TryRng};
826 for _ in 0..16 {
827 let mut buf = [0u8; 32];
828 SysRng.try_fill_bytes(&mut buf)
829 .map_err(|e| format!("crypto.p256_generate: OS RNG: {e}"))?;
830 if let Ok(sk) = SigningKey::from_slice(&buf) {
831 return Ok(ok(Value::Bytes(sk.to_bytes().to_vec())));
832 }
833 }
834 return Ok(err(Value::Str(
835 "crypto.p256_generate: failed to sample a valid scalar".into())));
836 }
837 if kind == "crypto" && op == "secp256k1_generate" {
844 self.ensure_kind_allowed("random")?;
845 use k256::ecdsa::SigningKey;
846 use rand::{rngs::SysRng, TryRng};
847 for _ in 0..16 {
848 let mut buf = [0u8; 32];
849 SysRng.try_fill_bytes(&mut buf)
850 .map_err(|e| format!("crypto.secp256k1_generate: OS RNG: {e}"))?;
851 if let Ok(sk) = SigningKey::from_slice(&buf) {
852 return Ok(ok(Value::Bytes(sk.to_bytes().to_vec())));
853 }
854 }
855 return Ok(err(Value::Str(
856 "crypto.secp256k1_generate: failed to sample a valid scalar".into())));
857 }
858 if kind == "agent" {
871 let effect_kind = match op {
872 "local_complete" => "llm_local",
873 "cloud_complete" => "llm_cloud",
874 "cloud_stream" => "llm_cloud",
875 "send_a2a" => "a2a",
876 "call_mcp" => "mcp",
877 other => return Err(format!("unsupported agent.{other}")),
878 };
879 self.ensure_kind_allowed(effect_kind)?;
880 return match op {
888 "call_mcp" => Ok(self.dispatch_call_mcp(args)),
889 "local_complete" => Ok(dispatch_llm_local(args)),
890 "cloud_complete" => Ok(dispatch_llm_cloud(args)),
891 "cloud_stream" => Ok(self.dispatch_cloud_stream(args)),
892 _ => Ok(ok(Value::Str(format!("<{effect_kind} stub>").into()))),
893 };
894 }
895 if kind == "stream" {
896 self.ensure_kind_allowed("stream")?;
903 return match op {
904 "next" => Ok(self.dispatch_stream_next(args)),
905 "collect" => Ok(self.dispatch_stream_collect(args)),
906 other => Err(format!("unsupported stream.{other}")),
907 };
908 }
909 if kind == "http" && matches!(op, "send" | "get" | "post" | "stream_lines") {
910 self.ensure_kind_allowed("net")?;
911 return match op {
912 "send" => {
913 let req = expect_record(args.first())?;
914 Ok(http_send_record(self, req))
915 }
916 "get" => {
917 let url = expect_str(args.first())?.to_string();
918 self.ensure_host_allowed(&url)?;
919 Ok(http_send_simple("GET", &url, None, "", None))
920 }
921 "post" => {
922 let url = expect_str(args.first())?.to_string();
923 let body = expect_bytes(args.get(1))?.clone();
924 let content_type = expect_str(args.get(2))?.to_string();
925 self.ensure_host_allowed(&url)?;
926 Ok(http_send_simple("POST", &url, Some(body), &content_type, None))
927 }
928 "stream_lines" => {
929 let url = expect_str(args.first())?.to_string();
930 let headers_val = args.get(1).cloned().unwrap_or(Value::Map(Default::default()));
931 let body = expect_str(args.get(2))?.to_string();
932 self.ensure_host_allowed(&url)?;
933 Ok(http_stream_lines_impl(self, &url, &headers_val, &body))
934 }
935 _ => unreachable!(),
936 };
937 }
938 if kind == "arrow" && op == "read_csv" {
944 self.ensure_kind_allowed("fs_read")?;
945 let path = expect_str(args.first())?.to_string();
946 let resolved = self.resolve_read_path(&path);
947 if !self.policy.allow_fs_read.is_empty()
948 && !self.policy.allow_fs_read.iter().any(|a| resolved.starts_with(a))
949 {
950 return Err(format!("arrow.read_csv: `{path}` outside --allow-fs-read"));
951 }
952 return match crate::arrow::read_csv_at(&resolved) {
953 Ok(v) => Ok(ok(v)),
954 Err(e) => Ok(err(Value::Str(e.into()))),
955 };
956 }
957 if kind == "arrow" && (op == "read_parquet" || op == "read_parquet_cols") {
961 self.ensure_kind_allowed("fs_read")?;
962 let path = expect_str(args.first())?.to_string();
963 let resolved = self.resolve_read_path(&path);
964 if !self.policy.allow_fs_read.is_empty()
965 && !self.policy.allow_fs_read.iter().any(|a| resolved.starts_with(a))
966 {
967 return Err(format!("arrow.{op}: `{path}` outside --allow-fs-read"));
968 }
969 let r = if op == "read_parquet" {
970 crate::arrow::read_parquet_at(&resolved)
971 } else {
972 let cols = match args.get(1) {
973 Some(Value::List(items)) => {
974 let mut out = Vec::with_capacity(items.len());
975 for v in items.iter() {
976 match v {
977 Value::Str(s) => out.push(s.to_string()),
978 other => return Err(format!(
979 "arrow.read_parquet_cols: column name not Str: {other:?}")),
980 }
981 }
982 out
983 }
984 other => return Err(format!(
985 "arrow.read_parquet_cols: expected List[Str], got {other:?}")),
986 };
987 crate::arrow::read_parquet_cols_at(&resolved, &cols)
988 };
989 return match r {
990 Ok(v) => Ok(ok(v)),
991 Err(e) => Ok(err(Value::Str(e.into()))),
992 };
993 }
994 if kind == "arrow" && (op == "write_parquet" || op == "write_csv") {
997 self.ensure_kind_allowed("fs_write")?;
998 let table_v = args.first().cloned().unwrap_or(Value::Unit);
999 let rb = match &table_v {
1000 Value::ArrowTable(t) => Arc::clone(t),
1001 other => return Err(format!("arrow.{op}: first arg must be arrow.Table, got {other:?}")),
1002 };
1003 let path = expect_str(args.get(1))?.to_string();
1004 if let Err(e) = self.ensure_fs_write_path(&path) {
1005 return Ok(err(Value::Str(format!("arrow.{op}: {e}").into())));
1006 }
1007 let r = if op == "write_parquet" {
1008 crate::arrow::write_parquet_at(&rb, std::path::Path::new(&path))
1009 } else {
1010 crate::arrow::write_csv_at(&rb, std::path::Path::new(&path))
1011 };
1012 return match r {
1013 Ok(_) => Ok(ok(Value::Unit)),
1014 Err(e) => Ok(err(Value::Str(e.into()))),
1015 };
1016 }
1017 if kind == "net" && op == "default_opts" {
1022 return Ok(ServeOpts::lex_defaults().to_value());
1023 }
1024 if kind == "tls" {
1032 return match op {
1033 "from_pem_files" => {
1034 self.ensure_kind_allowed("fs_read")?;
1035 dispatch_tls_from_pem_files(self, args)
1036 }
1037 "self_signed" => dispatch_tls_self_signed(args),
1038 other => Err(format!("unsupported tls.{other}")),
1039 };
1040 }
1041 if kind == "redis" {
1045 self.ensure_kind_allowed("net")?;
1046 } else {
1047 self.ensure_kind_allowed(kind)?;
1048 }
1049 match (kind, op) {
1050 ("io", "print") => {
1051 let line = expect_str(args.first())?;
1052 self.sink.print_line(line);
1053 Ok(Value::Unit)
1054 }
1055 ("io", "read") => {
1056 let path = expect_str(args.first())?.to_string();
1057 let resolved = self.resolve_read_path(&path);
1058 if !self.policy.allow_fs_read.is_empty()
1065 && !self.policy.allow_fs_read.iter().any(|a| resolved.starts_with(a))
1066 {
1067 return Err(format!("read of `{path}` outside --allow-fs-read"));
1068 }
1069 match std::fs::read_to_string(&resolved) {
1070 Ok(s) => Ok(ok(Value::Str(s.into()))),
1071 Err(e) => Ok(err(Value::Str(format!("{e}").into()))),
1072 }
1073 }
1074 ("io", "readline") => {
1075 use std::io::BufRead;
1076 let stdin = std::io::stdin();
1077 let mut line = String::new();
1078 match stdin.lock().read_line(&mut line) {
1079 Ok(0) => Ok(Value::Variant { name: "None".into(), args: vec![] }),
1080 Ok(_) => {
1081 if line.ends_with('\n') { line.pop(); }
1082 if line.ends_with('\r') { line.pop(); }
1083 Ok(Value::Variant { name: "Some".into(), args: vec![Value::Str(line.into())] })
1084 }
1085 Err(_) => Ok(Value::Variant { name: "None".into(), args: vec![] }),
1086 }
1087 }
1088 ("io", "argv") => {
1089 let list: Vec<Value> = self.program_args.iter()
1090 .map(|s| Value::Str(s.as_str().into()))
1091 .collect();
1092 Ok(Value::List(list.into()))
1093 }
1094 ("io", "write") => {
1095 let path = expect_str(args.first())?.to_string();
1096 let contents = expect_str(args.get(1))?.to_string();
1097 if !self.policy.allow_fs_write.is_empty() {
1101 let raw = std::env::current_dir()
1102 .map(|cwd| cwd.join(&path))
1103 .unwrap_or_else(|_| std::path::PathBuf::from(&path));
1104 let p = std::fs::canonicalize(&raw).unwrap_or_else(|_| {
1108 raw.parent()
1109 .and_then(|par| std::fs::canonicalize(par).ok())
1110 .map(|par| par.join(raw.file_name().unwrap_or_default()))
1111 .unwrap_or(raw)
1112 });
1113 let allowed = self.policy.allow_fs_write.iter().any(|a| {
1114 let ca = std::fs::canonicalize(a).unwrap_or_else(|_| a.clone());
1115 p.starts_with(&ca)
1116 });
1117 if !allowed {
1118 return Err(format!("write to `{path}` outside --allow-fs-write"));
1119 }
1120 }
1121 match std::fs::write(&path, contents) {
1122 Ok(_) => Ok(ok(Value::Unit)),
1123 Err(e) => Ok(err(Value::Str(format!("{e}").into()))),
1124 }
1125 }
1126 ("time", "now") => {
1127 if let Ok(s) = std::env::var("LEX_TEST_NOW") {
1129 if let Ok(secs) = s.trim().parse::<i64>() {
1130 return Ok(Value::Int(secs));
1131 }
1132 }
1133 let secs = SystemTime::now().duration_since(UNIX_EPOCH)
1134 .map_err(|e| format!("time: {e}"))?.as_secs();
1135 Ok(Value::Int(secs as i64))
1136 }
1137 ("time", "now_ms") => {
1138 if let Ok(s) = std::env::var("LEX_TEST_NOW") {
1143 if let Ok(secs) = s.trim().parse::<i64>() {
1144 return Ok(Value::Int(secs.saturating_mul(1000)));
1145 }
1146 }
1147 let ms = SystemTime::now().duration_since(UNIX_EPOCH)
1148 .map_err(|e| format!("time: {e}"))?.as_millis();
1149 Ok(Value::Int(ms as i64))
1150 }
1151 ("time", "now_str") => {
1152 if let Ok(s) = std::env::var("LEX_TEST_NOW") {
1156 if let Ok(secs) = s.trim().parse::<i64>() {
1157 let dt = chrono::DateTime::<chrono::Utc>::from_timestamp(secs, 0)
1158 .unwrap_or_else(chrono::Utc::now);
1159 return Ok(Value::Str(dt.to_rfc3339().into()));
1160 }
1161 }
1162 Ok(Value::Str(chrono::Utc::now().to_rfc3339().into()))
1163 }
1164 ("time", "mono_ns") => {
1165 static MONO_START: OnceLock<std::time::Instant> = OnceLock::new();
1173 let start = MONO_START.get_or_init(std::time::Instant::now);
1174 let dur = std::time::Instant::now().duration_since(*start);
1175 Ok(Value::Int(dur.as_nanos() as i64))
1176 }
1177 ("time", "sleep_ms") => {
1178 let n = expect_int(args.first())?;
1186 if n > 0 {
1187 let ms = (n as u64).min(60_000);
1188 std::thread::sleep(std::time::Duration::from_millis(ms));
1189 }
1190 Ok(Value::Unit)
1191 }
1192 ("time", "sleep") => {
1193 let nanos = expect_int(args.first())?;
1199 if nanos > 0 {
1200 let bounded_nanos = (nanos as u64).min(60_000 * 1_000_000);
1201 std::thread::sleep(std::time::Duration::from_nanos(bounded_nanos));
1202 }
1203 Ok(Value::Unit)
1204 }
1205 ("rand", "int_in") => {
1206 let lo = expect_int(args.first())?;
1208 let hi = expect_int(args.get(1))?;
1209 Ok(Value::Int((lo + hi) / 2))
1210 }
1211 ("env", "get") => {
1216 let name = expect_str(args.first())?;
1217 Ok(match std::env::var(name) {
1218 Ok(v) => Value::Variant {
1219 name: "Some".into(),
1220 args: vec![Value::Str(v.into())],
1221 },
1222 Err(_) => Value::Variant { name: "None".into(), args: Vec::new() },
1223 })
1224 }
1225 ("budget", _) => {
1226 Ok(Value::Unit)
1229 }
1230 ("net", "get") => {
1231 let url = expect_str(args.first())?.to_string();
1232 self.ensure_host_allowed(&url)?;
1233 Ok(http_request("GET", &url, None))
1234 }
1235 ("net", "post") => {
1236 let url = expect_str(args.first())?.to_string();
1237 let body = expect_str(args.get(1))?.to_string();
1238 self.ensure_host_allowed(&url)?;
1239 Ok(http_request("POST", &url, Some(&body)))
1240 }
1241 ("net", "serve") => {
1242 let port = match args.first() {
1243 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1244 _ => return Err("net.serve(port, handler): port must be Int 0..=65535".into()),
1245 };
1246 let handler_name = expect_str(args.get(1))?.to_string();
1247 let program = self.program.clone()
1248 .ok_or_else(|| "net.serve requires a Program reference; use DefaultHandler::with_program".to_string())?;
1249 let policy = self.policy.clone();
1250 serve_http(port, handler_name, program, policy, None, ServeOpts::from_env())
1251 }
1252 ("net", "serve_fn") => {
1253 let port = match args.first() {
1254 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1255 _ => return Err("net.serve_fn(port, handler): port must be Int 0..=65535".into()),
1256 };
1257 let closure = match args.into_iter().nth(1) {
1258 Some(c @ Value::Closure { .. }) => c,
1259 _ => return Err("net.serve_fn(port, handler): handler must be a closure".into()),
1260 };
1261 let program = self.program.clone()
1262 .ok_or_else(|| "net.serve_fn requires a Program reference; use DefaultHandler::with_program".to_string())?;
1263 let policy = self.policy.clone();
1264 serve_http_fn(port, closure, program, policy, ServeOpts::from_env())
1265 }
1266 ("net", "serve_routed") => {
1267 let port = match args.first() {
1268 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1269 _ => return Err("net.serve_routed(port, routes, fallback): port must be Int 0..=65535".into()),
1270 };
1271 let routes_val = args.get(1).cloned()
1272 .ok_or_else(|| "net.serve_routed(port, routes, fallback): missing routes".to_string())?;
1273 let fallback = match args.into_iter().nth(2) {
1274 Some(c @ Value::Closure { .. }) => c,
1275 _ => return Err("net.serve_routed(port, routes, fallback): fallback must be a closure".into()),
1276 };
1277 let routes = decode_routes_arg(routes_val)?;
1278 let program = self.program.clone()
1279 .ok_or_else(|| "net.serve_routed requires a Program reference; use DefaultHandler::with_program".to_string())?;
1280 let policy = self.policy.clone();
1281 serve_http_routed(port, routes, fallback, program, policy, ServeOpts::from_env())
1282 }
1283 ("net", "serve_with") => {
1284 let port = match args.first() {
1286 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1287 _ => return Err("net.serve_with(port, handler, opts): port must be Int 0..=65535".into()),
1288 };
1289 let handler_name = expect_str(args.get(1))?.to_string();
1290 let opts = decode_serve_opts(args.get(2)
1291 .ok_or_else(|| "net.serve_with(port, handler, opts): missing opts".to_string())?)?;
1292 let program = self.program.clone()
1293 .ok_or_else(|| "net.serve_with requires a Program reference; use DefaultHandler::with_program".to_string())?;
1294 let policy = self.policy.clone();
1295 serve_http(port, handler_name, program, policy, None, opts)
1296 }
1297 ("net", "serve_fn_with") => {
1298 let port = match args.first() {
1300 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1301 _ => return Err("net.serve_fn_with(port, handler, opts): port must be Int 0..=65535".into()),
1302 };
1303 let opts = decode_serve_opts(args.get(2)
1304 .ok_or_else(|| "net.serve_fn_with(port, handler, opts): missing opts".to_string())?)?;
1305 let closure = match args.into_iter().nth(1) {
1306 Some(c @ Value::Closure { .. }) => c,
1307 _ => return Err("net.serve_fn_with(port, handler, opts): handler must be a closure".into()),
1308 };
1309 let program = self.program.clone()
1310 .ok_or_else(|| "net.serve_fn_with requires a Program reference; use DefaultHandler::with_program".to_string())?;
1311 let policy = self.policy.clone();
1312 serve_http_fn(port, closure, program, policy, opts)
1313 }
1314 ("net", "serve_routed_with") => {
1315 let port = match args.first() {
1317 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1318 _ => return Err("net.serve_routed_with(port, routes, fallback, opts): port must be Int 0..=65535".into()),
1319 };
1320 let routes_val = args.get(1).cloned()
1321 .ok_or_else(|| "net.serve_routed_with(port, routes, fallback, opts): missing routes".to_string())?;
1322 let opts = decode_serve_opts(args.get(3)
1323 .ok_or_else(|| "net.serve_routed_with(port, routes, fallback, opts): missing opts".to_string())?)?;
1324 let fallback = match args.into_iter().nth(2) {
1325 Some(c @ Value::Closure { .. }) => c,
1326 _ => return Err("net.serve_routed_with(port, routes, fallback, opts): fallback must be a closure".into()),
1327 };
1328 let routes = decode_routes_arg(routes_val)?;
1329 let program = self.program.clone()
1330 .ok_or_else(|| "net.serve_routed_with requires a Program reference; use DefaultHandler::with_program".to_string())?;
1331 let policy = self.policy.clone();
1332 serve_http_routed(port, routes, fallback, program, policy, opts)
1333 }
1334 ("net", "serve_quic") => self.dispatch_serve_quic_named(args),
1335 ("net", "serve_quic_fn") => self.dispatch_serve_quic_fn(args),
1336 ("net", "serve_quic_routed") => self.dispatch_serve_quic_routed(args),
1337 ("net", "serve_tls") => {
1338 let port = match args.first() {
1339 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1340 _ => return Err("net.serve_tls(port, cert, key, handler): port must be Int 0..=65535".into()),
1341 };
1342 let cert_path = expect_str(args.get(1))?.to_string();
1343 let key_path = expect_str(args.get(2))?.to_string();
1344 let handler_name = expect_str(args.get(3))?.to_string();
1345 let program = self.program.clone()
1346 .ok_or_else(|| "net.serve_tls requires a Program reference".to_string())?;
1347 let policy = self.policy.clone();
1348 let cert = std::fs::read(&cert_path)
1349 .map_err(|e| format!("net.serve_tls: read cert {cert_path}: {e}"))?;
1350 let key = std::fs::read(&key_path)
1351 .map_err(|e| format!("net.serve_tls: read key {key_path}: {e}"))?;
1352 serve_http(port, handler_name, program, policy, Some(TlsConfig { cert, key }), ServeOpts::from_env())
1353 }
1354 ("net", "serve_ws") => {
1355 let port = match args.first() {
1356 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1357 _ => return Err("net.serve_ws(port, on_message): port must be Int 0..=65535".into()),
1358 };
1359 let handler_name = expect_str(args.get(1))?.to_string();
1360 let program = self.program.clone()
1361 .ok_or_else(|| "net.serve_ws requires a Program reference".to_string())?;
1362 let policy = self.policy.clone();
1363 let registry = Arc::new(crate::ws::ChatRegistry::default());
1364 crate::ws::serve_ws(port, handler_name, program, policy, registry)
1365 }
1366 ("net", "serve_ws_fn") => {
1367 let port = match args.first() {
1368 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1369 _ => return Err("net.serve_ws_fn(port, subprotocol, handler): port must be Int 0..=65535".into()),
1370 };
1371 let subprotocol = expect_str(args.get(1))?.to_string();
1372 let closure = match args.into_iter().nth(2) {
1373 Some(c @ Value::Closure { .. }) => c,
1374 _ => return Err("net.serve_ws_fn(port, subprotocol, handler): handler must be a closure".into()),
1375 };
1376 let program = self.program.clone()
1377 .ok_or_else(|| "net.serve_ws_fn requires a Program reference; use DefaultHandler::with_program".to_string())?;
1378 let policy = self.policy.clone();
1379 let registry = Arc::new(crate::ws::ChatRegistry::default());
1380 crate::ws::serve_ws_fn(port, subprotocol, closure, program, policy, registry)
1381 }
1382 ("net", "serve_ws_fn_auth") => {
1383 let port = match args.first() {
1385 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1386 _ => return Err("net.serve_ws_fn_auth(port, subprotocol, auth, on_message): port must be Int 0..=65535".into()),
1387 };
1388 let subprotocol = expect_str(args.get(1))?.to_string();
1389 let mut it = args.into_iter().skip(2);
1390 let auth_closure = match it.next() {
1391 Some(c @ Value::Closure { .. }) => c,
1392 _ => return Err("net.serve_ws_fn_auth(port, subprotocol, auth, on_message): auth must be a closure".into()),
1393 };
1394 let handler_closure = match it.next() {
1395 Some(c @ Value::Closure { .. }) => c,
1396 _ => return Err("net.serve_ws_fn_auth(port, subprotocol, auth, on_message): on_message must be a closure".into()),
1397 };
1398 let program = self.program.clone()
1399 .ok_or_else(|| "net.serve_ws_fn_auth requires a Program reference; use DefaultHandler::with_program".to_string())?;
1400 let policy = self.policy.clone();
1401 let registry = Arc::new(crate::ws::ChatRegistry::default());
1402 crate::ws::serve_ws_fn_auth(
1403 port, subprotocol, auth_closure, handler_closure,
1404 program, policy, registry,
1405 )
1406 }
1407 ("net", "serve_ws_fn_actor") => {
1408 let port = match args.first() {
1410 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
1411 _ => return Err("net.serve_ws_fn_actor(port, subprotocol, name_of, on_message): port must be Int 0..=65535".into()),
1412 };
1413 let subprotocol = expect_str(args.get(1))?.to_string();
1414 let mut it = args.into_iter().skip(2);
1415 let name_of_closure = match it.next() {
1416 Some(c @ Value::Closure { .. }) => c,
1417 _ => return Err("net.serve_ws_fn_actor(port, subprotocol, name_of, on_message): name_of must be a closure".into()),
1418 };
1419 let on_message_closure = match it.next() {
1420 Some(c @ Value::Closure { .. }) => c,
1421 _ => return Err("net.serve_ws_fn_actor(port, subprotocol, name_of, on_message): on_message must be a closure".into()),
1422 };
1423 let program = self.program.clone()
1424 .ok_or_else(|| "net.serve_ws_fn_actor requires a Program reference; use DefaultHandler::with_program".to_string())?;
1425 let policy = self.policy.clone();
1426 let registry = Arc::new(crate::ws::ChatRegistry::default());
1427 crate::ws::serve_ws_fn_actor(
1428 port, subprotocol, name_of_closure, on_message_closure,
1429 program, policy, registry,
1430 )
1431 }
1432 ("net", "dial_ws") => {
1433 let url = expect_str(args.first())?.to_string();
1435 let subprotocol = expect_str(args.get(1))?.to_string();
1436 let on_open = match args.get(2).cloned() {
1437 Some(c @ Value::Closure { .. }) => c,
1438 _ => return Err(
1439 "net.dial_ws(url, subprotocol, on_open, on_message): on_open must be a closure".into(),
1440 ),
1441 };
1442 let on_message = match args.into_iter().nth(3) {
1443 Some(c @ Value::Closure { .. }) => c,
1444 _ => return Err(
1445 "net.dial_ws(url, subprotocol, on_open, on_message): on_message must be a closure".into(),
1446 ),
1447 };
1448 let program = self.program.clone().ok_or_else(|| {
1449 "net.dial_ws requires a Program reference; use DefaultHandler::with_program".to_string()
1450 })?;
1451 let policy = self.policy.clone();
1452 crate::ws::dial_ws(url, subprotocol, on_open, on_message, program, policy)
1453 }
1454 ("net", "dial_ws_actor") => {
1455 let url = expect_str(args.first())?.to_string();
1457 let subprotocol = expect_str(args.get(1))?.to_string();
1458 let name = expect_str(args.get(2))?.to_string();
1459 let on_open = match args.get(3).cloned() {
1460 Some(c @ Value::Closure { .. }) => c,
1461 _ => return Err(
1462 "net.dial_ws_actor(url, subprotocol, name, on_open, on_message): on_open must be a closure".into(),
1463 ),
1464 };
1465 let on_message = match args.into_iter().nth(4) {
1466 Some(c @ Value::Closure { .. }) => c,
1467 _ => return Err(
1468 "net.dial_ws_actor(url, subprotocol, name, on_open, on_message): on_message must be a closure".into(),
1469 ),
1470 };
1471 let program = self.program.clone().ok_or_else(|| {
1472 "net.dial_ws_actor requires a Program reference; use DefaultHandler::with_program".to_string()
1473 })?;
1474 let policy = self.policy.clone();
1475 crate::ws::dial_ws_actor(url, subprotocol, name, on_open, on_message, program, policy)
1476 }
1477 ("chat", "broadcast") => {
1478 let registry = self.chat_registry.as_ref()
1479 .ok_or_else(|| "chat.broadcast called outside a net.serve_ws handler".to_string())?;
1480 let room = expect_str(args.first())?;
1481 let body = expect_str(args.get(1))?;
1482 crate::ws::chat_broadcast(registry, room, body);
1483 Ok(Value::Unit)
1484 }
1485 ("chat", "send") => {
1486 let registry = self.chat_registry.as_ref()
1487 .ok_or_else(|| "chat.send called outside a net.serve_ws handler".to_string())?;
1488 let conn_id = match args.first() {
1489 Some(Value::Int(n)) if *n >= 0 => *n as u64,
1490 _ => return Err("chat.send: conn_id must be non-negative Int".into()),
1491 };
1492 let body = expect_str(args.get(1))?;
1493 Ok(Value::Bool(crate::ws::chat_send(registry, conn_id, body)))
1494 }
1495 ("kv", "open") => {
1496 let path = expect_str(args.first())?.to_string();
1497 if !self.policy.allow_fs_write.is_empty() {
1501 let p = std::path::Path::new(&path);
1502 if !self.policy.allow_fs_write.iter().any(|a| p.starts_with(a)) {
1503 return Ok(err(Value::Str(format!(
1504 "kv.open: `{path}` outside --allow-fs-write").into())));
1505 }
1506 }
1507 match sled::open(&path) {
1508 Ok(db) => {
1509 let handle = next_kv_handle();
1510 kv_registry().lock().unwrap().insert(handle, db);
1511 Ok(ok(Value::Int(handle as i64)))
1512 }
1513 Err(e) => Ok(err(Value::Str(format!("kv.open: {e}").into()))),
1514 }
1515 }
1516 ("kv", "close") => {
1517 let h = expect_kv_handle(args.first())?;
1518 kv_registry().lock().unwrap().remove(h);
1519 Ok(Value::Unit)
1520 }
1521 ("kv", "get") => {
1522 let h = expect_kv_handle(args.first())?;
1523 let key = expect_str(args.get(1))?;
1524 let mut reg = kv_registry().lock().unwrap();
1525 let db = reg.touch_get(h).ok_or_else(|| "kv.get: closed or unknown Kv handle".to_string())?;
1526 match db.get(key.as_bytes()) {
1527 Ok(Some(ivec)) => Ok(some(Value::Bytes(ivec.to_vec()))),
1528 Ok(None) => Ok(none()),
1529 Err(e) => Err(format!("kv.get: {e}")),
1530 }
1531 }
1532 ("kv", "put") => {
1533 let h = expect_kv_handle(args.first())?;
1534 let key = expect_str(args.get(1))?.to_string();
1535 let val = expect_bytes(args.get(2))?.clone();
1536 let mut reg = kv_registry().lock().unwrap();
1537 let db = reg.touch_get(h).ok_or_else(|| "kv.put: closed or unknown Kv handle".to_string())?;
1538 match db.insert(key.as_bytes(), val) {
1539 Ok(_) => Ok(ok(Value::Unit)),
1540 Err(e) => Ok(err(Value::Str(format!("kv.put: {e}").into()))),
1541 }
1542 }
1543 ("kv", "delete") => {
1544 let h = expect_kv_handle(args.first())?;
1545 let key = expect_str(args.get(1))?;
1546 let mut reg = kv_registry().lock().unwrap();
1547 let db = reg.touch_get(h).ok_or_else(|| "kv.delete: closed or unknown Kv handle".to_string())?;
1548 match db.remove(key.as_bytes()) {
1549 Ok(_) => Ok(ok(Value::Unit)),
1550 Err(e) => Ok(err(Value::Str(format!("kv.delete: {e}").into()))),
1551 }
1552 }
1553 ("kv", "contains") => {
1554 let h = expect_kv_handle(args.first())?;
1555 let key = expect_str(args.get(1))?;
1556 let mut reg = kv_registry().lock().unwrap();
1557 let db = reg.touch_get(h).ok_or_else(|| "kv.contains: closed or unknown Kv handle".to_string())?;
1558 match db.contains_key(key.as_bytes()) {
1559 Ok(present) => Ok(Value::Bool(present)),
1560 Err(e) => Err(format!("kv.contains: {e}")),
1561 }
1562 }
1563 ("kv", "list_prefix") => {
1564 let h = expect_kv_handle(args.first())?;
1565 let prefix = expect_str(args.get(1))?;
1566 let mut reg = kv_registry().lock().unwrap();
1567 let db = reg.touch_get(h).ok_or_else(|| "kv.list_prefix: closed or unknown Kv handle".to_string())?;
1568 let mut keys: Vec<Value> = Vec::new();
1569 for kv in db.scan_prefix(prefix.as_bytes()) {
1570 let (k, _) = kv.map_err(|e| format!("kv.list_prefix: {e}"))?;
1571 let s = String::from_utf8_lossy(&k).to_string();
1572 keys.push(Value::Str(s.into()));
1573 }
1574 Ok(Value::List(keys.into()))
1575 }
1576 ("sql", "open") => {
1577 let path = expect_str(args.first())?.to_string();
1578 if path.starts_with("postgres://") || path.starts_with("postgresql://") {
1579 match postgres::Client::connect(&path, postgres::NoTls) {
1581 Ok(client) => {
1582 let handle = next_sql_handle();
1583 sql_registry().lock().unwrap().insert(handle, SqlConn::Postgres(client));
1584 Ok(ok(Value::Int(handle as i64)))
1585 }
1586 Err(e) => Ok(err(pg_err_to_sql_error(e, "sql.open"))),
1587 }
1588 } else {
1589 if path != ":memory:" && !self.policy.allow_fs_write.is_empty() {
1592 let p = std::path::Path::new(&path);
1593 if !self.policy.allow_fs_write.iter().any(|a| p.starts_with(a)) {
1594 return Ok(err(sql_error(
1595 format!("sql.open: `{path}` outside --allow-fs-write"),
1596 None, None,
1597 )));
1598 }
1599 }
1600 match rusqlite::Connection::open(&path) {
1601 Ok(conn) => {
1602 let handle = next_sql_handle();
1603 sql_registry().lock().unwrap().insert(handle, SqlConn::Sqlite(conn));
1604 Ok(ok(Value::Int(handle as i64)))
1605 }
1606 Err(e) => Ok(err(sqlite_err_to_sql_error(e, "sql.open"))),
1607 }
1608 }
1609 }
1610 ("sql", "close") => {
1611 let h = expect_sql_handle(args.first())?;
1612 sql_registry().lock().unwrap().remove(h);
1613 Ok(Value::Unit)
1614 }
1615 ("sql", "exec") => {
1616 let h = expect_sql_handle(args.first())?;
1617 let stmt = expect_str(args.get(1))?.to_string();
1618 let params = expect_sql_params(args.get(2))?;
1619 let arc = sql_registry().lock().unwrap()
1620 .touch_get(h)
1621 .ok_or_else(|| "sql.exec: closed or unknown Db handle".to_string())?;
1622 let mut conn = arc.lock().unwrap();
1623 match &mut *conn {
1624 SqlConn::Sqlite(c) => {
1625 let bound = sqlite_params(¶ms);
1626 let bind: Vec<&dyn rusqlite::ToSql> =
1627 bound.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
1628 match c.execute(&stmt, rusqlite::params_from_iter(bind.iter())) {
1629 Ok(n) => Ok(ok(Value::Int(n as i64))),
1630 Err(e) => Ok(err(sqlite_err_to_sql_error(e, "sql.exec"))),
1631 }
1632 }
1633 SqlConn::Postgres(c) => {
1634 let pg = pg_param_refs(¶ms);
1635 let refs: Vec<&(dyn postgres::types::ToSql + Sync)> =
1636 pg.iter().map(|b| b.as_ref()).collect();
1637 match c.execute(stmt.as_str(), &refs) {
1638 Ok(n) => Ok(ok(Value::Int(n as i64))),
1639 Err(e) => Ok(err(pg_err_to_sql_error(e, "sql.exec"))),
1640 }
1641 }
1642 }
1643 }
1644 ("sql", "query") => {
1645 let h = expect_sql_handle(args.first())?;
1646 let stmt_str = expect_str(args.get(1))?.to_string();
1647 let params = expect_sql_params(args.get(2))?;
1648 let arc = sql_registry().lock().unwrap()
1649 .touch_get(h)
1650 .ok_or_else(|| "sql.query: closed or unknown Db handle".to_string())?;
1651 let mut conn = arc.lock().unwrap();
1652 Ok(match &mut *conn {
1653 SqlConn::Sqlite(c) => sql_run_query_sqlite(c, &stmt_str, ¶ms),
1654 SqlConn::Postgres(c) => sql_run_query_pg(c, &stmt_str, ¶ms),
1655 })
1656 }
1657 ("sql", "query_iter") => {
1663 let h = expect_sql_handle(args.first())?;
1664 let stmt_str = expect_str(args.get(1))?.to_string();
1665 let params = expect_sql_params(args.get(2))?;
1666 let arc = sql_registry().lock().unwrap()
1667 .touch_get(h)
1668 .ok_or_else(|| "sql.query_iter: closed or unknown Db handle".to_string())?;
1669
1670 let (sender, receiver) = std::sync::mpsc::sync_channel::<Result<Value, String>>(
1674 CURSOR_CHANNEL_CAPACITY,
1675 );
1676 let cursor_h = next_cursor_handle();
1677 cursor_registry().lock().unwrap().insert(cursor_h, receiver);
1678
1679 let arc_for_thread = Arc::clone(&arc);
1680 let is_sqlite = matches!(*arc.lock().unwrap(), SqlConn::Sqlite(_));
1686 std::thread::spawn(move || {
1687 if is_sqlite {
1688 sqlite_cursor_producer(arc_for_thread, stmt_str, params, sender);
1689 } else {
1690 pg_cursor_producer(arc_for_thread, stmt_str, params, sender);
1691 }
1692 });
1693
1694 Ok(ok(Value::Variant {
1695 name: "__IterCursor".into(),
1696 args: vec![Value::Int(cursor_h as i64)],
1697 }))
1698 }
1699 ("sql", "cursor_next") => {
1705 let h = match args.first() {
1706 Some(Value::Int(n)) if *n >= 0 => *n as u64,
1707 _ => return Err("sql.cursor_next: expected cursor handle (Int)".into()),
1708 };
1709 let rx_arc = match cursor_registry().lock().unwrap().touch_get(h) {
1710 Some(a) => a,
1711 None => return Ok(Value::Variant { name: "None".into(), args: vec![] }),
1712 };
1713 let recv_result = {
1718 let rx = match rx_arc.lock() {
1719 Ok(g) => g,
1720 Err(p) => p.into_inner(),
1721 };
1722 rx.recv()
1723 };
1724 match recv_result {
1725 Ok(Ok(row)) => Ok(Value::Variant {
1726 name: "Some".into(),
1727 args: vec![row],
1728 }),
1729 Ok(Err(_)) | Err(_) => {
1730 cursor_registry().lock().unwrap().remove(h);
1734 Ok(Value::Variant { name: "None".into(), args: vec![] })
1735 }
1736 }
1737 }
1738 ("sql", "begin") => {
1743 let h = expect_sql_handle(args.first())?;
1744 let arc = sql_registry().lock().unwrap()
1745 .touch_get(h)
1746 .ok_or_else(|| "sql.begin: closed or unknown Db handle".to_string())?;
1747 let mut conn = arc.lock().unwrap();
1748 match &mut *conn {
1749 SqlConn::Sqlite(c) => match c.execute_batch("BEGIN") {
1750 Ok(()) => Ok(ok(Value::Int(h as i64))),
1751 Err(e) => Ok(err(sqlite_err_to_sql_error(e, "sql.begin"))),
1752 },
1753 SqlConn::Postgres(c) => match c.batch_execute("BEGIN") {
1754 Ok(()) => Ok(ok(Value::Int(h as i64))),
1755 Err(e) => Ok(err(pg_err_to_sql_error(e, "sql.begin"))),
1756 },
1757 }
1758 }
1759 ("sql", "commit") => {
1760 let h = expect_sql_handle(args.first())?;
1761 let arc = sql_registry().lock().unwrap()
1762 .touch_get(h)
1763 .ok_or_else(|| "sql.commit: closed or unknown SqlTx handle".to_string())?;
1764 let mut conn = arc.lock().unwrap();
1765 match &mut *conn {
1766 SqlConn::Sqlite(c) => match c.execute_batch("COMMIT") {
1767 Ok(()) => Ok(ok(Value::Unit)),
1768 Err(e) => Ok(err(sqlite_err_to_sql_error(e, "sql.commit"))),
1769 },
1770 SqlConn::Postgres(c) => match c.batch_execute("COMMIT") {
1771 Ok(()) => Ok(ok(Value::Unit)),
1772 Err(e) => Ok(err(pg_err_to_sql_error(e, "sql.commit"))),
1773 },
1774 }
1775 }
1776 ("sql", "rollback") => {
1777 let h = expect_sql_handle(args.first())?;
1778 let arc = sql_registry().lock().unwrap()
1779 .touch_get(h)
1780 .ok_or_else(|| "sql.rollback: closed or unknown SqlTx handle".to_string())?;
1781 let mut conn = arc.lock().unwrap();
1782 match &mut *conn {
1783 SqlConn::Sqlite(c) => match c.execute_batch("ROLLBACK") {
1784 Ok(()) => Ok(ok(Value::Unit)),
1785 Err(e) => Ok(err(sqlite_err_to_sql_error(e, "sql.rollback"))),
1786 },
1787 SqlConn::Postgres(c) => match c.batch_execute("ROLLBACK") {
1788 Ok(()) => Ok(ok(Value::Unit)),
1789 Err(e) => Ok(err(pg_err_to_sql_error(e, "sql.rollback"))),
1790 },
1791 }
1792 }
1793 ("sql", "exec_tx") => {
1794 let h = expect_sql_handle(args.first())?;
1795 let stmt = expect_str(args.get(1))?.to_string();
1796 let params = expect_sql_params(args.get(2))?;
1797 let arc = sql_registry().lock().unwrap()
1798 .touch_get(h)
1799 .ok_or_else(|| "sql.exec_tx: closed or unknown SqlTx handle".to_string())?;
1800 let mut conn = arc.lock().unwrap();
1801 match &mut *conn {
1802 SqlConn::Sqlite(c) => {
1803 let bound = sqlite_params(¶ms);
1804 let bind: Vec<&dyn rusqlite::ToSql> =
1805 bound.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
1806 match c.execute(&stmt, rusqlite::params_from_iter(bind.iter())) {
1807 Ok(n) => Ok(ok(Value::Int(n as i64))),
1808 Err(e) => Ok(err(sqlite_err_to_sql_error(e, "sql.exec_tx"))),
1809 }
1810 }
1811 SqlConn::Postgres(c) => {
1812 let pg = pg_param_refs(¶ms);
1813 let refs: Vec<&(dyn postgres::types::ToSql + Sync)> =
1814 pg.iter().map(|b| b.as_ref()).collect();
1815 match c.execute(stmt.as_str(), &refs) {
1816 Ok(n) => Ok(ok(Value::Int(n as i64))),
1817 Err(e) => Ok(err(pg_err_to_sql_error(e, "sql.exec_tx"))),
1818 }
1819 }
1820 }
1821 }
1822 ("sql", "query_tx") => {
1823 let h = expect_sql_handle(args.first())?;
1824 let stmt_str = expect_str(args.get(1))?.to_string();
1825 let params = expect_sql_params(args.get(2))?;
1826 let arc = sql_registry().lock().unwrap()
1827 .touch_get(h)
1828 .ok_or_else(|| "sql.query_tx: closed or unknown SqlTx handle".to_string())?;
1829 let mut conn = arc.lock().unwrap();
1830 Ok(match &mut *conn {
1831 SqlConn::Sqlite(c) => sql_run_query_sqlite(c, &stmt_str, ¶ms),
1832 SqlConn::Postgres(c) => sql_run_query_pg(c, &stmt_str, ¶ms),
1833 })
1834 }
1835 ("sql", "get_str") => Ok(sql_get_col(&args, |v| match v {
1836 Value::Str(s) => Some(Value::Str(s.clone())),
1837 Value::Int(n) => Some(Value::Str(n.to_string().into())),
1838 _ => None,
1839 })?),
1840 ("sql", "get_int") => Ok(sql_get_col(&args, |v| match v {
1841 Value::Int(n) => Some(Value::Int(*n)),
1842 Value::Float(f) => Some(Value::Int(*f as i64)),
1843 _ => None,
1844 })?),
1845 ("sql", "get_float") => Ok(sql_get_col(&args, |v| match v {
1846 Value::Float(f) => Some(Value::Float(*f)),
1847 Value::Int(n) => Some(Value::Float(*n as f64)),
1848 _ => None,
1849 })?),
1850 ("sql", "get_bool") => Ok(sql_get_col(&args, |v| match v {
1851 Value::Bool(b) => Some(Value::Bool(*b)),
1852 Value::Int(n) => Some(Value::Bool(*n != 0)),
1853 _ => None,
1854 })?),
1855
1856 ("redis", "connect") => {
1865 let url = expect_str(args.first())?.to_string();
1866 self.ensure_host_allowed(&url)?;
1867 match redis::Client::open(url.as_str()) {
1868 Ok(client) => match client.get_connection() {
1869 Ok(conn) => {
1870 let handle = next_redis_handle();
1871 redis_registry().lock().unwrap().insert(handle, RedisEntry { url, conn });
1872 Ok(ok(Value::Int(handle as i64)))
1873 }
1874 Err(e) => Ok(err(Value::Str(format!("redis.connect: {e}").into()))),
1875 },
1876 Err(e) => Ok(err(Value::Str(format!("redis.connect: {e}").into()))),
1877 }
1878 }
1879 ("redis", "close") => {
1880 let h = expect_redis_handle(args.first())?;
1881 redis_registry().lock().unwrap().remove(h);
1882 Ok(Value::Unit)
1883 }
1884 ("redis", "get") => {
1885 let h = expect_redis_handle(args.first())?;
1886 let key = expect_str(args.get(1))?.to_string();
1887 let mut reg = redis_registry().lock().unwrap();
1888 let entry = reg.touch_get_mut(h)
1889 .ok_or_else(|| "redis.get: closed or unknown ConnRedis handle".to_string())?;
1890 use redis::Commands;
1891 match entry.conn.get::<_, Option<String>>(&key) {
1892 Ok(Some(v)) => Ok(some(Value::Str(v.into()))),
1893 Ok(None) => Ok(none()),
1894 Err(e) => Err(format!("redis.get: {e}")),
1895 }
1896 }
1897 ("redis", "set") => {
1898 let h = expect_redis_handle(args.first())?;
1899 let key = expect_str(args.get(1))?.to_string();
1900 let val = expect_str(args.get(2))?.to_string();
1901 let mut reg = redis_registry().lock().unwrap();
1902 let entry = reg.touch_get_mut(h)
1903 .ok_or_else(|| "redis.set: closed or unknown ConnRedis handle".to_string())?;
1904 use redis::Commands;
1905 entry.conn.set::<_, _, ()>(&key, &val)
1906 .map_err(|e| format!("redis.set: {e}"))?;
1907 Ok(Value::Unit)
1908 }
1909 ("redis", "set_ex") => {
1910 let h = expect_redis_handle(args.first())?;
1911 let key = expect_str(args.get(1))?.to_string();
1912 let val = expect_str(args.get(2))?.to_string();
1913 let ttl = expect_int(args.get(3))?;
1914 let mut reg = redis_registry().lock().unwrap();
1915 let entry = reg.touch_get_mut(h)
1916 .ok_or_else(|| "redis.set_ex: closed or unknown ConnRedis handle".to_string())?;
1917 use redis::Commands;
1918 entry.conn.set_ex::<_, _, ()>(&key, &val, ttl as u64)
1919 .map_err(|e| format!("redis.set_ex: {e}"))?;
1920 Ok(Value::Unit)
1921 }
1922 ("redis", "del") => {
1923 let h = expect_redis_handle(args.first())?;
1924 let key = expect_str(args.get(1))?.to_string();
1925 let mut reg = redis_registry().lock().unwrap();
1926 let entry = reg.touch_get_mut(h)
1927 .ok_or_else(|| "redis.del: closed or unknown ConnRedis handle".to_string())?;
1928 use redis::Commands;
1929 entry.conn.del::<_, ()>(&key)
1930 .map_err(|e| format!("redis.del: {e}"))?;
1931 Ok(Value::Unit)
1932 }
1933 ("redis", "exists") => {
1934 let h = expect_redis_handle(args.first())?;
1935 let key = expect_str(args.get(1))?.to_string();
1936 let mut reg = redis_registry().lock().unwrap();
1937 let entry = reg.touch_get_mut(h)
1938 .ok_or_else(|| "redis.exists: closed or unknown ConnRedis handle".to_string())?;
1939 use redis::Commands;
1940 let present: bool = entry.conn.exists(&key)
1941 .map_err(|e| format!("redis.exists: {e}"))?;
1942 Ok(Value::Bool(present))
1943 }
1944 ("redis", "expire") => {
1945 let h = expect_redis_handle(args.first())?;
1946 let key = expect_str(args.get(1))?.to_string();
1947 let ttl = expect_int(args.get(2))?;
1948 let mut reg = redis_registry().lock().unwrap();
1949 let entry = reg.touch_get_mut(h)
1950 .ok_or_else(|| "redis.expire: closed or unknown ConnRedis handle".to_string())?;
1951 use redis::Commands;
1952 entry.conn.expire::<_, ()>(&key, ttl)
1953 .map_err(|e| format!("redis.expire: {e}"))?;
1954 Ok(Value::Unit)
1955 }
1956 ("redis", "publish") => {
1957 let h = expect_redis_handle(args.first())?;
1958 let channel = expect_str(args.get(1))?.to_string();
1959 let msg = expect_str(args.get(2))?.to_string();
1960 let mut reg = redis_registry().lock().unwrap();
1961 let entry = reg.touch_get_mut(h)
1962 .ok_or_else(|| "redis.publish: closed or unknown ConnRedis handle".to_string())?;
1963 use redis::Commands;
1964 let n: i64 = entry.conn.publish(&channel, &msg)
1965 .map_err(|e| format!("redis.publish: {e}"))?;
1966 Ok(Value::Int(n))
1967 }
1968 ("redis", "subscribe") => {
1973 let h = expect_redis_handle(args.first())?;
1974 let channel = expect_str(args.get(1))?.to_string();
1975 let closure = match args.into_iter().nth(2) {
1976 Some(c @ Value::Closure { .. }) => c,
1977 _ => return Err("redis.subscribe: handler must be a Closure".into()),
1978 };
1979 let program = self.program.clone()
1980 .ok_or("redis.subscribe: no program; call DefaultHandler::with_program")?;
1981 let policy = self.policy.clone();
1982 let url = redis_registry().lock().unwrap()
1983 .get_url(h)
1984 .ok_or("redis.subscribe: closed or unknown ConnRedis handle")?;
1985 let client = redis::Client::open(url.as_str())
1986 .map_err(|e| format!("redis.subscribe: {e}"))?;
1987 let mut conn = client.get_connection()
1988 .map_err(|e| format!("redis.subscribe: {e}"))?;
1989 let mut pubsub = conn.as_pubsub();
1990 pubsub.subscribe(&channel)
1991 .map_err(|e| format!("redis.subscribe: {e}"))?;
1992 loop {
1993 let msg = pubsub.get_message()
1994 .map_err(|e| format!("redis.subscribe: {e}"))?;
1995 let ch: String = msg.get_channel_name().to_string();
1996 let payload: String = msg.get_payload()
1997 .map_err(|e| format!("redis.subscribe: payload: {e}"))?;
1998 let handler = DefaultHandler::new(policy.clone())
1999 .with_program(Arc::clone(&program));
2000 let mut vm = Vm::with_handler(&program, Box::new(handler));
2001 vm.invoke_closure_value(closure.clone(), vec![
2002 Value::Str(ch.into()),
2003 Value::Str(payload.into()),
2004 ]).map_err(|e| format!("redis.subscribe: handler: {e:?}"))?;
2005 }
2006 }
2007 ("redis", "psubscribe") => {
2008 let h = expect_redis_handle(args.first())?;
2009 let pattern = expect_str(args.get(1))?.to_string();
2010 let closure = match args.into_iter().nth(2) {
2011 Some(c @ Value::Closure { .. }) => c,
2012 _ => return Err("redis.psubscribe: handler must be a Closure".into()),
2013 };
2014 let program = self.program.clone()
2015 .ok_or("redis.psubscribe: no program; call DefaultHandler::with_program")?;
2016 let policy = self.policy.clone();
2017 let url = redis_registry().lock().unwrap()
2018 .get_url(h)
2019 .ok_or("redis.psubscribe: closed or unknown ConnRedis handle")?;
2020 let client = redis::Client::open(url.as_str())
2021 .map_err(|e| format!("redis.psubscribe: {e}"))?;
2022 let mut conn = client.get_connection()
2023 .map_err(|e| format!("redis.psubscribe: {e}"))?;
2024 let mut pubsub = conn.as_pubsub();
2025 pubsub.psubscribe(&pattern)
2026 .map_err(|e| format!("redis.psubscribe: {e}"))?;
2027 loop {
2028 let msg = pubsub.get_message()
2029 .map_err(|e| format!("redis.psubscribe: {e}"))?;
2030 let pat: String = msg.get_pattern()
2031 .ok()
2032 .and_then(|v: Option<String>| v)
2033 .unwrap_or_else(|| pattern.clone());
2034 let ch: String = msg.get_channel_name().to_string();
2035 let payload: String = msg.get_payload()
2036 .map_err(|e| format!("redis.psubscribe: payload: {e}"))?;
2037 let handler = DefaultHandler::new(policy.clone())
2038 .with_program(Arc::clone(&program));
2039 let mut vm = Vm::with_handler(&program, Box::new(handler));
2040 vm.invoke_closure_value(closure.clone(), vec![
2041 Value::Str(pat.into()),
2042 Value::Str(ch.into()),
2043 Value::Str(payload.into()),
2044 ]).map_err(|e| format!("redis.psubscribe: handler: {e:?}"))?;
2045 }
2046 }
2047 ("redis", "lpush") => {
2048 let h = expect_redis_handle(args.first())?;
2049 let key = expect_str(args.get(1))?.to_string();
2050 let val = expect_str(args.get(2))?.to_string();
2051 let mut reg = redis_registry().lock().unwrap();
2052 let entry = reg.touch_get_mut(h)
2053 .ok_or_else(|| "redis.lpush: closed or unknown ConnRedis handle".to_string())?;
2054 use redis::Commands;
2055 let n: i64 = entry.conn.lpush(&key, &val)
2056 .map_err(|e| format!("redis.lpush: {e}"))?;
2057 Ok(Value::Int(n))
2058 }
2059 ("redis", "rpush") => {
2060 let h = expect_redis_handle(args.first())?;
2061 let key = expect_str(args.get(1))?.to_string();
2062 let val = expect_str(args.get(2))?.to_string();
2063 let mut reg = redis_registry().lock().unwrap();
2064 let entry = reg.touch_get_mut(h)
2065 .ok_or_else(|| "redis.rpush: closed or unknown ConnRedis handle".to_string())?;
2066 use redis::Commands;
2067 let n: i64 = entry.conn.rpush(&key, &val)
2068 .map_err(|e| format!("redis.rpush: {e}"))?;
2069 Ok(Value::Int(n))
2070 }
2071 ("redis", "brpop") => {
2072 let h = expect_redis_handle(args.first())?;
2075 let key = expect_str(args.get(1))?.to_string();
2076 let timeout = expect_int(args.get(2))?;
2077 let mut reg = redis_registry().lock().unwrap();
2078 let entry = reg.touch_get_mut(h)
2079 .ok_or_else(|| "redis.brpop: closed or unknown ConnRedis handle".to_string())?;
2080 use redis::Commands;
2081 let result: Option<(String, String)> = entry.conn
2084 .brpop(&key, timeout as f64)
2085 .map_err(|e| format!("redis.brpop: {e}"))?;
2086 match result {
2087 Some((_, v)) => Ok(some(Value::Str(v.into()))),
2088 None => Ok(none()),
2089 }
2090 }
2091 ("redis", "llen") => {
2092 let h = expect_redis_handle(args.first())?;
2093 let key = expect_str(args.get(1))?.to_string();
2094 let mut reg = redis_registry().lock().unwrap();
2095 let entry = reg.touch_get_mut(h)
2096 .ok_or_else(|| "redis.llen: closed or unknown ConnRedis handle".to_string())?;
2097 use redis::Commands;
2098 let n: i64 = entry.conn.llen(&key)
2099 .map_err(|e| format!("redis.llen: {e}"))?;
2100 Ok(Value::Int(n))
2101 }
2102 ("redis", "hset") => {
2103 let h = expect_redis_handle(args.first())?;
2104 let key = expect_str(args.get(1))?.to_string();
2105 let field = expect_str(args.get(2))?.to_string();
2106 let val = expect_str(args.get(3))?.to_string();
2107 let mut reg = redis_registry().lock().unwrap();
2108 let entry = reg.touch_get_mut(h)
2109 .ok_or_else(|| "redis.hset: closed or unknown ConnRedis handle".to_string())?;
2110 use redis::Commands;
2111 entry.conn.hset::<_, _, _, ()>(&key, &field, &val)
2112 .map_err(|e| format!("redis.hset: {e}"))?;
2113 Ok(Value::Unit)
2114 }
2115 ("redis", "hget") => {
2116 let h = expect_redis_handle(args.first())?;
2117 let key = expect_str(args.get(1))?.to_string();
2118 let field = expect_str(args.get(2))?.to_string();
2119 let mut reg = redis_registry().lock().unwrap();
2120 let entry = reg.touch_get_mut(h)
2121 .ok_or_else(|| "redis.hget: closed or unknown ConnRedis handle".to_string())?;
2122 use redis::Commands;
2123 match entry.conn.hget::<_, _, Option<String>>(&key, &field) {
2124 Ok(Some(v)) => Ok(some(Value::Str(v.into()))),
2125 Ok(None) => Ok(none()),
2126 Err(e) => Err(format!("redis.hget: {e}")),
2127 }
2128 }
2129 ("redis", "hdel") => {
2130 let h = expect_redis_handle(args.first())?;
2131 let key = expect_str(args.get(1))?.to_string();
2132 let field = expect_str(args.get(2))?.to_string();
2133 let mut reg = redis_registry().lock().unwrap();
2134 let entry = reg.touch_get_mut(h)
2135 .ok_or_else(|| "redis.hdel: closed or unknown ConnRedis handle".to_string())?;
2136 use redis::Commands;
2137 entry.conn.hdel::<_, _, ()>(&key, &field)
2138 .map_err(|e| format!("redis.hdel: {e}"))?;
2139 Ok(Value::Unit)
2140 }
2141 ("redis", "hgetall") => {
2142 let h = expect_redis_handle(args.first())?;
2143 let key = expect_str(args.get(1))?.to_string();
2144 let mut reg = redis_registry().lock().unwrap();
2145 let entry = reg.touch_get_mut(h)
2146 .ok_or_else(|| "redis.hgetall: closed or unknown ConnRedis handle".to_string())?;
2147 use redis::Commands;
2148 let map: std::collections::HashMap<String, String> = entry.conn
2149 .hgetall(&key)
2150 .map_err(|e| format!("redis.hgetall: {e}"))?;
2151 let pairs: Vec<Value> = map.into_iter()
2152 .map(|(k, v)| Value::Tuple(vec![Value::Str(k.into()), Value::Str(v.into())]))
2153 .collect();
2154 Ok(Value::List(pairs.into()))
2155 }
2156
2157 ("proc", "spawn") => {
2158 let cmd = expect_str(args.first())?.to_string();
2172 let raw_args = match args.get(1) {
2173 Some(Value::List(items)) => items,
2174 Some(other) => return Err(format!(
2175 "proc.spawn: args must be List[Str], got {other:?}")),
2176 None => return Err("proc.spawn: missing args list".into()),
2177 };
2178 let str_args: Vec<String> = raw_args.iter().map(|v| match v {
2179 Value::Str(s) => Ok(s.to_string()),
2180 other => Err(format!("proc.spawn: arg must be Str, got {other:?}")),
2181 }).collect::<Result<Vec<_>, _>>()?;
2182
2183 if !self.policy.allow_proc.is_empty() {
2187 let basename = std::path::Path::new(&cmd)
2188 .file_name()
2189 .and_then(|s| s.to_str())
2190 .unwrap_or(&cmd);
2191 if !self.policy.allow_proc.iter().any(|a| a == basename) {
2192 return Ok(err(Value::Str(format!(
2193 "proc.spawn: `{cmd}` not in --allow-proc {:?}",
2194 self.policy.allow_proc
2195 ).into())));
2196 }
2197 }
2198
2199 if str_args.len() > 1024 {
2202 return Ok(err(Value::Str(
2203 SmolStr::new_inline("proc.spawn: arg-count exceeds 1024"))));
2204 }
2205 if str_args.iter().any(|a| a.len() > 65_536) {
2206 return Ok(err(Value::Str(
2207 "proc.spawn: per-arg length exceeds 64 KiB".into())));
2208 }
2209
2210 let output = std::process::Command::new(&cmd)
2211 .args(&str_args)
2212 .output();
2213 match output {
2214 Ok(o) => {
2215 let mut rec = indexmap::IndexMap::new();
2216 rec.insert("stdout".into(), Value::Str(
2217 String::from_utf8_lossy(&o.stdout).into_owned().into()));
2218 rec.insert("stderr".into(), Value::Str(
2219 String::from_utf8_lossy(&o.stderr).into_owned().into()));
2220 rec.insert("exit_code".into(), Value::Int(
2221 o.status.code().unwrap_or(-1) as i64));
2222 Ok(ok(Value::record_dynamic(rec)))
2223 }
2224 Err(e) => Ok(err(Value::Str(format!("spawn `{cmd}`: {e}").into()))),
2225 }
2226 }
2227 other => Err(format!("unsupported effect {}.{}", other.0, other.1)),
2228 }
2229 }
2230
2231 fn spawn_for_worker(&self) -> Option<Box<dyn lex_bytecode::vm::EffectHandler + Send>> {
2254 let mut fresh = DefaultHandler::new(self.policy.clone());
2255 fresh.budget_remaining = std::sync::Arc::clone(&self.budget_remaining);
2258 fresh.budget_ceiling = self.budget_ceiling;
2259 fresh.read_root = self.read_root.clone();
2260 fresh.program = self.program.clone();
2261 fresh.chat_registry = self.chat_registry.clone();
2262 fresh.streams = std::sync::Arc::clone(&self.streams);
2267 fresh.next_stream_id = std::sync::Arc::clone(&self.next_stream_id);
2268 fresh.program_args = self.program_args.clone();
2269 Some(Box::new(fresh))
2270 }
2271}
2272
2273pub struct TlsConfig {
2283 pub cert: Vec<u8>,
2284 pub key: Vec<u8>,
2285}
2286
2287fn serve_http(
2288 port: u16,
2289 handler_name: String,
2290 program: Arc<Program>,
2291 policy: Policy,
2292 tls: Option<TlsConfig>,
2293 opts: ServeOpts,
2294) -> Result<Value, String> {
2295 match tls {
2296 None => serve_http_plain(port, handler_name, program, policy, opts),
2297 Some(cfg) => serve_http_tls_legacy(port, handler_name, program, policy, cfg),
2298 }
2299}
2300
2301fn serve_http_plain(
2311 port: u16,
2312 handler_name: String,
2313 program: Arc<Program>,
2314 policy: Policy,
2315 opts: ServeOpts,
2316) -> Result<Value, String> {
2317 use http_body_util::BodyExt as _;
2318 use hyper::server::conn::http1;
2319 use hyper::service::service_fn;
2320 use hyper_util::rt::{TokioExecutor, TokioIo};
2321 use hyper_util::server::conn::auto;
2322 use tokio::net::TcpListener as TokioTcpListener;
2323
2324 let inline_vm = opts.inline_vm;
2325 let http2 = opts.http2;
2326 let host = opts.host.clone();
2327 let rt = tokio::runtime::Builder::new_multi_thread()
2328 .enable_all()
2329 .build()
2330 .map_err(|e| format!("net.serve: tokio runtime: {e}"))?;
2331 rt.block_on(async move {
2332 let listener = TokioTcpListener::bind((host.as_str(), port))
2333 .await
2334 .map_err(|e| format!("net.serve bind {host}:{port}: {e}"))?;
2335 eprintln!(
2336 "net.serve: listening on http://{host}:{port}{}{}",
2337 if inline_vm { " (inline-vm)" } else { "" },
2338 if http2 { " (http1+http2)" } else { "" }
2339 );
2340 loop {
2341 let (stream, _) = listener
2342 .accept()
2343 .await
2344 .map_err(|e| format!("net.serve accept: {e}"))?;
2345 let io = TokioIo::new(stream);
2346 let program = Arc::clone(&program);
2347 let policy = policy.clone();
2348 let handler_name = handler_name.clone();
2349 tokio::spawn(async move {
2350 let program2 = Arc::clone(&program);
2351 let policy2 = policy.clone();
2352 let handler_name2 = handler_name.clone();
2353 let svc = service_fn(move |req: hyper::Request<hyper::body::Incoming>| {
2354 let program = Arc::clone(&program2);
2355 let policy = policy2.clone();
2356 let handler_name = handler_name2.clone();
2357 async move {
2358 let (parts, body) = req.into_parts();
2359 let body_bytes = body
2360 .collect()
2361 .await
2362 .map(|c| c.to_bytes())
2363 .unwrap_or_default();
2364 let result = if inline_vm {
2365 let lex_req = build_request_value_parts(&parts, &body_bytes);
2369 let handler = DefaultHandler::new(policy)
2370 .with_program(Arc::clone(&program));
2371 let mut vm = Vm::with_handler(&program, Box::new(handler));
2372 let r = vm.call(&handler_name, vec![lex_req]);
2373 Ok(r.map(|v| unpack_response(&mut vm, &v)))
2376 } else {
2377 tokio::task::spawn_blocking(move || {
2378 let lex_req = build_request_value_parts(&parts, &body_bytes);
2379 let handler = DefaultHandler::new(policy)
2380 .with_program(Arc::clone(&program));
2381 let mut vm = Vm::with_handler(&program, Box::new(handler));
2382 let r = vm.call(&handler_name, vec![lex_req]);
2383 r.map(|v| unpack_response(&mut vm, &v))
2384 })
2385 .await
2386 };
2387 Ok::<_, std::convert::Infallible>(match result {
2388 Ok(Ok(unpacked)) => build_hyper_response(unpacked),
2389 Ok(Err(e)) => error_response(500, &format!("internal error: {e}")),
2390 Err(e) => error_response(500, &format!("task panicked: {e}")),
2391 })
2392 }
2393 });
2394 let result = if http2 {
2395 auto::Builder::new(TokioExecutor::new())
2396 .serve_connection(io, svc)
2397 .await
2398 .map_err(|e| e.to_string())
2399 } else {
2400 http1::Builder::new()
2401 .serve_connection(io, svc)
2402 .await
2403 .map_err(|e| e.to_string())
2404 };
2405 if let Err(e) = result {
2406 eprintln!("net.serve: connection error: {e}");
2407 }
2408 });
2409 }
2410 })
2411}
2412
2413fn serve_http_tls_legacy(
2415 port: u16,
2416 handler_name: String,
2417 program: Arc<Program>,
2418 policy: Policy,
2419 cfg: TlsConfig,
2420) -> Result<Value, String> {
2421 let ssl = tiny_http::SslConfig {
2422 certificate: cfg.cert,
2423 private_key: cfg.key,
2424 };
2425 let server = tiny_http::Server::https(("0.0.0.0", port), ssl)
2426 .map_err(|e| format!("net.serve_tls bind {port}: {e}"))?;
2427 eprintln!("net.serve: listening on https://0.0.0.0:{port}");
2428 for req in server.incoming_requests() {
2429 let program = Arc::clone(&program);
2430 let policy = policy.clone();
2431 let handler_name = handler_name.clone();
2432 std::thread::spawn(move || handle_request_tls(req, program, policy, handler_name));
2433 }
2434 Ok(Value::Unit)
2435}
2436
2437fn handle_request_tls(
2438 mut req: tiny_http::Request,
2439 program: Arc<Program>,
2440 policy: Policy,
2441 handler_name: String,
2442) {
2443 let lex_req = build_request_value_tiny(&mut req);
2444 let handler = DefaultHandler::new(policy).with_program(Arc::clone(&program));
2445 let mut vm = Vm::with_handler(&program, Box::new(handler));
2446 match vm.call(&handler_name, vec![lex_req]) {
2447 Ok(resp) => {
2448 let (status, body, headers) = unpack_response(&mut vm, &resp);
2454 respond_with_body_tls(req, status, body, headers);
2455 }
2456 Err(e) => {
2457 let response = tiny_http::Response::from_string(format!("internal error: {e}"))
2458 .with_status_code(500);
2459 let _ = req.respond(response);
2460 }
2461 }
2462}
2463
2464fn serve_http_fn(
2469 port: u16,
2470 closure: Value,
2471 program: Arc<Program>,
2472 policy: Policy,
2473 opts: ServeOpts,
2474) -> Result<Value, String> {
2475 use http_body_util::BodyExt as _;
2476 use hyper::server::conn::http1;
2477 use hyper::service::service_fn;
2478 use hyper_util::rt::{TokioExecutor, TokioIo};
2479 use hyper_util::server::conn::auto;
2480 use tokio::net::TcpListener as TokioTcpListener;
2481
2482 let inline_vm = opts.inline_vm;
2483 let http2 = opts.http2;
2484 let host = opts.host.clone();
2485 let rt = tokio::runtime::Builder::new_multi_thread()
2486 .enable_all()
2487 .build()
2488 .map_err(|e| format!("net.serve_fn: tokio runtime: {e}"))?;
2489 rt.block_on(async move {
2490 let listener = TokioTcpListener::bind((host.as_str(), port))
2491 .await
2492 .map_err(|e| format!("net.serve_fn bind {host}:{port}: {e}"))?;
2493 eprintln!(
2494 "net.serve_fn: listening on http://{host}:{port}{}{}",
2495 if inline_vm { " (inline-vm)" } else { "" },
2496 if http2 { " (http1+http2)" } else { "" }
2497 );
2498 loop {
2499 let (stream, _) = listener
2500 .accept()
2501 .await
2502 .map_err(|e| format!("net.serve_fn accept: {e}"))?;
2503 let io = TokioIo::new(stream);
2504 let program = Arc::clone(&program);
2505 let policy = policy.clone();
2506 let closure = closure.clone();
2507 tokio::spawn(async move {
2508 let program2 = Arc::clone(&program);
2509 let policy2 = policy.clone();
2510 let closure2 = closure.clone();
2511 let svc = service_fn(move |req: hyper::Request<hyper::body::Incoming>| {
2512 let program = Arc::clone(&program2);
2513 let policy = policy2.clone();
2514 let closure = closure2.clone();
2515 async move {
2516 let (parts, body) = req.into_parts();
2517 let body_bytes = body
2518 .collect()
2519 .await
2520 .map(|c| c.to_bytes())
2521 .unwrap_or_default();
2522 let result = if inline_vm {
2523 let lex_req = build_request_value_parts(&parts, &body_bytes);
2524 let handler = DefaultHandler::new(policy)
2525 .with_program(Arc::clone(&program));
2526 let mut vm = Vm::with_handler(&program, Box::new(handler));
2527 let scope = vm.enter_request_scope();
2534 let r = vm.invoke_closure_value(closure, vec![lex_req]);
2535 let r = r.map(|v| unpack_response(&mut vm, &v));
2539 vm.exit_request_scope(scope);
2540 Ok(r)
2541 } else {
2542 tokio::task::spawn_blocking(move || {
2543 let lex_req = build_request_value_parts(&parts, &body_bytes);
2544 let handler = DefaultHandler::new(policy)
2545 .with_program(Arc::clone(&program));
2546 let mut vm = Vm::with_handler(&program, Box::new(handler));
2547 let scope = vm.enter_request_scope();
2548 let r = vm.invoke_closure_value(closure, vec![lex_req]);
2549 let r = r.map(|v| unpack_response(&mut vm, &v));
2550 vm.exit_request_scope(scope);
2551 r
2552 })
2553 .await
2554 };
2555 Ok::<_, std::convert::Infallible>(match result {
2556 Ok(Ok(unpacked)) => build_hyper_response(unpacked),
2557 Ok(Err(e)) => error_response(500, &format!("internal error: {e}")),
2558 Err(e) => error_response(500, &format!("task panicked: {e}")),
2559 })
2560 }
2561 });
2562 let result = if http2 {
2563 auto::Builder::new(TokioExecutor::new())
2564 .serve_connection(io, svc)
2565 .await
2566 .map_err(|e| e.to_string())
2567 } else {
2568 http1::Builder::new()
2569 .serve_connection(io, svc)
2570 .await
2571 .map_err(|e| e.to_string())
2572 };
2573 if let Err(e) = result {
2574 eprintln!("net.serve_fn: connection error: {e}");
2575 }
2576 });
2577 }
2578 })
2579}
2580
2581#[derive(Clone, Debug)]
2585pub(crate) enum RouteSeg {
2586 Literal(String),
2587 Param(String),
2590}
2591
2592fn compile_path_pattern(pat: &str) -> Result<Vec<RouteSeg>, String> {
2596 if pat.is_empty() {
2597 return Err("path pattern must be non-empty (use \"/\" for the root)".into());
2598 }
2599 if !pat.starts_with('/') {
2600 return Err(format!("path pattern must start with '/' (got {pat:?})"));
2601 }
2602 let mut segs = Vec::new();
2603 for raw in pat.split('/') {
2604 if let Some(name) = raw.strip_prefix(':') {
2605 if name.is_empty() {
2606 return Err(format!(
2607 ":-segment in pattern {pat:?} must have a name (e.g. :id)"
2608 ));
2609 }
2610 segs.push(RouteSeg::Param(name.to_string()));
2611 } else {
2612 segs.push(RouteSeg::Literal(raw.to_string()));
2613 }
2614 }
2615 Ok(segs)
2616}
2617
2618fn match_path_pattern(
2624 segs: &[RouteSeg],
2625 path: &str,
2626) -> Option<std::collections::BTreeMap<lex_bytecode::MapKey, Value>> {
2627 let path_segs: Vec<&str> = path.split('/').collect();
2628 if path_segs.len() != segs.len() {
2629 return None;
2630 }
2631 let mut params = std::collections::BTreeMap::new();
2632 for (pat, p) in segs.iter().zip(path_segs.iter()) {
2633 match pat {
2634 RouteSeg::Literal(lit) => {
2635 if lit != p {
2636 return None;
2637 }
2638 }
2639 RouteSeg::Param(name) => {
2640 params.insert(
2641 lex_bytecode::MapKey::Str(name.clone()),
2642 Value::Str((*p).into()),
2643 );
2644 }
2645 }
2646 }
2647 Some(params)
2648}
2649
2650fn decode_routes_arg(
2655 v: Value,
2656) -> Result<Vec<(String, Vec<RouteSeg>, Value)>, String> {
2657 let list = match v {
2658 Value::List(xs) => xs,
2659 _ => return Err("net.serve_routed: routes must be a List".into()),
2660 };
2661 let mut out = Vec::with_capacity(list.len());
2662 for (i, item) in list.into_iter().enumerate() {
2663 let tup = match item {
2664 Value::Tuple(xs) if xs.len() == 3 => xs,
2665 other => return Err(format!(
2666 "net.serve_routed: route #{i} must be a (method, pattern, handler) 3-tuple, got {other:?}"
2667 )),
2668 };
2669 let mut it = tup.into_iter();
2670 let method_raw = match it.next() {
2671 Some(Value::Str(s)) => s.to_string(),
2672 _ => return Err(format!("net.serve_routed: route #{i} method must be Str")),
2673 };
2674 let method = if method_raw == "*" { method_raw } else { method_raw.to_uppercase() };
2676 let pattern = match it.next() {
2677 Some(Value::Str(s)) => s.to_string(),
2678 _ => return Err(format!("net.serve_routed: route #{i} path-pattern must be Str")),
2679 };
2680 let segs = compile_path_pattern(&pattern)
2681 .map_err(|e| format!("net.serve_routed: route #{i} ({pattern:?}): {e}"))?;
2682 let closure = match it.next() {
2683 Some(c @ Value::Closure { .. }) => c,
2684 _ => return Err(format!("net.serve_routed: route #{i} handler must be a closure")),
2685 };
2686 out.push((method, segs, closure));
2687 }
2688 Ok(out)
2689}
2690
2691pub(crate) fn dispatch_route<'a>(
2696 routes: &'a [(String, Vec<RouteSeg>, Value)],
2697 req_method: &str,
2698 req_path: &str,
2699) -> Option<(&'a Value, std::collections::BTreeMap<lex_bytecode::MapKey, Value>)> {
2700 let req_method_upper = req_method.to_ascii_uppercase();
2701 for (m, segs, closure) in routes {
2702 if m != "*" && m != &req_method_upper {
2703 continue;
2704 }
2705 if let Some(params) = match_path_pattern(segs, req_path) {
2706 return Some((closure, params));
2707 }
2708 }
2709 None
2710}
2711
2712pub(crate) fn stamp_path_params(
2716 req: &mut Value,
2717 params: std::collections::BTreeMap<lex_bytecode::MapKey, Value>,
2718) {
2719 if let Value::Record { fields: rec, .. } = req {
2720 rec.insert("path_params".into(), Value::Map(params));
2721 }
2722}
2723
2724fn serve_http_routed(
2730 port: u16,
2731 routes: Vec<(String, Vec<RouteSeg>, Value)>,
2732 fallback: Value,
2733 program: Arc<Program>,
2734 policy: Policy,
2735 opts: ServeOpts,
2736) -> Result<Value, String> {
2737 use http_body_util::BodyExt as _;
2738 use hyper::server::conn::http1;
2739 use hyper::service::service_fn;
2740 use hyper_util::rt::{TokioExecutor, TokioIo};
2741 use hyper_util::server::conn::auto;
2742 use tokio::net::TcpListener as TokioTcpListener;
2743
2744 let inline_vm = opts.inline_vm;
2745 let http2 = opts.http2;
2746 let host = opts.host.clone();
2747 let routes = Arc::new(routes);
2748 let rt = tokio::runtime::Builder::new_multi_thread()
2749 .enable_all()
2750 .build()
2751 .map_err(|e| format!("net.serve_routed: tokio runtime: {e}"))?;
2752 rt.block_on(async move {
2753 let listener = TokioTcpListener::bind((host.as_str(), port))
2754 .await
2755 .map_err(|e| format!("net.serve_routed bind {host}:{port}: {e}"))?;
2756 eprintln!(
2757 "net.serve_routed: listening on http://{host}:{port} ({} routes{}{})",
2758 routes.len(),
2759 if inline_vm { ", inline-vm" } else { "" },
2760 if http2 { ", http1+http2" } else { "" }
2761 );
2762 loop {
2763 let (stream, _) = listener
2764 .accept()
2765 .await
2766 .map_err(|e| format!("net.serve_routed accept: {e}"))?;
2767 let io = TokioIo::new(stream);
2768 let program = Arc::clone(&program);
2769 let policy = policy.clone();
2770 let routes = Arc::clone(&routes);
2771 let fallback = fallback.clone();
2772 tokio::spawn(async move {
2773 let program2 = Arc::clone(&program);
2774 let policy2 = policy.clone();
2775 let routes2 = Arc::clone(&routes);
2776 let fallback2 = fallback.clone();
2777 let svc = service_fn(move |req: hyper::Request<hyper::body::Incoming>| {
2778 let program = Arc::clone(&program2);
2779 let policy = policy2.clone();
2780 let routes = Arc::clone(&routes2);
2781 let fallback = fallback2.clone();
2782 async move {
2783 let (parts, body) = req.into_parts();
2784 let body_bytes = body
2785 .collect()
2786 .await
2787 .map(|c| c.to_bytes())
2788 .unwrap_or_default();
2789 let method = parts.method.as_str().to_string();
2790 let path = match parts.uri.path() {
2791 "" => "/".to_string(),
2792 p => p.to_string(),
2793 };
2794 let result = if inline_vm {
2795 let mut lex_req = build_request_value_parts(&parts, &body_bytes);
2796 let (closure, params) = match dispatch_route(&routes, &method, &path) {
2797 Some((c, p)) => (c.clone(), p),
2798 None => (fallback.clone(), std::collections::BTreeMap::new()),
2799 };
2800 stamp_path_params(&mut lex_req, params);
2801 let handler = DefaultHandler::new(policy)
2802 .with_program(Arc::clone(&program));
2803 let mut vm = Vm::with_handler(&program, Box::new(handler));
2804 let r = vm.invoke_closure_value(closure, vec![lex_req]);
2805 Ok(r.map(|v| unpack_response(&mut vm, &v)))
2808 } else {
2809 tokio::task::spawn_blocking(move || {
2810 let mut lex_req = build_request_value_parts(&parts, &body_bytes);
2811 let (closure, params) = match dispatch_route(&routes, &method, &path) {
2812 Some((c, p)) => (c.clone(), p),
2813 None => (fallback.clone(), std::collections::BTreeMap::new()),
2814 };
2815 stamp_path_params(&mut lex_req, params);
2816 let handler = DefaultHandler::new(policy)
2817 .with_program(Arc::clone(&program));
2818 let mut vm = Vm::with_handler(&program, Box::new(handler));
2819 let r = vm.invoke_closure_value(closure, vec![lex_req]);
2820 r.map(|v| unpack_response(&mut vm, &v))
2821 })
2822 .await
2823 };
2824 Ok::<_, std::convert::Infallible>(match result {
2825 Ok(Ok(unpacked)) => build_hyper_response(unpacked),
2826 Ok(Err(e)) => error_response(500, &format!("internal error: {e}")),
2827 Err(e) => error_response(500, &format!("task panicked: {e}")),
2828 })
2829 }
2830 });
2831 let result = if http2 {
2832 auto::Builder::new(TokioExecutor::new())
2833 .serve_connection(io, svc)
2834 .await
2835 .map_err(|e| e.to_string())
2836 } else {
2837 http1::Builder::new()
2838 .serve_connection(io, svc)
2839 .await
2840 .map_err(|e| e.to_string())
2841 };
2842 if let Err(e) = result {
2843 eprintln!("net.serve_routed: connection error: {e}");
2844 }
2845 });
2846 }
2847 })
2848}
2849
2850fn env_inline_vm() -> bool {
2855 match std::env::var("LEX_NET_INLINE_VM") {
2856 Ok(v) => {
2857 let s = v.trim().to_ascii_lowercase();
2858 s == "1" || s == "true"
2859 }
2860 Err(_) => false,
2861 }
2862}
2863
2864#[derive(Debug, Clone)]
2870pub(crate) struct ServeOpts {
2871 pub(crate) http2: bool,
2872 pub(crate) inline_vm: bool,
2873 pub(crate) host: String,
2874}
2875
2876impl ServeOpts {
2877 fn from_env() -> Self {
2881 Self {
2882 http2: env_http2(),
2883 inline_vm: env_inline_vm(),
2884 host: "0.0.0.0".to_string(),
2885 }
2886 }
2887
2888 fn lex_defaults() -> Self {
2893 Self {
2894 http2: false,
2895 inline_vm: false,
2896 host: "0.0.0.0".to_string(),
2897 }
2898 }
2899
2900 fn to_value(&self) -> Value {
2902 let mut rec = indexmap::IndexMap::new();
2903 rec.insert("http2".to_string(), Value::Bool(self.http2));
2904 rec.insert("inline_vm".to_string(), Value::Bool(self.inline_vm));
2905 rec.insert("host".to_string(), Value::Str(self.host.clone().into()));
2906 Value::record_dynamic(rec)
2907 }
2908}
2909
2910fn decode_serve_opts(v: &Value) -> Result<ServeOpts, String> {
2915 let rec = match v {
2916 Value::Record { fields: r, .. } => r,
2917 other => return Err(format!("opts must be a Record, got {other:?}")),
2918 };
2919 let http2 = match rec.get("http2") {
2920 Some(Value::Bool(b)) => *b,
2921 _ => return Err("opts.http2 must be Bool".into()),
2922 };
2923 let inline_vm = match rec.get("inline_vm") {
2924 Some(Value::Bool(b)) => *b,
2925 _ => return Err("opts.inline_vm must be Bool".into()),
2926 };
2927 let host = match rec.get("host") {
2928 Some(Value::Str(s)) => s.to_string(),
2929 _ => return Err("opts.host must be Str".into()),
2930 };
2931 Ok(ServeOpts { http2, inline_vm, host })
2932}
2933
2934fn make_tls_config_value(cert_pem: Vec<u8>, key_pem: Vec<u8>) -> Value {
2944 let mut rec = indexmap::IndexMap::new();
2945 rec.insert("cert".into(), Value::Bytes(cert_pem));
2946 rec.insert("key".into(), Value::Bytes(key_pem));
2947 Value::record_dynamic(rec)
2948}
2949
2950#[cfg(feature = "quic")]
2951fn decode_tls_config(v: &Value) -> Result<crate::quic::QuicTls, String> {
2952 let rec = match v {
2953 Value::Record { fields: r, .. } => r,
2954 other => return Err(format!("TlsConfig: expected Record, got {other:?}")),
2955 };
2956 let cert = match rec.get("cert") {
2957 Some(Value::Bytes(b)) => b.to_vec(),
2958 _ => return Err("TlsConfig.cert: must be Bytes".into()),
2959 };
2960 let key = match rec.get("key") {
2961 Some(Value::Bytes(b)) => b.to_vec(),
2962 _ => return Err("TlsConfig.key: must be Bytes".into()),
2963 };
2964 Ok(crate::quic::QuicTls { cert_pem: cert, key_pem: key })
2965}
2966
2967fn dispatch_tls_from_pem_files(
2968 handler: &DefaultHandler,
2969 args: Vec<Value>,
2970) -> Result<Value, String> {
2971 let cert_path = expect_str(args.first())?.to_string();
2972 let key_path = expect_str(args.get(1))?.to_string();
2973 let cert_resolved = handler.resolve_read_path(&cert_path);
2974 let key_resolved = handler.resolve_read_path(&key_path);
2975 if !handler.policy.allow_fs_read.is_empty() {
2976 let allowed = |p: &std::path::Path| -> bool {
2977 handler.policy.allow_fs_read.iter().any(|a| p.starts_with(a))
2978 };
2979 if !allowed(&cert_resolved) {
2980 return Ok(err(Value::Str(
2981 format!("tls.from_pem_files: cert `{cert_path}` outside --allow-fs-read").into(),
2982 )));
2983 }
2984 if !allowed(&key_resolved) {
2985 return Ok(err(Value::Str(
2986 format!("tls.from_pem_files: key `{key_path}` outside --allow-fs-read").into(),
2987 )));
2988 }
2989 }
2990 let cert = match std::fs::read(&cert_resolved) {
2991 Ok(b) => b,
2992 Err(e) => return Ok(err(Value::Str(format!("read cert {cert_path}: {e}").into()))),
2993 };
2994 let key = match std::fs::read(&key_resolved) {
2995 Ok(b) => b,
2996 Err(e) => return Ok(err(Value::Str(format!("read key {key_path}: {e}").into()))),
2997 };
2998 Ok(ok(make_tls_config_value(cert, key)))
2999}
3000
3001#[cfg(feature = "quic")]
3002fn dispatch_tls_self_signed(args: Vec<Value>) -> Result<Value, String> {
3003 let hostname = expect_str(args.first())?.to_string();
3004 match crate::quic::self_signed_pem(&hostname) {
3005 Ok((cert, key)) => Ok(ok(make_tls_config_value(cert, key))),
3006 Err(e) => Ok(err(Value::Str(format!("tls.self_signed: {e}").into()))),
3007 }
3008}
3009
3010#[cfg(not(feature = "quic"))]
3011fn dispatch_tls_self_signed(_args: Vec<Value>) -> Result<Value, String> {
3012 Ok(err(Value::Str(
3013 "tls.self_signed: lex-runtime was compiled without the `quic` feature (needed for rcgen)".into(),
3014 )))
3015}
3016
3017impl DefaultHandler {
3018 #[cfg(feature = "quic")]
3019 fn dispatch_serve_quic_named(&self, args: Vec<Value>) -> Result<Value, String> {
3020 let port = match args.first() {
3021 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
3022 _ => return Err("net.serve_quic(port, tls, handler): port must be Int 0..=65535".into()),
3023 };
3024 let tls = decode_tls_config(args.get(1)
3025 .ok_or_else(|| "net.serve_quic(port, tls, handler): missing tls".to_string())?)?;
3026 let handler_name = expect_str(args.get(2))?.to_string();
3027 let program = self.program.clone()
3028 .ok_or_else(|| "net.serve_quic requires a Program reference; use DefaultHandler::with_program".to_string())?;
3029 let policy = self.policy.clone();
3030 crate::quic::serve_http3_named(port, handler_name, tls, program, policy, ServeOpts::from_env())
3031 }
3032
3033 #[cfg(feature = "quic")]
3034 fn dispatch_serve_quic_fn(&self, args: Vec<Value>) -> Result<Value, String> {
3035 let port = match args.first() {
3036 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
3037 _ => return Err("net.serve_quic_fn(port, tls, handler): port must be Int 0..=65535".into()),
3038 };
3039 let tls = decode_tls_config(args.get(1)
3040 .ok_or_else(|| "net.serve_quic_fn(port, tls, handler): missing tls".to_string())?)?;
3041 let closure = match args.into_iter().nth(2) {
3042 Some(c @ Value::Closure { .. }) => c,
3043 _ => return Err("net.serve_quic_fn(port, tls, handler): handler must be a closure".into()),
3044 };
3045 let program = self.program.clone()
3046 .ok_or_else(|| "net.serve_quic_fn requires a Program reference; use DefaultHandler::with_program".to_string())?;
3047 let policy = self.policy.clone();
3048 crate::quic::serve_http3_fn(port, closure, tls, program, policy, ServeOpts::from_env())
3049 }
3050
3051 #[cfg(feature = "quic")]
3052 fn dispatch_serve_quic_routed(&self, args: Vec<Value>) -> Result<Value, String> {
3053 let port = match args.first() {
3054 Some(Value::Int(n)) if (0..=65535).contains(n) => *n as u16,
3055 _ => return Err("net.serve_quic_routed(port, tls, routes, fallback): port must be Int 0..=65535".into()),
3056 };
3057 let tls = decode_tls_config(args.get(1)
3058 .ok_or_else(|| "net.serve_quic_routed(port, tls, routes, fallback): missing tls".to_string())?)?;
3059 let routes_val = args.get(2).cloned()
3060 .ok_or_else(|| "net.serve_quic_routed(port, tls, routes, fallback): missing routes".to_string())?;
3061 let fallback = match args.into_iter().nth(3) {
3062 Some(c @ Value::Closure { .. }) => c,
3063 _ => return Err("net.serve_quic_routed(port, tls, routes, fallback): fallback must be a closure".into()),
3064 };
3065 let routes = decode_routes_arg(routes_val)?;
3066 let program = self.program.clone()
3067 .ok_or_else(|| "net.serve_quic_routed requires a Program reference; use DefaultHandler::with_program".to_string())?;
3068 let policy = self.policy.clone();
3069 crate::quic::serve_http3_routed(port, routes, fallback, tls, program, policy, ServeOpts::from_env())
3070 }
3071
3072 #[cfg(not(feature = "quic"))]
3073 fn dispatch_serve_quic_named(&self, _args: Vec<Value>) -> Result<Value, String> {
3074 Err("net.serve_quic: lex-runtime was compiled without the `quic` feature (needed for quinn + h3)".into())
3075 }
3076 #[cfg(not(feature = "quic"))]
3077 fn dispatch_serve_quic_fn(&self, _args: Vec<Value>) -> Result<Value, String> {
3078 Err("net.serve_quic_fn: lex-runtime was compiled without the `quic` feature (needed for quinn + h3)".into())
3079 }
3080 #[cfg(not(feature = "quic"))]
3081 fn dispatch_serve_quic_routed(&self, _args: Vec<Value>) -> Result<Value, String> {
3082 Err("net.serve_quic_routed: lex-runtime was compiled without the `quic` feature (needed for quinn + h3)".into())
3083 }
3084}
3085
3086fn env_http2() -> bool {
3096 match std::env::var("LEX_NET_HTTP2") {
3097 Ok(v) => {
3098 let s = v.trim().to_ascii_lowercase();
3099 s == "1" || s == "true"
3100 }
3101 Err(_) => false,
3102 }
3103}
3104
3105pub(crate) fn build_request_value_parts(
3107 parts: &hyper::http::request::Parts,
3108 body: &bytes::Bytes,
3109) -> Value {
3110 let method = parts.method.as_str().to_string();
3111 let path = parts.uri.path().to_string();
3119 let query = parts.uri.query().map(str::to_string).unwrap_or_default();
3120 let mut headers_map = std::collections::BTreeMap::new();
3121 for (name, val) in &parts.headers {
3122 if let Ok(v) = val.to_str() {
3123 headers_map.insert(
3124 lex_bytecode::MapKey::Str(name.as_str().to_ascii_lowercase()),
3125 Value::Str(v.to_string().into()),
3126 );
3127 }
3128 }
3129 let body_str = String::from_utf8_lossy(body).into_owned();
3130 let mut rec = indexmap::IndexMap::new();
3131 rec.insert("method".into(), Value::Str(method.into()));
3132 rec.insert("path".into(), Value::Str(path.into()));
3133 rec.insert("query".into(), Value::Str(query.into()));
3134 rec.insert("body".into(), Value::Str(body_str.into()));
3135 rec.insert("headers".into(), Value::Map(headers_map));
3136 rec.insert("path_params".into(), Value::Map(std::collections::BTreeMap::new()));
3137 Value::record_dynamic(rec)
3138}
3139
3140fn build_request_value_tiny(req: &mut tiny_http::Request) -> Value {
3142 let method = format!("{:?}", req.method()).to_uppercase();
3143 let url = req.url().to_string();
3144 let (path, query) = match url.split_once('?') {
3145 Some((p, q)) => (p.to_string(), q.to_string()),
3146 None => (url, String::new()),
3147 };
3148 let mut headers_map = std::collections::BTreeMap::new();
3149 for h in req.headers() {
3150 headers_map.insert(
3151 lex_bytecode::MapKey::Str(h.field.as_str().as_str().to_ascii_lowercase()),
3152 Value::Str(h.value.as_str().to_string().into()),
3153 );
3154 }
3155 let mut body = String::new();
3156 let _ = req.as_reader().read_to_string(&mut body);
3157 let mut rec = indexmap::IndexMap::new();
3158 rec.insert("method".into(), Value::Str(method.into()));
3159 rec.insert("path".into(), Value::Str(path.into()));
3160 rec.insert("query".into(), Value::Str(query.into()));
3161 rec.insert("body".into(), Value::Str(body.into()));
3162 rec.insert("headers".into(), Value::Map(headers_map));
3163 rec.insert("path_params".into(), Value::Map(std::collections::BTreeMap::new()));
3164 Value::record_dynamic(rec)
3165}
3166
3167pub(crate) fn unpack_response(vm: &mut Vm, v: &Value) -> UnpackedResponse {
3168 if !matches!(v, Value::Record { .. } | Value::ArenaRecord { .. }) {
3174 return (
3175 500,
3176 ResponseBodyOut::Str(format!("handler returned non-record: {v:?}")),
3177 vec![],
3178 );
3179 }
3180
3181 let status = vm.get_record_field(v, "status").and_then(|s| match s {
3182 Value::Int(n) => Some(n as u16),
3183 _ => None,
3184 }).unwrap_or(200);
3185
3186 let body = match vm.get_record_field(v, "body") {
3190 Some(Value::Variant { name, mut args }) if args.len() == 1 => {
3191 let inner = args.pop().unwrap();
3192 match (name.as_str(), inner) {
3193 ("BodyStr", Value::Str(s)) => ResponseBodyOut::Str(s.to_string()),
3195 ("BodyStream", iter_v) => {
3196 let drained = materialize_lazy_iter(vm, iter_v);
3197 ResponseBodyOut::TextChunks(drain_iter_str(&drained))
3198 }
3199 ("BodyBytes", iter_v) => {
3200 let drained = materialize_lazy_iter(vm, iter_v);
3201 ResponseBodyOut::BytesChunks(drain_iter_bytes(&drained))
3202 }
3203 _ => ResponseBodyOut::Str(String::new()),
3204 }
3205 }
3206 Some(Value::Str(s)) => ResponseBodyOut::Str(s.to_string()),
3212 _ => ResponseBodyOut::Str(String::new()),
3213 };
3214
3215 let headers: Vec<(String, String)> = match vm.get_record_field(v, "headers") {
3216 Some(Value::Map(hmap)) => hmap.iter().filter_map(|(k, val)| {
3217 if let (lex_bytecode::MapKey::Str(name), Value::Str(s)) = (k, val) {
3218 Some((name.clone(), s.to_string()))
3219 } else {
3220 None
3221 }
3222 }).collect(),
3223 _ => vec![],
3224 };
3225
3226 (status, body, headers)
3227}
3228
3229type HyperRespBody =
3230 http_body_util::combinators::BoxBody<bytes::Bytes, std::convert::Infallible>;
3231
3232fn build_hyper_response(
3242 (status, body, headers): UnpackedResponse,
3243) -> hyper::Response<HyperRespBody> {
3244 use http_body_util::BodyExt as _;
3245 let boxed_body: HyperRespBody = match body {
3246 ResponseBodyOut::Str(s) => {
3247 http_body_util::Full::new(bytes::Bytes::from(s.into_bytes())).boxed()
3248 }
3249 ResponseBodyOut::TextChunks(chunks) | ResponseBodyOut::BytesChunks(chunks) => {
3250 HyperChunkedBody::from(chunks).boxed()
3251 }
3252 };
3253 let mut builder = hyper::Response::builder().status(status);
3254 for (name, val) in headers {
3255 builder = builder.header(name, val);
3256 }
3257 builder
3258 .body(boxed_body)
3259 .unwrap_or_else(|_| error_response(500, "response build error"))
3260}
3261
3262fn error_response(status: u16, msg: &str) -> hyper::Response<HyperRespBody> {
3263 use http_body_util::BodyExt as _;
3264 hyper::Response::builder()
3265 .status(status)
3266 .body(
3267 http_body_util::Full::new(bytes::Bytes::from(msg.to_owned()))
3268 .boxed(),
3269 )
3270 .unwrap_or_else(|_| {
3271 use http_body_util::BodyExt as _;
3272 hyper::Response::new(http_body_util::Empty::new().map_err(|e| match e {}).boxed())
3273 })
3274}
3275
3276struct HyperChunkedBody {
3279 chunks: std::collections::VecDeque<Vec<u8>>,
3280}
3281
3282impl From<Vec<Vec<u8>>> for HyperChunkedBody {
3283 fn from(chunks: Vec<Vec<u8>>) -> Self {
3284 Self {
3285 chunks: chunks.into_iter().filter(|c| !c.is_empty()).collect(),
3286 }
3287 }
3288}
3289
3290impl hyper::body::Body for HyperChunkedBody {
3291 type Data = bytes::Bytes;
3292 type Error = std::convert::Infallible;
3293
3294 fn poll_frame(
3295 mut self: std::pin::Pin<&mut Self>,
3296 _cx: &mut std::task::Context<'_>,
3297 ) -> std::task::Poll<Option<Result<hyper::body::Frame<Self::Data>, Self::Error>>> {
3298 match self.chunks.pop_front() {
3299 Some(chunk) => std::task::Poll::Ready(Some(Ok(hyper::body::Frame::data(
3300 bytes::Bytes::from(chunk),
3301 )))),
3302 None => std::task::Poll::Ready(None),
3303 }
3304 }
3305}
3306
3307fn respond_with_body_tls(
3311 req: tiny_http::Request,
3312 status: u16,
3313 body: ResponseBodyOut,
3314 headers: Vec<(String, String)>,
3315) {
3316 let tiny_headers: Vec<tiny_http::Header> = headers
3317 .into_iter()
3318 .filter_map(|(name, val)| format!("{name}: {val}").parse::<tiny_http::Header>().ok())
3319 .collect();
3320 match body {
3321 ResponseBodyOut::Str(s) => {
3322 let mut response = tiny_http::Response::from_string(s).with_status_code(status);
3323 for h in tiny_headers {
3324 response.add_header(h);
3325 }
3326 let _ = req.respond(response);
3327 }
3328 ResponseBodyOut::TextChunks(chunks) | ResponseBodyOut::BytesChunks(chunks) => {
3329 let reader = ChunkReader::new(chunks);
3330 let response = tiny_http::Response::new(
3331 tiny_http::StatusCode(status),
3332 tiny_headers,
3333 reader,
3334 None,
3335 None,
3336 );
3337 let _ = req.respond(response);
3338 }
3339 }
3340}
3341
3342pub(crate) type UnpackedResponse = (u16, ResponseBodyOut, Vec<(String, String)>);
3352
3353pub(crate) enum ResponseBodyOut {
3354 Str(String),
3355 TextChunks(Vec<Vec<u8>>),
3359 BytesChunks(Vec<Vec<u8>>),
3362}
3363
3364fn drain_iter_str(v: &Value) -> Vec<Vec<u8>> {
3377 match v {
3378 Value::Variant { name, args }
3379 if name == "__IterEager" && args.len() == 2 =>
3380 {
3381 if let (Value::List(items), Value::Int(idx)) = (&args[0], &args[1]) {
3382 items.iter().skip(*idx as usize).filter_map(|item| {
3383 if let Value::Str(s) = item { Some(s.as_bytes().to_vec()) } else { None }
3384 }).collect()
3385 } else {
3386 Vec::new()
3387 }
3388 }
3389 _ => Vec::new(),
3390 }
3391}
3392
3393fn drain_iter_bytes(v: &Value) -> Vec<Vec<u8>> {
3397 match v {
3398 Value::Variant { name, args }
3399 if name == "__IterEager" && args.len() == 2 =>
3400 {
3401 if let (Value::List(items), Value::Int(idx)) = (&args[0], &args[1]) {
3402 items.iter().skip(*idx as usize).filter_map(|item| {
3403 if let Value::List(ints) = item {
3404 Some(ints.iter().filter_map(|i| match i {
3405 Value::Int(n) => Some((*n & 0xff) as u8),
3406 _ => None,
3407 }).collect::<Vec<u8>>())
3408 } else {
3409 None
3410 }
3411 }).collect()
3412 } else {
3413 Vec::new()
3414 }
3415 }
3416 _ => Vec::new(),
3417 }
3418}
3419
3420fn materialize_lazy_iter(vm: &mut Vm, v: Value) -> Value {
3433 let mut current = v;
3434 let mut items: Vec<Value> = Vec::new();
3435 loop {
3436 match current {
3437 Value::Variant { name, args } if name == "__IterLazy" && args.len() == 2 => {
3438 let seed = args[0].clone();
3439 let step = args[1].clone();
3440 match vm.invoke_closure_value(step.clone(), vec![seed]) {
3441 Ok(Value::Variant { name: opt, args: opt_args })
3442 if opt == "None" =>
3443 {
3444 let _ = opt_args;
3445 break;
3446 }
3447 Ok(Value::Variant { name: opt, args: opt_args })
3448 if opt == "Some" && opt_args.len() == 1 =>
3449 {
3450 if let Value::Tuple(pair) = &opt_args[0] {
3451 if pair.len() == 2 {
3452 items.push(pair[0].clone());
3453 current = Value::Variant {
3454 name: "__IterLazy".to_string(),
3455 args: vec![pair[1].clone(), step],
3456 };
3457 continue;
3458 }
3459 }
3460 break;
3462 }
3463 _ => break,
3464 }
3465 }
3466 other => {
3469 if items.is_empty() {
3470 return other;
3471 }
3472 let _ = other;
3475 break;
3476 }
3477 }
3478 }
3479 Value::Variant {
3480 name: "__IterEager".to_string(),
3481 args: vec![
3482 Value::List(items.into_iter().collect()),
3483 Value::Int(0),
3484 ],
3485 }
3486}
3487
3488
3489struct ChunkReader {
3495 chunks: std::collections::VecDeque<Vec<u8>>,
3496 cursor: usize,
3497}
3498
3499impl ChunkReader {
3500 fn new(chunks: Vec<Vec<u8>>) -> Self {
3501 Self {
3502 chunks: chunks.into_iter().filter(|c| !c.is_empty()).collect(),
3503 cursor: 0,
3504 }
3505 }
3506}
3507
3508impl std::io::Read for ChunkReader {
3509 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
3510 loop {
3511 let Some(front) = self.chunks.front() else {
3512 return Ok(0);
3513 };
3514 let remaining = &front[self.cursor..];
3515 if remaining.is_empty() {
3516 self.chunks.pop_front();
3517 self.cursor = 0;
3518 continue;
3519 }
3520 let n = remaining.len().min(buf.len());
3521 buf[..n].copy_from_slice(&remaining[..n]);
3522 self.cursor += n;
3523 if self.cursor >= front.len() {
3524 self.chunks.pop_front();
3525 self.cursor = 0;
3526 }
3527 return Ok(n);
3528 }
3529 }
3530}
3531
3532fn http_request(method: &str, url: &str, body: Option<&str>) -> Value {
3538 use std::time::Duration;
3539 let agent: ureq::Agent = ureq::Agent::config_builder()
3544 .timeout_connect(Some(Duration::from_secs(10)))
3545 .timeout_recv_body(Some(Duration::from_secs(30)))
3546 .timeout_send_body(Some(Duration::from_secs(10)))
3547 .http_status_as_error(false)
3548 .build()
3549 .into();
3550 let resp = match (method, body) {
3551 ("GET", _) => agent.get(url).call(),
3552 ("POST", Some(b)) => agent.post(url).send(b),
3553 ("POST", None) => agent.post(url).send(""),
3554 (m, _) => return err_value(format!("unsupported method: {m}")),
3555 };
3556 match resp {
3557 Ok(mut r) => {
3558 let status = r.status().as_u16();
3559 let body = r.body_mut().read_to_string().unwrap_or_default();
3560 if (200..300).contains(&status) {
3561 Value::Variant { name: "Ok".into(), args: vec![Value::Str(body.into())] }
3562 } else {
3563 err_value(format!("status {status}: {body}"))
3564 }
3565 }
3566 Err(e) => err_value(format!("transport: {e}")),
3567 }
3568}
3569
3570fn http_stream_agent() -> ureq::Agent {
3577 use std::time::Duration;
3578 ureq::Agent::config_builder()
3579 .timeout_global(Some(Duration::from_secs(600)))
3580 .http_status_as_error(false)
3581 .build()
3582 .into()
3583}
3584
3585fn http_agent(timeout_ms: Option<u64>) -> ureq::Agent {
3598 use std::time::Duration;
3599 match timeout_ms {
3600 Some(ms) => ureq::Agent::config_builder()
3601 .timeout_global(Some(Duration::from_millis(ms)))
3602 .http_status_as_error(false)
3603 .build()
3604 .into(),
3605 None => ureq::Agent::config_builder()
3606 .timeout_connect(Some(Duration::from_secs(10)))
3607 .timeout_recv_body(Some(Duration::from_secs(30)))
3608 .timeout_send_body(Some(Duration::from_secs(10)))
3609 .http_status_as_error(false)
3610 .build()
3611 .into(),
3612 }
3613}
3614
3615fn http_error_value(e: ureq::Error) -> Value {
3619 let (ctor, payload): (&str, Option<String>) = match &e {
3620 ureq::Error::Timeout(_) => ("TimeoutError", None),
3621 ureq::Error::Tls(s) => ("TlsError", Some((*s).into())),
3622 ureq::Error::Pem(p) => ("TlsError", Some(format!("{p}"))),
3623 ureq::Error::Rustls(r) => ("TlsError", Some(format!("{r}"))),
3624 _ => ("NetworkError", Some(format!("{e}"))),
3625 };
3626 let args = match payload { Some(s) => vec![Value::Str(s.into())], None => vec![] };
3627 let inner = Value::Variant { name: ctor.into(), args };
3628 Value::Variant { name: "Err".into(), args: vec![inner] }
3629}
3630
3631fn http_decode_err(msg: String) -> Value {
3632 let inner = Value::Variant {
3633 name: "DecodeError".into(),
3634 args: vec![Value::Str(msg.into())],
3635 };
3636 Value::Variant { name: "Err".into(), args: vec![inner] }
3637}
3638
3639fn http_send_simple(
3644 method: &str,
3645 url: &str,
3646 body: Option<Vec<u8>>,
3647 content_type: &str,
3648 timeout_ms: Option<u64>,
3649) -> Value {
3650 http_send_full(method, url, body, content_type, &[], timeout_ms)
3651}
3652
3653fn http_send_full(
3654 method: &str,
3655 url: &str,
3656 body: Option<Vec<u8>>,
3657 content_type: &str,
3658 headers: &[(String, String)],
3659 timeout_ms: Option<u64>,
3660) -> Value {
3661 let agent = http_agent(timeout_ms);
3662 let method_upper = method.to_ascii_uppercase();
3668 let body_bytes: Vec<u8> = body.unwrap_or_default();
3669 let resp = match method_upper.as_str() {
3670 "GET" => {
3675 let mut req = agent.get(url);
3676 if !content_type.is_empty() { req = req.header("content-type", content_type); }
3677 for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
3678 req.call()
3679 }
3680 "HEAD" => {
3681 let mut req = agent.head(url);
3682 for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
3683 req.call()
3684 }
3685 "DELETE" => {
3686 let mut req = agent.delete(url);
3687 for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
3688 req.call()
3689 }
3690 "POST" => {
3695 let mut req = agent.post(url);
3696 if !content_type.is_empty() { req = req.header("content-type", content_type); }
3697 for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
3698 req.send(&body_bytes[..])
3699 }
3700 "PUT" => {
3701 let mut req = agent.put(url);
3702 if !content_type.is_empty() { req = req.header("content-type", content_type); }
3703 for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
3704 req.send(&body_bytes[..])
3705 }
3706 "PATCH" => {
3707 let mut req = agent.patch(url);
3708 if !content_type.is_empty() { req = req.header("content-type", content_type); }
3709 for (k, v) in headers { req = req.header(k.as_str(), v.as_str()); }
3710 req.send(&body_bytes[..])
3711 }
3712 m => {
3713 return http_decode_err(format!("unsupported method: {m}"));
3714 }
3715 };
3716 match resp {
3717 Ok(mut r) => {
3718 let status = r.status().as_u16() as i64;
3719 let headers_map = collect_response_headers(r.headers());
3720 let body_bytes = match r.body_mut().with_config().limit(10 * 1024 * 1024).read_to_vec() {
3721 Ok(b) => b,
3722 Err(e) => return http_decode_err(format!("body read: {e}")),
3723 };
3724 let mut rec = indexmap::IndexMap::new();
3725 rec.insert("status".into(), Value::Int(status));
3726 rec.insert("headers".into(), Value::Map(headers_map));
3727 rec.insert("body".into(), Value::Bytes(body_bytes));
3728 Value::Variant { name: "Ok".into(), args: vec![Value::record_dynamic(rec)] }
3729 }
3730 Err(e) => http_error_value(e),
3731 }
3732}
3733
3734fn collect_response_headers(
3735 headers: &ureq::http::HeaderMap,
3736) -> std::collections::BTreeMap<lex_bytecode::MapKey, Value> {
3737 let mut out = std::collections::BTreeMap::new();
3738 for (name, value) in headers.iter() {
3739 let v = value.to_str().unwrap_or("").to_string();
3740 out.insert(lex_bytecode::MapKey::Str(name.as_str().to_string()), Value::Str(v.into()));
3741 }
3742 out
3743}
3744
3745fn http_send_record(handler: &DefaultHandler, req: &indexmap::IndexMap<smol_str::SmolStr, Value>) -> Value {
3749 let method = match req.get("method") {
3750 Some(Value::Str(s)) => s.to_string(),
3751 _ => return http_decode_err("HttpRequest.method must be Str".into()),
3752 };
3753 let url = match req.get("url") {
3754 Some(Value::Str(s)) => s.to_string(),
3755 _ => return http_decode_err("HttpRequest.url must be Str".into()),
3756 };
3757 if let Err(e) = handler.ensure_host_allowed(&url) {
3758 return http_decode_err(e);
3759 }
3760 let body = match req.get("body") {
3761 Some(Value::Variant { name, args }) if name == "None" => None,
3762 Some(Value::Variant { name, args }) if name == "Some" => match args.as_slice() {
3763 [Value::Bytes(b)] => Some(b.clone()),
3764 _ => return http_decode_err("HttpRequest.body Some payload must be Bytes".into()),
3765 },
3766 _ => return http_decode_err("HttpRequest.body must be Option[Bytes]".into()),
3767 };
3768 let timeout_ms = match req.get("timeout_ms") {
3769 Some(Value::Variant { name, .. }) if name == "None" => None,
3770 Some(Value::Variant { name, args }) if name == "Some" => match args.as_slice() {
3771 [Value::Int(n)] if *n >= 0 => Some(*n as u64),
3772 _ => return http_decode_err(
3773 "HttpRequest.timeout_ms Some payload must be a non-negative Int".into()),
3774 },
3775 _ => return http_decode_err("HttpRequest.timeout_ms must be Option[Int]".into()),
3776 };
3777 let headers: Vec<(String, String)> = match req.get("headers") {
3778 Some(Value::Map(m)) => m.iter().filter_map(|(k, v)| {
3779 let kk = match k { lex_bytecode::MapKey::Str(s) => s.clone(), _ => return None };
3780 let vv = match v { Value::Str(s) => s.to_string(), _ => return None };
3781 Some((kk, vv))
3782 }).collect(),
3783 _ => return http_decode_err("HttpRequest.headers must be Map[Str, Str]".into()),
3784 };
3785 http_send_full(&method, &url, body, "", &headers, timeout_ms)
3786}
3787
3788fn expect_record(v: Option<&Value>) -> Result<&indexmap::IndexMap<smol_str::SmolStr, Value>, String> {
3789 match v {
3790 Some(Value::Record { fields: r, .. }) => Ok(r),
3791 Some(other) => Err(format!("expected Record, got {other:?}")),
3792 None => Err("missing Record argument".into()),
3793 }
3794}
3795
3796fn err_value(msg: String) -> Value {
3797 Value::Variant { name: "Err".into(), args: vec![Value::Str(msg.into())] }
3798}
3799
3800fn expect_str(v: Option<&Value>) -> Result<&str, String> {
3801 match v {
3802 Some(Value::Str(s)) => Ok(s),
3803 Some(other) => Err(format!("expected Str arg, got {other:?}")),
3804 None => Err("missing argument".into()),
3805 }
3806}
3807
3808fn expect_int(v: Option<&Value>) -> Result<i64, String> {
3809 match v {
3810 Some(Value::Int(n)) => Ok(*n),
3811 Some(other) => Err(format!("expected Int arg, got {other:?}")),
3812 None => Err("missing argument".into()),
3813 }
3814}
3815
3816fn ok(v: Value) -> Value {
3817 Value::Variant { name: "Ok".into(), args: vec![v] }
3818}
3819fn err(v: Value) -> Value {
3820 Value::Variant { name: "Err".into(), args: vec![v] }
3821}
3822
3823fn http_stream_lines_impl(_handler: &DefaultHandler, url: &str, headers_val: &Value, body: &str) -> Value {
3833 let body_bytes = body.as_bytes().to_vec();
3834 let agent = http_stream_agent();
3837 let mut req = agent.post(url);
3838 if let Value::Map(headers) = headers_val {
3839 for (k, v) in headers {
3840 let key_str = match k {
3841 lex_bytecode::MapKey::Str(s) => s.as_str(),
3842 _ => continue,
3843 };
3844 if let Value::Str(val) = v {
3845 req = req.header(key_str, val.as_str());
3846 }
3847 }
3848 }
3849 match req.send(&body_bytes[..]) {
3850 Ok(resp) => {
3851 let bytes = match resp.into_body().with_config().read_to_vec() {
3852 Ok(b) => b,
3853 Err(e) => return err(Value::Str(format!("http.stream_lines: body read: {e}").into())),
3854 };
3855 let raw_text = String::from_utf8_lossy(&bytes).into_owned();
3856 let text = decode_unicode_escapes(&raw_text);
3857 let items: std::collections::VecDeque<Value> = text.lines()
3858 .map(|l| Value::Str(l.to_string().into()))
3859 .collect();
3860 let iter_val = Value::Variant {
3861 name: "__IterEager".into(),
3862 args: vec![Value::List(items), Value::Int(0)],
3863 };
3864 ok(iter_val)
3865 }
3866 Err(e) => err(Value::Str(format!("http.stream_lines: {e}").into())),
3867 }
3868}
3869
3870fn decode_unicode_escapes(s: &str) -> String {
3871 let mut result = String::with_capacity(s.len());
3872 let mut chars = s.chars().peekable();
3873 while let Some(c) = chars.next() {
3874 if c != '\\' {
3875 result.push(c);
3876 continue;
3877 }
3878 match chars.peek() {
3879 Some('u') => {
3880 chars.next();
3881 let hex: String = (0..4).filter_map(|_| chars.next()).collect();
3882 if hex.len() == 4 {
3883 if let Ok(n) = u32::from_str_radix(&hex, 16) {
3884 if let Some(ch) = char::from_u32(n) {
3885 result.push(ch);
3886 continue;
3887 }
3888 }
3889 }
3890 result.push('\\');
3891 result.push('u');
3892 result.push_str(&hex);
3893 }
3894 _ => result.push(c),
3895 }
3896 }
3897 result
3898}
3899
3900fn sql_error(message: impl Into<String>, code: Option<String>, detail: Option<String>) -> Value {
3904 let some = |s: String| Value::Variant { name: "Some".into(), args: vec![Value::Str(s.into())] };
3905 let none = || Value::Variant { name: "None".into(), args: vec![] };
3906 let mut rec = indexmap::IndexMap::new();
3907 let msg: String = message.into();
3908 rec.insert("message".into(), Value::Str(msg.into()));
3909 rec.insert("code".into(), match code {
3910 Some(c) => some(c),
3911 None => none(),
3912 });
3913 rec.insert("detail".into(), match detail {
3914 Some(d) => some(d),
3915 None => none(),
3916 });
3917 Value::record_dynamic(rec)
3918}
3919
3920fn sqlite_err_to_sql_error(e: rusqlite::Error, op: &str) -> Value {
3930 let message = format!("{op}: {e}");
3931 match &e {
3932 rusqlite::Error::SqliteFailure(ffi, detail_opt) => {
3933 sql_error(
3934 message,
3935 Some(sqlite_extended_code_name(ffi.extended_code)),
3936 detail_opt.clone(),
3937 )
3938 }
3939 rusqlite::Error::SqlInputError { error, msg, .. } => {
3940 sql_error(
3941 message,
3942 Some(sqlite_extended_code_name(error.extended_code)),
3943 Some(msg.clone()),
3944 )
3945 }
3946 _ => sql_error(message, None, None),
3947 }
3948}
3949
3950fn sqlite_extended_code_name(code: i32) -> String {
3956 use rusqlite::ffi::*;
3957 let s = match code {
3958 SQLITE_BUSY => "SQLITE_BUSY",
3959 SQLITE_LOCKED => "SQLITE_LOCKED",
3960 SQLITE_READONLY => "SQLITE_READONLY",
3961 SQLITE_IOERR => "SQLITE_IOERR",
3962 SQLITE_CORRUPT => "SQLITE_CORRUPT",
3963 SQLITE_NOTFOUND => "SQLITE_NOTFOUND",
3964 SQLITE_FULL => "SQLITE_FULL",
3965 SQLITE_CANTOPEN => "SQLITE_CANTOPEN",
3966 SQLITE_PROTOCOL => "SQLITE_PROTOCOL",
3967 SQLITE_SCHEMA => "SQLITE_SCHEMA",
3968 SQLITE_TOOBIG => "SQLITE_TOOBIG",
3969 SQLITE_CONSTRAINT => "SQLITE_CONSTRAINT",
3970 SQLITE_CONSTRAINT_CHECK => "SQLITE_CONSTRAINT_CHECK",
3971 SQLITE_CONSTRAINT_FOREIGNKEY => "SQLITE_CONSTRAINT_FOREIGNKEY",
3972 SQLITE_CONSTRAINT_NOTNULL => "SQLITE_CONSTRAINT_NOTNULL",
3973 SQLITE_CONSTRAINT_PRIMARYKEY => "SQLITE_CONSTRAINT_PRIMARYKEY",
3974 SQLITE_CONSTRAINT_TRIGGER => "SQLITE_CONSTRAINT_TRIGGER",
3975 SQLITE_CONSTRAINT_UNIQUE => "SQLITE_CONSTRAINT_UNIQUE",
3976 SQLITE_CONSTRAINT_VTAB => "SQLITE_CONSTRAINT_VTAB",
3977 SQLITE_CONSTRAINT_ROWID => "SQLITE_CONSTRAINT_ROWID",
3978 SQLITE_MISMATCH => "SQLITE_MISMATCH",
3979 SQLITE_RANGE => "SQLITE_RANGE",
3980 SQLITE_NOTADB => "SQLITE_NOTADB",
3981 SQLITE_AUTH => "SQLITE_AUTH",
3982 _ => return format!("SQLITE_ERROR_{code}"),
3983 };
3984 s.to_string()
3985}
3986
3987fn pg_err_to_sql_error(e: postgres::Error, op: &str) -> Value {
3991 let message = format!("{op}: {e}");
3992 let code = e.as_db_error().map(|db| db.code().code().to_string());
3993 let detail = e.as_db_error().and_then(|db| db.detail().map(|s| s.to_string()));
3994 sql_error(message, code, detail)
3995}
3996
3997impl DefaultHandler {
3998 fn dispatch_call_mcp(&mut self, args: Vec<Value>) -> Value {
4004 let server = match args.first() {
4005 Some(Value::Str(s)) => s.clone(),
4006 _ => return err(Value::Str(
4007 "agent.call_mcp(server, tool, args_json): server must be Str".into())),
4008 };
4009 let tool = match args.get(1) {
4010 Some(Value::Str(s)) => s.clone(),
4011 _ => return err(Value::Str(
4012 "agent.call_mcp(server, tool, args_json): tool must be Str".into())),
4013 };
4014 let args_json = match args.get(2) {
4015 Some(Value::Str(s)) => s.clone(),
4016 _ => return err(Value::Str(
4017 "agent.call_mcp(server, tool, args_json): args_json must be Str".into())),
4018 };
4019 let parsed: serde_json::Value = match serde_json::from_str(&args_json) {
4020 Ok(v) => v,
4021 Err(e) => return err(Value::Str(format!(
4022 "agent.call_mcp: args_json is not valid JSON: {e}").into())),
4023 };
4024 match self.mcp_clients.call(&server, &tool, parsed) {
4025 Ok(result) => ok(Value::Str(
4026 serde_json::to_string(&result).unwrap_or_else(|_| "null".into()).into())),
4027 Err(e) => err(Value::Str(e.into())),
4028 }
4029 }
4030
4031 fn dispatch_cloud_stream(&mut self, args: Vec<Value>) -> Value {
4037 let _prompt = match args.first() {
4038 Some(Value::Str(s)) => s.clone(),
4039 _ => return err(Value::Str(
4040 "agent.cloud_stream(prompt): prompt must be Str".into())),
4041 };
4042 let chunks: Vec<String> = match std::env::var("LEX_LLM_STREAM_FIXTURE") {
4043 Ok(v) => v.split('|').map(|s| s.to_string()).collect(),
4044 Err(_) => return err(Value::Str(
4045 "agent.cloud_stream: live streaming not yet implemented; \
4046 set LEX_LLM_STREAM_FIXTURE='chunk1|chunk2|…' for tests".into())),
4047 };
4048 let handle = self.register_stream(chunks.into_iter());
4049 ok(stream_handle_value(handle))
4050 }
4051
4052 fn dispatch_stream_next(&mut self, args: Vec<Value>) -> Value {
4058 let handle = match args.first().and_then(stream_handle_id) {
4059 Some(h) => h,
4060 None => return Value::Variant { name: "None".into(), args: vec![] },
4061 };
4062 let mut streams = match self.streams.lock() {
4063 Ok(g) => g,
4064 Err(_) => return Value::Variant { name: "None".into(), args: vec![] },
4065 };
4066 match streams.get_mut(&handle).and_then(|it| it.next()) {
4067 Some(chunk) => some(Value::Str(chunk.into())),
4068 None => {
4069 streams.remove(&handle);
4070 Value::Variant { name: "None".into(), args: vec![] }
4071 }
4072 }
4073 }
4074
4075 fn dispatch_stream_collect(&mut self, args: Vec<Value>) -> Value {
4080 let handle = match args.first().and_then(stream_handle_id) {
4081 Some(h) => h,
4082 None => return Value::List(std::collections::VecDeque::new()),
4083 };
4084 let mut iter = {
4085 let mut streams = match self.streams.lock() {
4086 Ok(g) => g,
4087 Err(_) => return Value::List(std::collections::VecDeque::new()),
4088 };
4089 match streams.remove(&handle) {
4090 Some(it) => it,
4091 None => return Value::List(std::collections::VecDeque::new()),
4092 }
4093 };
4094 let mut out: std::collections::VecDeque<Value> = std::collections::VecDeque::new();
4095 for chunk in iter.by_ref() {
4096 out.push_back(Value::Str(chunk.into()));
4097 }
4098 Value::List(out)
4099 }
4100
4101 fn register_stream<I>(&self, iter: I) -> String
4105 where
4106 I: Iterator<Item = String> + Send + 'static,
4107 {
4108 let id = self
4109 .next_stream_id
4110 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
4111 let handle = format!("stream_{id}");
4112 if let Ok(mut streams) = self.streams.lock() {
4113 streams.insert(handle.clone(), Box::new(iter));
4114 }
4115 handle
4116 }
4117}
4118
4119fn stream_handle_value(handle: String) -> Value {
4124 Value::Variant {
4125 name: "__StreamHandle".into(),
4126 args: vec![Value::Str(handle.into())],
4127 }
4128}
4129
4130fn stream_handle_id(v: &Value) -> Option<String> {
4134 match v {
4135 Value::Variant { name, args } if name == "__StreamHandle" => match args.first() {
4136 Some(Value::Str(h)) => Some(h.to_string()),
4137 _ => None,
4138 },
4139 _ => None,
4140 }
4141}
4142
4143fn dispatch_llm_local(args: Vec<Value>) -> Value {
4148 let prompt = match args.first() {
4149 Some(Value::Str(s)) => s.clone(),
4150 _ => return err(Value::Str(
4151 "agent.local_complete(prompt): prompt must be Str".into())),
4152 };
4153 match crate::llm::local_complete(&prompt) {
4154 Ok(text) => ok(Value::Str(text.into())),
4155 Err(e) => err(Value::Str(e.into())),
4156 }
4157}
4158
4159fn dispatch_llm_cloud(args: Vec<Value>) -> Value {
4166 let prompt = match args.first() {
4167 Some(Value::Str(s)) => s.clone(),
4168 _ => return err(Value::Str(
4169 "agent.cloud_complete(prompt): prompt must be Str".into())),
4170 };
4171 match crate::llm::cloud_complete(&prompt) {
4172 Ok(text) => ok(Value::Str(text.into())),
4173 Err(e) => err(Value::Str(e.into())),
4174 }
4175}
4176
4177fn some(v: Value) -> Value {
4178 Value::Variant { name: "Some".into(), args: vec![v] }
4179}
4180fn none() -> Value {
4181 Value::Variant { name: "None".into(), args: vec![] }
4182}
4183
4184fn expect_bytes(v: Option<&Value>) -> Result<&Vec<u8>, String> {
4185 match v {
4186 Some(Value::Bytes(b)) => Ok(b),
4187 Some(other) => Err(format!("expected Bytes arg, got {other:?}")),
4188 None => Err("missing argument".into()),
4189 }
4190}
4191
4192fn expect_kv_handle(v: Option<&Value>) -> Result<u64, String> {
4193 match v {
4194 Some(Value::Int(n)) if *n >= 0 => Ok(*n as u64),
4195 Some(other) => Err(format!("expected Kv handle (Int), got {other:?}")),
4196 None => Err("missing Kv argument".into()),
4197 }
4198}
4199
4200fn expect_sql_handle(v: Option<&Value>) -> Result<u64, String> {
4201 match v {
4202 Some(Value::Int(n)) if *n >= 0 => Ok(*n as u64),
4203 Some(other) => Err(format!("expected Db handle (Int), got {other:?}")),
4204 None => Err("missing Db argument".into()),
4205 }
4206}
4207
4208#[allow(dead_code)]
4209fn expect_str_list(v: Option<&Value>) -> Result<Vec<String>, String> {
4210 match v {
4211 Some(Value::List(items)) => items.iter().map(|x| match x {
4212 Value::Str(s) => Ok(s.to_string()),
4213 other => Err(format!("expected List[Str] element, got {other:?}")),
4214 }).collect(),
4215 Some(other) => Err(format!("expected List[Str], got {other:?}")),
4216 None => Err("missing List[Str] argument".into()),
4217 }
4218}
4219
4220fn expect_sql_params(v: Option<&Value>) -> Result<Vec<SqlParamValue>, String> {
4223 let items = match v {
4224 Some(Value::List(xs)) => xs,
4225 Some(other) => return Err(format!("expected List[SqlParam], got {other:?}")),
4226 None => return Err("missing params argument".into()),
4227 };
4228 items.iter().map(|item| {
4229 match item {
4230 Value::Variant { name, args } => match name.as_str() {
4231 "PStr" => match args.first() {
4232 Some(Value::Str(s)) => Ok(SqlParamValue::Text(s.to_string())),
4233 _ => Err("PStr requires a Str argument".into()),
4234 },
4235 "PInt" => match args.first() {
4236 Some(Value::Int(n)) => Ok(SqlParamValue::Integer(*n)),
4237 _ => Err("PInt requires an Int argument".into()),
4238 },
4239 "PFloat" => match args.first() {
4240 Some(Value::Float(f)) => Ok(SqlParamValue::Real(*f)),
4241 _ => Err("PFloat requires a Float argument".into()),
4242 },
4243 "PBool" => match args.first() {
4244 Some(Value::Bool(b)) => Ok(SqlParamValue::Bool(*b)),
4245 _ => Err("PBool requires a Bool argument".into()),
4246 },
4247 "PNull" => Ok(SqlParamValue::Null),
4248 other => Err(format!("unknown SqlParam constructor `{other}`")),
4249 },
4250 Value::Str(s) => Ok(SqlParamValue::Text(s.to_string())),
4252 other => Err(format!("expected SqlParam variant, got {other:?}")),
4253 }
4254 }).collect()
4255}
4256
4257fn sqlite_params(params: &[SqlParamValue]) -> Vec<rusqlite::types::Value> {
4259 params.iter().map(|p| match p {
4260 SqlParamValue::Text(s) => rusqlite::types::Value::Text(s.clone()),
4261 SqlParamValue::Integer(n) => rusqlite::types::Value::Integer(*n),
4262 SqlParamValue::Real(f) => rusqlite::types::Value::Real(*f),
4263 SqlParamValue::Bool(b) => rusqlite::types::Value::Integer(*b as i64),
4264 SqlParamValue::Null => rusqlite::types::Value::Null,
4265 }).collect()
4266}
4267
4268fn pg_param_refs(params: &[SqlParamValue]) -> Vec<Box<dyn postgres::types::ToSql + Sync>> {
4270 params.iter().map(|p| -> Box<dyn postgres::types::ToSql + Sync> {
4271 match p {
4272 SqlParamValue::Text(s) => Box::new(s.clone()),
4273 SqlParamValue::Integer(n) => Box::new(*n),
4274 SqlParamValue::Real(f) => Box::new(*f),
4275 SqlParamValue::Bool(b) => Box::new(*b),
4276 SqlParamValue::Null => Box::new(Option::<String>::None),
4277 }
4278 }).collect()
4279}
4280
4281fn sql_run_query_sqlite(
4283 conn: &rusqlite::Connection,
4284 stmt_str: &str,
4285 params: &[SqlParamValue],
4286) -> Value {
4287 let mut stmt = match conn.prepare(stmt_str) {
4288 Ok(s) => s,
4289 Err(e) => return err(sqlite_err_to_sql_error(e, "sql.query")),
4290 };
4291 let column_count = stmt.column_count();
4292 let column_names: Vec<String> = (0..column_count)
4293 .map(|i| stmt.column_name(i).unwrap_or("").to_string())
4294 .collect();
4295 let bound = sqlite_params(params);
4296 let bind: Vec<&dyn rusqlite::ToSql> = bound.iter()
4297 .map(|p| p as &dyn rusqlite::ToSql)
4298 .collect();
4299 let mut rows = match stmt.query(rusqlite::params_from_iter(bind.iter())) {
4300 Ok(r) => r,
4301 Err(e) => return err(sqlite_err_to_sql_error(e, "sql.query")),
4302 };
4303 let mut out: Vec<Value> = Vec::new();
4304 loop {
4305 let row = match rows.next() {
4306 Ok(Some(r)) => r,
4307 Ok(None) => break,
4308 Err(e) => return err(sqlite_err_to_sql_error(e, "sql.query")),
4309 };
4310 let mut rec = indexmap::IndexMap::new();
4311 for (i, name) in column_names.iter().enumerate() {
4312 let cell = match row.get_ref(i) {
4313 Ok(c) => sql_value_ref_to_lex(c),
4314 Err(e) => return err(sqlite_err_to_sql_error(e, &format!("sql.query: column {i}"))),
4315 };
4316 rec.insert(name.clone(), cell);
4317 }
4318 out.push(Value::record_dynamic(rec));
4319 }
4320 ok(Value::List(out.into()))
4321}
4322
4323fn sql_run_query_pg(
4325 client: &mut postgres::Client,
4326 stmt_str: &str,
4327 params: &[SqlParamValue],
4328) -> Value {
4329 let pg = pg_param_refs(params);
4330 let refs: Vec<&(dyn postgres::types::ToSql + Sync)> =
4331 pg.iter().map(|b| b.as_ref()).collect();
4332 let rows = match client.query(stmt_str, &refs) {
4333 Ok(r) => r,
4334 Err(e) => return err(pg_err_to_sql_error(e, "sql.query")),
4335 };
4336 let out: std::collections::VecDeque<Value> = rows.iter().map(|row| {
4337 Value::record_dynamic(pg_row_to_lex_record(row))
4338 }).collect();
4339 ok(Value::List(out))
4340}
4341
4342fn pg_row_to_lex_record(row: &postgres::Row) -> indexmap::IndexMap<String, Value> {
4344 use postgres::types::Type;
4345 let mut rec = indexmap::IndexMap::new();
4346 for (i, col) in row.columns().iter().enumerate() {
4347 let ty = col.type_();
4348 let val = if *ty == Type::INT2 || *ty == Type::INT4 || *ty == Type::INT8 {
4349 row.get::<_, Option<i64>>(i).map(Value::Int).unwrap_or(Value::Unit)
4350 } else if *ty == Type::FLOAT4 || *ty == Type::FLOAT8 {
4351 row.get::<_, Option<f64>>(i).map(Value::Float).unwrap_or(Value::Unit)
4352 } else if *ty == Type::BOOL {
4353 row.get::<_, Option<bool>>(i).map(Value::Bool).unwrap_or(Value::Unit)
4354 } else if *ty == Type::BYTEA {
4355 row.get::<_, Option<Vec<u8>>>(i).map(Value::Bytes).unwrap_or(Value::Unit)
4356 } else {
4357 row.get::<_, Option<String>>(i).map(|s| Value::Str(s.into())).unwrap_or(Value::Unit)
4358 };
4359 rec.insert(col.name().to_string(), val);
4360 }
4361 rec
4362}
4363
4364fn sql_get_col<F>(args: &[Value], convert: F) -> Result<Value, String>
4366where
4367 F: Fn(&Value) -> Option<Value>,
4368{
4369 let row = args.first().ok_or("sql.get_*: missing row argument")?;
4370 let col = match args.get(1) {
4371 Some(Value::Str(s)) => s.as_str(),
4372 Some(other) => return Err(format!("sql.get_*: column name must be Str, got {other:?}")),
4373 None => return Err("sql.get_*: missing column name argument".into()),
4374 };
4375 let cell = match row {
4376 Value::Record { fields: rec, .. } => rec.get(col).cloned(),
4377 other => return Err(format!("sql.get_*: row must be a Record, got {other:?}")),
4378 };
4379 Ok(match cell.and_then(|v| convert(&v)) {
4380 Some(v) => Value::Variant { name: "Some".into(), args: vec![v] },
4381 None => Value::Variant { name: "None".into(), args: vec![] },
4382 })
4383}
4384
4385fn sql_value_ref_to_lex(v: rusqlite::types::ValueRef<'_>) -> Value {
4386 use rusqlite::types::ValueRef;
4387 match v {
4388 ValueRef::Null => Value::Unit,
4389 ValueRef::Integer(n) => Value::Int(n),
4390 ValueRef::Real(f) => Value::Float(f),
4391 ValueRef::Text(s) => Value::Str(String::from_utf8_lossy(s).into_owned().into()),
4392 ValueRef::Blob(b) => Value::Bytes(b.to_vec()),
4393 }
4394}
4395
4396#[derive(Clone, Copy, PartialEq, PartialOrd)]
4399enum LogLevel { Debug, Info, Warn, Error }
4400
4401#[derive(Clone, Copy, PartialEq)]
4402enum LogFormat { Text, Json }
4403
4404#[derive(Clone)]
4405enum LogSink {
4406 Stderr,
4407 File(std::sync::Arc<Mutex<std::fs::File>>),
4408}
4409
4410struct LogState {
4411 level: LogLevel,
4412 format: LogFormat,
4413 sink: LogSink,
4414}
4415
4416fn log_state() -> &'static Mutex<LogState> {
4417 static STATE: OnceLock<Mutex<LogState>> = OnceLock::new();
4418 STATE.get_or_init(|| Mutex::new(LogState {
4419 level: LogLevel::Info,
4420 format: LogFormat::Text,
4421 sink: LogSink::Stderr,
4422 }))
4423}
4424
4425fn parse_log_level(s: &str) -> Option<LogLevel> {
4426 match s {
4427 "debug" => Some(LogLevel::Debug),
4428 "info" => Some(LogLevel::Info),
4429 "warn" => Some(LogLevel::Warn),
4430 "error" => Some(LogLevel::Error),
4431 _ => None,
4432 }
4433}
4434
4435fn level_label(l: LogLevel) -> &'static str {
4436 match l {
4437 LogLevel::Debug => "debug",
4438 LogLevel::Info => "info",
4439 LogLevel::Warn => "warn",
4440 LogLevel::Error => "error",
4441 }
4442}
4443
4444fn emit_log(level: LogLevel, msg: &str) {
4445 let state = log_state().lock().unwrap();
4446 if level < state.level {
4447 return;
4448 }
4449 let ts = chrono::Utc::now().to_rfc3339();
4450 let line = match state.format {
4451 LogFormat::Text => format!("[{}] {}: {}\n", ts, level_label(level), msg),
4452 LogFormat::Json => {
4453 let escaped = msg
4457 .replace('\\', "\\\\")
4458 .replace('"', "\\\"")
4459 .replace('\n', "\\n")
4460 .replace('\r', "\\r");
4461 format!(
4462 "{{\"ts\":\"{ts}\",\"level\":\"{}\",\"msg\":\"{escaped}\"}}\n",
4463 level_label(level),
4464 )
4465 }
4466 };
4467 let sink = state.sink.clone();
4468 drop(state);
4469 match sink {
4470 LogSink::Stderr => {
4471 use std::io::Write;
4472 let _ = std::io::stderr().write_all(line.as_bytes());
4473 }
4474 LogSink::File(f) => {
4475 use std::io::Write;
4476 if let Ok(mut g) = f.lock() {
4477 let _ = g.write_all(line.as_bytes());
4478 }
4479 }
4480 }
4481}
4482
4483pub(crate) struct ProcessState {
4484 child: std::process::Child,
4485 stdout: Option<std::io::BufReader<std::process::ChildStdout>>,
4486 stderr: Option<std::io::BufReader<std::process::ChildStderr>>,
4487}
4488
4489fn process_registry() -> &'static Mutex<ProcessRegistry> {
4503 static REGISTRY: OnceLock<Mutex<ProcessRegistry>> = OnceLock::new();
4504 REGISTRY.get_or_init(|| Mutex::new(ProcessRegistry::with_capacity(MAX_PROCESS_HANDLES)))
4505}
4506
4507const MAX_PROCESS_HANDLES: usize = 256;
4508
4509type SharedProcessState = Arc<Mutex<ProcessState>>;
4510
4511pub(crate) struct ProcessRegistry {
4512 entries: indexmap::IndexMap<u64, SharedProcessState>,
4513 cap: usize,
4514}
4515
4516impl ProcessRegistry {
4517 pub(crate) fn with_capacity(cap: usize) -> Self {
4518 Self { entries: indexmap::IndexMap::new(), cap }
4519 }
4520
4521 pub(crate) fn insert(&mut self, handle: u64, state: ProcessState) {
4525 if self.entries.len() >= self.cap {
4526 self.entries.shift_remove_index(0);
4527 }
4528 self.entries.insert(handle, Arc::new(Mutex::new(state)));
4529 }
4530
4531 pub(crate) fn touch_get(&mut self, handle: u64) -> Option<SharedProcessState> {
4535 let idx = self.entries.get_index_of(&handle)?;
4536 self.entries.move_index(idx, self.entries.len() - 1);
4537 self.entries.get(&handle).cloned()
4538 }
4539
4540 pub(crate) fn remove(&mut self, handle: u64) {
4545 self.entries.shift_remove(&handle);
4546 }
4547
4548 #[cfg(test)]
4549 pub(crate) fn len(&self) -> usize { self.entries.len() }
4550}
4551
4552fn next_process_handle() -> u64 {
4553 static COUNTER: AtomicU64 = AtomicU64::new(1);
4554 COUNTER.fetch_add(1, Ordering::SeqCst)
4555}
4556
4557#[cfg(all(test, unix))]
4558mod process_registry_tests {
4559 use super::{ProcessRegistry, ProcessState};
4560
4561 fn fresh_state() -> ProcessState {
4565 let child = std::process::Command::new("true")
4566 .stdout(std::process::Stdio::null())
4567 .stderr(std::process::Stdio::null())
4568 .spawn()
4569 .expect("spawn `true`");
4570 ProcessState { child, stdout: None, stderr: None }
4571 }
4572
4573 #[test]
4574 fn insert_and_get_round_trip() {
4575 let mut r = ProcessRegistry::with_capacity(4);
4576 r.insert(1, fresh_state());
4577 assert!(r.touch_get(1).is_some());
4578 assert!(r.touch_get(2).is_none());
4579 }
4580
4581 #[test]
4582 fn touch_get_returns_distinct_arcs_for_distinct_handles() {
4583 let mut r = ProcessRegistry::with_capacity(4);
4584 r.insert(1, fresh_state());
4585 r.insert(2, fresh_state());
4586 let a = r.touch_get(1).unwrap();
4587 let b = r.touch_get(2).unwrap();
4588 assert!(!std::sync::Arc::ptr_eq(&a, &b));
4590 }
4591
4592 #[test]
4593 fn cap_evicts_lru_on_overflow() {
4594 let mut r = ProcessRegistry::with_capacity(2);
4595 r.insert(1, fresh_state());
4596 r.insert(2, fresh_state());
4597 let _ = r.touch_get(1);
4598 r.insert(3, fresh_state());
4599 assert!(r.touch_get(1).is_some(), "1 was MRU, should survive");
4600 assert!(r.touch_get(2).is_none(), "2 was LRU, should be evicted");
4601 assert!(r.touch_get(3).is_some(), "3 just inserted, should survive");
4602 assert_eq!(r.len(), 2);
4603 }
4604
4605 #[test]
4606 fn cap_with_no_touches_evicts_in_insertion_order() {
4607 let mut r = ProcessRegistry::with_capacity(2);
4608 r.insert(10, fresh_state());
4609 r.insert(20, fresh_state());
4610 r.insert(30, fresh_state());
4611 assert!(r.touch_get(10).is_none());
4612 assert!(r.touch_get(20).is_some());
4613 assert!(r.touch_get(30).is_some());
4614 }
4615
4616 #[test]
4617 fn remove_drops_entry() {
4618 let mut r = ProcessRegistry::with_capacity(4);
4619 r.insert(1, fresh_state());
4620 r.remove(1);
4621 assert!(r.touch_get(1).is_none());
4622 assert_eq!(r.len(), 0);
4623 }
4624
4625 #[test]
4626 fn many_inserts_stay_bounded_at_cap() {
4627 let cap = 8;
4628 let mut r = ProcessRegistry::with_capacity(cap);
4629 for i in 0..(cap as u64 * 3) {
4630 r.insert(i, fresh_state());
4631 assert!(r.len() <= cap);
4632 }
4633 assert_eq!(r.len(), cap);
4634 }
4635
4636 #[test]
4637 fn outstanding_arc_outlives_remove() {
4638 let mut r = ProcessRegistry::with_capacity(4);
4642 r.insert(1, fresh_state());
4643 let arc = r.touch_get(1).expect("entry exists");
4644 r.remove(1);
4645 assert!(r.touch_get(1).is_none());
4647 let _state = arc.lock().unwrap();
4648 }
4649}
4650
4651fn expect_process_handle(v: Option<&Value>) -> Result<u64, String> {
4652 match v {
4653 Some(Value::Int(n)) if *n >= 0 => Ok(*n as u64),
4654 Some(other) => Err(format!("expected ProcessHandle (Int), got {other:?}")),
4655 None => Err("missing ProcessHandle argument".into()),
4656 }
4657}
4658
4659fn kv_registry() -> &'static Mutex<KvRegistry> {
4671 static REGISTRY: OnceLock<Mutex<KvRegistry>> = OnceLock::new();
4672 REGISTRY.get_or_init(|| Mutex::new(KvRegistry::with_capacity(MAX_KV_HANDLES)))
4673}
4674
4675const MAX_KV_HANDLES: usize = 256;
4681
4682pub(crate) struct KvRegistry {
4687 entries: indexmap::IndexMap<u64, sled::Db>,
4688 cap: usize,
4689}
4690
4691impl KvRegistry {
4692 pub(crate) fn with_capacity(cap: usize) -> Self {
4693 Self { entries: indexmap::IndexMap::new(), cap }
4694 }
4695
4696 pub(crate) fn insert(&mut self, handle: u64, db: sled::Db) {
4699 if self.entries.len() >= self.cap {
4700 self.entries.shift_remove_index(0);
4701 }
4702 self.entries.insert(handle, db);
4703 }
4704
4705 pub(crate) fn touch_get(&mut self, handle: u64) -> Option<&sled::Db> {
4707 let idx = self.entries.get_index_of(&handle)?;
4708 self.entries.move_index(idx, self.entries.len() - 1);
4709 self.entries.get(&handle)
4710 }
4711
4712 pub(crate) fn remove(&mut self, handle: u64) {
4714 self.entries.shift_remove(&handle);
4715 }
4716
4717 #[cfg(test)]
4718 pub(crate) fn len(&self) -> usize { self.entries.len() }
4719}
4720
4721fn next_kv_handle() -> u64 {
4722 static COUNTER: AtomicU64 = AtomicU64::new(1);
4723 COUNTER.fetch_add(1, Ordering::SeqCst)
4724}
4725
4726struct RedisEntry {
4741 url: String,
4742 conn: redis::Connection,
4743}
4744
4745struct RedisRegistry {
4746 entries: indexmap::IndexMap<u64, RedisEntry>,
4747 cap: usize,
4748}
4749
4750impl RedisRegistry {
4751 fn with_capacity(cap: usize) -> Self {
4752 Self { entries: indexmap::IndexMap::new(), cap }
4753 }
4754
4755 fn insert(&mut self, handle: u64, entry: RedisEntry) {
4756 if self.entries.len() >= self.cap {
4757 self.entries.shift_remove_index(0);
4758 }
4759 self.entries.insert(handle, entry);
4760 }
4761
4762 fn touch_get_mut(&mut self, handle: u64) -> Option<&mut RedisEntry> {
4763 let idx = self.entries.get_index_of(&handle)?;
4764 self.entries.move_index(idx, self.entries.len() - 1);
4765 self.entries.get_mut(&handle)
4766 }
4767
4768 fn get_url(&self, handle: u64) -> Option<String> {
4771 self.entries.get(&handle).map(|e| e.url.clone())
4772 }
4773
4774 fn remove(&mut self, handle: u64) {
4775 self.entries.shift_remove(&handle);
4776 }
4777}
4778
4779fn redis_registry() -> &'static Mutex<RedisRegistry> {
4780 static REGISTRY: OnceLock<Mutex<RedisRegistry>> = OnceLock::new();
4781 REGISTRY.get_or_init(|| Mutex::new(RedisRegistry::with_capacity(MAX_REDIS_HANDLES)))
4782}
4783
4784const MAX_REDIS_HANDLES: usize = 256;
4785
4786fn next_redis_handle() -> u64 {
4787 static COUNTER: AtomicU64 = AtomicU64::new(1);
4788 COUNTER.fetch_add(1, Ordering::SeqCst)
4789}
4790
4791fn expect_redis_handle(v: Option<&Value>) -> Result<u64, String> {
4792 match v {
4793 Some(Value::Int(n)) if *n >= 0 => Ok(*n as u64),
4794 Some(other) => Err(format!("expected ConnRedis (Int), got {other:?}")),
4795 None => Err("missing ConnRedis argument".into()),
4796 }
4797}
4798
4799fn sql_registry() -> &'static Mutex<SqlRegistry> {
4806 static REGISTRY: OnceLock<Mutex<SqlRegistry>> = OnceLock::new();
4807 REGISTRY.get_or_init(|| Mutex::new(SqlRegistry::with_capacity(MAX_SQL_HANDLES)))
4808}
4809
4810const MAX_SQL_HANDLES: usize = 256;
4811
4812const CURSOR_CHANNEL_CAPACITY: usize = 64;
4835const MAX_CURSOR_HANDLES: usize = 256;
4836
4837type CursorReceiver = std::sync::mpsc::Receiver<Result<Value, String>>;
4838
4839pub(crate) struct CursorRegistry {
4840 entries: indexmap::IndexMap<u64, Arc<Mutex<CursorReceiver>>>,
4845 cap: usize,
4846}
4847
4848impl CursorRegistry {
4849 pub(crate) fn with_capacity(cap: usize) -> Self {
4850 Self { entries: indexmap::IndexMap::new(), cap }
4851 }
4852
4853 pub(crate) fn insert(&mut self, handle: u64, rx: CursorReceiver) {
4854 if self.entries.len() >= self.cap {
4855 self.entries.shift_remove_index(0);
4856 }
4857 self.entries.insert(handle, Arc::new(Mutex::new(rx)));
4858 }
4859
4860 pub(crate) fn touch_get(&mut self, handle: u64) -> Option<Arc<Mutex<CursorReceiver>>> {
4861 let idx = self.entries.get_index_of(&handle)?;
4862 self.entries.move_index(idx, self.entries.len() - 1);
4863 self.entries.get(&handle).cloned()
4864 }
4865
4866 pub(crate) fn remove(&mut self, handle: u64) {
4867 self.entries.shift_remove(&handle);
4868 }
4869}
4870
4871fn cursor_registry() -> &'static Mutex<CursorRegistry> {
4872 static REGISTRY: OnceLock<Mutex<CursorRegistry>> = OnceLock::new();
4873 REGISTRY.get_or_init(|| Mutex::new(CursorRegistry::with_capacity(MAX_CURSOR_HANDLES)))
4874}
4875
4876fn next_cursor_handle() -> u64 {
4877 static COUNTER: AtomicU64 = AtomicU64::new(1);
4878 COUNTER.fetch_add(1, Ordering::SeqCst)
4879}
4880
4881fn sqlite_cursor_producer(
4887 conn_arc: Arc<Mutex<SqlConn>>,
4888 stmt_str: String,
4889 params: Vec<SqlParamValue>,
4890 sender: std::sync::mpsc::SyncSender<Result<Value, String>>,
4891) {
4892 let mut conn_guard = match conn_arc.lock() {
4893 Ok(g) => g,
4894 Err(p) => p.into_inner(),
4895 };
4896 let SqlConn::Sqlite(c) = &mut *conn_guard else {
4897 let _ = sender.send(Err("sqlite_cursor_producer called on non-sqlite conn".into()));
4898 return;
4899 };
4900 let mut stmt = match c.prepare(&stmt_str) {
4901 Ok(s) => s,
4902 Err(e) => { let _ = sender.send(Err(format!("prepare: {e}"))); return; }
4903 };
4904 let column_count = stmt.column_count();
4905 let column_names: Vec<String> = (0..column_count)
4906 .map(|i| stmt.column_name(i).unwrap_or("").to_string())
4907 .collect();
4908 let bound = sqlite_params(¶ms);
4909 let bind: Vec<&dyn rusqlite::ToSql> =
4910 bound.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
4911 let mut rows = match stmt.query(rusqlite::params_from_iter(bind.iter())) {
4912 Ok(r) => r,
4913 Err(e) => { let _ = sender.send(Err(format!("query: {e}"))); return; }
4914 };
4915 loop {
4916 match rows.next() {
4917 Ok(None) => break,
4918 Err(e) => {
4919 let _ = sender.send(Err(format!("row: {e}")));
4920 break;
4921 }
4922 Ok(Some(row)) => {
4923 let mut rec = indexmap::IndexMap::new();
4924 for (i, name) in column_names.iter().enumerate() {
4925 let val = match row.get_ref(i) {
4926 Ok(vr) => sql_value_ref_to_lex(vr),
4927 Err(_) => Value::Unit,
4928 };
4929 rec.insert(name.clone(), val);
4930 }
4931 if sender.send(Ok(Value::record_dynamic(rec))).is_err() {
4932 break;
4933 }
4934 }
4935 }
4936 }
4937}
4938
4939fn pg_cursor_producer(
4943 conn_arc: Arc<Mutex<SqlConn>>,
4944 stmt_str: String,
4945 params: Vec<SqlParamValue>,
4946 sender: std::sync::mpsc::SyncSender<Result<Value, String>>,
4947) {
4948 let mut conn_guard = match conn_arc.lock() {
4949 Ok(g) => g,
4950 Err(p) => p.into_inner(),
4951 };
4952 let SqlConn::Postgres(c) = &mut *conn_guard else {
4953 let _ = sender.send(Err("pg_cursor_producer called on non-postgres conn".into()));
4954 return;
4955 };
4956 let pg = pg_param_refs(¶ms);
4957 let refs: Vec<&(dyn postgres::types::ToSql + Sync)> =
4958 pg.iter().map(|b| b.as_ref()).collect();
4959 let mut tx = match c.transaction() {
4960 Ok(t) => t,
4961 Err(e) => { let _ = sender.send(Err(format!("begin: {e}"))); return; }
4962 };
4963 let cur_name = format!("__lex_cur_{}", next_cursor_handle());
4966 if let Err(e) = tx.execute(
4967 &format!("DECLARE \"{cur_name}\" NO SCROLL CURSOR FOR {stmt_str}"),
4968 &refs,
4969 ) {
4970 let _ = sender.send(Err(format!("declare: {e}")));
4971 return;
4972 }
4973 let fetch_sql = format!("FETCH 64 FROM \"{cur_name}\"");
4974 'outer: loop {
4975 let batch = match tx.query(&fetch_sql, &[]) {
4976 Ok(r) => r,
4977 Err(e) => { let _ = sender.send(Err(format!("fetch: {e}"))); break; }
4978 };
4979 if batch.is_empty() {
4980 break;
4981 }
4982 for row in batch.iter() {
4983 let rec = pg_row_to_lex_record(row);
4984 if sender.send(Ok(Value::record_dynamic(rec))).is_err() {
4985 break 'outer;
4986 }
4987 }
4988 }
4989 let _ = tx.execute(&format!("CLOSE \"{cur_name}\""), &[]);
4990 let _ = tx.commit();
4991}
4992
4993#[derive(Debug, Clone)]
4995enum SqlParamValue {
4996 Text(String),
4997 Integer(i64),
4998 Real(f64),
4999 Bool(bool),
5000 Null,
5001}
5002
5003pub(crate) enum SqlConn {
5005 Sqlite(rusqlite::Connection),
5006 Postgres(postgres::Client),
5007}
5008
5009type SharedConn = Arc<Mutex<SqlConn>>;
5010
5011pub(crate) struct SqlRegistry {
5012 entries: indexmap::IndexMap<u64, SharedConn>,
5013 cap: usize,
5014}
5015
5016impl SqlRegistry {
5017 pub(crate) fn with_capacity(cap: usize) -> Self {
5018 Self { entries: indexmap::IndexMap::new(), cap }
5019 }
5020
5021 pub(crate) fn insert(&mut self, handle: u64, conn: SqlConn) {
5022 if self.entries.len() >= self.cap {
5023 self.entries.shift_remove_index(0);
5024 }
5025 self.entries.insert(handle, Arc::new(Mutex::new(conn)));
5026 }
5027
5028 pub(crate) fn touch_get(&mut self, handle: u64) -> Option<SharedConn> {
5032 let idx = self.entries.get_index_of(&handle)?;
5033 self.entries.move_index(idx, self.entries.len() - 1);
5034 self.entries.get(&handle).cloned()
5035 }
5036
5037 pub(crate) fn remove(&mut self, handle: u64) {
5038 self.entries.shift_remove(&handle);
5039 }
5040
5041 #[cfg(test)]
5042 pub(crate) fn len(&self) -> usize { self.entries.len() }
5043}
5044
5045fn next_sql_handle() -> u64 {
5046 static COUNTER: AtomicU64 = AtomicU64::new(1);
5047 COUNTER.fetch_add(1, Ordering::SeqCst)
5048}
5049
5050#[cfg(test)]
5051mod sql_registry_tests {
5052 use super::{SqlConn, SqlRegistry};
5053
5054 fn fresh() -> SqlConn {
5055 SqlConn::Sqlite(rusqlite::Connection::open_in_memory().expect("open in-memory sqlite"))
5056 }
5057
5058 #[test]
5059 fn insert_and_get_round_trip() {
5060 let mut r = SqlRegistry::with_capacity(4);
5061 r.insert(1, fresh());
5062 assert!(r.touch_get(1).is_some());
5063 assert!(r.touch_get(2).is_none());
5064 }
5065
5066 #[test]
5067 fn cap_evicts_lru_on_overflow() {
5068 let mut r = SqlRegistry::with_capacity(2);
5069 r.insert(1, fresh());
5070 r.insert(2, fresh());
5071 let _ = r.touch_get(1);
5072 r.insert(3, fresh());
5073 assert!(r.touch_get(1).is_some(), "1 was MRU, should survive");
5074 assert!(r.touch_get(2).is_none(), "2 was LRU, should be evicted");
5075 assert!(r.touch_get(3).is_some(), "3 just inserted");
5076 assert_eq!(r.len(), 2);
5077 }
5078
5079 #[test]
5080 fn remove_drops_entry() {
5081 let mut r = SqlRegistry::with_capacity(4);
5082 r.insert(1, fresh());
5083 r.remove(1);
5084 assert!(r.touch_get(1).is_none());
5085 assert_eq!(r.len(), 0);
5086 }
5087
5088 #[test]
5089 fn many_inserts_stay_bounded_at_cap() {
5090 let cap = 8;
5091 let mut r = SqlRegistry::with_capacity(cap);
5092 for i in 0..(cap as u64 * 3) {
5093 r.insert(i, fresh());
5094 assert!(r.len() <= cap);
5095 }
5096 assert_eq!(r.len(), cap);
5097 }
5098}
5099
5100#[cfg(test)]
5101mod kv_registry_tests {
5102 use super::KvRegistry;
5103
5104 fn fresh_db(tag: &str) -> sled::Db {
5107 let dir = std::env::temp_dir().join(format!(
5108 "lex-kv-reg-{}-{}-{}",
5109 std::process::id(),
5110 tag,
5111 std::time::SystemTime::now()
5112 .duration_since(std::time::UNIX_EPOCH)
5113 .unwrap()
5114 .as_nanos()
5115 ));
5116 sled::open(&dir).expect("sled open")
5117 }
5118
5119 #[test]
5120 fn insert_and_get_round_trip() {
5121 let mut r = KvRegistry::with_capacity(4);
5122 r.insert(1, fresh_db("a"));
5123 assert!(r.touch_get(1).is_some());
5124 assert!(r.touch_get(2).is_none());
5125 }
5126
5127 #[test]
5128 fn cap_evicts_lru_on_overflow() {
5129 let mut r = KvRegistry::with_capacity(2);
5131 r.insert(1, fresh_db("c1"));
5132 r.insert(2, fresh_db("c2"));
5133 let _ = r.touch_get(1);
5134 r.insert(3, fresh_db("c3"));
5135 assert!(r.touch_get(1).is_some(), "1 was MRU, should survive");
5136 assert!(r.touch_get(2).is_none(), "2 was LRU, should be evicted");
5137 assert!(r.touch_get(3).is_some(), "3 just inserted, should survive");
5138 assert_eq!(r.len(), 2);
5139 }
5140
5141 #[test]
5142 fn cap_with_no_touches_evicts_in_insertion_order() {
5143 let mut r = KvRegistry::with_capacity(2);
5145 r.insert(10, fresh_db("f1"));
5146 r.insert(20, fresh_db("f2"));
5147 r.insert(30, fresh_db("f3"));
5148 assert!(r.touch_get(10).is_none());
5149 assert!(r.touch_get(20).is_some());
5150 assert!(r.touch_get(30).is_some());
5151 }
5152
5153 #[test]
5154 fn remove_drops_entry() {
5155 let mut r = KvRegistry::with_capacity(4);
5156 r.insert(1, fresh_db("r1"));
5157 r.remove(1);
5158 assert!(r.touch_get(1).is_none());
5159 assert_eq!(r.len(), 0);
5160 }
5161
5162 #[test]
5163 fn remove_unknown_handle_is_noop() {
5164 let mut r = KvRegistry::with_capacity(4);
5165 r.insert(1, fresh_db("u1"));
5166 r.remove(999);
5167 assert!(r.touch_get(1).is_some());
5168 }
5169
5170 #[test]
5171 fn many_inserts_stay_bounded_at_cap() {
5172 let cap = 8;
5175 let mut r = KvRegistry::with_capacity(cap);
5176 for i in 0..(cap as u64 * 3) {
5177 r.insert(i, fresh_db(&format!("b{i}")));
5178 assert!(r.len() <= cap);
5179 }
5180 assert_eq!(r.len(), cap);
5181 }
5182}
5183
5184#[cfg(test)]
5191mod unpack_response_tests {
5192 use super::*;
5193 use std::sync::Arc;
5194 use indexmap::IndexMap;
5195 use lex_bytecode::{Const, Op, Program, Value};
5196 use lex_bytecode::program::{Function, ZERO_BODY_HASH};
5197 use lex_bytecode::vm::Vm;
5198
5199 fn build_arena_response_program() -> Arc<Program> {
5204 let constants = vec![
5205 Const::FieldName("status".into()), Const::FieldName("body".into()), Const::Int(200), Const::VariantName("BodyStr".into()), Const::Str("hello".into()), ];
5211 let mut function_names = IndexMap::new();
5212 function_names.insert("handler".to_string(), 0);
5213 Arc::new(Program {
5214 constants,
5215 functions: vec![Function {
5216 name: "handler".into(),
5217 arity: 0,
5218 locals_count: 0,
5219 code: vec![
5220 Op::PushConst(2), Op::PushConst(4), Op::MakeVariant { name_idx: 3, arity: 1 }, Op::AllocArenaRecord { shape_idx: 0, field_count: 2 }, Op::Return,
5225 ],
5226 effects: vec![],
5227 body_hash: ZERO_BODY_HASH,
5228 refinements: vec![],
5229 field_ic_sites: 0,
5230 }],
5231 function_names,
5232 module_aliases: IndexMap::new(),
5233 entry: Some(0),
5234 record_shapes: vec![vec![0, 1]], })
5236 }
5237
5238 #[test]
5244 fn unpack_response_reads_arena_record_via_slab() {
5245 let p = build_arena_response_program();
5246 let mut vm = Vm::new(&p);
5247 let scope = vm.enter_request_scope();
5248
5249 let resp = vm.invoke(p.function_names["handler"], vec![]).unwrap();
5250 assert!(matches!(resp, Value::ArenaRecord { .. }),
5253 "expected ArenaRecord (slab path), got {resp:?}");
5254
5255 let (status, body, headers) = unpack_response(&mut vm, &resp);
5256 vm.exit_request_scope(scope);
5257
5258 assert_eq!(status, 200);
5259 assert!(headers.is_empty());
5260 match body {
5261 ResponseBodyOut::Str(s) => assert_eq!(s, "hello"),
5262 _ => panic!("expected BodyStr"),
5263 }
5264 }
5265
5266 #[test]
5271 fn unpack_response_reads_heap_record() {
5272 let p = build_arena_response_program();
5273 let mut vm = Vm::new(&p);
5274
5275 let resp = vm.invoke(p.function_names["handler"], vec![]).unwrap();
5277 assert!(matches!(resp, Value::Record { .. }),
5278 "expected heap Record (fallback path), got {resp:?}");
5279
5280 let (status, body, headers) = unpack_response(&mut vm, &resp);
5281 assert_eq!(status, 200);
5282 assert!(headers.is_empty());
5283 match body {
5284 ResponseBodyOut::Str(s) => assert_eq!(s, "hello"),
5285 _ => panic!("expected BodyStr"),
5286 }
5287 }
5288
5289 #[test]
5292 fn unpack_response_falls_back_to_500_on_non_record() {
5293 let p = build_arena_response_program();
5294 let mut vm = Vm::new(&p);
5295 let v = Value::Int(7);
5296 let (status, _body, _headers) = unpack_response(&mut vm, &v);
5297 assert_eq!(status, 500);
5298 }
5299}