权限和模式统一

This commit is contained in:
hc 2026-04-12 00:46:49 +08:00
parent de0fad496c
commit a3256d15f4
6 changed files with 76 additions and 48 deletions

View File

@ -31,12 +31,18 @@ REPL 内的会话管理命令优先使用 slash command。
REPL 内所有 slash command 由程序直接处理,不进入 agent loop。 REPL 内所有 slash command 由程序直接处理,不进入 agent loop。
REPL 会显示当前模式提示符,例如 `build >``plan >`
当前项目支持最小 memory使用 `/remember` 保存长期信息,使用 `/memory` 查看。 当前项目支持最小 memory使用 `/remember` 保存长期信息,使用 `/memory` 查看。
写操作和 Bash 默认需要确认,可通过 `--auto-approve``/permissions auto-on` 跳过。 写操作和 Bash 默认需要确认,可通过 `--auto-approve``/permissions auto-on` 跳过。
工作区外访问和 `plan` 模式限制属于硬性边界,不通过审批放行。
支持 `/mode build``/mode plan` 两种模式,`plan` 为只读规划模式。 支持 `/mode build``/mode plan` 两种模式,`plan` 为只读规划模式。
`/clear` 是清空当前上下文的主别名,等价于 `/new`
未明确说明时,使用以下默认值: 未明确说明时,使用以下默认值:
- 工作目录:当前进程目录 - 工作目录:当前进程目录

View File

@ -24,6 +24,7 @@
- 修改已有文件时,优先使用 `Edit`,而不是 `Bash``Write` - 修改已有文件时,优先使用 `Edit`,而不是 `Bash``Write`
- 需要创建或覆盖文件时,优先使用 `Write` 工具,而不是 `Bash` - 需要创建或覆盖文件时,优先使用 `Write` 工具,而不是 `Bash`
- 会话与 memory 管理优先使用 slash command而不是自然语言或 `Bash` 探查对应文件 - 会话与 memory 管理优先使用 slash command而不是自然语言或 `Bash` 探查对应文件
- 优先通过 `/help` 查看当前命令,而不是猜测命令格式
- 长期有价值的项目约束或偏好,优先使用 `/remember` 保存 - 长期有价值的项目约束或偏好,优先使用 `/remember` 保存
- 高风险操作会触发确认,优先先读再改,减少无意义审批 - 高风险操作会触发确认,优先先读再改,减少无意义审批
- 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行 - 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行

View File

@ -42,45 +42,54 @@ def handle_command(
args = parsed["args"] args = parsed["args"]
if name == "/help": if name == "/help":
console.print("/help - 显示命令帮助") console.print("会话命令")
console.print("/clear - 清空当前上下文并创建新 session") console.print(" /history 查看当前工作目录的历史会话")
console.print("/history - 查看历史 session") console.print(" /resume <id-or-index> 恢复指定会话")
console.print("/resume <id-or-index> - 恢复指定 session") console.print(" /new 创建新会话")
console.print("/new - 创建全新 session") console.print(" /clear /new 的别名,用于清空当前上下文")
console.print("/remember <text> - 保存长期 memory") console.print("")
console.print("/memory - 查看当前项目 memory") console.print("Memory 命令")
console.print("/mode - 查看当前模式") console.print(" /memory 查看当前项目 memory")
console.print("/mode build - 切换到 build 模式") console.print(" /remember <text> 追加保存长期信息")
console.print("/mode plan - 切换到 plan 模式") console.print("")
console.print("/build - 切换到 build 模式") console.print("模式命令")
console.print("/plan - 切换到 plan 模式") console.print(" /mode 查看当前模式")
console.print("/permissions - 查看当前权限状态") console.print(" /mode build 切换到 build 模式")
console.print("/permissions auto-on - 开启自动批准") console.print(" /mode plan 切换到 plan 模式")
console.print("/permissions auto-off - 关闭自动批准") console.print(" /build /mode build 的简写")
console.print(" /plan /mode plan 的简写")
console.print("")
console.print("权限命令")
console.print(" /permissions 查看当前权限状态")
console.print(" /permissions auto-on 开启自动批准")
console.print(" /permissions auto-off 关闭自动批准")
console.print("")
console.print("退出方式")
console.print(" exit / quit 退出 REPL")
return agent return agent
if name in {"/clear", "/new"}: if name in {"/clear", "/new"}:
session_meta = store.create_session(config.model) session_meta = store.create_session(config.model)
console.print(f"已创建新 session: {session_meta.get('session_id')}") console.print(f"已创建新会话: {session_meta.get('session_id')}")
return build_agent(root, config, store, permissions, session_meta, []) return build_agent(root, config, store, permissions, session_meta, [])
if name in {"/build", "/plan"}: if name in {"/build", "/plan"}:
target_mode = "build" if name == "/build" else "plan" target_mode = "build" if name == "/build" else "plan"
mode.set_mode(target_mode) mode.set_mode(target_mode)
permissions.set_mode(target_mode) permissions.set_mode(target_mode)
console.print(f"当前模式: {target_mode}") console.print(f"模式已切换为: {target_mode}")
return agent return agent
if name == "/mode": if name == "/mode":
if not args: if not args:
console.print(f"当前模式: {mode.mode}") console.print(f"模式状态\n 当前模式: {mode.mode}")
return agent return agent
if args in {"build", "plan"}: if args in {"build", "plan"}:
mode.set_mode(args) mode.set_mode(args)
permissions.set_mode(args) permissions.set_mode(args)
console.print(f"当前模式: {args}") console.print(f"模式已切换为: {args}")
return agent return agent
console.print("[red]error:[/red] 仅支持 /mode build 或 /mode plan") console.print("命令错误: 仅支持 /mode build 或 /mode plan")
return agent return agent
if name == "/history": if name == "/history":
@ -89,45 +98,52 @@ def handle_command(
if name == "/resume": if name == "/resume":
if not args: if not args:
console.print("[red]error:[/red] 缺少 resume 目标") console.print("命令错误: 缺少 resume 目标")
return agent return agent
session_id = store.resolve_session_id(args) session_id = store.resolve_session_id(args)
session_meta = store.load_meta(session_id) session_meta = store.load_meta(session_id)
restored_history = store.load_messages(session_id) restored_history = store.load_messages(session_id)
console.print(f"已恢复 session: {session_meta.get('session_id')}") console.print(f"已恢复会话: {session_meta.get('session_id')}")
return build_agent(root, config, store, permissions, session_meta, restored_history) return build_agent(root, config, store, permissions, session_meta, restored_history)
if name == "/memory": if name == "/memory":
content = memory.read() content = memory.read()
console.print(content or "当前项目还没有 memory。") console.print(content or "当前项目还没有已保存的 memory。")
return agent return agent
if name == "/permissions": if name == "/permissions":
if args == "auto-on": if args == "auto-on":
permissions.set_auto_approve(True) permissions.set_auto_approve(True)
console.print("已开启 auto_approve") console.print("权限状态\n auto_approve: 开启")
return agent return agent
if args == "auto-off": if args == "auto-off":
permissions.set_auto_approve(False) permissions.set_auto_approve(False)
console.print("已关闭 auto_approve") console.print("权限状态\n auto_approve: 关闭")
return agent return agent
status = permissions.status() status = permissions.status()
console.print(f"auto_approve: {status['auto_approve']}") auto_allowed_items = list(status["auto_allowed_tools"] if isinstance(status["auto_allowed_tools"], list) else [])
console.print(f"mode: {status['mode']}") confirm_required_items = list(
console.print(f"workspace: {status['workspace']}") status["confirm_required_tools"] if isinstance(status["confirm_required_tools"], list) else []
console.print(f"boundary: {status['boundary_policy']}") )
console.print(f"自动允许: {', '.join(status['auto_allowed_tools'])}") auto_allowed = ", ".join(str(item) for item in auto_allowed_items)
console.print(f"需要确认: {', '.join(status['confirm_required_tools'])}") confirm_required = ", ".join(str(item) for item in confirm_required_items)
console.print("权限状态")
console.print(f" auto_approve: {'开启' if status['auto_approve'] else '关闭'}")
console.print(f" mode: {status['mode']}")
console.print(f" workspace: {status['workspace']}")
console.print(f" boundary: {status['boundary_policy']}")
console.print(f" 自动允许: {auto_allowed}")
console.print(f" 需要确认: {confirm_required}")
return agent return agent
if name == "/remember": if name == "/remember":
if not args: if not args:
console.print("[red]error:[/red] 缺少需要保存的 memory 内容") console.print("命令错误: 缺少需要保存的 memory 内容")
return agent return agent
path = memory.append(args) path = memory.append(args)
console.print(f"已写入 memory: {path}") console.print(f"已写入 memory: {path}")
return agent return agent
console.print(f"[red]error:[/red] 不支持的命令: {name},输入 /help 查看帮助") console.print(f"命令错误: 不支持的命令 {name},输入 /help 查看帮助")
return agent return agent

View File

@ -289,9 +289,9 @@ class Agent:
def _check_tool_permission(self, name: str, payload: dict[str, Any]) -> str | None: def _check_tool_permission(self, name: str, payload: dict[str, Any]) -> str | None:
if not self.permission_checker: if not self.permission_checker:
return None return None
if not self.permission_checker.is_allowed(name, payload): if self.permission_checker.is_hard_blocked(name, payload):
return self.permission_checker.denial_reason(name, payload) return self.permission_checker.denial_reason(name, payload)
if self.permission_checker.requires_confirmation(name): if self.permission_checker.requires_confirmation(name, payload):
if not self.confirm_tool: if not self.confirm_tool:
return self.permission_checker.denial_reason(name, payload) return self.permission_checker.denial_reason(name, payload)
if not self.confirm_tool(name, payload): if not self.confirm_tool(name, payload):

View File

@ -33,9 +33,9 @@ def render_stream(agent: Agent, user_input: str) -> None:
elif event["type"] == "tool_result": elif event["type"] == "tool_result":
output = str(event.get("output", "")) output = str(event.get("output", ""))
if ( if (
output.startswith("Tool blocked in plan mode:") output.startswith("当前处于 plan 模式,禁止执行工具:")
or output.startswith("Permission denied for tool:") or output.startswith("当前未批准工具执行:")
or output.startswith("Bash command appears to target paths outside the workspace") or output.startswith("当前命令疑似指向工作区外路径,已拒绝执行 Bash")
): ):
console.print(f"[red]{output}[/red]") console.print(f"[red]{output}[/red]")
else: else:
@ -148,10 +148,10 @@ def run(
render_stream(agent, prompt) render_stream(agent, prompt)
return return
console.print("[bold cyan]cc-slim[/bold cyan] REPL输入 exit 或 quit 退出。") console.print("[bold cyan]cc-slim[/bold cyan] REPL输入 /help 查看命令,输入 exit 或 quit 退出。")
while True: while True:
try: try:
user_input = typer.prompt(">") user_input = typer.prompt(f"[{mode.mode}] >")
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
console.print() console.print()
break break

View File

@ -13,23 +13,28 @@ class PermissionChecker:
self.auto_allowed_tools = ["Read", "Glob", "Grep"] self.auto_allowed_tools = ["Read", "Glob", "Grep"]
self.confirm_required_tools = ["Write", "Edit", "Bash"] self.confirm_required_tools = ["Write", "Edit", "Bash"]
def is_allowed(self, tool_name: str, payload: dict[str, object] | None = None) -> bool: def is_hard_blocked(self, tool_name: str, payload: dict[str, object] | None = None) -> bool:
if self.mode == "plan" and tool_name in self.confirm_required_tools: if self.mode == "plan" and tool_name in self.confirm_required_tools:
return False return True
if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))): if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))):
return False return True
return True return False
def is_allowed(self, tool_name: str, payload: dict[str, object] | None = None) -> bool:
return not self.is_hard_blocked(tool_name, payload)
def denial_reason(self, tool_name: str, payload: dict[str, object] | None = None) -> str: def denial_reason(self, tool_name: str, payload: dict[str, object] | None = None) -> str:
if self.mode == "plan" and tool_name in self.confirm_required_tools: if self.mode == "plan" and tool_name in self.confirm_required_tools:
return f"Tool blocked in plan mode: {tool_name}" return f"当前处于 plan 模式,禁止执行工具: {tool_name}"
if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))): if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))):
return "Bash command appears to target paths outside the workspace" return "当前命令疑似指向工作区外路径,已拒绝执行 Bash"
return f"Permission denied for tool: {tool_name}" return f"当前未批准工具执行: {tool_name}"
def requires_confirmation(self, tool_name: str) -> bool: def requires_confirmation(self, tool_name: str, payload: dict[str, object] | None = None) -> bool:
if self.auto_approve: if self.auto_approve:
return False return False
if self.is_hard_blocked(tool_name, payload):
return False
return tool_name in self.confirm_required_tools return tool_name in self.confirm_required_tools
def set_auto_approve(self, enabled: bool) -> None: def set_auto_approve(self, enabled: bool) -> None: