@bolloon/bolloon-agent 0.4.29 → 0.4.31

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 (83) 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/contacts/chain.js +579 -0
  4. package/dist/agents/contacts/cli.js +158 -0
  5. package/dist/agents/contacts/consent.js +124 -0
  6. package/dist/agents/contacts/grant-payload.js +45 -0
  7. package/dist/agents/contacts/grants.js +451 -0
  8. package/dist/agents/contacts/policy.js +264 -0
  9. package/dist/agents/contacts/preview-types.js +6 -0
  10. package/dist/agents/contacts/providers.js +341 -0
  11. package/dist/agents/contacts/store.js +326 -0
  12. package/dist/agents/contacts/tools.js +154 -0
  13. package/dist/agents/contacts/types.js +148 -0
  14. package/dist/agents/economic-policy.js +7 -0
  15. package/dist/agents/gateway-network.js +6 -0
  16. package/dist/agents/local-signer.js +59 -0
  17. package/dist/agents/network-pulse.js +450 -0
  18. package/dist/agents/pi-sdk.js +10 -0
  19. package/dist/agents/task-contract.js +322 -0
  20. package/dist/agents/task-inbox.js +267 -0
  21. package/dist/agents/task-transport.js +280 -0
  22. package/dist/agents/x402/transaction-store.js +8 -1
  23. package/dist/cli/commands/agent.js +230 -0
  24. package/dist/cli/commands/index.js +61 -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 +185 -0
  33. package/dist/cli/mcp/tools.js +612 -0
  34. package/dist/cli/protocol-envelope.js +346 -0
  35. package/dist/cli-entry.js +141 -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/contacts/chain.js +619 -0
  41. package/dist/electron-build/agents/contacts/chain.js.map +1 -0
  42. package/dist/electron-build/agents/contacts/consent.js +163 -0
  43. package/dist/electron-build/agents/contacts/consent.js.map +1 -0
  44. package/dist/electron-build/agents/contacts/grant-payload.js +52 -0
  45. package/dist/electron-build/agents/contacts/grant-payload.js.map +1 -0
  46. package/dist/electron-build/agents/contacts/grants.js +500 -0
  47. package/dist/electron-build/agents/contacts/grants.js.map +1 -0
  48. package/dist/electron-build/agents/contacts/policy.js +271 -0
  49. package/dist/electron-build/agents/contacts/policy.js.map +1 -0
  50. package/dist/electron-build/agents/contacts/preview-types.js +8 -0
  51. package/dist/electron-build/agents/contacts/preview-types.js.map +1 -0
  52. package/dist/electron-build/agents/contacts/providers.js +384 -0
  53. package/dist/electron-build/agents/contacts/providers.js.map +1 -0
  54. package/dist/electron-build/agents/contacts/store.js +370 -0
  55. package/dist/electron-build/agents/contacts/store.js.map +1 -0
  56. package/dist/electron-build/agents/contacts/tools.js +159 -0
  57. package/dist/electron-build/agents/contacts/tools.js.map +1 -0
  58. package/dist/electron-build/agents/contacts/types.js +166 -0
  59. package/dist/electron-build/agents/contacts/types.js.map +1 -0
  60. package/dist/electron-build/agents/economic-policy.js +7 -0
  61. package/dist/electron-build/agents/economic-policy.js.map +1 -1
  62. package/dist/electron-build/agents/external-events.js.map +1 -1
  63. package/dist/electron-build/agents/gateway-network.js +6 -0
  64. package/dist/electron-build/agents/gateway-network.js.map +1 -1
  65. package/dist/electron-build/agents/goal-store.js.map +1 -1
  66. package/dist/electron-build/agents/network-pulse.js +504 -0
  67. package/dist/electron-build/agents/network-pulse.js.map +1 -0
  68. package/dist/electron-build/agents/pi-sdk.js +10 -0
  69. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  70. package/dist/electron-build/agents/x402/transaction-store.js +8 -1
  71. package/dist/electron-build/agents/x402/transaction-store.js.map +1 -1
  72. package/dist/electron-build/security/tool-gate.js +4 -0
  73. package/dist/electron-build/security/tool-gate.js.map +1 -1
  74. package/dist/index.js +30 -0
  75. package/dist/security/tool-gate.js +4 -0
  76. package/dist/web/mobile-contacts.js +329 -0
  77. package/dist/web/mobile-core.js +481 -15
  78. package/dist/web/mobile.html +32 -0
  79. package/dist/web/mobile.js +139 -0
  80. package/dist/web/routes-contacts.js +420 -0
  81. package/dist/web/server.js +34 -0
  82. package/dist/web/task-frame-server.js +211 -0
  83. package/package.json +1 -1
@@ -0,0 +1,612 @@
1
+ /**
2
+ * tools.ts — P4 MCP 的 tools / resources 定义 (17 tool + 7 resource)
3
+ *
4
+ * 设计纪律 (P4 任务书 + `docs/wiki/agent-access-layer.md` §1):
5
+ * ① **一个 tool = 一条 P3 子命令**。这里只做两件事: 校验入参 (白名单 + 类型)、
6
+ * 拼出等价于 CLI 的 argv。**没有任何业务判断** (不算钱、不判成败、不写盘、不发付款)。
7
+ * ② **客户端给的是具名参数, 不是 argv** —— 参数名走白名单, 未知参数一律 `INVALID_ARGUMENT`。
8
+ * 由此它**不可能**注入 `--private-key` / `--mode` / `--force` 之类选项去绕过政策 (§六条禁止)。
9
+ * ③ 值里不许以 `-` 开头 (防"值 = 选项"注入)。
10
+ * ④ 没实现的 P3 子命令 (`task send|inbox|accept|reject|complete|cancel` · `network leave`)
11
+ * **不暴露成 tool**: 暴露了就得在 MCP 层假装成功, 那正好违反"失败不得变成功"。
12
+ * 需要它们的能力时, MCP 侧应直接读 P3 的 `C_NOT_IMPLEMENTED` 说明 (见 §NOT_EXPOSED)。
13
+ */
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import { fileURLToPath } from 'url';
17
+ import { failEnvelope } from '../protocol-envelope.js';
18
+ import { currentPackageRoot } from '../../utils/version-info.js';
19
+ import { callP3 } from './bridge.js';
20
+ // ── 入参校验 (唯一入口; 客户端**不能**直接给 argv) ────────────────────────────
21
+ const MAX_LEN = 4000;
22
+ class Params {
23
+ args;
24
+ allowed;
25
+ seen = new Set();
26
+ bad = [];
27
+ constructor(args, allowed) {
28
+ this.args = args;
29
+ this.allowed = allowed;
30
+ }
31
+ touch(key) {
32
+ this.seen.add(key);
33
+ }
34
+ str(key, opts = {}) {
35
+ this.touch(key);
36
+ const v = this.args[key];
37
+ if (v === undefined || v === null || v === '') {
38
+ if (opts.required)
39
+ this.bad.push(`缺少必填参数 '${key}'`);
40
+ return undefined;
41
+ }
42
+ if (typeof v !== 'string') {
43
+ this.bad.push(`参数 '${key}' 必须是字符串 (收到 ${typeof v})`);
44
+ return undefined;
45
+ }
46
+ if (v.startsWith('-')) {
47
+ this.bad.push(`参数 '${key}' 不许以 '-' 开头 (防"值当成命令选项"注入命令行)`);
48
+ return undefined;
49
+ }
50
+ if (v.length > (opts.maxLen ?? MAX_LEN)) {
51
+ this.bad.push(`参数 '${key}' 太长 (> ${opts.maxLen ?? MAX_LEN} 字符)`);
52
+ return undefined;
53
+ }
54
+ return v;
55
+ }
56
+ /** 正数 (金额/限额这类: 只收**字符串**形式的十进制, 与 P3 的原子单位口径一致) */
57
+ strNumber(key) {
58
+ const v = this.str(key);
59
+ if (v === undefined)
60
+ return undefined;
61
+ if (!/^\d+(\.\d+)?$/.test(v)) {
62
+ this.bad.push(`参数 '${key}' 必须是正的十进制数字串 (如 "0.05"), 收到 ${JSON.stringify(v)}`);
63
+ return undefined;
64
+ }
65
+ return v;
66
+ }
67
+ /** 整数毫秒 (tool 超时) */
68
+ intMs(key) {
69
+ this.touch(key);
70
+ const v = this.args[key];
71
+ if (v === undefined || v === null)
72
+ return undefined;
73
+ if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0) {
74
+ this.bad.push(`参数 '${key}' 必须是正整数 (毫秒)`);
75
+ return undefined;
76
+ }
77
+ return Math.floor(v);
78
+ }
79
+ bool(key) {
80
+ this.touch(key);
81
+ const v = this.args[key];
82
+ if (v === undefined || v === null)
83
+ return false;
84
+ if (typeof v !== 'boolean') {
85
+ this.bad.push(`参数 '${key}' 必须是布尔值`);
86
+ return false;
87
+ }
88
+ return v;
89
+ }
90
+ /** 字符串数组 (也接受逗号分隔的单串) */
91
+ strList(key) {
92
+ this.touch(key);
93
+ const v = this.args[key];
94
+ if (v === undefined || v === null || v === '')
95
+ return [];
96
+ const list = Array.isArray(v) ? v : (typeof v === 'string' ? v.split(',') : null);
97
+ if (!list) {
98
+ this.bad.push(`参数 '${key}' 必须是字符串数组或逗号分隔的字符串`);
99
+ return [];
100
+ }
101
+ const out = [];
102
+ for (const item of list) {
103
+ if (typeof item !== 'string' || !item.trim()) {
104
+ this.bad.push(`参数 '${key}' 里每一项都必须是非空字符串`);
105
+ continue;
106
+ }
107
+ const s = item.trim();
108
+ if (s.startsWith('-')) {
109
+ this.bad.push(`参数 '${key}' 的值不许以 '-' 开头`);
110
+ continue;
111
+ }
112
+ out.push(s);
113
+ }
114
+ return out;
115
+ }
116
+ /** 任意 JSON 对象 (task run 的 --input) */
117
+ obj(key) {
118
+ this.touch(key);
119
+ const v = this.args[key];
120
+ if (v === undefined || v === null)
121
+ return undefined;
122
+ if (typeof v !== 'object') {
123
+ this.bad.push(`参数 '${key}' 必须是 JSON 对象/数组`);
124
+ return undefined;
125
+ }
126
+ return v;
127
+ }
128
+ /** 白名单之外的一律拒 (这是"不能注入选项"的机制保证) */
129
+ finish() {
130
+ const unknown = Object.keys(this.args).filter((k) => !this.allowed.includes(k));
131
+ if (unknown.length)
132
+ this.bad.push(`不认识的参数: ${unknown.join(', ')} (只接受: ${this.allowed.join(', ')})`);
133
+ if (!this.bad.length)
134
+ return null;
135
+ return failEnvelope('INVALID_ARGUMENT', `MCP tool 入参不合法: ${this.bad.join('; ')}`, { issues: this.bad, acceptedParams: [...this.allowed] }, [], 'needs_human');
136
+ }
137
+ /** 全局项 (超时 / 幂等键) —— 幂等键只在该 tool 声明了 `requestId` 时才透传 */
138
+ opts() {
139
+ const o = {};
140
+ if (this.allowed.includes('requestId')) {
141
+ const rid = this.str('requestId', { maxLen: 512 });
142
+ if (rid)
143
+ o.requestId = rid;
144
+ }
145
+ const t = this.intMs(TIMEOUT_KEY);
146
+ if (t)
147
+ o.timeoutMs = t;
148
+ return o;
149
+ }
150
+ }
151
+ /** 统一收尾: 校验通过 → plan; 不通过 → INVALID_ARGUMENT 信封 (不抛异常, 不给半成品) */
152
+ function plan(p, group, argv) {
153
+ const opts = p.opts(); // 先解析全局项 (类型错与其它问题一起报)
154
+ const err = p.finish();
155
+ if (err)
156
+ return { ok: false, envelope: err };
157
+ return { ok: true, plan: Object.keys(opts).length ? { group, argv, opts } : { group, argv } };
158
+ }
159
+ const TIMEOUT_KEY = 'timeoutMs';
160
+ const TIMEOUT_PROP = { type: 'integer', minimum: 1, description: '本次调用的硬超时 (毫秒); 超时返回 P3 信封 code=TIMEOUT (不是"打印一行错误就退出")' };
161
+ // ── 17 个 tool ──────────────────────────────────────────────────────────────
162
+ export const TOOLS = [
163
+ {
164
+ name: 'bolloon_network_join',
165
+ title: '加入 Bolloon 网络',
166
+ description: '加入网络 (幂等)。给 link 就按链接加入 (orbitdb:// / ipns:// / https://.../registry); 不给 link 走文档驱动的全球入网。' +
167
+ '返回 P3 信封: code=NETWORK_JOINED 才是真加入; ok:false 一律别当成功读。' +
168
+ '硬约束: 本 tool 只调 `bolloon network join` 服务层, 不复制逻辑; 私钥永不参与, 也不返回。',
169
+ inputSchema: {
170
+ type: 'object',
171
+ additionalProperties: false,
172
+ properties: {
173
+ link: { type: 'string', description: '入网链接 (不给则走全球入网文档)' },
174
+ name: { type: 'string', description: '本节点在网络里的名字' },
175
+ url: { type: 'string', description: '覆盖入网文档地址' },
176
+ capabilities: { type: 'array', items: { type: 'string' }, description: '要声明的能力 (如 ["research","data"])' },
177
+ timeoutMs: TIMEOUT_PROP,
178
+ },
179
+ },
180
+ build(args) {
181
+ const p = new Params(args, ['link', 'name', 'url', 'capabilities', TIMEOUT_KEY]);
182
+ const argv = ['join'];
183
+ const link = p.str('link', { maxLen: 2048 });
184
+ if (link)
185
+ argv.push(link);
186
+ const name = p.str('name', { maxLen: 200 });
187
+ if (name)
188
+ argv.push('--name', name);
189
+ const url = p.str('url', { maxLen: 2048 });
190
+ if (url)
191
+ argv.push('--url', url);
192
+ const caps = p.strList('capabilities');
193
+ if (caps.length)
194
+ argv.push('--capabilities', caps.join(','));
195
+ return plan(p, 'network', argv);
196
+ },
197
+ },
198
+ {
199
+ name: 'bolloon_network_status',
200
+ title: '本机入网态',
201
+ description: '只读: DID / 已加入网络 / P2P 是否在线 / manifest / registry 状态。' +
202
+ '本机没入网时给 code=NETWORK_NOT_JOINED + next_action=rejoin_network (不是"网络上没人")。',
203
+ inputSchema: { type: 'object', additionalProperties: false, properties: { timeoutMs: TIMEOUT_PROP } },
204
+ build(args) {
205
+ const p = new Params(args, [TIMEOUT_KEY]);
206
+ return plan(p, 'network', ['status']);
207
+ },
208
+ },
209
+ {
210
+ name: 'bolloon_agent_register',
211
+ title: '声明能力 (注册服务)',
212
+ description: '把自己的一项能力登记进 registry (落 ~/.bolloon/agent-registry.json)。' +
213
+ '只写**服务声明**; 不碰钱包、不发付款、不伪造信誉。成功码 AGENT_REGISTERED。' +
214
+ '注意: manifest 是进程内结构 (agent-manifest-protocol 不落盘), register 不写 manifest。',
215
+ inputSchema: {
216
+ type: 'object',
217
+ additionalProperties: false,
218
+ required: ['capability'],
219
+ properties: {
220
+ capability: { type: 'string', description: '要声明的能力名 (如 research)' },
221
+ name: { type: 'string', description: '显示名 (默认 bolloon-agent)' },
222
+ price: { type: 'string', description: '单价 (十进制串, 如 "0.05")' },
223
+ per: { type: 'string', description: '计价单位 (默认 query)' },
224
+ description: { type: 'string', description: '服务说明' },
225
+ wallet: { type: 'string', description: '收款地址 (公开地址; 绝不是私钥)' },
226
+ endpoint: { type: 'string', description: '服务端点' },
227
+ timeoutMs: TIMEOUT_PROP,
228
+ },
229
+ },
230
+ build(args) {
231
+ const p = new Params(args, ['capability', 'name', 'price', 'per', 'description', 'wallet', 'endpoint', TIMEOUT_KEY]);
232
+ const cap = p.str('capability', { required: true, maxLen: 200 });
233
+ const argv = ['register'];
234
+ if (cap)
235
+ argv.push('--capability', cap);
236
+ const name = p.str('name', { maxLen: 200 });
237
+ if (name)
238
+ argv.push('--name', name);
239
+ const price = p.strNumber('price');
240
+ if (price)
241
+ argv.push('--price', price);
242
+ const per = p.str('per', { maxLen: 40 });
243
+ if (per)
244
+ argv.push('--per', per);
245
+ const desc = p.str('description', { maxLen: 500 });
246
+ if (desc)
247
+ argv.push('--description', desc);
248
+ const wallet = p.str('wallet', { maxLen: 200 });
249
+ if (wallet)
250
+ argv.push('--wallet', wallet);
251
+ const endpoint = p.str('endpoint', { maxLen: 512 });
252
+ if (endpoint)
253
+ argv.push('--endpoint', endpoint);
254
+ // ★ 不接受任何私钥类入参 (钱包只收公开地址); 命中的键名一律在上面白名单外被拒
255
+ return plan(p, 'agent', argv);
256
+ },
257
+ },
258
+ {
259
+ name: 'bolloon_agent_discover',
260
+ title: '按能力发现服务',
261
+ description: '只读: 在 registry 里按能力/关键字找服务 (OrbitDB 优先, 离线回退本地文件)。' +
262
+ '找不到给 code=CAPABILITY_NOT_FOUND + next_action=redefine_capability; ' +
263
+ '发现是只读的 (重试安全), **绝不因为找不到就发起付款**。',
264
+ inputSchema: {
265
+ type: 'object',
266
+ additionalProperties: false,
267
+ required: ['query'],
268
+ properties: { query: { type: 'string', description: '能力名或关键字' }, timeoutMs: TIMEOUT_PROP },
269
+ },
270
+ build(args) {
271
+ const p = new Params(args, ['query', TIMEOUT_KEY]);
272
+ const q = p.str('query', { required: true, maxLen: 200 });
273
+ return plan(p, 'agent', q ? ['discover', q] : ['discover']);
274
+ },
275
+ },
276
+ {
277
+ name: 'bolloon_agent_manifest',
278
+ title: '本进程 manifest',
279
+ description: '只读: 本进程的 manifest (agents/capabilities) + 远端 manifest 缓存数。' +
280
+ '如实标注 scope=in-process / persisted=false —— 一次性 stdio 进程里读到空是正常的, 不等于本机没注册过。',
281
+ inputSchema: { type: 'object', additionalProperties: false, properties: { timeoutMs: TIMEOUT_PROP } },
282
+ build(args) {
283
+ const p = new Params(args, [TIMEOUT_KEY]);
284
+ return plan(p, 'agent', ['manifest']);
285
+ },
286
+ },
287
+ {
288
+ name: 'bolloon_task_run',
289
+ title: '跑一个任务 (M1 唯一入口)',
290
+ description: '买能力 + 执行 + 报告卡 (与 `bolloon task "<任务>" --budget 0.05` 同一条服务路径 task-runner.runTask)。' +
291
+ '预算硬上限由服务层钳制 (budget ≤ 0.05 / perPurchase ≤ 0.02 / daily ≤ 0.10), MCP 层不放大也不绕过。' +
292
+ '★ local-dev 最多给 TASK_COMPLETED + next_action=verify_result —— 链上没动钱, **绝不说成 TASK_VERIFIED**。' +
293
+ '同一 requestId 重发是幂等的 (不会产生第二笔付款)。',
294
+ inputSchema: {
295
+ type: 'object',
296
+ additionalProperties: false,
297
+ required: ['task'],
298
+ properties: {
299
+ task: { type: 'string', description: '任务正文 (私有, 不进公开投影)' },
300
+ budget: { type: 'string', description: '总预算 (十进制串, 服务层上限 0.05)' },
301
+ perPurchase: { type: 'string', description: '单笔上限 (默认上限 0.02)' },
302
+ daily: { type: 'string', description: '日上限 (默认上限 0.10)' },
303
+ requestId: { type: 'string', description: '幂等键 (不给就由契约层确定性派生)' },
304
+ input: { type: 'object', description: '任务输入 (JSON 对象)' },
305
+ timeoutMs: TIMEOUT_PROP,
306
+ },
307
+ },
308
+ build(args) {
309
+ const p = new Params(args, ['task', 'budget', 'perPurchase', 'daily', 'requestId', 'input', TIMEOUT_KEY]);
310
+ const task = p.str('task', { required: true, maxLen: MAX_LEN });
311
+ const argv = ['run'];
312
+ if (task)
313
+ argv.push(task);
314
+ const budget = p.strNumber('budget');
315
+ if (budget)
316
+ argv.push('--budget', budget);
317
+ const per = p.strNumber('perPurchase');
318
+ if (per)
319
+ argv.push('--per-purchase', per);
320
+ const daily = p.strNumber('daily');
321
+ if (daily)
322
+ argv.push('--daily', daily);
323
+ const input = p.obj('input');
324
+ if (input !== undefined)
325
+ argv.push('--input', JSON.stringify(input));
326
+ return plan(p, 'task', argv);
327
+ },
328
+ },
329
+ {
330
+ name: 'bolloon_task_list',
331
+ title: '本地任务/交易一览',
332
+ description: '只读: 本地交易记录 + Goal 一览 (只给 id/状态/结算事实, **不含任务正文**)。' +
333
+ '每条都带 `settlement` 口径 (chain / local-dev / none) 与 `success` (§7.1 判据)。',
334
+ inputSchema: {
335
+ type: 'object',
336
+ additionalProperties: false,
337
+ properties: { requestId: { type: 'string', description: '只看这个幂等键的交易' }, timeoutMs: TIMEOUT_PROP },
338
+ },
339
+ build(args) {
340
+ const p = new Params(args, ['requestId', TIMEOUT_KEY]);
341
+ return plan(p, 'task', ['list']);
342
+ },
343
+ },
344
+ {
345
+ name: 'bolloon_task_status',
346
+ title: '任务/交易状态',
347
+ description: '只读: 按 transactionId / requestId / goalId 查状态。成功判据 = status=verified ∧ 支付事实 ∈ {fully_settled, payment_verified}。' +
348
+ '`paying` + 事实未定 → code=PAYMENT_PENDING + next_action=reconcile (**先对账, 绝不重付**)。' +
349
+ 'local-dev 记录出现在 verified → 如实报 LOCAL_DEV_NOT_CHAIN (不顺着说成功)。',
350
+ inputSchema: {
351
+ type: 'object',
352
+ additionalProperties: false,
353
+ required: ['id'],
354
+ properties: { id: { type: 'string', description: 'transactionId / requestId / goalId' }, timeoutMs: TIMEOUT_PROP },
355
+ },
356
+ build(args) {
357
+ const p = new Params(args, ['id', TIMEOUT_KEY]);
358
+ const id = p.str('id', { required: true, maxLen: 512 });
359
+ return plan(p, 'task', id ? ['status', id] : ['status']);
360
+ },
361
+ },
362
+ {
363
+ name: 'bolloon_task_result',
364
+ title: '交付证据 (不输出正文)',
365
+ description: '只读: 交付是否在盘 / 字节数 / 各类哈希 / 验真分档。' +
366
+ '★ **交付正文绝不出本机** (P1 §5.6 私有层): 只给哈希与在盘证明 (`bodyPrinted:false`)。',
367
+ inputSchema: {
368
+ type: 'object',
369
+ additionalProperties: false,
370
+ required: ['id'],
371
+ properties: { id: { type: 'string', description: 'transactionId (或 requestId)' }, timeoutMs: TIMEOUT_PROP },
372
+ },
373
+ build(args) {
374
+ const p = new Params(args, ['id', TIMEOUT_KEY]);
375
+ const id = p.str('id', { required: true, maxLen: 512 });
376
+ return plan(p, 'task', id ? ['result', id] : ['result']);
377
+ },
378
+ },
379
+ {
380
+ name: 'bolloon_task_retry',
381
+ title: '恢复计划 (不发付款)',
382
+ description: '★ **只出计划, 绝不代付款**: 走 `x402/payment-recovery.planTransactionRecovery` (唯一决策点), 返回 `paid:false` + `mustNotRepay`。' +
383
+ '付款不确定时 next_action=reconcile —— 顺序永远是"先对账, 再决定 retry"。' +
384
+ '真正付款只能由**持钱包的一方**走同一 requestId 的幂等路径, MCP 层不持有也不调用支付能力。',
385
+ inputSchema: {
386
+ type: 'object',
387
+ additionalProperties: false,
388
+ required: ['id'],
389
+ properties: { id: { type: 'string', description: 'transactionId / requestId / goalId' }, timeoutMs: TIMEOUT_PROP },
390
+ },
391
+ build(args) {
392
+ const p = new Params(args, ['id', TIMEOUT_KEY]);
393
+ const id = p.str('id', { required: true, maxLen: 512 });
394
+ return plan(p, 'task', id ? ['retry', id] : ['retry']);
395
+ },
396
+ },
397
+ {
398
+ name: 'bolloon_wallet_status',
399
+ title: '钱包可用性 + 策略 (只读)',
400
+ description: '只读: 钱包是否存在 (地址是公开的收款地址) / 单笔与日限额 / 白名单 / 最近签名审计摘要。' +
401
+ '★ **绝不返回私钥、助记词、seed**; 本 tool 也不创建钱包 (不为了看一眼状态就生成新钱包)。' +
402
+ '模型侧改策略的能力**没有**通过 MCP 暴露 (见 §NOT_EXPOSED)。',
403
+ inputSchema: { type: 'object', additionalProperties: false, properties: { timeoutMs: TIMEOUT_PROP } },
404
+ build(args) {
405
+ const p = new Params(args, [TIMEOUT_KEY]);
406
+ return plan(p, 'wallet', ['status']);
407
+ },
408
+ },
409
+ {
410
+ name: 'bolloon_payment_pending',
411
+ title: '待放行的付款',
412
+ description: '只读: 列出在等你放行的付款请求 (~/.bolloon/payment-approvals.json)。' +
413
+ '有 pending 时 next_action=approve_payment —— 这是**正确表达**, 不是错误也不是重试信号。',
414
+ inputSchema: { type: 'object', additionalProperties: false, properties: { timeoutMs: TIMEOUT_PROP } },
415
+ build(args) {
416
+ const p = new Params(args, [TIMEOUT_KEY]);
417
+ return plan(p, 'payment', ['pending']);
418
+ },
419
+ },
420
+ {
421
+ name: 'bolloon_payment_approve',
422
+ title: '批准一笔待放行付款 (不发付款)',
423
+ description: '★ **只改审批状态, 不发起付款** (CLI/MCP 都不持有钱包; 真正支付由持钱包的一方/executor 执行, 返回里 `paid` 字段如实反映)。' +
424
+ '批准**不能**绕过 payment policy: 放行闸 `authorizeWalletSignature` (9 项, fail-closed) 仍在支付路径上, 这里碰不到它。' +
425
+ '无用户授权时 MCP **不许**切自主支付 —— 本 tool 没有"改模式/授权"的参数, 这是刻意的。',
426
+ inputSchema: {
427
+ type: 'object',
428
+ additionalProperties: false,
429
+ required: ['approvalId'],
430
+ properties: { approvalId: { type: 'string', description: '审批 id (来自 bolloon_payment_pending)' }, timeoutMs: TIMEOUT_PROP },
431
+ },
432
+ build(args) {
433
+ const p = new Params(args, ['approvalId', TIMEOUT_KEY]);
434
+ const id = p.str('approvalId', { required: true, maxLen: 512 });
435
+ return plan(p, 'payment', id ? ['approve', id] : ['approve']);
436
+ },
437
+ },
438
+ {
439
+ name: 'bolloon_payment_reject',
440
+ title: '拒绝一笔待放行付款',
441
+ description: '只改审批状态为拒绝; 拒绝后不会付款, 同一 requestId 的幂等保护仍在。',
442
+ inputSchema: {
443
+ type: 'object',
444
+ additionalProperties: false,
445
+ required: ['approvalId'],
446
+ properties: { approvalId: { type: 'string', description: '审批 id' }, timeoutMs: TIMEOUT_PROP },
447
+ },
448
+ build(args) {
449
+ const p = new Params(args, ['approvalId', TIMEOUT_KEY]);
450
+ const id = p.str('approvalId', { required: true, maxLen: 512 });
451
+ return plan(p, 'payment', id ? ['reject', id] : ['reject']);
452
+ },
453
+ },
454
+ {
455
+ name: 'bolloon_trade_list',
456
+ title: '交易一览',
457
+ description: '只读: 本地交易记录一览 (lifecycle + settlementFact + settlement 口径 + 事件数), 按口径分开写 (chain / local-dev / none)。',
458
+ inputSchema: {
459
+ type: 'object',
460
+ additionalProperties: false,
461
+ properties: { requestId: { type: 'string', description: '只看这个幂等键的交易' }, timeoutMs: TIMEOUT_PROP },
462
+ },
463
+ build(args) {
464
+ const p = new Params(args, ['requestId', TIMEOUT_KEY]);
465
+ return plan(p, 'trade', ['list']);
466
+ },
467
+ },
468
+ {
469
+ name: 'bolloon_trade_show',
470
+ title: '单笔交易原文',
471
+ description: '只读: 单笔交易记录 (状态 + 结算事实 + 哈希 + 事件链 + 责任候选 + 争议), **只读不写, 绝不修改交易历史**。' +
472
+ '付款回执原文属私有层 → 只给 `paymentReceiptPresent: boolean`, 原文不出本机。',
473
+ inputSchema: {
474
+ type: 'object',
475
+ additionalProperties: false,
476
+ required: ['id'],
477
+ properties: { id: { type: 'string', description: 'transactionId (或 requestId)' }, timeoutMs: TIMEOUT_PROP },
478
+ },
479
+ build(args) {
480
+ const p = new Params(args, ['id', TIMEOUT_KEY]);
481
+ const id = p.str('id', { required: true, maxLen: 512 });
482
+ return plan(p, 'trade', id ? ['show', id] : ['show']);
483
+ },
484
+ },
485
+ {
486
+ name: 'bolloon_trade_reconcile',
487
+ title: '单笔对账 / 恢复计划 (只读, 不发付款)',
488
+ description: '给 transactionId → 单笔恢复计划 (`planTransactionRecovery`, 唯一决策点, **只读**, `paid:false`)。' +
489
+ '★ 无 id 的**全局对账**不在 MCP 暴露: 那份实现 (`reconcilePendingTransactions`) 会钉结算事实/改状态/清死锁 —— ' +
490
+ '那是本机运维动作 (`bolloon trade reconcile`), 不是给远端 Agent 的。' +
491
+ '★ 顺序铁律: 先 reconcile, 再决定 retry。',
492
+ inputSchema: {
493
+ type: 'object',
494
+ additionalProperties: false,
495
+ required: ['transactionId'],
496
+ properties: { transactionId: { type: 'string', description: '要出恢复计划的那笔交易' }, timeoutMs: TIMEOUT_PROP },
497
+ },
498
+ build(args) {
499
+ const p = new Params(args, ['transactionId', TIMEOUT_KEY]);
500
+ const id = p.str('transactionId', { required: true, maxLen: 512 });
501
+ return plan(p, 'trade', id ? ['reconcile', id] : ['reconcile']);
502
+ },
503
+ },
504
+ ];
505
+ /** tool 名 → 定义 */
506
+ export const TOOL_BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
507
+ /**
508
+ * **刻意不暴露**的 P3 子命令 (暴露 = 必须在 MCP 层假装成功, 违反"失败不得变成功"):
509
+ * task complete · task cancel → P3 里如实报 `C_NOT_IMPLEMENTED` (没有可写的任务状态存储/交易层无 cancelled)
510
+ * task send · task inbox · task accept · task reject → **写操作** (DID 签名 + 落本机台账 + 对外发帧),
511
+ * 不在本轮的 17 个冻结清单里; 要纳入 MCP 必须单独裁决 (它会改变本机对外承诺, 不是只读能力)
512
+ * network leave → 仓库里不存在 leaveNetwork
513
+ * network init / peers · agent inspect · trade events · wallet policy(只读) / set-policy
514
+ * → 前几个是"本机节点生命周期/本地诊断", 不该给远端 Agent 调;
515
+ * **wallet set-policy 尤其不暴露** —— 让远端 Agent 改支付策略 = 绕过 payment policy (六条禁止之一)。
516
+ */
517
+ export const NOT_EXPOSED = [
518
+ { command: 'bolloon task complete|cancel', reason: 'P3 如实报 C_NOT_IMPLEMENTED (没有可写的任务状态存储 / 交易层无 cancelled); 暴露会逼 MCP 层假装成功' },
519
+ { command: 'bolloon task send|inbox|accept|reject', reason: '写操作 (签名 + 落本机台账 + 对外发帧), 不在 P4 的 17 个冻结清单里; 纳入 MCP 需单独裁决' },
520
+ { command: 'bolloon network leave', reason: '仓库没有 leaveNetwork; P3 报 C_NOT_IMPLEMENTED' },
521
+ { command: 'bolloon trade reconcile (无 id 的全局对账)', reason: '会钉结算事实/改状态/清死锁 = 写交易记录; 属本机运维 (bolloon trade reconcile), MCP 只给单笔只读计划' },
522
+ { command: 'bolloon network init|peers', reason: '本机节点生命周期 / 本进程 peer 列表, 不是给远端 Agent 的能力' },
523
+ { command: 'bolloon wallet set-policy', reason: '远端改支付策略 = 绕过 payment policy (六条禁止); 必须由本机用户执行' },
524
+ { command: 'bolloon agent inspect / trade events', reason: '诊断/审计视角; 等有明确外部需求再按同一薄包装方式加' },
525
+ ];
526
+ /** 一个"信封资源": 内容 = P3 信封原样 JSON (含 ok/code/next_action, 绝不改写成 MCP 成功) */
527
+ async function envelopeResource(uri, group, argv, timeoutMs) {
528
+ const env = await callP3({ group, argv, opts: timeoutMs ? { timeoutMs } : undefined });
529
+ return { uri, mimeType: 'application/json', text: JSON.stringify(env, null, 2), ok: env.ok };
530
+ }
531
+ /** `bolloon://network/capabilities`: 由**两份 P3 信封**拼成的投影 (组合, 不是重实现) */
532
+ async function capabilitiesPayload() {
533
+ const [status, manifest] = await Promise.all([
534
+ callP3({ group: 'network', argv: ['status'] }),
535
+ callP3({ group: 'agent', argv: ['manifest'] }),
536
+ ]);
537
+ const data = (status.data || {});
538
+ const mdata = (manifest.data || {});
539
+ const gatewayCaps = Array.isArray(data.gatewayJoin?.capabilities) ? data.gatewayJoin.capabilities : [];
540
+ const localCaps = Array.isArray(mdata.agents)
541
+ ? Array.from(new Set(mdata.agents.flatMap((a) => (a.capabilities || []).map(String))))
542
+ : [];
543
+ const env = {
544
+ ok: status.ok && manifest.ok,
545
+ code: status.ok && manifest.ok ? 'OK' : (status.code === 'NETWORK_NOT_JOINED' ? 'NETWORK_NOT_JOINED' : status.code),
546
+ message: `本机能力投影: 本地 manifest ${localCaps.length} 项, 网络声明 ${gatewayCaps.length} 项 (registry ${data.registry?.ready ? '就绪' : '未就绪'}, ${data.registry?.services ?? 0} 条服务)`,
547
+ data: {
548
+ composed: true,
549
+ localCapabilities: localCaps,
550
+ gatewayCapabilities: gatewayCaps,
551
+ registry: data.registry || null,
552
+ joinedNetworks: Array.isArray(data.joinedNetworks) ? data.joinedNetworks.map((n) => ({ name: n.name, networkId: n.networkId, serviceCount: n.serviceCount })) : [],
553
+ sources: ['bolloon network status', 'bolloon agent manifest'],
554
+ note: '这是两份 P3 信封的投影 (只做组合, 没有重算任何业务事实); 具体服务声明请用 bolloon_agent_discover',
555
+ },
556
+ evidence: Array.from(new Set([...(status.evidence || []), ...(manifest.evidence || [])].map(String))),
557
+ next_action: status.next_action ?? null,
558
+ };
559
+ return { uri: 'bolloon://network/capabilities', mimeType: 'application/json', text: JSON.stringify(env, null, 2), ok: env.ok };
560
+ }
561
+ /** `bolloon://skill/current`: 指向 `skills/bolloon-network/SKILL.md` (对外唯一入口说明) */
562
+ function skillCandidatePaths() {
563
+ const here = fileURLToPath(import.meta.url);
564
+ const out = [];
565
+ if (process.env.BOLLOON_SKILL_PATH)
566
+ out.push(process.env.BOLLOON_SKILL_PATH);
567
+ for (const up of [['..', '..', '..'], ['..', '..'], ['..']]) {
568
+ out.push(path.resolve(path.dirname(here), ...up, 'skills', 'bolloon-network', 'SKILL.md'));
569
+ }
570
+ out.push(path.resolve(process.cwd(), 'skills', 'bolloon-network', 'SKILL.md'));
571
+ try {
572
+ out.push(path.resolve(currentPackageRoot(), 'skills', 'bolloon-network', 'SKILL.md'));
573
+ }
574
+ catch { /* 忽略 */ }
575
+ return out;
576
+ }
577
+ export async function skillPayload(uri) {
578
+ const p = skillCandidatePaths().find((f) => { try {
579
+ return fs.existsSync(f);
580
+ }
581
+ catch {
582
+ return false;
583
+ } });
584
+ if (!p) {
585
+ const env = failEnvelope('NOT_FOUND', '找不到 skills/bolloon-network/SKILL.md (可用 BOLLOON_SKILL_PATH 指定)', { searched: skillCandidatePaths() }, [], 'needs_human');
586
+ return { uri, mimeType: 'application/json', text: JSON.stringify(env, null, 2), ok: false };
587
+ }
588
+ const text = fs.readFileSync(p, 'utf8');
589
+ return { uri, mimeType: 'text/markdown', text, ok: true };
590
+ }
591
+ export const RESOURCES = [
592
+ { uri: 'bolloon://network/status', name: '本机入网态', description: 'DID / 网络 / P2P / manifest / registry (bolloon network status 的信封原样)', mimeType: 'application/json', load: () => envelopeResource('bolloon://network/status', 'network', ['status']) },
593
+ { uri: 'bolloon://network/capabilities', name: '本机能力投影', description: '本地 manifest 能力 + 网络声明能力 + registry 状态 (两份 P3 信封的投影)', mimeType: 'application/json', load: () => capabilitiesPayload() },
594
+ { uri: 'bolloon://agent/manifest', name: '本进程 manifest', description: 'agents/capabilities (scope=in-process, persisted=false)', mimeType: 'application/json', load: () => envelopeResource('bolloon://agent/manifest', 'agent', ['manifest']) },
595
+ { uri: 'bolloon://tasks/recent', name: '最近任务/交易', description: '本地交易 + Goal (只给 id/状态/结算口径, 不含任务正文)', mimeType: 'application/json', load: () => envelopeResource('bolloon://tasks/recent', 'task', ['list']) },
596
+ { uri: 'bolloon://trades/recent', name: '最近交易', description: '交易一览 (chain / local-dev / none 口径分开写)', mimeType: 'application/json', load: () => envelopeResource('bolloon://trades/recent', 'trade', ['list']) },
597
+ { uri: 'bolloon://wallet/policy', name: '支付策略', description: '单笔/日限额 + 白名单 + 速率 + 今日已用 (只读, 无私钥)', mimeType: 'application/json', load: () => envelopeResource('bolloon://wallet/policy', 'wallet', ['policy']) },
598
+ { uri: 'bolloon://skill/current', name: 'bolloon-network Skill', description: 'skills/bolloon-network/SKILL.md 原文 (对外唯一入口说明)', mimeType: 'text/markdown', load: () => skillPayload('bolloon://skill/current') },
599
+ ];
600
+ export const RESOURCE_BY_URI = new Map(RESOURCES.map((r) => [r.uri, r]));
601
+ /** MCP `tools/list` 的返回体 (不带内部 build 函数) */
602
+ export function listToolsPayload() {
603
+ return TOOLS.map((t) => ({
604
+ name: t.name,
605
+ description: `[${t.title}] ${t.description}`,
606
+ inputSchema: t.inputSchema,
607
+ }));
608
+ }
609
+ /** MCP `resources/list` 的返回体 */
610
+ export function listResourcesPayload() {
611
+ return RESOURCES.map((r) => ({ uri: r.uri, name: r.name, description: r.description, mimeType: r.mimeType }));
612
+ }