@bolloon/bolloon-agent 0.4.30 → 0.4.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/agents/agent-gateway.js +5 -0
  2. package/dist/agents/agent-manifest-protocol.js +29 -0
  3. package/dist/agents/chain/chain-config.js +504 -0
  4. package/dist/agents/chain/chain-index-query.js +135 -0
  5. package/dist/agents/chain/chain-indexer.js +1045 -0
  6. package/dist/agents/chain/chain-settlement.js +284 -0
  7. package/dist/agents/chain/chain-state-store.js +245 -0
  8. package/dist/agents/chain/chain-wallet.js +198 -0
  9. package/dist/agents/chain/escrow-client.js +433 -0
  10. package/dist/agents/chain/index.js +20 -0
  11. package/dist/agents/chain/onchain-trade.js +691 -0
  12. package/dist/agents/economic-policy.js +7 -0
  13. package/dist/agents/gateway-network.js +6 -0
  14. package/dist/agents/local-signer.js +59 -0
  15. package/dist/agents/network-pulse.js +912 -0
  16. package/dist/agents/task/task-onchain-runner.js +284 -0
  17. package/dist/agents/task-contract.js +322 -0
  18. package/dist/agents/task-inbox.js +267 -0
  19. package/dist/agents/task-transport.js +280 -0
  20. package/dist/agents/x402/paid-info-store.js +75 -6
  21. package/dist/agents/x402/transaction-store.js +8 -1
  22. package/dist/cli/commands/agent.js +230 -0
  23. package/dist/cli/commands/chain.js +926 -0
  24. package/dist/cli/commands/index.js +69 -0
  25. package/dist/cli/commands/mcp.js +98 -0
  26. package/dist/cli/commands/network.js +334 -0
  27. package/dist/cli/commands/payment.js +125 -0
  28. package/dist/cli/commands/tasks.js +1236 -0
  29. package/dist/cli/commands/trade.js +229 -0
  30. package/dist/cli/commands/wallet.js +450 -0
  31. package/dist/cli/mcp/bridge.js +105 -0
  32. package/dist/cli/mcp/server.js +188 -0
  33. package/dist/cli/mcp/tools.js +955 -0
  34. package/dist/cli/protocol-envelope.js +358 -0
  35. package/dist/cli-entry.js +146 -17
  36. package/dist/electron-build/agents/agent-gateway.js +5 -0
  37. package/dist/electron-build/agents/agent-gateway.js.map +1 -1
  38. package/dist/electron-build/agents/agent-manifest-protocol.js +62 -0
  39. package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
  40. package/dist/electron-build/agents/chain/chain-config.js +556 -0
  41. package/dist/electron-build/agents/chain/chain-config.js.map +1 -0
  42. package/dist/electron-build/agents/chain/chain-index-query.js +145 -0
  43. package/dist/electron-build/agents/chain/chain-index-query.js.map +1 -0
  44. package/dist/electron-build/agents/chain/chain-indexer.js +1095 -0
  45. package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -0
  46. package/dist/electron-build/agents/chain/chain-settlement.js +291 -0
  47. package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -0
  48. package/dist/electron-build/agents/chain/escrow-client.js +443 -0
  49. package/dist/electron-build/agents/chain/escrow-client.js.map +1 -0
  50. package/dist/electron-build/agents/economic-policy.js +7 -0
  51. package/dist/electron-build/agents/economic-policy.js.map +1 -1
  52. package/dist/electron-build/agents/gateway-network.js +6 -0
  53. package/dist/electron-build/agents/gateway-network.js.map +1 -1
  54. package/dist/electron-build/agents/network-pulse.js +979 -0
  55. package/dist/electron-build/agents/network-pulse.js.map +1 -0
  56. package/dist/electron-build/agents/x402/paid-info-store.js +76 -6
  57. package/dist/electron-build/agents/x402/paid-info-store.js.map +1 -1
  58. package/dist/electron-build/agents/x402/transaction-store.js +8 -1
  59. package/dist/electron-build/agents/x402/transaction-store.js.map +1 -1
  60. package/dist/web/chain-explorer.js +617 -0
  61. package/dist/web/explorer.html +163 -0
  62. package/dist/web/index.html +5 -0
  63. package/dist/web/routes-chain-index.js +108 -0
  64. package/dist/web/routes-onchain-trade.js +99 -0
  65. package/dist/web/server.js +42 -0
  66. package/dist/web/task-frame-server.js +211 -0
  67. package/package.json +2 -1
  68. package/scripts/build-web.ts +20 -1
@@ -0,0 +1,284 @@
1
+ /**
2
+ * task-onchain-runner.ts — P4: 把链上 escrow **接进任务链路**的任务级入口
3
+ * =========================================================================
4
+ * 与 `runTask()` (x402/local-dev 支付) 并列的一条任务链路 —— 区别只在钱的走法:
5
+ *
6
+ * 预算闸(M1) → 荐资源 → beginTransaction → [createEscrowV2 → 装+真跑技能 → submitProofV2 → releaseV2]
7
+ * ↑ 每一步都走 verifyPaymentOnChain
8
+ * → 验真门(只有 chainSettled=true 才 verified) → 报告卡
9
+ *
10
+ * 借用的都是任务侧既有件: task-budget / resource-advisor / installBundle /
11
+ * executeContractSkill / resource-contract 保真校验 / report-card / transaction-store。
12
+ * 不重写任何支付判定: 链上判定只有 P3 chain-settlement 一份, 钱的状态只有
13
+ * settlement-state 一份, 链上事实只有 chain-state-store 一份。
14
+ *
15
+ * 三条硬规则 (与 runTask 一致):
16
+ * ① local-dev 永远到不了 fully_settled; ② chainSettled!==true 永远不 verified;
17
+ * ③ 非法迁移拒绝并给原因, 不静默修正。
18
+ * 另加一条 (链上特有): 任何一步链上事实不成立 → 原样停下, **不重付/不自动退款**;
19
+ * 重启时先读 chain-state.json 重建 (recoverOnchainTrade) 再决定续跑。
20
+ */
21
+ import * as fs from 'fs';
22
+ import * as path from 'path';
23
+ import { resolveTaskBudget } from './task-budget.js';
24
+ import { adviseResource } from './resource-advisor.js';
25
+ import { defaultRequestId, installBundle, deriveSkillInput, extractSources, extractConclusion } from './task-runner.js';
26
+ import { buildReportCard, renderReportCard } from './report-card.js';
27
+ import { loadResourceContract, executeContractSkill, validateResourceOutput, verifyInstallFidelity, } from '../x402/resource-contract.js';
28
+ import { beginTransaction, readTransaction, updateTransaction } from '../x402/transaction-store.js';
29
+ import { writeDeliveryContent } from '../x402/settlement-state.js';
30
+ import { computeContentHash } from '../x402/paid-info-protocol.js';
31
+ import { runOnchainTradeLoop, resumeOnchainTrade, recoverOnchainTrade, sha256Digest, checkOnchainAmount, onchainTaskKey, applyChainSettlementToTransaction, } from '../chain/onchain-trade.js';
32
+ /**
33
+ * 跑一条"链上托管"的任务: 买(托管) → 真执行 → 卖方上链承诺 → 买方释放 → 验真。
34
+ */
35
+ export async function runTaskOnchain(opts) {
36
+ const stages = [];
37
+ let last = Date.now();
38
+ const mark = (stage, note) => {
39
+ const now = Date.now();
40
+ stages.push({ stage, note, ms: now - last });
41
+ last = now;
42
+ opts.onStage?.(stage, note);
43
+ };
44
+ const decimals = opts.tokenDecimals ?? 6;
45
+ const requestId = opts.requestId || defaultRequestId(opts.task, Number(opts.budget ?? 0.05));
46
+ const taskId = requestId; // 任务键 = 幂等键 (重复跑同一任务不会新开托管)
47
+ const taskKey = onchainTaskKey(taskId);
48
+ const outputIssues = [];
49
+ // ① 预算闸 (M1: 单任务 0.05 / 单次 0.02, 所有层取最小值; 非法 → 不记账不花钱)
50
+ const parsed = resolveTaskBudget({ taskBudget: opts.budget, perPurchase: opts.perPurchase });
51
+ if (!parsed.ok || !parsed.plan) {
52
+ const card = buildReportCard({ task: opts.task, executed: false, paid: false, evidenceRef: {}, blocker: `预算不合法: ${parsed.error}` });
53
+ mark('report', '预算非法 → 没有托管, 没有花钱');
54
+ return {
55
+ ok: false, taskKey, requestId, transactionId: '', card, text: renderReportCard(card), budget: null,
56
+ chain: { verified: false, reason: '预算非法', statuses: [] }, outputIssues: [String(parsed.error)],
57
+ recovery: recoverOnchainTrade({ home: opts.home, taskKey }), stages,
58
+ };
59
+ }
60
+ const budget = parsed.plan;
61
+ mark('prepare', `任务: ${opts.task} · 任务预算 ${budget.taskBudget} / 单次上限 ${budget.perPurchase} USDC`);
62
+ // ② 荐资源 (用户不点名; 没有可执行资源就不买)
63
+ let chosen = opts.choose ? await opts.choose() : null;
64
+ if (!chosen && !opts.choose) {
65
+ const advised = await adviseResource({ task: opts.task, home: opts.home, cwd: opts.cwd, skillPaths: opts.skillPaths });
66
+ if (advised.needed && advised.chosen) {
67
+ chosen = {
68
+ name: advised.chosen.name, version: advised.chosen.version, dir: advised.chosen.dir,
69
+ contract: advised.chosen.contract, why: advised.chosen.why,
70
+ };
71
+ }
72
+ else {
73
+ const card = buildReportCard({ task: opts.task, executed: false, paid: false, evidenceRef: {}, blocker: `没有可用的可执行资源: ${advised.reason}` });
74
+ mark('report', '没有可执行资源 → 不买, 如实报告');
75
+ return {
76
+ ok: false, taskKey, requestId, transactionId: '', card, text: renderReportCard(card), budget,
77
+ chain: { verified: false, reason: '没有可执行资源', statuses: [] }, outputIssues: [advised.reason],
78
+ recovery: recoverOnchainTrade({ home: opts.home, taskKey }), stages,
79
+ };
80
+ }
81
+ }
82
+ if (!chosen) {
83
+ const card = buildReportCard({ task: opts.task, executed: false, paid: false, evidenceRef: {}, blocker: '注入的资源选择器没有给出候选' });
84
+ return {
85
+ ok: false, taskKey, requestId, transactionId: '', card, text: renderReportCard(card), budget,
86
+ chain: { verified: false, reason: '没有候选', statuses: [] }, outputIssues: ['没有候选'],
87
+ recovery: recoverOnchainTrade({ home: opts.home, taskKey }), stages,
88
+ };
89
+ }
90
+ mark('acquire', `选中 ${chosen.name} (${(chosen.why || []).slice(0, 2).join(' / ') || '名字匹配'})`);
91
+ // ③ 金额 = 单次购买上限 (与 M1 硬约束一致); 转原子单位
92
+ const amountAtomic = BigInt(Math.round(budget.perPurchase * 10 ** decimals));
93
+ const amountChk = checkOnchainAmount({ amountAtomic, decimals, budget: { taskBudget: budget.taskBudget, perPurchase: budget.perPurchase } });
94
+ if (!amountChk.ok) {
95
+ const card = buildReportCard({ task: opts.task, executed: false, paid: false, evidenceRef: {}, blocker: `预算门拒绝 (${amountChk.layer}): ${amountChk.reason}` });
96
+ return {
97
+ ok: false, taskKey, requestId, transactionId: '', card, text: renderReportCard(card), budget,
98
+ chain: { verified: false, reason: amountChk.reason, statuses: [] }, outputIssues: [amountChk.reason],
99
+ recovery: recoverOnchainTrade({ home: opts.home, taskKey }), stages,
100
+ };
101
+ }
102
+ // ④ 交易记录 (任务链路侧) —— 链上事实与它分开记, 最后一起进报告卡
103
+ const { record: created } = await beginTransaction({
104
+ requestId,
105
+ metadata: { itemId: chosen.name, price: amountChk.amountUsdc, currency: 'USDC', network: opts.network || 'unknown', payTo: opts.agentAddress, providerDid: 'did:key:zLocalSeller' },
106
+ buyerDid: opts.buyerDid || 'did:key:zTaskBuyer',
107
+ }, opts.home);
108
+ const transactionId = created.transactionId;
109
+ if (created.status === 'discovered')
110
+ await updateTransaction(transactionId, { status: 'quoted' }, opts.home);
111
+ await updateTransaction(transactionId, {
112
+ status: 'paying', paymentMode: 'escrow', settlementFact: 'payment_submitted', amount: amountChk.amountUsdc,
113
+ event: { kind: 'escrow:intent', detail: `${amountChk.amountUsdc} USDC → escrow ${opts.client.escrowAddress}` },
114
+ }, opts.home);
115
+ const tradeReq = {
116
+ client: opts.client, home: opts.home, taskId, agentAddress: opts.agentAddress,
117
+ amountAtomic, paymentAsset: opts.paymentAsset,
118
+ termsDigest: sha256Digest(`terms:${taskId}`), quoteDigest: sha256Digest(`quote:${taskId}`),
119
+ inputDigest: sha256Digest(`input:${taskId}`), manifestDigest: sha256Digest(`manifest:${taskId}`),
120
+ proofVersion: opts.proofVersion ?? 1,
121
+ deadline: opts.deadline ?? BigInt(Math.floor(Date.now() / 1000) + 3600),
122
+ confirmationWindow: opts.confirmationWindow ?? 3600,
123
+ budget: { taskBudget: budget.taskBudget, perPurchase: budget.perPurchase },
124
+ network: opts.network || 'unknown', tokenDecimals: decimals,
125
+ buyerSigner: opts.buyerSigner, sellerSigner: opts.sellerSigner,
126
+ env: opts.env, persistState: opts.persistState, verifyOnChain: opts.verifyOnChain,
127
+ };
128
+ // ⑤ 幂等/恢复: 已经链上走过的任务**不新开托管** (先读盘重建)
129
+ const before = recoverOnchainTrade({ home: opts.home, taskKey });
130
+ let loopVerified = false, loopReason = '';
131
+ const txHashes = {};
132
+ const statuses = [];
133
+ let execResult;
134
+ let resultDigest;
135
+ let deliveredContent = '';
136
+ let fidelity = { ok: false };
137
+ let outChk = { ok: false };
138
+ let conclusion = '';
139
+ let sources = [];
140
+ let contract = chosen.contract;
141
+ /** 真执行 (装 + 跑 + 契约校验) —— 与 runTask 同一套件, 不下链 */
142
+ const doExecute = async () => {
143
+ const SHARE = await import('../skill-share.js');
144
+ const collected = await SHARE.collectSkillBundle(chosen.dir, { name: chosen.name });
145
+ if (!collected?.ok || !collected?.bundle)
146
+ return { ok: false, reason: `技能打包失败: ${collected?.error || '未知'}` };
147
+ deliveredContent = JSON.stringify(collected.bundle);
148
+ const contentHash = computeContentHash(deliveredContent);
149
+ const installDir = path.join(opts.home, '.bolloon', 'tasks', taskId, 'skills', chosen.name);
150
+ fs.mkdirSync(installDir, { recursive: true });
151
+ const installed = installBundle(deliveredContent, installDir);
152
+ fidelity = installed.ok ? await verifyInstallFidelity({ content: deliveredContent, rec: { contentHash }, installDir }) : { ok: false, issues: [installed.error] };
153
+ if (!installed.ok) {
154
+ outputIssues.push(installed.error || '安装失败');
155
+ return { ok: false, reason: installed.error };
156
+ }
157
+ const loaded = await loadResourceContract(installDir);
158
+ if (loaded.ok && loaded.contract)
159
+ contract = loaded.contract;
160
+ const input = opts.input ?? deriveSkillInput(contract, opts.task);
161
+ const exec = await executeContractSkill({ contract, skillDir: installDir, input, allowedTools: ['skill_exec', 'read_file'], allowCodeExecution: true });
162
+ outChk = validateResourceOutput(contract, exec.output);
163
+ sources = extractSources(exec.output);
164
+ conclusion = extractConclusion(exec.output).conclusion || '';
165
+ const execution = { ok: exec.execution.ok === true && outChk.ok === true, reason: exec.execution.reason };
166
+ // 交易记录补执行证据
167
+ await updateTransaction(transactionId, {
168
+ execution: { ...exec.execution, ok: exec.execution.ok === true, schemaOk: outChk.ok === true },
169
+ resourceOutcome: { installed: fidelity.ok, executed: exec.execution.ok === true, outputContract: outChk.ok ? 'pass' : 'fail', criteriaHit: outChk.ok && sources.length > 0 && !!conclusion },
170
+ event: { kind: 'resource_outcome', detail: `installed=${fidelity.ok} executed=${exec.execution.ok === true} contract=${outChk.ok ? 'pass' : 'fail'}` },
171
+ }, opts.home);
172
+ if (!execution.ok)
173
+ outputIssues.push(...(outChk.issues || []), exec.execution.reason || '执行未达标');
174
+ // ★ 链上结果承诺 = 我手上这份交付内容的摘要 (买方可以自己复算)
175
+ return {
176
+ ok: execution.ok,
177
+ resultDigest: sha256Digest(deliveredContent),
178
+ manifestDigest: sha256Digest(`manifest:${taskId}`),
179
+ reason: execution.reason,
180
+ };
181
+ };
182
+ if (!before.found) {
183
+ const loop = await runOnchainTradeLoop(tradeReq, doExecute);
184
+ txHashes.create = loop.txHashes.create;
185
+ txHashes.proof = loop.txHashes.proof;
186
+ txHashes.release = loop.txHashes.release;
187
+ loopVerified = loop.verified;
188
+ loopReason = loop.verifiedReason;
189
+ execResult = loop.execution ? { ok: loop.execution.ok, reason: loop.execution.reason } : undefined;
190
+ resultDigest = loop.execution?.resultDigest;
191
+ for (const s of loop.steps)
192
+ statuses.push(`${s.method}:${s.chainStatus}`);
193
+ mark('acquire', loop.verified ? '链上闭环成立 (create/proof/release)' : `链上闭环未成立: ${loopReason.slice(0, 60)}`);
194
+ }
195
+ else {
196
+ // 已有链上事实 → 只做安全续跑 (绝不重付)
197
+ mark('acquire', `发现已有链上事实 (nextAction=${before.nextAction}) → 走恢复路径, 不新开托管`);
198
+ const resumed = await resumeOnchainTrade({ ...tradeReq, pendingResultDigest: opts.pendingResultDigest });
199
+ loopVerified = resumed.verified;
200
+ loopReason = resumed.reason;
201
+ for (const s of resumed.steps)
202
+ statuses.push(`${s.method}:${s.chainStatus}`);
203
+ txHashes.create = before.create?.txHash;
204
+ txHashes.proof = before.proof?.txHash;
205
+ txHashes.release = before.release?.txHash;
206
+ }
207
+ // ⑥ 验真门 + 报告卡
208
+ const afterRecovery = recoverOnchainTrade({ home: opts.home, taskKey });
209
+ const releaseRec = afterRecovery.release;
210
+ const payment = {
211
+ mode: 'escrow', chainSettled: loopVerified, trust: loopVerified ? 'verified' : 'self-attested',
212
+ txHash: txHashes.release,
213
+ };
214
+ let gateVerified = false;
215
+ let gateReason = loopReason;
216
+ if (loopVerified && releaseRec?.txHash) {
217
+ // 用**链上判定**重算一遍 (重放判定, 不新写判定逻辑): 释放记录 → 验真门
218
+ const { verifyChainSettlement } = await import('../chain/chain-settlement.js');
219
+ const verdict = await verifyChainSettlement(opts.client, {
220
+ txHash: releaseRec.txHash,
221
+ expect: { kind: 'escrow', taskKey, eventName: 'ReleasedV2', expectEscrowState: 'RELEASED' },
222
+ recorded: { blockNumber: releaseRec.blockNumber, confirmations: releaseRec.confirmations, status: releaseRec.status },
223
+ });
224
+ const recNow = await readTransaction(transactionId, opts.home);
225
+ const delivered = deliveredContent || (() => { try {
226
+ return fs.readFileSync(path.join(opts.home, '.bolloon', 'x402', 'deliveries', `${transactionId}.txt`), 'utf8');
227
+ }
228
+ catch {
229
+ return '';
230
+ } })();
231
+ if (delivered) {
232
+ const contentHash = computeContentHash(delivered);
233
+ const wr = writeDeliveryContent(transactionId, delivered, opts.home);
234
+ await updateTransaction(transactionId, {
235
+ contentHash, deliveryHash: contentHash, deliveryBytesHash: wr.hash, protocolVerified: true,
236
+ event: { kind: 'delivery_hash', detail: `deliveryHash=${contentHash.slice(0, 14)}… (与链上 resultHash 同源)` },
237
+ }, opts.home);
238
+ }
239
+ const gated = await applyChainSettlementToTransaction({
240
+ rec: (await readTransaction(transactionId, opts.home)),
241
+ home: opts.home, verdict, taskKey,
242
+ execution: recNow?.execution,
243
+ goalCriteriaMet: outChk.ok && sources.length > 0 && !!conclusion,
244
+ receipt: `chain-release:${releaseRec.txHash}`,
245
+ });
246
+ gateVerified = gated.verified;
247
+ gateReason = gated.verified ? gated.reason : `${gated.reason} (${gated.blockedBy.join(' | ')})`;
248
+ }
249
+ else if (!loopVerified) {
250
+ gateReason = `${loopReason} —— 链上没确定结算 → 不标 verified`;
251
+ }
252
+ const evidenceComplete = fidelity.ok && (execResult?.ok === true) && outChk.ok && sources.length > 0 && !!conclusion && gateVerified;
253
+ const card = buildReportCard({
254
+ task: opts.task,
255
+ conclusion: evidenceComplete ? (conclusion || '已完成') : '证据不足',
256
+ conclusionDetail: evidenceComplete ? undefined : ((outChk.issues || [])[0] || loopReason.slice(0, 120)),
257
+ skill: { name: chosen.name, version: chosen.version, dir: chosen.dir },
258
+ cost: { amount: amountChk.amountUsdc, currency: 'USDC', network: opts.network || 'unknown' },
259
+ payment,
260
+ sources,
261
+ outputContract: outChk.ok ? '通过' : '未通过',
262
+ resourceVerified: fidelity.ok ? '通过' : '未通过',
263
+ evidenceComplete,
264
+ executed: execResult?.ok === true,
265
+ paid: !!txHashes.create,
266
+ stage: 'report',
267
+ durationMs: stages.reduce((a, s) => a + s.ms, 0),
268
+ evidenceRef: { transactionId },
269
+ budgetLines: [...budget.why, ...amountChk.why],
270
+ blocker: evidenceComplete ? undefined : (loopVerified ? (outChk.ok ? undefined : `输出不符合资源契约: ${(outChk.issues || [])[0] || '未知'}`) : `链上未成立: ${loopReason.slice(0, 140)}`),
271
+ });
272
+ mark('report', card.status);
273
+ return {
274
+ ok: card.status === '已完成',
275
+ taskKey, requestId, transactionId, card, text: renderReportCard(card),
276
+ skill: { name: chosen.name, version: chosen.version, dir: chosen.dir },
277
+ amountUsdc: amountChk.amountUsdc, budget,
278
+ chain: { verified: gateVerified, reason: gateReason, ...txHashes, statuses },
279
+ execution: execResult,
280
+ outputIssues,
281
+ recovery: afterRecovery,
282
+ stages,
283
+ };
284
+ }
@@ -0,0 +1,322 @@
1
+ /**
2
+ * task-contract.ts — bolloon-task/1 任务协议 (Phase 1)
3
+ *
4
+ * 把「任务委派」与「交易协议」统一起来: agent_delegate 是快速委派, 这里给出**标准化任务生命周期**。
5
+ * 设计要点 (leo 2026-09-21 计划 + 支付规则修正):
6
+ * · 任务状态与支付事实**严格分离** —— "已付款" 永远不等于 "任务成功"
7
+ * · requestId 幂等; taskId 生命周期; capability 匹配; budget 约束; signature 证明来源
8
+ * · 任务正文只走 P2P / 受保护链路, **绝不进公开脉冲**
9
+ * · 支付模式: manual | policy | autonomous | **agent-authorized** (受控自主签名)
10
+ * · 私钥只在**本机进程**内可用: 公共网页 / P2P 消息 / Pulse / 公开交易记录永远拿不到;
11
+ * 每次签名写审计账本; 越权网络 / 越额 / 重复 requestId 一律拒绝
12
+ *
13
+ * 本文件是纯契约层 (类型 + 状态机 + 校验 + 签名 + 审计 + 公开投影), 不做 IO 编排。
14
+ */
15
+ import * as fs from 'fs';
16
+ import * as os from 'os';
17
+ import * as path from 'path';
18
+ import * as crypto from 'crypto';
19
+ import { canonicalize } from './network-pulse.js';
20
+ export const TASK_PROTOCOL = 'bolloon-task/1';
21
+ // ── 任务状态机 ──────────────────────────────────────────────────────────────
22
+ export const TASK_STATES = [
23
+ 'discovered', // 发现候选
24
+ 'quoted', // 已报价
25
+ 'policy_denied', // 策略拒绝 (终态)
26
+ 'submitted', // 已签名发出
27
+ 'accepted', // 对方接受
28
+ 'payment_required', // 等付款
29
+ 'paying', // 付款中
30
+ 'paid', // 付款已提交 (≠ 成功)
31
+ 'running', // 对方执行中
32
+ 'delivered', // 已交付
33
+ 'verified', // 已验真 (终态)
34
+ 'rejected', // 对方拒绝 (终态)
35
+ 'failed', // 失败 (终态)
36
+ 'cancelled', // 取消 (终态)
37
+ ];
38
+ /** 合法迁移图 (非法迁移一律**拒绝**, 不静默修正) */
39
+ export const TASK_TRANSITIONS = {
40
+ discovered: ['quoted', 'policy_denied', 'cancelled'],
41
+ quoted: ['submitted', 'policy_denied', 'cancelled'],
42
+ policy_denied: [],
43
+ submitted: ['accepted', 'rejected', 'payment_required', 'failed', 'cancelled'],
44
+ accepted: ['payment_required', 'paying', 'running', 'rejected', 'failed', 'cancelled'],
45
+ payment_required: ['paying', 'cancelled', 'failed'],
46
+ paying: ['paid', 'payment_required', 'failed'], // 不确定 → 回到 payment_required 等对账 (不许当失败)
47
+ paid: ['running', 'delivered', 'failed'],
48
+ running: ['delivered', 'failed'],
49
+ delivered: ['verified', 'failed'], // 交付后必须验真才算成功
50
+ verified: [],
51
+ rejected: [],
52
+ failed: [],
53
+ cancelled: [],
54
+ };
55
+ export const TASK_TERMINAL_STATES = ['policy_denied', 'verified', 'rejected', 'failed', 'cancelled'];
56
+ export function isTaskState(v) {
57
+ return typeof v === 'string' && TASK_STATES.includes(v);
58
+ }
59
+ export function isTaskTerminal(s) {
60
+ return TASK_TERMINAL_STATES.includes(s);
61
+ }
62
+ export function canTransitionTask(from, to) {
63
+ return Array.isArray(TASK_TRANSITIONS[from]) && TASK_TRANSITIONS[from].includes(to);
64
+ }
65
+ export function checkTaskMove(from, to) {
66
+ if (!isTaskState(from) || !isTaskState(to))
67
+ return { ok: false, reason: `未知任务状态: ${String(from)} → ${String(to)}` };
68
+ if (from === to)
69
+ return { ok: true };
70
+ if (!canTransitionTask(from, to)) {
71
+ return { ok: false, reason: `非法任务迁移 ${from} → ${to} (拒绝, 不静默修正)` };
72
+ }
73
+ return { ok: true };
74
+ }
75
+ // ── 支付事实 (与任务状态分离) ───────────────────────────────────────────────
76
+ /** 与 `x402/settlement-state.ts` 的两层事实保持同一套取值 (单测里有对齐断言) */
77
+ export const TASK_PAYMENT_FACTS = [
78
+ 'unpaid', 'payment_submitted', 'payment_verified', 'partially_settled',
79
+ 'fully_settled', 'refund_pending', 'refunded', 'unknown',
80
+ ];
81
+ export function isPaymentFact(v) {
82
+ return typeof v === 'string' && TASK_PAYMENT_FACTS.includes(v);
83
+ }
84
+ /** local-dev 能到哪一步 (与 x402 红线一致: 永不 fully_settled) */
85
+ export function maxFactForMode(mode) {
86
+ // 与 settlement-state 的 CHAIN_BACKED_FACTS / LOCAL_DEV_MAX_FACT 对齐 (单测有断言)
87
+ return mode === 'facilitator' ? 'fully_settled' : mode === 'local-dev' ? 'payment_submitted' : 'unpaid';
88
+ }
89
+ /** "成功" 的唯一判据: 任务 verified ∧ 支付事实到链上口径 */
90
+ export function isTaskSuccessful(task) {
91
+ return task.state === 'verified'
92
+ && (task.paymentFact === 'fully_settled' || task.paymentFact === 'payment_verified');
93
+ }
94
+ // ── 支付模式 (含 leo 修正的 agent-authorized) ───────────────────────────────
95
+ export const PAYMENT_MODES = ['manual', 'policy', 'autonomous', 'agent-authorized'];
96
+ export function isPaymentMode(v) {
97
+ return typeof v === 'string' && PAYMENT_MODES.includes(v);
98
+ }
99
+ // ── 幂等 id ─────────────────────────────────────────────────────────────────
100
+ function sha256Hex(s) {
101
+ return crypto.createHash('sha256').update(s).digest('hex');
102
+ }
103
+ /** 由 (任务正文, capability, 买方) 确定性派生 requestId —— 重发不会产生第二笔付款 */
104
+ export function taskRequestId(input) {
105
+ const h = sha256Hex(`${input.instruction.trim()}|${input.capability}|${input.buyerDid}|${input.salt || ''}`);
106
+ return `treq-${h.slice(0, 16)}`;
107
+ }
108
+ export function newTaskId(now = Date.now()) {
109
+ return `task-${now.toString(36)}-${crypto.randomBytes(3).toString('hex')}`;
110
+ }
111
+ // ── 校验 ────────────────────────────────────────────────────────────────────
112
+ const ATOMIC_RE = /^[0-9]+$/; // 原子单位必须是正整数 (浮点/负数/空一律拒)
113
+ export function validateTaskRequest(req, opts = {}) {
114
+ const issues = [];
115
+ const now = opts.now ?? Date.now();
116
+ if (req?.protocol !== TASK_PROTOCOL)
117
+ issues.push(`协议版本不对: ${String(req?.protocol)} (要 ${TASK_PROTOCOL})`);
118
+ for (const k of ['taskId', 'requestId', 'capability', 'instruction', 'buyerDid', 'providerDid']) {
119
+ if (!String(req?.[k] || '').trim())
120
+ issues.push(`缺字段 ${k}`);
121
+ }
122
+ if (!isPaymentMode(req?.paymentMode))
123
+ issues.push(`支付模式非法: ${String(req?.paymentMode)}`);
124
+ if (req?.budget) {
125
+ const b = req.budget;
126
+ if (!ATOMIC_RE.test(String(b.maxAmount ?? '')))
127
+ issues.push(`budget.maxAmount 必须是正整数原子单位字符串 (收到 ${String(b.maxAmount)})`);
128
+ if (b.currency !== 'USDC' && b.currency !== 'ETH')
129
+ issues.push(`budget.currency 只支持 USDC/ETH (收到 ${String(b.currency)})`);
130
+ if (!String(b.network || '').trim())
131
+ issues.push('budget.network 必填');
132
+ if (opts.allowedNetworks?.length && !opts.allowedNetworks.includes(String(b.network)))
133
+ issues.push(`网络不在允许列表: ${String(b.network)}`);
134
+ if (opts.maxAmountAtomic && ATOMIC_RE.test(String(b.maxAmount ?? '')) && BigInt(b.maxAmount) > BigInt(opts.maxAmountAtomic)) {
135
+ issues.push(`预算超过单笔上限: ${b.maxAmount} > ${opts.maxAmountAtomic}`);
136
+ }
137
+ }
138
+ if (req?.deadline !== undefined) {
139
+ if (!Number.isFinite(req.deadline))
140
+ issues.push('deadline 非法');
141
+ else if (req.deadline <= now)
142
+ issues.push(`deadline 已过期 (${req.deadline} <= ${now})`);
143
+ }
144
+ if (!String(req?.signature || '').trim())
145
+ issues.push('缺签名 (任务必须由发送方签名)');
146
+ // 时钟漂移 (防重放): 只看 deadline 是否荒谬地久远
147
+ const maxSkew = opts.maxSkewMs ?? 24 * 60 * 60 * 1000;
148
+ if (req?.deadline !== undefined && req.deadline > now + maxSkew * 30)
149
+ issues.push('deadline 过远 (疑似伪造)');
150
+ return { ok: issues.length === 0, issues };
151
+ }
152
+ /** 报价与请求是否自洽 (agent 不得接受被篡改的报价) */
153
+ export function validateQuoteAgainstRequest(quote, req, opts = {}) {
154
+ const issues = [];
155
+ if (quote?.protocol !== TASK_PROTOCOL)
156
+ issues.push(`报价协议版本不对: ${String(quote?.protocol)}`);
157
+ if (!quote?.taskId || quote.taskId !== req?.taskId)
158
+ issues.push('报价的 taskId 与请求不一致 (可能被篡改)');
159
+ if (!quote?.requestId || quote.requestId !== req?.requestId)
160
+ issues.push('报价的 requestId 与请求不一致 (回执不能跨请求复用)');
161
+ if (quote?.capability !== req?.capability)
162
+ issues.push(`报价能力与请求不一致: ${String(quote?.capability)} ≠ ${String(req?.capability)}`);
163
+ if (!ATOMIC_RE.test(String(quote?.amount ?? '')))
164
+ issues.push(`报价金额必须是正整数原子单位字符串 (收到 ${String(quote?.amount)})`);
165
+ if (req?.budget) {
166
+ if (quote?.currency !== req.budget.currency)
167
+ issues.push('报价币种与预算不一致');
168
+ if (quote?.network !== req.budget.network)
169
+ issues.push('报价网络与预算不一致');
170
+ if (ATOMIC_RE.test(String(quote?.amount ?? '')) && BigInt(String(quote?.amount)) > BigInt(req.budget.maxAmount)) {
171
+ issues.push(`报价超过任务预算: ${quote.amount} > ${req.budget.maxAmount}`);
172
+ }
173
+ }
174
+ if (opts.maxAmountAtomic && ATOMIC_RE.test(String(quote?.amount ?? '')) && BigInt(String(quote?.amount)) > BigInt(opts.maxAmountAtomic)) {
175
+ issues.push(`报价超过单笔上限: ${quote.amount} > ${opts.maxAmountAtomic}`);
176
+ }
177
+ return { ok: issues.length === 0, issues };
178
+ }
179
+ /** 接收方去重: 同一个 requestId 只接受一次 (重复请求不得重复执行/重复收费) */
180
+ export function dedupeInbox(inbox, incoming) {
181
+ const hit = inbox.find((x) => x.requestId === incoming.requestId);
182
+ return hit ? { dup: true, reason: `requestId ${incoming.requestId} 已在收件箱 (幂等: 不重复接受)` } : { dup: false };
183
+ }
184
+ // ── 签名 (受控自主签名: 私钥只在本机进程) ───────────────────────────────────
185
+ /** 签名覆盖内容: 去掉 signature 字段后的规范化 JSON */
186
+ export function signablePayload(payload) {
187
+ const { signature, ...rest } = payload;
188
+ void signature;
189
+ return canonicalize(rest);
190
+ }
191
+ export async function signTaskEnvelope(payload, keypair) {
192
+ const { KeyManager } = await import('@diap/sdk');
193
+ // 与 src/network/agent-network.ts 一致: 签名输入是 Uint8Array
194
+ const bytes = new TextEncoder().encode(signablePayload(payload));
195
+ const sig = await KeyManager.sign(keypair, bytes);
196
+ return { ...payload, signature: typeof sig === 'string' ? sig : Buffer.from(sig).toString('base64') };
197
+ }
198
+ /**
199
+ * 信封里的签名是 **base64 字符串** (便于走 JSON/P2P), 但 @diap/sdk 的
200
+ * `KeyManager.verify` 要的是 64 字节 `Uint8Array` (ed25519) —— 必须解码回去。
201
+ * 同时容忍 128 位十六进制写法 (老格式)。
202
+ */
203
+ export function decodeSignature(sig) {
204
+ if (/^[0-9a-f]{128}$/i.test(sig))
205
+ return new Uint8Array(Buffer.from(sig, 'hex'));
206
+ return new Uint8Array(Buffer.from(sig, 'base64'));
207
+ }
208
+ export async function verifyTaskEnvelope(payload, publicKey) {
209
+ try {
210
+ if (!payload?.signature)
211
+ return false;
212
+ const { KeyManager } = await import('@diap/sdk');
213
+ const bytes = new TextEncoder().encode(signablePayload(payload));
214
+ return await KeyManager.verify(publicKey, bytes, decodeSignature(payload.signature));
215
+ }
216
+ catch {
217
+ return false;
218
+ }
219
+ }
220
+ /**
221
+ * **受控自主签名** 的唯一放行判据。任何一条不满足 → 拒绝签名 (fail-closed)。
222
+ * 注意: 私钥永远不进这里 —— 这里只决定"允不允许让本机钱包模块去签"。
223
+ */
224
+ export function authorizeWalletSignature(req) {
225
+ const checks = {
226
+ modeIsAutonomous: req.mode === 'autonomous' || req.mode === 'agent-authorized',
227
+ agentAuthorized: req.agentAuthorized === true,
228
+ walletAvailable: req.walletAvailable === true,
229
+ networkAllowed: !req.allowedNetworks?.length || req.allowedNetworks.includes(req.network),
230
+ capabilityAllowed: !req.allowedCapabilities?.length || req.allowedCapabilities.includes(req.capability),
231
+ underPerTx: !req.maxPerTxAtomic || (ATOMIC_RE.test(req.amountAtomic) && BigInt(req.amountAtomic) <= BigInt(req.maxPerTxAtomic)),
232
+ underDaily: !req.dailyLimitAtomic || BigInt(req.spentTodayAtomic || '0') + BigInt(ATOMIC_RE.test(req.amountAtomic) ? req.amountAtomic : '0') <= BigInt(req.dailyLimitAtomic),
233
+ notDuplicate: !(req.signedRequestIds || []).includes(req.requestId),
234
+ amountIsInteger: ATOMIC_RE.test(req.amountAtomic),
235
+ };
236
+ const failed = Object.entries(checks).filter(([, v]) => !v).map(([k]) => k);
237
+ if (failed.length)
238
+ return { allowed: false, reason: `拒绝签名: ${failed.join(', ')}`, checks };
239
+ return { allowed: true, checks };
240
+ }
241
+ const auditFile = (h) => path.join(h || process.env.HOME || os.homedir(), '.bolloon', 'wallet-signatures.jsonl');
242
+ export async function recordSignatureAudit(entry, h) {
243
+ const row = { at: Date.now(), payloadDigest: entry.payloadDigest || '', ...entry };
244
+ try {
245
+ fs.mkdirSync(path.dirname(auditFile(h)), { recursive: true });
246
+ fs.appendFileSync(auditFile(h), JSON.stringify(row) + '\n', 'utf8');
247
+ // ★ 公开观察层: 钱包签名是一次真实活动 → 计一次签名 (fire-and-forget, 只计数不记内容)
248
+ try {
249
+ const np = await import('./network-pulse.js');
250
+ void np.recordNetworkEvent({
251
+ type: 'wallet_signed',
252
+ did: entry.signerFingerprint,
253
+ agentId: entry.taskId || entry.requestId,
254
+ taskId: entry.taskId || entry.requestId,
255
+ signed: true,
256
+ }, h);
257
+ }
258
+ catch { /* noop */ }
259
+ }
260
+ catch { /* 审计写失败不该炸主流程, 但调用方应看到 */ }
261
+ }
262
+ export async function readSignatureAudit(h, limit = 50) {
263
+ try {
264
+ const lines = fs.readFileSync(auditFile(h), 'utf8').trim().split('\n').filter(Boolean);
265
+ return lines.slice(-limit).map((l) => JSON.parse(l));
266
+ }
267
+ catch {
268
+ return [];
269
+ }
270
+ }
271
+ /** 审计条目里绝不允许出现的东西 */
272
+ export const AUDIT_FORBIDDEN_KEYS = ['privateKey', 'secret', 'instruction', 'taskText', 'mnemonic', 'seed'];
273
+ export function assertAuditSafe(entry) {
274
+ const issues = [];
275
+ const walk = (v, p) => {
276
+ if (Array.isArray(v))
277
+ return v.forEach((x, i) => walk(x, `${p}[${i}]`));
278
+ if (v && typeof v === 'object') {
279
+ for (const [k, val] of Object.entries(v)) {
280
+ if (AUDIT_FORBIDDEN_KEYS.includes(k))
281
+ issues.push(`${p}.${k} 不该出现在签名审计里`);
282
+ walk(val, `${p}.${k}`);
283
+ }
284
+ }
285
+ };
286
+ walk(entry, '$');
287
+ return issues;
288
+ }
289
+ /** 金额只以**区间**对外 (不暴露精确金额) */
290
+ export function amountBucket(amountAtomic) {
291
+ if (!ATOMIC_RE.test(amountAtomic))
292
+ return 'tiny';
293
+ const n = Number(amountAtomic);
294
+ if (n < 10_000)
295
+ return 'tiny'; // < 0.01 USDC
296
+ if (n < 100_000)
297
+ return 'small'; // < 0.1
298
+ if (n < 1_000_000)
299
+ return 'medium'; // < 1
300
+ return 'large';
301
+ }
302
+ /**
303
+ * 任务的**公开摘要**: 只保留可公开的粗粒度事实。
304
+ * 绝不包含: DID / 任务正文 / 精确金额 / 交易哈希 / 私有 Agent 名。
305
+ */
306
+ export function toPublicSummary(task, opts = {}) {
307
+ const group = opts.capabilityGroupOf ? opts.capabilityGroupOf(task.capability) : 'other';
308
+ const settlement = task.paymentFact === 'fully_settled' ? 'chain'
309
+ : task.paymentFact === 'payment_submitted' || task.paymentMode === 'local-dev' ? 'local-dev'
310
+ : 'none';
311
+ const kind = task.state === 'verified' ? 'trade_verified'
312
+ : task.state === 'delivered' ? 'task_completed'
313
+ : task.state === 'accepted' || task.state === 'running' ? 'task_accepted'
314
+ : 'task_posted';
315
+ return {
316
+ kind,
317
+ capabilityGroup: group,
318
+ ...(task.amountAtomic ? { amountBucket: amountBucket(task.amountAtomic) } : {}),
319
+ settlement,
320
+ state: task.state,
321
+ };
322
+ }