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
1117const TS_BLOCKED_GLOBALS: &[&str] = &[
1119 "Deno",
1120 "process",
1121 "Bun",
1122 "XMLHttpRequest",
1123 "WebSocket",
1124 "Worker",
1125 "SharedWorker",
1126 "navigator",
1127 "globalThis",
1128];
1129
1130const TS_BANNED_CALLABLES: &[&str] = &["eval", "Function", "require", "fetch"];
1132
1133const 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 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';", ®istry).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';", ®istry).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();", ®istry).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');", ®istry).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');", ®istry).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}