Link code_mode to rusty_v8

This commit is contained in:
pakrym-oai
2026-03-10 08:21:18 -07:00
parent 6b7253b123
commit 305a9c4baa
10 changed files with 812 additions and 391 deletions

19
MODULE.bazel.lock generated

File diff suppressed because one or more lines are too long

229
codex-rs/Cargo.lock generated
View File

@@ -948,6 +948,8 @@ dependencies = [
"cexpr",
"clang-sys",
"itertools 0.13.0",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
@@ -1151,6 +1153,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]]
name = "calendrical_calculations"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7"
dependencies = [
"core_maths",
"displaydoc",
]
[[package]]
name = "cassowary"
version = "0.3.0"
@@ -1572,7 +1584,7 @@ dependencies = [
"thiserror 2.0.18",
"tokio",
"url",
"which",
"which 8.0.0",
"wiremock",
"zip",
]
@@ -1893,8 +1905,9 @@ dependencies = [
"tracing-test",
"url",
"uuid",
"v8",
"walkdir",
"which",
"which 8.0.0",
"wildmatch",
"windows-sys 0.52.0",
"wiremock",
@@ -2105,7 +2118,7 @@ dependencies = [
"serde_json",
"tokio",
"tracing",
"which",
"which 8.0.0",
"wiremock",
]
@@ -2343,7 +2356,7 @@ dependencies = [
"tracing",
"urlencoding",
"webbrowser",
"which",
"which 8.0.0",
]
[[package]]
@@ -2383,7 +2396,7 @@ dependencies = [
"tree-sitter",
"tree-sitter-bash",
"url",
"which",
"which 8.0.0",
]
[[package]]
@@ -2542,7 +2555,7 @@ dependencies = [
"uuid",
"vt100",
"webbrowser",
"which",
"which 8.0.0",
"windows-sys 0.52.0",
"winsplit",
]
@@ -2916,6 +2929,15 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core_maths"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
dependencies = [
"libm",
]
[[package]]
name = "core_test_support"
version = "0.0.0"
@@ -3513,6 +3535,38 @@ dependencies = [
"subtle",
]
[[package]]
name = "diplomat"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9adb46b05e2f53dcf6a7dfc242e4ce9eb60c369b6b6eb10826a01e93167f59c6"
dependencies = [
"diplomat_core",
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "diplomat-runtime"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0569bd3caaf13829da7ee4e83dbf9197a0e1ecd72772da6d08f0b4c9285c8d29"
[[package]]
name = "diplomat_core"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51731530ed7f2d4495019abc7df3744f53338e69e2863a6a64ae91821c763df1"
dependencies = [
"proc-macro2",
"quote",
"serde",
"smallvec",
"strck",
"syn 2.0.114",
]
[[package]]
name = "dirs"
version = "6.0.0"
@@ -4119,6 +4173,16 @@ dependencies = [
"libc",
]
[[package]]
name = "fslock"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "futures"
version = "0.3.31"
@@ -4347,6 +4411,15 @@ dependencies = [
"regex-syntax 0.8.8",
]
[[package]]
name = "gzip-header"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2"
dependencies = [
"crc32fast",
]
[[package]]
name = "h2"
version = "0.4.13"
@@ -4804,6 +4877,28 @@ dependencies = [
"cc",
]
[[package]]
name = "icu_calendar"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e"
dependencies = [
"calendrical_calculations",
"displaydoc",
"icu_calendar_data",
"icu_locale",
"icu_locale_core",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_calendar_data"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d"
[[package]]
name = "icu_collections"
version = "2.1.1"
@@ -5238,6 +5333,12 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "ixdtf"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992"
[[package]]
name = "jiff"
version = "0.2.18"
@@ -6962,6 +7063,16 @@ dependencies = [
"yansi",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn 2.0.114",
]
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
@@ -7794,6 +7905,16 @@ dependencies = [
"webpki-roots 1.0.5",
]
[[package]]
name = "resb"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a067ab3b5ca3b4dc307d0de9cf75f9f5e6ca9717b192b2f28a36c83e5de9e76"
dependencies = [
"potential_utf",
"serde_core",
]
[[package]]
name = "resolv-conf"
version = "0.7.6"
@@ -9174,6 +9295,15 @@ dependencies = [
"serde_json",
]
[[package]]
name = "strck"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42316e70da376f3d113a68d138a60d8a9883c604fe97942721ec2068dab13a9f"
dependencies = [
"unicode-ident",
]
[[package]]
name = "streaming-iterator"
version = "0.1.9"
@@ -9403,6 +9533,39 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "temporal_capi"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a151e402c2bdb6a3a2a2f3f225eddaead2e7ce7dd5d3fa2090deb11b17aa4ed8"
dependencies = [
"diplomat",
"diplomat-runtime",
"icu_calendar",
"icu_locale",
"num-traits",
"temporal_rs",
"timezone_provider",
"writeable",
"zoneinfo64",
]
[[package]]
name = "temporal_rs"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88afde3bd75d2fc68d77a914bece426aa08aa7649ffd0cdd4a11c3d4d33474d1"
dependencies = [
"core_maths",
"icu_calendar",
"icu_locale",
"ixdtf",
"num-traits",
"timezone_provider",
"tinystr",
"writeable",
]
[[package]]
name = "term"
version = "0.7.0"
@@ -9610,6 +9773,18 @@ dependencies = [
"time-core",
]
[[package]]
name = "timezone_provider"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9ba0000e9e73862f3e7ca1ff159e2ddf915c9d8bb11e38a7874760f445d993"
dependencies = [
"tinystr",
"zerotrie",
"zerovec",
"zoneinfo64",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
@@ -10409,6 +10584,23 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "v8"
version = "146.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d97bcac5cdc5a195a4813f1855a6bc658f240452aac36caa12fd6c6f16026ab1"
dependencies = [
"bindgen",
"bitflags 2.10.0",
"fslock",
"gzip-header",
"home",
"miniz_oxide",
"paste",
"temporal_capi",
"which 6.0.3",
]
[[package]]
name = "valuable"
version = "0.1.1"
@@ -10708,6 +10900,18 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "which"
version = "6.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f"
dependencies = [
"either",
"home",
"rustix 0.38.44",
"winsafe",
]
[[package]]
name = "which"
version = "8.0.0"
@@ -11711,6 +11915,19 @@ version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
[[package]]
name = "zoneinfo64"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2e5597efbe7c421da8a7fd396b20b571704e787c21a272eecf35dfe9d386f0"
dependencies = [
"calendrical_calculations",
"icu_locale_core",
"potential_utf",
"resb",
"serde",
]
[[package]]
name = "zopfli"
version = "0.8.3"

View File

@@ -234,6 +234,7 @@ regex-lite = "0.1.8"
reqwest = "0.12"
rmcp = { version = "0.15.0", default-features = false }
runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" }
rusty_v8 = { package = "v8", version = "146.4.0" }
rustls = { version = "0.23", default-features = false, features = [
"ring",
"std",

View File

@@ -81,6 +81,7 @@ rmcp = { workspace = true, default-features = false, features = [
"schemars",
"server",
] }
rusty_v8 = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

View File

@@ -405,17 +405,6 @@ impl Codex {
warn!("{message}");
config.startup_warnings.push(message);
}
if config.features.enabled(Feature::CodeMode)
&& let Err(err) = resolve_compatible_node(config.js_repl_node_path.as_deref()).await
{
let message = format!(
"Disabled `code_mode` for this session because the configured Node runtime is unavailable or incompatible. {err}"
);
warn!("{message}");
let _ = config.features.disable(Feature::CodeMode);
config.startup_warnings.push(message);
}
let allowed_skills_for_implicit_invocation =
loaded_skills.allowed_skills_for_implicit_invocation();
let user_instructions = get_user_instructions(

View File

@@ -1,17 +1,16 @@
use std::process::ExitStatus;
use std::pin::pin;
use std::sync::Arc;
use std::sync::Once;
use crate::client_common::tools::ToolSpec;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::config::Config;
use crate::exec_env::create_env;
use crate::features::Feature;
use crate::function_tool::FunctionCallError;
use crate::tools::ToolRouter;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::context::ToolPayload;
use crate::tools::js_repl::resolve_compatible_node;
use crate::tools::router::ToolCall;
use crate::tools::router::ToolCallSource;
use codex_protocol::models::ContentItem;
@@ -19,17 +18,20 @@ use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseInputItem;
use rusty_v8 as v8;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value as JsonValue;
use serde_json::json;
use tokio::io::AsyncBufReadExt;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::io::BufReader;
use tokio::runtime::Handle;
use tokio::runtime::RuntimeFlavor;
const CODE_MODE_RUNNER_SOURCE: &str = include_str!("code_mode_runner.cjs");
const CODE_MODE_BRIDGE_SOURCE: &str = include_str!("code_mode_bridge.js");
const CODE_MODE_BOOTSTRAP_SOURCE: &str = include_str!("code_mode_bridge.js");
const CODE_MODE_BOOTSTRAP_FILENAME: &str = "code_mode_bootstrap.js";
const CODE_MODE_MAIN_FILENAME: &str = "code_mode_main.mjs";
const CODE_MODE_TOOLS_MODULE_NAME: &str = "tools.js";
static CODE_MODE_V8_INIT: Once = Once::new();
#[derive(Clone)]
struct ExecContext {
@@ -38,6 +40,12 @@ struct ExecContext {
tracker: SharedTurnDiffTracker,
}
struct CodeModeRuntimeState {
exec: ExecContext,
enabled_tools: Vec<EnabledTool>,
tools_module: Option<v8::Global<v8::Module>>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
enum CodeModeToolKind {
@@ -51,45 +59,15 @@ struct EnabledTool {
kind: CodeModeToolKind,
}
#[derive(Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum HostToNodeMessage {
Init {
enabled_tools: Vec<EnabledTool>,
source: String,
},
Response {
id: String,
content_items: Vec<JsonValue>,
},
}
#[derive(Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum NodeToHostMessage {
ToolCall {
id: String,
name: String,
#[serde(default)]
input: Option<JsonValue>,
},
Result {
content_items: Vec<JsonValue>,
},
}
pub(crate) fn instructions(config: &Config) -> Option<String> {
if !config.features.enabled(Feature::CodeMode) {
return None;
}
let mut section = String::from("## Code Mode\n");
section.push_str(
"- Use `code_mode` for JavaScript execution in a Node-backed `node:vm` context.\n",
);
section.push_str("- Use `code_mode` for JavaScript execution in an embedded V8 runtime.\n");
section.push_str("- `code_mode` is a freeform/custom tool. Direct `code_mode` calls must send raw JavaScript tool input. Do not wrap code in JSON, quotes, or markdown code fences.\n");
section.push_str("- Direct tool calls remain available while `code_mode` is enabled.\n");
section.push_str("- `code_mode` uses the same Node runtime resolution as `js_repl`. If needed, point `js_repl_node_path` at the Node binary you want Codex to use.\n");
section.push_str("- Import nested tools from `tools.js`, for example `import { exec_command } from \"tools.js\"` or `import { tools } from \"tools.js\"`. `tools[name]` and identifier wrappers like `await exec_command(args)` remain available for compatibility. Nested tool calls resolve to arrays of content items.\n");
section.push_str(
"- Function tools require JSON object arguments. Freeform tools require raw strings.\n",
@@ -112,156 +90,530 @@ pub(crate) async fn execute(
tracker,
};
let enabled_tools = build_enabled_tools(&exec);
let source = build_source(&code, &enabled_tools).map_err(FunctionCallError::RespondToModel)?;
execute_node(exec, source, enabled_tools)
.await
let bootstrap_source =
build_bootstrap_source(&enabled_tools).map_err(FunctionCallError::RespondToModel)?;
execute_v8(exec, code, enabled_tools, bootstrap_source)
.map_err(FunctionCallError::RespondToModel)
}
async fn execute_node(
fn execute_v8(
exec: ExecContext,
source: String,
code: String,
enabled_tools: Vec<EnabledTool>,
bootstrap_source: String,
) -> Result<Vec<FunctionCallOutputContentItem>, String> {
let node_path = resolve_compatible_node(exec.turn.config.js_repl_node_path.as_deref()).await?;
init_v8();
let env = create_env(&exec.turn.shell_environment_policy, None);
let mut cmd = tokio::process::Command::new(&node_path);
cmd.arg("--experimental-vm-modules");
cmd.arg("--eval");
cmd.arg(CODE_MODE_RUNNER_SOURCE);
cmd.current_dir(&exec.turn.cwd);
cmd.env_clear();
cmd.envs(env);
cmd.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true);
let mut child = cmd
.spawn()
.map_err(|err| format!("failed to start code_mode Node runtime: {err}"))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| "code_mode runner missing stdout".to_string())?;
let stderr = child
.stderr
.take()
.ok_or_else(|| "code_mode runner missing stderr".to_string())?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| "code_mode runner missing stdin".to_string())?;
let stderr_task = tokio::spawn(async move {
let mut reader = BufReader::new(stderr);
let mut buf = Vec::new();
let _ = reader.read_to_end(&mut buf).await;
String::from_utf8_lossy(&buf).trim().to_string()
let mut isolate = v8::Isolate::new(v8::CreateParams::default());
isolate.set_capture_stack_trace_for_uncaught_exceptions(true, 32);
isolate.set_host_import_module_dynamically_callback(code_mode_dynamic_import_callback);
isolate.set_slot(CodeModeRuntimeState {
exec,
enabled_tools: enabled_tools.clone(),
tools_module: None,
});
write_message(
&mut stdin,
&HostToNodeMessage::Init {
enabled_tools: enabled_tools.clone(),
source,
},
)
.await?;
let scope = pin!(v8::HandleScope::new(&mut isolate));
let scope = &mut scope.init();
let context = v8::Context::new(scope, Default::default());
let scope = &mut v8::ContextScope::new(scope, context);
let mut stdout_lines = BufReader::new(stdout).lines();
let mut final_content_items = None;
while let Some(line) = stdout_lines
.next_line()
.await
.map_err(|err| format!("failed to read code_mode runner stdout: {err}"))?
{
if line.trim().is_empty() {
install_tool_call_binding(scope)?;
run_script(scope, CODE_MODE_BOOTSTRAP_FILENAME, &bootstrap_source)?;
let tools_module = create_tools_module(scope, &enabled_tools)?;
let tools_module = v8::Global::new(scope, tools_module);
let Some(runtime_state) = scope.get_slot_mut::<CodeModeRuntimeState>() else {
return Err("code_mode runtime state missing".to_string());
};
runtime_state.tools_module = Some(tools_module);
let scope = pin!(v8::TryCatch::new(scope));
let scope = &mut scope.init();
let filename = v8_string(scope, CODE_MODE_MAIN_FILENAME)?;
let source = v8_string(scope, &code)?;
let origin = script_origin(scope, filename, true);
let mut source = v8::script_compiler::Source::new(source, Some(&origin));
let Some(module) = v8::script_compiler::compile_module(scope, &mut source) else {
return Err(format_v8_exception(scope));
};
let Some(instantiated) = module.instantiate_module(scope, resolve_code_mode_module) else {
return Err(format_v8_exception(scope));
};
if !instantiated {
return Err("failed to instantiate code_mode module".to_string());
}
let Some(result) = module.evaluate(scope) else {
return Err(format_v8_exception(scope));
};
if result.is_promise() {
let promise = v8::Local::<v8::Promise>::try_from(result)
.map_err(|_| "code_mode module evaluation did not return a promise".to_string())?;
wait_for_module_promise(scope, module, promise)?;
} else {
scope.perform_microtask_checkpoint();
}
read_content_items(scope)
}
fn init_v8() {
CODE_MODE_V8_INIT.call_once(|| {
let platform = v8::new_default_platform(0, false).make_shared();
v8::V8::initialize_platform(platform);
v8::V8::initialize();
});
}
fn install_tool_call_binding(scope: &mut v8::PinScope<'_, '_>) -> Result<(), String> {
let function = v8::Function::new(scope, code_mode_tool_call_callback)
.ok_or_else(|| "failed to install code_mode tool bridge".to_string())?;
let key = v8_string(scope, "__codex_tool_call")?;
let global = scope.get_current_context().global(scope);
if global.set(scope, key.into(), function.into()).is_some() {
Ok(())
} else {
Err("failed to bind __codex_tool_call".to_string())
}
}
fn run_script(
scope: &mut v8::PinScope<'_, '_>,
filename: &str,
source: &str,
) -> Result<(), String> {
let scope = pin!(v8::TryCatch::new(scope));
let scope = &mut scope.init();
let source = v8_string(scope, source)?;
let filename = v8_string(scope, filename)?;
let origin = script_origin(scope, filename, false);
let Some(script) = v8::Script::compile(scope, source, Some(&origin)) else {
return Err(format_v8_exception(scope));
};
if script.run(scope).is_none() {
return Err(format_v8_exception(scope));
}
Ok(())
}
fn script_origin<'s>(
scope: &mut v8::PinScope<'s, '_>,
filename: v8::Local<'s, v8::String>,
is_module: bool,
) -> v8::ScriptOrigin<'s> {
v8::ScriptOrigin::new(
scope,
filename.into(),
0,
0,
false,
0,
None,
false,
false,
is_module,
None,
)
}
fn create_tools_module<'s>(
scope: &mut v8::PinScope<'s, '_>,
enabled_tools: &[EnabledTool],
) -> Result<v8::Local<'s, v8::Module>, String> {
let mut export_names = vec![v8_string(scope, "tools")?];
for tool in enabled_tools {
if tool.name != "tools" && is_valid_identifier(&tool.name) {
export_names.push(v8_string(scope, &tool.name)?);
}
}
let module_name = v8_string(scope, CODE_MODE_TOOLS_MODULE_NAME)?;
Ok(v8::Module::create_synthetic_module(
scope,
module_name,
&export_names,
evaluate_tools_module,
))
}
fn evaluate_tools_module<'s>(
context: v8::Local<'s, v8::Context>,
module: v8::Local<'s, v8::Module>,
) -> Option<v8::Local<'s, v8::Value>> {
v8::callback_scope!(unsafe scope, context);
let Some(runtime_state) = scope.get_slot::<CodeModeRuntimeState>() else {
return throw_v8_exception(scope, "code_mode runtime state missing");
};
let enabled_tools = runtime_state.enabled_tools.clone();
let global = context.global(scope);
let tools_name = match v8::String::new(scope, "tools") {
Some(name) => name,
None => return throw_v8_exception(scope, "failed to allocate tools export name"),
};
let Some(tools_value) = global.get(scope, tools_name.into()) else {
return throw_v8_exception(scope, "code_mode tools namespace missing");
};
let Ok(tools_object) = v8::Local::<v8::Object>::try_from(tools_value) else {
return throw_v8_exception(scope, "code_mode tools namespace is not an object");
};
module.set_synthetic_module_export(scope, tools_name, tools_object.into())?;
for tool in enabled_tools {
if !is_valid_identifier(&tool.name) || tool.name == "tools" {
continue;
}
let message: NodeToHostMessage = serde_json::from_str(&line)
.map_err(|err| format!("invalid code_mode runner message: {err}; line={line}"))?;
match message {
NodeToHostMessage::ToolCall { id, name, input } => {
let response = HostToNodeMessage::Response {
id,
content_items: call_nested_tool(exec.clone(), name, input).await,
};
write_message(&mut stdin, &response).await?;
let Some(export_name) = v8::String::new(scope, &tool.name) else {
return throw_v8_exception(scope, "failed to allocate tool export name");
};
let Some(export_value) = tools_object.get(scope, export_name.into()) else {
return throw_v8_exception(
scope,
&format!("code_mode tool export `{}` is unavailable", tool.name),
);
};
module.set_synthetic_module_export(scope, export_name, export_value)?;
}
Some(v8::undefined(scope).into())
}
fn resolve_code_mode_module<'s>(
context: v8::Local<'s, v8::Context>,
specifier: v8::Local<'s, v8::String>,
_import_attributes: v8::Local<'s, v8::FixedArray>,
_referrer: v8::Local<'s, v8::Module>,
) -> Option<v8::Local<'s, v8::Module>> {
v8::callback_scope!(unsafe scope, context);
match resolve_tools_module(scope, specifier) {
Ok(module) => Some(module),
Err(error) => throw_v8_exception(scope, &error),
}
}
fn code_mode_dynamic_import_callback<'s, 'i>(
scope: &mut v8::PinScope<'s, 'i>,
_host_defined_options: v8::Local<'s, v8::Data>,
_resource_name: v8::Local<'s, v8::Value>,
specifier: v8::Local<'s, v8::String>,
_import_attributes: v8::Local<'s, v8::FixedArray>,
) -> Option<v8::Local<'s, v8::Promise>> {
let resolver = v8::PromiseResolver::new(scope)?;
let promise = resolver.get_promise(scope);
match resolve_tools_module(scope, specifier).and_then(|module| {
instantiate_and_evaluate_tools_module(scope, module)?;
Ok(module.get_module_namespace())
}) {
Ok(namespace) => {
let _ = resolver.resolve(scope, namespace);
}
Err(error) => {
let error = v8_string(scope, &error).ok()?;
let _ = resolver.reject(scope, error.into());
}
}
Some(promise)
}
fn resolve_tools_module<'s>(
scope: &mut v8::PinScope<'s, '_>,
specifier: v8::Local<'s, v8::String>,
) -> Result<v8::Local<'s, v8::Module>, String> {
let specifier = specifier.to_rust_string_lossy(scope);
if specifier != CODE_MODE_TOOLS_MODULE_NAME {
return Err(format!("Unsupported import in code_mode: {specifier}"));
}
let Some(runtime_state) = scope.get_slot::<CodeModeRuntimeState>() else {
return Err("code_mode runtime state missing".to_string());
};
let Some(tools_module) = runtime_state.tools_module.as_ref() else {
return Err("code_mode tools module missing".to_string());
};
Ok(v8::Local::new(scope, tools_module))
}
fn instantiate_and_evaluate_tools_module(
scope: &mut v8::PinScope<'_, '_>,
module: v8::Local<'_, v8::Module>,
) -> Result<(), String> {
match module.get_status() {
v8::ModuleStatus::Uninstantiated => {
let Some(instantiated) = module.instantiate_module(scope, resolve_code_mode_module)
else {
return Err("failed to instantiate code_mode tools module".to_string());
};
if !instantiated {
return Err("failed to instantiate code_mode tools module".to_string());
}
NodeToHostMessage::Result { content_items } => {
final_content_items = Some(output_content_items_from_json_values(content_items)?);
break;
}
v8::ModuleStatus::Instantiating => {
return Err("code_mode tools module is already instantiating".to_string());
}
v8::ModuleStatus::Instantiated
| v8::ModuleStatus::Evaluating
| v8::ModuleStatus::Evaluated => {}
v8::ModuleStatus::Errored => {
return Err(format_v8_value(scope, module.get_exception()));
}
}
match module.get_status() {
v8::ModuleStatus::Instantiated => {
let Some(result) = module.evaluate(scope) else {
return Err("failed to evaluate code_mode tools module".to_string());
};
if result.is_promise() {
let promise = v8::Local::<v8::Promise>::try_from(result).map_err(|_| {
"code_mode tools module evaluation did not return a promise".to_string()
})?;
scope.perform_microtask_checkpoint();
match promise.state() {
v8::PromiseState::Fulfilled => {}
v8::PromiseState::Rejected => {
return Err(format_v8_value(scope, promise.result(scope)));
}
v8::PromiseState::Pending => {
return Err("code_mode tools module evaluation did not settle".to_string());
}
}
}
}
v8::ModuleStatus::Evaluated => {}
v8::ModuleStatus::Evaluating => {
return Err("code_mode tools module is already evaluating".to_string());
}
v8::ModuleStatus::Errored => {
return Err(format_v8_value(scope, module.get_exception()));
}
v8::ModuleStatus::Uninstantiated | v8::ModuleStatus::Instantiating => {}
}
Ok(())
}
fn code_mode_tool_call_callback(
scope: &mut v8::PinScope<'_, '_>,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue<v8::Value>,
) {
let Some(resolver) = v8::PromiseResolver::new(scope) else {
return;
};
let promise = resolver.get_promise(scope);
rv.set(promise.into());
let result = run_tool_call(scope, &args).and_then(|value| json_to_v8(scope, &value));
match result {
Ok(value) => {
let _ = resolver.resolve(scope, value);
}
Err(error) => {
if let Some(error) = v8::String::new(scope, &error) {
let _ = resolver.reject(scope, error.into());
}
}
}
}
fn run_tool_call(
scope: &mut v8::PinScope<'_, '_>,
args: &v8::FunctionCallbackArguments,
) -> Result<JsonValue, String> {
let tool_name = args
.get(0)
.to_string(scope)
.ok_or_else(|| "code_mode tool call requires a tool name".to_string())?
.to_rust_string_lossy(scope);
let input = json_from_v8(scope, args.get(1))?;
let Some(exec) = scope
.get_slot::<CodeModeRuntimeState>()
.map(|runtime_state| runtime_state.exec.clone())
else {
return Err("code_mode runtime state missing".to_string());
};
let content_items = match Handle::current().runtime_flavor() {
RuntimeFlavor::MultiThread => tokio::task::block_in_place(|| {
Handle::current().block_on(call_nested_tool(exec, tool_name, input))
}),
RuntimeFlavor::CurrentThread => {
return Err("code_mode tool calls require a multi-thread Tokio runtime".to_string());
}
_ => {
return Err("code_mode tool calls require a supported Tokio runtime".to_string());
}
};
Ok(JsonValue::Array(content_items))
}
fn wait_for_module_promise(
scope: &mut v8::PinScope<'_, '_>,
module: v8::Local<'_, v8::Module>,
promise: v8::Local<'_, v8::Promise>,
) -> Result<(), String> {
for _ in 0..32 {
match promise.state() {
v8::PromiseState::Fulfilled => return Ok(()),
v8::PromiseState::Rejected => {
return Err(format_v8_value(scope, promise.result(scope)));
}
v8::PromiseState::Pending => {
scope.perform_microtask_checkpoint();
}
}
}
drop(stdin);
let status = child
.wait()
.await
.map_err(|err| format!("failed to wait for code_mode runner: {err}"))?;
let stderr = stderr_task
.await
.map_err(|err| format!("failed to collect code_mode stderr: {err}"))?;
match final_content_items {
Some(content_items) if status.success() => Ok(content_items),
Some(_) => Err(format_runner_failure(
"code_mode execution failed",
status,
&stderr,
)),
None => Err(format_runner_failure(
"code_mode runner exited without returning a result",
status,
&stderr,
)),
let stalled = module.get_stalled_top_level_await_message(scope);
if let Some((_module, message)) = stalled.into_iter().next() {
let pending = message.get(scope).to_rust_string_lossy(scope);
let filename = message
.get_script_resource_name(scope)
.map(|name| name.to_rust_string_lossy(scope))
.unwrap_or_else(|| CODE_MODE_MAIN_FILENAME.to_string());
let line = message.get_line_number(scope).unwrap_or_default();
return Err(format!("{filename}:{line}: {pending}"));
}
Err("code_mode top-level await did not settle".to_string())
}
async fn write_message(
stdin: &mut tokio::process::ChildStdin,
message: &HostToNodeMessage,
) -> Result<(), String> {
let line = serde_json::to_string(message)
.map_err(|err| format!("failed to serialize code_mode message: {err}"))?;
stdin
.write_all(line.as_bytes())
.await
.map_err(|err| format!("failed to write code_mode message: {err}"))?;
stdin
.write_all(b"\n")
.await
.map_err(|err| format!("failed to write code_mode message newline: {err}"))?;
stdin
.flush()
.await
.map_err(|err| format!("failed to flush code_mode message: {err}"))
fn read_content_items(
scope: &mut v8::PinScope<'_, '_>,
) -> Result<Vec<FunctionCallOutputContentItem>, String> {
let source = v8_string(
scope,
"JSON.stringify(globalThis.__codexContentItems ?? [])",
)?;
let script = v8::Script::compile(scope, source, None)
.ok_or_else(|| "failed to read code_mode content items".to_string())?;
let value = script
.run(scope)
.ok_or_else(|| "failed to evaluate code_mode content items".to_string())?;
let serialized = value
.to_string(scope)
.ok_or_else(|| "failed to serialize code_mode content items".to_string())?
.to_rust_string_lossy(scope);
let content_items = serde_json::from_str::<Vec<JsonValue>>(&serialized)
.map_err(|err| format!("invalid code_mode content items: {err}"))?;
output_content_items_from_json_values(content_items)
}
fn append_stderr(message: String, stderr: &str) -> String {
if stderr.trim().is_empty() {
return message;
}
format!("{message}\n\nnode stderr:\n{stderr}")
}
fn format_runner_failure(message: &str, status: ExitStatus, stderr: &str) -> String {
append_stderr(format!("{message} (status {status})"), stderr)
}
fn build_source(user_code: &str, enabled_tools: &[EnabledTool]) -> Result<String, String> {
fn build_bootstrap_source(enabled_tools: &[EnabledTool]) -> Result<String, String> {
let enabled_tools_json = serde_json::to_string(enabled_tools)
.map_err(|err| format!("failed to serialize enabled tools: {err}"))?;
Ok(CODE_MODE_BRIDGE_SOURCE
.replace(
"__CODE_MODE_ENABLED_TOOLS_PLACEHOLDER__",
&enabled_tools_json,
)
.replace("__CODE_MODE_USER_CODE_PLACEHOLDER__", user_code))
Ok(CODE_MODE_BOOTSTRAP_SOURCE.replace(
"__CODE_MODE_ENABLED_TOOLS_PLACEHOLDER__",
&enabled_tools_json,
))
}
fn v8_string<'s>(
scope: &mut v8::PinScope<'s, '_>,
text: &str,
) -> Result<v8::Local<'s, v8::String>, String> {
v8::String::new(scope, text).ok_or_else(|| "failed to allocate V8 string".to_string())
}
fn json_from_v8(
scope: &mut v8::PinScope<'_, '_>,
value: v8::Local<'_, v8::Value>,
) -> Result<Option<JsonValue>, String> {
if value.is_undefined() {
return Ok(None);
}
let Some(serialized) = v8::json::stringify(scope, value) else {
return Err("code_mode tool arguments must be JSON-serializable".to_string());
};
let serialized = serialized.to_rust_string_lossy(scope);
serde_json::from_str(&serialized)
.map(Some)
.map_err(|err| format!("invalid code_mode tool arguments: {err}"))
}
fn json_to_v8<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: &JsonValue,
) -> Result<v8::Local<'s, v8::Value>, String> {
let serialized = serde_json::to_string(value)
.map_err(|err| format!("failed to serialize code_mode tool response: {err}"))?;
let serialized = v8_string(scope, &serialized)?;
v8::json::parse(scope, serialized)
.ok_or_else(|| "failed to deserialize code_mode tool response into V8".to_string())
}
fn throw_v8_exception<'s, T>(
scope: &mut v8::PinScope<'s, '_>,
message: &str,
) -> Option<v8::Local<'s, T>> {
if let Some(message) = v8::String::new(scope, message) {
scope.throw_exception(message.into());
}
None
}
fn format_v8_exception(try_catch: &mut v8::PinnedRef<'_, v8::TryCatch<v8::HandleScope>>) -> String {
let Some(exception) = try_catch.exception() else {
return "JavaScript execution failed".to_string();
};
if let Some(stack_trace) = try_catch.stack_trace()
&& let Some(stack_trace) = stack_trace.to_string(try_catch)
{
let stack_trace = stack_trace.to_rust_string_lossy(try_catch);
if !stack_trace.trim().is_empty() {
return stack_trace;
}
}
let exception_string = exception
.to_string(try_catch)
.map(|value| value.to_rust_string_lossy(try_catch))
.unwrap_or_else(|| "JavaScript execution failed".to_string());
let Some(message) = try_catch.message() else {
return exception_string;
};
let filename = message
.get_script_resource_name(try_catch)
.and_then(|value| value.to_string(try_catch))
.map(|value| value.to_rust_string_lossy(try_catch))
.unwrap_or_else(|| "(unknown)".to_string());
let line = message.get_line_number(try_catch).unwrap_or_default();
format!("{filename}:{line}: {exception_string}")
}
fn format_v8_value(scope: &mut v8::PinScope<'_, '_>, value: v8::Local<'_, v8::Value>) -> String {
if value.is_object()
&& let Ok(object) = v8::Local::<v8::Object>::try_from(value)
&& let Some(stack_key) = v8::String::new(scope, "stack")
&& let Some(stack_value) = object.get(scope, stack_key.into())
&& let Some(stack_value) = stack_value.to_string(scope)
{
let stack_value = stack_value.to_rust_string_lossy(scope);
if !stack_value.trim().is_empty() {
return stack_value;
}
}
value
.to_string(scope)
.map(|value| value.to_rust_string_lossy(scope))
.unwrap_or_else(|| "JavaScript execution failed".to_string())
}
fn is_valid_identifier(name: &str) -> bool {
let mut chars = name.chars();
match chars.next() {
Some(c) if c == '_' || c == '$' || c.is_ascii_alphabetic() => {}
_ => return false,
}
chars.all(|c| c == '_' || c == '$' || c.is_ascii_alphanumeric())
}
fn build_enabled_tools(exec: &ExecContext) -> Vec<EnabledTool> {

View File

@@ -71,5 +71,3 @@ for (const name of __codexEnabledToolNames) {
});
}
}
__CODE_MODE_USER_CODE_PLACEHOLDER__

View File

@@ -1,205 +0,0 @@
'use strict';
const readline = require('node:readline');
const vm = require('node:vm');
const { SourceTextModule, SyntheticModule } = vm;
function createProtocol() {
const rl = readline.createInterface({
input: process.stdin,
crlfDelay: Infinity,
});
let nextId = 0;
const pending = new Map();
let initResolve;
let initReject;
const init = new Promise((resolve, reject) => {
initResolve = resolve;
initReject = reject;
});
rl.on('line', (line) => {
if (!line.trim()) {
return;
}
let message;
try {
message = JSON.parse(line);
} catch (error) {
initReject(error);
return;
}
if (message.type === 'init') {
initResolve(message);
return;
}
if (message.type === 'response') {
const entry = pending.get(message.id);
if (!entry) {
return;
}
pending.delete(message.id);
entry.resolve(Array.isArray(message.content_items) ? message.content_items : []);
return;
}
initReject(new Error(`Unknown protocol message type: ${message.type}`));
});
rl.on('close', () => {
const error = new Error('stdin closed');
initReject(error);
for (const entry of pending.values()) {
entry.reject(error);
}
pending.clear();
});
function send(message) {
return new Promise((resolve, reject) => {
process.stdout.write(`${JSON.stringify(message)}\n`, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
function request(type, payload) {
const id = `msg-${++nextId}`;
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
void send({ type, id, ...payload }).catch((error) => {
pending.delete(id);
reject(error);
});
});
}
return { init, request, send };
}
function readContentItems(context) {
try {
const serialized = vm.runInContext('JSON.stringify(globalThis.__codexContentItems ?? [])', context);
const contentItems = JSON.parse(serialized);
return Array.isArray(contentItems) ? contentItems : [];
} catch {
return [];
}
}
function isValidIdentifier(name) {
return /^[A-Za-z_$][0-9A-Za-z_$]*$/.test(name);
}
function createToolsNamespace(protocol, enabledTools) {
const tools = Object.create(null);
for (const { name } of enabledTools) {
const callTool = async (args) =>
protocol.request('tool_call', {
name: String(name),
input: args,
});
Object.defineProperty(tools, name, {
value: callTool,
configurable: false,
enumerable: true,
writable: false,
});
}
return Object.freeze(tools);
}
function createToolsModule(context, protocol, enabledTools) {
const tools = createToolsNamespace(protocol, enabledTools);
const exportNames = ['tools'];
for (const { name } of enabledTools) {
if (name !== 'tools' && isValidIdentifier(name)) {
exportNames.push(name);
}
}
const uniqueExportNames = [...new Set(exportNames)];
return new SyntheticModule(
uniqueExportNames,
function initToolsModule() {
this.setExport('tools', tools);
for (const exportName of uniqueExportNames) {
if (exportName !== 'tools') {
this.setExport(exportName, tools[exportName]);
}
}
},
{ context }
);
}
async function runModule(context, protocol, request) {
const toolsModule = createToolsModule(context, protocol, request.enabled_tools ?? []);
const mainModule = new SourceTextModule(request.source, {
context,
identifier: 'code_mode_main.mjs',
importModuleDynamically(specifier) {
if (specifier === 'tools.js') {
return toolsModule;
}
throw new Error(`Unsupported import in code_mode: ${specifier}`);
},
});
await mainModule.link(async (specifier) => {
if (specifier === 'tools.js') {
return toolsModule;
}
throw new Error(`Unsupported import in code_mode: ${specifier}`);
});
await mainModule.evaluate();
}
async function main() {
const protocol = createProtocol();
const request = await protocol.init;
const context = vm.createContext({
__codex_tool_call: async (name, input) =>
protocol.request('tool_call', {
name: String(name),
input,
}),
});
try {
await runModule(context, protocol, request);
await protocol.send({
type: 'result',
content_items: readContentItems(context),
});
process.exit(0);
} catch (error) {
process.stderr.write(`${String(error && error.stack ? error.stack : error)}\n`);
await protocol.send({
type: 'result',
content_items: readContentItems(context),
});
process.exit(1);
}
}
void main().catch(async (error) => {
try {
process.stderr.write(`${String(error && error.stack ? error.stack : error)}\n`);
} finally {
process.exitCode = 1;
}
});

View File

@@ -1549,7 +1549,7 @@ source: /[\s\S]+/
enabled_tool_names.join(", ")
};
let description = format!(
"Runs JavaScript in a Node-backed `node:vm` context. This is a freeform tool: send raw JavaScript source text (no JSON/quotes/markdown fences). Direct tool calls remain available while `code_mode` is enabled. Inside JavaScript, import nested tools from `tools.js`, for example `import {{ exec_command }} from \"tools.js\"` or `import {{ tools }} from \"tools.js\"`. `tools[name]` and identifier wrappers like `await shell(args)` remain available for compatibility when the tool name is a valid JS identifier. Nested tool calls resolve to arrays of content items. Function tools require JSON object arguments. Freeform tools require raw strings. Use synchronous `add_content(value)` with a content item or content-item array, including `add_content(await exec_command(...))`, to return the same content items a direct tool call would expose to the model. Only content passed to `add_content(value)` is surfaced back to the model. Enabled nested tools: {enabled_list}."
"Runs JavaScript in an embedded V8 runtime. This is a freeform tool: send raw JavaScript source text (no JSON/quotes/markdown fences). Direct tool calls remain available while `code_mode` is enabled. Inside JavaScript, import nested tools from `tools.js`, for example `import {{ exec_command }} from \"tools.js\"` or `import {{ tools }} from \"tools.js\"`. `tools[name]` and identifier wrappers like `await shell(args)` remain available for compatibility when the tool name is a valid JS identifier. Nested tool calls resolve to arrays of content items. Function tools require JSON object arguments. Freeform tools require raw strings. Use synchronous `add_content(value)` with a content item or content-item array, including `add_content(await exec_command(...))`, to return the same content items a direct tool call would expose to the model. Only content passed to `add_content(value)` is surfaced back to the model. Enabled nested tools: {enabled_list}."
);
ToolSpec::Freeform(FreeformTool {

View File

@@ -16,6 +16,7 @@ use core_test_support::test_codex::test_codex;
use pretty_assertions::assert_eq;
use regex_lite::Regex;
use std::fs;
use std::path::PathBuf;
use wiremock::MockServer;
fn custom_tool_output_text_and_success(
@@ -134,3 +135,51 @@ async fn code_mode_can_apply_patch_via_nested_tool() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn code_mode_does_not_require_node_startup_validation() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let mut builder = test_codex().with_config(|config| {
let _ = config.features.enable(Feature::CodeMode);
config.js_repl_node_path = Some(PathBuf::from("/definitely/missing/node"));
});
let test = builder.build(&server).await?;
responses::mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-1"),
ev_custom_tool_call(
"call-1",
"code_mode",
"add_content({ type: \"input_text\", text: \"v8-backed\" });",
),
ev_completed("resp-1"),
]),
)
.await;
let second_mock = responses::mount_sse_once(
&server,
sse(vec![
ev_assistant_message("msg-1", "done"),
ev_completed("resp-2"),
]),
)
.await;
test.submit_turn("use code_mode without Node").await?;
let req = second_mock.single_request();
let (output, success) = custom_tool_output_text_and_success(&req, "call-1");
assert_ne!(
success,
Some(false),
"code_mode call failed unexpectedly: {output}"
);
assert_eq!(output, "v8-backed");
Ok(())
}