blob: edbf69bbe9dcdc0c5f8c05976f589f6c092b55d3 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! An isolated, persistent JavaScript REPL. This is independent of Code Mode:
//! no modules, Node, network inspector, filesystem or trusted plugin powers.
//! The caller owns its lifetime and supplies fresh authority for each evaluation.
use crate::{
CellAbort, CellContext, CellDiagnostic, CellDiagnosticKind, CellLimits, CellResult,
bridge::maka_code,
};
use deno_core::{
InspectorMsg, InspectorMsgKind, InspectorSessionKind, JsRuntime, JsRuntimeInspector,
RuntimeOptions, v8,
};
use maka_agent_facts::tools::ToolExecutor;
use maka_code_runtime::{ToolScope, complete};
use serde_json::{Value, json};
use std::sync::Arc;
use tokio::{
runtime::Handle,
sync::{mpsc, oneshot},
};
use tokio_util::sync::CancellationToken;
pub struct Repl {
sender: mpsc::Sender<Evaluation>,
stopped: CancellationToken,
worker: tokio::task::JoinHandle<Result<(), CellAbort>>,
}
struct Evaluation {
source: String,
executor: Arc<dyn ToolExecutor>,
cancellation: CancellationToken,
context: CellContext,
timeout: std::time::Duration,
reply: oneshot::Sender<Result<CellResult, CellAbort>>,
}
impl Repl {
pub fn is_closed(&self) -> bool {
self.stopped.is_cancelled() || self.worker.is_finished()
}
/// `bootstrap` is trusted facade source, evaluated once. It can capture the
/// two bridge functions `call(name, input)` and `emit(CellOutput)`; these
/// are not globals and contain no persistent invocation authority.
pub fn new(bootstrap: impl Into<String>, limits: CellLimits) -> Self {
let bootstrap = bootstrap.into();
crate::initialize_platform();
let (sender, receiver) = mpsc::channel(1);
let stopped = CancellationToken::new();
let stop = stopped.clone();
let host = Handle::current();
let worker = tokio::task::spawn_blocking(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| CellAbort::Internal(e.to_string()))?
.block_on(serve(bootstrap, limits, receiver, stop, host))
});
Self {
sender,
stopped,
worker,
}
}
pub async fn evaluate(
&self,
source: String,
executor: Arc<dyn ToolExecutor>,
cancellation: CancellationToken,
context: CellContext,
timeout: std::time::Duration,
) -> Result<CellResult, CellAbort> {
let cancellation = cancellation.child_token();
let _cancel_on_drop = cancellation.clone().drop_guard();
let (reply, receive) = oneshot::channel();
tokio::select! {
biased;
_ = cancellation.cancelled() => return Err(CellAbort::Cancelled),
result = self.sender.send(Evaluation { source, executor, cancellation: cancellation.clone(), context, timeout, reply }) => {
result.map_err(|_| closed())?;
}
}
// Do not race cancellation against settlement of an admitted evaluation.
receive.await.map_err(|_| closed())?
}
pub async fn close(self) -> Result<(), CellAbort> {
self.stopped.cancel();
self.worker
.await
.map_err(|e| CellAbort::Internal(e.to_string()))?
}
}
fn closed() -> CellAbort {
CellAbort::Internal("REPL is closed; reset it before evaluating more code".into())
}
async fn serve(
bootstrap: String,
limits: CellLimits,
mut receiver: mpsc::Receiver<Evaluation>,
stopped: CancellationToken,
host: Handle,
) -> Result<(), CellAbort> {
let mut runtime = JsRuntime::try_new(RuntimeOptions {
inspector: true,
extensions: vec![maka_code::init()],
create_params: Some(v8::CreateParams::default().heap_limits(0, limits.heap_bytes)),
..Default::default()
})
.map_err(|e| CellAbort::Internal(e.to_string()))?;
runtime.execute_script("maka:repl/bootstrap", format!(r#"(() => {{
const call = Deno.core.ops.op_maka_tool;
const emit = Deno.core.ops.op_maka_emit;
for (const name of ["Deno", "console", "Atomics", "SharedArrayBuffer", "WebAssembly", "__bootstrap"])
delete globalThis[name];
{bootstrap}
}})()"#)).map_err(|e| CellAbort::Internal(e.to_string()))?;
let isolate = runtime.v8_isolate().thread_safe_handle();
let heap_exhausted = CancellationToken::new();
let heap_stop = heap_exhausted.clone();
let heap_isolate = isolate.clone();
runtime.add_near_heap_limit_callback(move |current, _| {
heap_stop.cancel();
heap_isolate.terminate_execution();
current.saturating_add(16 * 1024 * 1024)
});
let (replies, mut responses) = mpsc::unbounded_channel();
let mut inspector = JsRuntimeInspector::create_local_session(
runtime.inspector(),
Box::new(move |msg: InspectorMsg| {
if let InspectorMsgKind::Message(_) = msg.kind {
let _ = replies.send(msg.content);
}
}),
InspectorSessionKind::NonBlocking {
wait_for_disconnect: false,
},
);
let mut sequence = 0;
loop {
let request = tokio::select! {
biased;
_ = stopped.cancelled() => break,
request = receiver.recv() => match request { Some(request) => request, None => break },
};
if request.cancellation.is_cancelled() {
let _ = request.reply.send(Err(CellAbort::Cancelled));
continue;
}
if request.timeout.is_zero()
|| std::time::Instant::now()
.checked_add(request.timeout)
.is_none()
{
let _ = request.reply.send(Ok(complete(
Err(CellDiagnostic::limit("invalid REPL timeout")),
vec![],
)));
continue;
}
let scope = ToolScope::new(
request.context.clone(),
request.executor,
request.cancellation.child_token(),
host.clone(),
);
runtime.op_state().borrow_mut().put(scope.clone());
runtime.op_state().borrow_mut().put(request.context.clone());
let finished = CancellationToken::new();
let finish = finished.clone();
let cancellation = request.cancellation.clone();
let stop = stopped.clone();
let terminate = isolate.clone();
let interrupted = CancellationToken::new();
let interrupt = interrupted.clone();
let timeout = request.timeout;
let monitor = host.spawn(async move {
tokio::select! {
biased;
_ = finish.cancelled() => return,
_ = cancellation.cancelled() => {},
_ = stop.cancelled() => {},
_ = tokio::time::sleep(timeout) => {},
}
interrupt.cancel();
terminate.terminate_execution();
});
sequence += 1;
// Local inspector transport gives real REPL lexical bindings and await;
// there is deliberately no inspector socket or arbitrary CDP endpoint.
inspector.post_message(
sequence,
"Runtime.evaluate",
Some(json!({
"expression":request.source,
"replMode":true, "awaitPromise":true, "returnByValue":false,
"objectGroup":"maka-repl-result"
})),
);
let mut completed = false;
let result = tokio::select! {
biased;
_ = interrupted.cancelled() => Err(CellDiagnostic::limit("REPL interrupted; reset required")),
_ = heap_exhausted.cancelled() => Err(CellDiagnostic::limit("JavaScript heap; reset required")),
result = async {
runtime.run_event_loop(Default::default()).await
.map_err(|e| CellDiagnostic::new(CellDiagnosticKind::ExecutionError, e.to_string()))?;
let response = responses.recv().await.ok_or_else(|| CellDiagnostic::new(CellDiagnosticKind::ExecutionError, "inspector closed"))?;
let value: Value = serde_json::from_str(&response)
.map_err(|e| CellDiagnostic::new(CellDiagnosticKind::ExecutionError, e.to_string()))?;
if let Some(error) = value.get("error") {
return Err(CellDiagnostic::new(CellDiagnosticKind::ExecutionError,
error["message"].as_str().unwrap_or("inspector evaluation failed")));
}
let result = value.get("result").filter(|result| result.is_object())
.ok_or_else(|| CellDiagnostic::new(CellDiagnosticKind::ExecutionError, "inspector response missing result"))?;
// A regular JavaScript exception still completed the evaluation.
completed = true;
if let Some(error) = result.get("exceptionDetails") {
let message = error["exception"]["description"].as_str()
.or_else(|| error["text"].as_str()).or_else(|| error["message"].as_str())
.unwrap_or("JavaScript execution failed");
Err(CellDiagnostic::new(CellDiagnosticKind::ExecutionError, message))
} else { Ok(Value::Null) }
} => result,
};
// Drain native work even after a JS exception, timeout or a lost caller.
scope.cancel();
scope.settle().await;
finished.cancel();
monitor
.await
.map_err(|e| CellAbort::Internal(e.to_string()))?;
let poisoned = !completed || interrupted.is_cancelled() || heap_exhausted.is_cancelled();
let result = if let Some(error) = request.context.failure() {
Err(CellAbort::Tool(error))
} else if request.cancellation.is_cancelled() || stopped.is_cancelled() {
Err(CellAbort::Cancelled)
} else {
Ok(complete(result, scope.calls()))
};
// Publish terminal state before waking the observer. The Session still
// owns and joins this worker on reset/close, but it cannot be reused.
if poisoned {
stopped.cancel();
}
let _ = request.reply.send(result);
if poisoned {
break;
}
// Release inspector's completion-value references, not user bindings.
inspector.post_message(
0,
"Runtime.releaseObjectGroup",
Some(json!({"objectGroup":"maka-repl-result"})),
);
let _ = responses.recv().await;
runtime.op_state().borrow_mut().take::<Arc<ToolScope>>();
runtime.op_state().borrow_mut().take::<CellContext>();
}
Ok(())
}