agent-safe-pipelineSafe
§ · Nested detail page

policy verdict 与一次性 grant 深度解析

远端 authority 不只返回 allow/block,它还必须把授权 token 绑定到当前 intentHash,并限制在 intent TTL 内。

§ 00 · IMPORT CHAIN

导入链与契约

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 不能假装拥有执行权。

src/packages/pipeline/src/decision/DecisionAuthority.ts · L16-44TypeScript
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 自己声明的“我被批准了”。

§ 01 · CORE IMPL

核心实现,逐段拆

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

src/packages/pipeline/src/decision/DecionisGate.ts · L63-128TypeScript
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。

src/packages/pipeline/src/execution/AuthorizationVerifier.ts · L64-118TypeScript
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      });
§ 02 · KEY FLOW

关键流程

GateDecision(ALLOW + token) -> consume-token -> VerifiedAuthorization -> SafeExecutor final checks

安全点在两端闭环: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 全部一致。

§ 03 · EDGE CASES

边界 / 失败模式

  • packages/pipeline/src/decision/DecionisGate.ts:127 authority 异常统一 AUTHORITY_UNAVAILABLE。
  • packages/pipeline/src/execution/AuthorizationVerifier.ts:94 consume-token 非 2xx 返回 null,不执行。
  • packages/pipeline/src/execution/AuthorizationVerifier.ts:107 过期 claims 直接返回 null。
  • packages/pipeline/src/execution/SafeExecutor.ts:68 grant 过期时间超过 intent.expiresAt 也被拒绝。