feat: add permission modes and CLI orchestration layer

Permission Modes:
- Add default/plan/auto_accept modes controlling tool access
- Plan mode restricts Task agent to read-only tools only
- Auto-accept mode bypasses approval prompts (with confirmation)

Approval Scaffolding:
- Add ApprovalRule/ApprovalRuleSet for granular tool control
- Pattern-based matching on tool name and arguments
- Default rules for common safe/dangerous patterns
- Prep for future bidirectional approval flow

CLI Refactor:
- Default to Task agent (main orchestrator)
- Add --mode flag and runtime mode switching
- Integrate prompt_toolkit for better UX:
  - Persistent command history (~/.webber_history)
  - Tab completion for commands and file paths
  - Auto-suggest from history
- Deprecate standalone 'explore' command

Other:
- Split CHANGELOG.md into per-package files
- Update AGENTS.md release procedure for both packages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-14 08:48:07 +01:00
co-authored by Claude Opus 4.5
parent acf231eb66
commit b7956f88ed
14 changed files with 1047 additions and 367 deletions
+103 -71
View File
@@ -1,11 +1,9 @@
"""
Tool registrations for the Task agent.
The Task agent has access to ALL tools:
- Read-only tools (same as Explore/Plan)
- Write tools (edit, write, bash full)
- External tools (web search)
- Orchestration (spawn sub-agents)
The Task agent has access to tools based on permission mode:
- Plan mode: Read-only tools only
- Default/auto_accept: All tools including write operations
"""
from pydantic_ai import Agent, RunContext
@@ -20,19 +18,8 @@ from src.domains.tools.shell.bash import BashReadOnlyTool
from src.domains.tools.shell.bash_full import BashTool
def register_task_tools(agent: Agent[AgentContext, str]) -> None:
"""
Register all tools with the Task agent.
Includes:
- Read-only tools: read_file, glob_files, grep_content, bash_readonly
- Write tools: edit_file, write_file, bash
- External: web_search
- Orchestration: spawn_agent
"""
# === Read-only tools ===
def _register_read_file(agent: Agent[AgentContext, str]) -> None:
"""Register read_file tool."""
@agent.tool
async def read_file(
ctx: RunContext[AgentContext],
@@ -60,6 +47,9 @@ def register_task_tools(agent: Agent[AgentContext, str]) -> None:
)
return result.to_string()
def _register_glob_files(agent: Agent[AgentContext, str]) -> None:
"""Register glob_files tool."""
@agent.tool
async def glob_files(
ctx: RunContext[AgentContext],
@@ -91,6 +81,9 @@ def register_task_tools(agent: Agent[AgentContext, str]) -> None:
)
return result.to_string()
def _register_grep_content(agent: Agent[AgentContext, str]) -> None:
"""Register grep_content tool."""
@agent.tool
async def grep_content(
ctx: RunContext[AgentContext],
@@ -124,6 +117,9 @@ def register_task_tools(agent: Agent[AgentContext, str]) -> None:
)
return result.to_string()
def _register_bash_readonly(agent: Agent[AgentContext, str]) -> None:
"""Register bash_readonly tool."""
@agent.tool
async def bash_readonly(
ctx: RunContext[AgentContext],
@@ -159,6 +155,94 @@ def register_task_tools(agent: Agent[AgentContext, str]) -> None:
)
return result.to_string()
def _register_spawn_agent(agent: Agent[AgentContext, str], readonly_only: bool = False) -> None:
"""Register spawn_agent tool."""
@agent.tool
async def spawn_agent(
ctx: RunContext[AgentContext],
agent_type: str,
prompt: str,
working_dir: str | None = None
) -> str:
"""Spawn a sub-agent to handle a focused task.
Use this to offload work to specialized agents:
- "explore": Fast codebase searches and analysis (read-only)
- "plan": Design implementation strategies (read-only)
Args:
agent_type: Type of agent to spawn ("explore" or "plan")
prompt: Task description for the sub-agent
working_dir: Working directory for the sub-agent (default: current)
Returns:
Sub-agent's consolidated response.
Examples:
- spawn_agent(agent_type="explore", prompt="find all test files")
- spawn_agent(agent_type="plan", prompt="design user auth feature")
IMPORTANT:
- Use sub-agents to keep context focused and efficient
- Explore agent for research, Plan agent for design
- Cannot spawn nested Task agents (recursion risk)
"""
from src.domains.agents.base import get_agent
# Validate agent type
allowed_types = ["explore", "plan"]
if agent_type not in allowed_types:
if agent_type == "task":
return "Error: Cannot spawn nested Task agents (recursion risk)"
return f"Error: Unknown agent type '{agent_type}'. Allowed: {allowed_types}"
sub_agent = get_agent(agent_type)
if not sub_agent:
return f"Error: Agent '{agent_type}' not found in registry"
try:
result = await sub_agent.run(
prompt=prompt,
working_dir=working_dir or ctx.deps.working_dir,
allowed_paths=ctx.deps.allowed_paths,
)
return result
except Exception as e:
return f"Sub-agent error: {e}"
def register_readonly_tools(agent: Agent[AgentContext, str]) -> None:
"""
Register read-only tools with the agent.
Used in plan mode. Includes:
- read_file, glob_files, grep_content, bash_readonly
- spawn_agent (restricted to explore/plan)
"""
_register_read_file(agent)
_register_glob_files(agent)
_register_grep_content(agent)
_register_bash_readonly(agent)
_register_spawn_agent(agent, readonly_only=True)
def register_task_tools(agent: Agent[AgentContext, str]) -> None:
"""
Register all tools with the Task agent.
Includes:
- Read-only tools: read_file, glob_files, grep_content, bash_readonly
- Write tools: edit_file, write_file, bash
- External: web_search
- Orchestration: spawn_agent
"""
# Register read-only tools via helpers
_register_read_file(agent)
_register_glob_files(agent)
_register_grep_content(agent)
_register_bash_readonly(agent)
# === Write tools ===
@agent.tool
@@ -295,56 +379,4 @@ def register_task_tools(agent: Agent[AgentContext, str]) -> None:
return result.to_string()
# === Orchestration tools ===
@agent.tool
async def spawn_agent(
ctx: RunContext[AgentContext],
agent_type: str,
prompt: str,
working_dir: str | None = None
) -> str:
"""Spawn a sub-agent to handle a focused task.
Use this to offload work to specialized agents:
- "explore": Fast codebase searches and analysis (read-only)
- "plan": Design implementation strategies (read-only)
Args:
agent_type: Type of agent to spawn ("explore" or "plan")
prompt: Task description for the sub-agent
working_dir: Working directory for the sub-agent (default: current)
Returns:
Sub-agent's consolidated response.
Examples:
- spawn_agent(agent_type="explore", prompt="find all test files")
- spawn_agent(agent_type="plan", prompt="design user auth feature")
IMPORTANT:
- Use sub-agents to keep context focused and efficient
- Explore agent for research, Plan agent for design
- Cannot spawn nested Task agents (recursion risk)
"""
from src.domains.agents.base import get_agent
# Validate agent type
allowed_types = ["explore", "plan"]
if agent_type not in allowed_types:
if agent_type == "task":
return "Error: Cannot spawn nested Task agents (recursion risk)"
return f"Error: Unknown agent type '{agent_type}'. Allowed: {allowed_types}"
sub_agent = get_agent(agent_type)
if not sub_agent:
return f"Error: Agent '{agent_type}' not found in registry"
try:
result = await sub_agent.run(
prompt=prompt,
working_dir=working_dir or ctx.deps.working_dir,
allowed_paths=ctx.deps.allowed_paths,
)
return result
except Exception as e:
return f"Sub-agent error: {e}"
_register_spawn_agent(agent, readonly_only=False)