From de0fad496cc7706fe6668986667afaa38946c2bf Mon Sep 17 00:00:00 2001 From: hc Date: Sat, 11 Apr 2026 23:16:45 +0800 Subject: [PATCH] add workspace --- AGENTS.md | 3 +++ SKILLS/cli-core.md | 1 + src/cc_slim/commands.py | 2 ++ src/cc_slim/engine.py | 10 ++++++---- src/cc_slim/main.py | 8 ++++++-- src/cc_slim/permissions.py | 28 +++++++++++++++++++++++++--- 6 files changed, 43 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4fe7aa8..3a36995 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,8 @@ 行动时必须以运行时注入的环境信息为准,特别是平台、shell、工作目录和可用工具列表。 +当前 workspace 是默认操作边界,不应主动读写工作区外文件,也不应主动探索无关路径。 + REPL 内的会话管理命令优先使用 slash command。 REPL 内所有 slash command 由程序直接处理,不进入 agent loop。 @@ -49,6 +51,7 @@ REPL 内所有 slash command 由程序直接处理,不进入 agent loop。 - `Read`:用于读取文件内容。 - 需要按文件名或路径模式查找时,优先使用 `Glob`。 - 需要搜索文件内容时,优先使用 `Grep`。 +- 默认只应在当前 workspace 内读写和执行与项目相关的操作。 - 修改已有文件内容时,优先使用 `Edit` 工具。 - 创建新文件时,优先使用 `Write` 工具。 - `Bash`:用于执行必须通过 shell 完成的最小命令。 diff --git a/SKILLS/cli-core.md b/SKILLS/cli-core.md index 6e28bcb..4627c90 100644 --- a/SKILLS/cli-core.md +++ b/SKILLS/cli-core.md @@ -27,6 +27,7 @@ - 长期有价值的项目约束或偏好,优先使用 `/remember` 保存 - 高风险操作会触发确认,优先先读再改,减少无意义审批 - 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行 +- 先在 workspace 内定位和操作,避免无关路径探索 - Windows 下先验证 shell 兼容性,再选择命令写法 ## 沟通风格 diff --git a/src/cc_slim/commands.py b/src/cc_slim/commands.py index b10b31f..9e22807 100644 --- a/src/cc_slim/commands.py +++ b/src/cc_slim/commands.py @@ -115,6 +115,8 @@ def handle_command( 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'])}") return agent diff --git a/src/cc_slim/engine.py b/src/cc_slim/engine.py index 213a5bc..70cc58e 100644 --- a/src/cc_slim/engine.py +++ b/src/cc_slim/engine.py @@ -184,8 +184,10 @@ class Agent: f"- sys.platform: {sys.platform}", f"- shell: {shell_name}", f"- workspace: {workspace}", + "- 默认边界: workspace only", f"- 可用工具: {tool_names}", "- 行动时必须以以上运行环境信息为准,不要默认套用 Unix/Linux 命令习惯。", + "- 默认只应在当前 workspace 内读写文件并执行与项目相关的操作,不要主动探索工作区外路径。", ] ) @@ -287,13 +289,13 @@ 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): - return self.permission_checker.denial_reason(name) + if not self.permission_checker.is_allowed(name, payload): + return self.permission_checker.denial_reason(name, payload) if self.permission_checker.requires_confirmation(name): if not self.confirm_tool: - return self.permission_checker.denial_reason(name) + return self.permission_checker.denial_reason(name, payload) if not self.confirm_tool(name, payload): - return self.permission_checker.denial_reason(name) + return self.permission_checker.denial_reason(name, payload) return None def _run_tool(self, name: str, payload: dict[str, Any]) -> str: diff --git a/src/cc_slim/main.py b/src/cc_slim/main.py index 0d3d273..40f42a4 100644 --- a/src/cc_slim/main.py +++ b/src/cc_slim/main.py @@ -32,7 +32,11 @@ def render_stream(agent: Agent, user_input: str) -> None: console.print(f"[cyan]->[/cyan] {event['name']}({event['input']})") 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:"): + 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") + ): console.print(f"[red]{output}[/red]") else: console.print(f"[green]✓[/green] {event['name']} done") @@ -112,7 +116,7 @@ def run( store = SessionStore(root) memory = MemoryStore(root) mode = ModeState() - permissions = PermissionChecker(auto_approve=auto_approve) + permissions = PermissionChecker(root, auto_approve=auto_approve) permissions.set_mode(mode.mode) if history: render_history(store) diff --git a/src/cc_slim/permissions.py b/src/cc_slim/permissions.py index dc00f85..dbfba90 100644 --- a/src/cc_slim/permissions.py +++ b/src/cc_slim/permissions.py @@ -1,21 +1,30 @@ from __future__ import annotations +import re +from pathlib import Path + class PermissionChecker: - def __init__(self, auto_approve: bool = False) -> None: + def __init__(self, workspace: Path, auto_approve: bool = False) -> None: + self.workspace = workspace.resolve() self.auto_approve = auto_approve self.mode = "build" + self.boundary_policy = "workspace only" self.auto_allowed_tools = ["Read", "Glob", "Grep"] self.confirm_required_tools = ["Write", "Edit", "Bash"] - def is_allowed(self, tool_name: str) -> bool: + def is_allowed(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 - def denial_reason(self, tool_name: str) -> 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: return f"Tool blocked in plan mode: {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}" def requires_confirmation(self, tool_name: str) -> bool: @@ -34,6 +43,19 @@ class PermissionChecker: return { "auto_approve": self.auto_approve, "mode": self.mode, + "workspace": str(self.workspace), + "boundary_policy": self.boundary_policy, "auto_allowed_tools": list(self.auto_allowed_tools), "confirm_required_tools": list(self.confirm_required_tools), } + + def _bash_targets_outside_workspace(self, command: str) -> bool: + workspace_text = str(self.workspace).lower() + windows_paths = re.findall(r"[A-Za-z]:\\[^\s\"']+", command) + unix_paths = re.findall(r"(?