Skip to main content

langshell_deno/
lib.rs

1use deno_ast::{MediaType, ParseParams, SourceMapOption};
2use deno_core::{Extension, JsRuntime, OpState, PollEventLoopOptions, RuntimeOptions, op2, v8};
3use deno_error::JsErrorBox;
4use ic_auth_types::deterministic_cbor_into;
5use langshell_core::{
6    CallStatus, Diagnostic, ErrorObject, ExternalCallRecord, Language, LanguageRuntime, Metrics,
7    RunRequest, RunResult, RunStatus, RuntimeFuture, SessionId, SessionLimits, ToolCallContext,
8    ToolRegistry, digest_bytes, digest_json, truncate_utf8,
9};
10use serde::{Deserialize, Serialize};
11use serde_json::{Map, Value, json};
12use std::{
13    cell::RefCell,
14    collections::HashMap,
15    fmt,
16    rc::Rc,
17    sync::{
18        Arc, Condvar, Mutex as StdMutex,
19        atomic::{AtomicBool, Ordering},
20    },
21    thread::JoinHandle,
22    time::{Duration, Instant},
23};
24use tokio::sync::{mpsc, oneshot};
25
26pub const DENO_SNAPSHOT_MAGIC: &str = "langshell-deno-snapshot/v1";
27
28#[derive(Clone)]
29pub struct DenoRuntime {
30    inner: Arc<DenoRuntimeInner>,
31}
32
33struct DenoRuntimeInner {
34    tx: mpsc::UnboundedSender<DenoCommand>,
35    join: StdMutex<Option<JoinHandle<()>>>,
36}
37
38impl Drop for DenoRuntimeInner {
39    fn drop(&mut self) {
40        let (reply, rx) = std::sync::mpsc::channel();
41        let _ = self.tx.send(DenoCommand::Shutdown { reply });
42        let _ = rx.recv_timeout(Duration::from_secs(5));
43        if let Some(join) = self.join.lock().expect("deno worker join lock").take() {
44            let _ = join.join();
45        }
46    }
47}
48
49impl fmt::Debug for DenoRuntime {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        f.debug_struct("DenoRuntime").finish_non_exhaustive()
52    }
53}
54
55impl DenoRuntime {
56    pub fn new(registry: ToolRegistry, default_limits: SessionLimits) -> Self {
57        let (tx, rx) = mpsc::unbounded_channel();
58        let join = std::thread::Builder::new()
59            .name("langshell-deno".to_owned())
60            .spawn(move || run_worker_thread(rx, registry, default_limits))
61            .expect("failed to spawn langshell-deno worker thread");
62        Self {
63            inner: Arc::new(DenoRuntimeInner {
64                tx,
65                join: StdMutex::new(Some(join)),
66            }),
67        }
68    }
69
70    pub async fn create_session(
71        &self,
72        session_id: SessionId,
73        limits: Option<SessionLimits>,
74    ) -> Result<(), ErrorObject> {
75        let (reply, rx) = oneshot::channel();
76        self.send(DenoCommand::CreateSession {
77            session_id,
78            limits,
79            reply,
80        })?;
81        rx.await.unwrap_or_else(|_| Err(worker_closed_error()))
82    }
83
84    pub async fn run(&self, request: RunRequest) -> RunResult {
85        let (reply, rx) = oneshot::channel();
86        if let Err(error) = self.send(DenoCommand::Run { request, reply }) {
87            return runtime_error_result(error);
88        }
89        rx.await
90            .unwrap_or_else(|_| runtime_error_result(worker_closed_error()))
91    }
92
93    pub async fn destroy_session(&self, session_id: &SessionId) -> Result<bool, ErrorObject> {
94        let (reply, rx) = oneshot::channel();
95        self.send(DenoCommand::DestroySession {
96            session_id: session_id.clone(),
97            reply,
98        })?;
99        rx.await.map_err(|_| worker_closed_error())
100    }
101
102    pub async fn list_sessions(&self) -> Result<Vec<SessionId>, ErrorObject> {
103        let (reply, rx) = oneshot::channel();
104        self.send(DenoCommand::ListSessions { reply })?;
105        rx.await.map_err(|_| worker_closed_error())
106    }
107
108    pub async fn snapshot_session(&self, session_id: &SessionId) -> Result<Vec<u8>, ErrorObject> {
109        let (reply, rx) = oneshot::channel();
110        self.send(DenoCommand::SnapshotSession {
111            session_id: session_id.clone(),
112            reply,
113        })?;
114        rx.await.unwrap_or_else(|_| Err(worker_closed_error()))
115    }
116
117    pub async fn restore_session(
118        &self,
119        snapshot: &[u8],
120        session_id: Option<SessionId>,
121    ) -> Result<SessionId, ErrorObject> {
122        let (reply, rx) = oneshot::channel();
123        self.send(DenoCommand::RestoreSession {
124            snapshot: snapshot.to_vec(),
125            session_id,
126            reply,
127        })?;
128        rx.await.unwrap_or_else(|_| Err(worker_closed_error()))
129    }
130
131    fn send(&self, command: DenoCommand) -> Result<(), ErrorObject> {
132        self.inner
133            .tx
134            .send(command)
135            .map_err(|_| worker_closed_error())
136    }
137}
138
139impl LanguageRuntime for DenoRuntime {
140    fn language(&self) -> Language {
141        Language::TypeScript
142    }
143
144    fn create_session(
145        &self,
146        session_id: SessionId,
147        limits: Option<SessionLimits>,
148    ) -> RuntimeFuture<'_, Result<(), ErrorObject>> {
149        Box::pin(async move { DenoRuntime::create_session(self, session_id, limits).await })
150    }
151
152    fn run(&self, request: RunRequest) -> RuntimeFuture<'_, RunResult> {
153        Box::pin(async move { DenoRuntime::run(self, request).await })
154    }
155
156    fn destroy_session(
157        &self,
158        session_id: SessionId,
159    ) -> RuntimeFuture<'_, Result<bool, ErrorObject>> {
160        Box::pin(async move { DenoRuntime::destroy_session(self, &session_id).await })
161    }
162
163    fn list_sessions(&self) -> RuntimeFuture<'_, Result<Vec<SessionId>, ErrorObject>> {
164        Box::pin(async move { DenoRuntime::list_sessions(self).await })
165    }
166
167    fn snapshot_session(
168        &self,
169        session_id: SessionId,
170    ) -> RuntimeFuture<'_, Result<Vec<u8>, ErrorObject>> {
171        Box::pin(async move { DenoRuntime::snapshot_session(self, &session_id).await })
172    }
173
174    fn restore_session(
175        &self,
176        snapshot: Vec<u8>,
177        session_id: Option<SessionId>,
178    ) -> RuntimeFuture<'_, Result<SessionId, ErrorObject>> {
179        Box::pin(async move { DenoRuntime::restore_session(self, &snapshot, session_id).await })
180    }
181
182    fn can_restore_snapshot(&self, snapshot: &[u8]) -> bool {
183        is_deno_snapshot(snapshot)
184    }
185}
186
187pub fn is_deno_snapshot(snapshot: &[u8]) -> bool {
188    #[derive(Deserialize)]
189    struct Peek {
190        magic: String,
191    }
192    ciborium::from_reader::<Peek, _>(snapshot)
193        .ok()
194        .map(|peek| peek.magic == DENO_SNAPSHOT_MAGIC)
195        .unwrap_or(false)
196}
197
198enum DenoCommand {
199    CreateSession {
200        session_id: SessionId,
201        limits: Option<SessionLimits>,
202        reply: oneshot::Sender<Result<(), ErrorObject>>,
203    },
204    Run {
205        request: RunRequest,
206        reply: oneshot::Sender<RunResult>,
207    },
208    DestroySession {
209        session_id: SessionId,
210        reply: oneshot::Sender<bool>,
211    },
212    ListSessions {
213        reply: oneshot::Sender<Vec<SessionId>>,
214    },
215    SnapshotSession {
216        session_id: SessionId,
217        reply: oneshot::Sender<Result<Vec<u8>, ErrorObject>>,
218    },
219    RestoreSession {
220        snapshot: Vec<u8>,
221        session_id: Option<SessionId>,
222        reply: oneshot::Sender<Result<SessionId, ErrorObject>>,
223    },
224    Shutdown {
225        reply: std::sync::mpsc::Sender<()>,
226    },
227}
228
229fn run_worker_thread(
230    mut rx: mpsc::UnboundedReceiver<DenoCommand>,
231    registry: ToolRegistry,
232    default_limits: SessionLimits,
233) {
234    let runtime = tokio::runtime::Builder::new_current_thread()
235        .enable_all()
236        .build()
237        .expect("failed to build langshell-deno tokio runtime");
238    runtime.block_on(async move {
239        let mut worker = DenoWorker::new(registry, default_limits);
240        while let Some(command) = rx.recv().await {
241            if let DenoCommand::Shutdown { reply } = command {
242                let _ = reply.send(());
243                break;
244            }
245            worker.handle(command).await;
246        }
247    });
248}
249
250struct DenoWorker {
251    sessions: HashMap<String, DenoSession>,
252    registry: ToolRegistry,
253    default_limits: SessionLimits,
254}
255
256impl DenoWorker {
257    fn new(registry: ToolRegistry, default_limits: SessionLimits) -> Self {
258        Self {
259            sessions: HashMap::new(),
260            registry,
261            default_limits,
262        }
263    }
264
265    async fn handle(&mut self, command: DenoCommand) {
266        match command {
267            DenoCommand::CreateSession {
268                session_id,
269                limits,
270                reply,
271            } => {
272                let result = self.create_session(session_id, limits);
273                let _ = reply.send(result);
274            }
275            DenoCommand::Run { request, reply } => {
276                let result = self.run(request).await;
277                let _ = reply.send(result);
278            }
279            DenoCommand::DestroySession { session_id, reply } => {
280                let _ = reply.send(self.sessions.remove(&session_id.0).is_some());
281            }
282            DenoCommand::ListSessions { reply } => {
283                let mut ids: Vec<_> = self.sessions.keys().cloned().map(SessionId).collect();
284                ids.sort_by(|a, b| a.0.cmp(&b.0));
285                let _ = reply.send(ids);
286            }
287            DenoCommand::SnapshotSession { session_id, reply } => {
288                let result = self.snapshot_session(&session_id);
289                let _ = reply.send(result);
290            }
291            DenoCommand::RestoreSession {
292                snapshot,
293                session_id,
294                reply,
295            } => {
296                let result = self.restore_session(&snapshot, session_id);
297                let _ = reply.send(result);
298            }
299            DenoCommand::Shutdown { reply } => {
300                let _ = reply.send(());
301            }
302        }
303    }
304
305    fn create_session(
306        &mut self,
307        session_id: SessionId,
308        limits: Option<SessionLimits>,
309    ) -> Result<(), ErrorObject> {
310        if !self.sessions.contains_key(&session_id.0) {
311            let limits = limits.unwrap_or_else(|| self.default_limits.clone());
312            let session = DenoSession::new(session_id.clone(), limits, &self.registry)?;
313            self.sessions.insert(session_id.0, session);
314        }
315        Ok(())
316    }
317
318    async fn run(&mut self, request: RunRequest) -> RunResult {
319        if request.language != Language::TypeScript {
320            return RunResult::error(
321                RunStatus::ValidationError,
322                ErrorObject::new(
323                    "UNSUPPORTED_FEATURE",
324                    "The Deno backend only executes TypeScript.",
325                ),
326                String::new(),
327                Metrics::default(),
328            );
329        }
330        if request.validate_only {
331            return validate_request(&request, &self.registry);
332        }
333
334        let limits = effective_limits(&self.default_limits, &request);
335        let session_id = request.session_id.clone();
336        let mut session = match self.sessions.remove(&session_id.0) {
337            Some(mut session) => {
338                session.limits = limits;
339                session
340            }
341            None => match DenoSession::new(session_id.clone(), limits, &self.registry) {
342                Ok(session) => session,
343                Err(error) => {
344                    return RunResult::error(
345                        RunStatus::RuntimeError,
346                        error,
347                        String::new(),
348                        Metrics::default(),
349                    );
350                }
351            },
352        };
353
354        let result = session.run(request, &self.registry).await;
355        self.sessions.insert(session_id.0, session);
356        result
357    }
358
359    fn snapshot_session(&mut self, session_id: &SessionId) -> Result<Vec<u8>, ErrorObject> {
360        let session = self.sessions.get_mut(&session_id.0).ok_or_else(|| {
361            ErrorObject::new(
362                "SESSION_NOT_FOUND",
363                format!("TypeScript session {} does not exist.", session_id.0),
364            )
365        })?;
366        let snapshot = SnapshotEnvelope {
367            magic: DENO_SNAPSHOT_MAGIC.to_owned(),
368            version: langshell_core::SNAPSHOT_VERSION,
369            session_id: session_id.0.clone(),
370            limits: session.limits.clone(),
371            globals: session.snapshot_globals()?,
372            capability_digest: capability_digest(&self.registry),
373        };
374        let mut buf = Vec::with_capacity(512);
375        deterministic_cbor_into(&snapshot, &mut buf).map_err(|err| {
376            ErrorObject::new(
377                "SNAPSHOT_CORRUPT",
378                format!("Failed to serialize Deno snapshot: {err}"),
379            )
380        })?;
381        Ok(buf)
382    }
383
384    fn restore_session(
385        &mut self,
386        snapshot: &[u8],
387        session_id: Option<SessionId>,
388    ) -> Result<SessionId, ErrorObject> {
389        let snapshot: SnapshotEnvelope = ciborium::from_reader(snapshot).map_err(|err| {
390            ErrorObject::new("SNAPSHOT_CORRUPT", format!("Invalid Deno snapshot: {err}"))
391        })?;
392        if snapshot.magic != DENO_SNAPSHOT_MAGIC {
393            return Err(ErrorObject::new(
394                "SNAPSHOT_CORRUPT",
395                "Deno snapshot magic mismatch.",
396            ));
397        }
398        if snapshot.version != langshell_core::SNAPSHOT_VERSION {
399            return Err(ErrorObject::new(
400                "SNAPSHOT_VERSION_MISMATCH",
401                format!("Snapshot version {} is not supported.", snapshot.version),
402            ));
403        }
404        if snapshot.capability_digest != capability_digest(&self.registry) {
405            return Err(ErrorObject::new(
406                "SNAPSHOT_CAPABILITY_MISMATCH",
407                "Snapshot was created with a different capability set.",
408            ));
409        }
410
411        let id = session_id.unwrap_or(SessionId(snapshot.session_id));
412        let mut session = DenoSession::new(id.clone(), snapshot.limits, &self.registry)?;
413        session.restore_globals(snapshot.globals)?;
414        self.sessions.insert(id.0.clone(), session);
415        Ok(id)
416    }
417}
418
419struct DenoSession {
420    id: SessionId,
421    limits: SessionLimits,
422    runtime: JsRuntime,
423}
424
425impl DenoSession {
426    fn new(
427        id: SessionId,
428        limits: SessionLimits,
429        registry: &ToolRegistry,
430    ) -> Result<Self, ErrorObject> {
431        let heap_max = (limits.memory_mb as usize)
432            .saturating_mul(1024 * 1024)
433            .max(1);
434        let create_params = v8::Isolate::create_params().heap_limits(0, heap_max);
435        let mut runtime = JsRuntime::try_new(RuntimeOptions {
436            extensions: vec![langshell_extension(registry.clone(), limits.clone())],
437            create_params: Some(create_params),
438            ..Default::default()
439        })
440        .map_err(|err| ErrorObject::new("RUNTIME_ERROR", err.to_string()))?;
441
442        install_tool_globals(&mut runtime, registry)?;
443        Ok(Self {
444            id,
445            limits,
446            runtime,
447        })
448    }
449
450    async fn run(&mut self, request: RunRequest, registry: &ToolRegistry) -> RunResult {
451        let started = Instant::now();
452        if let Some(error) = static_validation_error(&request.code, registry) {
453            return RunResult::error(
454                code_to_status(&error.code, true),
455                error,
456                String::new(),
457                metrics(started, 0),
458            );
459        }
460
461        let js = match transpile_typescript(&request.code) {
462            Ok(js) => js,
463            Err(error) => {
464                return RunResult::error(
465                    code_to_status(&error.code, true),
466                    error,
467                    String::new(),
468                    metrics(started, 0),
469                );
470            }
471        };
472
473        self.reset_run_state(registry);
474        if let Err(error) = self.inject_inputs(&request.inputs) {
475            return RunResult::error(
476                RunStatus::ValidationError,
477                error,
478                String::new(),
479                metrics(started, 0),
480            );
481        }
482
483        let wrapped = wrap_user_code(&js);
484        let run_error = self
485            .execute_user_code(
486                &wrapped,
487                Duration::from_millis(u64::from(request.timeout_ms.unwrap_or(self.limits.wall_ms))),
488            )
489            .await
490            .err();
491        let state = self.run_state_snapshot();
492        if let Some(error) = run_error.or(state.error.clone()) {
493            let mut result = RunResult::error(
494                code_to_status(&error.code, false),
495                error,
496                String::new(),
497                metrics(started, state.records.len() as u32),
498            );
499            apply_streams(&mut result, state, &self.limits);
500            return result;
501        }
502
503        let result_value = match self.read_result() {
504            Ok(value) => value,
505            Err(error) => {
506                let mut result = RunResult::error(
507                    RunStatus::ValidationError,
508                    error,
509                    String::new(),
510                    metrics(started, state.records.len() as u32),
511                );
512                apply_streams(&mut result, state, &self.limits);
513                return result;
514            }
515        };
516
517        let mut result = RunResult::ok(
518            result_value,
519            String::new(),
520            metrics(started, state.records.len() as u32),
521        );
522        apply_streams(&mut result, state, &self.limits);
523        if request.return_snapshot {
524            result.snapshot_id = Some(format!("snap_{}", digest_bytes(self.id.0.as_bytes())));
525        }
526        result
527    }
528
529    fn reset_run_state(&mut self, registry: &ToolRegistry) {
530        let op_state = self.runtime.op_state();
531        let mut state = op_state.borrow_mut();
532        let data = state.borrow_mut::<DenoOpState>();
533        data.registry = registry.clone();
534        data.limits = self.limits.clone();
535        data.stdout.clear();
536        data.stderr.clear();
537        data.records.clear();
538        data.started_calls = 0;
539        data.error = None;
540    }
541
542    fn run_state_snapshot(&self) -> RunStateSnapshot {
543        let op_state = self.runtime.op_state();
544        let state = op_state.borrow();
545        let data = state.borrow::<DenoOpState>();
546        RunStateSnapshot {
547            stdout: data.stdout.clone(),
548            stderr: data.stderr.clone(),
549            records: data.records.clone(),
550            error: data.error.clone(),
551        }
552    }
553
554    fn inject_inputs(&mut self, inputs: &Map<String, Value>) -> Result<(), ErrorObject> {
555        if inputs.is_empty() {
556            return Ok(());
557        }
558        let inputs = serde_json::to_string(inputs).map_err(|err| {
559            ErrorObject::new("INVALID_ARGUMENT", format!("inputs are not JSON: {err}"))
560        })?;
561        execute_script_unit(
562            &mut self.runtime,
563            "<langshell-inputs>",
564            format!("globalThis.__langshell_restore_globals({inputs});"),
565        )
566    }
567
568    async fn execute_user_code(
569        &mut self,
570        code: &str,
571        timeout: Duration,
572    ) -> Result<(), ErrorObject> {
573        let timed_out = Arc::new(AtomicBool::new(false));
574        let timer_done = Arc::new((StdMutex::new(false), Condvar::new()));
575        let handle = self.runtime.v8_isolate().thread_safe_handle();
576        let timer_timed_out = timed_out.clone();
577        let timer_done_thread = timer_done.clone();
578        let timer = std::thread::spawn(move || {
579            let (lock, cvar) = &*timer_done_thread;
580            let done = lock.lock().expect("timer mutex poisoned");
581            let (done, wait) = cvar
582                .wait_timeout_while(done, timeout, |done| !*done)
583                .expect("timer condvar poisoned");
584            if !*done && wait.timed_out() {
585                timer_timed_out.store(true, Ordering::SeqCst);
586                handle.terminate_execution();
587            }
588        });
589
590        let value = match self
591            .runtime
592            .execute_script("<langshell-run>", code.to_owned())
593        {
594            Ok(value) => value,
595            Err(err) => {
596                stop_timer(timer_done, timer);
597                if timed_out.load(Ordering::SeqCst) {
598                    self.runtime.v8_isolate().cancel_terminate_execution();
599                    return Err(timeout_error());
600                }
601                return Err(error_from_js(err.to_string(), false));
602            }
603        };
604
605        let resolve = self.runtime.resolve(value);
606        let resolved = tokio::time::timeout(
607            timeout,
608            self.runtime
609                .with_event_loop_promise(resolve, PollEventLoopOptions::default()),
610        )
611        .await;
612        stop_timer(timer_done, timer);
613        if timed_out.load(Ordering::SeqCst) {
614            self.runtime.v8_isolate().cancel_terminate_execution();
615            return Err(timeout_error());
616        }
617        match resolved {
618            Ok(Ok(_)) => Ok(()),
619            Ok(Err(err)) => Err(error_from_js(err.to_string(), false)),
620            Err(_) => {
621                self.runtime.v8_isolate().terminate_execution();
622                self.runtime.v8_isolate().cancel_terminate_execution();
623                Err(timeout_error())
624            }
625        }
626    }
627
628    fn read_result(&mut self) -> Result<Option<Value>, ErrorObject> {
629        let value = self
630            .runtime
631            .execute_script("<langshell-result>", "globalThis.result")
632            .map_err(|err| error_from_js(err.to_string(), false))?;
633        v8_to_json(&mut self.runtime, value)
634    }
635
636    fn snapshot_globals(&mut self) -> Result<Value, ErrorObject> {
637        let value = self
638            .runtime
639            .execute_script(
640                "<langshell-snapshot>",
641                "JSON.stringify(globalThis.__langshell_snapshot_globals())",
642            )
643            .map_err(|err| ErrorObject::new("SNAPSHOT_CORRUPT", err.to_string()))?;
644        let globals = v8_to_string(&mut self.runtime, value)?;
645        serde_json::from_str(&globals).map_err(|err| {
646            ErrorObject::new(
647                "SNAPSHOT_CORRUPT",
648                format!("Deno snapshot globals did not produce valid JSON: {err}"),
649            )
650        })
651    }
652
653    fn restore_globals(&mut self, globals: Value) -> Result<(), ErrorObject> {
654        let globals = serde_json::to_string(&globals).map_err(|err| {
655            ErrorObject::new("SNAPSHOT_CORRUPT", format!("Invalid globals: {err}"))
656        })?;
657        execute_script_unit(
658            &mut self.runtime,
659            "<langshell-restore>",
660            format!("globalThis.__langshell_restore_globals({globals});"),
661        )
662        .map_err(|err| ErrorObject::new("SNAPSHOT_CORRUPT", err.message))
663    }
664}
665
666fn stop_timer(timer_done: Arc<(StdMutex<bool>, Condvar)>, timer: std::thread::JoinHandle<()>) {
667    let (lock, cvar) = &*timer_done;
668    if let Ok(mut done) = lock.lock() {
669        *done = true;
670        cvar.notify_one();
671    }
672    let _ = timer.join();
673}
674
675#[derive(Debug, Serialize, Deserialize)]
676struct SnapshotEnvelope {
677    magic: String,
678    version: u32,
679    session_id: String,
680    limits: SessionLimits,
681    globals: Value,
682    capability_digest: String,
683}
684
685struct DenoOpState {
686    registry: ToolRegistry,
687    limits: SessionLimits,
688    stdout: String,
689    stderr: String,
690    records: Vec<ExternalCallRecord>,
691    started_calls: u32,
692    error: Option<ErrorObject>,
693}
694
695impl DenoOpState {
696    fn new(registry: ToolRegistry, limits: SessionLimits) -> Self {
697        Self {
698            registry,
699            limits,
700            stdout: String::new(),
701            stderr: String::new(),
702            records: Vec::new(),
703            started_calls: 0,
704            error: None,
705        }
706    }
707}
708
709struct RunStateSnapshot {
710    stdout: String,
711    stderr: String,
712    records: Vec<ExternalCallRecord>,
713    error: Option<ErrorObject>,
714}
715
716#[op2(fast)]
717pub fn op_print(state: &mut OpState, #[string] msg: &str, is_err: bool) {
718    let data = state.borrow_mut::<DenoOpState>();
719    if is_err {
720        data.stderr.push_str(msg);
721    } else {
722        data.stdout.push_str(msg);
723    }
724}
725
726#[op2]
727#[serde]
728fn op_langshell_call_tool_sync(
729    state: &mut OpState,
730    #[string] name: String,
731    #[serde] args: Vec<serde_json::Value>,
732    #[serde] kwargs: serde_json::Map<String, serde_json::Value>,
733) -> Result<serde_json::Value, JsErrorBox> {
734    let (tool, ctx) = prepare_tool_call(state, name, args, kwargs)?;
735    if tool.async_mode {
736        let error = ErrorObject::new(
737            "TYPE_ERROR",
738            format!(
739                "Tool {} is asynchronous; call it with await.",
740                tool.capability.name
741            ),
742        );
743        state.borrow_mut::<DenoOpState>().error = Some(error.clone());
744        return Err(error_object_to_js(error));
745    }
746    let outcome = futures::executor::block_on(run_tool(tool, ctx));
747    finish_tool_call(state, outcome)
748}
749
750#[op2(async(lazy))]
751#[serde]
752async fn op_langshell_call_tool_async(
753    state: Rc<RefCell<OpState>>,
754    #[string] name: String,
755    #[serde] args: Vec<serde_json::Value>,
756    #[serde] kwargs: serde_json::Map<String, serde_json::Value>,
757) -> Result<serde_json::Value, JsErrorBox> {
758    let (tool, ctx) = {
759        let mut state = state.borrow_mut();
760        prepare_tool_call(&mut state, name, args, kwargs)?
761    };
762    let outcome = run_tool(tool, ctx).await;
763    let mut state = state.borrow_mut();
764    finish_tool_call(&mut state, outcome)
765}
766
767fn langshell_extension(registry: ToolRegistry, limits: SessionLimits) -> Extension {
768    Extension {
769        name: "langshell_deno",
770        ops: std::borrow::Cow::Owned(vec![
771            op_langshell_call_tool_sync(),
772            op_langshell_call_tool_async(),
773        ]),
774        middleware_fn: Some(Box::new(|op| match op.name {
775            "op_print" => op_print(),
776            _ => op,
777        })),
778        op_state_fn: Some(Box::new(move |state| {
779            state.put(DenoOpState::new(registry, limits));
780        })),
781        ..Default::default()
782    }
783}
784
785fn install_tool_globals(
786    runtime: &mut JsRuntime,
787    registry: &ToolRegistry,
788) -> Result<(), ErrorObject> {
789    let tools: Vec<_> = registry
790        .names()
791        .into_iter()
792        .filter_map(|name| {
793            registry.get(&name).map(|tool| {
794                json!({
795                    "name": name,
796                    "asyncMode": tool.async_mode,
797                })
798            })
799        })
800        .collect();
801    let tools = serde_json::to_string(&tools)
802        .map_err(|err| ErrorObject::new("SERIALIZE_ERROR", format!("tool metadata: {err}")))?;
803    let script = format!(
804        r#"
805const __langshellToolDefs = {tools};
806const __langshellOps = Deno.core.ops;
807Object.defineProperty(globalThis, "__langshell_ops", {{ value: __langshellOps, configurable: false }});
808Object.defineProperty(globalThis, "LangShell", {{
809  value: Object.freeze({{
810    callTool: (name, args = [], kwargs = {{}}) => __langshellOps.op_langshell_call_tool_async(name, args, kwargs),
811    callToolSync: (name, args = [], kwargs = {{}}) => __langshellOps.op_langshell_call_tool_sync(name, args, kwargs),
812  }}),
813  configurable: true,
814}});
815for (const tool of __langshellToolDefs) {{
816  const call = tool.asyncMode
817    ? (...args) => __langshellOps.op_langshell_call_tool_async(tool.name, args, {{}})
818    : (...args) => __langshellOps.op_langshell_call_tool_sync(tool.name, args, {{}});
819  Object.defineProperty(globalThis, tool.name, {{ value: call, writable: false, configurable: true }});
820}}
821Object.defineProperty(globalThis, "__langshell_tag_value", {{
822  value: function tag(value, seen) {{
823    if (value === null) return null;
824    const t = typeof value;
825    if (t === "bigint") return {{ __lang: "bigint", v: String(value) }};
826    if (t === "function" || t === "symbol" || t === "undefined") return undefined;
827    if (t !== "object") return value;
828    if (seen.has(value)) throw new TypeError("cyclic value cannot be snapshotted");
829    seen.add(value);
830    if (value instanceof Date) return {{ __lang: "date", v: value.toISOString() }};
831    if (value instanceof Map) {{
832      const entries = [];
833      for (const [k, v] of value.entries()) {{
834        const tk = tag(k, seen);
835        const tv = tag(v, seen);
836        if (tk === undefined || tv === undefined) continue;
837        entries.push([tk, tv]);
838      }}
839      return {{ __lang: "map", v: entries }};
840    }}
841    if (value instanceof Set) {{
842      const items = [];
843      for (const v of value.values()) {{
844        const tv = tag(v, seen);
845        if (tv !== undefined) items.push(tv);
846      }}
847      return {{ __lang: "set", v: items }};
848    }}
849    if (value instanceof Uint8Array) {{
850      return {{ __lang: "uint8array", v: Array.from(value) }};
851    }}
852    if (Array.isArray(value)) {{
853      return value.map((item) => {{
854        const tv = tag(item, seen);
855        return tv === undefined ? null : tv;
856      }});
857    }}
858    const out = {{}};
859    for (const [k, v] of Object.entries(value)) {{
860      const tv = tag(v, seen);
861      if (tv !== undefined) out[k] = tv;
862    }}
863    return out;
864  }},
865  configurable: false,
866}});
867Object.defineProperty(globalThis, "__langshell_untag_value", {{
868  value: function untag(value) {{
869    if (value === null || typeof value !== "object") return value;
870    if (Array.isArray(value)) return value.map(untag);
871    const tag = value.__lang;
872    if (tag === "bigint") return BigInt(value.v);
873    if (tag === "date") return new Date(value.v);
874    if (tag === "uint8array") return new Uint8Array(value.v);
875    if (tag === "map") return new Map(value.v.map(([k, v]) => [untag(k), untag(v)]));
876    if (tag === "set") return new Set(value.v.map(untag));
877    const out = {{}};
878    for (const [k, v] of Object.entries(value)) out[k] = untag(v);
879    return out;
880  }},
881  configurable: false,
882}});
883Object.defineProperty(globalThis, "__langshell_restore_globals", {{
884  value: (globals) => {{
885    for (const [key, value] of Object.entries(globals ?? {{}})) {{
886      const restored = globalThis.__langshell_untag_value(value);
887      Object.defineProperty(globalThis, key, {{ value: restored, writable: true, configurable: true }});
888    }}
889  }},
890  configurable: false,
891}});
892Object.defineProperty(globalThis, "__langshell_snapshot_globals", {{
893  value: () => {{
894    const out = {{}};
895    for (const key of Object.getOwnPropertyNames(globalThis)) {{
896      if (globalThis.__langshell_baseline.has(key) || key.startsWith("__langshell")) continue;
897      const value = globalThis[key];
898      if (typeof value === "function" || typeof value === "symbol" || typeof value === "undefined") continue;
899      try {{
900        const tagged = globalThis.__langshell_tag_value(value, new WeakSet());
901        if (tagged !== undefined) out[key] = tagged;
902      }} catch (_) {{}}
903    }}
904    return out;
905  }},
906  configurable: false,
907}});
908Object.defineProperty(globalThis, "__langshell_baseline", {{
909  value: new Set(Object.getOwnPropertyNames(globalThis)),
910  configurable: false,
911}});
912try {{
913  Object.defineProperty(globalThis, "Deno", {{ value: undefined, writable: false, configurable: true }});
914}} catch (_) {{}}
915"#
916    );
917    execute_script_unit(runtime, "<langshell-bootstrap>", script)
918}
919
920fn prepare_tool_call(
921    state: &mut OpState,
922    name: String,
923    args: Vec<Value>,
924    kwargs: Map<String, Value>,
925) -> Result<(langshell_core::RegisteredTool, ToolCallContext), JsErrorBox> {
926    let data = state.borrow_mut::<DenoOpState>();
927    data.started_calls = data.started_calls.saturating_add(1);
928    if data.started_calls > data.limits.max_external_calls {
929        let error = ErrorObject::new(
930            "EXTERNAL_CALLS_EXCEEDED",
931            format!(
932                "External call limit {} exceeded.",
933                data.limits.max_external_calls
934            ),
935        );
936        data.error = Some(error.clone());
937        return Err(error_object_to_js(error));
938    }
939    let Some(tool) = data.registry.get(&name).cloned() else {
940        let error = ErrorObject::new(
941            "UNKNOWN_TOOL",
942            format!("Function {name} is not registered."),
943        )
944        .with_hint("Call list_tools() or describe_tool() to inspect registered functions.");
945        data.error = Some(error.clone());
946        return Err(error_object_to_js(error));
947    };
948    Ok((tool, ToolCallContext { name, args, kwargs }))
949}
950
951async fn run_tool(
952    tool: langshell_core::RegisteredTool,
953    ctx: ToolCallContext,
954) -> (Result<Value, ErrorObject>, ExternalCallRecord) {
955    let started = Instant::now();
956    let request_digest = digest_json(&json!({"args": ctx.args, "kwargs": ctx.kwargs}));
957    let side_effect = tool.capability.side_effect;
958    let name = tool.capability.name.clone();
959    match tool.call(ctx).await {
960        Ok(value) => {
961            let response_digest = Some(digest_json(&value));
962            (
963                Ok(value),
964                ExternalCallRecord {
965                    name,
966                    side_effect,
967                    duration_ms: elapsed_ms(started),
968                    status: CallStatus::Ok,
969                    request_digest,
970                    response_digest,
971                    error: None,
972                },
973            )
974        }
975        Err(error) => {
976            let error_object = ErrorObject::new(error.code, error.message);
977            (
978                Err(error_object.clone()),
979                ExternalCallRecord {
980                    name,
981                    side_effect,
982                    duration_ms: elapsed_ms(started),
983                    status: CallStatus::Error,
984                    request_digest,
985                    response_digest: None,
986                    error: Some(error_object),
987                },
988            )
989        }
990    }
991}
992
993fn finish_tool_call(
994    state: &mut OpState,
995    outcome: (Result<Value, ErrorObject>, ExternalCallRecord),
996) -> Result<Value, JsErrorBox> {
997    let (result, record) = outcome;
998    let data = state.borrow_mut::<DenoOpState>();
999    data.records.push(record);
1000    match result {
1001        Ok(value) => Ok(value),
1002        Err(error) => {
1003            data.error = Some(error.clone());
1004            Err(error_object_to_js(error))
1005        }
1006    }
1007}
1008
1009fn validate_request(request: &RunRequest, registry: &ToolRegistry) -> RunResult {
1010    let started = Instant::now();
1011    if let Some(error) = static_validation_error(&request.code, registry) {
1012        return RunResult::error(
1013            code_to_status(&error.code, true),
1014            error,
1015            String::new(),
1016            metrics(started, 0),
1017        );
1018    }
1019    match transpile_typescript(&request.code) {
1020        Ok(_) => RunResult::ok(None, String::new(), metrics(started, 0)),
1021        Err(error) => RunResult::error(
1022            code_to_status(&error.code, true),
1023            error,
1024            String::new(),
1025            metrics(started, 0),
1026        ),
1027    }
1028}
1029
1030fn transpile_typescript(code: &str) -> Result<String, ErrorObject> {
1031    let specifier = deno_core::resolve_url("file:///langshell-run.ts")
1032        .map_err(|err| ErrorObject::new("RUNTIME_ERROR", err.to_string()))?;
1033    let parsed = deno_ast::parse_module(ParseParams {
1034        specifier,
1035        text: code.to_owned().into(),
1036        media_type: MediaType::TypeScript,
1037        capture_tokens: false,
1038        scope_analysis: false,
1039        maybe_syntax: None,
1040    })
1041    .map_err(|err| ErrorObject::new("SYNTAX_ERROR", err.to_string()))?;
1042    let transpiled = parsed
1043        .transpile(
1044            &deno_ast::TranspileOptions {
1045                imports_not_used_as_values: deno_ast::ImportsNotUsedAsValues::Remove,
1046                decorators: deno_ast::DecoratorsTranspileOption::Ecma,
1047                ..Default::default()
1048            },
1049            &deno_ast::TranspileModuleOptions { module_kind: None },
1050            &deno_ast::EmitOptions {
1051                source_map: SourceMapOption::None,
1052                inline_sources: false,
1053                ..Default::default()
1054            },
1055        )
1056        .map_err(|err| ErrorObject::new("SYNTAX_ERROR", err.to_string()))?;
1057    Ok(transpiled.into_source().text)
1058}
1059
1060fn wrap_user_code(js: &str) -> String {
1061    format!(
1062        r#"
1063(async () => {{
1064  with (globalThis) {{
1065{js}
1066  }}
1067}})()
1068"#
1069    )
1070}
1071
1072fn v8_to_json(
1073    runtime: &mut JsRuntime,
1074    value: v8::Global<v8::Value>,
1075) -> Result<Option<Value>, ErrorObject> {
1076    deno_core::scope!(scope, runtime);
1077    let local = v8::Local::new(scope, value);
1078    if local.is_undefined() {
1079        return Ok(None);
1080    }
1081    serde_v8::from_v8::<Value>(scope, local)
1082        .map(Some)
1083        .map_err(|err| {
1084            ErrorObject::new(
1085                "RESULT_NOT_SERIALIZABLE",
1086                format!("TypeScript result could not be converted to JSON: {err}"),
1087            )
1088            .with_hint("Assign result to a JSON-compatible value before returning.")
1089        })
1090}
1091
1092fn v8_to_string(
1093    runtime: &mut JsRuntime,
1094    value: v8::Global<v8::Value>,
1095) -> Result<String, ErrorObject> {
1096    deno_core::scope!(scope, runtime);
1097    let local = v8::Local::new(scope, value);
1098    serde_v8::from_v8::<String>(scope, local).map_err(|err| {
1099        ErrorObject::new(
1100            "RESULT_NOT_SERIALIZABLE",
1101            format!("TypeScript value could not be converted to string: {err}"),
1102        )
1103    })
1104}
1105
1106fn execute_script_unit(
1107    runtime: &mut JsRuntime,
1108    name: &'static str,
1109    source: String,
1110) -> Result<(), ErrorObject> {
1111    runtime
1112        .execute_script(name, source)
1113        .map(|_| ())
1114        .map_err(|err| error_from_js(err.to_string(), false))
1115}
1116
1117/// Globals that can break the LangShell sandbox; member access or call is forbidden.
1118const TS_BLOCKED_GLOBALS: &[&str] = &[
1119    "Deno",
1120    "process",
1121    "Bun",
1122    "XMLHttpRequest",
1123    "WebSocket",
1124    "Worker",
1125    "SharedWorker",
1126    "navigator",
1127    "globalThis",
1128];
1129
1130/// Direct callable identifiers that escape the sandbox.
1131const TS_BANNED_CALLABLES: &[&str] = &["eval", "Function", "require", "fetch"];
1132
1133/// Free-functions that look like external capabilities; flagged when not registered.
1134const TS_SUSPICIOUS_NAMES: &[&str] = &["fetch_url", "query_db", "send_email"];
1135
1136fn static_validation_error(code: &str, registry: &ToolRegistry) -> Option<ErrorObject> {
1137    use deno_ast::swc::ast::{
1138        CallExpr, Callee, Expr, ImportDecl, MemberExpr, ModuleDecl, NamedExport, Program,
1139    };
1140    use deno_ast::swc::ecma_visit::{Visit, VisitWith};
1141
1142    let specifier = match deno_core::resolve_url("file:///langshell-validate.ts") {
1143        Ok(s) => s,
1144        Err(_) => return None,
1145    };
1146    let parsed = match deno_ast::parse_module(ParseParams {
1147        specifier,
1148        text: code.to_owned().into(),
1149        media_type: MediaType::TypeScript,
1150        capture_tokens: false,
1151        scope_analysis: false,
1152        maybe_syntax: None,
1153    }) {
1154        Ok(p) => p,
1155        // Let `transpile_typescript` produce a precise SYNTAX_ERROR.
1156        Err(_) => return None,
1157    };
1158
1159    struct V<'r> {
1160        registry: &'r ToolRegistry,
1161        error: Option<ErrorObject>,
1162    }
1163
1164    impl<'r> V<'r> {
1165        fn flag_unsupported(&mut self, what: &str) {
1166            if self.error.is_some() {
1167                return;
1168            }
1169            self.error = Some(
1170                ErrorObject::new(
1171                    "UNSUPPORTED_FEATURE",
1172                    format!(
1173                        "Use of {what:?} is not supported in the LangShell Deno sandbox."
1174                    ),
1175                )
1176                .with_hint(
1177                    "Use a registered capability such as read_text, fetch_json, or list_tools instead.",
1178                ),
1179            );
1180        }
1181
1182        fn flag_unknown_tool(&mut self, name: &str) {
1183            if self.error.is_some() {
1184                return;
1185            }
1186            self.error = Some(
1187                ErrorObject::new(
1188                    "UNKNOWN_TOOL",
1189                    format!("Function {name} is not registered."),
1190                )
1191                .with_hint("Call list_tools() to inspect available capabilities."),
1192            );
1193        }
1194    }
1195
1196    impl<'r> Visit for V<'r> {
1197        fn visit_module_decl(&mut self, node: &ModuleDecl) {
1198            if self.error.is_some() {
1199                return;
1200            }
1201            match node {
1202                ModuleDecl::Import(_)
1203                | ModuleDecl::ExportDecl(_)
1204                | ModuleDecl::ExportNamed(_)
1205                | ModuleDecl::ExportDefaultDecl(_)
1206                | ModuleDecl::ExportDefaultExpr(_)
1207                | ModuleDecl::ExportAll(_) => {
1208                    self.flag_unsupported("import/export");
1209                }
1210                _ => {}
1211            }
1212        }
1213
1214        fn visit_import_decl(&mut self, _node: &ImportDecl) {
1215            self.flag_unsupported("import");
1216        }
1217
1218        fn visit_named_export(&mut self, _node: &NamedExport) {
1219            self.flag_unsupported("export");
1220        }
1221
1222        fn visit_call_expr(&mut self, node: &CallExpr) {
1223            if self.error.is_some() {
1224                return;
1225            }
1226            match &node.callee {
1227                Callee::Import(_) => {
1228                    self.flag_unsupported("dynamic import()");
1229                    return;
1230                }
1231                Callee::Expr(expr) => {
1232                    if let Expr::Ident(ident) = expr.as_ref() {
1233                        let name = ident.sym.as_ref();
1234                        if TS_BANNED_CALLABLES.contains(&name) {
1235                            self.flag_unsupported(&format!("{name}(...)"));
1236                            return;
1237                        }
1238                        if TS_SUSPICIOUS_NAMES.contains(&name) && !self.registry.contains(name) {
1239                            self.flag_unknown_tool(name);
1240                            return;
1241                        }
1242                    }
1243                }
1244                _ => {}
1245            }
1246            node.visit_children_with(self);
1247        }
1248
1249        fn visit_member_expr(&mut self, node: &MemberExpr) {
1250            if self.error.is_some() {
1251                return;
1252            }
1253            if let Expr::Ident(ident) = node.obj.as_ref() {
1254                let name = ident.sym.as_ref();
1255                if TS_BLOCKED_GLOBALS.contains(&name) {
1256                    self.flag_unsupported(name);
1257                    return;
1258                }
1259            }
1260            node.visit_children_with(self);
1261        }
1262    }
1263
1264    let mut v = V {
1265        registry,
1266        error: None,
1267    };
1268    let program = parsed.program();
1269    match program.as_ref() {
1270        Program::Module(module) => module.visit_children_with(&mut v),
1271        Program::Script(script) => script.visit_children_with(&mut v),
1272    }
1273    v.error
1274}
1275
1276fn effective_limits(default_limits: &SessionLimits, request: &RunRequest) -> SessionLimits {
1277    let mut limits = request
1278        .limits
1279        .clone()
1280        .unwrap_or_else(|| default_limits.clone());
1281    if let Some(timeout_ms) = request.timeout_ms {
1282        limits.wall_ms = timeout_ms;
1283    }
1284    limits
1285}
1286
1287fn code_to_status(code: &str, validation: bool) -> RunStatus {
1288    match code {
1289        "PERMISSION_DENIED" => RunStatus::PermissionDenied,
1290        "WAITING_FOR_APPROVAL" => RunStatus::WaitingForApproval,
1291        "TIMEOUT_WALL" | "TIMEOUT_CPU" | "TIMEOUT_TOOL" => RunStatus::Timeout,
1292        "CANCELLED" => RunStatus::Cancelled,
1293        "MEMORY_EXCEEDED" | "STDOUT_EXCEEDED" | "EXTERNAL_CALLS_EXCEEDED" | "STACK_OVERFLOW" => {
1294            RunStatus::ResourceExhausted
1295        }
1296        "SYNTAX_ERROR"
1297        | "TYPE_ERROR"
1298        | "UNKNOWN_TOOL"
1299        | "UNSUPPORTED_FEATURE"
1300        | "RESULT_NOT_SERIALIZABLE"
1301        | "SNAPSHOT_VERSION_MISMATCH"
1302        | "SNAPSHOT_CAPABILITY_MISMATCH"
1303        | "SNAPSHOT_CORRUPT" => RunStatus::ValidationError,
1304        _ if validation => RunStatus::ValidationError,
1305        _ => RunStatus::RuntimeError,
1306    }
1307}
1308
1309fn error_from_js(message: String, validation: bool) -> ErrorObject {
1310    let code = if validation || message.contains("SyntaxError") {
1311        "SYNTAX_ERROR"
1312    } else if message.contains("execution terminated") {
1313        "TIMEOUT_WALL"
1314    } else {
1315        "RUNTIME_ERROR"
1316    };
1317    ErrorObject::new(code, message)
1318}
1319
1320fn error_object_to_js(error: ErrorObject) -> JsErrorBox {
1321    JsErrorBox::generic(format!("{}: {}", error.code, error.message))
1322}
1323
1324fn timeout_error() -> ErrorObject {
1325    ErrorObject::new(
1326        "TIMEOUT_WALL",
1327        "TypeScript execution exceeded the wall-clock limit.",
1328    )
1329}
1330
1331fn runtime_error_result(error: ErrorObject) -> RunResult {
1332    RunResult::error(
1333        RunStatus::RuntimeError,
1334        error,
1335        String::new(),
1336        Metrics::default(),
1337    )
1338}
1339
1340fn worker_closed_error() -> ErrorObject {
1341    ErrorObject::new("RUNTIME_ERROR", "LangShell Deno worker is not available.")
1342}
1343
1344fn truncate_stdout(stdout: String, limits: &SessionLimits) -> (String, bool) {
1345    let mut stdout = stdout;
1346    let truncated = truncate_utf8(&mut stdout, limits.max_stdout_bytes as usize);
1347    (stdout, truncated)
1348}
1349
1350fn apply_streams(result: &mut RunResult, state: RunStateSnapshot, limits: &SessionLimits) {
1351    let (stdout, stdout_truncated) = truncate_stdout(state.stdout, limits);
1352    let (stderr, stderr_truncated) = truncate_stdout(state.stderr, limits);
1353    result.stdout = stdout;
1354    result.stderr = stderr;
1355    result.external_calls = state.records;
1356    if stdout_truncated || stderr_truncated {
1357        result.diagnostics.push(Diagnostic::warning(
1358            "STDOUT_EXCEEDED",
1359            format!(
1360                "stdout/stderr exceeded the {} byte limit and was truncated.",
1361                limits.max_stdout_bytes
1362            ),
1363        ));
1364    }
1365}
1366
1367fn metrics(started: Instant, external_calls_count: u32) -> Metrics {
1368    Metrics {
1369        duration_ms: elapsed_ms(started),
1370        memory_peak_bytes: 0,
1371        instructions: 0,
1372        external_calls_count,
1373    }
1374}
1375
1376fn elapsed_ms(started: Instant) -> u32 {
1377    u32::try_from(started.elapsed().as_millis()).unwrap_or(u32::MAX)
1378}
1379
1380fn capability_digest(registry: &ToolRegistry) -> String {
1381    digest_json(&json!(registry.names()))
1382}
1383
1384#[cfg(test)]
1385mod tests {
1386    use super::*;
1387    use langshell_core::{Capability, RegisteredTool, SideEffect};
1388    use std::sync::{LazyLock, Mutex};
1389
1390    static DENO_TEST_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
1391    static DENO_TEST_RUNTIME: LazyLock<DenoRuntime> = LazyLock::new(|| {
1392        let mut registry = ToolRegistry::new();
1393        registry
1394            .register(RegisteredTool::asynchronous(
1395                Capability::new("fetch_json", "test fetch", SideEffect::Network),
1396                |ctx| {
1397                    Box::pin(async move {
1398                        Ok(json!({
1399                            "url": ctx.args.first().and_then(Value::as_str).unwrap_or_default(),
1400                        }))
1401                    })
1402                },
1403            ))
1404            .unwrap();
1405        DenoRuntime::new(registry, SessionLimits::default())
1406    });
1407
1408    fn run_deno_test(test: impl std::future::Future<Output = ()>) {
1409        let _guard = DENO_TEST_LOCK.lock().expect("deno test lock");
1410        tokio::runtime::Runtime::new()
1411            .expect("deno test runtime")
1412            .block_on(test);
1413    }
1414
1415    #[test]
1416    fn runs_typescript_and_reuses_state() {
1417        run_deno_test(async {
1418            let runtime = &*DENO_TEST_RUNTIME;
1419            let mut first = RunRequest::new("ts-state-unit", "cache = { k: 1 }").unwrap();
1420            first.language = Language::TypeScript;
1421            assert_eq!(runtime.run(first).await.status, RunStatus::Ok);
1422
1423            let mut second = RunRequest::new("ts-state-unit", "result = cache.k + 1").unwrap();
1424            second.language = Language::TypeScript;
1425            let result = runtime.run(second).await;
1426            assert_eq!(result.status, RunStatus::Ok, "{result:?}");
1427            assert_eq!(result.result, Some(json!(2)));
1428        });
1429    }
1430
1431    #[test]
1432    fn supports_async_external_function() {
1433        run_deno_test(async {
1434            let runtime = &*DENO_TEST_RUNTIME;
1435            let mut request = RunRequest::new(
1436                "ts-fetch-unit",
1437                r#"
1438data = await fetch_json("https://api.example.com/item")
1439result = { url: data.url }
1440"#,
1441            )
1442            .unwrap();
1443            request.language = Language::TypeScript;
1444            let result = runtime.run(request).await;
1445            assert_eq!(result.status, RunStatus::Ok, "{result:?}");
1446            assert_eq!(
1447                result.result,
1448                Some(json!({"url": "https://api.example.com/item"}))
1449            );
1450            assert_eq!(result.metrics.external_calls_count, 1);
1451        });
1452    }
1453
1454    #[test]
1455    fn snapshots_json_globals() {
1456        run_deno_test(async {
1457            let runtime = &*DENO_TEST_RUNTIME;
1458            let mut first = RunRequest::new("ts-snap-json", "state = { step: 1 }").unwrap();
1459            first.language = Language::TypeScript;
1460            assert_eq!(runtime.run(first).await.status, RunStatus::Ok);
1461
1462            let snapshot = runtime
1463                .snapshot_session(&SessionId("ts-snap-json".to_owned()))
1464                .await
1465                .unwrap();
1466            runtime
1467                .restore_session(
1468                    &snapshot,
1469                    Some(SessionId("ts-snap-json-restore".to_owned())),
1470                )
1471                .await
1472                .unwrap();
1473
1474            let mut second =
1475                RunRequest::new("ts-snap-json-restore", "result = state.step").unwrap();
1476            second.language = Language::TypeScript;
1477            let result = runtime.run(second).await;
1478            assert_eq!(result.status, RunStatus::Ok, "{result:?}");
1479            assert_eq!(result.result, Some(json!(1)));
1480        });
1481    }
1482
1483    #[test]
1484    fn ast_validator_blocks_real_import() {
1485        let registry = ToolRegistry::new();
1486        let err = static_validation_error("import x from 'mod';", &registry).unwrap();
1487        assert_eq!(err.code, "UNSUPPORTED_FEATURE");
1488    }
1489
1490    #[test]
1491    fn ast_validator_allows_blocked_word_in_string_literal() {
1492        let registry = ToolRegistry::new();
1493        assert!(
1494            static_validation_error("const s = 'import Deno.fetch eval';", &registry).is_none()
1495        );
1496    }
1497
1498    #[test]
1499    fn ast_validator_blocks_global_member_access() {
1500        let registry = ToolRegistry::new();
1501        let err = static_validation_error("const v = Deno.cwd();", &registry).unwrap();
1502        assert_eq!(err.code, "UNSUPPORTED_FEATURE");
1503    }
1504
1505    #[test]
1506    fn ast_validator_blocks_eval_call() {
1507        let registry = ToolRegistry::new();
1508        let err = static_validation_error("eval('1+1');", &registry).unwrap();
1509        assert_eq!(err.code, "UNSUPPORTED_FEATURE");
1510    }
1511
1512    #[test]
1513    fn ast_validator_flags_unregistered_suspicious_call() {
1514        let registry = ToolRegistry::new();
1515        let err = static_validation_error("fetch_url('x');", &registry).unwrap();
1516        assert_eq!(err.code, "UNKNOWN_TOOL");
1517    }
1518
1519    #[test]
1520    fn snapshots_preserve_typed_globals() {
1521        run_deno_test(async {
1522            let runtime = &*DENO_TEST_RUNTIME;
1523            let mut first = RunRequest::new(
1524                "ts-snap-typed",
1525                "big = 9007199254740993n; bytes = new Uint8Array([1,2,3]); when = new Date(0);",
1526            )
1527            .unwrap();
1528            first.language = Language::TypeScript;
1529            assert_eq!(runtime.run(first).await.status, RunStatus::Ok);
1530
1531            let snap = runtime
1532                .snapshot_session(&SessionId("ts-snap-typed".to_owned()))
1533                .await
1534                .unwrap();
1535            assert!(is_deno_snapshot(&snap));
1536            runtime
1537                .restore_session(&snap, Some(SessionId("ts-snap-typed-restore".to_owned())))
1538                .await
1539                .unwrap();
1540
1541            let mut probe = RunRequest::new(
1542                "ts-snap-typed-restore",
1543            "result = { isBig: typeof big === 'bigint' && big === 9007199254740993n, bytes: Array.from(bytes), iso: when.toISOString() };",
1544        )
1545        .unwrap();
1546            probe.language = Language::TypeScript;
1547            let result = runtime.run(probe).await;
1548            assert_eq!(result.status, RunStatus::Ok, "{result:?}");
1549            assert_eq!(
1550                result.result,
1551                Some(json!({
1552                    "isBig": true,
1553                    "bytes": [1, 2, 3],
1554                    "iso": "1970-01-01T00:00:00.000Z",
1555                }))
1556            );
1557        });
1558    }
1559}