diff --git a/AGENTS.md b/AGENTS.md index eefc008..af1e071 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,8 @@ REPL 内所有 slash command 由程序直接处理,不进入 agent loop。 当前项目支持最小 memory:使用 `/remember` 保存长期信息,使用 `/memory` 查看。 +写操作和 Bash 默认需要确认,可通过 `--auto-approve` 或 `/permissions auto-on` 跳过。 + 未明确说明时,使用以下默认值: - 工作目录:当前进程目录 diff --git a/SKILLS/cli-core.md b/SKILLS/cli-core.md index 6a295db..d2f4179 100644 --- a/SKILLS/cli-core.md +++ b/SKILLS/cli-core.md @@ -25,6 +25,7 @@ - 需要创建或覆盖文件时,优先使用 `Write` 工具,而不是 `Bash` - 会话与 memory 管理优先使用 slash command,而不是自然语言或 `Bash` 探查对应文件 - 长期有价值的项目约束或偏好,优先使用 `/remember` 保存 +- 高风险操作会触发确认,优先先读再改,减少无意义审批 - Windows 下先验证 shell 兼容性,再选择命令写法 ## 沟通风格 diff --git a/src/cc_slim/commands.py b/src/cc_slim/commands.py index 48b4033..e519fe6 100644 --- a/src/cc_slim/commands.py +++ b/src/cc_slim/commands.py @@ -7,6 +7,7 @@ from rich.console import Console from cc_slim.engine import Agent from cc_slim.memory import MemoryStore +from cc_slim.permissions import PermissionChecker from cc_slim.session import SessionStore @@ -26,6 +27,7 @@ def handle_command( config: Any, store: SessionStore, memory: MemoryStore, + permissions: PermissionChecker, agent: Agent, build_agent: Any, render_history: Any, @@ -45,6 +47,9 @@ def handle_command( console.print("/new - 创建全新 session") console.print("/remember - 保存长期 memory") console.print("/memory - 查看当前项目 memory") + console.print("/permissions - 查看当前权限状态") + console.print("/permissions auto-on - 开启自动批准") + console.print("/permissions auto-off - 关闭自动批准") return agent if name in {"/clear", "/new"}: @@ -71,6 +76,22 @@ def handle_command( console.print(content or "当前项目还没有 memory。") return agent + if name == "/permissions": + if args == "auto-on": + permissions.set_auto_approve(True) + console.print("已开启 auto_approve") + return agent + if args == "auto-off": + permissions.set_auto_approve(False) + console.print("已关闭 auto_approve") + return agent + + status = permissions.status() + console.print(f"auto_approve: {status['auto_approve']}") + console.print(f"自动允许: {', '.join(status['auto_allowed_tools'])}") + console.print(f"需要确认: {', '.join(status['confirm_required_tools'])}") + return agent + if name == "/remember": if not args: console.print("[red]error:[/red] 缺少需要保存的 memory 内容") diff --git a/src/cc_slim/engine.py b/src/cc_slim/engine.py index e3eaf67..86de4c8 100644 --- a/src/cc_slim/engine.py +++ b/src/cc_slim/engine.py @@ -14,6 +14,7 @@ from anthropic import Anthropic from openai import OpenAI from cc_slim.memory import MemoryStore +from cc_slim.permissions import PermissionChecker from cc_slim.session import SessionStore from cc_slim.tools import Tool @@ -67,12 +68,16 @@ class Agent: session_store: SessionStore | None = None, session_id: str | None = None, history: list[dict[str, Any]] | None = None, + permission_checker: PermissionChecker | None = None, + confirm_tool: Any | None = None, ) -> None: self.config = config self.tools = {tool.name: tool for tool in tools} self.history: list[dict[str, Any]] = list(history or []) self.session_store = session_store self.session_id = session_id + self.permission_checker = permission_checker + self.confirm_tool = confirm_tool self.system_prompt = self._build_system_prompt(workspace) self.client = self._build_client() @@ -266,6 +271,11 @@ class Agent: return result def _run_tool(self, name: str, payload: dict[str, Any]) -> str: + if self.permission_checker and self.permission_checker.requires_confirmation(name): + if not self.confirm_tool: + return f"Permission denied for tool: {name}" + if not self.confirm_tool(name, payload): + return f"Permission denied for tool: {name}" tool = self.tools.get(name) if not tool: return f"Tool not found: {name}" diff --git a/src/cc_slim/main.py b/src/cc_slim/main.py index d08c683..cc76744 100644 --- a/src/cc_slim/main.py +++ b/src/cc_slim/main.py @@ -10,6 +10,7 @@ from rich.table import Table from cc_slim.commands import handle_command, parse_command from cc_slim.engine import Agent, resolve_config from cc_slim.memory import MemoryStore +from cc_slim.permissions import PermissionChecker from cc_slim.session import SessionStore from cc_slim.tools import build_default_tools @@ -67,6 +68,7 @@ def build_agent( root: Path, config: object, store: SessionStore, + permissions: PermissionChecker, session_meta: dict[str, object], restored_history: list[dict[str, object]] | None = None, ) -> Agent: @@ -77,9 +79,17 @@ def build_agent( session_store=store, session_id=str(session_meta["session_id"]), history=restored_history or [], + permission_checker=permissions, + confirm_tool=confirm_tool, ) +def confirm_tool(name: str, payload: dict[str, object]) -> bool: + console.print(f"即将执行 [{name}] {payload},是否允许? [y/N]", markup=False, end=" ") + answer = input() + return answer.strip().lower() in {"y", "yes"} + + @app.command() def run( prompt: Optional[str] = typer.Argument(None, help="单次执行的用户输入"), @@ -91,10 +101,12 @@ def run( max_turns: Optional[int] = typer.Option(None, help="最大工具循环轮数"), history: bool = typer.Option(False, "--history", help="列出当前工作目录的历史 session"), resume: Optional[str] = typer.Option(None, "--resume", help="按 session id 或序号恢复历史 session"), + auto_approve: bool = typer.Option(False, "--auto-approve", help="跳过高风险工具确认"), ) -> None: root = cwd.resolve() store = SessionStore(root) memory = MemoryStore(root) + permissions = PermissionChecker(auto_approve=auto_approve) if history: render_history(store) return @@ -119,7 +131,7 @@ def run( else: session_meta = store.create_session(config.model) - agent = build_agent(root, config, store, session_meta, restored_history) + agent = build_agent(root, config, store, permissions, session_meta, restored_history) if prompt: render_stream(agent, prompt) @@ -147,6 +159,7 @@ def run( config=config, store=store, memory=memory, + permissions=permissions, agent=agent, build_agent=build_agent, render_history=render_history, diff --git a/src/cc_slim/permissions.py b/src/cc_slim/permissions.py new file mode 100644 index 0000000..92774e0 --- /dev/null +++ b/src/cc_slim/permissions.py @@ -0,0 +1,23 @@ +from __future__ import annotations + + +class PermissionChecker: + def __init__(self, auto_approve: bool = False) -> None: + self.auto_approve = auto_approve + self.auto_allowed_tools = ["Read", "Glob", "Grep"] + self.confirm_required_tools = ["Write", "Edit", "Bash"] + + def requires_confirmation(self, tool_name: str) -> bool: + if self.auto_approve: + return False + return tool_name in self.confirm_required_tools + + def set_auto_approve(self, enabled: bool) -> None: + self.auto_approve = enabled + + def status(self) -> dict[str, object]: + return { + "auto_approve": self.auto_approve, + "auto_allowed_tools": list(self.auto_allowed_tools), + "confirm_required_tools": list(self.confirm_required_tools), + }