Stop filtering model tools in js_repl_tools_only mode (#12069)

## Summary
This change removes tool-list filtering in `js_repl_tools_only` mode and
relies on the normal model tool descriptions, while still enforcing that
tool execution must go through `js_repl` + `codex.tool(...)`.

## Motivation
The previous `js_repl_tools_only` filtering hid most tools from the
model request, which diverged from standard tool-list behavior and made
signatures less discoverable. I tested that this filtering is not
needed, and the model can follow the prompt to only call tools via
`js_repl`.

## What Changed
- `filter_tools_for_model(...)` in `core/src/tools/spec.rs` is now a
pass-through (no filtering when `js_repl_tools_only` is enabled).
- Updated tests to assert that model tools are not filtered in
`js_repl_tools_only` mode.
- Updated dynamic-tool test to assert dynamic tools remain visible in
model tool specs.
- Removed obsolete test helper used only by the old filtering
assertions.

## Safety / Behavior
- This commit does **not** relax execution policy.
- Direct model tool calls remain blocked in `js_repl_tools_only` mode
(except internal `js_repl` tools), and callers are instructed to use
`js_repl` + `codex.tool(...)`.

## Testing
- `cargo test -p codex-core js_repl_tools_only`
- Manual rollout validation showed the model can follow the `js_repl`
routing instructions without needing filtered tool lists.



#### [git stack](https://github.com/magus/git-stack-cli)
- 👉 `1` https://github.com/openai/codex/pull/12069
-  `2` https://github.com/openai/codex/pull/10673
-  `3` https://github.com/openai/codex/pull/10670
This commit is contained in:
Curtis 'Fjord' Hawthorne
2026-02-18 07:31:15 -08:00
committed by GitHub
parent cc3bbd7852
commit 491b4946ae
2 changed files with 1 additions and 105 deletions

View File

@@ -114,17 +114,6 @@ impl ToolsConfig {
}
}
pub(crate) fn filter_tools_for_model(tools: Vec<ToolSpec>, config: &ToolsConfig) -> Vec<ToolSpec> {
if !config.js_repl_tools_only {
return tools;
}
tools
.into_iter()
.filter(|spec| matches!(spec.name(), "js_repl" | "js_repl_reset"))
.collect()
}
/// Generic JSONSchema subset needed for our tool definitions
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "lowercase")]
@@ -1711,27 +1700,6 @@ mod tests {
}
}
fn assert_contains_tool_specs(tools: &[ToolSpec], expected_subset: &[&str]) {
use std::collections::HashSet;
let mut names = HashSet::new();
let mut duplicates = Vec::new();
for name in tools.iter().map(tool_name) {
if !names.insert(name) {
duplicates.push(name);
}
}
assert!(
duplicates.is_empty(),
"duplicate tool entries detected: {duplicates:?}"
);
for expected in expected_subset {
assert!(
names.contains(expected),
"expected tool {expected} to be present; had: {names:?}"
);
}
}
fn shell_tool_name(config: &ToolsConfig) -> Option<&'static str> {
match config.shell_type {
ConfigShellToolType::Default => Some("shell"),
@@ -1954,77 +1922,6 @@ mod tests {
assert_contains_tool_names(&tools, &["js_repl", "js_repl_reset"]);
}
#[test]
fn js_repl_tools_only_filters_model_tools() {
let config = test_config();
let model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::JsRepl);
features.enable(Feature::JsReplToolsOnly);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: Some(WebSearchMode::Cached),
});
let (tools, _) = build_specs(&tools_config, None, None, &[]).build();
let filtered = filter_tools_for_model(
tools.iter().map(|tool| tool.spec.clone()).collect(),
&tools_config,
);
assert_contains_tool_specs(&filtered, &["js_repl", "js_repl_reset"]);
assert!(
!filtered.iter().any(|tool| tool_name(tool) == "shell"),
"expected non-js_repl tools to be hidden when js_repl_tools_only is enabled"
);
}
#[test]
fn js_repl_tools_only_hides_dynamic_tools_from_model_tools() {
let config = test_config();
let model_info =
ModelsManager::construct_model_info_offline_for_tests("gpt-5-codex", &config);
let mut features = Features::with_defaults();
features.enable(Feature::JsRepl);
features.enable(Feature::JsReplToolsOnly);
let tools_config = ToolsConfig::new(&ToolsConfigParams {
model_info: &model_info,
features: &features,
web_search_mode: Some(WebSearchMode::Cached),
});
let dynamic_tools = vec![DynamicToolSpec {
name: "dynamic_echo".to_string(),
description: "echo dynamic payload".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"text": {"type": "string"}
},
"required": ["text"],
"additionalProperties": false
}),
}];
let (tools, _) = build_specs(&tools_config, None, None, &dynamic_tools).build();
assert!(
tools.iter().any(|tool| tool.spec.name() == "dynamic_echo"),
"expected dynamic tool in full router specs"
);
let filtered = filter_tools_for_model(
tools.iter().map(|tool| tool.spec.clone()).collect(),
&tools_config,
);
assert!(
!filtered
.iter()
.any(|tool| tool_name(tool) == "dynamic_echo"),
"expected dynamic tools to be hidden from direct model tools in js_repl_tools_only mode"
);
assert_contains_tool_specs(&filtered, &["js_repl", "js_repl_reset"]);
}
fn assert_model_tools(
model_slug: &str,
features: &Features,