northcinder.shop
§ 00 · Source-level Observatory

northcinder 买家侧 Agent 商务防线

NorthCinder 不是把购物流程交给一个会点击网页的模型,而是把搜索、排序、信任信号、人工确认、签名 mandate 与 checkout rail 拆成多个互相制衡的源码边界。本页只看代码:本地 MCP server 在 client/src/server.ts:339 建立搜索入口,远端 bridge 在 remote/src/mcp-server.ts:123 刻意保持只读,授权存储在 client/src/authorization.ts:299 生成一次性确认码,真正 checkout 则由 packages/checkout/src/orchestrator.ts:101 的 mandate 硬门控制。

★ 1.2k
GitHub Stars
81k
LOC
425
Files
2026-08-18
Last commit
Buyer MCP host -> client/src/server.ts:339 search_products -> client/src/server.ts:1244 request_purchase_authorization -> client/src/server.ts:1402 approve_purchase -> client/src/server.ts:1505 complete_checkout -> packages/checkout/src/orchestrator.ts:107 verifyMandate -> client/src/audit-log.ts:57 append JSONL
§ 00 · OVERVIEW

它解决什么,凭什么值得拆

这个仓库的核心价值在于把“Agent 能买东西”改写成“Agent 只能请求、解释和执行被验证过的购买尝试”。搜索工具在 client/src/server.ts:339 后会把服务端排序结果重新校验,client/src/server.ts:426 记录 rankingVerified 与 coverage,浏览器观察结果从 client/src/server.ts:572 进入但在 client/src/server.ts:1323 被拒绝直接授权,人工授权工具从 client/src/server.ts:1244 开始,把 confirmation code 放到本地渠道而非 MCP 返回值。这让模型可以推荐和解释,但不能仅凭一次搜索结果绕过用户确认。

更值得拆的是它的失败语义:client/src/server.ts:1338 拒绝跨币种 shipping,client/src/server.ts:1351 校验 cap 与 offer total,client/src/server.ts:1541 拒绝 pending authorization,client/src/server.ts:1568 在 checkout 中标记执行中,而 packages/checkout/src/orchestrator.ts:123 对 mandate verification 失败只返回结构化错误。审计层在 client/src/audit-log.ts:57 以 append-only JSONL 落盘,订单图在 packages/orders/src/store.ts:136 保留无法解析邮件,整体设计偏向可追责而不是自动化炫技。

▸核心数据

架构模式
本地 buyer MCP server + 可选只读 remote bridge + checkout orchestrator + file-backed audit/nonce/order stores。
核心抽象
Offer、SearchRankResponse、TrustSignal、Authorization、PurchaseMandate、CheckoutRail、OrderGraph。
性能/精度数据
trust engine 搜索路径预算 800ms,见 service/src/trust/engine.ts:63 与 service/test/trust-engine.test.ts:73。
下游依赖
MCP host、browser observation agent、ACP rail、Shopify cart permalink、mail drop/IMAP order parser。
本文导航 — 6 个章节依次:概述 → 架构图 → 核心模块 → 关键流程 → 测试与验证 → 风险。子页面在每个 H3 末尾以 深度阅读 → 入口展开。
§ 01 · ARCHITECTURE

架构边界:本地可买,远端只读

源码把本地 server 与 remote bridge 分得很清楚。client/src/server.ts:1 的注释定义本地 MCP server 覆盖 search/ranking/auth/checkout/audit,而 remote/src/mcp-server.ts:1 明确 remote bridge 只有 search 和 trust 两个只读工具。服务端搜索返回后,本地在 client/src/server.ts:285 追加本地历史证据但不影响排序,在 client/src/server.ts:403 把 ranking tamper warning 写进 brief。这条边界避免了把购买权限暴露给远端 API key 持有人。

▸系统拓扑

local MCP host -> client/src/server.ts search_products -> service/src/orchestrator/orchestrator.ts rank/search -> client/src/server.ts verifies ranking + coverage -> request_purchase_authorization -> local code file -> approve_purchase -> signed PurchaseMandate -> complete_checkout -> checkout orchestrator -> rail remote MCP bridge -> remote/src/mcp-server.ts search_products / get_trust_signal only -> no authorization, no checkout, no watches, no profile, no orders

▸模块清单

模块拆分不是按 UI 页面拆,而是按交易边界拆。client/src/server.ts:113 注入 service、authorization、checkout、audit、profile、watches 和 orders;client/src/checkout-wiring.ts:51 组合 rail 优先级;packages/checkout/src/index.ts:1 只导出 mandate hard gate 和 rails;packages/orders/src/parser.ts:16 则把邮件解析保持为确定性插件链。

client
MCP 工具面:client/src/server.ts:507 search、client/src/server.ts:1244 authorization、client/src/server.ts:1505 checkout、client/src/server.ts:904 watches。
remote
自托管 bridge:remote/src/mcp-server.ts:31 明确无 authorization/checkout,remote/src/mcp-server.ts:76 清洗上游错误。
checkout
强制 mandate gate:packages/checkout/src/orchestrator.ts:77 rail 选择先于 nonce,packages/checkout/src/orchestrator.ts:107 verifyMandate 后才执行。
trust
信任信号 cache-first:service/src/trust/engine.ts:44 TTL,service/src/trust/engine.ts:119 冷探测预算,service/src/trust/engine.ts:163 后台刷新。
orders
订单图与 checkout audit 分离:packages/orders/src/store.ts:8 说明相邻而非扩展,packages/orders/src/store.ts:153 只读适配 checkout record。
tests
行为测试覆盖安全边界:client/test/authorization.test.ts:47 查 code 不泄漏,client/test/ranking-verification.test.ts:119 查排序篡改。

▸关键数据结构

src/packages/checkout/src/orchestrator.ts · L22-42TypeScript
22/** Durable record of an authorized checkout, citing the mandate that gated it. */
23export interface OrderRecord {
24  orderId: string;
25  createdAt: string;
26  offerId: string;
27  merchantId: string;
28  /** Canonical merchant domain paired with merchantId to avoid local-history collisions. */
29  merchantDomain: string;
30  railId: string;
31  /** "completed" = purchase made by the rail; "handed_off" = user's own session completes it. */
32  status: "completed" | "handed_off";
33  /** The mandate that authorized this checkout (cited in full for audit). */
34  mandateId: string;
35  mandate: PurchaseMandate;
36  evidence: RailEvidence;
37}
38
39export type CheckoutOutcome =
40  | { ok: true; order: OrderRecord }
41  | { ok: false; stage: "mandate"; error: MandateRejection }
42  | { ok: false; stage: "rail"; error: CheckoutError };
深度阅读 · MCP 工具边界
§ 02 · CORE MODULES

核心模块:搜索验证、授权、审计

▸搜索结果不会被服务端排名单独决定

本地 search_products 在 client/src/server.ts:339 校验服务端响应 schema,并在 client/src/server.ts:371 按 sourceStore+offer.id 建缓存。随后 client/src/server.ts:382 重新计算 ranking,client/src/server.ts:387 校验 store coverage,client/src/server.ts:426 把结果写进 structured output。测试在 client/test/ranking-verification.test.ts:112 构造被赞助结果插队的假服务,确保 client/test/ranking-verification.test.ts:155 能看到 rankingVerified=false。

src/client/src/server.ts · L339-386TypeScript
339  function finalizeSearch(params: {
340    searchId: string;
341    interpreted: InterpretedQuery;
342    data: SearchRankResponse;
343    continuedFrom?: string;
344  }) {
345    const { searchId, interpreted, continuedFrom } = params;
346    const parsedData = SearchRankResponseSchema.safeParse(params.data);
347    if (!parsedData.success) {
348      return failure({
349        code: "invalid_service_response",
350        message: "configured engine returned an invalid search response",
351      });
352    }
353    const data = parsedData.data;
354    const query = interpreted.criteria;
355    const { results, storeStatuses, trustSignals, registeredStores, browserObservationReport } = data;
356
357    seenSearches.set(searchId, interpreted);
358
359    for (const r of results) {
360      seenOffers.set(offerKey(r.offer.sourceStore, r.offer.id), r.offer);
361      const merchantKey = trustKey(r.offer.merchant);
362      seenMerchants.set(merchantKey, r.offer.merchant);
363      const keys = seenMerchantKeysById.get(r.offer.merchant.id) ?? new Set<string>();
364      keys.add(merchantKey);
365      seenMerchantKeysById.set(r.offer.merchant.id, keys);
366    }
367
368    const verification = verifySearchRanking(data, query);
369    const coverage = verifyStoreCoverage(registeredStores, storeStatuses);
370    const brief = composeBuyersBrief({
371      searchId,
372      results,
373      interpretedQuery: interpreted,
374      storeStatuses,
375      trustSignals,
376    });
377    seenBriefs.set(searchId, brief);
378
379    const localTrustEvidence: Record<string, ReturnType<typeof localTrustEvidenceFor>> = {};
380    const localTrustEvidenceKeys: Record<string, string> = {};
381    for (const f of brief.finalists) {
382      const finalistKey = offerKey(f.sourceStore, f.offerId);
383      const fullMerchant = seenOffers.get(finalistKey)?.merchant;
384      if (!fullMerchant) continue;
385      const merchantKey = trustKey(fullMerchant);
386      localTrustEvidenceKeys[finalistKey] = merchantKey;

▸授权码本地化,mandate 绑定四元组

授权 store 的注释从 client/src/authorization.ts:1 就声明三步流程,client/src/authorization.ts:198 确保 summary 不带 code,client/src/authorization.ts:240 保存 offer/maxAmount/intent 快照,client/src/authorization.ts:255 把 merchant、offer、variant、total、nonce 纳入 fingerprint,client/src/authorization.ts:299 写入 0600 code file。对应测试在 client/test/authorization.test.ts:47 和 client/test/authorization.test.ts:84 检查 code 与路径都不从 MCP 泄漏。

src/client/src/authorization.ts · L240-315TypeScript
240    /** Fingerprint over the canonical tuple AS DISPLAYED to the human. */
241    fingerprint: string;
242    /**
243     * Private request-time snapshot of the WHOLE offer as displayed to the
244     * human. approve() deep-compares the live offer against it (catches
245     * mutation of ANY field — incl. product.id, variant gid, title — not
246     * just the fingerprinted ones), and checkout reads THIS, never the
247     * externally-reachable live object.
248     */
249    offerSnapshot: Offer;
250    /** Snapshot of the cap shown on the trusted channels (mutation-detected at approve). */
251    approvedCap: Money;
252    /** Snapshot of the intent line the human read (mutation-detected at approve). */
253    intentSnapshot: string;
254    /** True while a checkout attempt is executing (decline is refused then). */
255    checkoutInFlight: boolean;
256  }
257  const records = new Map<string, InternalRecord>();
258
259  /** Fingerprint over the authorization's CURRENT tuple + the issuance nonce. */
260  function currentFingerprint(auth: Authorization, nonce: string): string {
261    const total = offerTotal(auth.offer);
262    return orderFingerprint({
263      merchantId: auth.offer.merchant.id,
264      offerId: auth.offer.id,
265      // Bind the PURCHASE-relevant identities, not just the offer id: the
266      // ACP rail buys by product.id, cart-permalink by the variant gid.
267      productId: auth.offer.product.id,
268      variantKey: auth.offer.product.attributes[VARIANT_ATTRIBUTE] ?? "",
269      totalMinor: total.amount,
270      currency: total.currency,
271      nonce,
272    });
273  }
274
275  /**
276   * Defensive read view (the CHECKOUT door): a deep clone whose offer and cap
277   * come from the private request-time snapshots — approve-time truth, never
278   * the externally-reachable live object. Post-approve mutation of a held
279   * reference therefore cannot re-aim what checkout verifies or charges.
280   */
281  function snapshotView(record: InternalRecord): Authorization {
282    return {
283      ...structuredClone(record.authorization),
284      offer: structuredClone(record.offerSnapshot),
285      maxAmount: structuredClone(record.approvedCap),
286      intent: record.intentSnapshot,
287    };
288  }
289
290  function expireIfDue(record: InternalRecord): void {
291    const auth = record.authorization;
292    if (auth.status === "pending" && now().getTime() > new Date(auth.expiresAt).getTime()) {
293      auth.status = "expired";
294      rmSync(record.codeFile, { force: true });
295    }
296  }
297
298  return {
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(),
深度阅读 · 人工授权与签名 mandate
§ 03 · KEY FLOW

关键流程:从 search 到 checkout attempt

购买链被拆成 request、approve、complete 三段。client/src/server.ts:1244 只创建 pending request,client/src/server.ts:1402 需要人类提供一次性 code,client/src/server.ts:1505 才尝试 checkout。真正的安全关键点在 packages/checkout/src/orchestrator.ts:101:rail selection 之后先 verifyMandate,packages/checkout/src/orchestrator.ts:127 才允许 rail.execute 接触 VerifiedMandate。

▸购买授权三段式

search_products -> seenOffers request_purchase_authorization validate offer / cap / shipping currency write local code file, return summary only approve_purchase compare human code, sign mandate complete_checkout verify mandate, consume nonce, execute rail

▸订单与信任的旁路证据

email drop / IMAP -> packages/orders parser -> order graph checkout order log -> read-only adapter -> order graph view merchant trust signal -> service evidence + local history ranking remains service-derived; local history is display-only
深度阅读 · 信任信号、审计与订单图
§ 04 · TESTS & VALIDATION

测试验证:失败路径比成功路径更关键

测试侧覆盖的是交易系统真正怕的路径:client/test/authorization.test.ts:98 验证签名 mandate,client/test/authorization.test.ts:122 限制错误 code 次数,client/test/authorization.test.ts:141 阻止过期 approval,client/test/authorization.test.ts:177 校验四元组文本,client/test/ranking-verification.test.ts:171 断言 audit 写入 tamper,service/test/trust-engine.test.ts:101 检查冷探测超时返回 unknown。

▸测试覆盖

排名验证测试直接构造 sponsored offer 被服务端置顶的场景。client/test/ranking-verification.test.ts:74 定义第一个自然排序结果,client/test/ranking-verification.test.ts:86 定义 sponsored offer,client/test/ranking-verification.test.ts:119 把 sponsored 放到第一位,client/test/ranking-verification.test.ts:144 调用 search_products,client/test/ranking-verification.test.ts:155 检查 rankingVerified=false,client/test/ranking-verification.test.ts:164 再查文本中包含 divergence。

tests/client/test/ranking-verification.test.ts · L112-174TypeScript
112      error: {
113        code: "invalid_service_response",
114        message: "configured engine returned an invalid search response",
115      },
116    });
117    expect(existsSync(auditPath) ? readFileSync(auditPath, "utf8") : "").toBe(auditBefore);
118
119    const authorization = await client.callTool({
120      name: "request_purchase_authorization",
121      arguments: { offerId: provenanceStrippedBrowser.id, sourceStore: provenanceStrippedBrowser.sourceStore },
122    });
123    expect(JSON.parse((authorization.content as Array<{ text: string }>)[0]!.text)).toMatchObject({
124      error: { code: "unknown_offer" },
125    });
126    await client.close();
127  });
128
129  it("an HONEST service is verified: rankingVerified true in output and audit log", async () => {
130    const honest: SearchRankResponse = {
131      results: rankOffers(OFFERS, { text: "sneaker" }, { trust: TRUST }),
132      storeStatuses: STORE_STATUSES,
133      trustSignals: TRUST,
134    };
135    const { client, auditPath } = await connectedClient(serviceReturning(honest));
136    const result = await client.callTool({ name: "search_products", arguments: { text: "sneaker" } });
137    expect(result.isError ?? false).toBe(false);
138    const structured = result.structuredContent as { rankingVerified: unknown; rankingDivergences?: unknown };
139    expect(structured.rankingVerified).toBe(true);
140    expect(structured.rankingDivergences).toBeUndefined();
141    const text = (result.content as Array<{ text: string }>)[0]!.text;
142    expect(text).toContain("Ranking verified");
143    expect(text).not.toContain(RANKING_TAMPER_WARNING);
144
145    const search = auditSearchLine(auditPath);
146    expect(search.rankingVerified).toBe(true);
147    await client.close();
148  });
149
150  it("TAMPER: a fake service returning a boosted re-order is flagged rankingVerified false with the exact divergence", async () => {
151    const ranked = rankOffers(OFFERS, { text: "sneaker" }, { trust: TRUST });
152    // Honest order is [cheap, mid, paid] (sponsored strictly last). The tampered
153    // service "boosts" the sponsored offer to the top — the classic pay-for-rank.
154    expect(ranked.map((r) => r.offer.id)).toEqual(["cheap", "mid", "paid"]);
155    const boosted: SearchRankResponse = {
156      results: [ranked[2]!, ranked[0]!, ranked[1]!],
157      storeStatuses: STORE_STATUSES,
158      trustSignals: TRUST,
159    };
160    const { client, auditPath } = await connectedClient(serviceReturning(boosted));
161    const result = await client.callTool({ name: "search_products", arguments: { text: "sneaker" } });
162    expect(result.isError ?? false).toBe(false); // results still returned — but loudly flagged
163
164    const structured = result.structuredContent as {
165      rankingVerified: unknown;
166      rankingDivergences?: Array<{ kind: string; position: number; expected: { offerKey: string }; actual: { offerKey: string } }>;
167    };
168    expect(structured.rankingVerified).toBe(false);
169    expect(structured.rankingDivergences).toBeDefined();
170    expect(structured.rankingDivergences![0]).toEqual({
171      kind: "order_mismatch",
172      position: 1,
173      expected: { offerKey: "ebay:cheap", score: ranked[0]!.score },
174      actual: { offerKey: "ebay:paid", score: ranked[2]!.score },

▸已知边界 / 失败模式

  • client/src/server.ts:1027 和 client/src/server.ts:1312 都拒绝同一个 offerId 跨 store 的模糊选择,避免 silently pick。
  • client/src/server.ts:1044 与 packages/checkout/src/orchestrator.ts:65 双重拒绝 browser observation 直接进入购买。
  • client/src/server.ts:1580 规定 no_rail 不烧 nonce,但 verification 后 rail 失败会消耗授权,偏向防重复付款。
  • packages/orders/src/parser.ts:45 插件异常会降级到 unparsed,packages/orders/src/store.ts:176 保留记录,不吞邮件。
§ 05 · HIGHLIGHTS

亮点:交易权限被拆成可审查接口

本地优先权限
授权 code 只写 buyer-local 渠道,client/src/authorization.ts:315 写入文件,client/src/authorization.ts:330 设置 0600,MCP 只返回 summary。
远端最小权限
remote bridge 在 remote/src/mcp-server.ts:21 明确不包含授权、checkout、watch、profile、orders;remote/src/mcp-server.ts:199 仍复用排序校验。
client/src/audit-log.ts · L57-79TypeScript
57export function readAuditPage(
58  path: string,
59  opts: { page?: number; pageSize?: number; chunkSize?: number } = {},
60): AuditPage {
61  const pageSize = Math.max(1, opts.pageSize ?? AUDIT_PAGE_SIZE_DEFAULT);
62
63  let totalEntries = 0;
64  forEachLineFromEnd(
65    path,
66    (line) => {
67      if (line.trim().length > 0) totalEntries += 1;
68    },
69    opts.chunkSize,
70  );
71
72  const totalPages = Math.max(1, Math.ceil(totalEntries / pageSize));
73  const page = Math.min(Math.max(1, opts.page ?? 1), totalPages);
74  // Newest first: page 1 starts at the END of the file. `offset`/`windowEnd`
75  // are ranks counted from EOF (rank 0 = newest non-blank line).
76  const offset = (page - 1) * pageSize;
77  const windowEnd = page * pageSize;
78
79  const rawLines: string[] = [];
service/src/trust/engine.ts · L58-84TypeScript
58  /** Set when deny/allow is authoritative — the engine can skip probes entirely. */
59  shortCircuit: boolean;
60}
61
62/** Curation layer: instant, live, no I/O. Only the verifiable DOMAIN is matched. */
63function evaluateSeed(seed: TrustSeed, domain: string): SeedEvaluation {
64  const denied = findEntry(seed.deny, domain);
65  if (denied) {
66    return { inputs: { denyHit: true }, evidence: [{ source: "seed-list", detail: `deny-listed: ${denied.detail}` }], shortCircuit: true };
67  }
68  const allowed = findEntry(seed.allow, domain);
69  if (allowed) {
70    return { inputs: { allowHit: true }, evidence: [{ source: "seed-list", detail: `allow-listed: ${allowed.detail}` }], shortCircuit: true };
71  }
72  const platform = findEntry(seed.knownPlatformDomains, domain);
73  if (platform) {
74    return { inputs: { platformHit: true }, evidence: [{ source: "platform-heuristic", detail: platform.detail }], shortCircuit: false };
75  }
76  return { inputs: {}, evidence: [], shortCircuit: false };
77}
78
79function toSignal(merchant: Merchant, inputs: TrustDerivationInputs, evidence: TrustEvidence[]): TrustSignal {
80  const { level } = deriveTrustLevel(inputs);
81  // Evidence is non-empty by construction (schema min(1)); a merchant with no
82  // curation and no measured signals still carries the explicit unknown floor.
83  const lines =
84    evidence.length > 0
§ 06 · RISKS & GAPS

技术亮点之外,别忽略这些

风险主要不是“代码没有防线”,而是工程集成时容易误解这些防线。client/src/checkout-wiring.ts:57 只有配置 ACP merchants 才启用 ACP rail,client/src/checkout-wiring.ts:73 默认还会加入 cart-permalink handoff;client/src/server.ts:1512 说明一个 mandate 只授权一次尝试。如果宿主 Agent UI 没有清晰呈现 pending/approved/declined 状态,安全语义会被用户体验稀释。

体验依赖宿主
源码能确保 code 不走 MCP,但 client/src/server.ts:1409 仍要求宿主向用户索取 code;宿主 UI 不清晰会造成误操作。
远端服务仍是关键依赖
本地能校验排序,但候选供给来自 service;remote/src/mcp-server.ts:178 对 upstream failure 只能结构化返回,无法替用户补齐商品来源。
冷信任信号有限
trust engine 在 service/src/trust/engine.ts:119 预算内探测,超时会返回 unknown;这是诚实失败,不是完整风险画像。
订单解析是确定性插件
邮件 parser 在 packages/orders/src/parser.ts:31 逐个插件尝试,未知格式会进入 unparsed,自动化覆盖度取决于插件库。
诚实记录 — 这部分列的是我在源码里看到、但作者没有明确文档化的限制。读者请按需取舍。