add workspace
This commit is contained in:
parent
84ef3d2f9a
commit
de0fad496c
@ -25,6 +25,8 @@
|
|||||||
|
|
||||||
行动时必须以运行时注入的环境信息为准,特别是平台、shell、工作目录和可用工具列表。
|
行动时必须以运行时注入的环境信息为准,特别是平台、shell、工作目录和可用工具列表。
|
||||||
|
|
||||||
|
当前 workspace 是默认操作边界,不应主动读写工作区外文件,也不应主动探索无关路径。
|
||||||
|
|
||||||
REPL 内的会话管理命令优先使用 slash command。
|
REPL 内的会话管理命令优先使用 slash command。
|
||||||
|
|
||||||
REPL 内所有 slash command 由程序直接处理,不进入 agent loop。
|
REPL 内所有 slash command 由程序直接处理,不进入 agent loop。
|
||||||
@ -49,6 +51,7 @@ REPL 内所有 slash command 由程序直接处理,不进入 agent loop。
|
|||||||
- `Read`:用于读取文件内容。
|
- `Read`:用于读取文件内容。
|
||||||
- 需要按文件名或路径模式查找时,优先使用 `Glob`。
|
- 需要按文件名或路径模式查找时,优先使用 `Glob`。
|
||||||
- 需要搜索文件内容时,优先使用 `Grep`。
|
- 需要搜索文件内容时,优先使用 `Grep`。
|
||||||
|
- 默认只应在当前 workspace 内读写和执行与项目相关的操作。
|
||||||
- 修改已有文件内容时,优先使用 `Edit` 工具。
|
- 修改已有文件内容时,优先使用 `Edit` 工具。
|
||||||
- 创建新文件时,优先使用 `Write` 工具。
|
- 创建新文件时,优先使用 `Write` 工具。
|
||||||
- `Bash`:用于执行必须通过 shell 完成的最小命令。
|
- `Bash`:用于执行必须通过 shell 完成的最小命令。
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
- 长期有价值的项目约束或偏好,优先使用 `/remember` 保存
|
- 长期有价值的项目约束或偏好,优先使用 `/remember` 保存
|
||||||
- 高风险操作会触发确认,优先先读再改,减少无意义审批
|
- 高风险操作会触发确认,优先先读再改,减少无意义审批
|
||||||
- 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行
|
- 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行
|
||||||
|
- 先在 workspace 内定位和操作,避免无关路径探索
|
||||||
- Windows 下先验证 shell 兼容性,再选择命令写法
|
- Windows 下先验证 shell 兼容性,再选择命令写法
|
||||||
|
|
||||||
## 沟通风格
|
## 沟通风格
|
||||||
|
|||||||
@ -115,6 +115,8 @@ def handle_command(
|
|||||||
status = permissions.status()
|
status = permissions.status()
|
||||||
console.print(f"auto_approve: {status['auto_approve']}")
|
console.print(f"auto_approve: {status['auto_approve']}")
|
||||||
console.print(f"mode: {status['mode']}")
|
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['auto_allowed_tools'])}")
|
||||||
console.print(f"需要确认: {', '.join(status['confirm_required_tools'])}")
|
console.print(f"需要确认: {', '.join(status['confirm_required_tools'])}")
|
||||||
return agent
|
return agent
|
||||||
|
|||||||
@ -184,8 +184,10 @@ class Agent:
|
|||||||
f"- sys.platform: {sys.platform}",
|
f"- sys.platform: {sys.platform}",
|
||||||
f"- shell: {shell_name}",
|
f"- shell: {shell_name}",
|
||||||
f"- workspace: {workspace}",
|
f"- workspace: {workspace}",
|
||||||
|
"- 默认边界: workspace only",
|
||||||
f"- 可用工具: {tool_names}",
|
f"- 可用工具: {tool_names}",
|
||||||
"- 行动时必须以以上运行环境信息为准,不要默认套用 Unix/Linux 命令习惯。",
|
"- 行动时必须以以上运行环境信息为准,不要默认套用 Unix/Linux 命令习惯。",
|
||||||
|
"- 默认只应在当前 workspace 内读写文件并执行与项目相关的操作,不要主动探索工作区外路径。",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -287,13 +289,13 @@ 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):
|
if not self.permission_checker.is_allowed(name, payload):
|
||||||
return self.permission_checker.denial_reason(name)
|
return self.permission_checker.denial_reason(name, payload)
|
||||||
if self.permission_checker.requires_confirmation(name):
|
if self.permission_checker.requires_confirmation(name):
|
||||||
if not self.confirm_tool:
|
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):
|
if not self.confirm_tool(name, payload):
|
||||||
return self.permission_checker.denial_reason(name)
|
return self.permission_checker.denial_reason(name, payload)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _run_tool(self, name: str, payload: dict[str, Any]) -> str:
|
def _run_tool(self, name: str, payload: dict[str, Any]) -> str:
|
||||||
|
|||||||
@ -32,7 +32,11 @@ def render_stream(agent: Agent, user_input: str) -> None:
|
|||||||
console.print(f"[cyan]->[/cyan] {event['name']}({event['input']})")
|
console.print(f"[cyan]->[/cyan] {event['name']}({event['input']})")
|
||||||
elif event["type"] == "tool_result":
|
elif event["type"] == "tool_result":
|
||||||
output = str(event.get("output", ""))
|
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]")
|
console.print(f"[red]{output}[/red]")
|
||||||
else:
|
else:
|
||||||
console.print(f"[green]✓[/green] {event['name']} done")
|
console.print(f"[green]✓[/green] {event['name']} done")
|
||||||
@ -112,7 +116,7 @@ def run(
|
|||||||
store = SessionStore(root)
|
store = SessionStore(root)
|
||||||
memory = MemoryStore(root)
|
memory = MemoryStore(root)
|
||||||
mode = ModeState()
|
mode = ModeState()
|
||||||
permissions = PermissionChecker(auto_approve=auto_approve)
|
permissions = PermissionChecker(root, auto_approve=auto_approve)
|
||||||
permissions.set_mode(mode.mode)
|
permissions.set_mode(mode.mode)
|
||||||
if history:
|
if history:
|
||||||
render_history(store)
|
render_history(store)
|
||||||
|
|||||||
@ -1,21 +1,30 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
class PermissionChecker:
|
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.auto_approve = auto_approve
|
||||||
self.mode = "build"
|
self.mode = "build"
|
||||||
|
self.boundary_policy = "workspace only"
|
||||||
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) -> 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:
|
if self.mode == "plan" and tool_name in self.confirm_required_tools:
|
||||||
return False
|
return False
|
||||||
|
if tool_name == "Bash" and payload and self._bash_targets_outside_workspace(str(payload.get("command", ""))):
|
||||||
|
return False
|
||||||
return True
|
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:
|
if self.mode == "plan" and tool_name in self.confirm_required_tools:
|
||||||
return f"Tool blocked in plan mode: {tool_name}"
|
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}"
|
return f"Permission denied for tool: {tool_name}"
|
||||||
|
|
||||||
def requires_confirmation(self, tool_name: str) -> bool:
|
def requires_confirmation(self, tool_name: str) -> bool:
|
||||||
@ -34,6 +43,19 @@ class PermissionChecker:
|
|||||||
return {
|
return {
|
||||||
"auto_approve": self.auto_approve,
|
"auto_approve": self.auto_approve,
|
||||||
"mode": self.mode,
|
"mode": self.mode,
|
||||||
|
"workspace": str(self.workspace),
|
||||||
|
"boundary_policy": self.boundary_policy,
|
||||||
"auto_allowed_tools": list(self.auto_allowed_tools),
|
"auto_allowed_tools": list(self.auto_allowed_tools),
|
||||||
"confirm_required_tools": list(self.confirm_required_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"(?<![A-Za-z]):?(/[^^\s\"']+)", command)
|
||||||
|
candidates = windows_paths + unix_paths
|
||||||
|
for candidate in candidates:
|
||||||
|
normalized = str(Path(candidate).resolve()).lower()
|
||||||
|
if workspace_text not in normalized:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user