审批门
你在模块1中建立的允许列表只有一种模式:屏蔽列表中所有不在列表中的内容。这对演示来说没问题。这对真正的Harness来说不合适。
线人没有人可以问。子 Agent需要继承父 Agent的部分信任,而不是全部的授权清单。而本地Agent负责人可能只想审批一次npm install express,三步后又被问一次。
同一个门。三种不同的操作模式。我们将通过将配置本身的形状从布尔数演进到函数,再到可辨识联合类型来实现。
学习成果
createBashTool接受三种模式的 ApprovalConfig 可辨识联合类型:interactive、background 和 delegated。每种模式在needsApproval返回为真时都会形成形状。
快速路径
- 定义
ApprovalConfig为具有三种模态的可辨识联合类型 - 写入返回
needsApproval函数的createApproval(config) - 将结果传递给
createBashTool,验证每种模式的表现不同
动手练习 2.3
用可配置的审批系统取代静态的安全前缀检查。
要求:
- 定义
ApprovalConfig有三个变体:{ mode: "interactive" }、{ mode: "background" }、{ mode: "delegated"; trust: string[] } - 写入返回
(input) => boolean的createApproval(config: ApprovalConfig) - 更新
createBashTool以接受审批函数作为参数 - 用同一个命令测试每个模式,并验证门的行为变化
实现提示:
background所有事情都会false(审批)退货。这是针对CI和自动运行的delegated会对照输入config.trust并只审批匹配interactive会将输入与保险前缀列表对照,并审批安全前缀列表。其他任何事都需要人类的审批- 函数返回时
true需要审批时,false命令可以运行时返回
第一阶段:布尔值
最简单的审批门是布尔值:
needsApproval: true这会阻挡所有调用。ls、pwd、rm -rf,全部都没了。虽然没用,但能定下形状。needsApproval问题是“我们是否应该在运行前暂停等待人类的认可?”
第二阶段:功能
函数可以让你根据输入回答这个问题:
needsApproval: ({ command }) => {
if (SAFE_PREFIXES.some(p => command.startsWith(p))) return false;
return true;
}好多了。ls跑。rm -rf块。但函数只知道一条内置规则。CI和本地终端有相同的登机口。子 Agent和它的父 Agent一样拥有相同的门。你无法在不重写函数的情况下重新配置。
第三阶段:可辨识联合类型
配置里承载了模式。工厂根据以下模式构建了该功能:
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采用了审批函数,而不是安全前缀列表:
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)";
},
});
}调用网站上的三种模式是这样的:
// 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"] }),
);**注意:为什么是可辨识联合类型而不是三个函数**
你完全可以写三个独立的函数:interactiveApproval、backgroundApproval、delegatedApproval。可辨识联合类型之所以胜出,是因为配置是数据,而不是代码。你可以从AGENTS.md加载,用Zod(z.discriminatedUnion("mode", [...]))验证,跨子 Agent边界序列化,允许用户在不触碰Harness代码的情况下切换模式。
这会解锁什么
background是显而易见的。Agent在CI里运行,没有人可以问,你信任提示词,愿意放手。
delegated才是有趣的。当子 Agent启动时,你不会给它完整的保险前缀列表。你把它需要的特定命令交给它。只读探索器的表现pwd、find、git status。一个执行器进行测试,结果是有点npm test,npm run build。父 Agent以命令决定将哪些信托委托。
interactive你已经在做的,只是现在用配置来表达。
动手试试
试试每个模式,用一个应该阻挡的命令和一个应该通过的命令:
bun run index.ts . "Run: git status"在interactive模式下,这个问题会通过(它在安全名单上)。在background模式下,什么都过去了。在trust: ["git status"]``delegated模式下,它会通过。
bun run index.ts . "Run: npm install express"在interactive模式下,这个会被阻挡。在background模式下,它能运行(可能失败是因为我们没有接线NPM,但那是另一个问题)。在delegated模式下,除非npm install在信任列表中,否则会被屏蔽。
npx tsc --noEmit**注意:审批结果与命令结果**
命令可以被审批,但因普通原因仍然失败。npm test通过门,然后因为测试失败而非零退出。那是指挥部的问题,不是审批的问题。调试时保持它们分开。
提交
git add index.ts
git commit -m "feat(approval): add discriminated union config with three modes"完成标准
- [ ]
ApprovalConfig是可辨识联合类型,interactive、background、delegated - [ ]
createApproval(config)返回一个needsApproval函数 - [ ]
createBashTool接受审批函数作为参数 - [ ] 每种模式至少在一个安全命令和一个不安全命令下表现正确
- [ ]
npx tsc --noEmit
**注意:会话级信任升级**
交互模式每次都会自动拒绝所有未知指令。这很快就会让人厌烦。试着添加一个跟踪用户在会话中审批的模式的 Set<string>。用户审批npm test后,添加模式。接下来的npm test 调用跳过了提示词。添加一个trust --list命令来显示哪些是可信的。现在考虑细节:npm install应该信任所有信息,还是只npm install express完全信任?
参考实现
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" }));