use crate::config::constants::tools as tool_names;
use crate::tool_policy::ToolPolicy;
use crate::tools::handlers::PlanningWorkflowState;
use crate::tools::names::canonical_tool_name;
use indexmap::IndexMap;
use super::ToolRegistry;
const PLAN_MODE_EXPLORATION_TOOLS: &[&str] = &[
tool_names::EXEC_COMMAND,
tool_names::CODE_SEARCH,
tool_names::READ_FILE,
tool_names::LIST_FILES,
tool_names::GREP_FILE,
tool_names::TASK_TRACKER,
];
impl ToolRegistry {
pub fn enable_planning(&self) {
let was_active = self.planning_workflow_state.is_active();
self.planning_workflow_state.enable();
if !was_active {
*self.cached_available_tools.write() = None;
self.tool_catalog_state.note_explicit_refresh("planning_workflow_enabled");
}
}
pub fn disable_planning(&self) {
let was_active = self.planning_workflow_state.is_active();
self.planning_workflow_state.disable();
if was_active {
*self.cached_available_tools.write() = None;
self.tool_catalog_state.note_explicit_refresh("planning_workflow_disabled");
}
}
#[inline]
pub fn is_planning_active(&self) -> bool {
self.planning_workflow_state.is_active()
}
#[inline]
pub fn planning_workflow_state(&self) -> PlanningWorkflowState {
self.planning_workflow_state.clone()
}
pub async fn apply_planning_mode_policy_overrides(&self) {
if self.planning_mode_policy_overrides.read().is_some() {
return;
}
let mut saved: IndexMap<String, ToolPolicy> = IndexMap::new();
for &tool in PLAN_MODE_EXPLORATION_TOOLS {
let canonical = canonical_tool_name(tool);
let current = self.get_tool_policy(canonical).await;
if current != ToolPolicy::Allow {
saved.insert(canonical.to_owned(), current);
if let Err(err) = self.set_tool_policy(canonical, ToolPolicy::Allow).await {
tracing::warn!(tool = %canonical, error = %err, "failed to apply plan-mode policy override");
}
}
}
if !saved.is_empty() {
*self.planning_mode_policy_overrides.write() = Some(saved);
}
}
pub async fn restore_post_planning_policies(&self) {
let saved = self.planning_mode_policy_overrides.write().take();
if let Some(saved) = saved {
for (tool, policy) in saved {
if let Err(err) = self.set_tool_policy(&tool, policy).await {
tracing::warn!(tool = %tool, error = %err, "failed to restore post-planning policy");
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::constants::tools;
use tempfile::TempDir;
#[tokio::test]
async fn enable_planning_short_circuits_when_already_active() {
let temp_dir = TempDir::new().expect("tempdir");
let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
registry.enable_planning();
let after_first = registry.tool_catalog_state().current_epoch();
registry.enable_planning();
let after_second = registry.tool_catalog_state().current_epoch();
assert_eq!(after_first, after_second, "enabling an already-active planning workflow must not bump the epoch");
assert!(registry.is_planning_active());
}
#[tokio::test]
async fn planning_transitions_invalidate_available_tools() {
let temp_dir = TempDir::new().expect("tempdir");
let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
let inactive = registry.available_tools().await;
assert!(!inactive.contains(&tools::CODE_SEARCH.to_string()));
assert!(!inactive.contains(&tools::REQUEST_USER_INPUT.to_string()));
registry.enable_planning();
let active = registry.available_tools().await;
assert!(active.contains(&tools::CODE_SEARCH.to_string()));
assert!(active.contains(&tools::REQUEST_USER_INPUT.to_string()));
registry.disable_planning();
let inactive_again = registry.available_tools().await;
assert!(!inactive_again.contains(&tools::CODE_SEARCH.to_string()));
assert!(!inactive_again.contains(&tools::REQUEST_USER_INPUT.to_string()));
}
#[tokio::test]
async fn apply_planning_mode_policy_overrides_allows_exploration_tools() {
let temp_dir = TempDir::new().expect("tempdir");
let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
registry.apply_planning_mode_policy_overrides().await;
for &tool in PLAN_MODE_EXPLORATION_TOOLS {
let canonical = canonical_tool_name(tool);
assert_eq!(
registry.get_tool_policy(canonical).await,
ToolPolicy::Allow,
"{canonical} should be Allow during planning"
);
}
}
#[tokio::test]
async fn apply_planning_mode_policy_overrides_is_idempotent() {
let temp_dir = TempDir::new().expect("tempdir");
let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
registry.apply_planning_mode_policy_overrides().await;
registry.apply_planning_mode_policy_overrides().await;
for &tool in PLAN_MODE_EXPLORATION_TOOLS {
let canonical = canonical_tool_name(tool);
assert_eq!(
registry.get_tool_policy(canonical).await,
ToolPolicy::Allow,
"{canonical} should remain Allow after double-apply"
);
}
}
#[tokio::test]
async fn restore_post_planning_policies_restores_original_policies() {
let temp_dir = TempDir::new().expect("tempdir");
let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
registry.set_tool_policy(tools::EXEC_COMMAND, ToolPolicy::Prompt).await.ok();
registry.apply_planning_mode_policy_overrides().await;
assert_eq!(registry.get_tool_policy(tools::EXEC_COMMAND).await, ToolPolicy::Allow);
registry.restore_post_planning_policies().await;
assert_eq!(
registry.get_tool_policy(tools::EXEC_COMMAND).await,
ToolPolicy::Prompt,
"exec_command policy should be restored to Prompt"
);
}
#[tokio::test]
async fn restore_post_planning_policies_preserves_unrelated_tools() {
let temp_dir = TempDir::new().expect("tempdir");
let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
registry.set_tool_policy(tools::APPLY_PATCH, ToolPolicy::Deny).await.ok();
registry.apply_planning_mode_policy_overrides().await;
registry.restore_post_planning_policies().await;
assert_eq!(
registry.get_tool_policy(tools::APPLY_PATCH).await,
ToolPolicy::Deny,
"unrelated tool policy must not be touched"
);
}
#[tokio::test]
async fn restore_without_apply_is_noop() {
let temp_dir = TempDir::new().expect("tempdir");
let registry = ToolRegistry::new(temp_dir.path().to_path_buf()).await;
registry.restore_post_planning_policies().await;
for &tool in PLAN_MODE_EXPLORATION_TOOLS {
let canonical = canonical_tool_name(tool);
let policy = registry.get_tool_policy(canonical).await;
assert!(
matches!(policy, ToolPolicy::Allow | ToolPolicy::Prompt),
"{canonical} policy should be unchanged: {policy:?}"
);
}
}
}