权限和模式统一

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 会显示当前模式提示符,例如 `build >``plan >`
当前项目支持最小 memory使用 `/remember` 保存长期信息,使用 `/memory` 查看。
写操作和 Bash 默认需要确认,可通过 `--auto-approve``/permissions auto-on` 跳过。
工作区外访问和 `plan` 模式限制属于硬性边界,不通过审批放行。
支持 `/mode build``/mode plan` 两种模式,`plan` 为只读规划模式。
`/clear` 是清空当前上下文的主别名,等价于 `/new`
未明确说明时,使用以下默认值:
- 工作目录:当前进程目录

View File

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

View File

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

View File

@ -289,9 +289,9 @@ class Agent:
def _check_tool_permission(self, name: str, payload: dict[str, Any]) -> str | None:
if not self.permission_checker:
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)
if self.permission_checker.requires_confirmation(name):
if self.permission_checker.requires_confirmation(name, payload):
if not self.confirm_tool:
return self.permission_checker.denial_reason(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":
output = str(event.get("output", ""))
if (
output.startswith("Tool blocked in plan mode:")
or output.startswith("Permission denied for tool:")
or output.startswith("Bash command appears to target paths outside the workspace")
output.startswith("当前处于 plan 模式,禁止执行工具:")
or output.startswith("当前未批准工具执行:")
or output.startswith("当前命令疑似指向工作区外路径,已拒绝执行 Bash")
):
console.print(f"[red]{output}[/red]")
else:
@ -148,10 +148,10 @@ def run(
render_stream(agent, prompt)
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:
try:
user_input = typer.prompt(">")
user_input = typer.prompt(f"[{mode.mode}] >")
except (EOFError, KeyboardInterrupt):
console.print()
break

View File

@ -13,23 +13,28 @@ class PermissionChecker:
self.auto_allowed_tools = ["Read", "Glob", "Grep"]
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:
return False
if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))):
return False
return True
if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))):
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:
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", ""))):
return "Bash command appears to target paths outside the workspace"
return f"Permission denied for tool: {tool_name}"
return "当前命令疑似指向工作区外路径,已拒绝执行 Bash"
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:
return False
if self.is_hard_blocked(tool_name, payload):
return False
return tool_name in self.confirm_required_tools
def set_auto_approve(self, enabled: bool) -> None: