fix(engine): remove Arcee tool catalog exception
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user