1use lex_bytecode::vm::{EffectHandler, Vm};
8use lex_bytecode::{Program, Value};
9use std::path::PathBuf;
10use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
11use std::sync::{Mutex, OnceLock};
12use std::sync::Arc;
13use std::time::{SystemTime, UNIX_EPOCH};
14
15use crate::builtins::{call_pure_builtin, is_pure_call};
16use crate::policy::Policy;
17
18mod approval;
19mod dispatch;
20mod fs;
21mod http_client;
22mod http_serve;
23mod kv;
24mod llm;
25mod logging;
26mod proc;
27mod redis_store;
28mod sql;
29mod udp;
30
31pub use approval::{ApprovalSink, NullApprovalSink, StdinApprovalSink};
32pub use http_serve::TlsConfig;
33#[cfg(feature = "quic")]
34pub(crate) use http_serve::{
35 build_request_value_parts, dispatch_route, stamp_path_params, unpack_response, ResponseBodyOut,
36 RouteSeg, ServeOpts, UnpackedResponse,
37};
38use http_client::*;
39use http_serve::*;
40use kv::*;
41use llm::*;
42use redis_store::*;
43use sql::*;
44use udp::*;
45
46pub trait IoSink: Send {
49 fn print_line(&mut self, s: &str);
50}
51
52pub struct StdoutSink;
53impl IoSink for StdoutSink {
54 fn print_line(&mut self, s: &str) {
55 use std::io::Write;
56 println!("{s}");
57 let _ = std::io::stdout().flush();
58 }
59}
60
61#[derive(Default)]
62pub struct CapturedSink { pub lines: Vec<String> }
63impl IoSink for CapturedSink {
64 fn print_line(&mut self, s: &str) { self.lines.push(s.to_string()); }
65}
66
67pub type StreamRegistry =
70 std::collections::HashMap<String, Box<dyn Iterator<Item = String> + Send>>;
71
72pub const NO_EXIT: i64 = i64::MIN;
75
76pub struct DefaultHandler {
77 policy: Policy,
78 pub sink: Box<dyn IoSink>,
79 pub read_root: Option<PathBuf>,
82 pub budget_remaining: Arc<AtomicU64>,
89 pub budget_ceiling: Option<u64>,
93 pub program: Option<Arc<Program>>,
97 pub chat_registry: Option<Arc<crate::ws::ChatRegistry>>,
101 pub mcp_clients: crate::mcp_client::McpClientCache,
107 pub streams: Arc<std::sync::Mutex<StreamRegistry>>,
114 pub next_stream_id: Arc<std::sync::atomic::AtomicU64>,
116 arena_stack: Vec<(u64, crate::arena::Arena)>,
129 next_scope_id: u64,
134 pub requested_exit: Arc<AtomicI64>,
147 pub program_args: Vec<String>,
150 pub approval_sink: Box<dyn ApprovalSink>,
154}
155
156impl DefaultHandler {
157 pub fn new(policy: Policy) -> Self {
158 let ceiling = policy.budget;
162 let initial = ceiling.unwrap_or(u64::MAX);
163 Self {
164 policy,
165 sink: Box::new(StdoutSink),
166 read_root: None,
167 budget_remaining: Arc::new(AtomicU64::new(initial)),
168 budget_ceiling: ceiling,
169 program: None,
170 chat_registry: None,
171 mcp_clients: crate::mcp_client::McpClientCache::with_capacity(16),
172 streams: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
173 next_stream_id: Arc::new(std::sync::atomic::AtomicU64::new(0)),
174 arena_stack: Vec::new(),
175 next_scope_id: 1,
176 requested_exit: Arc::new(AtomicI64::new(NO_EXIT)),
177 program_args: Vec::new(),
178 approval_sink: Box::new(NullApprovalSink),
179 }
180 }
181
182 pub fn with_approval_sink(mut self, sink: Box<dyn ApprovalSink>) -> Self {
183 self.approval_sink = sink; self
184 }
185
186 pub fn active_arena(&self) -> Option<&crate::arena::Arena> {
192 self.arena_stack.last().map(|(_, a)| a)
193 }
194
195 pub fn arena_stack_depth(&self) -> usize {
198 self.arena_stack.len()
199 }
200
201 pub fn with_program(mut self, program: Arc<Program>) -> Self {
202 self.program = Some(program); self
203 }
204
205 pub fn with_chat_registry(mut self, registry: Arc<crate::ws::ChatRegistry>) -> Self {
206 self.chat_registry = Some(registry); self
207 }
208
209 pub fn with_sink(mut self, sink: Box<dyn IoSink>) -> Self {
210 self.sink = sink; self
211 }
212
213 pub fn with_read_root(mut self, root: PathBuf) -> Self {
214 self.read_root = Some(root); self
215 }
216
217 pub fn with_program_args(mut self, args: Vec<String>) -> Self {
218 self.program_args = args; self
219 }
220
221 fn ensure_kind_allowed(&self, kind: &str) -> Result<(), String> {
222 if self.policy.allow_effects.contains(kind) {
223 Ok(())
224 } else {
225 Err(format!("effect `{kind}` not in --allow-effects"))
226 }
227 }
228
229 fn resolve_read_path(&self, p: &str) -> PathBuf {
230 match &self.read_root {
231 Some(root) => root.join(p.trim_start_matches('/')),
232 None => PathBuf::from(p),
233 }
234 }
235
236 fn ensure_host_allowed(&self, url: &str) -> Result<(), String> {
240 if self.policy.allow_net_host.is_empty() { return Ok(()); }
241 let host = extract_host(url).unwrap_or("");
242 if self.policy.allow_net_host.iter().any(|h| host == h) {
243 Ok(())
244 } else {
245 Err(format!(
246 "net call to host `{host}` not in --allow-net-host {:?}",
247 self.policy.allow_net_host,
248 ))
249 }
250 }
251}
252
253fn extract_host(url: &str) -> Option<&str> {
254 let rest = url
255 .strip_prefix("http://")
256 .or_else(|| url.strip_prefix("https://"))
257 .or_else(|| url.strip_prefix("redis://"))
258 .or_else(|| url.strip_prefix("rediss://"))
259 .map(|r| r.split_once('@').map(|(_, after)| after).unwrap_or(r))?;
261 let host_port = match rest.find('/') {
262 Some(i) => &rest[..i],
263 None => rest,
264 };
265 Some(match host_port.rsplit_once(':') {
266 Some((h, _)) => h,
267 None => host_port,
268 })
269}
270
271fn expect_record(v: Option<&Value>) -> Result<&indexmap::IndexMap<smol_str::SmolStr, Value>, String> {
272 match v {
273 Some(Value::Record { fields: r, .. }) => Ok(r),
274 Some(other) => Err(format!("expected Record, got {other:?}")),
275 None => Err("missing Record argument".into()),
276 }
277}
278
279fn err_value(msg: String) -> Value {
280 Value::Variant { name: "Err".into(), args: vec![Value::Str(msg.into())] }
281}
282
283fn expect_str(v: Option<&Value>) -> Result<&str, String> {
284 match v {
285 Some(Value::Str(s)) => Ok(s),
286 Some(other) => Err(format!("expected Str arg, got {other:?}")),
287 None => Err("missing argument".into()),
288 }
289}
290
291fn expect_int(v: Option<&Value>) -> Result<i64, String> {
292 match v {
293 Some(Value::Int(n)) => Ok(*n),
294 Some(other) => Err(format!("expected Int arg, got {other:?}")),
295 None => Err("missing argument".into()),
296 }
297}
298
299fn ok(v: Value) -> Value {
300 Value::Variant { name: "Ok".into(), args: vec![v] }
301}
302fn err(v: Value) -> Value {
303 Value::Variant { name: "Err".into(), args: vec![v] }
304}
305
306fn vcs_store_root() -> std::path::PathBuf {
309 if let Ok(p) = std::env::var("LEX_STORE_ROOT") {
310 return std::path::PathBuf::from(p);
311 }
312 let home = std::env::var("HOME")
313 .map(std::path::PathBuf::from)
314 .unwrap_or_else(|_| std::path::PathBuf::from("."));
315 home.join(".lex/store")
316}
317
318fn decode_unicode_escapes(s: &str) -> String {
319 let mut result = String::with_capacity(s.len());
320 let mut chars = s.chars().peekable();
321 while let Some(c) = chars.next() {
322 if c != '\\' {
323 result.push(c);
324 continue;
325 }
326 match chars.peek() {
327 Some('u') => {
328 chars.next();
329 let hex: String = (0..4).filter_map(|_| chars.next()).collect();
330 if hex.len() == 4 {
331 if let Ok(n) = u32::from_str_radix(&hex, 16) {
332 if let Some(ch) = char::from_u32(n) {
333 result.push(ch);
334 continue;
335 }
336 }
337 }
338 result.push('\\');
339 result.push('u');
340 result.push_str(&hex);
341 }
342 _ => result.push(c),
343 }
344 }
345 result
346}
347
348fn some(v: Value) -> Value {
349 Value::Variant { name: "Some".into(), args: vec![v] }
350}
351fn none() -> Value {
352 Value::Variant { name: "None".into(), args: vec![] }
353}
354
355fn expect_bytes(v: Option<&Value>) -> Result<&Vec<u8>, String> {
356 match v {
357 Some(Value::Bytes(b)) => Ok(b),
358 Some(other) => Err(format!("expected Bytes arg, got {other:?}")),
359 None => Err("missing argument".into()),
360 }
361}
362
363#[allow(dead_code)]
364fn expect_str_list(v: Option<&Value>) -> Result<Vec<String>, String> {
365 match v {
366 Some(Value::List(items)) => items.iter().map(|x| match x {
367 Value::Str(s) => Ok(s.to_string()),
368 other => Err(format!("expected List[Str] element, got {other:?}")),
369 }).collect(),
370 Some(other) => Err(format!("expected List[Str], got {other:?}")),
371 None => Err("missing List[Str] argument".into()),
372 }
373}
374