Remove js_repl feature (#19410)

This commit is contained in:
Curtis 'Fjord' Hawthorne authored and GitHub committed 2026-04-24 17:49:29 -07:00
1 parent cf02e9c052
commit 8a559e7938
63 files changed
+77 -9261

No files matched your search

+1 -2
View File
@@ -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(),
}
-1
View File
@@ -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,
-300
View File
@@ -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());
}
-3
View File
@@ -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
-6
View File
@@ -1,6 +0,0 @@
/*!
* Meriyah v7.0.0
* Source: npm package meriyah@7.0.0 (dist/meriyah.umd.min.js)
* License: ISC (see third_party/meriyah/LICENSE)
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).meriyah={})}(this,(function(e){"use strict";const t=((e,t)=>{const r=new Uint32Array(69632);let n=0,o=0;for(;n<2597;){const a=e[n++];if(a<0)o-=a;else{let i=e[n++];2&a&&(i=t[i]),1&a?r.fill(i,o,o+=e[n++]):r[o++]=i}}return r})([-1,2,26,2,27,2,5,-1,0,77595648,3,44,2,3,0,14,2,61,2,62,3,0,3,0,3168796671,0,4294956992,2,1,2,0,2,41,3,0,4,0,4294966523,3,0,4,2,16,2,63,2,0,0,4294836735,0,3221225471,0,4294901942,2,64,0,134152192,3,0,2,0,4294951935,3,0,2,0,2683305983,0,2684354047,2,17,2,0,0,4294961151,3,0,2,2,19,2,0,0,608174079,2,0,2,58,2,7,2,6,0,4286643967,3,0,2,2,1,3,0,3,0,4294901711,2,40,0,4089839103,0,2961209759,0,1342439375,0,4294543342,0,3547201023,0,1577204103,0,4194240,0,4294688750,2,2,0,80831,0,4261478351,0,4294549486,2,2,0,2967484831,0,196559,0,3594373100,0,3288319768,0,8469959,0,65472,2,3,0,4093640191,0,929054175,0,65487,0,4294828015,0,4092591615,0,1885355487,0,982991,2,3,2,0,0,2163244511,0,4227923919,0,4236247022,2,69,0,4284449919,0,851904,2,4,2,12,0,67076095,-1,2,70,0,1073741743,0,4093607775,-1,0,50331649,0,3265266687,2,33,0,4294844415,0,4278190047,2,20,2,137,-1,3,0,2,2,23,2,0,2,9,2,0,2,15,2,22,3,0,10,2,72,2,0,2,73,2,74,2,75,2,0,2,76,2,0,2,11,0,261632,2,25,3,0,2,2,13,2,4,3,0,18,2,77,2,5,3,0,2,2,78,0,2151677951,2,29,2,10,0,909311,3,0,2,0,814743551,2,48,0,67090432,3,0,2,2,42,2,0,2,6,2,0,2,30,2,8,0,268374015,2,108,2,51,2,0,2,79,0,134153215,-1,2,7,2,0,2,8,0,2684354559,0,67044351,0,3221160064,2,9,2,18,3,0,2,2,53,0,1046528,3,0,3,2,10,2,0,2,127,0,4294960127,2,9,2,6,2,11,0,4294377472,2,12,3,0,16,2,13,2,0,2,80,2,9,2,0,2,81,2,82,2,83,0,12288,2,54,0,1048577,2,84,2,14,-1,2,14,0,131042,2,85,2,86,2,87,2,0,2,34,-83,3,0,7,0,1046559,2,0,2,15,2,0,0,2147516671,2,21,3,88,2,2,0,-16,2,89,0,524222462,2,4,2,0,0,4269801471,2,4,3,0,2,2,28,2,16,3,0,2,2,49,2,0,-1,2,17,-16,3,0,206,-2,3,0,692,2,71,-1,2,17,2,9,3,0,8,2,91,2,18,2,0,0,3220242431,3,0,3,2,19,2,92,2,93,3,0,2,2,94,2,0,2,20,2,95,2,0,0,4351,2,0,2,10,3,0,2,0,67043391,0,3909091327,2,0,2,24,2,10,2,20,3,0,2,0,67076097,2,8,2,0,2,21,0,67059711,0,4236247039,3,0,2,0,939524103,0,8191999,2,99,2,100,2,22,2,23,3,0,3,0,67057663,3,0,349,2,101,2,102,2,7,-264,3,0,11,2,24,3,0,2,2,32,-1,0,3774349439,2,103,2,104,3,0,2,2,19,2,105,3,0,10,2,9,2,17,2,0,2,46,2,0,2,31,2,106,2,25,0,1638399,0,57344,2,107,3,0,3,2,20,2,26,2,27,2,5,2,28,2,0,2,8,2,109,-1,2,110,2,111,2,112,-1,3,0,3,2,12,-2,2,0,2,29,-3,0,536870912,-4,2,20,2,0,2,36,0,1,2,0,2,65,2,6,2,12,2,9,2,0,2,113,-1,3,0,4,2,9,2,23,2,114,2,7,2,0,2,115,2,0,2,116,2,117,2,118,2,0,2,10,3,0,9,2,21,2,30,2,31,2,119,2,120,-2,2,121,2,122,2,30,2,21,2,8,-2,2,123,2,30,3,32,2,-1,2,0,2,39,-2,0,4277137519,0,2269118463,-1,3,20,2,-1,2,33,2,38,2,0,3,30,2,2,35,2,19,-3,3,0,2,2,34,-1,2,0,2,35,2,0,2,35,2,0,2,47,2,0,0,4294950463,2,37,-7,2,0,0,203775,2,125,0,4227858432,2,20,2,43,2,36,2,17,2,37,2,17,2,124,2,21,3,0,2,2,38,0,2151677888,2,0,2,12,0,4294901764,2,145,2,0,2,56,2,55,0,5242879,3,0,2,0,402644511,-1,2,128,2,39,0,3,-1,2,129,2,130,2,0,0,67045375,2,40,0,4226678271,0,3766565279,0,2039759,2,132,2,41,0,1046437,0,6,3,0,2,0,3288270847,0,3,3,0,2,0,67043519,-5,2,0,0,4282384383,0,1056964609,-1,3,0,2,0,67043345,-1,2,0,2,42,2,23,2,50,2,11,2,59,2,38,-5,2,0,2,12,-3,3,0,2,0,2147484671,2,133,0,4190109695,2,52,-2,2,134,0,4244635647,0,27,2,0,2,8,2,43,2,0,2,66,2,17,2,0,2,42,-3,2,31,-2,2,0,2,45,2,57,2,44,2,45,2,135,2,46,0,8388351,-2,2,136,0,3028287487,2,47,2,138,0,33259519,2,23,2,7,2,48,-7,2,21,0,4294836223,0,3355443199,0,134152199,-2,2,67,-2,3,0,28,2,32,-3,3,0,3,2,49,3,0,6,2,50,-81,2,17,3,0,2,2,36,3,0,33,2,25,2,30,3,0,124,2,12,3,0,18,2,38,-213,2,0,2,32,-54,3,0,17,2,42,2,8,2,23,2,0,2,8,2,23,2,51,2,0,2,21,2,52,2,139,2,25,-13,2,0,2,53,-6,3,0,2,-1,2,140,2,10,-1,3,0,2,0,4294936575,2,0,0,4294934783,-2,0,8323099,3,0,230,2,30,2,54,2,8,-3,3,0,3,2,35,-271,2,141,3,0,9,2,142,2,143,2,55,3,0,11,2,7,-72,3,0,3,2,144,0,1677656575,-130,2,26,-16,2,0,2,24,2,38,-16,0,4161266656,0,4071,0,15360,-4,0,28,-13,3,0,2,2,56,2,0,2,146,2,147,2,60,2,0,2,148,2,149,2,150,3,0,10,2,151,2,152,2,22,3,56,2,3,153,2,3,57,2,0,4294954999,2,0,-16,2,0,2,90,2,0,0,2105343,0,4160749584,0,65534,-34,2,8,2,155,-6,0,4194303871,0,4294903771,2,0,2,58,2,98,-3,2,0,0,1073684479,0,17407,-9,2,17,2,49,2,0,2,32,-14,2,17,2,32,-6,2,17,2,12,-6,2,8,0,3225419775,-7,2,156,3,0,6,0,8323103,-1,3,0,2,2,59,-37,2,60,2,157,2,158,2,159,2,160,2,161,-105,2,26,-32,3,0,1335,-1,3,0,136,2,9,3,0,180,2,24,3,0,233,2,162,3,0,18,2,9,-77,3,0,16,2,9,-47,3,0,154,2,6,3,0,264,2,32,-22116,3,0,7,2,25,-6130,3,5,2,-1,0,69207040,3,44,2,3,0,14,2,61,2,62,-3,0,3168731136,0,4294956864,2,1,2,0,2,41,3,0,4,0,4294966275,3,0,4,2,16,2,63,2,0,2,34,-1,2,17,2,64,-1,2,0,0,2047,0,4294885376,3,0,2,0,3145727,0,2617294944,0,4294770688,2,25,2,65,3,0,2,0,131135,2,96,0,70256639,0,71303167,0,272,2,42,2,6,0,65279,2,0,2,48,-1,2,97,2,66,0,4278255616,0,4294836227,0,4294549473,0,600178175,0,2952806400,0,268632067,0,4294543328,0,Line truncated
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-1
View File
@@ -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;
-12
View File
@@ -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,
-172
View File
@@ -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,
-10
View File
@@ -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>,