feat(phase5): implement mode consolidation and tool availability system
Implements Phase 5 from the roadmap with complete mode-based tool filtering: - Add Mode enum (Chat/Code) with FromStr trait implementation - Extend Config with ModeConfig for per-mode tool availability - Update ToolRegistry to enforce mode-based filtering - Add --code/-c CLI argument to start in code mode - Implement TUI commands: :mode, :code, :chat, :tools - Add operating mode indicator to status line (💬/💻 badges) - Create comprehensive documentation in docs/phase5-mode-system.md Default configuration: - Chat mode: only web_search allowed - Code mode: all tools allowed (wildcard *) All code compiles cleanly with cargo clippy passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
182
crates/owlen-core/src/mode.rs
Normal file
182
crates/owlen-core/src/mode.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
//! Operating modes for Owlen
|
||||
//!
|
||||
//! Defines the different modes in which Owlen can operate and their associated
|
||||
//! tool availability policies.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Operating mode for Owlen
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Mode {
|
||||
/// Chat mode - limited tool access, safe for general conversation
|
||||
#[default]
|
||||
Chat,
|
||||
/// Code mode - full tool access for development tasks
|
||||
Code,
|
||||
}
|
||||
|
||||
impl Mode {
|
||||
/// Get the display name for this mode
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
Mode::Chat => "chat",
|
||||
Mode::Code => "code",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Mode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.display_name())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Mode {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"chat" => Ok(Mode::Chat),
|
||||
"code" => Ok(Mode::Code),
|
||||
_ => Err(format!(
|
||||
"Invalid mode: '{}'. Valid modes are 'chat' or 'code'",
|
||||
s
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for tool availability in different modes
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ModeConfig {
|
||||
/// Tools allowed in chat mode
|
||||
#[serde(default = "ModeConfig::default_chat_tools")]
|
||||
pub chat: ModeToolConfig,
|
||||
/// Tools allowed in code mode
|
||||
#[serde(default = "ModeConfig::default_code_tools")]
|
||||
pub code: ModeToolConfig,
|
||||
}
|
||||
|
||||
impl Default for ModeConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
chat: Self::default_chat_tools(),
|
||||
code: Self::default_code_tools(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModeConfig {
|
||||
fn default_chat_tools() -> ModeToolConfig {
|
||||
ModeToolConfig {
|
||||
allowed_tools: vec!["web_search".to_string()],
|
||||
}
|
||||
}
|
||||
|
||||
fn default_code_tools() -> ModeToolConfig {
|
||||
ModeToolConfig {
|
||||
allowed_tools: vec!["*".to_string()], // All tools allowed
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a tool is allowed in the given mode
|
||||
pub fn is_tool_allowed(&self, mode: Mode, tool_name: &str) -> bool {
|
||||
let config = match mode {
|
||||
Mode::Chat => &self.chat,
|
||||
Mode::Code => &self.code,
|
||||
};
|
||||
|
||||
config.is_tool_allowed(tool_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool configuration for a specific mode
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ModeToolConfig {
|
||||
/// List of allowed tools. Use "*" to allow all tools.
|
||||
pub allowed_tools: Vec<String>,
|
||||
}
|
||||
|
||||
impl ModeToolConfig {
|
||||
/// Check if a tool is allowed in this mode
|
||||
pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
|
||||
// Check for wildcard
|
||||
if self.allowed_tools.iter().any(|t| t == "*") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if tool is explicitly listed
|
||||
self.allowed_tools.iter().any(|t| t == tool_name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mode_display() {
|
||||
assert_eq!(Mode::Chat.to_string(), "chat");
|
||||
assert_eq!(Mode::Code.to_string(), "code");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mode_from_str() {
|
||||
assert_eq!("chat".parse::<Mode>(), Ok(Mode::Chat));
|
||||
assert_eq!("code".parse::<Mode>(), Ok(Mode::Code));
|
||||
assert_eq!("CHAT".parse::<Mode>(), Ok(Mode::Chat));
|
||||
assert_eq!("CODE".parse::<Mode>(), Ok(Mode::Code));
|
||||
assert!("invalid".parse::<Mode>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_mode() {
|
||||
assert_eq!(Mode::default(), Mode::Chat);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chat_mode_restrictions() {
|
||||
let config = ModeConfig::default();
|
||||
|
||||
// Web search should be allowed in chat mode
|
||||
assert!(config.is_tool_allowed(Mode::Chat, "web_search"));
|
||||
|
||||
// Code exec should not be allowed in chat mode
|
||||
assert!(!config.is_tool_allowed(Mode::Chat, "code_exec"));
|
||||
assert!(!config.is_tool_allowed(Mode::Chat, "file_write"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_mode_allows_all() {
|
||||
let config = ModeConfig::default();
|
||||
|
||||
// All tools should be allowed in code mode
|
||||
assert!(config.is_tool_allowed(Mode::Code, "web_search"));
|
||||
assert!(config.is_tool_allowed(Mode::Code, "code_exec"));
|
||||
assert!(config.is_tool_allowed(Mode::Code, "file_write"));
|
||||
assert!(config.is_tool_allowed(Mode::Code, "anything"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wildcard_tool_config() {
|
||||
let config = ModeToolConfig {
|
||||
allowed_tools: vec!["*".to_string()],
|
||||
};
|
||||
|
||||
assert!(config.is_tool_allowed("any_tool"));
|
||||
assert!(config.is_tool_allowed("another_tool"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_explicit_tool_list() {
|
||||
let config = ModeToolConfig {
|
||||
allowed_tools: vec!["tool1".to_string(), "tool2".to_string()],
|
||||
};
|
||||
|
||||
assert!(config.is_tool_allowed("tool1"));
|
||||
assert!(config.is_tool_allowed("tool2"));
|
||||
assert!(!config.is_tool_allowed("tool3"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user