agent-safe-pipelineSafe
§ · Nested detail page

意图绑定和 canonical hash 深度解析

从 proposal 到 sha256 binding 的这段代码,决定了后续 policy verdict 是否能精确指向同一个动作。

§ 00 · IMPORT CHAIN

导入链与契约

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。

src/packages/pipeline/src/intent/ExecutionIntent.ts · L12-64TypeScript
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 自己声明的“我被批准了”。

§ 01 · CORE IMPL

核心实现,逐段拆

▸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。

src/packages/pipeline/src/intent/IntentCapture.ts · L32-55TypeScript
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。

src/packages/pipeline/src/intent/CanonicalIntentHasher.ts · L26-45TypeScript
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  }
§ 02 · KEY FLOW

关键流程

proposal + trusted context -> ExecutionIntent -> AuthorityIntentBinding -> canonical JSON -> sha256:intentHash

流程的关键是 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 在返回后不可变。

§ 03 · EDGE CASES

边界 / 失败模式

  • 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 会被拒绝。