policy verdict 与一次性 grant 深度解析
远端 authority 不只返回 allow/block,它还必须把授权 token 绑定到当前 intentHash,并限制在 intent TTL 内。
导入链与契约
packages/pipeline/src/decision/DecisionAuthority.ts:16 的 GateDecision 是执行端唯一相信的 verdict 容器;packages/pipeline/src/decision/DecisionAuthority.ts:22 到 packages/pipeline/src/decision/DecisionAuthority.ts:25 把 authorization token 和 expiresAt 做成可空字段,意味着 ESCALATE/BLOCK 不能假装拥有执行权。
16export interface GateDecision { 17 readonly verdict: DecisionVerdict; 18 readonly decisionId: string; 19 readonly dossierId: string | null; 20 readonly intentHash: string; 21 readonly reasonCodes: readonly string[]; 22 readonly authorization: { 23 readonly token: string; 24 readonly expiresAt: string; 25 } | null; 26 readonly failClosed: boolean; 27} 28 29export interface DecisionAuthority { 30 evaluate(intent: CapturedIntent, evidence?: DecisionEvidence): Promise<GateDecision>; 31} 32 33export class FailClosedDecision { 34 public static create(intentHash: string, reasonCode: string): GateDecision { 35 return immutableGateDecision({ 36 verdict: "BLOCK", 37 decisionId: "unavailable", 38 dossierId: null, 39 intentHash, 40 reasonCodes: [reasonCode], 41 authorization: null, 42 failClosed: true, 43 }); 44 }
▸读这页时先看三个不变量
第一,页面里的每个 path:NN 都指向当前源码快照;第二,所有授权都必须绑定到同一个 intentHash;第三,执行端只消费一次性 grant,不接收 agent 自己声明的“我被批准了”。
核心实现,逐段拆
▸DecionisGate 对 authority 响应 fail-closed
packages/pipeline/src/decision/DecionisGate.ts:70 发起 enforce-and-bind 请求,packages/pipeline/src/decision/DecionisGate.ts:85 遇到非 2xx 立即 AUTHORITY_REQUEST_FAILED,packages/pipeline/src/decision/DecionisGate.ts:88 用 BoundedResponseBody 限制响应体,packages/pipeline/src/decision/DecionisGate.ts:92 再 parse schema。packages/pipeline/src/decision/DecionisGate.ts:110 会把缺 grant 的 ALLOW 降级成 BLOCK。
63 public async evaluate( 64 captured: CapturedIntent, 65 evidence?: DecisionEvidence, 66 ): Promise<GateDecision> { 67 const controller = new AbortController(); 68 const timeout = setTimeout(() => controller.abort(), this.timeoutMs); 69 try { 70 const response = await this.fetchImpl(`${this.baseUrl}/v1/authority/enforce-and-bind`, { 71 method: "POST", 72 headers: { 73 authorization: `Bearer ${this.apiKey}`, 74 "content-type": "application/json", 75 "idempotency-key": captured.intent.idempotencyKey, 76 }, 77 body: JSON.stringify({ 78 ...CanonicalIntentHasher.bindingOf(captured.intent), 79 intent_hash: captured.intentHash, 80 mode: "ENFORCEMENT", 81 ...(evidence === undefined ? {} : { evidence }), 82 }), 83 signal: controller.signal, 84 }); 85 if (!response.ok) { 86 return FailClosedDecision.create(captured.intentHash, "AUTHORITY_REQUEST_FAILED"); 87 } 88 const text = await BoundedResponseBody.read(response, MAX_RESPONSE_BYTES); 89 if (text === null) { 90 return FailClosedDecision.create(captured.intentHash, "AUTHORITY_RESPONSE_TOO_LARGE"); 91 } 92 const parsed = AuthorityResponseSchema.parse(JSON.parse(text)); 93 if (parsed.action_hash !== captured.intentHash) { 94 return FailClosedDecision.create(captured.intentHash, "AUTHORITY_BINDING_MISMATCH"); 95 } 96 const verdict = 97 parsed.status === "ALLOW" 98 ? "ALLOW" 99 : parsed.status === "ESCALATE" || parsed.status === "REVIEW_REQUIRED" 100 ? "ESCALATE" 101 : "BLOCK"; 102 const canExecute = 103 verdict === "ALLOW" && 104 parsed.should_execute && 105 parsed.dossier_id !== null && 106 parsed.execution_token !== null && 107 parsed.execution_token_expires_at !== null && 108 Date.parse(parsed.execution_token_expires_at) > Date.now() && 109 Date.parse(parsed.execution_token_expires_at) <= Date.parse(captured.intent.expiresAt); 110 if (verdict === "ALLOW" && !canExecute) { 111 return FailClosedDecision.create(captured.intentHash, "AUTHORITY_GRANT_MISSING"); 112 } 113 return immutableGateDecision({ 114 verdict, 115 decisionId: parsed.decision_id, 116 dossierId: parsed.dossier_id, 117 intentHash: parsed.action_hash, 118 reasonCodes: parsed.reason_codes, 119 authorization: canExecute 120 ? { 121 token: parsed.execution_token as string, 122 expiresAt: parsed.execution_token_expires_at as string, 123 } 124 : null, 125 failClosed: parsed.status === "ERROR", 126 }); 127 } catch { 128 return FailClosedDecision.create(captured.intentHash, "AUTHORITY_UNAVAILABLE");
▸Grant verifier 消费 token 时再次提交 intent binding
packages/pipeline/src/execution/AuthorizationVerifier.ts:64 的 verifyAndConsume 先拒绝非 ALLOW 和 hash mismatch,packages/pipeline/src/execution/AuthorizationVerifier.ts:80 调用 consume-token,body 中 packages/pipeline/src/execution/AuthorizationVerifier.ts:87 发送 execution_token,packages/pipeline/src/execution/AuthorizationVerifier.ts:89 重新附上 canonical binding。返回后 packages/pipeline/src/execution/AuthorizationVerifier.ts:103 到 packages/pipeline/src/execution/AuthorizationVerifier.ts:110 逐项核对 claims。
64 public async verifyAndConsume( 65 captured: CapturedIntent, 66 decision: GateDecision, 67 ): Promise<VerifiedAuthorization | null> { 68 if ( 69 decision.verdict !== "ALLOW" || 70 decision.failClosed || 71 decision.authorization === null || 72 decision.dossierId === null || 73 decision.intentHash !== captured.intentHash 74 ) { 75 return null; 76 } 77 const controller = new AbortController(); 78 const timeout = setTimeout(() => controller.abort(), this.timeoutMs); 79 try { 80 const response = await this.fetchImpl(`${this.baseUrl}/v1/execution/consume-token`, { 81 method: "POST", 82 headers: { 83 authorization: `Bearer ${this.apiKey}`, 84 "content-type": "application/json", 85 }, 86 body: JSON.stringify({ 87 execution_token: decision.authorization.token, 88 intent_hash: captured.intentHash, 89 intent: CanonicalIntentHasher.bindingOf(captured.intent), 90 consumed_by: captured.intent.actor.id, 91 }), 92 signal: controller.signal, 93 }); 94 if (!response.ok) return null; 95 const text = await BoundedResponseBody.read(response, MAX_RESPONSE_BYTES); 96 if (text === null) return null; 97 const parsed = ConsumeResponseSchema.parse(JSON.parse(text)); 98 const claimExpiryMs = parsed.claims.exp * 1_000; 99 const decisionExpirySeconds = Math.floor( 100 Date.parse(decision.authorization.expiresAt) / 1_000, 101 ); 102 if ( 103 parsed.claims.binding.intent_hash !== captured.intentHash || 104 parsed.claims.decision_id !== decision.decisionId || 105 parsed.claims.dossier_id !== decision.dossierId || 106 parsed.claims.exp !== decisionExpirySeconds || 107 claimExpiryMs <= Date.now() || 108 claimExpiryMs > Date.parse(captured.intent.expiresAt) 109 ) { 110 return null; 111 } 112 return Object.freeze({ 113 decisionId: parsed.claims.decision_id, 114 dossierId: parsed.claims.dossier_id, 115 grantId: parsed.claims.jti, 116 intentHash: parsed.claims.binding.intent_hash, 117 expiresAt: new Date(parsed.claims.exp * 1_000).toISOString(), 118 });
关键流程
安全点在两端闭环:authority 端由 packages/pipeline/src/decision/DecionisGate.ts:93 确认 action_hash,执行端由 packages/pipeline/src/execution/AuthorizationVerifier.ts:103 确认 claims.binding.intent_hash。即使 token 被拿到,packages/pipeline/src/execution/SafeExecutor.ts:61 到 packages/pipeline/src/execution/SafeExecutor.ts:68 还会要求 decisionId、dossierId、grantId、intentHash 和 expiry 全部一致。
边界 / 失败模式
packages/pipeline/src/decision/DecionisGate.ts:127authority 异常统一 AUTHORITY_UNAVAILABLE。packages/pipeline/src/execution/AuthorizationVerifier.ts:94consume-token 非 2xx 返回 null,不执行。packages/pipeline/src/execution/AuthorizationVerifier.ts:107过期 claims 直接返回 null。packages/pipeline/src/execution/SafeExecutor.ts:68grant 过期时间超过 intent.expiresAt 也被拒绝。