northcinder.shop
§ · Nested detail page

从 pending code 到 single-use mandate 深度解析

这一页看购买权限如何从模型输出变成人类确认后的签名 mandate:code 不从 MCP 返回,fingerprint 绑定购买四元组,checkout 前再做 cryptographic verification。

§ 00 · IMPORT CHAIN

导入链与契约

授权 store 的注释在 client/src/authorization.ts:1 定义 request/approve/decline 三步,client/src/authorization.ts:45 设置 15 分钟 TTL,client/src/authorization.ts:49 限制三次错误尝试,client/src/authorization.ts:179 生成随机 code 并使用 timingSafeEqual 比较。

src/client/src/authorization.ts · L1-27TypeScript
1/**
2 * The explicit human approval gate (spec §4 invariant 4):
3 *
4 *   request_purchase_authorization  → PENDING authorization + human summary
5 *   approve_purchase(code)          → mandate SIGNED, authorization approved
6 *   decline_purchase                → authorization VOID (symmetric, guilt-free)
7 *
8 * The confirmation code is written to
9 * `<configDir>/pending-authorizations/<id>.code` (0600). The standard MCP
10 * runtime disables the optional STDERR banner because hosts commonly capture
11 * child-process stderr. The tool result deliberately omits the code. Another
12 * program the buyer runs under the same OS account can read buyer-local state;
13 * separate accounts are an optional local hardening boundary.
14 * Wrong-code attempts are bounded; pending authorizations expire.
15 *
16 * approval "sign what you see": both trusted channels render the FOUR-TUPLE
17 * (merchant of record, exact item + variant, all-in total with an honest
18 * unknown-tax statement, payment context per rail) plus an order fingerprint
19 * displayed WITH the code. The fingerprint commits the code to the tuple —
20 * approval recomputes it and rejects any tuple mutated after issuance — and
21 * the mandate nonce is fixed at request time so the mandate signed at
22 * approval is exactly the order the human saw fingerprinted.
23 *
24 * The purchase mandate is signed only AT APPROVAL TIME (issuance = the human
25 * act), with the user's local ed25519 keypair, binding offer id, merchant,
26 * and a hard spending cap.
27 */
§ 01 · CORE IMPL

核心实现,逐段拆

▸request:写本地 code,不回传给 MCP

client/src/authorization.ts:299 创建 authorizationId、code、nonce 与 fingerprint,client/src/authorization.ts:315 写 code file,client/src/authorization.ts:321 记录 merchant/offer/total,client/src/authorization.ts:330 chmod 到 0600。测试在 client/test/authorization.test.ts:47 与 client/test/authorization.test.ts:84 证明 summary 与 stderr 不泄漏 code。

src/client/src/authorization.ts · L299-345TypeScript
299    request(offer, opts) {
300      const at = now();
301      const id = `auth_${randomUUID()}`;
302      // Own copies: the authorization's tuple must not alias caller-held
303      // objects (a later caller-side mutation must not silently retarget it).
304      const ownOffer = structuredClone(offer);
305      const maxAmount = structuredClone(opts.maxAmount ?? offerTotal(offer));
306      const paymentContext = opts.paymentContext ?? "none";
307      const authorization: Authorization = {
308        id,
309        status: "pending",
310        offer: ownOffer,
311        intent: opts.intent,
312        maxAmount,
313        paymentContext,
314        createdAt: at.toISOString(),
315        expiresAt: new Date(at.getTime() + ttlMs).toISOString(),
316        attemptsRemaining: maxAttempts,
317      };
318
319      const code = generateCode();
320      // The mandate nonce is fixed NOW so the fingerprint displayed with the
321      // code commits to the exact mandate that approval will sign.
322      const nonce = randomBytes(18).toString("base64url");
323      const fingerprint = currentFingerprint(authorization, nonce);
324      const codeFile = join(pendingDir, `${id}.code`);
325      // Every buyer-local approval channel shows the FULL four-tuple the mandate will sign
326      // (merchant of record, exact item + variant, all-in total with honest
327      // unknown-tax statement, payment context) plus the hard cap the
328      // (untrusted) host agent chose — "sign what you see".
329      const tupleLines = renderOrderTuple(ownOffer, paymentContext);
330      const capLine = `HARD SPENDING CAP: ${formatMoney(maxAmount)} — the agent may spend up to this amount`;
331      // local UI second door: the approval-page URL rides the same buyer-local
332      // channels as the code — never the tool result.
333      const approvalUrl = options.approvalUrl?.(id);
334      const approvalUrlLine =
335        approvalUrl !== undefined ? `Or review & approve/decline in your browser: ${approvalUrl}` : undefined;
336      // Fail CLOSED but never let the underlying fs error — which typically
337      // embeds the absolute configDir/pending-authorizations path — reach a
338      // tool-facing error string; request_purchase_authorization
339      // calls this via deps.authorizations.request at server.ts).
340      runFsOp(() => {
341        mkdirSync(pendingDir, { recursive: true, mode: 0o700 });
342        writeFileSync(
343          codeFile,
344          [
345            code,

▸approve:签名并限制错误尝试

client/src/authorization.ts:346 开始 approve 流程,client/src/authorization.ts:369 递增 wrongAttempts,client/src/authorization.ts:376 达到上限后 declined,client/src/authorization.ts:388 重新计算 fingerprint,client/src/authorization.ts:397 签发 mandate。测试在 client/test/authorization.test.ts:122 覆盖错误码上限。

src/client/src/authorization.ts · L346-415TypeScript
346            `# Authorization ${id} · order-fingerprint ${fingerprint}`,
347            `# ${authorization.intent}`,
348            ...tupleLines.map((line) => `# ${line}`),
349            `# ${capLine}`,
350            `# Give the first line of this file to your agent ONLY if you approve this purchase.`,
351            ...(approvalUrlLine !== undefined ? [`# ${approvalUrlLine}`] : []),
352            `# ${DECLINE_CHANNEL_LINE}`,
353            ``,
354          ].join("\n"),
355          { mode: 0o600 },
356        );
357        chmodSync(codeFile, 0o600);
358      }, "purchase authorization request failed: the confirmation code could not be written");
359
360      if (!options.quiet) {
361        // Optional low-level diagnostic mode only. The standard MCP runtime
362        // sets quiet=true because hosts commonly capture child-process stderr.
363        process.stderr.write(
364          [
365            ``,
366            `┌─ ${BRAND_NAME} purchase approval ──────────────────────────────────────`,
367            `│ ${authorization.intent}`,
368            ...tupleLines.map((line) => `│ ${line}`),
369            `│ ${capLine}`,
370            `│ Authorization ${id}`,
371            `│ CONFIRMATION CODE: ${code} · order-fingerprint ${fingerprint}`,
372            `│ Give this code to your agent ONLY if you approve this purchase.`,
373            ...(approvalUrlLine !== undefined ? [`│ ${approvalUrlLine}`] : []),
374            `│ ${DECLINE_CHANNEL_LINE}`,
375            `└─────────────────────────────────────────────────────────────────`,
376            ``,
377          ].join("\n"),
378        );
379      }
380
381      records.set(id, {
382        authorization,
383        code,
384        codeFile,
385        nonce,
386        fingerprint,
387        offerSnapshot: structuredClone(ownOffer),
388        approvedCap: structuredClone(maxAmount),
389        intentSnapshot: opts.intent,
390        checkoutInFlight: false,
391      });
392      // local UI push hook (best-effort, contained): the approval push must never
393      // fail — or delay reporting — the authorization request itself.
394      if (options.onRequested !== undefined) {
395        try {
396          options.onRequested({
397            authorizationId: id,
398            fingerprint,
399            intent: authorization.intent,
400            expiresAt: authorization.expiresAt,
401            ...(approvalUrl !== undefined ? { approvalUrl } : {}),
402          });
403        } catch {
404          process.stderr.write(
405            `[${BRAND_NAME}] approval push hook failed (authorization ${id}): hook_error\n`,
406          );
407        }
408      }
409      return {
410        authorization,
411        summary: buildAuthorizationSummary(authorization),
412        codeDelivery: { file: codeFile, stderr: !options.quiet },
413        fingerprint,
414      };
415    },
§ 02 · KEY FLOW

关键流程

request_purchase_authorization -> local code file approve_purchase -> code compare + fingerprint check -> signed mandate complete_checkout -> verifyMandate -> consume nonce -> rail.execute

server 层在 client/src/server.ts:1338 先拒绝 mixed-currency shipping,client/src/server.ts:1359 拒绝 cap 小于 total,client/src/server.ts:1431 才调用 approve,client/src/server.ts:1455 返回 mandate id。这些校验把错误尽量提前到人类确认之前。

▸验证联动

这页把实现和测试一起看:源码引用说明控制点,测试引用说明这些控制点已经被行为化约束,避免只把 README 的安全承诺复述成架构结论。

§ 03 · EDGE CASES

边界 / 失败模式

  • client/src/server.ts:1471 把 decline 做成对称工具,不需要 code,用户说不时不会被二次门禁卡住。
  • client/src/authorization.ts:290 会在过期后清理 code file,避免 stale code 长期有效。
  • client/test/authorization.test.ts:141 覆盖过期无法 approve,client/test/authorization.test.ts:160 覆盖 approve twice 失败。
Sub-page · 2026-08-20 Source · cinderline/northcinder ↑ northcinder 主页面