03 · 审计日志与风险拦截
用 after_tool hook 记录工具调用日志,用 before_tool hook 拦截危险操作——让 agent 的行为可追溯、可控制。
审计日志
当 agent 在真实仓库中工作时,用户需要知道它做了什么。特别是当结果不符合预期时,需要回溯"agent 在第几步做了什么操作"。
审计日志 hook 是 after_tool 类型——它在每次工具执行后记录调用信息:
export function createAuditLogHook(): Hook {
const logs: Array<{
timestamp: string;
tool: string;
args: string;
resultPreview: string;
}> = [];
return {
name: "audit_log",
timing: "after_tool",
handler: async (ctx: AfterToolContext) => {
const timestamp = new Date().toISOString();
const argsPreview = JSON.stringify(ctx.args).slice(0, 100);
const resultPreview = ctx.result.slice(0, 100);
logs.push({ timestamp, tool: ctx.tool, args: argsPreview, resultPreview });
},
};
}记录的信息包括:
- 时间戳——精确到毫秒,方便定位
- 工具名——知道是搜索、读取还是修改
- 参数摘要——搜索了什么关键词、修改了什么文件
- 结果摘要——执行结果的前 100 个字符
日志存在 hook 的闭包中,不会写入文件。这在教学项目中足够了——如果需要持久化审计日志,可以把 logs.push(...) 改成写文件。
before_tool 拦截
before_tool hook 可以在工具执行前拦截调用。返回 skip: true 时,工具不会执行:
const result = await manager.executeBeforeTool({ tool: "search", args: {} });
if (result.skip) {
// 工具不会执行,用 result.skipResult 作为替代
}这和权限系统的 deny 类似,但更灵活——hook 可以看到工具参数的具体内容,做更精细的判断。
例如,一个防止修改 package.json 的 hook:
const protectPackageJson: Hook = {
name: "protect_package_json",
timing: "before_tool",
handler: async (ctx: BeforeToolContext) => {
if (
(ctx.tool === "write_file" || ctx.tool === "patch_file") &&
String(ctx.args.path ?? "").endsWith("package.json")
) {
return {
skip: true,
skipResult: "package.json 不允许被直接修改,请使用 npm/pnpm 命令操作依赖",
};
}
},
};Hook 链的执行规则
多个 hook 形成一条处理链。理解链的执行规则很重要:
before_tool 链:
hook A 执行 → hook B 执行 → hook C 执行
↑
如果 B 返回 skip: true
C 不执行,直接返回after_tool 链:
hook A 执行 → hook B 执行 → hook C 执行
↑
如果 B 报错
记录错误,C 继续执行区别在于:before_tool 中 skip 会中断后续 hook(因为工具已经被跳过了,后续 hook 没必要执行);after_tool 中错误不会中断(因为已经执行了,日志还是要记的)。
Hook 报错时的安全设计
Hook 的核心设计原则:hook 报错不中断 agent 主流程。
try {
const result = await hook.handler(ctx);
// ...
} catch (error) {
console.error(`Hook ${hook.name} 错误: ${error.message}`);
// 不 throw,继续执行后续 hook
}为什么?因为 hook 是"附加功能"——格式化、日志、验证。它们不应该成为 agent 的单点故障。如果 Prettier 格式化失败了,agent 应该继续工作,只是少了格式化这一步。
如果某个 hook 真的需要"强制中断",它应该在 before_tool 中返回 skip: true,而不是抛出异常。
在 main.ts 中的使用
main.ts 中注册了审计日志 hook:
const hookManager = new HookManager();
hookManager.register(createAuditLogHook());然后在调用 runAgent 时传入:
const result = await runAgent(state, model, tools, {
permissionGuard,
emitter,
contextManager,
projectRules,
skill: skillMatch?.skill,
hookManager,
});如果需要更多 hook,只需多注册几个:
hookManager.register(createAuditLogHook());
hookManager.register(createAutoFormatHook());不需要改 agent 的代码——这就是 hook 系统的可扩展性。
下一节总结 hook 系统的设计和产品价值。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。