@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.
- package/dist/agents/agent-gateway.js +5 -0
- package/dist/agents/agent-manifest-protocol.js +29 -0
- package/dist/agents/chain/chain-config.js +504 -0
- package/dist/agents/chain/chain-index-query.js +135 -0
- package/dist/agents/chain/chain-indexer.js +1045 -0
- package/dist/agents/chain/chain-settlement.js +284 -0
- package/dist/agents/chain/chain-state-store.js +245 -0
- package/dist/agents/chain/chain-wallet.js +198 -0
- package/dist/agents/chain/escrow-client.js +433 -0
- package/dist/agents/chain/index.js +20 -0
- package/dist/agents/chain/onchain-trade.js +691 -0
- package/dist/agents/economic-policy.js +7 -0
- package/dist/agents/gateway-network.js +6 -0
- package/dist/agents/local-signer.js +59 -0
- package/dist/agents/network-pulse.js +912 -0
- package/dist/agents/task/task-onchain-runner.js +284 -0
- package/dist/agents/task-contract.js +322 -0
- package/dist/agents/task-inbox.js +267 -0
- package/dist/agents/task-transport.js +280 -0
- package/dist/agents/x402/paid-info-store.js +75 -6
- package/dist/agents/x402/transaction-store.js +8 -1
- package/dist/cli/commands/agent.js +230 -0
- package/dist/cli/commands/chain.js +926 -0
- package/dist/cli/commands/index.js +69 -0
- package/dist/cli/commands/mcp.js +98 -0
- package/dist/cli/commands/network.js +334 -0
- package/dist/cli/commands/payment.js +125 -0
- package/dist/cli/commands/tasks.js +1236 -0
- package/dist/cli/commands/trade.js +229 -0
- package/dist/cli/commands/wallet.js +450 -0
- package/dist/cli/mcp/bridge.js +105 -0
- package/dist/cli/mcp/server.js +188 -0
- package/dist/cli/mcp/tools.js +955 -0
- package/dist/cli/protocol-envelope.js +358 -0
- package/dist/cli-entry.js +146 -17
- package/dist/electron-build/agents/agent-gateway.js +5 -0
- package/dist/electron-build/agents/agent-gateway.js.map +1 -1
- package/dist/electron-build/agents/agent-manifest-protocol.js +62 -0
- package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
- package/dist/electron-build/agents/chain/chain-config.js +556 -0
- package/dist/electron-build/agents/chain/chain-config.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-index-query.js +145 -0
- package/dist/electron-build/agents/chain/chain-index-query.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-indexer.js +1095 -0
- package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-settlement.js +291 -0
- package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -0
- package/dist/electron-build/agents/chain/escrow-client.js +443 -0
- package/dist/electron-build/agents/chain/escrow-client.js.map +1 -0
- package/dist/electron-build/agents/economic-policy.js +7 -0
- package/dist/electron-build/agents/economic-policy.js.map +1 -1
- package/dist/electron-build/agents/gateway-network.js +6 -0
- package/dist/electron-build/agents/gateway-network.js.map +1 -1
- package/dist/electron-build/agents/network-pulse.js +979 -0
- package/dist/electron-build/agents/network-pulse.js.map +1 -0
- package/dist/electron-build/agents/x402/paid-info-store.js +76 -6
- package/dist/electron-build/agents/x402/paid-info-store.js.map +1 -1
- package/dist/electron-build/agents/x402/transaction-store.js +8 -1
- package/dist/electron-build/agents/x402/transaction-store.js.map +1 -1
- package/dist/web/chain-explorer.js +617 -0
- package/dist/web/explorer.html +163 -0
- package/dist/web/index.html +5 -0
- package/dist/web/routes-chain-index.js +108 -0
- package/dist/web/routes-onchain-trade.js +99 -0
- package/dist/web/server.js +42 -0
- package/dist/web/task-frame-server.js +211 -0
- package/package.json +2 -1
- package/scripts/build-web.ts +20 -1
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools.ts — MCP 的 tools / resources 定义 (17 个 P4 tool + 7 个 P6 链只读 tool + 3 个 P6b 链写 tool = 27; 7 + 3 = 10 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
|
+
* `chain trade expire`)
|
|
12
|
+
* **不暴露成 tool**: 暴露了就得在 MCP 层假装成功, 那正好违反"失败不得变成功"。
|
|
13
|
+
* 需要它们的能力时, MCP 侧应直接读 P3 的 `C_NOT_IMPLEMENTED` 说明 (见 §NOT_EXPOSED)。
|
|
14
|
+
* ⑤ ★ 链上**写** tool (`bolloon_chain_trade_create|submit_proof|release`) 是真签名 + 真移钱,
|
|
15
|
+
* 所以额外有两条: **授权意图参数必须显式携带** (缺 → `NOT_AUTHORIZED`, 见 `requireWriteIntent`),
|
|
16
|
+
* 以及**真签名只能由本机唯一放行闸 `authorizeWalletSignature` 产生** —— MCP 层不复制、不旁路。
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
import { failEnvelope } from '../protocol-envelope.js';
|
|
22
|
+
import { currentPackageRoot } from '../../utils/version-info.js';
|
|
23
|
+
import { callP3 } from './bridge.js';
|
|
24
|
+
// 授权意图词表**只有一份** (task-contract 的冻结口径) —— 这里 import 它, 不另抄一份
|
|
25
|
+
import { PAYMENT_MODES, isPaymentMode } from '../../agents/task-contract.js';
|
|
26
|
+
// ── 入参校验 (唯一入口; 客户端**不能**直接给 argv) ────────────────────────────
|
|
27
|
+
const MAX_LEN = 4000;
|
|
28
|
+
class Params {
|
|
29
|
+
args;
|
|
30
|
+
allowed;
|
|
31
|
+
seen = new Set();
|
|
32
|
+
bad = [];
|
|
33
|
+
constructor(args, allowed) {
|
|
34
|
+
this.args = args;
|
|
35
|
+
this.allowed = allowed;
|
|
36
|
+
}
|
|
37
|
+
touch(key) {
|
|
38
|
+
this.seen.add(key);
|
|
39
|
+
}
|
|
40
|
+
str(key, opts = {}) {
|
|
41
|
+
this.touch(key);
|
|
42
|
+
const v = this.args[key];
|
|
43
|
+
if (v === undefined || v === null || v === '') {
|
|
44
|
+
if (opts.required)
|
|
45
|
+
this.bad.push(`缺少必填参数 '${key}'`);
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if (typeof v !== 'string') {
|
|
49
|
+
this.bad.push(`参数 '${key}' 必须是字符串 (收到 ${typeof v})`);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (v.startsWith('-')) {
|
|
53
|
+
this.bad.push(`参数 '${key}' 不许以 '-' 开头 (防"值当成命令选项"注入命令行)`);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (v.length > (opts.maxLen ?? MAX_LEN)) {
|
|
57
|
+
this.bad.push(`参数 '${key}' 太长 (> ${opts.maxLen ?? MAX_LEN} 字符)`);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return v;
|
|
61
|
+
}
|
|
62
|
+
/** 正数 (金额/限额这类: 只收**字符串**形式的十进制, 与 P3 的原子单位口径一致) */
|
|
63
|
+
strNumber(key, opts = {}) {
|
|
64
|
+
const v = this.str(key, opts);
|
|
65
|
+
if (v === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
if (!/^\d+(\.\d+)?$/.test(v)) {
|
|
68
|
+
this.bad.push(`参数 '${key}' 必须是正的十进制数字串 (如 "0.05"), 收到 ${JSON.stringify(v)}`);
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return v;
|
|
72
|
+
}
|
|
73
|
+
/** 整数毫秒 (tool 超时) */
|
|
74
|
+
intMs(key) {
|
|
75
|
+
this.touch(key);
|
|
76
|
+
const v = this.args[key];
|
|
77
|
+
if (v === undefined || v === null)
|
|
78
|
+
return undefined;
|
|
79
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0) {
|
|
80
|
+
this.bad.push(`参数 '${key}' 必须是正整数 (毫秒)`);
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return Math.floor(v);
|
|
84
|
+
}
|
|
85
|
+
/** 正整数 (秒/计数这类非毫秒的整数参数) */
|
|
86
|
+
intPos(key) {
|
|
87
|
+
this.touch(key);
|
|
88
|
+
const v = this.args[key];
|
|
89
|
+
if (v === undefined || v === null)
|
|
90
|
+
return undefined;
|
|
91
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v <= 0) {
|
|
92
|
+
this.bad.push(`参数 '${key}' 必须是正整数`);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return Math.floor(v);
|
|
96
|
+
}
|
|
97
|
+
bool(key) {
|
|
98
|
+
this.touch(key);
|
|
99
|
+
const v = this.args[key];
|
|
100
|
+
if (v === undefined || v === null)
|
|
101
|
+
return false;
|
|
102
|
+
if (typeof v !== 'boolean') {
|
|
103
|
+
this.bad.push(`参数 '${key}' 必须是布尔值`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return v;
|
|
107
|
+
}
|
|
108
|
+
/** 字符串数组 (也接受逗号分隔的单串) */
|
|
109
|
+
strList(key) {
|
|
110
|
+
this.touch(key);
|
|
111
|
+
const v = this.args[key];
|
|
112
|
+
if (v === undefined || v === null || v === '')
|
|
113
|
+
return [];
|
|
114
|
+
const list = Array.isArray(v) ? v : (typeof v === 'string' ? v.split(',') : null);
|
|
115
|
+
if (!list) {
|
|
116
|
+
this.bad.push(`参数 '${key}' 必须是字符串数组或逗号分隔的字符串`);
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const out = [];
|
|
120
|
+
for (const item of list) {
|
|
121
|
+
if (typeof item !== 'string' || !item.trim()) {
|
|
122
|
+
this.bad.push(`参数 '${key}' 里每一项都必须是非空字符串`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const s = item.trim();
|
|
126
|
+
if (s.startsWith('-')) {
|
|
127
|
+
this.bad.push(`参数 '${key}' 的值不许以 '-' 开头`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
out.push(s);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/** 任意 JSON 对象 (task run 的 --input) */
|
|
135
|
+
obj(key) {
|
|
136
|
+
this.touch(key);
|
|
137
|
+
const v = this.args[key];
|
|
138
|
+
if (v === undefined || v === null)
|
|
139
|
+
return undefined;
|
|
140
|
+
if (typeof v !== 'object') {
|
|
141
|
+
this.bad.push(`参数 '${key}' 必须是 JSON 对象/数组`);
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
return v;
|
|
145
|
+
}
|
|
146
|
+
/** 白名单之外的一律拒 (这是"不能注入选项"的机制保证) */
|
|
147
|
+
finish() {
|
|
148
|
+
const unknown = Object.keys(this.args).filter((k) => !this.allowed.includes(k));
|
|
149
|
+
if (unknown.length)
|
|
150
|
+
this.bad.push(`不认识的参数: ${unknown.join(', ')} (只接受: ${this.allowed.join(', ')})`);
|
|
151
|
+
if (!this.bad.length)
|
|
152
|
+
return null;
|
|
153
|
+
return failEnvelope('INVALID_ARGUMENT', `MCP tool 入参不合法: ${this.bad.join('; ')}`, { issues: this.bad, acceptedParams: [...this.allowed] }, [], 'needs_human');
|
|
154
|
+
}
|
|
155
|
+
/** 全局项 (超时 / 幂等键) —— 幂等键只在该 tool 声明了 `requestId` 时才透传 */
|
|
156
|
+
opts() {
|
|
157
|
+
const o = {};
|
|
158
|
+
if (this.allowed.includes('requestId')) {
|
|
159
|
+
const rid = this.str('requestId', { maxLen: 512 });
|
|
160
|
+
if (rid)
|
|
161
|
+
o.requestId = rid;
|
|
162
|
+
}
|
|
163
|
+
const t = this.intMs(TIMEOUT_KEY);
|
|
164
|
+
if (t)
|
|
165
|
+
o.timeoutMs = t;
|
|
166
|
+
return o;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** 统一收尾: 校验通过 → plan; 不通过 → INVALID_ARGUMENT 信封 (不抛异常, 不给半成品) */
|
|
170
|
+
function plan(p, group, argv) {
|
|
171
|
+
const opts = p.opts(); // 先解析全局项 (类型错与其它问题一起报)
|
|
172
|
+
const err = p.finish();
|
|
173
|
+
if (err)
|
|
174
|
+
return { ok: false, envelope: err };
|
|
175
|
+
return { ok: true, plan: Object.keys(opts).length ? { group, argv, opts } : { group, argv } };
|
|
176
|
+
}
|
|
177
|
+
const TIMEOUT_KEY = 'timeoutMs';
|
|
178
|
+
const TIMEOUT_PROP = { type: 'integer', minimum: 1, description: '本次调用的硬超时 (毫秒); 超时返回 P3 信封 code=TIMEOUT (不是"打印一行错误就退出")' };
|
|
179
|
+
// ── ★ 链上**写** tool 的授权意图 (硬要求 ②) ──────────────────────────────────
|
|
180
|
+
const PAYMENT_MODE_LIST = PAYMENT_MODES.join(' | ');
|
|
181
|
+
/**
|
|
182
|
+
* 写 tool (`bolloon_chain_trade_create|submit_proof|release`) 的**授权意图声明**校验。
|
|
183
|
+
*
|
|
184
|
+
* 硬要求: 调用方必须**显式**给出 `paymentMode` **与** `requestId`
|
|
185
|
+
* · 缺任何一个 → `NOT_AUTHORIZED` (fail-closed, **绝不默认放行**, 也不替调用方猜)
|
|
186
|
+
* · 给了但值非法 → `INVALID_ARGUMENT`
|
|
187
|
+
*
|
|
188
|
+
* ★ 为什么这不等于"在 MCP 层另开一道闸": 这里只校验"声明齐不齐 / 词表合不合法",
|
|
189
|
+
* **不判**"能不能签"。`paymentMode` 只被翻译成 CLI 的 `--payment-mode`, 真正放不放行
|
|
190
|
+
* 仍由本机唯一放行闸 `authorizeWalletSignature` (fail-closed) 决定 ——
|
|
191
|
+
* 它的 `modeIsAutonomous` 只认 `autonomous` / `agent-authorized`, 所以声明
|
|
192
|
+
* `manual` / `policy` 会被闸直接拒。即: 声明**只能收紧, 不可能放权**。
|
|
193
|
+
*/
|
|
194
|
+
function requireWriteIntent(args) {
|
|
195
|
+
const mode = args.paymentMode;
|
|
196
|
+
const rid = args.requestId;
|
|
197
|
+
const missing = [];
|
|
198
|
+
if (mode === undefined || mode === null || mode === '')
|
|
199
|
+
missing.push('paymentMode');
|
|
200
|
+
if (rid === undefined || rid === null || rid === '')
|
|
201
|
+
missing.push('requestId');
|
|
202
|
+
if (missing.length) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
envelope: failEnvelope('NOT_AUTHORIZED', `链上写操作必须显式声明授权意图, 缺: ${missing.join(' + ')}。MCP 层 fail-closed —— 声明缺失不默认放行。`, {
|
|
206
|
+
missing,
|
|
207
|
+
requiresExplicitAuthorization: true,
|
|
208
|
+
acceptedPaymentModes: [...PAYMENT_MODES],
|
|
209
|
+
why: '链上写 = 真签名 + 真移钱: 调用方必须先自己确认本机钱包已授权按该意图签名; MCP 不代你假定已授权',
|
|
210
|
+
}, [], 'needs_human'),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (typeof mode !== 'string' || !isPaymentMode(mode.trim())) {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
envelope: failEnvelope('INVALID_ARGUMENT', `paymentMode 非法: ${JSON.stringify(mode).slice(0, 60)} (要 ${PAYMENT_MODE_LIST})`, { paymentMode: typeof mode === 'string' ? mode.slice(0, 60) : typeof mode, accepted: [...PAYMENT_MODES] }, [], 'needs_human'),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return { ok: true };
|
|
220
|
+
}
|
|
221
|
+
/** 3 个链上写 tool 共用的参数说明 (口径一致, 不各写一套) */
|
|
222
|
+
const WRITE_INTENT_PROPS = {
|
|
223
|
+
paymentMode: { type: 'string', enum: [...PAYMENT_MODES], description: `★ 必填: 授权意图 —— 声明按哪种支付模式签这笔链上写 (${PAYMENT_MODE_LIST})。缺它 → NOT_AUTHORIZED (fail-closed)。manual/policy 会被本机放行闸直接拒` },
|
|
224
|
+
requestId: { type: 'string', description: '★ 必填: 显式幂等/授权键 (本次声明的 id)。它参与放行闸 requestId 的确定性派生: 同一个 requestId 重复声明 → 闸按 notDuplicate 拒 (同一次意图只签一次)' },
|
|
225
|
+
};
|
|
226
|
+
// ── 17 个 tool ──────────────────────────────────────────────────────────────
|
|
227
|
+
export const TOOLS = [
|
|
228
|
+
{
|
|
229
|
+
name: 'bolloon_network_join',
|
|
230
|
+
title: '加入 Bolloon 网络',
|
|
231
|
+
description: '加入网络 (幂等)。给 link 就按链接加入 (orbitdb:// / ipns:// / https://.../registry); 不给 link 走文档驱动的全球入网。' +
|
|
232
|
+
'返回 P3 信封: code=NETWORK_JOINED 才是真加入; ok:false 一律别当成功读。' +
|
|
233
|
+
'硬约束: 本 tool 只调 `bolloon network join` 服务层, 不复制逻辑; 私钥永不参与, 也不返回。',
|
|
234
|
+
inputSchema: {
|
|
235
|
+
type: 'object',
|
|
236
|
+
additionalProperties: false,
|
|
237
|
+
properties: {
|
|
238
|
+
link: { type: 'string', description: '入网链接 (不给则走全球入网文档)' },
|
|
239
|
+
name: { type: 'string', description: '本节点在网络里的名字' },
|
|
240
|
+
url: { type: 'string', description: '覆盖入网文档地址' },
|
|
241
|
+
capabilities: { type: 'array', items: { type: 'string' }, description: '要声明的能力 (如 ["research","data"])' },
|
|
242
|
+
timeoutMs: TIMEOUT_PROP,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
build(args) {
|
|
246
|
+
const p = new Params(args, ['link', 'name', 'url', 'capabilities', TIMEOUT_KEY]);
|
|
247
|
+
const argv = ['join'];
|
|
248
|
+
const link = p.str('link', { maxLen: 2048 });
|
|
249
|
+
if (link)
|
|
250
|
+
argv.push(link);
|
|
251
|
+
const name = p.str('name', { maxLen: 200 });
|
|
252
|
+
if (name)
|
|
253
|
+
argv.push('--name', name);
|
|
254
|
+
const url = p.str('url', { maxLen: 2048 });
|
|
255
|
+
if (url)
|
|
256
|
+
argv.push('--url', url);
|
|
257
|
+
const caps = p.strList('capabilities');
|
|
258
|
+
if (caps.length)
|
|
259
|
+
argv.push('--capabilities', caps.join(','));
|
|
260
|
+
return plan(p, 'network', argv);
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: 'bolloon_network_status',
|
|
265
|
+
title: '本机入网态',
|
|
266
|
+
description: '只读: DID / 已加入网络 / P2P 是否在线 / manifest / registry 状态。' +
|
|
267
|
+
'本机没入网时给 code=NETWORK_NOT_JOINED + next_action=rejoin_network (不是"网络上没人")。',
|
|
268
|
+
inputSchema: { type: 'object', additionalProperties: false, properties: { timeoutMs: TIMEOUT_PROP } },
|
|
269
|
+
build(args) {
|
|
270
|
+
const p = new Params(args, [TIMEOUT_KEY]);
|
|
271
|
+
return plan(p, 'network', ['status']);
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: 'bolloon_agent_register',
|
|
276
|
+
title: '声明能力 (注册服务)',
|
|
277
|
+
description: '把自己的一项能力登记进 registry (落 ~/.bolloon/agent-registry.json)。' +
|
|
278
|
+
'只写**服务声明**; 不碰钱包、不发付款、不伪造信誉。成功码 AGENT_REGISTERED。' +
|
|
279
|
+
'注意: manifest 是进程内结构 (agent-manifest-protocol 不落盘), register 不写 manifest。',
|
|
280
|
+
inputSchema: {
|
|
281
|
+
type: 'object',
|
|
282
|
+
additionalProperties: false,
|
|
283
|
+
required: ['capability'],
|
|
284
|
+
properties: {
|
|
285
|
+
capability: { type: 'string', description: '要声明的能力名 (如 research)' },
|
|
286
|
+
name: { type: 'string', description: '显示名 (默认 bolloon-agent)' },
|
|
287
|
+
price: { type: 'string', description: '单价 (十进制串, 如 "0.05")' },
|
|
288
|
+
per: { type: 'string', description: '计价单位 (默认 query)' },
|
|
289
|
+
description: { type: 'string', description: '服务说明' },
|
|
290
|
+
wallet: { type: 'string', description: '收款地址 (公开地址; 绝不是私钥)' },
|
|
291
|
+
endpoint: { type: 'string', description: '服务端点' },
|
|
292
|
+
timeoutMs: TIMEOUT_PROP,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
build(args) {
|
|
296
|
+
const p = new Params(args, ['capability', 'name', 'price', 'per', 'description', 'wallet', 'endpoint', TIMEOUT_KEY]);
|
|
297
|
+
const cap = p.str('capability', { required: true, maxLen: 200 });
|
|
298
|
+
const argv = ['register'];
|
|
299
|
+
if (cap)
|
|
300
|
+
argv.push('--capability', cap);
|
|
301
|
+
const name = p.str('name', { maxLen: 200 });
|
|
302
|
+
if (name)
|
|
303
|
+
argv.push('--name', name);
|
|
304
|
+
const price = p.strNumber('price');
|
|
305
|
+
if (price)
|
|
306
|
+
argv.push('--price', price);
|
|
307
|
+
const per = p.str('per', { maxLen: 40 });
|
|
308
|
+
if (per)
|
|
309
|
+
argv.push('--per', per);
|
|
310
|
+
const desc = p.str('description', { maxLen: 500 });
|
|
311
|
+
if (desc)
|
|
312
|
+
argv.push('--description', desc);
|
|
313
|
+
const wallet = p.str('wallet', { maxLen: 200 });
|
|
314
|
+
if (wallet)
|
|
315
|
+
argv.push('--wallet', wallet);
|
|
316
|
+
const endpoint = p.str('endpoint', { maxLen: 512 });
|
|
317
|
+
if (endpoint)
|
|
318
|
+
argv.push('--endpoint', endpoint);
|
|
319
|
+
// ★ 不接受任何私钥类入参 (钱包只收公开地址); 命中的键名一律在上面白名单外被拒
|
|
320
|
+
return plan(p, 'agent', argv);
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: 'bolloon_agent_discover',
|
|
325
|
+
title: '按能力发现服务',
|
|
326
|
+
description: '只读: 在 registry 里按能力/关键字找服务 (OrbitDB 优先, 离线回退本地文件)。' +
|
|
327
|
+
'找不到给 code=CAPABILITY_NOT_FOUND + next_action=redefine_capability; ' +
|
|
328
|
+
'发现是只读的 (重试安全), **绝不因为找不到就发起付款**。',
|
|
329
|
+
inputSchema: {
|
|
330
|
+
type: 'object',
|
|
331
|
+
additionalProperties: false,
|
|
332
|
+
required: ['query'],
|
|
333
|
+
properties: { query: { type: 'string', description: '能力名或关键字' }, timeoutMs: TIMEOUT_PROP },
|
|
334
|
+
},
|
|
335
|
+
build(args) {
|
|
336
|
+
const p = new Params(args, ['query', TIMEOUT_KEY]);
|
|
337
|
+
const q = p.str('query', { required: true, maxLen: 200 });
|
|
338
|
+
return plan(p, 'agent', q ? ['discover', q] : ['discover']);
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: 'bolloon_agent_manifest',
|
|
343
|
+
title: '本进程 manifest',
|
|
344
|
+
description: '只读: 本进程的 manifest (agents/capabilities) + 远端 manifest 缓存数。' +
|
|
345
|
+
'如实标注 scope=in-process / persisted=false —— 一次性 stdio 进程里读到空是正常的, 不等于本机没注册过。',
|
|
346
|
+
inputSchema: { type: 'object', additionalProperties: false, properties: { timeoutMs: TIMEOUT_PROP } },
|
|
347
|
+
build(args) {
|
|
348
|
+
const p = new Params(args, [TIMEOUT_KEY]);
|
|
349
|
+
return plan(p, 'agent', ['manifest']);
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: 'bolloon_task_run',
|
|
354
|
+
title: '跑一个任务 (M1 唯一入口)',
|
|
355
|
+
description: '买能力 + 执行 + 报告卡 (与 `bolloon task "<任务>" --budget 0.05` 同一条服务路径 task-runner.runTask)。' +
|
|
356
|
+
'预算硬上限由服务层钳制 (budget ≤ 0.05 / perPurchase ≤ 0.02 / daily ≤ 0.10), MCP 层不放大也不绕过。' +
|
|
357
|
+
'★ local-dev 最多给 TASK_COMPLETED + next_action=verify_result —— 链上没动钱, **绝不说成 TASK_VERIFIED**。' +
|
|
358
|
+
'同一 requestId 重发是幂等的 (不会产生第二笔付款)。',
|
|
359
|
+
inputSchema: {
|
|
360
|
+
type: 'object',
|
|
361
|
+
additionalProperties: false,
|
|
362
|
+
required: ['task'],
|
|
363
|
+
properties: {
|
|
364
|
+
task: { type: 'string', description: '任务正文 (私有, 不进公开投影)' },
|
|
365
|
+
budget: { type: 'string', description: '总预算 (十进制串, 服务层上限 0.05)' },
|
|
366
|
+
perPurchase: { type: 'string', description: '单笔上限 (默认上限 0.02)' },
|
|
367
|
+
daily: { type: 'string', description: '日上限 (默认上限 0.10)' },
|
|
368
|
+
requestId: { type: 'string', description: '幂等键 (不给就由契约层确定性派生)' },
|
|
369
|
+
input: { type: 'object', description: '任务输入 (JSON 对象)' },
|
|
370
|
+
timeoutMs: TIMEOUT_PROP,
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
build(args) {
|
|
374
|
+
const p = new Params(args, ['task', 'budget', 'perPurchase', 'daily', 'requestId', 'input', TIMEOUT_KEY]);
|
|
375
|
+
const task = p.str('task', { required: true, maxLen: MAX_LEN });
|
|
376
|
+
const argv = ['run'];
|
|
377
|
+
if (task)
|
|
378
|
+
argv.push(task);
|
|
379
|
+
const budget = p.strNumber('budget');
|
|
380
|
+
if (budget)
|
|
381
|
+
argv.push('--budget', budget);
|
|
382
|
+
const per = p.strNumber('perPurchase');
|
|
383
|
+
if (per)
|
|
384
|
+
argv.push('--per-purchase', per);
|
|
385
|
+
const daily = p.strNumber('daily');
|
|
386
|
+
if (daily)
|
|
387
|
+
argv.push('--daily', daily);
|
|
388
|
+
const input = p.obj('input');
|
|
389
|
+
if (input !== undefined)
|
|
390
|
+
argv.push('--input', JSON.stringify(input));
|
|
391
|
+
return plan(p, 'task', argv);
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: 'bolloon_task_list',
|
|
396
|
+
title: '本地任务/交易一览',
|
|
397
|
+
description: '只读: 本地交易记录 + Goal 一览 (只给 id/状态/结算事实, **不含任务正文**)。' +
|
|
398
|
+
'每条都带 `settlement` 口径 (chain / local-dev / none) 与 `success` (§7.1 判据)。',
|
|
399
|
+
inputSchema: {
|
|
400
|
+
type: 'object',
|
|
401
|
+
additionalProperties: false,
|
|
402
|
+
properties: { requestId: { type: 'string', description: '只看这个幂等键的交易' }, timeoutMs: TIMEOUT_PROP },
|
|
403
|
+
},
|
|
404
|
+
build(args) {
|
|
405
|
+
const p = new Params(args, ['requestId', TIMEOUT_KEY]);
|
|
406
|
+
return plan(p, 'task', ['list']);
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
name: 'bolloon_task_status',
|
|
411
|
+
title: '任务/交易状态',
|
|
412
|
+
description: '只读: 按 transactionId / requestId / goalId 查状态。成功判据 = status=verified ∧ 支付事实 ∈ {fully_settled, payment_verified}。' +
|
|
413
|
+
'`paying` + 事实未定 → code=PAYMENT_PENDING + next_action=reconcile (**先对账, 绝不重付**)。' +
|
|
414
|
+
'local-dev 记录出现在 verified → 如实报 LOCAL_DEV_NOT_CHAIN (不顺着说成功)。',
|
|
415
|
+
inputSchema: {
|
|
416
|
+
type: 'object',
|
|
417
|
+
additionalProperties: false,
|
|
418
|
+
required: ['id'],
|
|
419
|
+
properties: { id: { type: 'string', description: 'transactionId / requestId / goalId' }, timeoutMs: TIMEOUT_PROP },
|
|
420
|
+
},
|
|
421
|
+
build(args) {
|
|
422
|
+
const p = new Params(args, ['id', TIMEOUT_KEY]);
|
|
423
|
+
const id = p.str('id', { required: true, maxLen: 512 });
|
|
424
|
+
return plan(p, 'task', id ? ['status', id] : ['status']);
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: 'bolloon_task_result',
|
|
429
|
+
title: '交付证据 (不输出正文)',
|
|
430
|
+
description: '只读: 交付是否在盘 / 字节数 / 各类哈希 / 验真分档。' +
|
|
431
|
+
'★ **交付正文绝不出本机** (P1 §5.6 私有层): 只给哈希与在盘证明 (`bodyPrinted:false`)。',
|
|
432
|
+
inputSchema: {
|
|
433
|
+
type: 'object',
|
|
434
|
+
additionalProperties: false,
|
|
435
|
+
required: ['id'],
|
|
436
|
+
properties: { id: { type: 'string', description: 'transactionId (或 requestId)' }, timeoutMs: TIMEOUT_PROP },
|
|
437
|
+
},
|
|
438
|
+
build(args) {
|
|
439
|
+
const p = new Params(args, ['id', TIMEOUT_KEY]);
|
|
440
|
+
const id = p.str('id', { required: true, maxLen: 512 });
|
|
441
|
+
return plan(p, 'task', id ? ['result', id] : ['result']);
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
name: 'bolloon_task_retry',
|
|
446
|
+
title: '恢复计划 (不发付款)',
|
|
447
|
+
description: '★ **只出计划, 绝不代付款**: 走 `x402/payment-recovery.planTransactionRecovery` (唯一决策点), 返回 `paid:false` + `mustNotRepay`。' +
|
|
448
|
+
'付款不确定时 next_action=reconcile —— 顺序永远是"先对账, 再决定 retry"。' +
|
|
449
|
+
'真正付款只能由**持钱包的一方**走同一 requestId 的幂等路径, MCP 层不持有也不调用支付能力。',
|
|
450
|
+
inputSchema: {
|
|
451
|
+
type: 'object',
|
|
452
|
+
additionalProperties: false,
|
|
453
|
+
required: ['id'],
|
|
454
|
+
properties: { id: { type: 'string', description: 'transactionId / requestId / goalId' }, timeoutMs: TIMEOUT_PROP },
|
|
455
|
+
},
|
|
456
|
+
build(args) {
|
|
457
|
+
const p = new Params(args, ['id', TIMEOUT_KEY]);
|
|
458
|
+
const id = p.str('id', { required: true, maxLen: 512 });
|
|
459
|
+
return plan(p, 'task', id ? ['retry', id] : ['retry']);
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: 'bolloon_wallet_status',
|
|
464
|
+
title: '钱包可用性 + 策略 (只读)',
|
|
465
|
+
description: '只读: 钱包是否存在 (地址是公开的收款地址) / 单笔与日限额 / 白名单 / 最近签名审计摘要。' +
|
|
466
|
+
'★ **绝不返回私钥、助记词、seed**; 本 tool 也不创建钱包 (不为了看一眼状态就生成新钱包)。' +
|
|
467
|
+
'模型侧改策略的能力**没有**通过 MCP 暴露 (见 §NOT_EXPOSED)。',
|
|
468
|
+
inputSchema: { type: 'object', additionalProperties: false, properties: { timeoutMs: TIMEOUT_PROP } },
|
|
469
|
+
build(args) {
|
|
470
|
+
const p = new Params(args, [TIMEOUT_KEY]);
|
|
471
|
+
return plan(p, 'wallet', ['status']);
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: 'bolloon_payment_pending',
|
|
476
|
+
title: '待放行的付款',
|
|
477
|
+
description: '只读: 列出在等你放行的付款请求 (~/.bolloon/payment-approvals.json)。' +
|
|
478
|
+
'有 pending 时 next_action=approve_payment —— 这是**正确表达**, 不是错误也不是重试信号。',
|
|
479
|
+
inputSchema: { type: 'object', additionalProperties: false, properties: { timeoutMs: TIMEOUT_PROP } },
|
|
480
|
+
build(args) {
|
|
481
|
+
const p = new Params(args, [TIMEOUT_KEY]);
|
|
482
|
+
return plan(p, 'payment', ['pending']);
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
name: 'bolloon_payment_approve',
|
|
487
|
+
title: '批准一笔待放行付款 (不发付款)',
|
|
488
|
+
description: '★ **只改审批状态, 不发起付款** (CLI/MCP 都不持有钱包; 真正支付由持钱包的一方/executor 执行, 返回里 `paid` 字段如实反映)。' +
|
|
489
|
+
'批准**不能**绕过 payment policy: 放行闸 `authorizeWalletSignature` (9 项, fail-closed) 仍在支付路径上, 这里碰不到它。' +
|
|
490
|
+
'无用户授权时 MCP **不许**切自主支付 —— 本 tool 没有"改模式/授权"的参数, 这是刻意的。',
|
|
491
|
+
inputSchema: {
|
|
492
|
+
type: 'object',
|
|
493
|
+
additionalProperties: false,
|
|
494
|
+
required: ['approvalId'],
|
|
495
|
+
properties: { approvalId: { type: 'string', description: '审批 id (来自 bolloon_payment_pending)' }, timeoutMs: TIMEOUT_PROP },
|
|
496
|
+
},
|
|
497
|
+
build(args) {
|
|
498
|
+
const p = new Params(args, ['approvalId', TIMEOUT_KEY]);
|
|
499
|
+
const id = p.str('approvalId', { required: true, maxLen: 512 });
|
|
500
|
+
return plan(p, 'payment', id ? ['approve', id] : ['approve']);
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
name: 'bolloon_payment_reject',
|
|
505
|
+
title: '拒绝一笔待放行付款',
|
|
506
|
+
description: '只改审批状态为拒绝; 拒绝后不会付款, 同一 requestId 的幂等保护仍在。',
|
|
507
|
+
inputSchema: {
|
|
508
|
+
type: 'object',
|
|
509
|
+
additionalProperties: false,
|
|
510
|
+
required: ['approvalId'],
|
|
511
|
+
properties: { approvalId: { type: 'string', description: '审批 id' }, timeoutMs: TIMEOUT_PROP },
|
|
512
|
+
},
|
|
513
|
+
build(args) {
|
|
514
|
+
const p = new Params(args, ['approvalId', TIMEOUT_KEY]);
|
|
515
|
+
const id = p.str('approvalId', { required: true, maxLen: 512 });
|
|
516
|
+
return plan(p, 'payment', id ? ['reject', id] : ['reject']);
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
name: 'bolloon_trade_list',
|
|
521
|
+
title: '交易一览',
|
|
522
|
+
description: '只读: 本地交易记录一览 (lifecycle + settlementFact + settlement 口径 + 事件数), 按口径分开写 (chain / local-dev / none)。',
|
|
523
|
+
inputSchema: {
|
|
524
|
+
type: 'object',
|
|
525
|
+
additionalProperties: false,
|
|
526
|
+
properties: { requestId: { type: 'string', description: '只看这个幂等键的交易' }, timeoutMs: TIMEOUT_PROP },
|
|
527
|
+
},
|
|
528
|
+
build(args) {
|
|
529
|
+
const p = new Params(args, ['requestId', TIMEOUT_KEY]);
|
|
530
|
+
return plan(p, 'trade', ['list']);
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: 'bolloon_trade_show',
|
|
535
|
+
title: '单笔交易原文',
|
|
536
|
+
description: '只读: 单笔交易记录 (状态 + 结算事实 + 哈希 + 事件链 + 责任候选 + 争议), **只读不写, 绝不修改交易历史**。' +
|
|
537
|
+
'付款回执原文属私有层 → 只给 `paymentReceiptPresent: boolean`, 原文不出本机。',
|
|
538
|
+
inputSchema: {
|
|
539
|
+
type: 'object',
|
|
540
|
+
additionalProperties: false,
|
|
541
|
+
required: ['id'],
|
|
542
|
+
properties: { id: { type: 'string', description: 'transactionId (或 requestId)' }, timeoutMs: TIMEOUT_PROP },
|
|
543
|
+
},
|
|
544
|
+
build(args) {
|
|
545
|
+
const p = new Params(args, ['id', TIMEOUT_KEY]);
|
|
546
|
+
const id = p.str('id', { required: true, maxLen: 512 });
|
|
547
|
+
return plan(p, 'trade', id ? ['show', id] : ['show']);
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
name: 'bolloon_trade_reconcile',
|
|
552
|
+
title: '单笔对账 / 恢复计划 (只读, 不发付款)',
|
|
553
|
+
description: '给 transactionId → 单笔恢复计划 (`planTransactionRecovery`, 唯一决策点, **只读**, `paid:false`)。' +
|
|
554
|
+
'★ 无 id 的**全局对账**不在 MCP 暴露: 那份实现 (`reconcilePendingTransactions`) 会钉结算事实/改状态/清死锁 —— ' +
|
|
555
|
+
'那是本机运维动作 (`bolloon trade reconcile`), 不是给远端 Agent 的。' +
|
|
556
|
+
'★ 顺序铁律: 先 reconcile, 再决定 retry。',
|
|
557
|
+
inputSchema: {
|
|
558
|
+
type: 'object',
|
|
559
|
+
additionalProperties: false,
|
|
560
|
+
required: ['transactionId'],
|
|
561
|
+
properties: { transactionId: { type: 'string', description: '要出恢复计划的那笔交易' }, timeoutMs: TIMEOUT_PROP },
|
|
562
|
+
},
|
|
563
|
+
build(args) {
|
|
564
|
+
const p = new Params(args, ['transactionId', TIMEOUT_KEY]);
|
|
565
|
+
const id = p.str('transactionId', { required: true, maxLen: 512 });
|
|
566
|
+
return plan(p, 'trade', id ? ['reconcile', id] : ['reconcile']);
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
// ── P6: 链上能力 (7 个只读/本地缓存 tool; 全部薄包装 `bolloon chain ...`) ──────
|
|
570
|
+
{
|
|
571
|
+
name: 'bolloon_chain_status',
|
|
572
|
+
title: '链配置 + 可达性 + 钱包可用性 (只读)',
|
|
573
|
+
description: '只读: chainId / RPC / escrow 与 token 地址 / 确认数门槛 (confirmed=1, finalized=12) / RPC 是否可达 / ' +
|
|
574
|
+
'合约 bytecode / 钱包是否可用 (只给**公开地址**与余额, 私钥永不返回、永不打印)。' +
|
|
575
|
+
'★ 链未配置 → code=CHAIN_NOT_CONFIGURED (列出缺哪些); RPC 不可达 → CHAIN_UNAVAILABLE (绝不报成 0 余额/空状态)。' +
|
|
576
|
+
'★ local-dev (chainId 31337) 永不产出 fully_settled。',
|
|
577
|
+
inputSchema: { type: 'object', additionalProperties: false, properties: { timeoutMs: TIMEOUT_PROP } },
|
|
578
|
+
build(args) {
|
|
579
|
+
const p = new Params(args, [TIMEOUT_KEY]);
|
|
580
|
+
return plan(p, 'chain', ['status']);
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
name: 'bolloon_chain_escrow_show',
|
|
585
|
+
title: '读链上 escrow (只读)',
|
|
586
|
+
description: '只读: 按 taskKey 读 `AgentEscrow` 里的 escrow 19 字段 (state = ACTIVE|RELEASED|DISPUTED|REFUNDED, 金额, buyer/agent, ' +
|
|
587
|
+
'各 hash, deadline, proofVersion)。' +
|
|
588
|
+
'★ 「链上没有」(ESCROW_NOT_FOUND) 与「读不到」(CHAIN_UNAVAILABLE) 是两个码 —— 读不到绝不当成不存在。' +
|
|
589
|
+
'★ `state=RELEASED` 只是链上事实, 不等于本机已 verified (判据是 receipt + 事件 + 确认数)。',
|
|
590
|
+
inputSchema: {
|
|
591
|
+
type: 'object',
|
|
592
|
+
additionalProperties: false,
|
|
593
|
+
required: ['taskKey'],
|
|
594
|
+
properties: { taskKey: { type: 'string', description: '链上 taskKey (0x + 64 hex, keccak256 域标签派生)' }, timeoutMs: TIMEOUT_PROP },
|
|
595
|
+
},
|
|
596
|
+
build(args) {
|
|
597
|
+
const p = new Params(args, ['taskKey', TIMEOUT_KEY]);
|
|
598
|
+
const k = p.str('taskKey', { required: true, maxLen: 80 });
|
|
599
|
+
return plan(p, 'chain', k ? ['escrow', 'show', k] : ['escrow', 'show']);
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
name: 'bolloon_chain_timeline',
|
|
604
|
+
title: 'taskKey 链上时间线 (只读)',
|
|
605
|
+
description: '只读: 本机索引里该 taskKey 的**链上事件时间线** (块号/logIndex 升序, 每条带 finality: ' +
|
|
606
|
+
'observed(确认数 < confirmed) / confirmed(≥1) / finalized(≥12)) + 本机视角 (chain-state.json 的 create/proof/release)。' +
|
|
607
|
+
'据此可重建 create → proof → release。' +
|
|
608
|
+
'★ 有被回退的记录 → code=REORG_SUSPECTED (绝不报成功); 索引里没有 → ESCROW_NOT_FOUND (先 `bolloon_chain_index_sync`)。',
|
|
609
|
+
inputSchema: {
|
|
610
|
+
type: 'object',
|
|
611
|
+
additionalProperties: false,
|
|
612
|
+
required: ['taskKey'],
|
|
613
|
+
properties: { taskKey: { type: 'string', description: '链上 taskKey (0x + 64 hex)' }, timeoutMs: TIMEOUT_PROP },
|
|
614
|
+
},
|
|
615
|
+
build(args) {
|
|
616
|
+
const p = new Params(args, ['taskKey', TIMEOUT_KEY]);
|
|
617
|
+
const k = p.str('taskKey', { required: true, maxLen: 80 });
|
|
618
|
+
return plan(p, 'chain', k ? ['timeline', k] : ['timeline']);
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
name: 'bolloon_chain_index_status',
|
|
623
|
+
title: '索引高度 / 最后同步 (只读)',
|
|
624
|
+
description: '只读: 本机链上事件索引的高度 (lastSyncedBlock)、最后同步时间、事件数/suspect 数、索引起点 (部署块与来源)、' +
|
|
625
|
+
'确认数门槛、reorgDepth。**不发 RPC** (只读本机索引文件); 从未同步时 synced=false (不假装有数据)。',
|
|
626
|
+
inputSchema: { type: 'object', additionalProperties: false, properties: { timeoutMs: TIMEOUT_PROP } },
|
|
627
|
+
build(args) {
|
|
628
|
+
const p = new Params(args, [TIMEOUT_KEY]);
|
|
629
|
+
return plan(p, 'chain', ['index', 'status']);
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: 'bolloon_chain_index_stats',
|
|
634
|
+
title: '链上事件统计 (只读)',
|
|
635
|
+
description: '只读: tasks / created / proof / released / refunded / disputed / expired 计数 + finality 分档 + 事件名直方图。' +
|
|
636
|
+
'★ suspect (被回退) 的记录**不计入**业务计数, 单独报出 —— 统计不是链上事实, 判据永远是 receipt/事件/确认数。',
|
|
637
|
+
inputSchema: { type: 'object', additionalProperties: false, properties: { timeoutMs: TIMEOUT_PROP } },
|
|
638
|
+
build(args) {
|
|
639
|
+
const p = new Params(args, [TIMEOUT_KEY]);
|
|
640
|
+
return plan(p, 'chain', ['index', 'stats']);
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
name: 'bolloon_chain_index_sync',
|
|
645
|
+
title: '增量同步链上事件到本机索引 (只写本地缓存)',
|
|
646
|
+
description: '★ 只做一件事: 从**本机上次同步高度 +1** 到当前 head 用 `eth_getLogs` 分页扫 v2 事件, 落 `~/.bolloon/chain/index.json`。' +
|
|
647
|
+
'**不动钱、不碰私钥、不改交易记录、不写结算事实** (索引是可删可重建的缓存, 不是事实源)。' +
|
|
648
|
+
'起点来自部署 manifest 的 deployment block (不猜 0); 检出重组 → 记录标 suspect 且 code=REORG_SUSPECTED (不静默丢弃)。' +
|
|
649
|
+
'★ 索引身份 (chainId + escrowAddress + 部署块) 与当前链不一致 (换过合约部署/anvil 重启) → **不扫不写**, ' +
|
|
650
|
+
'报 `INDEX_IDENTITY_CHANGED` + next_action=needs_human (这不是重组); 修法 `bolloon chain index rebuild` (干净重建)。',
|
|
651
|
+
inputSchema: {
|
|
652
|
+
type: 'object',
|
|
653
|
+
additionalProperties: false,
|
|
654
|
+
properties: { fromBlock: { type: 'string', description: '可选: 从这一块起补扫 (十进制块号, 不给=增量)' }, timeoutMs: TIMEOUT_PROP },
|
|
655
|
+
},
|
|
656
|
+
build(args) {
|
|
657
|
+
const p = new Params(args, ['fromBlock', TIMEOUT_KEY]);
|
|
658
|
+
const argv = ['index', 'sync'];
|
|
659
|
+
const from = p.strNumber('fromBlock');
|
|
660
|
+
if (from)
|
|
661
|
+
argv.push('--from-block', from);
|
|
662
|
+
return plan(p, 'chain', argv);
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: 'bolloon_chain_trade_recover',
|
|
667
|
+
title: '链上交易恢复状态 (只读, 不发交易)',
|
|
668
|
+
description: '★ **纯读盘** (~/.bolloon/chain/chain-state.json): 按 taskId / taskKey 重建本机链上事实, 给出 nextAction ' +
|
|
669
|
+
'(create_escrow / submit_proof / release / verify_only / done / needs_human) 与 mustNotRepay。' +
|
|
670
|
+
'**不发交易、不重付、不自动退款、不碰私钥** (链上写操作一律必须由本机持钱包的一方执行)。' +
|
|
671
|
+
'★ 有被标可疑的记录 → REORG_SUSPECTED; 结论未定 → CHAIN_UNCERTAIN + next_action=reconcile (不确定绝不报成功)。',
|
|
672
|
+
inputSchema: {
|
|
673
|
+
type: 'object',
|
|
674
|
+
additionalProperties: false,
|
|
675
|
+
properties: {
|
|
676
|
+
taskId: { type: 'string', description: '任务 id (与 create 时同一个; 派生 taskKey)' },
|
|
677
|
+
taskKey: { type: 'string', description: '或直接给链上 taskKey (0x + 64 hex)' },
|
|
678
|
+
timeoutMs: TIMEOUT_PROP,
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
build(args) {
|
|
682
|
+
const p = new Params(args, ['taskId', 'taskKey', TIMEOUT_KEY]);
|
|
683
|
+
const taskId = p.str('taskId', { maxLen: 512 });
|
|
684
|
+
const taskKey = p.str('taskKey', { maxLen: 80 });
|
|
685
|
+
const argv = ['trade', 'recover'];
|
|
686
|
+
if (taskId)
|
|
687
|
+
argv.push('--task-id', taskId);
|
|
688
|
+
else if (taskKey)
|
|
689
|
+
argv.push('--task-key', taskKey);
|
|
690
|
+
return plan(p, 'chain', argv);
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
// ── P6b: 链上**写** tool (3 个) —— 与只读链 tool 的区别只有一个: 它们**真签名 + 真移钱** ──
|
|
694
|
+
// 薄包装同一批 P3/P4 服务函数 (`chain trade create|submit-proof|release` → onchain-trade 的
|
|
695
|
+
// createEscrowStep / submitProofStep / releaseStep), **不复制任何业务逻辑**;
|
|
696
|
+
// 授权意图 (`paymentMode` + `requestId`) 必须显式携带 (见 requireWriteIntent), 失败按信封原样返回。
|
|
697
|
+
{
|
|
698
|
+
name: 'bolloon_chain_trade_create',
|
|
699
|
+
title: '★ 真签名真移钱: 建链上 escrow (买方付款进托管)',
|
|
700
|
+
description: '★★★ **这是真写操作: 会用本机钱包私钥真签名, 并把真 USDC 从买方打进链上 escrow 托管 (链上真 txHash)**。' +
|
|
701
|
+
'等价于 `bolloon chain trade create --task-id <id> --agent <addr> --amount <USDC> --payment-mode <mode> --request-id <rid>`。' +
|
|
702
|
+
'**调用方必须自己保证已授权** —— 本 tool 只声明意图, 不授予任何权限: 真签名只由本机唯一放行闸 ' +
|
|
703
|
+
'`authorizeWalletSignature` (fail-closed, 9 项) 决定, 私钥永不返回、永不打印。' +
|
|
704
|
+
'★ **必填** `paymentMode` 与 `requestId` (授权意图): 缺 → `NOT_AUTHORIZED`; 词表外的值 → `INVALID_ARGUMENT`; ' +
|
|
705
|
+
'`manual`/`policy` → 被放行闸拒 (modeIsAutonomous); 同一个 `requestId` 重复声明 → 被闸按 notDuplicate 拒。' +
|
|
706
|
+
'金额上限沿用 M1 硬约束 (单任务 0.05 / 单次购买 0.02 USDC, 与 economic-policy 三层取最小); 超了给 `BUDGET_EXCEEDED` 并指明哪一层, **不发交易**。' +
|
|
707
|
+
'失败按 P3 信封原样返回 (`isError=true`), 绝不变成 MCP 成功; 链上结论未定 → `CHAIN_UNCERTAIN` + reconcile, 不确定绝不报成功。',
|
|
708
|
+
inputSchema: {
|
|
709
|
+
type: 'object',
|
|
710
|
+
additionalProperties: false,
|
|
711
|
+
required: ['taskId', 'agent', 'amount', 'paymentMode', 'requestId'],
|
|
712
|
+
properties: {
|
|
713
|
+
taskId: { type: 'string', description: '任务 id —— 链上 taskKey 由它确定性派生 (keccak256 域标签 bolloon.task.v1)' },
|
|
714
|
+
agent: { type: 'string', description: '卖方收款地址 (0x + 40 hex), escrow 释放后钱真打到它' },
|
|
715
|
+
amount: { type: 'string', description: '托管金额 (十进制 USDC 串, 如 "0.02"); M1 单次购买上限 0.02' },
|
|
716
|
+
deadline: { type: 'number', description: '托管截止时间 (unix 秒); 不给 = max(链上最新块时间, 本机时间) + 确认窗口' },
|
|
717
|
+
confirmationWindow: { type: 'number', description: '买方确认窗口 (秒, 默认 3600); 过后卖方才能 claimAfterTimeout' },
|
|
718
|
+
proofVersion: { type: 'number', description: 'proof 版本 (默认 1, 参与 proofHash 口径)' },
|
|
719
|
+
...WRITE_INTENT_PROPS,
|
|
720
|
+
timeoutMs: TIMEOUT_PROP,
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
build(args) {
|
|
724
|
+
const intent = requireWriteIntent(args);
|
|
725
|
+
if (!intent.ok)
|
|
726
|
+
return { ok: false, envelope: intent.envelope };
|
|
727
|
+
const p = new Params(args, ['taskId', 'agent', 'amount', 'deadline', 'confirmationWindow', 'proofVersion', 'paymentMode', 'requestId', TIMEOUT_KEY]);
|
|
728
|
+
const taskId = p.str('taskId', { required: true, maxLen: 512 });
|
|
729
|
+
const agent = p.str('agent', { required: true, maxLen: 80 });
|
|
730
|
+
const amount = p.strNumber('amount', { required: true });
|
|
731
|
+
const paymentMode = p.str('paymentMode', { required: true, maxLen: 40 });
|
|
732
|
+
const argv = ['trade', 'create'];
|
|
733
|
+
if (taskId)
|
|
734
|
+
argv.push('--task-id', taskId);
|
|
735
|
+
if (agent)
|
|
736
|
+
argv.push('--agent', agent);
|
|
737
|
+
if (amount)
|
|
738
|
+
argv.push('--amount', amount);
|
|
739
|
+
if (paymentMode)
|
|
740
|
+
argv.push('--payment-mode', paymentMode);
|
|
741
|
+
const deadline = p.intPos('deadline');
|
|
742
|
+
if (deadline !== undefined)
|
|
743
|
+
argv.push('--deadline', String(deadline));
|
|
744
|
+
const win = p.intPos('confirmationWindow');
|
|
745
|
+
if (win !== undefined)
|
|
746
|
+
argv.push('--confirmation-window', String(win));
|
|
747
|
+
const pv = p.intPos('proofVersion');
|
|
748
|
+
if (pv !== undefined)
|
|
749
|
+
argv.push('--proof-version', String(pv));
|
|
750
|
+
return plan(p, 'chain', argv);
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
name: 'bolloon_chain_trade_submit_proof',
|
|
755
|
+
title: '★ 真签名真上链: 提交交付证明 (seller 侧)',
|
|
756
|
+
description: '★★★ **这是真写操作: 会用**本机钱包**私钥真签名, 把交付结果摘要 (`resultHash`) 真上链 (`submitProofV2`, 链上真 txHash)**。' +
|
|
757
|
+
'等价于 `bolloon chain trade submit-proof --task-id <id> --result <正文|sha256:hex> --payment-mode <mode> --request-id <rid>`。' +
|
|
758
|
+
'常由**卖方节点**执行 (合约要求 `msg.sender == escrow.agent`); 只有当调用方就是本机钱包持有人时才可能成功。' +
|
|
759
|
+
'**调用方必须自己保证已授权**; 缺 `paymentMode`/`requestId` → `NOT_AUTHORIZED`, 放行闸拒 → `NOT_AUTHORIZED` 且**不发交易、不碰私钥**。' +
|
|
760
|
+
'★ 这一步**不动钱** (escrow 仍在 ACTIVE) —— 别把它的成功读成"已结算"。' +
|
|
761
|
+
'失败按信封原样返回 (`isError=true`); 未定 → `CHAIN_UNCERTAIN` (不确定绝不报成功)。',
|
|
762
|
+
inputSchema: {
|
|
763
|
+
type: 'object',
|
|
764
|
+
additionalProperties: false,
|
|
765
|
+
required: ['taskId', 'result', 'paymentMode', 'requestId'],
|
|
766
|
+
properties: {
|
|
767
|
+
taskId: { type: 'string', description: '任务 id (与 create 时必须同一个; 派生 taskKey)' },
|
|
768
|
+
result: { type: 'string', description: '交付结果正文或 "sha256:<hex>" 摘要 (链上 resultHash 的来源, 只上链摘要不含正文)' },
|
|
769
|
+
manifestDigest: { type: 'string', description: '可选: manifest 摘要 (正文 或 "sha256:<hex>")' },
|
|
770
|
+
...WRITE_INTENT_PROPS,
|
|
771
|
+
timeoutMs: TIMEOUT_PROP,
|
|
772
|
+
},
|
|
773
|
+
},
|
|
774
|
+
build(args) {
|
|
775
|
+
const intent = requireWriteIntent(args);
|
|
776
|
+
if (!intent.ok)
|
|
777
|
+
return { ok: false, envelope: intent.envelope };
|
|
778
|
+
const p = new Params(args, ['taskId', 'result', 'manifestDigest', 'paymentMode', 'requestId', TIMEOUT_KEY]);
|
|
779
|
+
const taskId = p.str('taskId', { required: true, maxLen: 512 });
|
|
780
|
+
const result = p.str('result', { required: true, maxLen: MAX_LEN });
|
|
781
|
+
const paymentMode = p.str('paymentMode', { required: true, maxLen: 40 });
|
|
782
|
+
const argv = ['trade', 'submit-proof'];
|
|
783
|
+
if (taskId)
|
|
784
|
+
argv.push('--task-id', taskId);
|
|
785
|
+
if (result)
|
|
786
|
+
argv.push('--result', result);
|
|
787
|
+
const manifest = p.str('manifestDigest', { maxLen: MAX_LEN });
|
|
788
|
+
if (manifest)
|
|
789
|
+
argv.push('--manifest-digest', manifest);
|
|
790
|
+
if (paymentMode)
|
|
791
|
+
argv.push('--payment-mode', paymentMode);
|
|
792
|
+
return plan(p, 'chain', argv);
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
name: 'bolloon_chain_trade_release',
|
|
797
|
+
title: '★ 真签名真移钱: 释放托管 (钱真打到 seller)',
|
|
798
|
+
description: '★★★ **这是真写操作: 会用本机钱包私钥真签名, 把 escrow 里的 USDC 真释放给 seller (`releaseV2`, 链上真 txHash, 钱真到账)**。' +
|
|
799
|
+
'等价于 `bolloon chain trade release --task-id <id> --payment-mode <mode> --request-id <rid>`。' +
|
|
800
|
+
'**调用方必须自己保证已授权** —— 声明只是声明, 真签名只由本机放行闸 `authorizeWalletSignature` (fail-closed) 决定; ' +
|
|
801
|
+
'缺 `paymentMode`/`requestId` → `NOT_AUTHORIZED` (不默认放行), 闸拒 → `NOT_AUTHORIZED` 且钱**没动**。' +
|
|
802
|
+
'★ 只有全过 (事件对上 + 合约 RELEASED + 确认数达标) 才 `grantsVerified=true`; 没过一律 `CHAIN_UNCERTAIN` (不许标 verified)。' +
|
|
803
|
+
'失败按信封原样返回 (`isError=true`), 绝不变成 MCP 成功。',
|
|
804
|
+
inputSchema: {
|
|
805
|
+
type: 'object',
|
|
806
|
+
additionalProperties: false,
|
|
807
|
+
required: ['taskId', 'paymentMode', 'requestId'],
|
|
808
|
+
properties: {
|
|
809
|
+
taskId: { type: 'string', description: '任务 id (与 create 时必须同一个; 派生 taskKey)' },
|
|
810
|
+
...WRITE_INTENT_PROPS,
|
|
811
|
+
timeoutMs: TIMEOUT_PROP,
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
build(args) {
|
|
815
|
+
const intent = requireWriteIntent(args);
|
|
816
|
+
if (!intent.ok)
|
|
817
|
+
return { ok: false, envelope: intent.envelope };
|
|
818
|
+
const p = new Params(args, ['taskId', 'paymentMode', 'requestId', TIMEOUT_KEY]);
|
|
819
|
+
const taskId = p.str('taskId', { required: true, maxLen: 512 });
|
|
820
|
+
const paymentMode = p.str('paymentMode', { required: true, maxLen: 40 });
|
|
821
|
+
const argv = ['trade', 'release'];
|
|
822
|
+
if (taskId)
|
|
823
|
+
argv.push('--task-id', taskId);
|
|
824
|
+
if (paymentMode)
|
|
825
|
+
argv.push('--payment-mode', paymentMode);
|
|
826
|
+
return plan(p, 'chain', argv);
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
];
|
|
830
|
+
/** tool 名 → 定义 */
|
|
831
|
+
export const TOOL_BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
|
|
832
|
+
/**
|
|
833
|
+
* **刻意不暴露**的 P3/P6 子命令 (暴露 = 必须在 MCP 层假装成功, 违反"失败不得变成功"):
|
|
834
|
+
* task complete · task cancel → P3 里如实报 `C_NOT_IMPLEMENTED` (没有可写的任务状态存储/交易层无 cancelled)
|
|
835
|
+
* task send · task inbox · task accept · task reject → **写操作** (DID 签名 + 落本机台账 + 对外发帧),
|
|
836
|
+
* 不在本轮的 17 个冻结清单里; 要纳入 MCP 必须单独裁决 (它会改变本机对外承诺, 不是只读能力)
|
|
837
|
+
* network leave → 仓库里不存在 leaveNetwork
|
|
838
|
+
* network init / peers · agent inspect · trade events · wallet policy(只读) / set-policy
|
|
839
|
+
* → 前几个是"本机节点生命周期/本地诊断", 不该给远端 Agent 调;
|
|
840
|
+
* **wallet set-policy 尤其不暴露** —— 让远端 Agent 改支付策略 = 绕过 payment policy (六条禁止之一)。
|
|
841
|
+
* ★ P6 追加: `bolloon chain trade create|submit-proof|release` 曾是「不暴露」——
|
|
842
|
+
* 现已改为**暴露为 MCP 写 tool (3 个)**, 但代价条件全部保留并加固 (2026-09-22 leo 拍板):
|
|
843
|
+
* · 唯一放行闸不变: 仍然只走 `sendChainTxGuarded` → `authorizeWalletSignature` (fail-closed),
|
|
844
|
+
* MCP 层**没有**任何旁路, 也不复制判钱/签发逻辑;
|
|
845
|
+
* · 每个写 tool **强制显式携带授权意图** (`paymentMode` + `requestId`): 缺 → `NOT_AUTHORIZED`,
|
|
846
|
+
* 非法 → `INVALID_ARGUMENT` (绝不默认放行);
|
|
847
|
+
* · 失败一律按 P3 信封原样返回 (`isError=true`), 不洗成 MCP 成功;
|
|
848
|
+
* · 私钥/任务正文不打印、不落盘; 审计仍写 `~/.bolloon/wallet-signatures.jsonl`;
|
|
849
|
+
* · 金额上限沿用 M1 硬约束 (0.05 / 0.02 / 0.10, 与 economic-policy 三层取最小)。
|
|
850
|
+
* `bolloon chain trade expire` **不暴露**: 仓库里没有这条子命令 (合约侧有 expireV2, CLI 未接) ——
|
|
851
|
+
* 暴露了就得在 MCP 层假装成功。
|
|
852
|
+
* `bolloon chain index sync` 例外地暴露: 它只读链 (eth_getLogs) 并刷新本机**可删可重建**的索引缓存,
|
|
853
|
+
* 不动钱、不碰私钥、不改交易记录/结算事实 —— 不构成"本机运维写动作"。
|
|
854
|
+
*/
|
|
855
|
+
export const NOT_EXPOSED = [
|
|
856
|
+
{ command: 'bolloon task complete|cancel', reason: 'P3 如实报 C_NOT_IMPLEMENTED (没有可写的任务状态存储 / 交易层无 cancelled); 暴露会逼 MCP 层假装成功' },
|
|
857
|
+
{ command: 'bolloon task send|inbox|accept|reject', reason: '写操作 (签名 + 落本机台账 + 对外发帧), 不在 P4 的 17 个冻结清单里; 纳入 MCP 需单独裁决' },
|
|
858
|
+
{ command: 'bolloon network leave', reason: '仓库没有 leaveNetwork; P3 报 C_NOT_IMPLEMENTED' },
|
|
859
|
+
{ command: 'bolloon trade reconcile (无 id 的全局对账)', reason: '会钉结算事实/改状态/清死锁 = 写交易记录; 属本机运维 (bolloon trade reconcile), MCP 只给单笔只读计划' },
|
|
860
|
+
{ command: 'bolloon network init|peers', reason: '本机节点生命周期 / 本进程 peer 列表, 不是给远端 Agent 的能力' },
|
|
861
|
+
{ command: 'bolloon wallet set-policy', reason: '远端改支付策略 = 绕过 payment policy (六条禁止); 必须由本机用户执行' },
|
|
862
|
+
{ command: 'bolloon agent inspect / trade events', reason: '诊断/审计视角; 等有明确外部需求再按同一薄包装方式加' },
|
|
863
|
+
{ command: 'bolloon chain trade expire', reason: '仓库里**没有**这条子命令 (合约侧有 permissionless expireV2, CLI 未接) —— 暴露会逼 MCP 层假装成功; 要暴露先把 CLI 子命令实现出来' },
|
|
864
|
+
];
|
|
865
|
+
/** 一个"信封资源": 内容 = P3 信封原样 JSON (含 ok/code/next_action, 绝不改写成 MCP 成功) */
|
|
866
|
+
async function envelopeResource(uri, group, argv, timeoutMs) {
|
|
867
|
+
const env = await callP3({ group, argv, opts: timeoutMs ? { timeoutMs } : undefined });
|
|
868
|
+
return { uri, mimeType: 'application/json', text: JSON.stringify(env, null, 2), ok: env.ok };
|
|
869
|
+
}
|
|
870
|
+
/** `bolloon://network/capabilities`: 由**两份 P3 信封**拼成的投影 (组合, 不是重实现) */
|
|
871
|
+
async function capabilitiesPayload() {
|
|
872
|
+
const [status, manifest] = await Promise.all([
|
|
873
|
+
callP3({ group: 'network', argv: ['status'] }),
|
|
874
|
+
callP3({ group: 'agent', argv: ['manifest'] }),
|
|
875
|
+
]);
|
|
876
|
+
const data = (status.data || {});
|
|
877
|
+
const mdata = (manifest.data || {});
|
|
878
|
+
const gatewayCaps = Array.isArray(data.gatewayJoin?.capabilities) ? data.gatewayJoin.capabilities : [];
|
|
879
|
+
const localCaps = Array.isArray(mdata.agents)
|
|
880
|
+
? Array.from(new Set(mdata.agents.flatMap((a) => (a.capabilities || []).map(String))))
|
|
881
|
+
: [];
|
|
882
|
+
const env = {
|
|
883
|
+
ok: status.ok && manifest.ok,
|
|
884
|
+
code: status.ok && manifest.ok ? 'OK' : (status.code === 'NETWORK_NOT_JOINED' ? 'NETWORK_NOT_JOINED' : status.code),
|
|
885
|
+
message: `本机能力投影: 本地 manifest ${localCaps.length} 项, 网络声明 ${gatewayCaps.length} 项 (registry ${data.registry?.ready ? '就绪' : '未就绪'}, ${data.registry?.services ?? 0} 条服务)`,
|
|
886
|
+
data: {
|
|
887
|
+
composed: true,
|
|
888
|
+
localCapabilities: localCaps,
|
|
889
|
+
gatewayCapabilities: gatewayCaps,
|
|
890
|
+
registry: data.registry || null,
|
|
891
|
+
joinedNetworks: Array.isArray(data.joinedNetworks) ? data.joinedNetworks.map((n) => ({ name: n.name, networkId: n.networkId, serviceCount: n.serviceCount })) : [],
|
|
892
|
+
sources: ['bolloon network status', 'bolloon agent manifest'],
|
|
893
|
+
note: '这是两份 P3 信封的投影 (只做组合, 没有重算任何业务事实); 具体服务声明请用 bolloon_agent_discover',
|
|
894
|
+
},
|
|
895
|
+
evidence: Array.from(new Set([...(status.evidence || []), ...(manifest.evidence || [])].map(String))),
|
|
896
|
+
next_action: status.next_action ?? null,
|
|
897
|
+
};
|
|
898
|
+
return { uri: 'bolloon://network/capabilities', mimeType: 'application/json', text: JSON.stringify(env, null, 2), ok: env.ok };
|
|
899
|
+
}
|
|
900
|
+
/** `bolloon://skill/current`: 指向 `skills/bolloon-network/SKILL.md` (对外唯一入口说明) */
|
|
901
|
+
function skillCandidatePaths() {
|
|
902
|
+
const here = fileURLToPath(import.meta.url);
|
|
903
|
+
const out = [];
|
|
904
|
+
if (process.env.BOLLOON_SKILL_PATH)
|
|
905
|
+
out.push(process.env.BOLLOON_SKILL_PATH);
|
|
906
|
+
for (const up of [['..', '..', '..'], ['..', '..'], ['..']]) {
|
|
907
|
+
out.push(path.resolve(path.dirname(here), ...up, 'skills', 'bolloon-network', 'SKILL.md'));
|
|
908
|
+
}
|
|
909
|
+
out.push(path.resolve(process.cwd(), 'skills', 'bolloon-network', 'SKILL.md'));
|
|
910
|
+
try {
|
|
911
|
+
out.push(path.resolve(currentPackageRoot(), 'skills', 'bolloon-network', 'SKILL.md'));
|
|
912
|
+
}
|
|
913
|
+
catch { /* 忽略 */ }
|
|
914
|
+
return out;
|
|
915
|
+
}
|
|
916
|
+
export async function skillPayload(uri) {
|
|
917
|
+
const p = skillCandidatePaths().find((f) => { try {
|
|
918
|
+
return fs.existsSync(f);
|
|
919
|
+
}
|
|
920
|
+
catch {
|
|
921
|
+
return false;
|
|
922
|
+
} });
|
|
923
|
+
if (!p) {
|
|
924
|
+
const env = failEnvelope('NOT_FOUND', '找不到 skills/bolloon-network/SKILL.md (可用 BOLLOON_SKILL_PATH 指定)', { searched: skillCandidatePaths() }, [], 'needs_human');
|
|
925
|
+
return { uri, mimeType: 'application/json', text: JSON.stringify(env, null, 2), ok: false };
|
|
926
|
+
}
|
|
927
|
+
const text = fs.readFileSync(p, 'utf8');
|
|
928
|
+
return { uri, mimeType: 'text/markdown', text, ok: true };
|
|
929
|
+
}
|
|
930
|
+
export const RESOURCES = [
|
|
931
|
+
{ uri: 'bolloon://network/status', name: '本机入网态', description: 'DID / 网络 / P2P / manifest / registry (bolloon network status 的信封原样)', mimeType: 'application/json', load: () => envelopeResource('bolloon://network/status', 'network', ['status']) },
|
|
932
|
+
{ uri: 'bolloon://network/capabilities', name: '本机能力投影', description: '本地 manifest 能力 + 网络声明能力 + registry 状态 (两份 P3 信封的投影)', mimeType: 'application/json', load: () => capabilitiesPayload() },
|
|
933
|
+
{ uri: 'bolloon://agent/manifest', name: '本进程 manifest', description: 'agents/capabilities (scope=in-process, persisted=false)', mimeType: 'application/json', load: () => envelopeResource('bolloon://agent/manifest', 'agent', ['manifest']) },
|
|
934
|
+
{ uri: 'bolloon://tasks/recent', name: '最近任务/交易', description: '本地交易 + Goal (只给 id/状态/结算口径, 不含任务正文)', mimeType: 'application/json', load: () => envelopeResource('bolloon://tasks/recent', 'task', ['list']) },
|
|
935
|
+
{ uri: 'bolloon://trades/recent', name: '最近交易', description: '交易一览 (chain / local-dev / none 口径分开写)', mimeType: 'application/json', load: () => envelopeResource('bolloon://trades/recent', 'trade', ['list']) },
|
|
936
|
+
{ uri: 'bolloon://wallet/policy', name: '支付策略', description: '单笔/日限额 + 白名单 + 速率 + 今日已用 (只读, 无私钥)', mimeType: 'application/json', load: () => envelopeResource('bolloon://wallet/policy', 'wallet', ['policy']) },
|
|
937
|
+
// ── P6: 链上能力 (信封原样; 链未配置时信封里就是 CHAIN_NOT_CONFIGURED, 不当成 MCP 成功) ──
|
|
938
|
+
{ uri: 'bolloon://chain/status', name: '链配置 + 可达性', description: 'chainId / RPC / 合约地址 / 确认数门槛 / 钱包可用性 (只给公开地址, 无私钥)', mimeType: 'application/json', load: () => envelopeResource('bolloon://chain/status', 'chain', ['status']) },
|
|
939
|
+
{ uri: 'bolloon://chain/index', name: '链上索引高度', description: '索引高度 / 最后同步时间 / 事件数 / suspect 数 (只读本机索引文件, 不发 RPC)', mimeType: 'application/json', load: () => envelopeResource('bolloon://chain/index', 'chain', ['index', 'status']) },
|
|
940
|
+
{ uri: 'bolloon://chain/index/stats', name: '链上事件统计', description: 'tasks/created/proof/released/refunded/disputed/expired + finality 分档 (suspect 不计入)', mimeType: 'application/json', load: () => envelopeResource('bolloon://chain/index/stats', 'chain', ['index', 'stats']) },
|
|
941
|
+
{ uri: 'bolloon://skill/current', name: 'bolloon-network Skill', description: 'skills/bolloon-network/SKILL.md 原文 (对外唯一入口说明)', mimeType: 'text/markdown', load: () => skillPayload('bolloon://skill/current') },
|
|
942
|
+
];
|
|
943
|
+
export const RESOURCE_BY_URI = new Map(RESOURCES.map((r) => [r.uri, r]));
|
|
944
|
+
/** MCP `tools/list` 的返回体 (不带内部 build 函数) */
|
|
945
|
+
export function listToolsPayload() {
|
|
946
|
+
return TOOLS.map((t) => ({
|
|
947
|
+
name: t.name,
|
|
948
|
+
description: `[${t.title}] ${t.description}`,
|
|
949
|
+
inputSchema: t.inputSchema,
|
|
950
|
+
}));
|
|
951
|
+
}
|
|
952
|
+
/** MCP `resources/list` 的返回体 */
|
|
953
|
+
export function listResourcesPayload() {
|
|
954
|
+
return RESOURCES.map((r) => ({ uri: r.uri, name: r.name, description: r.description, mimeType: r.mimeType }));
|
|
955
|
+
}
|