1use serde_json::Value;
2
3use crate::protocol::{
4 CodeModeRunnerInput, CodeModeRunnerOutput, CodeModeRunnerResult, CODE_MODE_STACK_SIZE_LIMIT,
5 RUNNER_STATE,
6};
7use crate::runner::jail::reset_execution_jail;
8use crate::runner::js_args::{javy_type_error, json_arg, optional_string_arg, required_string_arg};
9use crate::runner::limits::MEMORY_LIMIT_BYTES;
10use crate::wrapper::code_mode_runner_script;
11
12pub enum RunnerLoopOutcome {
13 Completed,
14 InputClosed,
15}
16
17pub struct RunnerError {
18 pub kind: String,
19 pub message: String,
20}
21
22impl From<String> for RunnerError {
23 fn from(message: String) -> Self {
24 Self {
25 kind: "server_error".to_string(),
26 message,
27 }
28 }
29}
30
31pub fn run_next() -> Result<RunnerLoopOutcome, RunnerError> {
32 let input = match runner_read_input() {
33 Ok(input) => input,
34 Err(RunnerReadError::InputClosed) => return Ok(RunnerLoopOutcome::InputClosed),
35 Err(RunnerReadError::Other(message)) => return Err(message.into()),
36 };
37 let CodeModeRunnerInput::Start { code, proxy } = input else {
38 return Err("runner expected start message".to_string().into());
39 };
40 reset_execution_jail();
41 let runtime = build_runtime()?;
42 install_host_bridge(&runtime).map_err(|message| RunnerError {
43 kind: "server_error".to_string(),
44 message,
45 })?;
46 run_wrapped(runtime, &code, &proxy)?;
47 Ok(RunnerLoopOutcome::Completed)
48}
49
50pub fn run_start_without_io(input: CodeModeRunnerInput) -> Result<CodeModeRunnerOutput, String> {
51 let CodeModeRunnerInput::Start { code, proxy } = input else {
52 return Err("runner expected start message".to_string());
53 };
54 let runtime = build_runtime().map_err(|err| err.message)?;
55 install_no_host_bridge(&runtime)?;
56 match run_wrapped_to_result(runtime, &code, &proxy) {
57 Ok(result) => Ok(CodeModeRunnerOutput::Done {
58 result: CodeModeRunnerResult::from_response_result(result),
59 logs: Vec::new(),
60 }),
61 Err(err) => Ok(CodeModeRunnerOutput::Error {
62 kind: err.kind,
63 message: err.message,
64 }),
65 }
66}
67
68pub fn emit_error(error: RunnerError) {
69 let _ = runner_emit(CodeModeRunnerOutput::Error {
70 kind: error.kind,
71 message: error.message,
72 });
73}
74
75pub fn reset_sequence() {
76 RUNNER_STATE.with(|state| {
77 if let Some(state) = state.borrow_mut().as_mut() {
78 state.next_seq = 0;
79 }
80 });
81}
82
83pub fn cleanup_execution_jail(drop_base: bool) {
84 crate::runner::jail::cleanup_execution_jail(drop_base);
85}
86
87fn build_runtime() -> Result<crate::javy::Runtime, RunnerError> {
88 let mut config = crate::javy::Config::default();
89 config
90 .redirect_stdout_to_stderr(true)
91 .memory_limit(MEMORY_LIMIT_BYTES)
92 .max_stack_size(CODE_MODE_STACK_SIZE_LIMIT);
93 crate::javy::Runtime::new(config).map_err(|err| RunnerError {
94 kind: "runtime_error".to_string(),
95 message: err.to_string(),
96 })
97}
98
99fn install_host_bridge(runtime: &crate::javy::Runtime) -> Result<(), String> {
100 runtime
101 .context()
102 .with(|cx| -> crate::javy::quickjs::Result<()> {
103 let globals = cx.globals();
104 globals.set(
105 "__somaEmitToolCall",
106 crate::javy::quickjs::Function::new(
107 cx.clone(),
108 crate::javy::quickjs::prelude::MutFn::new(|cx, args| {
109 javy_emit_tool_call(crate::javy::Args::hold(cx, args))
110 }),
111 )?,
112 )?;
113 globals.set(
114 "__somaEmitArtifactWrite",
115 crate::javy::quickjs::Function::new(
116 cx.clone(),
117 crate::javy::quickjs::prelude::MutFn::new(|cx, args| {
118 javy_emit_artifact_write(crate::javy::Args::hold(cx, args))
119 }),
120 )?,
121 )?;
122 globals.set(
123 "__somaEmitSnippetResolve",
124 crate::javy::quickjs::Function::new(
125 cx.clone(),
126 crate::javy::quickjs::prelude::MutFn::new(|cx, args| {
127 javy_emit_snippet_resolve(crate::javy::Args::hold(cx, args))
128 }),
129 )?,
130 )?;
131 globals.set(
132 "__somaEmitStepBegin",
133 crate::javy::quickjs::Function::new(
134 cx.clone(),
135 crate::javy::quickjs::prelude::MutFn::new(|cx, args| {
136 javy_emit_step_begin(crate::javy::Args::hold(cx, args))
137 }),
138 )?,
139 )?;
140 globals.set(
141 "__somaEmitStepResult",
142 crate::javy::quickjs::Function::new(
143 cx.clone(),
144 crate::javy::quickjs::prelude::MutFn::new(|cx, args| {
145 javy_emit_step_result(crate::javy::Args::hold(cx, args))
146 }),
147 )?,
148 )?;
149 Ok(())
150 })
151 .map_err(javy_error_message)
152}
153
154fn install_no_host_bridge(runtime: &crate::javy::Runtime) -> Result<(), String> {
155 runtime
156 .context()
157 .with(|cx| -> crate::javy::quickjs::Result<()> {
158 let globals = cx.globals();
159 for name in [
160 "__somaEmitToolCall",
161 "__somaEmitArtifactWrite",
162 "__somaEmitSnippetResolve",
163 "__somaEmitStepBegin",
164 "__somaEmitStepResult",
165 ] {
166 globals.set(
167 name,
168 crate::javy::quickjs::Function::new(
169 cx.clone(),
170 crate::javy::quickjs::prelude::MutFn::new(
171 |cx: crate::javy::quickjs::Ctx<'_>| {
172 Err::<u64, _>(javy_type_error(cx, "host bridge unavailable"))
173 },
174 ),
175 )?,
176 )?;
177 }
178 Ok(())
179 })
180 .map_err(javy_error_message)
181}
182
183fn run_wrapped(runtime: crate::javy::Runtime, code: &str, proxy: &str) -> Result<(), RunnerError> {
184 let result = run_wrapped_to_result(runtime, code, proxy)?;
185 runner_emit(CodeModeRunnerOutput::Done {
186 result: CodeModeRunnerResult::from_response_result(result),
187 logs: Vec::new(),
188 })
189 .map_err(RunnerError::from)
190}
191
192fn run_wrapped_to_result(
193 runtime: crate::javy::Runtime,
194 code: &str,
195 proxy: &str,
196) -> Result<Option<Value>, RunnerError> {
197 runtime
198 .context()
199 .with(|cx| cx.eval::<(), _>(code_mode_runner_script(code, proxy)))
200 .map_err(|err| RunnerError {
201 kind: "invalid_param".to_string(),
202 message: javy_error_message(err),
203 })?;
204 loop {
205 runtime
206 .resolve_pending_jobs()
207 .map_err(|err| err.to_string())?;
208 match javy_main_promise_state(&runtime)? {
209 JavyMainPromiseState::Resolved(result) => return Ok(result),
210 JavyMainPromiseState::Rejected(message) => return Err(classify_rejection(message)),
211 JavyMainPromiseState::Pending => {
212 let input = runner_read_input().map_err(RunnerReadError::into_runner_error)?;
213 javy_settle_pending_operation(&runtime, &input)?;
214 }
215 }
216 }
217}
218
219enum JavyMainPromiseState {
220 Pending,
221 Resolved(Option<Value>),
222 Rejected(String),
223}
224
225fn javy_main_promise_state(runtime: &crate::javy::Runtime) -> Result<JavyMainPromiseState, String> {
226 runtime
227 .context()
228 .with(|cx| -> crate::javy::quickjs::Result<JavyMainPromiseState> {
229 let promise: crate::javy::quickjs::Promise<'_> =
230 cx.globals().get("__somaMainPromise")?;
231 match promise.result::<crate::javy::quickjs::Value<'_>>() {
232 None => Ok(JavyMainPromiseState::Pending),
233 Some(Ok(val)) if val.is_undefined() => Ok(JavyMainPromiseState::Resolved(None)),
234 Some(Ok(val)) => match cx.json_stringify(val) {
235 Ok(Some(json_str)) => {
236 let text = json_str.to_string()?;
237 serde_json::from_str(&text)
238 .map(Some)
239 .map(JavyMainPromiseState::Resolved)
240 .or_else(|err| {
241 Ok(JavyMainPromiseState::Rejected(format!(
242 "Code Mode result must be JSON-serializable: {err}"
243 )))
244 })
245 }
246 Ok(None) => Ok(JavyMainPromiseState::Rejected(
247 "Code Mode result must be JSON-serializable".to_string(),
248 )),
249 Err(err) => Ok(JavyMainPromiseState::Rejected(javy_caught_error_message(
250 &cx, err,
251 ))),
252 },
253 Some(Err(err)) => Ok(JavyMainPromiseState::Rejected(javy_caught_error_message(
254 &cx, err,
255 ))),
256 }
257 })
258 .map_err(javy_error_message)
259}
260
261fn javy_settle_pending_operation(
262 runtime: &crate::javy::Runtime,
263 input: &CodeModeRunnerInput,
264) -> Result<(), String> {
265 let message = serde_json::to_string(input).map_err(|err| err.to_string())?;
266 runtime
267 .context()
268 .with(|cx| -> crate::javy::quickjs::Result<()> {
269 let settle: crate::javy::quickjs::Function<'_> =
270 cx.globals().get("__somaSettlePendingOperation")?;
271 settle.call::<_, ()>((message,))?;
272 Ok(())
273 })
274 .map_err(javy_error_message)?;
275 runtime
276 .resolve_pending_jobs()
277 .map_err(|err| err.to_string())
278}
279
280fn javy_emit_tool_call(args: crate::javy::Args<'_>) -> crate::javy::quickjs::Result<u64> {
281 let (cx, args) = args.release();
282 let id = required_string_arg(&cx, &args.0, 0, "callTool id must be a non-empty string")?;
283 let params = json_arg(&cx, &args.0, 1, "{}")?;
284 if !params.is_object() {
285 return Err(javy_type_error(cx, "callTool params must be a JSON object"));
286 }
287 let seq = next_runner_seq(&cx)?;
288 runner_emit(CodeModeRunnerOutput::ToolCall { seq, id, params })
289 .map_err(|err| javy_type_error(cx, err))?;
290 Ok(seq)
291}
292
293fn javy_emit_artifact_write(args: crate::javy::Args<'_>) -> crate::javy::quickjs::Result<u64> {
294 let (cx, args) = args.release();
295 let path = required_string_arg(&cx, &args.0, 0, "writeArtifact path must be a string")?;
296 let content = required_string_arg(&cx, &args.0, 1, "writeArtifact content must be a string")?;
297 let content_type = optional_string_arg(&cx, &args.0, 2)?;
298 let seq = next_runner_seq(&cx)?;
299 runner_emit(CodeModeRunnerOutput::ArtifactWrite {
300 seq,
301 path,
302 content,
303 content_type,
304 })
305 .map_err(|err| javy_type_error(cx, err))?;
306 Ok(seq)
307}
308
309fn javy_emit_snippet_resolve(args: crate::javy::Args<'_>) -> crate::javy::quickjs::Result<u64> {
310 let (cx, args) = args.release();
311 let name = required_string_arg(&cx, &args.0, 0, "snippet name must be a string")?;
312 let input = json_arg(&cx, &args.0, 1, "{}")?;
313 let seq = next_runner_seq(&cx)?;
314 runner_emit(CodeModeRunnerOutput::SnippetResolve { seq, name, input })
315 .map_err(|err| javy_type_error(cx, err))?;
316 Ok(seq)
317}
318
319fn javy_emit_step_begin(args: crate::javy::Args<'_>) -> crate::javy::quickjs::Result<u64> {
320 let (cx, args) = args.release();
321 let name = required_string_arg(&cx, &args.0, 0, "codemode.step name must be a string")?;
322 let seq = next_runner_seq(&cx)?;
323 runner_emit(CodeModeRunnerOutput::StepBegin { seq, name })
324 .map_err(|err| javy_type_error(cx, err))?;
325 Ok(seq)
326}
327
328fn javy_emit_step_result(args: crate::javy::Args<'_>) -> crate::javy::quickjs::Result<u64> {
329 let (cx, args) = args.release();
330 let seq = args
331 .0
332 .first()
333 .and_then(crate::javy::quickjs::Value::as_number)
334 .ok_or_else(|| javy_type_error(cx.clone(), "codemode.step result seq must be a number"))?
335 as u64;
336 let value = json_arg(&cx, &args.0, 1, "null")?;
337 runner_emit(CodeModeRunnerOutput::StepResult { seq, value })
338 .map_err(|err| javy_type_error(cx, err))?;
339 Ok(seq)
340}
341
342fn classify_rejection(message: String) -> RunnerError {
343 if let Some(kind) = extract_structured_kind(&message) {
344 return RunnerError { kind, message };
345 }
346 if message.contains("JSON-serializable") {
347 return RunnerError {
348 kind: "invalid_param".to_string(),
349 message,
350 };
351 }
352 RunnerError {
353 kind: "server_error".to_string(),
354 message,
355 }
356}
357
358fn extract_structured_kind(message: &str) -> Option<String> {
359 let start = message.find('{')?;
360 let end = message.rfind('}')?;
361 let Value::Object(map) = serde_json::from_str::<Value>(&message[start..=end]).ok()? else {
362 return None;
363 };
364 map.get("kind").and_then(Value::as_str).map(str::to_string)
365}
366
367fn next_runner_seq(cx: &crate::javy::quickjs::Ctx<'_>) -> crate::javy::quickjs::Result<u64> {
368 RUNNER_STATE
369 .with(|state| {
370 let mut state = state.borrow_mut();
371 let state = state
372 .as_mut()
373 .ok_or_else(|| "runner state is not initialized".to_string())?;
374 let seq = state.next_seq;
375 state.next_seq = state.next_seq.saturating_add(1);
376 Ok::<_, String>(seq)
377 })
378 .map_err(|err| javy_type_error(cx.clone(), err))
379}
380
381fn runner_emit(output: CodeModeRunnerOutput) -> Result<(), String> {
382 use std::io::Write;
383 RUNNER_STATE.with(|state| {
384 let mut state = state.borrow_mut();
385 let state = state
386 .as_mut()
387 .ok_or_else(|| "runner state is not initialized".to_string())?;
388 serde_json::to_writer(&mut state.writer, &output).map_err(|err| err.to_string())?;
389 state
390 .writer
391 .write_all(b"\n")
392 .map_err(|err| err.to_string())?;
393 state.writer.flush().map_err(|err| err.to_string())
394 })
395}
396
397enum RunnerReadError {
398 InputClosed,
399 Other(String),
400}
401
402impl RunnerReadError {
403 fn into_runner_error(self) -> RunnerError {
404 match self {
405 Self::InputClosed => "runner input closed".to_string().into(),
406 Self::Other(message) => message.into(),
407 }
408 }
409}
410
411fn runner_read_input() -> Result<CodeModeRunnerInput, RunnerReadError> {
412 use std::io::BufRead;
413 RUNNER_STATE.with(|state| {
414 let mut state = state.borrow_mut();
415 let state = state
416 .as_mut()
417 .ok_or_else(|| RunnerReadError::Other("runner state is not initialized".to_string()))?;
418 let mut line = String::new();
419 let read = state
420 .reader
421 .read_line(&mut line)
422 .map_err(|err| RunnerReadError::Other(err.to_string()))?;
423 if read == 0 {
424 return Err(RunnerReadError::InputClosed);
425 }
426 serde_json::from_str(&line).map_err(|err| RunnerReadError::Other(err.to_string()))
427 })
428}
429
430fn javy_error_message(error: crate::javy::quickjs::Error) -> String {
431 error.to_string()
432}
433
434fn javy_caught_error_message(
435 cx: &crate::javy::quickjs::Ctx<'_>,
436 error: crate::javy::quickjs::Error,
437) -> String {
438 match crate::javy::quickjs::CaughtError::from_error(cx, error) {
439 crate::javy::quickjs::CaughtError::Exception(exception) => {
440 exception.message().unwrap_or_else(|| exception.to_string())
441 }
442 crate::javy::quickjs::CaughtError::Value(value) => {
443 crate::javy::val_to_string(cx, value).unwrap_or_else(|err| err.to_string())
444 }
445 crate::javy::quickjs::CaughtError::Error(error) => error.to_string(),
446 }
447}