algocline_engine/
executor.rs1use std::path::PathBuf;
21
22use algocline_core::{Budget, ExecutionMetrics, ExecutionSpec};
23use mlua::LuaSerdeExt;
24use mlua_isle::{AsyncIsle, AsyncIsleDriver, IsleError};
25use mlua_pkg::{resolvers::FsResolver, sandbox::SymlinkAwareSandbox, Registry};
26
27use crate::bridge;
28use crate::llm_bridge::LlmRequest;
29use crate::session::Session;
30
31fn make_resolver(path: &std::path::Path) -> Option<FsResolver> {
37 let strict = std::env::var("ALC_PKG_STRICT")
38 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
39 .unwrap_or(false);
40
41 if strict {
42 FsResolver::new(path).ok()
43 } else {
44 SymlinkAwareSandbox::new(path)
45 .ok()
46 .map(FsResolver::with_sandbox)
47 }
48}
49
50const PRELUDE: &str = include_str!("prelude.lua");
53
54pub struct Executor {
63 isle: AsyncIsle,
65 _driver: AsyncIsleDriver,
66 lib_paths: Vec<PathBuf>,
68}
69
70impl Executor {
71 pub async fn new(lib_paths: Vec<PathBuf>) -> anyhow::Result<Self> {
72 let paths_for_shared = lib_paths.clone();
73 let (isle, driver) = AsyncIsle::spawn(move |lua| {
74 let mut reg = Registry::new();
75 for path in &paths_for_shared {
76 if let Some(resolver) = make_resolver(path) {
77 reg.add(resolver);
78 }
79 }
80 reg.install(lua)?;
81 Ok(())
82 })
83 .await?;
84
85 Ok(Self {
86 isle,
87 _driver: driver,
88 lib_paths,
89 })
90 }
91
92 pub async fn eval_simple(&self, code: String) -> Result<serde_json::Value, String> {
98 self.eval_simple_with_paths(code, vec![]).await
99 }
100
101 pub async fn eval_simple_with_paths(
108 &self,
109 code: String,
110 extra_lib_paths: Vec<PathBuf>,
111 ) -> Result<serde_json::Value, String> {
112 if extra_lib_paths.is_empty() {
113 let task = self.isle.spawn_exec(move |lua| {
115 let result: mlua::Value = lua
116 .load(&code)
117 .eval()
118 .map_err(|e| IsleError::Lua(e.to_string()))?;
119 let json: serde_json::Value = lua
120 .from_value(result)
121 .map_err(|e| IsleError::Lua(e.to_string()))?;
122 serde_json::to_string(&json)
123 .map_err(|e| IsleError::Lua(format!("JSON serialize: {e}")))
124 });
125 let json_str = task.await.map_err(|e| e.to_string())?;
126 return serde_json::from_str(&json_str).map_err(|e| format!("JSON parse: {e}"));
127 }
128
129 let mut effective = extra_lib_paths;
131 effective.extend(self.lib_paths.iter().cloned());
132
133 let (tmp_isle, _tmp_driver) = AsyncIsle::spawn(move |lua| {
134 let mut reg = Registry::new();
135 for path in &effective {
136 if let Some(resolver) = make_resolver(path) {
137 reg.add(resolver);
138 }
139 }
140 reg.install(lua)?;
141 Ok(())
142 })
143 .await
144 .map_err(|e| format!("eval_simple VM spawn failed: {e}"))?;
145
146 let task = tmp_isle.spawn_exec(move |lua| {
147 let result: mlua::Value = lua
148 .load(&code)
149 .eval()
150 .map_err(|e| IsleError::Lua(e.to_string()))?;
151 let json: serde_json::Value = lua
152 .from_value(result)
153 .map_err(|e| IsleError::Lua(e.to_string()))?;
154 serde_json::to_string(&json).map_err(|e| IsleError::Lua(format!("JSON serialize: {e}")))
155 });
156
157 let json_str = task.await.map_err(|e| e.to_string())?;
158 serde_json::from_str(&json_str).map_err(|e| format!("JSON parse: {e}"))
159 }
160
161 pub async fn start_session(
171 &self,
172 code: String,
173 ctx: serde_json::Value,
174 extra_lib_paths: Vec<PathBuf>,
175 ) -> Result<Session, String> {
176 let spec = ExecutionSpec::new(code, ctx);
177 let metrics = ExecutionMetrics::new();
178
179 if let Some(budget) = Budget::from_ctx(&spec.ctx) {
181 metrics.set_budget(budget);
182 }
183
184 let (llm_tx, llm_rx) = tokio::sync::mpsc::channel::<LlmRequest>(16);
185
186 let mut effective = extra_lib_paths;
189 effective.extend(self.lib_paths.iter().cloned());
190
191 let bridge_config = bridge::BridgeConfig {
192 llm_tx: Some(llm_tx),
193 ns: spec.namespace.clone(),
194 custom_metrics: metrics.custom_metrics_handle(),
195 budget: metrics.budget_handle(),
196 progress: metrics.progress_handle(),
197 lib_paths: effective.clone(), };
199 let lua_ctx = spec.ctx.clone();
200 let lua_code = spec.code.clone();
201
202 let (session_isle, session_driver) = AsyncIsle::spawn(move |lua| {
204 let mut reg = Registry::new();
205 for path in &effective {
206 if let Some(resolver) = make_resolver(path) {
207 reg.add(resolver);
208 }
209 }
210 reg.install(lua)?;
211 Ok(())
212 })
213 .await
214 .map_err(|e| format!("Session VM spawn failed: {e}"))?;
215
216 session_isle
219 .exec(move |lua| {
220 let alc_table = lua.create_table()?;
221 bridge::register(lua, &alc_table, bridge_config)?;
222 lua.globals().set("alc", alc_table)?;
223
224 let ctx_value = lua.to_value(&lua_ctx)?;
225 lua.globals().set("ctx", ctx_value)?;
226
227 lua.load(PRELUDE)
228 .exec()
229 .map_err(|e| IsleError::Lua(format!("Prelude load failed: {e}")))?;
230
231 Ok("ok".to_string())
234 })
235 .await
236 .map_err(|e| format!("Session setup failed: {e}"))?;
237
238 let wrapped_code = format!("return alc.json_encode((function()\n{lua_code}\nend)())");
240 let exec_task = session_isle.spawn_coroutine_eval(&wrapped_code);
241
242 drop(session_isle);
245
246 Ok(Session::new(llm_rx, exec_task, metrics, session_driver))
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use std::fs;
254
255 fn make_pkg_dir(parent: &std::path::Path, pkg_name: &str, init_lua: &str) -> PathBuf {
257 let pkg_dir = parent.join(pkg_name);
258 fs::create_dir_all(&pkg_dir).unwrap();
259 fs::write(pkg_dir.join("init.lua"), init_lua).unwrap();
260 parent.to_path_buf()
261 }
262
263 #[tokio::test]
265 async fn no_extra_lib_paths_eval_simple() {
266 let executor = Executor::new(vec![]).await.unwrap();
267 let result = executor.eval_simple("return 42".to_string()).await.unwrap();
268 assert_eq!(result, serde_json::json!(42));
269 }
270
271 #[tokio::test]
276 async fn extra_lib_paths_reachable_via_eval_simple_with_paths() {
277 let tmp = tempfile::tempdir().unwrap();
278 let pkg_root = make_pkg_dir(tmp.path(), "test_pkg", "return { value = 99 }");
279
280 let executor = Executor::new(vec![]).await.unwrap();
281 let code = r#"
282 local pkg = require("test_pkg")
283 return pkg.value
284 "#
285 .to_string();
286
287 let result = executor
288 .eval_simple_with_paths(code, vec![pkg_root])
289 .await
290 .unwrap();
291
292 assert_eq!(result, serde_json::json!(99));
293 }
294
295 #[tokio::test]
298 async fn extra_lib_paths_priority_over_default() {
299 let global_tmp = tempfile::tempdir().unwrap();
300 let extra_tmp = tempfile::tempdir().unwrap();
301
302 make_pkg_dir(global_tmp.path(), "test_pkg", "return { value = 1 }");
304 let extra_root = make_pkg_dir(extra_tmp.path(), "test_pkg", "return { value = 2 }");
306
307 let executor = Executor::new(vec![global_tmp.path().to_path_buf()])
309 .await
310 .unwrap();
311
312 let code = r#"
313 local pkg = require("test_pkg")
314 return pkg.value
315 "#
316 .to_string();
317
318 let result = executor
319 .eval_simple_with_paths(code, vec![extra_root])
320 .await
321 .unwrap();
322
323 assert_eq!(result, serde_json::json!(2));
325 }
326}