Codex 教程
按顺序阅读,循序渐进掌握相关 AI 工具与实战方法。
模型发出一次工具调用,到工具真正执行、结果回到模型,中间走的不是“调用一个函数”,而是一条固定顺序的流水线:
tools/pre-execute → 单调守卫 → tools/execute → tools/post-execute → finalizeContent → tools/result
前三个 waterfall 可以改写一次调用;由工具定义自身控制的 finalizeContent 与 tools/result 在其后运行。两个易混概念先分清:waterfall(瀑布式事件)允许监听器调用 next() 把决定权委托下去,或直接返回决策短路;单调守卫则是一道只许缩减、不许撤销的最终防线。
tools/pre-execute 是第一个 waterfall,承载“钩子、权限、沙箱”这类可重排策略——多个策略插件的先后顺序可在配置里调整。它返回类型化决策 PreToolDecision:
{ kind: 'allow' }:放行,继续走守卫与后续环节;{ kind: 'deny'; reason }:拒绝,物化为错误结果,工具主体被跳过;{ kind: 'ask'; reason? }:触发 ctx.approval 一次性询问,仅审批返回 allowed-once 才继续。参数不可被改写,因为历史记录、审计、UI 与执行必须保持一致。waterfall 的弱点是后注册的监听器可以推翻前者;当某个不变式需要“最终拒绝、任何人都不能撤销”时,用 ctx.tools.guard():
// ToolGuard 故意没有 allow 结果:返回字符串即拒绝,undefined 即维持现状
type ToolGuard = (execution: Readonly<ToolExecution>) => string | undefined
正因为没有 allow 分支,监听器顺序永远无法把一次拒绝变回允许——这就是“单调”:只收权限,不给权限。实践原则:可重排策略放 tools/pre-execute,必须最终生效的不变式放 ctx.tools.guard()。
tools/execute 负责环绕分派:超时、重试、指标收集都在这一层做。它拿到的 ToolDispatchExecution 视图是唯一可替换 exec.signal 的地方,规则是“可替换、不可移除”——注册表会在调用工具主体前重新融合调用方的 signal。tools/post-execute 在结果归一化前做检查或改写,返回 PostToolDecision(accept 可替换展示内容或规范值,block 把纠正反馈变成错误结果);内容替换只是展示策略而非保密策略,要隐藏程序化值必须替换 value 或 block。finalizeContent 是 ToolDefinition 自有回调,恰好被调用一次,只做内容层面的最后同步修正;随后结果冻结,tools/result 作为同步通知供审计,观测者无法变换结果、失败也被隔离。记忆法:pre-execute 决定“能不能做”,execute 决定“怎么做”,post-execute 决定“怎么呈现”,result 只负责“看一眼”。
权限门禁插件就是一个普通的 Cordis 插件,无需外部协议:
import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'
const DENY_TOOLS = new Set(['fs_write', 'fs_edit'])
async function isAllowed(exec: ToolExecution): Promise<boolean> {
if (DENY_TOOLS.has(exec.name)) return false
const raw = exec.arguments as { path?: string }
if (typeof raw.path === 'string' && raw.path.includes('.env')) return false
return true
}
export const name = 'permission-gate'
export function apply(ctx: Context) {
ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
if (!(await isAllowed(exec))) {
return { kind: 'deny', reason: 'Denied by policy: this tool is not allowed in the izzi workspace.' }
}
return next() // 放行:把决定权交给后续监听器
})
}
给所有工具统一加超时,则应包裹 tools/execute:
ctx.on('tools/execute', async (exec, next) => {
const originalSignal = exec.signal
exec.signal = AbortSignal.any([originalSignal, AbortSignal.timeout(30_000)])
try {
return await next() // 委托给真正的工具主体
} finally {
exec.signal = originalSignal // 恢复原信号,避免污染后续调用
}
})
需要“永远无法被放行”的拒绝时用 ctx.tools.guard():返回字符串即最终拒绝,任何监听器都无法撤销;它返回的卸载函数会在插件卸载时由框架自动调用。