跳到正文

审批门

你在模块1中建立的允许列表只有一种模式:屏蔽列表中所有不在列表中的内容。这对演示来说没问题。这对真正的Harness来说不合适。

线人没有人可以问。子 Agent需要继承父 Agent的部分信任,而不是全部的授权清单。而本地Agent负责人可能只想审批一次npm install express,三步后又被问一次。

同一个门。三种不同的操作模式。我们将通过将配置本身的形状从布尔数演进到函数,再到可辨识联合类型来实现。

学习成果

createBashTool接受三种模式的 ApprovalConfig 可辨识联合类型:interactivebackgrounddelegated。每种模式在needsApproval返回为真时都会形成形状。

快速路径

  1. 定义ApprovalConfig为具有三种模态的可辨识联合类型
  2. 写入返回needsApproval函数的createApproval(config)
  3. 将结果传递给createBashTool,验证每种模式的表现不同

动手练习 2.3

用可配置的审批系统取代静态的安全前缀检查。

要求:

  1. 定义ApprovalConfig有三个变体:{ mode: "interactive" }{ mode: "background" }{ mode: "delegated"; trust: string[] }
  2. 写入返回(input) => booleancreateApproval(config: ApprovalConfig)
  3. 更新createBashTool以接受审批函数作为参数
  4. 用同一个命令测试每个模式,并验证门的行为变化

实现提示:

  • background所有事情都会false(审批)退货。这是针对CI和自动运行的
  • delegated会对照输入config.trust并只审批匹配
  • interactive会将输入与保险前缀列表对照,并审批安全前缀列表。其他任何事都需要人类的审批
  • 函数返回时true需要审批时,false命令可以运行时返回

第一阶段:布尔值

最简单的审批门是布尔值:

ts
needsApproval: true

这会阻挡所有调用。lspwdrm -rf,全部都没了。虽然没用,但能定下形状。needsApproval问题是“我们是否应该在运行前暂停等待人类的认可?”

第二阶段:功能

函数可以让你根据输入回答这个问题:

ts
needsApproval: ({ command }) => {
  if (SAFE_PREFIXES.some(p => command.startsWith(p))) return false;
  return true;
}

好多了。ls跑。rm -rf块。但函数只知道一条内置规则。CI和本地终端有相同的登机口。子 Agent和它的父 Agent一样拥有相同的门。你无法在不重写函数的情况下重新配置。

第三阶段:可辨识联合类型

配置里承载了模式。工厂根据以下模式构建了该功能:

ts
type ApprovalConfig =
  | { mode: "interactive" }
  | { mode: "background" }
  | { mode: "delegated"; trust: string[] };

function createApproval(config: ApprovalConfig) {
  return ({ command }: { command: string }) => {
    if (config.mode === "background") return false;

    if (config.mode === "delegated") {
      return !config.trust.some((p) => command.trim().startsWith(p));
    }

    return !SAFE_PREFIXES.some((p) => command.trim().startsWith(p));
  };
}

三种模式,一个功能。可辨识联合类型让模式变成类型化且排他。TypeScript将config.trust缩小到只string[]``delegated分支内部,而这正是布尔和函数无法捕捉的错误。

现在createBashTool采用了审批函数,而不是安全前缀列表:

ts
function createBashTool(
  operations: BashOperations,
  needsApproval: (input: { command: string }) => boolean,
) {
  return tool({
    // ... same description and schema
    execute: async ({ command }) => {
      if (needsApproval({ command })) {
        return `Blocked: "${command}" requires approval.`;
      }
      const { stdout } = await operations.exec(command);
      return stdout || "(no output)";
    },
  });
}

调用网站上的三种模式是这样的:

ts
// Interactive: human approves anything not on the safe list
const bash = createBashTool(localOps, createApproval({ mode: "interactive" }));

// Background: auto-approve everything (CI, automation)
const bash = createBashTool(localOps, createApproval({ mode: "background" }));

// Delegated: subagent inherits a trust slice from its parent
const bash = createBashTool(
  localOps,
  createApproval({ mode: "delegated", trust: ["pwd", "find .", "git status"] }),
);

**注意:为什么是可辨识联合类型而不是三个函数**

你完全可以写三个独立的函数:interactiveApprovalbackgroundApprovaldelegatedApproval。可辨识联合类型之所以胜出,是因为配置是数据,而不是代码。你可以从AGENTS.md加载,用Zod(z.discriminatedUnion("mode", [...]))验证,跨子 Agent边界序列化,允许用户在不触碰Harness代码的情况下切换模式。

这会解锁什么

background是显而易见的。Agent在CI里运行,没有人可以问,你信任提示词,愿意放手。

delegated才是有趣的。当子 Agent启动时,你不会给它完整的保险前缀列表。你把它需要的特定命令交给它。只读探索器的表现pwdfindgit status。一个执行器进行测试,结果是有点npm testnpm run build。父 Agent以命令决定将哪些信托委托。

interactive你已经在做的,只是现在用配置来表达。

动手试试

试试每个模式,用一个应该阻挡的命令和一个应该通过的命令:

bash
bun run index.ts . "Run: git status"

interactive模式下,这个问题会通过(它在安全名单上)。在background模式下,什么都过去了。在trust: ["git status"]``delegated模式下,它会通过。

bash
bun run index.ts . "Run: npm install express"

interactive模式下,这个会被阻挡。在background模式下,它能运行(可能失败是因为我们没有接线NPM,但那是另一个问题)。在delegated模式下,除非npm install在信任列表中,否则会被屏蔽。

bash
npx tsc --noEmit

**注意:审批结果与命令结果**

命令可以被审批,但因普通原因仍然失败。npm test通过门,然后因为测试失败而非零退出。那是指挥部的问题,不是审批的问题。调试时保持它们分开。

提交

bash
git add index.ts
git commit -m "feat(approval): add discriminated union config with three modes"

完成标准

  • [ ] ApprovalConfig是可辨识联合类型,interactivebackgrounddelegated
  • [ ] createApproval(config)返回一个needsApproval函数
  • [ ] createBashTool接受审批函数作为参数
  • [ ] 每种模式至少在一个安全命令和一个不安全命令下表现正确
  • [ ] npx tsc --noEmit

**注意:会话级信任升级**

交互模式每次都会自动拒绝所有未知指令。这很快就会让人厌烦。试着添加一个跟踪用户在会话中审批的模式的 Set<string>。用户审批npm test后,添加模式。接下来的npm test 调用跳过了提示词。添加一个trust --list命令来显示哪些是可信的。现在考虑细节:npm install应该信任所有信息,还是只npm install express完全信任?

参考实现

ts
type ApprovalConfig =
  | { mode: "interactive" }
  | { mode: "background" }
  | { mode: "delegated"; trust: string[] };

function createApproval(config: ApprovalConfig) {
  return ({ command }: { command: string }) => {
    if (config.mode === "background") return false;

    if (config.mode === "delegated") {
      return !config.trust.some((p) => command.trim().startsWith(p));
    }

    return !SAFE_PREFIXES.some((p) => command.trim().startsWith(p));
  };
}

function createBashTool(
  operations: BashOperations,
  needsApproval: (input: { command: string }) => boolean,
) {
  return tool({
    description: `Execute a shell command in the working directory.

WHEN TO USE: running build commands, installing packages, running tests,
  git operations, directory listings.

WHEN NOT TO USE: reading file contents (use read instead).
  Searching for patterns (use grep instead).

DO NOT USE FOR: reading files (use read), searching code (use grep).

USAGE: command is a single shell string. Commands not approved by the
  approval policy are blocked and return a clear error message.`,
    inputSchema: z.object({
      command: z.string().describe("Shell command to execute"),
    }),
    execute: async ({ command }) => {
      if (needsApproval({ command })) {
        return `Blocked: "${command}" requires approval.`;
      }
      const { stdout } = await operations.exec(command);
      return stdout || "(no output)";
    },
  });
}

const bash = createBashTool(localOps, createApproval({ mode: "interactive" }));

非官方简体中文翻译 · 原课程来自 Vercel Academy