fix(engine): remove Arcee tool catalog exception

This commit is contained in:
CodeWhale Agent
2026-06-17 15:54:52 -07:00
parent 19510bcf95
commit 1bfcced43c
5 changed files with 25 additions and 164 deletions
+1 -3
View File
@@ -796,9 +796,7 @@ impl DeepSeekClient {
&self.base_url
}
/// Returns the active API provider for this client. Used by the turn loop
/// to apply provider-specific request policies (e.g. Arcee's reduced
/// first-turn tool surface that clears the Cloudflare WAF).
/// Returns the active API provider for this client.
pub fn api_provider(&self) -> ApiProvider {
self.api_provider
}
+4 -4
View File
@@ -3182,10 +3182,10 @@ use self::streaming::{
};
use self::tool_catalog::{
CODE_EXECUTION_TOOL_NAME, JS_EXECUTION_TOOL_NAME, MULTI_TOOL_PARALLEL_NAME,
REQUEST_USER_INPUT_NAME, active_tools_for_step, apply_provider_tool_policy,
build_model_tool_catalog, ensure_advanced_tooling, execute_code_execution_tool,
execute_tool_search, initial_active_tools, is_tool_search_tool,
maybe_hydrate_requested_deferred_tool, missing_tool_error_message,
REQUEST_USER_INPUT_NAME, active_tools_for_step, build_model_tool_catalog,
ensure_advanced_tooling, execute_code_execution_tool, execute_tool_search,
initial_active_tools, is_tool_search_tool, maybe_hydrate_requested_deferred_tool,
missing_tool_error_message,
};
#[cfg(test)]
use self::tool_catalog::{
+20 -91
View File
@@ -776,102 +776,31 @@ fn model_tool_catalog_applies_native_and_mcp_deferral() {
}
#[test]
fn arcee_provider_policy_defers_risky_tools_keeps_read_only_and_tool_search() {
fn plugin_or_benchmark_tools_marked_loaded_stay_active() {
let always_load = HashSet::new();
let mut catalog = vec![
api_tool("read_file"),
api_tool("list_dir"),
api_tool("git_status"),
api_tool("git_diff"),
api_tool("grep_files"),
api_tool("file_search"),
api_tool("update_plan"),
api_tool("checklist_write"),
api_tool("exec_shell"),
api_tool("apply_patch"),
api_tool("write_file"),
api_tool("edit_file"),
api_tool("fetch_url"),
api_tool("web_search"),
api_tool("tool_search_tool_regex"),
api_tool("tool_search_tool_bm25"),
];
let mut catalog = build_model_tool_catalog(
vec![api_tool("KB_search"), api_tool("read_file")],
Vec::new(),
AppMode::Agent,
&always_load,
);
apply_provider_tool_policy(&mut catalog, ApiProvider::Arcee, &always_load);
let defer = |name: &str| {
catalog
.iter()
.find(|tool| tool.name == name)
.and_then(|tool| tool.defer_loading)
};
// Benign read-only first-turn set stays active so the opening Arcee
// request clears Cloudflare's WAF.
for active in [
"read_file",
"list_dir",
"git_status",
"git_diff",
"grep_files",
"file_search",
"update_plan",
"checklist_write",
] {
assert_eq!(defer(active), Some(false), "{active} should stay active");
}
// Tool-search stays active so the deferred tail remains discoverable.
assert_eq!(defer("tool_search_tool_regex"), Some(false));
assert_eq!(defer("tool_search_tool_bm25"), Some(false));
// WAF-risky / mutating tools are deferred on the first Arcee turn.
for deferred in [
"exec_shell",
"apply_patch",
"write_file",
"edit_file",
"fetch_url",
"web_search",
] {
assert_eq!(defer(deferred), Some(true), "{deferred} should be deferred");
}
// Mirrors Engine::run after configure_plugin_tools(): plugin tools are
// explicitly kept loaded, and no provider-specific policy should re-defer
// them before the first model request.
let bench_tool = catalog
.iter_mut()
.find(|tool| tool.name == "KB_search")
.expect("benchmark tool in catalog");
bench_tool.defer_loading = Some(false);
ensure_advanced_tooling(&mut catalog, AppMode::Agent, &always_load);
let active = initial_active_tools(&catalog);
assert!(
active.contains("KB_search"),
"plugin/benchmark tools marked loaded must be callable on turn 1"
);
assert!(active.contains("read_file"));
assert!(active.contains("tool_search_tool_regex"));
assert!(!active.contains("exec_shell"));
assert!(!active.contains("apply_patch"));
}
#[test]
fn provider_tool_policy_is_noop_for_non_waf_providers() {
let always_load = HashSet::new();
let mut catalog = vec![api_tool("exec_shell"), api_tool("read_file")];
// DeepSeek has no reduced first-turn surface: the policy must leave the
// default deferral flags untouched (here: still unset).
apply_provider_tool_policy(&mut catalog, ApiProvider::Deepseek, &always_load);
assert!(catalog.iter().all(|tool| tool.defer_loading.is_none()));
}
#[test]
fn arcee_provider_policy_honors_always_load_override() {
let mut always_load = HashSet::new();
always_load.insert("exec_shell".to_string());
let mut catalog = vec![api_tool("exec_shell"), api_tool("apply_patch")];
apply_provider_tool_policy(&mut catalog, ApiProvider::Arcee, &always_load);
let defer = |name: &str| {
catalog
.iter()
.find(|tool| tool.name == name)
.and_then(|tool| tool.defer_loading)
};
// A user-pinned always_load tool stays active even on Arcee.
assert_eq!(defer("exec_shell"), Some(false));
// Other risky tools remain deferred.
assert_eq!(defer("apply_patch"), Some(true));
}
#[test]
@@ -11,7 +11,6 @@ use std::time::Duration;
use serde_json::{Value, json};
use crate::config::ApiProvider;
use crate::models::Tool;
use crate::tools::spec::{ToolError, ToolResult, optional_u64, required_str};
use crate::tui::app::AppMode;
@@ -85,63 +84,6 @@ pub(super) fn apply_native_tool_deferral(catalog: &mut [Tool], always_load: &Has
}
}
/// First-turn native tool surface for Arcee (Trinity).
///
/// Arcee's hosted API is fronted by Cloudflare, whose managed WAF returns
/// HTTP 403 "Access Denied" when a request body contains injection-like text.
/// CodeWhale's full agent catalog trips it: shell/patch/code-execution tool
/// descriptions and schemas carry example payloads (`rm -rf`, `../../`,
/// `<script>`, `DROP TABLE`, `eval(base64_decode(...))`) that match the
/// ruleset. Keeping only this benign, read-only set active on the first turn
/// lets the request clear the gateway; every other tool stays deferred in the
/// catalog and remains discoverable through tool-search. Live-verified: a
/// benign `list_dir` tool returns 200 while a risky shell description returns
/// 403 from `api.arcee.ai`.
pub(super) const ARCEE_FIRST_TURN_NATIVE_TOOLS: &[&str] = &[
"checklist_write",
"file_search",
"git_diff",
"git_status",
"grep_files",
"list_dir",
"read_file",
"update_plan",
];
/// Returns the provider-specific first-turn allow-list, or `None` when the
/// provider should use the default deferral policy.
fn provider_first_turn_native_tools(provider: ApiProvider) -> Option<&'static [&'static str]> {
match provider {
ApiProvider::Arcee => Some(ARCEE_FIRST_TURN_NATIVE_TOOLS),
_ => None,
}
}
/// Narrow the *active* tool surface for WAF-fronted providers on top of the
/// default deferral flags. The full catalog is preserved (deferred tools stay
/// present and discoverable via tool-search); only the first-turn `active`
/// partition is reduced so the opening request clears the provider gateway.
///
/// Tool-search tools and any user-pinned `always_load` tools stay active so the
/// model can still hydrate the deferred tail when it needs a tool outside the
/// reduced set.
pub(super) fn apply_provider_tool_policy(
catalog: &mut [Tool],
provider: ApiProvider,
always_load: &HashSet<String>,
) {
let Some(active) = provider_first_turn_native_tools(provider) else {
return;
};
for tool in catalog {
if is_tool_search_tool(&tool.name) || always_load.contains(&tool.name) {
tool.defer_loading = Some(false);
continue;
}
tool.defer_loading = Some(!active.contains(&tool.name.as_str()));
}
}
fn should_keep_mcp_tool_loaded(name: &str) -> bool {
matches!(
name,
-8
View File
@@ -92,14 +92,6 @@ impl Engine {
let mut tool_catalog = tools.unwrap_or_default();
if !tool_catalog.is_empty() {
ensure_advanced_tooling(&mut tool_catalog, mode, &self.config.tools_always_load);
// Provider-specific first-turn surface (e.g. Arcee's Cloudflare WAF
// rejects CodeWhale's full agent catalog). Runs after advanced
// tooling so code/js-execution and tool-search rows are policed too.
apply_provider_tool_policy(
&mut tool_catalog,
client.api_provider(),
&self.config.tools_always_load,
);
}
let mut active_tool_names = initial_active_tools(&tool_catalog);
let mut loop_guard = LoopGuard::default();