Compare commits
No commits in common. "de0fad496cc7706fe6668986667afaa38946c2bf" and "e9cf2e3533a120da44355c0f7512a09e14bd67d0" have entirely different histories.
de0fad496c
...
e9cf2e3533
15
AGENTS.md
15
AGENTS.md
@ -15,9 +15,9 @@
|
|||||||
|
|
||||||
当用户请求处理仓库内任务时,按以下最小闭环执行:
|
当用户请求处理仓库内任务时,按以下最小闭环执行:
|
||||||
|
|
||||||
1. 先判断运行时状态、已有上下文和当前用户输入是否已经足以决定下一步。
|
1. 先检查最相关的文件。
|
||||||
2. 只有在减少不确定性或执行动作确有必要时,才检查最相关的文件或调用工具。
|
2. 选择最小的下一步动作。
|
||||||
3. 选择最小的下一步动作。
|
3. 只有在减少不确定性或执行动作确有必要时才调用工具。
|
||||||
4. 使用工具后重新判断结果。
|
4. 使用工具后重新判断结果。
|
||||||
5. 持续循环,直到得到最终答案或出现必须由用户补充的信息。
|
5. 持续循环,直到得到最终答案或出现必须由用户补充的信息。
|
||||||
|
|
||||||
@ -25,18 +25,12 @@
|
|||||||
|
|
||||||
行动时必须以运行时注入的环境信息为准,特别是平台、shell、工作目录和可用工具列表。
|
行动时必须以运行时注入的环境信息为准,特别是平台、shell、工作目录和可用工具列表。
|
||||||
|
|
||||||
当前 workspace 是默认操作边界,不应主动读写工作区外文件,也不应主动探索无关路径。
|
|
||||||
|
|
||||||
REPL 内的会话管理命令优先使用 slash command。
|
REPL 内的会话管理命令优先使用 slash command。
|
||||||
|
|
||||||
REPL 内所有 slash command 由程序直接处理,不进入 agent loop。
|
`/history`、`/resume`、`/new` 由程序直接处理,不进入 agent loop。
|
||||||
|
|
||||||
当前项目支持最小 memory:使用 `/remember` 保存长期信息,使用 `/memory` 查看。
|
当前项目支持最小 memory:使用 `/remember` 保存长期信息,使用 `/memory` 查看。
|
||||||
|
|
||||||
写操作和 Bash 默认需要确认,可通过 `--auto-approve` 或 `/permissions auto-on` 跳过。
|
|
||||||
|
|
||||||
支持 `/mode build` 和 `/mode plan` 两种模式,`plan` 为只读规划模式。
|
|
||||||
|
|
||||||
未明确说明时,使用以下默认值:
|
未明确说明时,使用以下默认值:
|
||||||
|
|
||||||
- 工作目录:当前进程目录
|
- 工作目录:当前进程目录
|
||||||
@ -51,7 +45,6 @@ REPL 内所有 slash command 由程序直接处理,不进入 agent loop。
|
|||||||
- `Read`:用于读取文件内容。
|
- `Read`:用于读取文件内容。
|
||||||
- 需要按文件名或路径模式查找时,优先使用 `Glob`。
|
- 需要按文件名或路径模式查找时,优先使用 `Glob`。
|
||||||
- 需要搜索文件内容时,优先使用 `Grep`。
|
- 需要搜索文件内容时,优先使用 `Grep`。
|
||||||
- 默认只应在当前 workspace 内读写和执行与项目相关的操作。
|
|
||||||
- 修改已有文件内容时,优先使用 `Edit` 工具。
|
- 修改已有文件内容时,优先使用 `Edit` 工具。
|
||||||
- 创建新文件时,优先使用 `Write` 工具。
|
- 创建新文件时,优先使用 `Write` 工具。
|
||||||
- `Bash`:用于执行必须通过 shell 完成的最小命令。
|
- `Bash`:用于执行必须通过 shell 完成的最小命令。
|
||||||
|
|||||||
@ -23,11 +23,8 @@
|
|||||||
- 搜索文件内容时,优先使用 `Grep`,而不是 `Bash`
|
- 搜索文件内容时,优先使用 `Grep`,而不是 `Bash`
|
||||||
- 修改已有文件时,优先使用 `Edit`,而不是 `Bash` 或 `Write`
|
- 修改已有文件时,优先使用 `Edit`,而不是 `Bash` 或 `Write`
|
||||||
- 需要创建或覆盖文件时,优先使用 `Write` 工具,而不是 `Bash`
|
- 需要创建或覆盖文件时,优先使用 `Write` 工具,而不是 `Bash`
|
||||||
- 会话与 memory 管理优先使用 slash command,而不是自然语言或 `Bash` 探查对应文件
|
- 会话管理优先使用 slash command,而不是自然语言或 `Bash` 探查 session 文件
|
||||||
- 长期有价值的项目约束或偏好,优先使用 `/remember` 保存
|
- 长期有价值的项目约束或偏好,优先使用 `/remember` 保存
|
||||||
- 高风险操作会触发确认,优先先读再改,减少无意义审批
|
|
||||||
- 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行
|
|
||||||
- 先在 workspace 内定位和操作,避免无关路径探索
|
|
||||||
- Windows 下先验证 shell 兼容性,再选择命令写法
|
- Windows 下先验证 shell 兼容性,再选择命令写法
|
||||||
|
|
||||||
## 沟通风格
|
## 沟通风格
|
||||||
|
|||||||
@ -1,133 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from rich.console import Console
|
|
||||||
|
|
||||||
from cc_slim.engine import Agent
|
|
||||||
from cc_slim.memory import MemoryStore
|
|
||||||
from cc_slim.mode import ModeState
|
|
||||||
from cc_slim.permissions import PermissionChecker
|
|
||||||
from cc_slim.session import SessionStore
|
|
||||||
|
|
||||||
|
|
||||||
def parse_command(text: str) -> dict[str, str] | None:
|
|
||||||
command = text.strip()
|
|
||||||
if not command.startswith("/"):
|
|
||||||
return None
|
|
||||||
name, _, args = command.partition(" ")
|
|
||||||
return {"name": name, "args": args.strip()}
|
|
||||||
|
|
||||||
|
|
||||||
def handle_command(
|
|
||||||
text: str,
|
|
||||||
*,
|
|
||||||
console: Console,
|
|
||||||
root: Path,
|
|
||||||
config: Any,
|
|
||||||
store: SessionStore,
|
|
||||||
memory: MemoryStore,
|
|
||||||
mode: ModeState,
|
|
||||||
permissions: PermissionChecker,
|
|
||||||
agent: Agent,
|
|
||||||
build_agent: Any,
|
|
||||||
render_history: Any,
|
|
||||||
) -> Agent:
|
|
||||||
parsed = parse_command(text)
|
|
||||||
if not parsed:
|
|
||||||
return agent
|
|
||||||
|
|
||||||
name = parsed["name"]
|
|
||||||
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 - 关闭自动批准")
|
|
||||||
return agent
|
|
||||||
|
|
||||||
if name in {"/clear", "/new"}:
|
|
||||||
session_meta = store.create_session(config.model)
|
|
||||||
console.print(f"已创建新 session: {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}")
|
|
||||||
return agent
|
|
||||||
|
|
||||||
if name == "/mode":
|
|
||||||
if not args:
|
|
||||||
console.print(f"当前模式: {mode.mode}")
|
|
||||||
return agent
|
|
||||||
if args in {"build", "plan"}:
|
|
||||||
mode.set_mode(args)
|
|
||||||
permissions.set_mode(args)
|
|
||||||
console.print(f"当前模式: {args}")
|
|
||||||
return agent
|
|
||||||
console.print("[red]error:[/red] 仅支持 /mode build 或 /mode plan")
|
|
||||||
return agent
|
|
||||||
|
|
||||||
if name == "/history":
|
|
||||||
render_history(store)
|
|
||||||
return agent
|
|
||||||
|
|
||||||
if name == "/resume":
|
|
||||||
if not args:
|
|
||||||
console.print("[red]error:[/red] 缺少 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')}")
|
|
||||||
return build_agent(root, config, store, permissions, session_meta, restored_history)
|
|
||||||
|
|
||||||
if name == "/memory":
|
|
||||||
content = memory.read()
|
|
||||||
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"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
|
|
||||||
|
|
||||||
if name == "/remember":
|
|
||||||
if not args:
|
|
||||||
console.print("[red]error:[/red] 缺少需要保存的 memory 内容")
|
|
||||||
return agent
|
|
||||||
path = memory.append(args)
|
|
||||||
console.print(f"已写入 memory: {path}")
|
|
||||||
return agent
|
|
||||||
|
|
||||||
console.print(f"[red]error:[/red] 不支持的命令: {name},输入 /help 查看帮助")
|
|
||||||
return agent
|
|
||||||
@ -14,7 +14,6 @@ from anthropic import Anthropic
|
|||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
|
|
||||||
from cc_slim.memory import MemoryStore
|
from cc_slim.memory import MemoryStore
|
||||||
from cc_slim.permissions import PermissionChecker
|
|
||||||
from cc_slim.session import SessionStore
|
from cc_slim.session import SessionStore
|
||||||
from cc_slim.tools import Tool
|
from cc_slim.tools import Tool
|
||||||
|
|
||||||
@ -68,16 +67,12 @@ class Agent:
|
|||||||
session_store: SessionStore | None = None,
|
session_store: SessionStore | None = None,
|
||||||
session_id: str | None = None,
|
session_id: str | None = None,
|
||||||
history: list[dict[str, Any]] | None = None,
|
history: list[dict[str, Any]] | None = None,
|
||||||
permission_checker: PermissionChecker | None = None,
|
|
||||||
confirm_tool: Any | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.tools = {tool.name: tool for tool in tools}
|
self.tools = {tool.name: tool for tool in tools}
|
||||||
self.history: list[dict[str, Any]] = list(history or [])
|
self.history: list[dict[str, Any]] = list(history or [])
|
||||||
self.session_store = session_store
|
self.session_store = session_store
|
||||||
self.session_id = session_id
|
self.session_id = session_id
|
||||||
self.permission_checker = permission_checker
|
|
||||||
self.confirm_tool = confirm_tool
|
|
||||||
self.system_prompt = self._build_system_prompt(workspace)
|
self.system_prompt = self._build_system_prompt(workspace)
|
||||||
self.client = self._build_client()
|
self.client = self._build_client()
|
||||||
|
|
||||||
@ -113,20 +108,6 @@ class Agent:
|
|||||||
return
|
return
|
||||||
|
|
||||||
for call in result["tool_calls"]:
|
for call in result["tool_calls"]:
|
||||||
denied_output = self._check_tool_permission(call["name"], call["input"])
|
|
||||||
if denied_output is not None:
|
|
||||||
yield {"type": "tool_result", "name": call["name"], "output": denied_output}
|
|
||||||
self.history.append(
|
|
||||||
{
|
|
||||||
"role": "tool",
|
|
||||||
"tool_call_id": call["id"],
|
|
||||||
"name": call["name"],
|
|
||||||
"content": denied_output,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self._save_message(self.history[-1])
|
|
||||||
continue
|
|
||||||
|
|
||||||
yield {"type": "tool_call", "name": call["name"], "input": call["input"]}
|
yield {"type": "tool_call", "name": call["name"], "input": call["input"]}
|
||||||
tool_output = self._run_tool(call["name"], call["input"])
|
tool_output = self._run_tool(call["name"], call["input"])
|
||||||
yield {"type": "tool_result", "name": call["name"], "output": tool_output}
|
yield {"type": "tool_result", "name": call["name"], "output": tool_output}
|
||||||
@ -184,10 +165,8 @@ 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 内读写文件并执行与项目相关的操作,不要主动探索工作区外路径。",
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -286,18 +265,6 @@ class Agent:
|
|||||||
yield {"type": "text", "content": result["text"]}
|
yield {"type": "text", "content": result["text"]}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
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):
|
|
||||||
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, payload)
|
|
||||||
if not self.confirm_tool(name, payload):
|
|
||||||
return self.permission_checker.denial_reason(name, payload)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _run_tool(self, name: str, payload: dict[str, Any]) -> str:
|
def _run_tool(self, name: str, payload: dict[str, Any]) -> str:
|
||||||
tool = self.tools.get(name)
|
tool = self.tools.get(name)
|
||||||
if not tool:
|
if not tool:
|
||||||
|
|||||||
@ -7,11 +7,8 @@ import typer
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
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.engine import Agent, resolve_config
|
||||||
from cc_slim.memory import MemoryStore
|
from cc_slim.memory import MemoryStore
|
||||||
from cc_slim.mode import ModeState
|
|
||||||
from cc_slim.permissions import PermissionChecker
|
|
||||||
from cc_slim.session import SessionStore
|
from cc_slim.session import SessionStore
|
||||||
from cc_slim.tools import build_default_tools
|
from cc_slim.tools import build_default_tools
|
||||||
|
|
||||||
@ -31,14 +28,6 @@ def render_stream(agent: Agent, user_input: str) -> None:
|
|||||||
printed_text = False
|
printed_text = False
|
||||||
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", ""))
|
|
||||||
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")
|
console.print(f"[green]✓[/green] {event['name']} done")
|
||||||
elif event["type"] == "error":
|
elif event["type"] == "error":
|
||||||
if printed_text:
|
if printed_text:
|
||||||
@ -77,7 +66,6 @@ def build_agent(
|
|||||||
root: Path,
|
root: Path,
|
||||||
config: object,
|
config: object,
|
||||||
store: SessionStore,
|
store: SessionStore,
|
||||||
permissions: PermissionChecker,
|
|
||||||
session_meta: dict[str, object],
|
session_meta: dict[str, object],
|
||||||
restored_history: list[dict[str, object]] | None = None,
|
restored_history: list[dict[str, object]] | None = None,
|
||||||
) -> Agent:
|
) -> Agent:
|
||||||
@ -88,15 +76,53 @@ def build_agent(
|
|||||||
session_store=store,
|
session_store=store,
|
||||||
session_id=str(session_meta["session_id"]),
|
session_id=str(session_meta["session_id"]),
|
||||||
history=restored_history or [],
|
history=restored_history or [],
|
||||||
permission_checker=permissions,
|
|
||||||
confirm_tool=confirm_tool,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def confirm_tool(name: str, payload: dict[str, object]) -> bool:
|
def handle_repl_command(
|
||||||
console.print(f"即将执行 [{name}] {payload},是否允许? [y/N]", markup=False, end=" ")
|
user_input: str,
|
||||||
answer = input()
|
store: SessionStore,
|
||||||
return answer.strip().lower() in {"y", "yes"}
|
memory: MemoryStore,
|
||||||
|
config: object,
|
||||||
|
root: Path,
|
||||||
|
agent: Agent,
|
||||||
|
) -> Agent:
|
||||||
|
command = user_input.strip()
|
||||||
|
if command in {"--history", "/history"}:
|
||||||
|
render_history(store)
|
||||||
|
return agent
|
||||||
|
|
||||||
|
if command == "/new":
|
||||||
|
session_meta = store.create_session(config.model)
|
||||||
|
console.print(f"已创建新 session: {session_meta.get('session_id')}")
|
||||||
|
return build_agent(root, config, store, session_meta, [])
|
||||||
|
|
||||||
|
if command == "/memory":
|
||||||
|
content = memory.read()
|
||||||
|
console.print(content or "当前项目还没有 memory。")
|
||||||
|
return agent
|
||||||
|
|
||||||
|
if command.startswith("/remember "):
|
||||||
|
text = user_input.split(" ", 1)[1].strip()
|
||||||
|
if not text:
|
||||||
|
console.print("[red]error:[/red] 缺少需要保存的 memory 内容")
|
||||||
|
return agent
|
||||||
|
path = memory.append(text)
|
||||||
|
console.print(f"已写入 memory: {path}")
|
||||||
|
return agent
|
||||||
|
|
||||||
|
if command.startswith("--resume ") or command.startswith("/resume "):
|
||||||
|
target = command.split(maxsplit=1)[1].strip()
|
||||||
|
if not target:
|
||||||
|
console.print("[red]error:[/red] 缺少 resume 目标")
|
||||||
|
return agent
|
||||||
|
session_id = store.resolve_session_id(target)
|
||||||
|
session_meta = store.load_meta(session_id)
|
||||||
|
restored_history = store.load_messages(session_id)
|
||||||
|
console.print(f"已恢复 session: {session_meta.get('session_id')}")
|
||||||
|
return build_agent(root, config, store, session_meta, restored_history)
|
||||||
|
|
||||||
|
return agent
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
@ -110,14 +136,10 @@ def run(
|
|||||||
max_turns: Optional[int] = typer.Option(None, help="最大工具循环轮数"),
|
max_turns: Optional[int] = typer.Option(None, help="最大工具循环轮数"),
|
||||||
history: bool = typer.Option(False, "--history", help="列出当前工作目录的历史 session"),
|
history: bool = typer.Option(False, "--history", help="列出当前工作目录的历史 session"),
|
||||||
resume: Optional[str] = typer.Option(None, "--resume", help="按 session id 或序号恢复历史 session"),
|
resume: Optional[str] = typer.Option(None, "--resume", help="按 session id 或序号恢复历史 session"),
|
||||||
auto_approve: bool = typer.Option(False, "--auto-approve", help="跳过高风险工具确认"),
|
|
||||||
) -> None:
|
) -> None:
|
||||||
root = cwd.resolve()
|
root = cwd.resolve()
|
||||||
store = SessionStore(root)
|
store = SessionStore(root)
|
||||||
memory = MemoryStore(root)
|
memory = MemoryStore(root)
|
||||||
mode = ModeState()
|
|
||||||
permissions = PermissionChecker(root, auto_approve=auto_approve)
|
|
||||||
permissions.set_mode(mode.mode)
|
|
||||||
if history:
|
if history:
|
||||||
render_history(store)
|
render_history(store)
|
||||||
return
|
return
|
||||||
@ -142,7 +164,7 @@ def run(
|
|||||||
else:
|
else:
|
||||||
session_meta = store.create_session(config.model)
|
session_meta = store.create_session(config.model)
|
||||||
|
|
||||||
agent = build_agent(root, config, store, permissions, session_meta, restored_history)
|
agent = build_agent(root, config, store, session_meta, restored_history)
|
||||||
|
|
||||||
if prompt:
|
if prompt:
|
||||||
render_stream(agent, prompt)
|
render_stream(agent, prompt)
|
||||||
@ -161,21 +183,9 @@ def run(
|
|||||||
if not user_input.strip():
|
if not user_input.strip():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if parse_command(user_input):
|
if user_input.strip().startswith("/") or user_input.strip().startswith("--history") or user_input.strip().startswith("--resume"):
|
||||||
try:
|
try:
|
||||||
agent = handle_command(
|
agent = handle_repl_command(user_input, store, memory, config, root, agent)
|
||||||
user_input,
|
|
||||||
console=console,
|
|
||||||
root=root,
|
|
||||||
config=config,
|
|
||||||
store=store,
|
|
||||||
memory=memory,
|
|
||||||
mode=mode,
|
|
||||||
permissions=permissions,
|
|
||||||
agent=agent,
|
|
||||||
build_agent=build_agent,
|
|
||||||
render_history=render_history,
|
|
||||||
)
|
|
||||||
except Exception as exc: # pragma: no cover
|
except Exception as exc: # pragma: no cover
|
||||||
console.print(f"[red]error:[/red] {exc}")
|
console.print(f"[red]error:[/red] {exc}")
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
|
|
||||||
class ModeState:
|
|
||||||
def __init__(self, mode: str = "build") -> None:
|
|
||||||
self.mode = mode if mode in {"build", "plan"} else "build"
|
|
||||||
|
|
||||||
def set_mode(self, mode: str) -> None:
|
|
||||||
if mode in {"build", "plan"}:
|
|
||||||
self.mode = mode
|
|
||||||
|
|
||||||
def is_plan(self) -> bool:
|
|
||||||
return self.mode == "plan"
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
class PermissionChecker:
|
|
||||||
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, 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, 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:
|
|
||||||
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 set_mode(self, mode: str) -> None:
|
|
||||||
if mode in {"build", "plan"}:
|
|
||||||
self.mode = mode
|
|
||||||
|
|
||||||
def status(self) -> dict[str, object]:
|
|
||||||
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"(?<![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