意图绑定和 canonical hash 深度解析
从 proposal 到 sha256 binding 的这段代码,决定了后续 policy verdict 是否能精确指向同一个动作。
导入链与契约
packages/pipeline/src/intent/ExecutionIntent.ts:12 定义 agent proposal,packages/pipeline/src/intent/ExecutionIntent.ts:37 定义可信上下文,packages/pipeline/src/intent/ExecutionIntent.ts:48 再把二者合成 ExecutionIntent。这个拆分防止 agent 自己伪造 tenant、actor 或 downstreamTarget。
12export const AgentProposalSchema = z 13 .object({ 14 action: actionName, 15 target: z.string().trim().min(1).max(500), 16 parameters: JsonObjectSchema.default({}), 17 }) 18 .strict(); 19 20export const IntentActorSchema = z 21 .object({ 22 id: boundedId, 23 type: boundedId, 24 runtime: boundedId.optional(), 25 trustLevel: z.string().trim().min(1).max(80).optional(), 26 }) 27 .strict(); 28 29export const DownstreamTargetSchema = z 30 .object({ 31 system: boundedId, 32 operation: boundedId, 33 endpoint: z.string().trim().min(1).max(500).optional(), 34 }) 35 .strict(); 36 37export const TrustedIntentContextSchema = z 38 .object({ 39 tenantId: z.string().uuid(), 40 actor: IntentActorSchema, 41 downstreamTarget: DownstreamTargetSchema, 42 context: JsonObjectSchema.default({}), 43 correlationId: boundedId.optional(), 44 idempotencyKey: z.string().trim().min(1).max(180), 45 }) 46 .strict(); 47 48export const ExecutionIntentSchema = z 49 .object({ 50 version: z.literal("agent-safe.intent/1"), 51 intentId: z.string().uuid(), 52 tenantId: z.string().uuid(), 53 capturedAt: z.string().datetime(), 54 expiresAt: z.string().datetime(), 55 actor: IntentActorSchema, 56 action: actionName, 57 target: z.string().trim().min(1).max(500), 58 parameters: JsonObjectSchema, 59 downstreamTarget: DownstreamTargetSchema, 60 context: JsonObjectSchema, 61 correlationId: boundedId.optional(), 62 idempotencyKey: z.string().trim().min(1).max(180), 63 }) 64 .strict();
▸读这页时先看三个不变量
第一,页面里的每个 path:NN 都指向当前源码快照;第二,所有授权都必须绑定到同一个 intentHash;第三,执行端只消费一次性 grant,不接收 agent 自己声明的“我被批准了”。
核心实现,逐段拆
▸capture 阶段先限制复杂度
packages/pipeline/src/intent/IntentCapture.ts:32 的 capture 接收 proposal 和 trusted context;packages/pipeline/src/intent/IntentCapture.ts:33、packages/pipeline/src/intent/IntentCapture.ts:34 在 schema parse 前先做 bounded check,避免复杂 JSON 在 zod 或 canonicalize 阶段消耗过多资源。packages/pipeline/src/intent/IntentCapture.ts:39 组装 intent 时只从 trustedInput 取 tenantId、actor、downstreamTarget 和 idempotencyKey。
32 public capture(proposalInput: AgentProposal, trustedInput: TrustedIntentContext): CapturedIntent { 33 this.hasher.assertInputBounded(proposalInput); 34 this.hasher.assertInputBounded(trustedInput); 35 const proposal = AgentProposalSchema.parse(proposalInput); 36 const trusted = TrustedIntentContextSchema.parse(trustedInput); 37 const capturedAt = this.clock(); 38 const expiresAt = new Date(capturedAt.valueOf() + this.ttlSeconds * 1_000); 39 const intent = ExecutionIntentSchema.parse({ 40 version: "agent-safe.intent/1", 41 intentId: this.createId(), 42 tenantId: trusted.tenantId, 43 capturedAt: capturedAt.toISOString(), 44 expiresAt: expiresAt.toISOString(), 45 actor: trusted.actor, 46 action: proposal.action, 47 target: proposal.target, 48 parameters: proposal.parameters, 49 downstreamTarget: trusted.downstreamTarget, 50 context: trusted.context, 51 ...(trusted.correlationId === undefined ? {} : { correlationId: trusted.correlationId }), 52 idempotencyKey: trusted.idempotencyKey, 53 }); 54 return this.hasher.capture(intent); 55 }
▸canonical hash 只绑定 authority 需要看的字段
packages/pipeline/src/intent/CanonicalIntentHasher.ts:26 先把 intent 转成 binding,packages/pipeline/src/intent/CanonicalIntentHasher.ts:35 用排序后的 canonical JSON 生成字符串,packages/pipeline/src/intent/CanonicalIntentHasher.ts:38 再计算 sha256。packages/pipeline/src/intent/CanonicalIntentHasher.ts:47 到 packages/pipeline/src/intent/CanonicalIntentHasher.ts:97 的输入边界检查覆盖深度、数组长度、循环对象和 prototype pollution key。
26 public capture(intent: ExecutionIntent): CapturedIntent { 27 const binding = CanonicalIntentHasher.bindingOf(intent); 28 let entries = 0; 29 this.assertBounded(binding as unknown as JsonValue, 0, () => { 30 entries += 1; 31 if (entries > this.limits.maxEntries) { 32 throw new Error("INTENT_TOO_COMPLEX"); 33 } 34 }); 35 const canonicalIntent = CanonicalIntentHasher.stringify(binding as unknown as JsonValue); 36 const byteLength = Buffer.byteLength(canonicalIntent, "utf8"); 37 if (byteLength > this.limits.maxBytes) throw new Error("INTENT_TOO_LARGE"); 38 const digest = createHash("sha256").update(canonicalIntent, "utf8").digest("hex"); 39 return Object.freeze({ 40 intent: CanonicalIntentHasher.deepFreeze(intent), 41 canonicalIntent, 42 intentHash: `sha256:${digest}`, 43 byteLength, 44 }); 45 }
关键流程
流程的关键是 packages/pipeline/src/intent/CanonicalIntentHasher.ts:99 的 bindingOf:它把 camelCase runtime schema 映射成 authority 所需的 snake_case protocol fields。packages/pipeline/src/intent/CanonicalIntentHasher.ts:133 的 canonicalize 会按 key 排序,packages/pipeline/src/intent/CanonicalIntentHasher.ts:144 的 deepFreeze 则让 captured intent 在返回后不可变。
边界 / 失败模式
packages/pipeline/src/intent/CanonicalIntentHasher.ts:65深度越界直接 INTENT_TOO_DEEP。packages/pipeline/src/intent/CanonicalIntentHasher.ts:66循环引用直接 CYCLIC_INTENT。packages/pipeline/src/intent/CanonicalIntentHasher.ts:87拒绝 __proto__、constructor、prototype 这些危险 key。packages/pipeline/src/intent/CanonicalIntentHasher.ts:37超过 100KB 的 canonical intent 会被拒绝。