mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Remove js_repl feature (#19410)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
exports_files([
|
||||
"clippy.toml",
|
||||
"node-version.txt",
|
||||
])
|
||||
|
||||
filegroup(
|
||||
|
||||
@@ -1233,7 +1233,7 @@ If the session approval policy uses `Granular` with `request_permissions: false`
|
||||
|
||||
`dynamicTools` on `thread/start` and the corresponding `item/tool/call` request/response flow are experimental APIs. To enable them, set `initialize.params.capabilities.experimentalApi = true`.
|
||||
|
||||
Each dynamic tool may set `deferLoading`. When omitted, it defaults to `false`. Set it to `true` to keep the tool registered and callable by runtime features such as `js_repl`, while excluding it from the model-facing tool list sent on ordinary turns. When `tool_search` is available, deferred dynamic tools are searchable and can be exposed by a matching search result.
|
||||
Each dynamic tool may set `deferLoading`. When omitted, it defaults to `false`. Set it to `true` to keep the tool registered and callable by runtime features such as `code_mode`, while excluding it from the model-facing tool list sent on ordinary turns. When `tool_search` is available, deferred dynamic tools are searchable and can be exposed by a matching search result.
|
||||
|
||||
When a dynamic tool is invoked during a turn, the server sends an `item/tool/call` JSON-RPC request to the client:
|
||||
|
||||
|
||||
@@ -212,10 +212,12 @@ pub struct ConfigToml {
|
||||
/// Default: `300000` (5 minutes).
|
||||
pub background_terminal_max_timeout: Option<u64>,
|
||||
|
||||
/// Optional absolute path to the Node runtime used by `js_repl`.
|
||||
/// Deprecated: ignored.
|
||||
#[schemars(skip)]
|
||||
pub js_repl_node_path: Option<AbsolutePathBuf>,
|
||||
|
||||
/// Ordered list of directories to search for Node modules in `js_repl`.
|
||||
/// Deprecated: ignored.
|
||||
#[schemars(skip)]
|
||||
pub js_repl_node_module_dirs: Option<Vec<AbsolutePathBuf>>,
|
||||
|
||||
/// Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution.
|
||||
|
||||
@@ -41,8 +41,11 @@ pub struct ConfigProfile {
|
||||
pub chatgpt_base_url: Option<String>,
|
||||
/// Optional path to a file containing model instructions.
|
||||
pub model_instructions_file: Option<AbsolutePathBuf>,
|
||||
/// Deprecated: ignored.
|
||||
#[schemars(skip)]
|
||||
pub js_repl_node_path: Option<AbsolutePathBuf>,
|
||||
/// Ordered list of directories to search for Node modules in `js_repl`.
|
||||
/// Deprecated: ignored.
|
||||
#[schemars(skip)]
|
||||
pub js_repl_node_module_dirs: Option<Vec<AbsolutePathBuf>>,
|
||||
/// Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution.
|
||||
pub zsh_path: Option<AbsolutePathBuf>,
|
||||
|
||||
@@ -19,9 +19,7 @@ codex_rust_crate(
|
||||
"Cargo.toml",
|
||||
],
|
||||
allow_empty = True,
|
||||
) + [
|
||||
"//codex-rs:node-version.txt",
|
||||
],
|
||||
),
|
||||
rustc_env = {
|
||||
# Keep manifest-root path lookups inside the Bazel execroot for code
|
||||
# that relies on env!("CARGO_MANIFEST_DIR").
|
||||
|
||||
@@ -583,16 +583,6 @@
|
||||
"include_permissions_instructions": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"js_repl_node_module_dirs": {
|
||||
"description": "Ordered list of directories to search for Node modules in `js_repl`.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"js_repl_node_path": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2849,21 +2839,6 @@
|
||||
"description": "System instructions.",
|
||||
"type": "string"
|
||||
},
|
||||
"js_repl_node_module_dirs": {
|
||||
"description": "Ordered list of directories to search for Node modules in `js_repl`.",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"js_repl_node_path": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
}
|
||||
],
|
||||
"description": "Optional absolute path to the Node runtime used by `js_repl`."
|
||||
},
|
||||
"log_dir": {
|
||||
"allOf": [
|
||||
{
|
||||
|
||||
@@ -267,7 +267,6 @@ mod reload {
|
||||
model_provider: preserve_current_provider.then(|| config.model_provider_id.clone()),
|
||||
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
|
||||
main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(),
|
||||
js_repl_node_path: config.js_repl_node_path.clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,41 +42,6 @@ pub const LOCAL_AGENTS_MD_FILENAME: &str = "AGENTS.override.md";
|
||||
/// be concatenated with the following separator.
|
||||
const AGENTS_MD_SEPARATOR: &str = "\n\n--- project-doc ---\n\n";
|
||||
|
||||
fn render_js_repl_instructions(config: &Config) -> Option<String> {
|
||||
if !config.features.enabled(Feature::JsRepl) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut section = String::from("## JavaScript REPL (Node)\n");
|
||||
section.push_str(
|
||||
"- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n",
|
||||
);
|
||||
section.push_str("- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n");
|
||||
section.push_str(
|
||||
"- Helpers: `codex.cwd`, `codex.homeDir`, `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n",
|
||||
);
|
||||
section.push_str("- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n");
|
||||
section.push_str("- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }` containing encoded PNG/JPEG/WebP/GIF bytes, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n");
|
||||
section.push_str("- `codex.tool(...)` and `codex.emitImage(...)` keep stable helper identities across cells. Saved references and persisted objects can reuse them in later cells, but async callbacks that fire after a cell finishes still fail because no exec is active.\n");
|
||||
section.push_str("- Request full-resolution image processing with `detail: \"original\"` only when the `view_image` tool schema includes a `detail` argument. The same availability applies to `codex.emitImage(...)`: if `view_image.detail` is present, you may also pass `detail: \"original\"` there. Use this when high-fidelity image perception or precise localization is needed, especially for CUA agents.\n");
|
||||
section.push_str("- Raw MCP image blocks can request the same behavior by returning `_meta: { \"codex/imageDetail\": \"original\" }` on the image content item.\n");
|
||||
section.push_str("- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\", detail: \"original\" })`.\n");
|
||||
section.push_str("- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\", detail: \"original\" }))`.\n");
|
||||
section.push_str("- When encoding an image to send with `codex.emitImage(...)` or `view_image`, prefer JPEG at about 85 quality when lossy compression is acceptable; use PNG when transparency or lossless detail matters. Smaller uploads are faster and less likely to hit size limits.\n");
|
||||
section.push_str("- Top-level bindings persist across cells. If a cell throws, prior bindings remain available and bindings that finished initializing before the throw often remain usable in later cells. For code you plan to reuse across cells, prefer declaring or assigning it in direct top-level statements before operations that might throw. If you hit `SyntaxError: Identifier 'x' has already been declared`, first reuse the existing binding, reassign a previously declared `let`, or pick a new descriptive name. Use `{ ... }` only for a short temporary block when you specifically need local scratch names; do not wrap an entire cell in block scope if you want those names reusable later. Reset the kernel with `js_repl_reset` only when you need a clean state.\n");
|
||||
section.push_str("- Top-level static import declarations (for example `import x from \"./file.js\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")`, `await import(\"./file.js\")`, or `await import(\"/abs/path/file.mjs\")` instead. Imported local files must be ESM `.js`/`.mjs` files and run in the same REPL VM context. Bare package imports always resolve from REPL-global search roots (`CODEX_JS_REPL_NODE_MODULE_DIRS`, then cwd), not relative to the imported file location. Local files may statically import only other local relative/absolute/`file://` `.js`/`.mjs` files; package and builtin imports from local files must stay dynamic. `import.meta.resolve()` returns importable strings such as `file://...`, bare package names, and `node:...` specifiers. Local file modules reload between execs, while top-level bindings persist until `js_repl_reset`.\n");
|
||||
|
||||
if config.features.enabled(Feature::JsReplToolsOnly) {
|
||||
section.push_str("- Do not call tools directly; use `js_repl` + `codex.tool(...)` for all tool calls, including shell commands.\n");
|
||||
section
|
||||
.push_str("- MCP tools (if any) can also be called by name via `codex.tool(...)`.\n");
|
||||
}
|
||||
|
||||
section.push_str("- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.");
|
||||
|
||||
Some(section)
|
||||
}
|
||||
|
||||
/// Resolves AGENTS.md files into model-visible user instructions and source
|
||||
/// paths.
|
||||
pub struct AgentsMdManager<'a> {
|
||||
@@ -147,13 +112,6 @@ impl<'a> AgentsMdManager<'a> {
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(js_repl_section) = render_js_repl_instructions(self.config) {
|
||||
if !output.is_empty() {
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
output.push_str(&js_repl_section);
|
||||
}
|
||||
|
||||
if self.config.features.enabled(Feature::ChildAgentsMd) {
|
||||
if !output.is_empty() {
|
||||
output.push_str("\n\n");
|
||||
|
||||
@@ -199,40 +199,6 @@ async fn zero_byte_limit_disables_discovery() {
|
||||
assert_eq!(discovery, Vec::<AbsolutePathBuf>::new());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn js_repl_instructions_are_appended_when_enabled() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let mut cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await;
|
||||
cfg.features
|
||||
.enable(Feature::JsRepl)
|
||||
.expect("test config should allow js_repl");
|
||||
|
||||
let res = get_user_instructions(&cfg)
|
||||
.await
|
||||
.expect("js_repl instructions expected");
|
||||
let expected = "## JavaScript REPL (Node)\n- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n- Helpers: `codex.cwd`, `codex.homeDir`, `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }` containing encoded PNG/JPEG/WebP/GIF bytes, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n- `codex.tool(...)` and `codex.emitImage(...)` keep stable helper identities across cells. Saved references and persisted objects can reuse them in later cells, but async callbacks that fire after a cell finishes still fail because no exec is active.\n- Request full-resolution image processing with `detail: \"original\"` only when the `view_image` tool schema includes a `detail` argument. The same availability applies to `codex.emitImage(...)`: if `view_image.detail` is present, you may also pass `detail: \"original\"` there. Use this when high-fidelity image perception or precise localization is needed, especially for CUA agents.\n- Raw MCP image blocks can request the same behavior by returning `_meta: { \"codex/imageDetail\": \"original\" }` on the image content item.\n- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\", detail: \"original\" })`.\n- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\", detail: \"original\" }))`.\n- When encoding an image to send with `codex.emitImage(...)` or `view_image`, prefer JPEG at about 85 quality when lossy compression is acceptable; use PNG when transparency or lossless detail matters. Smaller uploads are faster and less likely to hit size limits.\n- Top-level bindings persist across cells. If a cell throws, prior bindings remain available and bindings that finished initializing before the throw often remain usable in later cells. For code you plan to reuse across cells, prefer declaring or assigning it in direct top-level statements before operations that might throw. If you hit `SyntaxError: Identifier 'x' has already been declared`, first reuse the existing binding, reassign a previously declared `let`, or pick a new descriptive name. Use `{ ... }` only for a short temporary block when you specifically need local scratch names; do not wrap an entire cell in block scope if you want those names reusable later. Reset the kernel with `js_repl_reset` only when you need a clean state.\n- Top-level static import declarations (for example `import x from \"./file.js\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")`, `await import(\"./file.js\")`, or `await import(\"/abs/path/file.mjs\")` instead. Imported local files must be ESM `.js`/`.mjs` files and run in the same REPL VM context. Bare package imports always resolve from REPL-global search roots (`CODEX_JS_REPL_NODE_MODULE_DIRS`, then cwd), not relative to the imported file location. Local files may statically import only other local relative/absolute/`file://` `.js`/`.mjs` files; package and builtin imports from local files must stay dynamic. `import.meta.resolve()` returns importable strings such as `file://...`, bare package names, and `node:...` specifiers. Local file modules reload between execs, while top-level bindings persist until `js_repl_reset`.\n- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.";
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn js_repl_tools_only_instructions_are_feature_gated() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let mut cfg = make_config(&tmp, /*limit*/ 4096, /*instructions*/ None).await;
|
||||
let mut features = cfg.features.get().clone();
|
||||
features
|
||||
.enable(Feature::JsRepl)
|
||||
.enable(Feature::JsReplToolsOnly);
|
||||
cfg.features
|
||||
.set(features)
|
||||
.expect("test config should allow js_repl tool restrictions");
|
||||
|
||||
let res = get_user_instructions(&cfg)
|
||||
.await
|
||||
.expect("js_repl instructions expected");
|
||||
let expected = "## JavaScript REPL (Node)\n- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n- Helpers: `codex.cwd`, `codex.homeDir`, `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }` containing encoded PNG/JPEG/WebP/GIF bytes, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n- `codex.tool(...)` and `codex.emitImage(...)` keep stable helper identities across cells. Saved references and persisted objects can reuse them in later cells, but async callbacks that fire after a cell finishes still fail because no exec is active.\n- Request full-resolution image processing with `detail: \"original\"` only when the `view_image` tool schema includes a `detail` argument. The same availability applies to `codex.emitImage(...)`: if `view_image.detail` is present, you may also pass `detail: \"original\"` there. Use this when high-fidelity image perception or precise localization is needed, especially for CUA agents.\n- Raw MCP image blocks can request the same behavior by returning `_meta: { \"codex/imageDetail\": \"original\" }` on the image content item.\n- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\", detail: \"original\" })`.\n- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\", detail: \"original\" }))`.\n- When encoding an image to send with `codex.emitImage(...)` or `view_image`, prefer JPEG at about 85 quality when lossy compression is acceptable; use PNG when transparency or lossless detail matters. Smaller uploads are faster and less likely to hit size limits.\n- Top-level bindings persist across cells. If a cell throws, prior bindings remain available and bindings that finished initializing before the throw often remain usable in later cells. For code you plan to reuse across cells, prefer declaring or assigning it in direct top-level statements before operations that might throw. If you hit `SyntaxError: Identifier 'x' has already been declared`, first reuse the existing binding, reassign a previously declared `let`, or pick a new descriptive name. Use `{ ... }` only for a short temporary block when you specifically need local scratch names; do not wrap an entire cell in block scope if you want those names reusable later. Reset the kernel with `js_repl_reset` only when you need a clean state.\n- Top-level static import declarations (for example `import x from \"./file.js\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")`, `await import(\"./file.js\")`, or `await import(\"/abs/path/file.mjs\")` instead. Imported local files must be ESM `.js`/`.mjs` files and run in the same REPL VM context. Bare package imports always resolve from REPL-global search roots (`CODEX_JS_REPL_NODE_MODULE_DIRS`, then cwd), not relative to the imported file location. Local files may statically import only other local relative/absolute/`file://` `.js`/`.mjs` files; package and builtin imports from local files must stay dynamic. `import.meta.resolve()` returns importable strings such as `file://...`, bare package names, and `node:...` specifiers. Local file modules reload between execs, while top-level bindings persist until `js_repl_reset`.\n- Do not call tools directly; use `js_repl` + `codex.tool(...)` for all tool calls, including shell commands.\n- MCP tools (if any) can also be called by name via `codex.tool(...)`.\n- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.";
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
|
||||
/// When both system instructions and AGENTS.md docs are present the two
|
||||
/// should be concatenated with the separator.
|
||||
#[tokio::test]
|
||||
|
||||
@@ -5260,8 +5260,6 @@ async fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
codex_self_exe: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
main_execve_wrapper_exe: None,
|
||||
js_repl_node_path: None,
|
||||
js_repl_node_module_dirs: Vec::new(),
|
||||
zsh_path: None,
|
||||
hide_agent_reasoning: false,
|
||||
show_raw_agent_reasoning: false,
|
||||
@@ -5458,8 +5456,6 @@ async fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
codex_self_exe: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
main_execve_wrapper_exe: None,
|
||||
js_repl_node_path: None,
|
||||
js_repl_node_module_dirs: Vec::new(),
|
||||
zsh_path: None,
|
||||
hide_agent_reasoning: false,
|
||||
show_raw_agent_reasoning: false,
|
||||
@@ -5610,8 +5606,6 @@ async fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
codex_self_exe: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
main_execve_wrapper_exe: None,
|
||||
js_repl_node_path: None,
|
||||
js_repl_node_module_dirs: Vec::new(),
|
||||
zsh_path: None,
|
||||
hide_agent_reasoning: false,
|
||||
show_raw_agent_reasoning: false,
|
||||
@@ -5747,8 +5741,6 @@ async fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
codex_self_exe: None,
|
||||
codex_linux_sandbox_exe: None,
|
||||
main_execve_wrapper_exe: None,
|
||||
js_repl_node_path: None,
|
||||
js_repl_node_module_dirs: Vec::new(),
|
||||
zsh_path: None,
|
||||
hide_agent_reasoning: false,
|
||||
show_raw_agent_reasoning: false,
|
||||
|
||||
@@ -495,12 +495,6 @@ pub struct Config {
|
||||
/// code via [`ConfigOverrides`].
|
||||
pub main_execve_wrapper_exe: Option<PathBuf>,
|
||||
|
||||
/// Optional absolute path to the Node runtime used by `js_repl`.
|
||||
pub js_repl_node_path: Option<PathBuf>,
|
||||
|
||||
/// Ordered list of directories to search for Node modules in `js_repl`.
|
||||
pub js_repl_node_module_dirs: Vec<PathBuf>,
|
||||
|
||||
/// Optional absolute path to patched zsh used by zsh-exec-bridge-backed shell execution.
|
||||
pub zsh_path: Option<PathBuf>,
|
||||
|
||||
@@ -1422,8 +1416,6 @@ pub struct ConfigOverrides {
|
||||
pub codex_self_exe: Option<PathBuf>,
|
||||
pub codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
pub main_execve_wrapper_exe: Option<PathBuf>,
|
||||
pub js_repl_node_path: Option<PathBuf>,
|
||||
pub js_repl_node_module_dirs: Option<Vec<PathBuf>>,
|
||||
pub zsh_path: Option<PathBuf>,
|
||||
pub base_instructions: Option<String>,
|
||||
pub developer_instructions: Option<String>,
|
||||
@@ -1642,8 +1634,6 @@ impl Config {
|
||||
codex_self_exe,
|
||||
codex_linux_sandbox_exe,
|
||||
main_execve_wrapper_exe,
|
||||
js_repl_node_path: js_repl_node_path_override,
|
||||
js_repl_node_module_dirs: js_repl_node_module_dirs_override,
|
||||
zsh_path: zsh_path_override,
|
||||
base_instructions,
|
||||
developer_instructions,
|
||||
@@ -2177,20 +2167,6 @@ impl Config {
|
||||
)
|
||||
.await?;
|
||||
let compact_prompt = compact_prompt.or(file_compact_prompt);
|
||||
let js_repl_node_path = js_repl_node_path_override
|
||||
.or(config_profile.js_repl_node_path.map(Into::into))
|
||||
.or(cfg.js_repl_node_path.map(Into::into));
|
||||
let js_repl_node_module_dirs = js_repl_node_module_dirs_override
|
||||
.or_else(|| {
|
||||
config_profile
|
||||
.js_repl_node_module_dirs
|
||||
.map(|dirs| dirs.into_iter().map(Into::into).collect::<Vec<PathBuf>>())
|
||||
})
|
||||
.or_else(|| {
|
||||
cfg.js_repl_node_module_dirs
|
||||
.map(|dirs| dirs.into_iter().map(Into::into).collect::<Vec<PathBuf>>())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let zsh_path = zsh_path_override
|
||||
.or(config_profile.zsh_path.map(Into::into))
|
||||
.or(cfg.zsh_path.map(Into::into));
|
||||
@@ -2414,8 +2390,6 @@ impl Config {
|
||||
codex_self_exe,
|
||||
codex_linux_sandbox_exe,
|
||||
main_execve_wrapper_exe,
|
||||
js_repl_node_path,
|
||||
js_repl_node_module_dirs,
|
||||
zsh_path,
|
||||
|
||||
hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false),
|
||||
|
||||
@@ -148,8 +148,6 @@ struct GuardianReviewSessionReuseKey {
|
||||
mcp_servers: Constrained<HashMap<String, McpServerConfig>>,
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
main_execve_wrapper_exe: Option<PathBuf>,
|
||||
js_repl_node_path: Option<PathBuf>,
|
||||
js_repl_node_module_dirs: Vec<PathBuf>,
|
||||
zsh_path: Option<PathBuf>,
|
||||
features: ManagedFeatures,
|
||||
include_apply_patch_tool: bool,
|
||||
@@ -175,8 +173,6 @@ impl GuardianReviewSessionReuseKey {
|
||||
mcp_servers: spawn_config.mcp_servers.clone(),
|
||||
codex_linux_sandbox_exe: spawn_config.codex_linux_sandbox_exe.clone(),
|
||||
main_execve_wrapper_exe: spawn_config.main_execve_wrapper_exe.clone(),
|
||||
js_repl_node_path: spawn_config.js_repl_node_path.clone(),
|
||||
js_repl_node_module_dirs: spawn_config.js_repl_node_module_dirs.clone(),
|
||||
zsh_path: spawn_config.zsh_path.clone(),
|
||||
features: spawn_config.features.clone(),
|
||||
include_apply_patch_tool: spawn_config.include_apply_patch_tool,
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
pub(crate) use codex_tools::can_request_original_image_detail;
|
||||
pub(crate) use codex_tools::normalize_output_image_detail;
|
||||
pub(crate) use codex_tools::sanitize_original_image_detail;
|
||||
|
||||
@@ -293,8 +293,6 @@ use crate::tasks::GhostSnapshotTask;
|
||||
use crate::tasks::ReviewTask;
|
||||
use crate::tasks::SessionTask;
|
||||
use crate::tasks::SessionTaskContext;
|
||||
use crate::tools::js_repl::JsReplHandle;
|
||||
use crate::tools::js_repl::resolve_compatible_node;
|
||||
use crate::tools::network_approval::NetworkApprovalService;
|
||||
use crate::tools::network_approval::build_blocked_request_observer;
|
||||
use crate::tools::network_approval::build_network_policy_decider;
|
||||
@@ -500,34 +498,6 @@ impl Codex {
|
||||
let _ = config.features.disable(Feature::Collab);
|
||||
}
|
||||
|
||||
if config.features.enabled(Feature::JsRepl)
|
||||
&& let Err(err) = resolve_compatible_node(config.js_repl_node_path.as_deref()).await
|
||||
{
|
||||
let _ = config.features.disable(Feature::JsRepl);
|
||||
let _ = config.features.disable(Feature::JsReplToolsOnly);
|
||||
let message = if config.features.enabled(Feature::JsRepl) {
|
||||
format!(
|
||||
"`js_repl` remains enabled because enterprise requirements pin it on, but the configured Node runtime is unavailable or incompatible. {err}"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Disabled `js_repl` for this session because the configured Node runtime is unavailable or incompatible. {err}"
|
||||
)
|
||||
};
|
||||
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 `exec` 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 user_instructions = AgentsMdManager::new(&config)
|
||||
.user_instructions(environment.as_deref())
|
||||
.await;
|
||||
|
||||
@@ -136,7 +136,6 @@ pub(super) async fn spawn_review_thread(
|
||||
codex_self_exe: parent_turn_context.codex_self_exe.clone(),
|
||||
codex_linux_sandbox_exe: parent_turn_context.codex_linux_sandbox_exe.clone(),
|
||||
tool_call_gate: Arc::new(ReadinessFlag::new()),
|
||||
js_repl: Arc::clone(&sess.js_repl),
|
||||
dynamic_tools: parent_turn_context.dynamic_tools.clone(),
|
||||
truncation_policy: model_info.truncation_policy.into(),
|
||||
turn_metadata_state,
|
||||
|
||||
@@ -25,7 +25,6 @@ pub(crate) struct Session {
|
||||
pub(super) idle_pending_input: Mutex<Vec<ResponseInputItem>>, // TODO (jif) merge with mailbox!
|
||||
pub(crate) guardian_review_session: GuardianReviewSessionManager,
|
||||
pub(crate) services: SessionServices,
|
||||
pub(super) js_repl: Arc<JsReplHandle>,
|
||||
pub(super) next_internal_sub_id: AtomicU64,
|
||||
}
|
||||
|
||||
@@ -766,18 +765,12 @@ impl Session {
|
||||
config.features.enabled(Feature::RuntimeMetrics),
|
||||
Self::build_model_client_beta_features_header(config.as_ref()),
|
||||
),
|
||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(
|
||||
config.js_repl_node_path.clone(),
|
||||
),
|
||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(),
|
||||
environment_manager,
|
||||
};
|
||||
services
|
||||
.model_client
|
||||
.set_window_generation(window_generation);
|
||||
let js_repl = Arc::new(JsReplHandle::with_node_path(
|
||||
config.js_repl_node_path.clone(),
|
||||
config.js_repl_node_module_dirs.clone(),
|
||||
));
|
||||
let (out_of_band_elicitation_paused, _out_of_band_elicitation_paused_rx) =
|
||||
watch::channel(false);
|
||||
|
||||
@@ -798,7 +791,6 @@ impl Session {
|
||||
idle_pending_input: Mutex::new(Vec::new()),
|
||||
guardian_review_session: GuardianReviewSessionManager::default(),
|
||||
services,
|
||||
js_repl,
|
||||
next_internal_sub_id: AtomicU64::new(0),
|
||||
});
|
||||
if let Some(network_policy_decider_session) = network_policy_decider_session {
|
||||
|
||||
@@ -3311,15 +3311,9 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
config.features.enabled(Feature::RuntimeMetrics),
|
||||
Session::build_model_client_beta_features_header(config.as_ref()),
|
||||
),
|
||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(
|
||||
config.js_repl_node_path.clone(),
|
||||
),
|
||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(),
|
||||
environment_manager: Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
};
|
||||
let js_repl = Arc::new(JsReplHandle::with_node_path(
|
||||
config.js_repl_node_path.clone(),
|
||||
config.js_repl_node_module_dirs.clone(),
|
||||
));
|
||||
|
||||
let plugin_outcome = services
|
||||
.plugins_manager
|
||||
@@ -3353,7 +3347,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
turn_environments,
|
||||
session_configuration.cwd.clone(),
|
||||
"turn_id".to_string(),
|
||||
Arc::clone(&js_repl),
|
||||
skills_outcome,
|
||||
);
|
||||
|
||||
@@ -3374,7 +3367,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
idle_pending_input: Mutex::new(Vec::new()),
|
||||
guardian_review_session: crate::guardian::GuardianReviewSessionManager::default(),
|
||||
services,
|
||||
js_repl,
|
||||
next_internal_sub_id: AtomicU64::new(0),
|
||||
};
|
||||
|
||||
@@ -4674,15 +4666,9 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
|
||||
config.features.enabled(Feature::RuntimeMetrics),
|
||||
Session::build_model_client_beta_features_header(config.as_ref()),
|
||||
),
|
||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(
|
||||
config.js_repl_node_path.clone(),
|
||||
),
|
||||
code_mode_service: crate::tools::code_mode::CodeModeService::new(),
|
||||
environment_manager: Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()),
|
||||
};
|
||||
let js_repl = Arc::new(JsReplHandle::with_node_path(
|
||||
config.js_repl_node_path.clone(),
|
||||
config.js_repl_node_module_dirs.clone(),
|
||||
));
|
||||
|
||||
let plugin_outcome = services
|
||||
.plugins_manager
|
||||
@@ -4716,7 +4702,6 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
|
||||
turn_environments,
|
||||
session_configuration.cwd.clone(),
|
||||
"turn_id".to_string(),
|
||||
Arc::clone(&js_repl),
|
||||
skills_outcome,
|
||||
));
|
||||
|
||||
@@ -4737,7 +4722,6 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
|
||||
idle_pending_input: Mutex::new(Vec::new()),
|
||||
guardian_review_session: crate::guardian::GuardianReviewSessionManager::default(),
|
||||
services,
|
||||
js_repl,
|
||||
next_internal_sub_id: AtomicU64::new(0),
|
||||
});
|
||||
|
||||
|
||||
@@ -87,7 +87,6 @@ pub(crate) struct TurnContext {
|
||||
pub(crate) codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
pub(crate) tool_call_gate: Arc<ReadinessFlag>,
|
||||
pub(crate) truncation_policy: TruncationPolicy,
|
||||
pub(crate) js_repl: Arc<JsReplHandle>,
|
||||
pub(crate) dynamic_tools: Vec<DynamicToolSpec>,
|
||||
pub(crate) turn_metadata_state: Arc<TurnMetadataState>,
|
||||
pub(crate) turn_skills: TurnSkillsContext,
|
||||
@@ -227,7 +226,6 @@ impl TurnContext {
|
||||
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.clone(),
|
||||
tool_call_gate: Arc::new(ReadinessFlag::new()),
|
||||
truncation_policy,
|
||||
js_repl: Arc::clone(&self.js_repl),
|
||||
dynamic_tools: self.dynamic_tools.clone(),
|
||||
turn_metadata_state: self.turn_metadata_state.clone(),
|
||||
turn_skills: self.turn_skills.clone(),
|
||||
@@ -406,7 +404,6 @@ impl Session {
|
||||
environments: Vec<TurnEnvironment>,
|
||||
cwd: AbsolutePathBuf,
|
||||
sub_id: String,
|
||||
js_repl: Arc<JsReplHandle>,
|
||||
skills_outcome: Arc<SkillLoadOutcome>,
|
||||
) -> TurnContext {
|
||||
let reasoning_effort = session_configuration.collaboration_mode.reasoning_effort();
|
||||
@@ -497,7 +494,6 @@ impl Session {
|
||||
codex_linux_sandbox_exe: per_turn_config.codex_linux_sandbox_exe.clone(),
|
||||
tool_call_gate: Arc::new(ReadinessFlag::new()),
|
||||
truncation_policy: model_info.truncation_policy.into(),
|
||||
js_repl,
|
||||
dynamic_tools: session_configuration.dynamic_tools.clone(),
|
||||
turn_metadata_state,
|
||||
turn_skills: TurnSkillsContext::new(skills_outcome),
|
||||
@@ -682,7 +678,6 @@ impl Session {
|
||||
turn_environments,
|
||||
cwd,
|
||||
sub_id,
|
||||
Arc::clone(&self.js_repl),
|
||||
skills_outcome,
|
||||
);
|
||||
turn_context.realtime_active = self.conversation.running_state().await.is_some();
|
||||
|
||||
@@ -677,14 +677,6 @@ impl Session {
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn cleanup_after_interrupt(&self, turn_context: &Arc<TurnContext>) {
|
||||
if let Some(manager) = turn_context.js_repl.manager_if_initialized()
|
||||
&& let Err(err) = manager.interrupt_turn_exec(&turn_context.sub_id).await
|
||||
{
|
||||
warn!("failed to interrupt js_repl kernel: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_task_abort(self: &Arc<Self>, task: RunningTask, reason: TurnAbortReason) {
|
||||
let sub_id = task.turn_context.sub_id.clone();
|
||||
if task.cancellation_token.is_cancelled() {
|
||||
@@ -713,23 +705,19 @@ impl Session {
|
||||
.abort(session_ctx, Arc::clone(&task.turn_context))
|
||||
.await;
|
||||
|
||||
if reason == TurnAbortReason::Interrupted {
|
||||
self.cleanup_after_interrupt(&task.turn_context).await;
|
||||
|
||||
if let Some(marker) = interrupted_turn_history_marker(
|
||||
if reason == TurnAbortReason::Interrupted
|
||||
&& let Some(marker) = interrupted_turn_history_marker(
|
||||
InterruptedTurnHistoryMarker::from_config(task.turn_context.config.as_ref()),
|
||||
) {
|
||||
self.record_into_history(std::slice::from_ref(&marker), task.turn_context.as_ref())
|
||||
.await;
|
||||
self.persist_rollout_items(&[RolloutItem::ResponseItem(marker)])
|
||||
.await;
|
||||
// Ensure the marker is durably visible before emitting TurnAborted: some clients
|
||||
// synchronously re-read the rollout on receipt of the abort event.
|
||||
if let Err(err) = self.flush_rollout().await {
|
||||
warn!(
|
||||
"failed to flush interrupted-turn marker before emitting TurnAborted: {err}"
|
||||
);
|
||||
}
|
||||
)
|
||||
{
|
||||
self.record_into_history(std::slice::from_ref(&marker), task.turn_context.as_ref())
|
||||
.await;
|
||||
self.persist_rollout_items(&[RolloutItem::ResponseItem(marker)])
|
||||
.await;
|
||||
// Ensure the marker is durably visible before emitting TurnAborted: some clients
|
||||
// synchronously re-read the rollout on receipt of the abort event.
|
||||
if let Err(err) = self.flush_rollout().await {
|
||||
warn!("failed to flush interrupted-turn marker before emitting TurnAborted: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ mod response_adapter;
|
||||
mod wait_handler;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -62,7 +61,7 @@ pub(crate) struct CodeModeService {
|
||||
}
|
||||
|
||||
impl CodeModeService {
|
||||
pub(crate) fn new(_js_repl_node_path: Option<PathBuf>) -> Self {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
inner: codex_code_mode::CodeModeService::new(),
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ pub type SharedTurnDiffTracker = Arc<Mutex<TurnDiffTracker>>;
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ToolCallSource {
|
||||
Direct,
|
||||
JsRepl,
|
||||
CodeMode {
|
||||
/// Runtime cell that issued the nested tool request.
|
||||
cell_id: String,
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::context::FunctionToolOutput;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::events::ToolEmitter;
|
||||
use crate::tools::events::ToolEventCtx;
|
||||
use crate::tools::events::ToolEventFailure;
|
||||
use crate::tools::events::ToolEventStage;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::js_repl::JS_REPL_PRAGMA_PREFIX;
|
||||
use crate::tools::js_repl::JsReplArgs;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::exec_output::StreamOutput;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
|
||||
pub struct JsReplHandler;
|
||||
pub struct JsReplResetHandler;
|
||||
|
||||
fn join_outputs(stdout: &str, stderr: &str) -> String {
|
||||
if stdout.is_empty() {
|
||||
stderr.to_string()
|
||||
} else if stderr.is_empty() {
|
||||
stdout.to_string()
|
||||
} else {
|
||||
format!("{stdout}\n{stderr}")
|
||||
}
|
||||
}
|
||||
|
||||
fn build_js_repl_exec_output(
|
||||
output: &str,
|
||||
error: Option<&str>,
|
||||
duration: Duration,
|
||||
) -> ExecToolCallOutput {
|
||||
let stdout = output.to_string();
|
||||
let stderr = error.unwrap_or("").to_string();
|
||||
let aggregated_output = join_outputs(&stdout, &stderr);
|
||||
ExecToolCallOutput {
|
||||
exit_code: if error.is_some() { 1 } else { 0 },
|
||||
stdout: StreamOutput::new(stdout),
|
||||
stderr: StreamOutput::new(stderr),
|
||||
aggregated_output: StreamOutput::new(aggregated_output),
|
||||
duration,
|
||||
timed_out: false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn emit_js_repl_exec_begin(
|
||||
session: &crate::session::session::Session,
|
||||
turn: &crate::session::turn_context::TurnContext,
|
||||
call_id: &str,
|
||||
) {
|
||||
let emitter = ToolEmitter::shell(
|
||||
vec!["js_repl".to_string()],
|
||||
turn.cwd.clone(),
|
||||
ExecCommandSource::Agent,
|
||||
/*freeform*/ false,
|
||||
);
|
||||
let ctx = ToolEventCtx::new(session, turn, call_id, /*turn_diff_tracker*/ None);
|
||||
emitter.emit(ctx, ToolEventStage::Begin).await;
|
||||
}
|
||||
|
||||
async fn emit_js_repl_exec_end(
|
||||
session: &crate::session::session::Session,
|
||||
turn: &crate::session::turn_context::TurnContext,
|
||||
call_id: &str,
|
||||
output: &str,
|
||||
error: Option<&str>,
|
||||
duration: Duration,
|
||||
) {
|
||||
let exec_output = build_js_repl_exec_output(output, error, duration);
|
||||
let emitter = ToolEmitter::shell(
|
||||
vec!["js_repl".to_string()],
|
||||
turn.cwd.clone(),
|
||||
ExecCommandSource::Agent,
|
||||
/*freeform*/ false,
|
||||
);
|
||||
let ctx = ToolEventCtx::new(session, turn, call_id, /*turn_diff_tracker*/ None);
|
||||
let stage = if error.is_some() {
|
||||
ToolEventStage::Failure(ToolEventFailure::Output(exec_output))
|
||||
} else {
|
||||
ToolEventStage::Success(exec_output)
|
||||
};
|
||||
emitter.emit(ctx, stage).await;
|
||||
}
|
||||
impl ToolHandler for JsReplHandler {
|
||||
type Output = FunctionToolOutput;
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
||||
matches!(
|
||||
payload,
|
||||
ToolPayload::Function { .. } | ToolPayload::Custom { .. }
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
cancellation_token,
|
||||
tracker,
|
||||
payload,
|
||||
call_id,
|
||||
..
|
||||
} = invocation;
|
||||
|
||||
if !session.features().enabled(Feature::JsRepl) {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"js_repl is disabled by feature flag".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let args = match payload {
|
||||
ToolPayload::Function { arguments } => parse_arguments(&arguments)?,
|
||||
ToolPayload::Custom { input } => parse_freeform_args(&input)?,
|
||||
_ => {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"js_repl expects custom or function payload".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let manager = turn.js_repl.manager().await?;
|
||||
let started_at = Instant::now();
|
||||
emit_js_repl_exec_begin(session.as_ref(), turn.as_ref(), &call_id).await;
|
||||
let result = manager
|
||||
.execute_with_cancellation(
|
||||
Arc::clone(&session),
|
||||
Arc::clone(&turn),
|
||||
cancellation_token,
|
||||
tracker,
|
||||
args,
|
||||
)
|
||||
.await;
|
||||
let result = match result {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
let message = err.to_string();
|
||||
emit_js_repl_exec_end(
|
||||
session.as_ref(),
|
||||
turn.as_ref(),
|
||||
&call_id,
|
||||
"",
|
||||
Some(&message),
|
||||
started_at.elapsed(),
|
||||
)
|
||||
.await;
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
let content = result.output;
|
||||
let mut items = Vec::with_capacity(result.content_items.len() + 1);
|
||||
if !content.is_empty() {
|
||||
items.push(FunctionCallOutputContentItem::InputText {
|
||||
text: content.clone(),
|
||||
});
|
||||
}
|
||||
items.extend(result.content_items);
|
||||
|
||||
emit_js_repl_exec_end(
|
||||
session.as_ref(),
|
||||
turn.as_ref(),
|
||||
&call_id,
|
||||
&content,
|
||||
/*error*/ None,
|
||||
started_at.elapsed(),
|
||||
)
|
||||
.await;
|
||||
|
||||
if items.is_empty() {
|
||||
Ok(FunctionToolOutput::from_text(content, Some(true)))
|
||||
} else {
|
||||
Ok(FunctionToolOutput::from_content(items, Some(true)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolHandler for JsReplResetHandler {
|
||||
type Output = FunctionToolOutput;
|
||||
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
|
||||
if !invocation.session.features().enabled(Feature::JsRepl) {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"js_repl is disabled by feature flag".to_string(),
|
||||
));
|
||||
}
|
||||
let manager = invocation.turn.js_repl.manager().await?;
|
||||
manager.reset().await?;
|
||||
Ok(FunctionToolOutput::from_text(
|
||||
"js_repl kernel reset".to_string(),
|
||||
Some(true),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_freeform_args(input: &str) -> Result<JsReplArgs, FunctionCallError> {
|
||||
if input.trim().is_empty() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"js_repl expects raw JavaScript tool input (non-empty). Provide JS source text, optionally with first-line `// codex-js-repl: ...`."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut args = JsReplArgs {
|
||||
code: input.to_string(),
|
||||
timeout_ms: None,
|
||||
};
|
||||
|
||||
let mut lines = input.splitn(2, '\n');
|
||||
let first_line = lines.next().unwrap_or_default();
|
||||
let rest = lines.next().unwrap_or_default();
|
||||
let trimmed = first_line.trim_start();
|
||||
let Some(pragma) = trimmed.strip_prefix(JS_REPL_PRAGMA_PREFIX) else {
|
||||
reject_json_or_quoted_source(&args.code)?;
|
||||
return Ok(args);
|
||||
};
|
||||
|
||||
let mut timeout_ms: Option<u64> = None;
|
||||
let directive = pragma.trim();
|
||||
if !directive.is_empty() {
|
||||
for token in directive.split_whitespace() {
|
||||
let (key, value) = token.split_once('=').ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"js_repl pragma expects space-separated key=value pairs (supported keys: timeout_ms); got `{token}`"
|
||||
))
|
||||
})?;
|
||||
match key {
|
||||
"timeout_ms" => {
|
||||
if timeout_ms.is_some() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"js_repl pragma specifies timeout_ms more than once".to_string(),
|
||||
));
|
||||
}
|
||||
let parsed = value.parse::<u64>().map_err(|_| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
"js_repl pragma timeout_ms must be an integer; got `{value}`"
|
||||
))
|
||||
})?;
|
||||
timeout_ms = Some(parsed);
|
||||
}
|
||||
_ => {
|
||||
return Err(FunctionCallError::RespondToModel(format!(
|
||||
"js_repl pragma only supports timeout_ms; got `{key}`"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rest.trim().is_empty() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"js_repl pragma must be followed by JavaScript source on subsequent lines".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
reject_json_or_quoted_source(rest)?;
|
||||
args.code = rest.to_string();
|
||||
args.timeout_ms = timeout_ms;
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
fn reject_json_or_quoted_source(code: &str) -> Result<(), FunctionCallError> {
|
||||
let trimmed = code.trim();
|
||||
if trimmed.starts_with("```") {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"js_repl expects raw JavaScript source, not markdown code fences. Resend plain JS only (optional first line `// codex-js-repl: ...`)."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
let Ok(value) = serde_json::from_str::<JsonValue>(trimmed) else {
|
||||
return Ok(());
|
||||
};
|
||||
match value {
|
||||
JsonValue::Object(_) | JsonValue::String(_) => Err(FunctionCallError::RespondToModel(
|
||||
"js_repl is a freeform tool and expects raw JavaScript source. Resend plain JS only (optional first line `// codex-js-repl: ...`); do not send JSON (`{\"code\":...}`), quoted code, or markdown fences."
|
||||
.to_string(),
|
||||
)),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "js_repl_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,90 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use super::parse_freeform_args;
|
||||
use crate::session::tests::make_session_and_context_with_rx;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecCommandSource;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_without_pragma() {
|
||||
let args = parse_freeform_args("console.log('ok');").expect("parse args");
|
||||
assert_eq!(args.code, "console.log('ok');");
|
||||
assert_eq!(args.timeout_ms, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_with_pragma() {
|
||||
let input = "// codex-js-repl: timeout_ms=15000\nconsole.log('ok');";
|
||||
let args = parse_freeform_args(input).expect("parse args");
|
||||
assert_eq!(args.code, "console.log('ok');");
|
||||
assert_eq!(args.timeout_ms, Some(15_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_rejects_unknown_key() {
|
||||
let err = parse_freeform_args("// codex-js-repl: nope=1\nconsole.log('ok');")
|
||||
.expect_err("expected error");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"js_repl pragma only supports timeout_ms; got `nope`"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_rejects_reset_key() {
|
||||
let err = parse_freeform_args("// codex-js-repl: reset=true\nconsole.log('ok');")
|
||||
.expect_err("expected error");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"js_repl pragma only supports timeout_ms; got `reset`"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_rejects_json_wrapped_code() {
|
||||
let err = parse_freeform_args(r#"{"code":"await doThing()"}"#).expect_err("expected error");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"js_repl is a freeform tool and expects raw JavaScript source. Resend plain JS only (optional first line `// codex-js-repl: ...`); do not send JSON (`{\"code\":...}`), quoted code, or markdown fences."
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn emit_js_repl_exec_end_sends_event() {
|
||||
let (session, turn, rx) = make_session_and_context_with_rx().await;
|
||||
super::emit_js_repl_exec_end(
|
||||
session.as_ref(),
|
||||
turn.as_ref(),
|
||||
"call-1",
|
||||
"hello",
|
||||
/*error*/ None,
|
||||
Duration::from_millis(12),
|
||||
)
|
||||
.await;
|
||||
|
||||
let event = tokio::time::timeout(Duration::from_secs(5), async {
|
||||
loop {
|
||||
let event = rx.recv().await.expect("event");
|
||||
if let EventMsg::ExecCommandEnd(end) = event.msg {
|
||||
break end;
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("timed out waiting for exec end");
|
||||
|
||||
assert_eq!(event.call_id, "call-1");
|
||||
assert_eq!(event.turn_id, turn.sub_id);
|
||||
assert_eq!(event.command, vec!["js_repl".to_string()]);
|
||||
assert_eq!(event.cwd, turn.cwd);
|
||||
assert_eq!(event.source, ExecCommandSource::Agent);
|
||||
assert_eq!(event.interaction_input, None);
|
||||
assert_eq!(event.stdout, "hello");
|
||||
assert_eq!(event.stderr, "");
|
||||
assert!(event.aggregated_output.contains("hello"));
|
||||
assert_eq!(event.exit_code, 0);
|
||||
assert_eq!(event.duration, Duration::from_millis(12));
|
||||
assert!(event.formatted_output.contains("hello"));
|
||||
assert!(!event.parsed_cmd.is_empty());
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
pub(crate) mod agent_jobs;
|
||||
pub(crate) mod apply_patch;
|
||||
mod dynamic;
|
||||
mod js_repl;
|
||||
mod list_dir;
|
||||
mod mcp;
|
||||
mod mcp_resource;
|
||||
@@ -37,8 +36,6 @@ pub use apply_patch::ApplyPatchHandler;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
pub use dynamic::DynamicToolHandler;
|
||||
pub use js_repl::JsReplHandler;
|
||||
pub use js_repl::JsReplResetHandler;
|
||||
pub use list_dir::ListDirHandler;
|
||||
pub use mcp::McpHandler;
|
||||
pub use mcp_resource::McpResourceHandler;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@ pub(crate) mod context;
|
||||
pub(crate) mod events;
|
||||
pub(crate) mod handlers;
|
||||
pub(crate) mod hook_names;
|
||||
pub(crate) mod js_repl;
|
||||
pub(crate) mod network_approval;
|
||||
pub(crate) mod orchestrator;
|
||||
pub(crate) mod parallel;
|
||||
|
||||
@@ -279,18 +279,6 @@ impl ToolRouter {
|
||||
payload,
|
||||
} = call;
|
||||
|
||||
let direct_js_repl_call = tool_name.namespace.is_none()
|
||||
&& matches!(tool_name.name.as_str(), "js_repl" | "js_repl_reset");
|
||||
if matches!(&source, ToolCallSource::Direct)
|
||||
&& turn.tools_config.js_repl_tools_only
|
||||
&& !direct_js_repl_call
|
||||
{
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"direct tool calls are disabled; use js_repl and codex.tool(...) instead"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let invocation = ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
|
||||
@@ -1,187 +1,15 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::session::tests::make_session_and_context;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::turn_diff_tracker::TurnDiffTracker;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_tools::ToolName;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use super::ToolCall;
|
||||
use super::ToolCallSource;
|
||||
use super::ToolRouter;
|
||||
use super::ToolRouterParams;
|
||||
|
||||
#[tokio::test]
|
||||
#[expect(
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "test builds a router from session-owned MCP manager state"
|
||||
)]
|
||||
async fn js_repl_tools_only_blocks_direct_tool_calls() -> anyhow::Result<()> {
|
||||
let (session, mut turn) = make_session_and_context().await;
|
||||
turn.tools_config.js_repl_tools_only = true;
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
let mcp_tools = session
|
||||
.services
|
||||
.mcp_connection_manager
|
||||
.read()
|
||||
.await
|
||||
.list_all_tools()
|
||||
.await;
|
||||
let deferred_mcp_tools = Some(mcp_tools.clone());
|
||||
let router = ToolRouter::from_config(
|
||||
&turn.tools_config,
|
||||
ToolRouterParams {
|
||||
deferred_mcp_tools,
|
||||
mcp_tools: Some(mcp_tools),
|
||||
unavailable_called_tools: Vec::new(),
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
);
|
||||
|
||||
let call = ToolCall {
|
||||
tool_name: ToolName::plain("shell"),
|
||||
call_id: "call-1".to_string(),
|
||||
payload: ToolPayload::Function {
|
||||
arguments: "{}".to_string(),
|
||||
},
|
||||
};
|
||||
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
let err = router
|
||||
.dispatch_tool_call_with_code_mode_result(
|
||||
session,
|
||||
turn,
|
||||
CancellationToken::new(),
|
||||
tracker,
|
||||
call,
|
||||
ToolCallSource::Direct,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.expect("direct tool calls should be blocked");
|
||||
let FunctionCallError::RespondToModel(message) = err else {
|
||||
panic!("expected RespondToModel, got {err:?}");
|
||||
};
|
||||
assert!(message.contains("direct tool calls are disabled"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[expect(
|
||||
clippy::await_holding_invalid_type,
|
||||
reason = "test builds a router from session-owned MCP manager state"
|
||||
)]
|
||||
async fn js_repl_tools_only_allows_js_repl_source_calls() -> anyhow::Result<()> {
|
||||
let (session, mut turn) = make_session_and_context().await;
|
||||
turn.tools_config.js_repl_tools_only = true;
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
let mcp_tools = session
|
||||
.services
|
||||
.mcp_connection_manager
|
||||
.read()
|
||||
.await
|
||||
.list_all_tools()
|
||||
.await;
|
||||
let deferred_mcp_tools = Some(mcp_tools.clone());
|
||||
let router = ToolRouter::from_config(
|
||||
&turn.tools_config,
|
||||
ToolRouterParams {
|
||||
deferred_mcp_tools,
|
||||
mcp_tools: Some(mcp_tools),
|
||||
unavailable_called_tools: Vec::new(),
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
);
|
||||
|
||||
let call = ToolCall {
|
||||
tool_name: ToolName::plain("shell"),
|
||||
call_id: "call-2".to_string(),
|
||||
payload: ToolPayload::Function {
|
||||
arguments: "{}".to_string(),
|
||||
},
|
||||
};
|
||||
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
let err = router
|
||||
.dispatch_tool_call_with_code_mode_result(
|
||||
session,
|
||||
turn,
|
||||
CancellationToken::new(),
|
||||
tracker,
|
||||
call,
|
||||
ToolCallSource::JsRepl,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.expect("shell call with empty args should fail");
|
||||
let message = err.to_string();
|
||||
assert!(
|
||||
!message.contains("direct tool calls are disabled"),
|
||||
"js_repl source should bypass direct-call policy gate"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn js_repl_tools_only_blocks_namespaced_js_repl_tool() -> anyhow::Result<()> {
|
||||
let (session, mut turn) = make_session_and_context().await;
|
||||
turn.tools_config.js_repl_tools_only = true;
|
||||
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
let router = ToolRouter::from_config(
|
||||
&turn.tools_config,
|
||||
ToolRouterParams {
|
||||
deferred_mcp_tools: None,
|
||||
mcp_tools: None,
|
||||
unavailable_called_tools: Vec::new(),
|
||||
parallel_mcp_server_names: HashSet::new(),
|
||||
discoverable_tools: None,
|
||||
dynamic_tools: turn.dynamic_tools.as_slice(),
|
||||
},
|
||||
);
|
||||
|
||||
let call = ToolCall {
|
||||
tool_name: ToolName::namespaced("mcp__server__", "js_repl"),
|
||||
call_id: "call-namespaced-js-repl".to_string(),
|
||||
payload: ToolPayload::Mcp {
|
||||
server: "server".to_string(),
|
||||
tool: "js_repl".to_string(),
|
||||
raw_arguments: "{}".to_string(),
|
||||
},
|
||||
};
|
||||
let tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
|
||||
let err = router
|
||||
.dispatch_tool_call_with_code_mode_result(
|
||||
session,
|
||||
turn,
|
||||
CancellationToken::new(),
|
||||
tracker,
|
||||
call,
|
||||
ToolCallSource::Direct,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.expect("namespaced js_repl calls should be blocked");
|
||||
let FunctionCallError::RespondToModel(message) = err else {
|
||||
panic!("expected RespondToModel, got {err:?}");
|
||||
};
|
||||
assert!(message.contains("direct tool calls are disabled"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[expect(
|
||||
clippy::await_holding_invalid_type,
|
||||
|
||||
@@ -80,8 +80,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
use crate::tools::handlers::CodeModeExecuteHandler;
|
||||
use crate::tools::handlers::CodeModeWaitHandler;
|
||||
use crate::tools::handlers::DynamicToolHandler;
|
||||
use crate::tools::handlers::JsReplHandler;
|
||||
use crate::tools::handlers::JsReplResetHandler;
|
||||
use crate::tools::handlers::ListDirHandler;
|
||||
use crate::tools::handlers::McpHandler;
|
||||
use crate::tools::handlers::McpResourceHandler;
|
||||
@@ -167,8 +165,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
let tool_suggest_handler = Arc::new(ToolSuggestHandler);
|
||||
let code_mode_handler = Arc::new(CodeModeExecuteHandler);
|
||||
let code_mode_wait_handler = Arc::new(CodeModeWaitHandler);
|
||||
let js_repl_handler = Arc::new(JsReplHandler);
|
||||
let js_repl_reset_handler = Arc::new(JsReplResetHandler);
|
||||
let unavailable_tool_handler = Arc::new(UnavailableToolHandler);
|
||||
let mut existing_spec_names = plan
|
||||
.specs
|
||||
@@ -212,12 +208,6 @@ pub(crate) fn build_specs_with_discoverable_tools(
|
||||
ToolHandlerKind::FollowupTaskV2 => {
|
||||
builder.register_handler(handler.name, Arc::new(FollowupTaskHandlerV2));
|
||||
}
|
||||
ToolHandlerKind::JsRepl => {
|
||||
builder.register_handler(handler.name, js_repl_handler.clone());
|
||||
}
|
||||
ToolHandlerKind::JsReplReset => {
|
||||
builder.register_handler(handler.name, js_repl_reset_handler.clone());
|
||||
}
|
||||
ToolHandlerKind::ListAgentsV2 => {
|
||||
builder.register_handler(handler.name, Arc::new(ListAgentsHandlerV2));
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@ fn tool_dispatch_invocation(invocation: &ToolInvocation) -> Option<ToolDispatchI
|
||||
runtime_cell_id: cell_id.clone(),
|
||||
runtime_tool_call_id: runtime_tool_call_id.clone(),
|
||||
},
|
||||
ToolCallSource::JsRepl => return None,
|
||||
};
|
||||
|
||||
Some(ToolDispatchInvocation {
|
||||
@@ -98,7 +97,6 @@ fn tool_dispatch_result(
|
||||
ToolCallSource::CodeMode { .. } => Some(ToolDispatchResult::CodeModeResponse {
|
||||
value: result.code_mode_result(payload),
|
||||
}),
|
||||
ToolCallSource::JsRepl => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,11 +129,6 @@ async fn dispatch_lifecycle_trace_records_direct_and_code_mode_requesters() -> a
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dispatch_lifecycle_trace_skips_noncanonical_boundaries() -> anyhow::Result<()> {
|
||||
assert_dispatch_trace_skips(ToolCallSource::JsRepl).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dispatch_lifecycle_trace_records_unsupported_tool_failures() -> anyhow::Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
@@ -234,35 +229,6 @@ async fn missing_code_mode_wait_traces_only_the_wait_tool_call() -> anyhow::Resu
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn assert_dispatch_trace_skips(source: ToolCallSource) -> anyhow::Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
let (mut session, turn) = make_session_and_context().await;
|
||||
attach_test_trace(&mut session, &turn, temp.path())?;
|
||||
|
||||
let registry = ToolRegistry::with_handler_for_test(
|
||||
codex_tools::ToolName::plain("test_tool"),
|
||||
Arc::new(TestHandler),
|
||||
);
|
||||
let session = Arc::new(session);
|
||||
let turn = Arc::new(turn);
|
||||
|
||||
registry
|
||||
.dispatch_any(test_invocation(
|
||||
session,
|
||||
turn,
|
||||
"skipped-call",
|
||||
"test_tool",
|
||||
source,
|
||||
"{}",
|
||||
))
|
||||
.await?;
|
||||
|
||||
let replayed = codex_rollout_trace::replay_bundle(single_bundle_dir(temp.path())?)?;
|
||||
assert_eq!(replayed.tool_calls, Default::default());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_invocation(
|
||||
session: Arc<Session>,
|
||||
turn: Arc<TurnContext>,
|
||||
|
||||
@@ -1,795 +0,0 @@
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::ResponseMock;
|
||||
use core_test_support::responses::ResponsesRequest;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_custom_tool_call;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::stdio_server_bin;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event_match;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tempfile::tempdir;
|
||||
use wiremock::MockServer;
|
||||
|
||||
fn custom_tool_output_text_and_success(
|
||||
req: &ResponsesRequest,
|
||||
call_id: &str,
|
||||
) -> (String, Option<bool>) {
|
||||
let (output, success) = req
|
||||
.custom_tool_call_output_content_and_success(call_id)
|
||||
.expect("custom tool output should be present");
|
||||
(output.unwrap_or_default(), success)
|
||||
}
|
||||
|
||||
fn assert_js_repl_ok(req: &ResponsesRequest, call_id: &str, expected_output: &str) {
|
||||
let (output, success) = custom_tool_output_text_and_success(req, call_id);
|
||||
assert_ne!(
|
||||
success,
|
||||
Some(false),
|
||||
"js_repl call failed unexpectedly: {output}"
|
||||
);
|
||||
assert!(output.contains(expected_output), "output was: {output}");
|
||||
}
|
||||
|
||||
fn assert_js_repl_err(req: &ResponsesRequest, call_id: &str, expected_output: &str) {
|
||||
let (output, success) = custom_tool_output_text_and_success(req, call_id);
|
||||
assert_ne!(success, Some(true), "js_repl call should fail: {output}");
|
||||
assert!(output.contains(expected_output), "output was: {output}");
|
||||
}
|
||||
|
||||
fn tool_names(body: &serde_json::Value) -> Vec<String> {
|
||||
body["tools"]
|
||||
.as_array()
|
||||
.expect("tools array should be present")
|
||||
.iter()
|
||||
.map(|tool| {
|
||||
tool.get("name")
|
||||
.and_then(|value| value.as_str())
|
||||
.or_else(|| tool.get("type").and_then(|value| value.as_str()))
|
||||
.expect("tool should have a name or type")
|
||||
.to_string()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn write_too_old_node_script(dir: &Path) -> Result<std::path::PathBuf> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let path = dir.join("old-node.cmd");
|
||||
fs::write(&path, "@echo off\r\necho v0.0.1\r\n")?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let path = dir.join("old-node.sh");
|
||||
fs::write(&path, "#!/bin/sh\necho v0.0.1\n")?;
|
||||
let mut permissions = fs::metadata(&path)?.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(&path, permissions)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[cfg(not(any(unix, windows)))]
|
||||
{
|
||||
anyhow::bail!("unsupported platform for js_repl test fixture");
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_js_repl_turn(
|
||||
server: &MockServer,
|
||||
prompt: &str,
|
||||
calls: &[(&str, &str)],
|
||||
) -> Result<ResponseMock> {
|
||||
let mut mocks = run_js_repl_sequence(server, prompt, calls).await?;
|
||||
Ok(mocks
|
||||
.pop()
|
||||
.expect("js_repl test should return a request mock"))
|
||||
}
|
||||
|
||||
async fn run_js_repl_sequence(
|
||||
server: &MockServer,
|
||||
prompt: &str,
|
||||
calls: &[(&str, &str)],
|
||||
) -> Result<Vec<ResponseMock>> {
|
||||
anyhow::ensure!(
|
||||
!calls.is_empty(),
|
||||
"js_repl test must include at least one call"
|
||||
);
|
||||
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::JsRepl)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let test = builder.build(server).await?;
|
||||
|
||||
responses::mount_sse_once(
|
||||
server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_custom_tool_call(calls[0].0, "js_repl", calls[0].1),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut mocks = Vec::with_capacity(calls.len());
|
||||
for (response_index, (call_id, js_input)) in calls.iter().enumerate().skip(1) {
|
||||
let response_id = format!("resp-{}", response_index + 1);
|
||||
let mock = responses::mount_sse_once(
|
||||
server,
|
||||
sse(vec![
|
||||
ev_response_created(&response_id),
|
||||
ev_custom_tool_call(call_id, "js_repl", js_input),
|
||||
ev_completed(&response_id),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
mocks.push(mock);
|
||||
}
|
||||
|
||||
let final_response_id = format!("resp-{}", calls.len() + 1);
|
||||
let final_mock = responses::mount_sse_once(
|
||||
server,
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed(&final_response_id),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
mocks.push(final_mock);
|
||||
|
||||
test.submit_turn(prompt).await?;
|
||||
Ok(mocks)
|
||||
}
|
||||
|
||||
async fn assert_failed_cell_followup(
|
||||
server: &MockServer,
|
||||
prompt: &str,
|
||||
failing_cell: &str,
|
||||
followup_cell: &str,
|
||||
expected_followup_output: &str,
|
||||
) -> Result<()> {
|
||||
let mocks = run_js_repl_sequence(
|
||||
server,
|
||||
prompt,
|
||||
&[("call-1", failing_cell), ("call-2", followup_cell)],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_js_repl_err(&mocks[0].single_request(), "call-1", "boom");
|
||||
assert_js_repl_ok(
|
||||
&mocks[1].single_request(),
|
||||
"call-2",
|
||||
expected_followup_output,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_is_not_advertised_when_startup_node_is_incompatible() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
if std::env::var_os("CODEX_JS_REPL_NODE_PATH").is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let temp = tempdir()?;
|
||||
let old_node = write_too_old_node_script(temp.path())?;
|
||||
|
||||
let mut builder = test_codex().with_config(move |config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::JsRepl)
|
||||
.expect("test config should allow feature update");
|
||||
config.js_repl_node_path = Some(old_node);
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
let warning = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::Warning(ev) if ev.message.contains("Disabled `js_repl` for this session") => {
|
||||
Some(ev.message.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert!(
|
||||
warning.contains("Node runtime"),
|
||||
"warning should explain the Node compatibility issue: {warning}"
|
||||
);
|
||||
|
||||
let request_mock = responses::mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
test.submit_turn("hello").await?;
|
||||
|
||||
let body = request_mock.single_request().body_json();
|
||||
let tools = tool_names(&body);
|
||||
assert!(
|
||||
!tools.iter().any(|tool| tool == "js_repl"),
|
||||
"js_repl should be omitted when startup validation fails: {tools:?}"
|
||||
);
|
||||
assert!(
|
||||
!tools.iter().any(|tool| tool == "js_repl_reset"),
|
||||
"js_repl_reset should be omitted when startup validation fails: {tools:?}"
|
||||
);
|
||||
let instructions = body["instructions"].as_str().unwrap_or_default();
|
||||
assert!(
|
||||
!instructions.contains("## JavaScript REPL (Node)"),
|
||||
"startup instructions should not mention js_repl when it is disabled: {instructions}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_persists_top_level_destructured_bindings_and_supports_tla() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mocks = run_js_repl_sequence(
|
||||
&server,
|
||||
"run js_repl twice",
|
||||
&[
|
||||
(
|
||||
"call-1",
|
||||
"const { context: liveContext, session } = await Promise.resolve({ context: 41, session: 1 }); console.log(liveContext + session);",
|
||||
),
|
||||
("call-2", "console.log(liveContext + session);"),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_js_repl_ok(&mocks[0].single_request(), "call-1", "42");
|
||||
assert_js_repl_ok(&mocks[1].single_request(), "call-2", "42");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_failed_cells_commit_initialized_bindings_only() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mocks = run_js_repl_sequence(
|
||||
&server,
|
||||
"run js_repl across a failed cell",
|
||||
&[
|
||||
("call-1", "const base = 40; console.log(base);"),
|
||||
(
|
||||
"call-2",
|
||||
"const { session } = await Promise.resolve({ session: 2 }); throw new Error(\"boom\"); const late = 99;",
|
||||
),
|
||||
("call-3", "console.log(base + session, typeof late);"),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_js_repl_ok(&mocks[0].single_request(), "call-1", "40");
|
||||
assert_js_repl_err(&mocks[1].single_request(), "call-2", "boom");
|
||||
assert_js_repl_ok(&mocks[2].single_request(), "call-3", "42 undefined");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_failed_cells_preserve_initialized_lexical_destructuring_bindings() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mocks = run_js_repl_sequence(
|
||||
&server,
|
||||
"run js_repl through partial destructuring failure",
|
||||
&[
|
||||
(
|
||||
"call-1",
|
||||
"const { a, b } = { a: 1, get b() { throw new Error(\"boom\"); } };",
|
||||
),
|
||||
(
|
||||
"call-2",
|
||||
"let aValue; try { aValue = a; } catch (error) { aValue = error.name; } let bValue; try { bValue = b; } catch (error) { bValue = error.name; } console.log(aValue, bValue);",
|
||||
),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_js_repl_err(&mocks[0].single_request(), "call-1", "boom");
|
||||
assert_js_repl_ok(&mocks[1].single_request(), "call-2", "1 ReferenceError");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_link_failures_keep_prior_module_state() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mocks = run_js_repl_sequence(
|
||||
&server,
|
||||
"run js_repl across a link failure",
|
||||
&[
|
||||
("call-1", "const answer = 41; console.log(answer);"),
|
||||
("call-2", "import value from \"./foo\";"),
|
||||
("call-3", "console.log(answer + 1);"),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_js_repl_ok(&mocks[0].single_request(), "call-1", "41");
|
||||
assert_js_repl_err(
|
||||
&mocks[1].single_request(),
|
||||
"call-2",
|
||||
"Top-level static import \"./foo\" is not supported in js_repl",
|
||||
);
|
||||
assert_js_repl_ok(&mocks[2].single_request(), "call-3", "42");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_failed_cells_do_not_commit_unreached_hoisted_bindings() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mocks = run_js_repl_sequence(
|
||||
&server,
|
||||
"run js_repl through hoisted binding failure",
|
||||
&[
|
||||
(
|
||||
"call-1",
|
||||
"var early = 1; throw new Error(\"boom\"); var late = 2; function fn() { return 1; }",
|
||||
),
|
||||
(
|
||||
"call-2",
|
||||
"const late = 40; const fn = 1; console.log(early + late + fn);",
|
||||
),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_js_repl_err(&mocks[0].single_request(), "call-1", "boom");
|
||||
assert_js_repl_ok(&mocks[1].single_request(), "call-2", "42");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_failed_cells_do_not_preserve_hoisted_function_reads_before_declaration()
|
||||
-> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mocks = run_js_repl_sequence(
|
||||
&server,
|
||||
"run js_repl through unsupported hoisted function reads",
|
||||
&[
|
||||
(
|
||||
"call-1",
|
||||
"foo(); throw new Error(\"boom\"); function foo() {}",
|
||||
),
|
||||
(
|
||||
"call-2",
|
||||
"let value; try { foo; value = \"present\"; } catch (error) { value = error.name; } console.log(value);",
|
||||
),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_js_repl_err(&mocks[0].single_request(), "call-1", "boom");
|
||||
assert_js_repl_ok(&mocks[1].single_request(), "call-2", "ReferenceError");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_failed_cells_preserve_functions_when_declaration_sites_are_reached() -> Result<()>
|
||||
{
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mocks = run_js_repl_sequence(
|
||||
&server,
|
||||
"run js_repl through supported function declaration persistence",
|
||||
&[
|
||||
("call-1", "function foo() {} throw new Error(\"boom\");"),
|
||||
("call-2", "console.log(typeof foo);"),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_js_repl_err(&mocks[0].single_request(), "call-1", "boom");
|
||||
assert_js_repl_ok(&mocks[1].single_request(), "call-2", "function");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_failed_cells_preserve_prior_binding_writes_without_new_bindings() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mocks = run_js_repl_sequence(
|
||||
&server,
|
||||
"run js_repl through failed prior-binding writes",
|
||||
&[
|
||||
("call-1", "let x = 1; console.log(x);"),
|
||||
("call-2", "x = 2; throw new Error(\"boom\");"),
|
||||
("call-3", "console.log(x);"),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_js_repl_ok(&mocks[0].single_request(), "call-1", "1");
|
||||
assert_js_repl_err(&mocks[1].single_request(), "call-2", "boom");
|
||||
assert_js_repl_ok(&mocks[2].single_request(), "call-3", "2");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_failed_cells_var_persistence_boundaries() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let cases = [
|
||||
(
|
||||
"run js_repl through supported pre-declaration var writes",
|
||||
"x = 5; y = 1; y += 2; z = 1; z++; throw new Error(\"boom\"); var x, y, z;",
|
||||
"console.log(x, y, z);",
|
||||
"5 3 2",
|
||||
),
|
||||
(
|
||||
"run js_repl through short-circuited logical var assignments",
|
||||
"x &&= 1; y ||= 2; z ??= 3; throw new Error(\"boom\"); var x, y, z;",
|
||||
"let xValue; try { xValue = x; } catch (error) { xValue = error.name; } console.log(xValue, y, z);",
|
||||
"ReferenceError 2 3",
|
||||
),
|
||||
(
|
||||
"run js_repl through unsupported shadowed nested var writes",
|
||||
"{ let x = 1; x = 2; } throw new Error(\"boom\"); var x;",
|
||||
"let value; try { value = x; } catch (error) { value = error.name; } console.log(value);",
|
||||
"ReferenceError",
|
||||
),
|
||||
(
|
||||
"run js_repl through unsupported nested assignment writes",
|
||||
"x = (y = 1); throw new Error(\"boom\"); var x, y;",
|
||||
"let yValue; try { yValue = y; } catch (error) { yValue = error.name; } console.log(x, yValue);",
|
||||
"1 ReferenceError",
|
||||
),
|
||||
(
|
||||
"run js_repl through unsupported var destructuring recovery",
|
||||
"var { a, b } = { a: 1, get b() { throw new Error(\"boom\"); } };",
|
||||
"let aValue; try { aValue = a; } catch (error) { aValue = error.name; } let bValue; try { bValue = b; } catch (error) { bValue = error.name; } console.log(aValue, bValue);",
|
||||
"ReferenceError ReferenceError",
|
||||
),
|
||||
];
|
||||
|
||||
for (prompt, failing_cell, followup_cell, expected_followup_output) in cases {
|
||||
assert_failed_cell_followup(
|
||||
&server,
|
||||
prompt,
|
||||
failing_cell,
|
||||
followup_cell,
|
||||
expected_followup_output,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_failed_cells_commit_non_empty_loop_vars_but_skip_empty_loops() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mocks = run_js_repl_sequence(
|
||||
&server,
|
||||
"run js_repl through failed loop bindings",
|
||||
&[
|
||||
(
|
||||
"call-1",
|
||||
"for (var item of [2]) {} for (var emptyItem of []) {} throw new Error(\"boom\");",
|
||||
),
|
||||
(
|
||||
"call-2",
|
||||
"let itemValue; try { itemValue = item; } catch (error) { itemValue = error.name; } let emptyValue; try { emptyValue = emptyItem; } catch (error) { emptyValue = error.name; } console.log(itemValue, emptyValue);",
|
||||
),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_js_repl_err(&mocks[0].single_request(), "call-1", "boom");
|
||||
assert_js_repl_ok(&mocks[1].single_request(), "call-2", "2 ReferenceError");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_keeps_function_to_string_stable() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mock = run_js_repl_turn(
|
||||
&server,
|
||||
"run js_repl through function toString",
|
||||
&[(
|
||||
"call-1",
|
||||
"function foo() { return 1; } console.log(foo.toString());",
|
||||
)],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let req = mock.single_request();
|
||||
assert_js_repl_ok(&req, "call-1", "function foo() { return 1; }");
|
||||
let (output, _) = custom_tool_output_text_and_success(&req, "call-1");
|
||||
assert!(!output.contains("__codexInternalMarkCommittedBindings"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_allows_globalthis_shadowing_with_instrumented_bindings() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mock = run_js_repl_turn(
|
||||
&server,
|
||||
"run js_repl with shadowed globalThis",
|
||||
&[(
|
||||
"call-1",
|
||||
"const globalThis = {}; const value = 1; console.log(typeof globalThis, value);",
|
||||
)],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let req = mock.single_request();
|
||||
assert_js_repl_ok(&req, "call-1", "object 1");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_can_invoke_builtin_tools() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mock = run_js_repl_turn(
|
||||
&server,
|
||||
"use js_repl to call a tool",
|
||||
&[(
|
||||
"call-1",
|
||||
"const toolOut = await codex.tool(\"list_mcp_resources\", {}); console.log(toolOut.type);",
|
||||
)],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let req = mock.single_request();
|
||||
let (output, success) = custom_tool_output_text_and_success(&req, "call-1");
|
||||
assert_ne!(
|
||||
success,
|
||||
Some(false),
|
||||
"js_repl call failed unexpectedly: {output}"
|
||||
);
|
||||
assert!(output.contains("function_call_output"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_can_invoke_mcp_tools_by_display_name() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let rmcp_test_server_bin = stdio_server_bin()?;
|
||||
let mut builder = test_codex().with_config(move |config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::JsRepl)
|
||||
.expect("test config should allow feature update");
|
||||
|
||||
let mut servers = config.mcp_servers.get().clone();
|
||||
servers.insert(
|
||||
"rmcp".to_string(),
|
||||
McpServerConfig {
|
||||
transport: McpServerTransportConfig::Stdio {
|
||||
command: rmcp_test_server_bin,
|
||||
args: Vec::new(),
|
||||
env: None,
|
||||
env_vars: Vec::new(),
|
||||
cwd: None,
|
||||
},
|
||||
experimental_environment: None,
|
||||
enabled: true,
|
||||
required: false,
|
||||
supports_parallel_tool_calls: false,
|
||||
disabled_reason: None,
|
||||
startup_timeout_sec: Some(Duration::from_secs(10)),
|
||||
tool_timeout_sec: None,
|
||||
default_tools_approval_mode: None,
|
||||
enabled_tools: None,
|
||||
disabled_tools: None,
|
||||
scopes: None,
|
||||
oauth_resource: None,
|
||||
tools: HashMap::new(),
|
||||
},
|
||||
);
|
||||
config
|
||||
.mcp_servers
|
||||
.set(servers)
|
||||
.expect("test mcp servers should accept any configuration");
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
responses::mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_custom_tool_call(
|
||||
"call-1",
|
||||
"js_repl",
|
||||
r#"
|
||||
const result = await codex.tool("mcp__rmcp__echo", { message: "ping" });
|
||||
console.log(result.output);
|
||||
"#,
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let final_mock = responses::mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
test.submit_turn("use js_repl to call an MCP tool").await?;
|
||||
|
||||
let req = final_mock.single_request();
|
||||
assert_js_repl_ok(&req, "call-1", "ECHOING: ping");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_tool_call_rejects_recursive_js_repl_invocation() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mock = run_js_repl_turn(
|
||||
&server,
|
||||
"use js_repl recursively",
|
||||
&[(
|
||||
"call-1",
|
||||
r#"
|
||||
try {
|
||||
await codex.tool("js_repl", "console.log('recursive')");
|
||||
console.log("unexpected-success");
|
||||
} catch (err) {
|
||||
console.log(String(err));
|
||||
}
|
||||
"#,
|
||||
)],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let req = mock.single_request();
|
||||
let (output, success) = custom_tool_output_text_and_success(&req, "call-1");
|
||||
assert_ne!(
|
||||
success,
|
||||
Some(false),
|
||||
"js_repl call failed unexpectedly: {output}"
|
||||
);
|
||||
assert!(
|
||||
output.contains("js_repl cannot invoke itself"),
|
||||
"expected recursion guard message, got output: {output}"
|
||||
);
|
||||
assert!(
|
||||
!output.contains("unexpected-success"),
|
||||
"recursive js_repl call unexpectedly succeeded: {output}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_does_not_expose_process_global() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mock = run_js_repl_turn(
|
||||
&server,
|
||||
"check process visibility",
|
||||
&[("call-1", "console.log(typeof process);")],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let req = mock.single_request();
|
||||
let (output, success) = custom_tool_output_text_and_success(&req, "call-1");
|
||||
assert_ne!(
|
||||
success,
|
||||
Some(false),
|
||||
"js_repl call failed unexpectedly: {output}"
|
||||
);
|
||||
assert!(output.contains("undefined"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_exposes_codex_path_helpers() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mock = run_js_repl_turn(
|
||||
&server,
|
||||
"check codex path helpers",
|
||||
&[(
|
||||
"call-1",
|
||||
"console.log(`cwd:${typeof codex.cwd}:${codex.cwd.length > 0}`); console.log(`home:${codex.homeDir === null || typeof codex.homeDir === \"string\"}`);",
|
||||
)],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let req = mock.single_request();
|
||||
let (output, success) = custom_tool_output_text_and_success(&req, "call-1");
|
||||
assert_ne!(
|
||||
success,
|
||||
Some(false),
|
||||
"js_repl call failed unexpectedly: {output}"
|
||||
);
|
||||
assert!(output.contains("cwd:string:true"));
|
||||
assert!(output.contains("home:true"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_blocks_sensitive_builtin_imports() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mock = run_js_repl_turn(
|
||||
&server,
|
||||
"import a blocked module",
|
||||
&[("call-1", "await import(\"node:process\");")],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let req = mock.single_request();
|
||||
let (output, success) = custom_tool_output_text_and_success(&req, "call-1");
|
||||
assert_ne!(
|
||||
success,
|
||||
Some(true),
|
||||
"blocked import unexpectedly succeeded: {output}"
|
||||
);
|
||||
assert!(output.contains("Importing module \"node:process\" is not allowed in js_repl"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -51,7 +51,6 @@ mod hooks;
|
||||
mod hooks_mcp;
|
||||
mod image_rollout;
|
||||
mod items;
|
||||
mod js_repl;
|
||||
mod json_result;
|
||||
mod live_cli;
|
||||
mod live_reload;
|
||||
|
||||
@@ -24,7 +24,6 @@ use codex_core::config::Config;
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::HttpRequestParams;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_mcp::MCP_SANDBOX_STATE_META_CAPABILITY;
|
||||
use codex_models_manager::manager::RefreshStrategy;
|
||||
@@ -48,7 +47,6 @@ use codex_utils_cargo_bin::cargo_bin;
|
||||
use core_test_support::assert_regex_match;
|
||||
use core_test_support::remote_env_env_var;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::ev_custom_tool_call;
|
||||
use core_test_support::responses::mount_models_once;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::skip_if_no_network;
|
||||
@@ -1328,90 +1326,6 @@ async fn stdio_image_responses_preserve_original_detail_metadata() -> anyhow::Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
#[serial(mcp_test_value)]
|
||||
async fn js_repl_emit_image_preserves_original_detail_for_mcp_images() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let call_id = "js-repl-rmcp-image";
|
||||
let rmcp_test_server_bin = stdio_server_bin()?;
|
||||
|
||||
let fixture = test_codex()
|
||||
.with_model("gpt-5.3-codex")
|
||||
.with_config(move |config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::JsRepl)
|
||||
.expect("test config should allow feature update");
|
||||
insert_mcp_server(
|
||||
config,
|
||||
"rmcp",
|
||||
stdio_transport(rmcp_test_server_bin, /*env*/ None, Vec::new()),
|
||||
TestMcpServerOptions::default(),
|
||||
);
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
wait_for_mcp_tool(&fixture, "mcp__rmcp__image_scenario").await?;
|
||||
|
||||
mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
ev_custom_tool_call(
|
||||
call_id,
|
||||
"js_repl",
|
||||
r#"
|
||||
const out = await codex.tool("mcp__rmcp__image_scenario", {
|
||||
scenario: "image_only_original_detail",
|
||||
});
|
||||
const imageItem = out.output.find((item) => item.type === "input_image");
|
||||
await codex.emitImage(imageItem);
|
||||
"#,
|
||||
),
|
||||
responses::ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let final_mock = mount_sse_once(
|
||||
&server,
|
||||
responses::sse(vec![
|
||||
responses::ev_assistant_message("msg-1", "done"),
|
||||
responses::ev_completed("resp-2"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
fixture
|
||||
.submit_turn("use js_repl to emit the rmcp image scenario output")
|
||||
.await?;
|
||||
|
||||
let output = final_mock.single_request().custom_tool_call_output(call_id);
|
||||
let output_items = output["output"]
|
||||
.as_array()
|
||||
.expect("js_repl output should be content items");
|
||||
let image_item = output_items
|
||||
.iter()
|
||||
.find(|item| item.get("type").and_then(Value::as_str) == Some("input_image"))
|
||||
.expect("js_repl should emit an input_image item");
|
||||
assert_eq!(
|
||||
image_item.get("detail").and_then(Value::as_str),
|
||||
Some("original")
|
||||
);
|
||||
assert!(
|
||||
image_item
|
||||
.get("image_url")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|image_url| image_url.starts_with("data:image/png;base64,")),
|
||||
"js_repl should emit a png data URL"
|
||||
);
|
||||
|
||||
server.verify().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
#[serial(mcp_test_value)]
|
||||
async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Result<()> {
|
||||
|
||||
@@ -96,10 +96,6 @@ async fn empty_turn_environments_omits_environment_backed_tools() -> Result<()>
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
.expect("unified exec should enable for test");
|
||||
config
|
||||
.features
|
||||
.enable(Feature::JsRepl)
|
||||
.expect("js repl should enable for test");
|
||||
config.include_apply_patch_tool = true;
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
@@ -112,14 +108,7 @@ async fn empty_turn_environments_omits_environment_backed_tools() -> Result<()>
|
||||
tools.contains(&"update_plan".to_string()),
|
||||
"non-environment tool should remain available; got {tools:?}"
|
||||
);
|
||||
for environment_tool in [
|
||||
"exec_command",
|
||||
"write_stdin",
|
||||
"js_repl",
|
||||
"js_repl_reset",
|
||||
"apply_patch",
|
||||
"view_image",
|
||||
] {
|
||||
for environment_tool in ["exec_command", "write_stdin", "apply_patch", "view_image"] {
|
||||
assert!(
|
||||
!tools.contains(&environment_tool.to_string()),
|
||||
"{environment_tool} should be omitted for explicit empty turn environments; got {tools:?}"
|
||||
|
||||
@@ -22,7 +22,6 @@ use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_custom_tool_call;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_models_once;
|
||||
@@ -871,233 +870,6 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_only
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_emit_image_attaches_local_image() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::JsRepl)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let TestCodex {
|
||||
codex,
|
||||
cwd,
|
||||
session_configured,
|
||||
..
|
||||
} = builder.build(&server).await?;
|
||||
|
||||
let call_id = "js-repl-view-image";
|
||||
let js_input = r#"
|
||||
const fs = await import("node:fs/promises");
|
||||
const path = await import("node:path");
|
||||
const imagePath = path.join(codex.tmpDir, "js-repl-view-image.png");
|
||||
const png = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==",
|
||||
"base64"
|
||||
);
|
||||
await fs.writeFile(imagePath, png);
|
||||
const out = await codex.tool("view_image", { path: imagePath });
|
||||
await codex.emitImage(out);
|
||||
"#;
|
||||
|
||||
let first_response = sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_custom_tool_call(call_id, "js_repl", js_input),
|
||||
ev_completed("resp-1"),
|
||||
]);
|
||||
responses::mount_sse_once(&server, first_response).await;
|
||||
|
||||
let second_response = sse(vec![
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]);
|
||||
let mock = responses::mount_sse_once(&server, second_response).await;
|
||||
|
||||
let session_model = session_configured.model.clone();
|
||||
codex
|
||||
.submit(Op::UserTurn {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "use js_repl to write an image and attach it".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
permission_profile: None,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut tool_event = None;
|
||||
wait_for_event_with_timeout(
|
||||
&codex,
|
||||
|event| match event {
|
||||
EventMsg::ViewImageToolCall(_) => {
|
||||
tool_event = Some(event.clone());
|
||||
false
|
||||
}
|
||||
EventMsg::TurnComplete(_) => true,
|
||||
_ => false,
|
||||
},
|
||||
VIEW_IMAGE_TURN_COMPLETE_TIMEOUT,
|
||||
)
|
||||
.await;
|
||||
let tool_event = match tool_event {
|
||||
Some(EventMsg::ViewImageToolCall(event)) => event,
|
||||
other => panic!("expected ViewImageToolCall event, got {other:?}"),
|
||||
};
|
||||
assert!(
|
||||
tool_event.path.ends_with("js-repl-view-image.png"),
|
||||
"unexpected image path: {}",
|
||||
tool_event.path.display()
|
||||
);
|
||||
|
||||
let req = mock.single_request();
|
||||
let body = req.body_json();
|
||||
assert_eq!(
|
||||
image_messages(&body).len(),
|
||||
0,
|
||||
"js_repl view_image should not inject a pending input image message"
|
||||
);
|
||||
|
||||
let custom_output = req.custom_tool_call_output(call_id);
|
||||
let output_items = custom_output
|
||||
.get("output")
|
||||
.and_then(Value::as_array)
|
||||
.expect("custom_tool_call_output should be a content item array");
|
||||
let image_url = output_items
|
||||
.iter()
|
||||
.find_map(|item| {
|
||||
(item.get("type").and_then(Value::as_str) == Some("input_image"))
|
||||
.then(|| item.get("image_url").and_then(Value::as_str))
|
||||
.flatten()
|
||||
})
|
||||
.expect("image_url present in js_repl custom tool output");
|
||||
assert!(
|
||||
image_url.starts_with("data:image/png;base64,"),
|
||||
"expected png data URL, got {image_url}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn js_repl_view_image_requires_explicit_emit() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
#[allow(clippy::expect_used)]
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::JsRepl)
|
||||
.expect("test config should allow feature update");
|
||||
});
|
||||
let TestCodex {
|
||||
codex,
|
||||
cwd,
|
||||
session_configured,
|
||||
..
|
||||
} = builder.build(&server).await?;
|
||||
|
||||
let call_id = "js-repl-view-image-no-emit";
|
||||
let js_input = r#"
|
||||
const fs = await import("node:fs/promises");
|
||||
const path = await import("node:path");
|
||||
const imagePath = path.join(codex.tmpDir, "js-repl-view-image-no-emit.png");
|
||||
const png = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==",
|
||||
"base64"
|
||||
);
|
||||
await fs.writeFile(imagePath, png);
|
||||
const out = await codex.tool("view_image", { path: imagePath });
|
||||
console.log(out.type);
|
||||
"#;
|
||||
|
||||
let first_response = sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_custom_tool_call(call_id, "js_repl", js_input),
|
||||
ev_completed("resp-1"),
|
||||
]);
|
||||
responses::mount_sse_once(&server, first_response).await;
|
||||
|
||||
let second_response = sse(vec![
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]);
|
||||
let mock = responses::mount_sse_once(&server, second_response).await;
|
||||
|
||||
let session_model = session_configured.model.clone();
|
||||
codex
|
||||
.submit(Op::UserTurn {
|
||||
environments: None,
|
||||
items: vec![UserInput::Text {
|
||||
text: "use js_repl to write an image but do not emit it".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
approvals_reviewer: None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
permission_profile: None,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
service_tier: None,
|
||||
summary: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut tool_event = None;
|
||||
wait_for_event_with_timeout(
|
||||
&codex,
|
||||
|event| match event {
|
||||
EventMsg::ViewImageToolCall(_) => {
|
||||
tool_event = Some(event.clone());
|
||||
false
|
||||
}
|
||||
EventMsg::TurnComplete(_) => true,
|
||||
_ => false,
|
||||
},
|
||||
VIEW_IMAGE_TURN_COMPLETE_TIMEOUT,
|
||||
)
|
||||
.await;
|
||||
let tool_event = match tool_event {
|
||||
Some(EventMsg::ViewImageToolCall(event)) => event,
|
||||
other => panic!("expected ViewImageToolCall event, got {other:?}"),
|
||||
};
|
||||
assert!(
|
||||
tool_event.path.ends_with("js-repl-view-image-no-emit.png"),
|
||||
"unexpected image path: {}",
|
||||
tool_event.path.display()
|
||||
);
|
||||
|
||||
let req = mock.single_request();
|
||||
let custom_output = req.custom_tool_call_output(call_id);
|
||||
let output_items = custom_output.get("output").and_then(Value::as_array);
|
||||
assert!(
|
||||
output_items.is_none_or(|items| items
|
||||
.iter()
|
||||
.all(|item| item.get("type").and_then(Value::as_str) != Some("input_image"))),
|
||||
"nested view_image should not auto-populate js_repl output"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -1572,4 +1344,3 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()>
|
||||
|
||||
Ok(())
|
||||
}
|
||||
use codex_features::Feature;
|
||||
|
||||
@@ -399,8 +399,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
|
||||
codex_self_exe: arg0_paths.codex_self_exe.clone(),
|
||||
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe.clone(),
|
||||
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe.clone(),
|
||||
js_repl_node_path: None,
|
||||
js_repl_node_module_dirs: None,
|
||||
zsh_path: None,
|
||||
base_instructions: None,
|
||||
developer_instructions: None,
|
||||
|
||||
@@ -10,7 +10,5 @@ codex_rust_crate(
|
||||
"Cargo.toml",
|
||||
],
|
||||
allow_empty = True,
|
||||
) + [
|
||||
"//codex-rs:node-version.txt",
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -79,13 +79,13 @@ pub enum Feature {
|
||||
CodexHooks,
|
||||
|
||||
// Experimental
|
||||
/// Enable JavaScript REPL tools backed by a persistent Node kernel.
|
||||
/// Removed compatibility flag for the deleted JavaScript REPL feature.
|
||||
JsRepl,
|
||||
/// Enable a minimal JavaScript mode backed by Node's built-in vm runtime.
|
||||
/// Enable JavaScript code mode backed by the in-process V8 runtime.
|
||||
CodeMode,
|
||||
/// Restrict model-visible tools to code mode entrypoints (`exec`, `wait`).
|
||||
CodeModeOnly,
|
||||
/// Only expose js_repl tools directly to the model.
|
||||
/// Removed compatibility flag for the deleted JavaScript REPL tool-only mode.
|
||||
JsReplToolsOnly,
|
||||
/// Use the single unified PTY-backed exec tool.
|
||||
UnifiedExec,
|
||||
@@ -388,6 +388,12 @@ impl Features {
|
||||
"tui_app_server" => {
|
||||
continue;
|
||||
}
|
||||
"js_repl" => {
|
||||
continue;
|
||||
}
|
||||
"js_repl_tools_only" => {
|
||||
continue;
|
||||
}
|
||||
"image_detail_original" => {
|
||||
continue;
|
||||
}
|
||||
@@ -457,10 +463,6 @@ impl Features {
|
||||
if self.enabled(Feature::CodeModeOnly) && !self.enabled(Feature::CodeMode) {
|
||||
self.enable(Feature::CodeMode);
|
||||
}
|
||||
if self.enabled(Feature::JsReplToolsOnly) && !self.enabled(Feature::JsRepl) {
|
||||
tracing::warn!("js_repl_tools_only requires js_repl; disabling js_repl_tools_only");
|
||||
self.disable(Feature::JsReplToolsOnly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,11 +646,7 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
FeatureSpec {
|
||||
id: Feature::JsRepl,
|
||||
key: "js_repl",
|
||||
stage: Stage::Experimental {
|
||||
name: "JavaScript REPL",
|
||||
menu_description: "Enable a persistent Node-backed JavaScript REPL for interactive website debugging and other inline JavaScript execution capabilities. Requires Node >= v22.22.0 installed.",
|
||||
announcement: "NEW: JavaScript REPL is now available in /experimental. Enable it, then start a new chat or restart Codex to use it.",
|
||||
},
|
||||
stage: Stage::Removed,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
@@ -666,7 +664,7 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
FeatureSpec {
|
||||
id: Feature::JsReplToolsOnly,
|
||||
key: "js_repl_tools_only",
|
||||
stage: Stage::UnderDevelopment,
|
||||
stage: Stage::Removed,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
|
||||
@@ -59,23 +59,6 @@ fn image_detail_original_is_removed_and_disabled_by_default() {
|
||||
assert_eq!(Feature::ImageDetailOriginal.default_enabled(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_repl_is_experimental_and_user_toggleable() {
|
||||
let spec = Feature::JsRepl.info();
|
||||
let stage = spec.stage;
|
||||
let expected_node_version = include_str!("../../node-version.txt").trim_end();
|
||||
|
||||
assert!(matches!(stage, Stage::Experimental { .. }));
|
||||
assert_eq!(stage.experimental_menu_name(), Some("JavaScript REPL"));
|
||||
assert_eq!(
|
||||
stage.experimental_menu_description().map(str::to_owned),
|
||||
Some(format!(
|
||||
"Enable a persistent Node-backed JavaScript REPL for interactive website debugging and other inline JavaScript execution capabilities. Requires Node >= v{expected_node_version} installed."
|
||||
))
|
||||
);
|
||||
assert_eq!(Feature::JsRepl.default_enabled(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_mode_only_requires_code_mode() {
|
||||
let mut features = Features::with_defaults();
|
||||
@@ -222,6 +205,20 @@ fn image_detail_original_is_a_removed_feature_key() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_repl_features_are_removed_feature_keys() {
|
||||
assert_eq!(Feature::JsRepl.stage(), Stage::Removed);
|
||||
assert_eq!(Feature::JsRepl.default_enabled(), false);
|
||||
assert_eq!(feature_for_key("js_repl"), Some(Feature::JsRepl));
|
||||
|
||||
assert_eq!(Feature::JsReplToolsOnly.stage(), Stage::Removed);
|
||||
assert_eq!(Feature::JsReplToolsOnly.default_enabled(), false);
|
||||
assert_eq!(
|
||||
feature_for_key("js_repl_tools_only"),
|
||||
Some(Feature::JsReplToolsOnly)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_call_mcp_elicitation_is_stable_and_enabled_by_default() {
|
||||
assert_eq!(Feature::ToolCallMcpElicitation.stage(), Stage::Stable);
|
||||
@@ -353,6 +350,25 @@ fn from_sources_ignores_removed_image_detail_original_feature_key() {
|
||||
assert_eq!(features, Features::with_defaults());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_sources_ignores_removed_js_repl_feature_keys() {
|
||||
let features_toml = FeaturesToml::from(BTreeMap::from([
|
||||
("js_repl".to_string(), true),
|
||||
("js_repl_tools_only".to_string(), true),
|
||||
]));
|
||||
|
||||
let features = Features::from_sources(
|
||||
FeatureConfigSource {
|
||||
features: Some(&features_toml),
|
||||
..Default::default()
|
||||
},
|
||||
FeatureConfigSource::default(),
|
||||
FeatureOverrides::default(),
|
||||
);
|
||||
|
||||
assert_eq!(features, Features::with_defaults());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_agent_v2_feature_config_deserializes_boolean_toggle() {
|
||||
let features: FeaturesToml = toml::from_str(
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
22.22.0
|
||||
@@ -22,7 +22,6 @@ schema and Responses API tool primitives that no longer need to live in
|
||||
- `ResponsesApiNamespace`
|
||||
- `ResponsesApiNamespaceTool`
|
||||
- code-mode `ToolSpec` adapters and `exec` / `wait` spec builders
|
||||
- JS REPL spec builders
|
||||
- MCP resource, `list_dir`, and `test_sync_tool` spec builders
|
||||
- local host tool spec builders for shell/exec/request-permissions/view-image
|
||||
- collaboration and agent-job `ToolSpec` builders for spawn/send/wait/close,
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
use crate::FreeformTool;
|
||||
use crate::FreeformToolFormat;
|
||||
use crate::JsonSchema;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::ToolSpec;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub fn create_js_repl_tool() -> ToolSpec {
|
||||
// Keep JS input freeform, but block the most common malformed payload shapes
|
||||
// (JSON wrappers, quoted strings, and markdown fences) before they reach the
|
||||
// runtime `reject_json_or_quoted_source` validation. The API's regex engine
|
||||
// does not support look-around, so this uses a "first significant token"
|
||||
// pattern rather than negative lookaheads.
|
||||
const JS_REPL_FREEFORM_GRAMMAR: &str = r#"
|
||||
start: pragma_source | plain_source
|
||||
|
||||
pragma_source: PRAGMA_LINE NEWLINE js_source
|
||||
plain_source: PLAIN_JS_SOURCE
|
||||
|
||||
js_source: JS_SOURCE
|
||||
|
||||
PRAGMA_LINE: /[ \t]*\/\/ codex-js-repl:[^\r\n]*/
|
||||
NEWLINE: /\r?\n/
|
||||
PLAIN_JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
|
||||
JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
|
||||
"#;
|
||||
|
||||
ToolSpec::Freeform(FreeformTool {
|
||||
name: "js_repl".to_string(),
|
||||
description: "Runs JavaScript in a persistent Node kernel with top-level await. This is a freeform tool: send raw JavaScript source text, optionally with a first-line pragma like `// codex-js-repl: timeout_ms=15000`; do not send JSON/quotes/markdown fences."
|
||||
.to_string(),
|
||||
format: FreeformToolFormat {
|
||||
r#type: "grammar".to_string(),
|
||||
syntax: "lark".to_string(),
|
||||
definition: JS_REPL_FREEFORM_GRAMMAR.to_string(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_js_repl_reset_tool() -> ToolSpec {
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "js_repl_reset".to_string(),
|
||||
description:
|
||||
"Restarts the js_repl kernel for this run and clears persisted top-level bindings."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::object(BTreeMap::new(), /*required*/ None, Some(false.into())),
|
||||
output_schema: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "js_repl_tool_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1,41 +0,0 @@
|
||||
use super::*;
|
||||
use crate::JsonSchema;
|
||||
use crate::ToolSpec;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn js_repl_tool_uses_expected_freeform_grammar() {
|
||||
let ToolSpec::Freeform(FreeformTool { format, .. }) = create_js_repl_tool() else {
|
||||
panic!("js_repl should use a freeform tool spec");
|
||||
};
|
||||
|
||||
assert_eq!(format.syntax, "lark");
|
||||
assert!(format.definition.contains("PRAGMA_LINE"));
|
||||
assert!(format.definition.contains("`[^`]"));
|
||||
assert!(format.definition.contains("``[^`]"));
|
||||
assert!(format.definition.contains("PLAIN_JS_SOURCE"));
|
||||
assert!(format.definition.contains("codex-js-repl:"));
|
||||
assert!(!format.definition.contains("(?!"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_repl_reset_tool_matches_expected_spec() {
|
||||
assert_eq!(
|
||||
create_js_repl_reset_tool(),
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "js_repl_reset".to_string(),
|
||||
description:
|
||||
"Restarts the js_repl kernel for this run and clears persisted top-level bindings."
|
||||
.to_string(),
|
||||
strict: false,
|
||||
defer_loading: None,
|
||||
parameters: JsonSchema::object(
|
||||
BTreeMap::new(),
|
||||
/*required*/ None,
|
||||
Some(false.into())
|
||||
),
|
||||
output_schema: None,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,6 @@ mod apply_patch_tool;
|
||||
mod code_mode;
|
||||
mod dynamic_tool;
|
||||
mod image_detail;
|
||||
mod js_repl_tool;
|
||||
mod json_schema;
|
||||
mod local_tool;
|
||||
mod mcp_resource_tool;
|
||||
@@ -55,8 +54,6 @@ pub use dynamic_tool::parse_dynamic_tool;
|
||||
pub use image_detail::can_request_original_image_detail;
|
||||
pub use image_detail::normalize_output_image_detail;
|
||||
pub use image_detail::sanitize_original_image_detail;
|
||||
pub use js_repl_tool::create_js_repl_reset_tool;
|
||||
pub use js_repl_tool::create_js_repl_tool;
|
||||
pub use json_schema::AdditionalProperties;
|
||||
pub use json_schema::JsonSchema;
|
||||
pub use json_schema::JsonSchemaPrimitiveType;
|
||||
|
||||
@@ -99,8 +99,6 @@ pub struct ToolsConfig {
|
||||
pub request_permissions_tool_enabled: bool,
|
||||
pub code_mode_enabled: bool,
|
||||
pub code_mode_only_enabled: bool,
|
||||
pub js_repl_enabled: bool,
|
||||
pub js_repl_tools_only: bool,
|
||||
pub can_request_original_image_detail: bool,
|
||||
pub collab_tools: bool,
|
||||
pub multi_agent_v2: bool,
|
||||
@@ -141,9 +139,6 @@ impl ToolsConfig {
|
||||
let include_apply_patch_tool = features.enabled(Feature::ApplyPatchFreeform);
|
||||
let include_code_mode = features.enabled(Feature::CodeMode);
|
||||
let include_code_mode_only = include_code_mode && features.enabled(Feature::CodeModeOnly);
|
||||
let include_js_repl = features.enabled(Feature::JsRepl);
|
||||
let include_js_repl_tools_only =
|
||||
include_js_repl && features.enabled(Feature::JsReplToolsOnly);
|
||||
let include_collab_tools = features.enabled(Feature::Collab);
|
||||
let include_multi_agent_v2 = features.enabled(Feature::MultiAgentV2);
|
||||
let include_agent_jobs = features.enabled(Feature::SpawnCsv);
|
||||
@@ -221,8 +216,6 @@ impl ToolsConfig {
|
||||
request_permissions_tool_enabled,
|
||||
code_mode_enabled: include_code_mode,
|
||||
code_mode_only_enabled: include_code_mode_only,
|
||||
js_repl_enabled: include_js_repl,
|
||||
js_repl_tools_only: include_js_repl_tools_only,
|
||||
can_request_original_image_detail: include_original_image_detail,
|
||||
collab_tools: include_collab_tools,
|
||||
multi_agent_v2: include_multi_agent_v2,
|
||||
|
||||
@@ -29,8 +29,6 @@ use crate::create_code_mode_tool;
|
||||
use crate::create_exec_command_tool;
|
||||
use crate::create_followup_task_tool;
|
||||
use crate::create_image_generation_tool;
|
||||
use crate::create_js_repl_reset_tool;
|
||||
use crate::create_js_repl_tool;
|
||||
use crate::create_list_agents_tool;
|
||||
use crate::create_list_dir_tool;
|
||||
use crate::create_list_mcp_resource_templates_tool;
|
||||
@@ -218,21 +216,6 @@ pub fn build_tool_registry_plan(
|
||||
);
|
||||
plan.register_handler("update_plan", ToolHandlerKind::Plan);
|
||||
|
||||
if config.has_environment && config.js_repl_enabled {
|
||||
plan.push_spec(
|
||||
create_js_repl_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.push_spec(
|
||||
create_js_repl_reset_tool(),
|
||||
/*supports_parallel_tool_calls*/ false,
|
||||
config.code_mode_enabled,
|
||||
);
|
||||
plan.register_handler("js_repl", ToolHandlerKind::JsRepl);
|
||||
plan.register_handler("js_repl_reset", ToolHandlerKind::JsReplReset);
|
||||
}
|
||||
|
||||
plan.push_spec(
|
||||
create_request_user_input_tool(request_user_input_tool_description(
|
||||
config.default_mode_request_user_input,
|
||||
|
||||
@@ -437,7 +437,6 @@ fn disabled_environment_omits_environment_backed_tools() {
|
||||
let model_info = model_info();
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
features.enable(Feature::JsRepl);
|
||||
let available_models = Vec::new();
|
||||
let mut tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
@@ -462,8 +461,6 @@ fn disabled_environment_omits_environment_backed_tools() {
|
||||
|
||||
assert_lacks_tool_name(&tools, "exec_command");
|
||||
assert_lacks_tool_name(&tools, "write_stdin");
|
||||
assert_lacks_tool_name(&tools, "js_repl");
|
||||
assert_lacks_tool_name(&tools, "js_repl_reset");
|
||||
assert_lacks_tool_name(&tools, "apply_patch");
|
||||
assert_lacks_tool_name(&tools, "list_dir");
|
||||
assert_lacks_tool_name(&tools, VIEW_IMAGE_TOOL_NAME);
|
||||
@@ -636,66 +633,6 @@ fn request_permissions_tool_is_independent_from_additional_permissions() {
|
||||
assert_lacks_tool_name(&tools, "request_permissions");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_repl_requires_feature_flag() {
|
||||
let model_info = model_info();
|
||||
let features = Features::with_defaults();
|
||||
|
||||
let available_models = Vec::new();
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
image_generation_tool_auth_allowed: true,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert!(
|
||||
!tools.iter().any(|tool| tool.spec.name() == "js_repl"),
|
||||
"js_repl should be disabled when the feature is off"
|
||||
);
|
||||
assert!(
|
||||
!tools.iter().any(|tool| tool.spec.name() == "js_repl_reset"),
|
||||
"js_repl_reset should be disabled when the feature is off"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_repl_enabled_adds_tools() {
|
||||
let model_info = model_info();
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::JsRepl);
|
||||
|
||||
let available_models = Vec::new();
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
available_models: &available_models,
|
||||
features: &features,
|
||||
image_generation_tool_auth_allowed: true,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
session_source: SessionSource::Cli,
|
||||
sandbox_policy: &SandboxPolicy::DangerFullAccess,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
});
|
||||
let (tools, _) = build_specs(
|
||||
&tools_config,
|
||||
/*mcp_tools*/ None,
|
||||
/*deferred_mcp_tools*/ None,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_contains_tool_names(&tools, &["js_repl", "js_repl_reset"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_generation_tools_require_feature_and_supported_model() {
|
||||
let supported_model_info = model_info();
|
||||
|
||||
@@ -18,8 +18,6 @@ pub enum ToolHandlerKind {
|
||||
CodeModeWait,
|
||||
DynamicTool,
|
||||
FollowupTaskV2,
|
||||
JsRepl,
|
||||
JsReplReset,
|
||||
ListAgentsV2,
|
||||
ListDir,
|
||||
Mcp,
|
||||
|
||||
@@ -1751,30 +1751,6 @@ async fn experimental_features_toggle_saves_on_exit() {
|
||||
assert_eq!(updates, vec![(expected_feature, true)]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn experimental_popup_shows_js_repl_node_requirement() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
let js_repl_description = FEATURES
|
||||
.iter()
|
||||
.find(|spec| spec.id == Feature::JsRepl)
|
||||
.and_then(|spec| spec.stage.experimental_menu_description())
|
||||
.expect("expected js_repl experimental description");
|
||||
let node_requirement = js_repl_description
|
||||
.split(". ")
|
||||
.find(|sentence| sentence.starts_with("Requires Node >= v"))
|
||||
.map(|sentence| sentence.trim_end_matches(" installed."))
|
||||
.expect("expected js_repl description to mention the Node requirement");
|
||||
|
||||
chat.open_experimental_popup();
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 120);
|
||||
assert!(
|
||||
popup.contains(node_requirement),
|
||||
"expected js_repl feature description to mention the required Node version, got:\n{popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn experimental_popup_omits_stable_guardian_approval() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
Reference in New Issue
Block a user