@bolloon/bolloon-agent 0.4.11 → 0.4.13
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/LICENSE +21 -0
- package/README.md +14 -8
- package/dist/agents/agent-registry.js +151 -0
- package/dist/agents/agent-reputation.js +54 -0
- package/dist/agents/agent-service-client.js +147 -0
- package/dist/agents/economic-policy.js +132 -0
- package/dist/agents/payment-approval.js +142 -0
- package/dist/agents/payment-gate.js +99 -0
- package/dist/agents/pi-sdk-tools.js +243 -0
- package/dist/agents/treasury-bridge.js +127 -0
- package/dist/index.js +42 -0
- package/dist/web/mobile.html +2 -0
- package/dist/web/mobile.js +38 -1
- package/dist/web/server.js +97 -0
- package/package.json +2 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 yuanjie liu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -98,11 +98,17 @@ npm start
|
|
|
98
98
|
| `OPENAI_API_KEY` | OpenAI 提供商(可选) |
|
|
99
99
|
| `DEEPSEEK_API_KEY` | DeepSeek 提供商(可选) |
|
|
100
100
|
| `ANTHROPIC_API_KEY` | Anthropic 提供商(可选) |
|
|
101
|
-
| `BOLLOON_LLM_PROVIDER` | 指定 LLM 提供商 |
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
| `BOLLOON_LLM_PROVIDER` | 指定 LLM 提供商 |
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 开源协议
|
|
106
|
+
|
|
107
|
+
本项目基于 [MIT 开源协议](./LICENSE) 发布。你可以自由使用、修改、分发本项目,但需保留版权声明和许可声明。详见 [LICENSE](./LICENSE) 文件。
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
<a name="english"></a>
|
|
106
112
|
|
|
107
113
|
## English
|
|
108
114
|
|
|
@@ -130,6 +136,6 @@ bolloon --help # All commands
|
|
|
130
136
|
|
|
131
137
|
Requires Node.js ≥ 18 and an LLM API key (`OPENAI_API_KEY` or `DEEPSEEK_API_KEY`).
|
|
132
138
|
|
|
133
|
-
### License
|
|
134
|
-
|
|
135
|
-
MIT
|
|
139
|
+
### License
|
|
140
|
+
|
|
141
|
+
This project is released under the [MIT License](./LICENSE). You are free to use, modify, and distribute this project, provided that you retain the copyright and permission notices. See the [LICENSE](./LICENSE) file for details.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-registry.ts — Agent 服务注册表 (2026-08-13, Phase E1)
|
|
3
|
+
*
|
|
4
|
+
* Agent Economic Network 的 Discovery 层: Agent 注册服务 (定价/能力声明/钱包),
|
|
5
|
+
* 其他 Agent 发现并调用.
|
|
6
|
+
*
|
|
7
|
+
* 存储: OrbitDB keyvalue 主存储 (去中心化, 跨设备同步) + 本地 JSON fallback.
|
|
8
|
+
* 复用 task-store 模式 (getCIDDatabase.openStore).
|
|
9
|
+
* 设计: registry 存整个服务列表到单键 'services' (兼容简单).
|
|
10
|
+
*/
|
|
11
|
+
import * as os from 'os';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { getCIDDatabase } from '../orbitdb/cid-database.js';
|
|
14
|
+
const home = () => process.env.HOME || os.homedir() || '/tmp';
|
|
15
|
+
export const REGISTRY_ORBIT_KEY = 'services';
|
|
16
|
+
/** OrbitDB store 名 (按 did 短名确定性) */
|
|
17
|
+
export function registryStoreName(did) {
|
|
18
|
+
const short = String(did || 'local').replace(/[^a-zA-Z0-9-]/g, '_').slice(0, 40) || 'local';
|
|
19
|
+
return `bolloon-agent-registry-${short}`;
|
|
20
|
+
}
|
|
21
|
+
/** 真实实现 (OrbitDB + 本地 fallback). 单例: 共享 CID 数据库单例. */
|
|
22
|
+
export class OrbitDBAgentRegistry {
|
|
23
|
+
did;
|
|
24
|
+
db;
|
|
25
|
+
localFile;
|
|
26
|
+
_store = null;
|
|
27
|
+
ready = false;
|
|
28
|
+
storeName;
|
|
29
|
+
storeAddress = '';
|
|
30
|
+
constructor(did, db = getCIDDatabase(), localFile = path.join(home(), '.bolloon', 'agent-registry.json')) {
|
|
31
|
+
this.did = did;
|
|
32
|
+
this.db = db;
|
|
33
|
+
this.localFile = localFile;
|
|
34
|
+
this.storeName = registryStoreName(did);
|
|
35
|
+
}
|
|
36
|
+
async warm() {
|
|
37
|
+
if (this.ready && this._store)
|
|
38
|
+
return true;
|
|
39
|
+
try {
|
|
40
|
+
this._store = await this.db.openStore(this.storeName, 'keyvalue');
|
|
41
|
+
this.storeAddress = this._store.address;
|
|
42
|
+
this.ready = true;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
this.ready = false;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async loadLocalFromDisk() {
|
|
51
|
+
try {
|
|
52
|
+
const { readFile } = await import('fs/promises');
|
|
53
|
+
const parsed = JSON.parse(await readFile(this.localFile, 'utf-8'));
|
|
54
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async saveLocalToDisk(list) {
|
|
61
|
+
try {
|
|
62
|
+
const { mkdir, writeFile } = await import('fs/promises');
|
|
63
|
+
await mkdir(path.dirname(this.localFile), { recursive: true });
|
|
64
|
+
await writeFile(this.localFile, JSON.stringify(list, null, 2), 'utf-8');
|
|
65
|
+
}
|
|
66
|
+
catch { /* 本地写失败静默 */ }
|
|
67
|
+
}
|
|
68
|
+
async loadLocal() {
|
|
69
|
+
return this.loadLocalFromDisk();
|
|
70
|
+
}
|
|
71
|
+
/** OrbitDB 读全部服务 (失败 → null) */
|
|
72
|
+
async orbitList() {
|
|
73
|
+
if (!this.ready || !this._store)
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
const v = await this._store.get(REGISTRY_ORBIT_KEY);
|
|
77
|
+
if (Array.isArray(v))
|
|
78
|
+
return v;
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async register(service) {
|
|
86
|
+
if (!service || !service.agentId || !service.service?.name) {
|
|
87
|
+
return { ok: false, error: 'agentId 和 service.name 必填' };
|
|
88
|
+
}
|
|
89
|
+
// 本地双写 (fallback 源)
|
|
90
|
+
const local = await this.loadLocalFromDisk();
|
|
91
|
+
const idx = local.findIndex((s) => s.agentId === service.agentId);
|
|
92
|
+
const now = new Date().toISOString();
|
|
93
|
+
const entry = { ...service, updatedAt: now, registeredAt: service.registeredAt || now };
|
|
94
|
+
if (idx >= 0)
|
|
95
|
+
local[idx] = entry;
|
|
96
|
+
else
|
|
97
|
+
local.push(entry);
|
|
98
|
+
await this.saveLocalToDisk(local);
|
|
99
|
+
// OrbitDB 写穿 (尽力而为)
|
|
100
|
+
if (this.ready && this._store) {
|
|
101
|
+
try {
|
|
102
|
+
const orbit = (await this.orbitList()) ?? [];
|
|
103
|
+
const oi = orbit.findIndex((s) => s.agentId === service.agentId);
|
|
104
|
+
if (oi >= 0)
|
|
105
|
+
orbit[oi] = entry;
|
|
106
|
+
else
|
|
107
|
+
orbit.push(entry);
|
|
108
|
+
await this._store.put(REGISTRY_ORBIT_KEY, orbit);
|
|
109
|
+
}
|
|
110
|
+
catch { /* orbit 写失败静默 */ }
|
|
111
|
+
}
|
|
112
|
+
return { ok: true };
|
|
113
|
+
}
|
|
114
|
+
async list() {
|
|
115
|
+
// OrbitDB 优先
|
|
116
|
+
const orbit = await this.orbitList();
|
|
117
|
+
if (orbit !== null)
|
|
118
|
+
return orbit;
|
|
119
|
+
return this.loadLocalFromDisk();
|
|
120
|
+
}
|
|
121
|
+
async discover(query) {
|
|
122
|
+
const all = await this.list();
|
|
123
|
+
const q = String(query || '').trim().toLowerCase();
|
|
124
|
+
if (!q)
|
|
125
|
+
return all;
|
|
126
|
+
return all.filter((s) => s.service?.name?.toLowerCase().includes(q) ||
|
|
127
|
+
s.service?.description?.toLowerCase().includes(q) ||
|
|
128
|
+
s.name?.toLowerCase().includes(q) ||
|
|
129
|
+
s.capabilities?.some((c) => c.toLowerCase().includes(q)));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
let _instance = null;
|
|
133
|
+
let _warmPromise = null;
|
|
134
|
+
/** 获取 Agent Registry 单例 (首次调用 warm). */
|
|
135
|
+
export function getAgentRegistry(did = '') {
|
|
136
|
+
if (!_instance)
|
|
137
|
+
_instance = new OrbitDBAgentRegistry(did || 'local');
|
|
138
|
+
return _instance;
|
|
139
|
+
}
|
|
140
|
+
/** 预热 Registry (server 启动调用). */
|
|
141
|
+
export function warmAgentRegistry(did = '') {
|
|
142
|
+
if (!_warmPromise) {
|
|
143
|
+
_warmPromise = getAgentRegistry(did).warm().then((ok) => ok).catch(() => false);
|
|
144
|
+
}
|
|
145
|
+
return _warmPromise;
|
|
146
|
+
}
|
|
147
|
+
/** 重置单例 (测试用) */
|
|
148
|
+
export function resetAgentRegistry() {
|
|
149
|
+
_instance = null;
|
|
150
|
+
_warmPromise = null;
|
|
151
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-reputation.ts — Agent 信誉系统 (2026-08-13, Phase M4)
|
|
3
|
+
*
|
|
4
|
+
* Agent Economic Protocol §7 Reputation: "Should I trust you?"
|
|
5
|
+
* 每次服务结算后更新: tasks++ / success++ | failed++ | disputed++,
|
|
6
|
+
* score = success / tasks. 存 Registry 的 service.reputation.
|
|
7
|
+
*
|
|
8
|
+
* 使用: provider 完成服务后调 recordOutcome(agentId, serviceName, outcome),
|
|
9
|
+
* buyer 调用前可查询 (registry 已带 reputation).
|
|
10
|
+
*/
|
|
11
|
+
import { getAgentRegistry } from './agent-registry.js';
|
|
12
|
+
/**
|
|
13
|
+
* 记录一次服务结果, 更新 provider 信誉.
|
|
14
|
+
* 找不到服务 → 错误 (需先 registry_register).
|
|
15
|
+
*/
|
|
16
|
+
export async function recordServiceOutcome(agentId, serviceName, outcome, registry) {
|
|
17
|
+
const reg = registry ?? getAgentRegistry();
|
|
18
|
+
const services = await reg.list();
|
|
19
|
+
const idx = services.findIndex((s) => s.agentId === agentId && s.service?.name === serviceName);
|
|
20
|
+
if (idx < 0) {
|
|
21
|
+
return { ok: false, error: `服务未注册: ${agentId}/${serviceName} (先 registry_register)` };
|
|
22
|
+
}
|
|
23
|
+
const svc = services[idx];
|
|
24
|
+
const rep = svc.reputation ?? { tasks: 0, success: 0, failed: 0, disputed: 0, score: 0 };
|
|
25
|
+
rep.tasks += 1;
|
|
26
|
+
if (outcome === 'success')
|
|
27
|
+
rep.success += 1;
|
|
28
|
+
else if (outcome === 'failed')
|
|
29
|
+
rep.failed += 1;
|
|
30
|
+
else
|
|
31
|
+
rep.disputed += 1;
|
|
32
|
+
rep.score = rep.tasks > 0 ? Math.round((rep.success / rep.tasks) * 100) / 100 : 0;
|
|
33
|
+
svc.reputation = rep;
|
|
34
|
+
const r = await reg.register(svc);
|
|
35
|
+
if (!r.ok)
|
|
36
|
+
return { ok: false, error: r.error };
|
|
37
|
+
return { ok: true, reputation: rep };
|
|
38
|
+
}
|
|
39
|
+
/** 查询 Agent 信誉 (Registry 读取) */
|
|
40
|
+
export async function queryReputation(agentId, serviceName, registry) {
|
|
41
|
+
const reg = registry ?? getAgentRegistry();
|
|
42
|
+
const services = await reg.list();
|
|
43
|
+
const entries = services
|
|
44
|
+
.filter((s) => s.agentId === agentId && (!serviceName || s.service?.name === serviceName))
|
|
45
|
+
.map((s) => ({
|
|
46
|
+
service: s.service?.name || '?',
|
|
47
|
+
reputation: s.reputation ?? { tasks: 0, success: 0, failed: 0, disputed: 0, score: 0 },
|
|
48
|
+
}));
|
|
49
|
+
return { ok: entries.length > 0, entries, error: entries.length === 0 ? `agent ${agentId} 无服务记录` : undefined };
|
|
50
|
+
}
|
|
51
|
+
/** 格式化信誉 (agent 工具输出用) */
|
|
52
|
+
export function formatReputation(rep) {
|
|
53
|
+
return `✅ ${rep.success} / ${rep.tasks} 任务 (score=${rep.score})${rep.failed ? `, ❌ failed=${rep.failed}` : ''}${rep.disputed ? `, ⚠️ disputed=${rep.disputed}` : ''}`;
|
|
54
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-service-client.ts — Agent 服务调用闭环 (2026-08-13, Phase E2)
|
|
3
|
+
*
|
|
4
|
+
* Agent Economic Network 的 Execution+Payment 层:
|
|
5
|
+
* buyer: 从 Registry 发现服务 → 调服务端点 → 收到 402 → x402 自动支付 → 拿结果
|
|
6
|
+
* provider: 基于 Registry 价格生成 402 响应 → 收钱 → 提供服务
|
|
7
|
+
*
|
|
8
|
+
* 复用: x402Pay.ts 的 x402Fetch (自动 402→支付→重试) + x402RequestPayment (生成 402).
|
|
9
|
+
* MVP: 闭环逻辑可测 (mock fetch), 真实链上支付需 funded wallet.
|
|
10
|
+
*/
|
|
11
|
+
import { getAgentRegistry } from './agent-registry.js';
|
|
12
|
+
/**
|
|
13
|
+
* buyer 侧: 调用 Agent 服务 (402 自动支付).
|
|
14
|
+
* 流程: registry 发现 → x402Fetch(url) → 402 → 自动签名支付 → 服务结果
|
|
15
|
+
*/
|
|
16
|
+
export async function serviceCall(opts) {
|
|
17
|
+
const { serviceName, args, privateKey, url, maxPaymentAmount, registry } = opts;
|
|
18
|
+
// 1. 从 Registry 发现服务
|
|
19
|
+
const reg = registry ?? getAgentRegistry();
|
|
20
|
+
const services = await reg.discover(serviceName);
|
|
21
|
+
if (services.length === 0) {
|
|
22
|
+
return { success: false, error: `注册表未找到服务: ${serviceName}` };
|
|
23
|
+
}
|
|
24
|
+
const service = services[0];
|
|
25
|
+
// 2. 确定端点
|
|
26
|
+
const endpoint = url || service.endpoint;
|
|
27
|
+
if (!endpoint) {
|
|
28
|
+
return { success: false, error: `服务 ${serviceName} 无端点 (endpoint), 无法调用`, service };
|
|
29
|
+
}
|
|
30
|
+
// 3. 调服务 (x402 自动支付: 402 → 签名 → 重试)
|
|
31
|
+
try {
|
|
32
|
+
const { x402Fetch } = await import('./x402/x402Pay.js');
|
|
33
|
+
// 2026-08-13: YAML 验证门 (不全部交给 AI) — 支付前先过 payment-policy.yaml 规则链
|
|
34
|
+
const { getPaymentGate } = await import('./payment-gate.js');
|
|
35
|
+
const gate = getPaymentGate();
|
|
36
|
+
const amount = parseFloat(service.service?.price?.amount || '0');
|
|
37
|
+
const gateVerdict = gate.evaluate({ service: service.service?.name, amount, recipient: service.wallet });
|
|
38
|
+
if (gateVerdict.decision === 'deny') {
|
|
39
|
+
return { success: false, error: `[payment-gate] ${gateVerdict.reason}`, service };
|
|
40
|
+
}
|
|
41
|
+
if (gateVerdict.decision === 'confirm') {
|
|
42
|
+
// 2026-08-13: 人工审批 — 创建 pending 审批请求 (不自动执行), UI/CLI 批准后重试
|
|
43
|
+
const { getApprovalStore } = await import('./payment-approval.js');
|
|
44
|
+
const store = getApprovalStore();
|
|
45
|
+
const approval = await store.create({
|
|
46
|
+
service: service.service?.name || serviceName,
|
|
47
|
+
amount,
|
|
48
|
+
recipient: service.wallet,
|
|
49
|
+
reason: gateVerdict.reason,
|
|
50
|
+
retryPayload: { serviceName, args, privateKey, maxPaymentAmount: maxPaymentAmount || service.service?.price?.amount },
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
error: `[payment-gate] 需人工确认: ${gateVerdict.reason} (approval=${approval.id}, 批准后自动执行)`,
|
|
55
|
+
service,
|
|
56
|
+
requiresApproval: true,
|
|
57
|
+
approvalId: approval.id,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Phase E3: Policy Engine 授权 (预算/白名单) — 通过才允许自动支付
|
|
61
|
+
let effectiveKey = privateKey;
|
|
62
|
+
if (privateKey) {
|
|
63
|
+
const { getEconomicPolicy } = await import('./economic-policy.js');
|
|
64
|
+
const policy = getEconomicPolicy();
|
|
65
|
+
const decision = await policy.check({
|
|
66
|
+
payTo: service.wallet,
|
|
67
|
+
amount,
|
|
68
|
+
currency: service.service?.price?.currency,
|
|
69
|
+
service: service.service?.name,
|
|
70
|
+
timestamp: Date.now(),
|
|
71
|
+
});
|
|
72
|
+
if (!decision.allowed) {
|
|
73
|
+
return { success: false, error: `[policy] ${decision.reason}`, service };
|
|
74
|
+
}
|
|
75
|
+
effectiveKey = privateKey; // policy 通过 → 允许签名支付
|
|
76
|
+
// 记录花费 (结算后) — 预记, 实际结算在链上
|
|
77
|
+
await policy.recordSpend(amount).catch(() => { });
|
|
78
|
+
}
|
|
79
|
+
const result = await x402Fetch({
|
|
80
|
+
url: endpoint,
|
|
81
|
+
method: 'POST',
|
|
82
|
+
body: JSON.stringify(args || {}),
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
privateKey: effectiveKey,
|
|
85
|
+
maxPaymentAmount: maxPaymentAmount || service.service?.price?.amount,
|
|
86
|
+
});
|
|
87
|
+
if (!result.success) {
|
|
88
|
+
return { success: false, error: result.error || 'x402 调用失败', service };
|
|
89
|
+
}
|
|
90
|
+
// 有 paymentInfo 说明经过了 402 支付; data 是服务结果
|
|
91
|
+
const paid = !!result.paymentInfo || (result.status ?? 0) >= 200;
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
service,
|
|
95
|
+
paid,
|
|
96
|
+
output: typeof result.data === 'string' ? result.data : JSON.stringify(result.data ?? ''),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
return { success: false, error: `服务调用异常: ${e?.message}`, service };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* provider 侧: 生成 x402 402 响应 (基于 Registry 价格).
|
|
105
|
+
* 返回 402 响应对象 (含 PaymentRequired header 语义), 供服务端点使用.
|
|
106
|
+
*/
|
|
107
|
+
export async function serviceRequestPayment(agentId, serviceName, registry) {
|
|
108
|
+
const reg = registry ?? getAgentRegistry();
|
|
109
|
+
const services = await reg.list();
|
|
110
|
+
const service = services.find((s) => s.agentId === agentId && s.service?.name === serviceName);
|
|
111
|
+
if (!service) {
|
|
112
|
+
return { error: `agent ${agentId} 未注册服务 ${serviceName}` };
|
|
113
|
+
}
|
|
114
|
+
const price = parseFloat(service.service.price.amount || '0');
|
|
115
|
+
const currency = service.service.price.currency || 'USDC';
|
|
116
|
+
const payTo = service.wallet;
|
|
117
|
+
if (!payTo)
|
|
118
|
+
return { error: '服务无收款钱包' };
|
|
119
|
+
return {
|
|
120
|
+
price,
|
|
121
|
+
currency,
|
|
122
|
+
payTo,
|
|
123
|
+
resourceDescription: `${serviceName} service by ${service.name}`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* provider 侧: 基于 Registry 价格生成 402 PaymentRequired 响应体.
|
|
128
|
+
* 复用 x402RequestPayment.
|
|
129
|
+
*/
|
|
130
|
+
export async function buildPaymentRequiredResponse(agentId, serviceName, registry) {
|
|
131
|
+
const info = await serviceRequestPayment(agentId, serviceName, registry);
|
|
132
|
+
if ('error' in info) {
|
|
133
|
+
return { status: 500, headers: {}, body: JSON.stringify({ error: info.error }) };
|
|
134
|
+
}
|
|
135
|
+
const { x402RequestPayment } = await import('./x402/x402Pay.js');
|
|
136
|
+
const r = x402RequestPayment({
|
|
137
|
+
price: info.price,
|
|
138
|
+
currency: info.currency,
|
|
139
|
+
payTo: info.payTo,
|
|
140
|
+
resourceDescription: info.resourceDescription,
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
status: r.statusCode,
|
|
144
|
+
headers: r.headers,
|
|
145
|
+
body: r.body,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* economic-policy.ts — Policy Engine (2026-08-13, Phase E3)
|
|
3
|
+
*
|
|
4
|
+
* Agent 支付授权安全核心 (参考 arXiv:2605.30998 free-riding 防护):
|
|
5
|
+
* LLM 只见 Payment Intent, 不见私钥; Policy Engine 是唯一签名入口.
|
|
6
|
+
*
|
|
7
|
+
* 规则:
|
|
8
|
+
* - amount < per_transaction_limit
|
|
9
|
+
* - recipient (payTo) in allowed
|
|
10
|
+
* - service in allowed
|
|
11
|
+
* - daily budget not exceeded (冻结)
|
|
12
|
+
* - 速率限制 (rate limit)
|
|
13
|
+
*
|
|
14
|
+
* 预算持久化: ~/.bolloon/economic-policy.json (daily spend 滚动).
|
|
15
|
+
*/
|
|
16
|
+
import * as os from 'os';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import * as fs from 'fs/promises';
|
|
19
|
+
const home = () => process.env.HOME || os.homedir() || '/tmp';
|
|
20
|
+
/** 默认策略 (保守) */
|
|
21
|
+
const DEFAULT_CONFIG = {
|
|
22
|
+
perTransactionLimit: 1, // 单笔 ≤ $1
|
|
23
|
+
dailyLimit: 10, // 每日 ≤ $10
|
|
24
|
+
allowedRecipients: [],
|
|
25
|
+
allowedServices: [],
|
|
26
|
+
rateLimitPerMinute: 5,
|
|
27
|
+
};
|
|
28
|
+
export class LocalEconomicPolicy {
|
|
29
|
+
cfg = { ...DEFAULT_CONFIG };
|
|
30
|
+
file;
|
|
31
|
+
/** 支付时间戳窗口 (速率限制) */
|
|
32
|
+
payTimestamps = [];
|
|
33
|
+
/** 今日花费 */
|
|
34
|
+
_dailySpent = 0;
|
|
35
|
+
_dayKey = '';
|
|
36
|
+
constructor(file = path.join(home(), '.bolloon', 'economic-policy.json')) {
|
|
37
|
+
this.file = file;
|
|
38
|
+
}
|
|
39
|
+
config() { return { ...this.cfg }; }
|
|
40
|
+
updateConfig(patch) {
|
|
41
|
+
this.cfg = { ...this.cfg, ...patch };
|
|
42
|
+
}
|
|
43
|
+
/** 加载持久化 (daily spend 跨重启) */
|
|
44
|
+
async load() {
|
|
45
|
+
try {
|
|
46
|
+
const raw = JSON.parse(await fs.readFile(this.file, 'utf-8'));
|
|
47
|
+
if (raw.dailySpent !== undefined)
|
|
48
|
+
this._dailySpent = Number(raw.dailySpent) || 0;
|
|
49
|
+
if (raw.dayKey)
|
|
50
|
+
this._dayKey = raw.dayKey;
|
|
51
|
+
if (raw.config)
|
|
52
|
+
this.cfg = { ...DEFAULT_CONFIG, ...raw.config };
|
|
53
|
+
await this.resetIfNewDay();
|
|
54
|
+
}
|
|
55
|
+
catch { /* 无持久化, 用默认 */ }
|
|
56
|
+
}
|
|
57
|
+
async persist() {
|
|
58
|
+
try {
|
|
59
|
+
await fs.mkdir(path.dirname(this.file), { recursive: true });
|
|
60
|
+
await fs.writeFile(this.file, JSON.stringify({
|
|
61
|
+
dailySpent: this._dailySpent,
|
|
62
|
+
dayKey: this._dayKey,
|
|
63
|
+
config: this.cfg,
|
|
64
|
+
}, null, 2), 'utf-8');
|
|
65
|
+
}
|
|
66
|
+
catch { /* 持久化失败静默 */ }
|
|
67
|
+
}
|
|
68
|
+
async dailySpent() {
|
|
69
|
+
await this.resetIfNewDay();
|
|
70
|
+
return this._dailySpent;
|
|
71
|
+
}
|
|
72
|
+
async resetIfNewDay() {
|
|
73
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
74
|
+
if (this._dayKey !== today) {
|
|
75
|
+
this._dayKey = today;
|
|
76
|
+
this._dailySpent = 0;
|
|
77
|
+
this.payTimestamps = [];
|
|
78
|
+
await this.persist();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async check(intent) {
|
|
82
|
+
await this.resetIfNewDay();
|
|
83
|
+
const amount = Number(intent.amount) || 0;
|
|
84
|
+
const payTo = String(intent.payTo || '').toLowerCase();
|
|
85
|
+
const service = String(intent.service || '').toLowerCase();
|
|
86
|
+
// 1. 单笔上限
|
|
87
|
+
if (amount > this.cfg.perTransactionLimit) {
|
|
88
|
+
return { allowed: false, reason: `单笔超限: ${amount} > ${this.cfg.perTransactionLimit}`, dailySpent: this._dailySpent };
|
|
89
|
+
}
|
|
90
|
+
// 2. 收款方白名单
|
|
91
|
+
if (this.cfg.allowedRecipients.length > 0 &&
|
|
92
|
+
!this.cfg.allowedRecipients.some((r) => r.toLowerCase() === payTo)) {
|
|
93
|
+
return { allowed: false, reason: `收款方不在白名单: ${payTo}`, dailySpent: this._dailySpent };
|
|
94
|
+
}
|
|
95
|
+
// 3. 服务白名单
|
|
96
|
+
if (service && this.cfg.allowedServices.length > 0 &&
|
|
97
|
+
!this.cfg.allowedServices.some((s) => s.toLowerCase() === service)) {
|
|
98
|
+
return { allowed: false, reason: `服务不在白名单: ${service}`, dailySpent: this._dailySpent };
|
|
99
|
+
}
|
|
100
|
+
// 4. 每日预算
|
|
101
|
+
if (this._dailySpent + amount > this.cfg.dailyLimit) {
|
|
102
|
+
return { allowed: false, reason: `日预算超限: ${this._dailySpent} + ${amount} > ${this.cfg.dailyLimit}`, dailySpent: this._dailySpent };
|
|
103
|
+
}
|
|
104
|
+
// 5. 速率限制
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
this.payTimestamps = this.payTimestamps.filter((t) => now - t < 60_000);
|
|
107
|
+
if (this.payTimestamps.length >= this.cfg.rateLimitPerMinute) {
|
|
108
|
+
return { allowed: false, reason: `速率超限 (${this.cfg.rateLimitPerMinute}/min)`, dailySpent: this._dailySpent };
|
|
109
|
+
}
|
|
110
|
+
return { allowed: true, dailySpent: this._dailySpent };
|
|
111
|
+
}
|
|
112
|
+
async recordSpend(amount) {
|
|
113
|
+
await this.resetIfNewDay();
|
|
114
|
+
this._dailySpent += Number(amount) || 0;
|
|
115
|
+
this.payTimestamps.push(Date.now());
|
|
116
|
+
await this.persist();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
let _instance = null;
|
|
120
|
+
/** 获取 Policy Engine 单例 */
|
|
121
|
+
export function getEconomicPolicy() {
|
|
122
|
+
if (!_instance) {
|
|
123
|
+
const p = new LocalEconomicPolicy();
|
|
124
|
+
void p.load();
|
|
125
|
+
_instance = p;
|
|
126
|
+
}
|
|
127
|
+
return _instance;
|
|
128
|
+
}
|
|
129
|
+
/** 重置单例 (测试用) */
|
|
130
|
+
export function resetEconomicPolicy() {
|
|
131
|
+
_instance = null;
|
|
132
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* payment-approval.ts — 人工支付审批 (2026-08-13)
|
|
3
|
+
*
|
|
4
|
+
* YAML 支付验证门 (payment-gate) 判定 confirm 的支付请求 → 进入人工审批流程:
|
|
5
|
+
* - createApproval: 创建 pending 审批请求 (持久化)
|
|
6
|
+
* - approve(id): 人工批准 → 自动重试支付 (executor)
|
|
7
|
+
* - reject(id): 人工拒绝
|
|
8
|
+
* - list/pending: 查询
|
|
9
|
+
*
|
|
10
|
+
* 持久化: ~/.bolloon/payment-approvals.json
|
|
11
|
+
* 审批后执行: 注入 executor (serviceCall 重试 / 链上支付), 由调用方提供.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'fs/promises';
|
|
14
|
+
import * as os from 'os';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
const home = () => process.env.HOME || os.homedir() || '/tmp';
|
|
17
|
+
let _executor = null;
|
|
18
|
+
/** 注入批准后执行器 (serviceCall 重试 / 链上支付) */
|
|
19
|
+
export function setApprovalExecutor(fn) {
|
|
20
|
+
_executor = fn;
|
|
21
|
+
}
|
|
22
|
+
export class PaymentApprovalStore {
|
|
23
|
+
file;
|
|
24
|
+
approvals = [];
|
|
25
|
+
constructor(file = path.join(home(), '.bolloon', 'payment-approvals.json')) {
|
|
26
|
+
this.file = file;
|
|
27
|
+
}
|
|
28
|
+
async load() {
|
|
29
|
+
try {
|
|
30
|
+
const raw = JSON.parse(await fs.readFile(this.file, 'utf-8'));
|
|
31
|
+
if (Array.isArray(raw))
|
|
32
|
+
this.approvals = raw;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
this.approvals = [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async persist() {
|
|
39
|
+
try {
|
|
40
|
+
await fs.mkdir(path.dirname(this.file), { recursive: true });
|
|
41
|
+
await fs.writeFile(this.file, JSON.stringify(this.approvals, null, 2), 'utf-8');
|
|
42
|
+
}
|
|
43
|
+
catch { /* 静默 */ }
|
|
44
|
+
}
|
|
45
|
+
/** 创建审批请求 (pending) */
|
|
46
|
+
async create(req) {
|
|
47
|
+
await this.load();
|
|
48
|
+
const approval = {
|
|
49
|
+
id: `pay-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
50
|
+
service: req.service,
|
|
51
|
+
amount: req.amount,
|
|
52
|
+
recipient: req.recipient,
|
|
53
|
+
reason: req.reason,
|
|
54
|
+
status: 'pending',
|
|
55
|
+
createdAt: Date.now(),
|
|
56
|
+
retryPayload: req.retryPayload,
|
|
57
|
+
};
|
|
58
|
+
this.approvals.push(approval);
|
|
59
|
+
await this.persist();
|
|
60
|
+
return approval;
|
|
61
|
+
}
|
|
62
|
+
/** 待审批列表 */
|
|
63
|
+
async pending() {
|
|
64
|
+
await this.load();
|
|
65
|
+
return this.approvals.filter((a) => a.status === 'pending');
|
|
66
|
+
}
|
|
67
|
+
/** 全部 (含历史) */
|
|
68
|
+
async list() {
|
|
69
|
+
await this.load();
|
|
70
|
+
return [...this.approvals].reverse();
|
|
71
|
+
}
|
|
72
|
+
async get(id) {
|
|
73
|
+
await this.load();
|
|
74
|
+
return this.approvals.find((a) => a.id === id) ?? null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 人工批准 → 执行支付 (executor) → executed/failed.
|
|
78
|
+
* 未注入 executor 时直接标记 approved (调用方自行处理).
|
|
79
|
+
*/
|
|
80
|
+
async approve(id, approver = 'user') {
|
|
81
|
+
await this.load();
|
|
82
|
+
const a = this.approvals.find((x) => x.id === id);
|
|
83
|
+
if (!a)
|
|
84
|
+
return { ok: false, error: `审批 ${id} 不存在` };
|
|
85
|
+
if (a.status !== 'pending')
|
|
86
|
+
return { ok: false, error: `审批 ${id} 状态 ${a.status}, 不可批准` };
|
|
87
|
+
a.status = 'approved';
|
|
88
|
+
a.decidedAt = Date.now();
|
|
89
|
+
await this.persist();
|
|
90
|
+
// 批准后执行支付
|
|
91
|
+
if (_executor) {
|
|
92
|
+
const r = await _executor(a);
|
|
93
|
+
a.status = r.ok ? 'executed' : 'failed';
|
|
94
|
+
a.result = r.ok ? r.result : r.error;
|
|
95
|
+
await this.persist();
|
|
96
|
+
if (!r.ok)
|
|
97
|
+
return { ok: false, approval: a, error: r.error };
|
|
98
|
+
}
|
|
99
|
+
return { ok: true, approval: a };
|
|
100
|
+
}
|
|
101
|
+
/** 人工拒绝 */
|
|
102
|
+
async reject(id, approver = 'user') {
|
|
103
|
+
await this.load();
|
|
104
|
+
const a = this.approvals.find((x) => x.id === id);
|
|
105
|
+
if (!a)
|
|
106
|
+
return { ok: false, error: `审批 ${id} 不存在` };
|
|
107
|
+
if (a.status !== 'pending')
|
|
108
|
+
return { ok: false, error: `审批 ${id} 状态 ${a.status}, 不可拒绝` };
|
|
109
|
+
a.status = 'rejected';
|
|
110
|
+
a.decidedAt = Date.now();
|
|
111
|
+
await this.persist();
|
|
112
|
+
return { ok: true, approval: a };
|
|
113
|
+
}
|
|
114
|
+
/** 清理过期 pending (超时自动标记 rejected) */
|
|
115
|
+
async expireStale(timeoutMs = 60 * 60 * 1000) {
|
|
116
|
+
await this.load();
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
let expired = 0;
|
|
119
|
+
for (const a of this.approvals) {
|
|
120
|
+
if (a.status === 'pending' && now - a.createdAt > timeoutMs) {
|
|
121
|
+
a.status = 'rejected';
|
|
122
|
+
a.decidedAt = now;
|
|
123
|
+
a.result = '审批超时自动拒绝';
|
|
124
|
+
expired++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (expired > 0)
|
|
128
|
+
await this.persist();
|
|
129
|
+
return expired;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
let _store = null;
|
|
133
|
+
/** 单例 */
|
|
134
|
+
export function getApprovalStore() {
|
|
135
|
+
if (!_store)
|
|
136
|
+
_store = new PaymentApprovalStore();
|
|
137
|
+
return _store;
|
|
138
|
+
}
|
|
139
|
+
export function resetApprovalStore() {
|
|
140
|
+
_store = null;
|
|
141
|
+
_executor = null;
|
|
142
|
+
}
|