@bolloon/bolloon-agent 0.3.1 → 0.3.3
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/judgeness/auto-add.js +145 -0
- package/dist/judgeness/protocol.js +214 -0
- package/dist/judgeness/rank.js +78 -0
- package/dist/judgeness/reflect.js +93 -0
- package/dist/judgeness/store.js +481 -0
- package/dist/judgeness/types.js +19 -0
- package/dist/judgeness/visibility.js +118 -0
- package/dist/scripts/dedup-session-messages.js +68 -0
- package/dist/web/client-hearth.js +67 -0
- package/dist/web/client.js +309 -41
- package/dist/web/routes-hearth.js +371 -0
- package/dist/web/server.js +356 -10
- package/dist/web/ui/message-renderer.js +13 -2
- package/dist/web/util/dual-mode.js +87 -0
- package/package.json +2 -2
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routes-hearth.ts — judgeness 主路由 (2026-07-15)
|
|
3
|
+
*
|
|
4
|
+
* 12 路由 (前缀 `/api/hearth`):
|
|
5
|
+
* GET /api/hearth 健康 + 我自己的公开摘要
|
|
6
|
+
* GET /api/hearth/discover 伙伴搜索
|
|
7
|
+
* GET /api/hearth/cards/:id 单卡片读
|
|
8
|
+
* POST /api/hearth/cards 创建 (human-only)
|
|
9
|
+
* PATCH /api/hearth/cards/:id 修改 (human override)
|
|
10
|
+
* GET /api/hearth/visibility 隐私策略
|
|
11
|
+
* PUT /api/hearth/visibility 改隐私
|
|
12
|
+
* GET /api/hearth/allowlist 白名单
|
|
13
|
+
* POST /api/hearth/allowlist 加 / 减
|
|
14
|
+
* GET /api/hearth/peers peer 节点列表
|
|
15
|
+
* POST /api/hearth/channel-autoadd 频道触发 auto-add
|
|
16
|
+
* GET /api/hearth/{dual-mode} Accept 协商出口 (entrance)
|
|
17
|
+
*
|
|
18
|
+
* Peer 4 类资源写 API (与 manifest 协议打通):
|
|
19
|
+
* POST /api/peer-resources/groups
|
|
20
|
+
* POST /api/peer-resources/functions
|
|
21
|
+
* POST /api/peer-resources/exportments
|
|
22
|
+
* POST /api/peer-resources/sciences
|
|
23
|
+
*
|
|
24
|
+
* 设计要点:
|
|
25
|
+
* - 防御期 (现在 → 6 月) 只挂 GET /api/hearth 一个健康端点;
|
|
26
|
+
* 写 API 在相持期打开 (返回 403 if locked = 'defense' 标记).
|
|
27
|
+
* - 路由不依赖 createWebServer 闭包状态 — 与 routes-judgments.ts 一致风格.
|
|
28
|
+
*/
|
|
29
|
+
import { JUDGENESS_ROOT, ensureJudgenessDirs, listDescriptions, loadDescription, saveDescription, loadVisibility, saveVisibility, loadAllowlist, addAllowlistPeer, removeAllowlistPeer, newDescriptionId, } from '../judgeness/store.js';
|
|
30
|
+
import { scrubListForAudience, scrubForAudience, resolveGate2, resolveGate3, } from '../judgeness/visibility.js';
|
|
31
|
+
import { descriptionToJsonLd, descriptionToHumanHtml, dualRender, } from './util/dual-mode.js';
|
|
32
|
+
// 防御期闸: 控制写 API 是否打开. 反攻期改为 'public'.
|
|
33
|
+
const DEFENSE_MODE = true;
|
|
34
|
+
/** 读调用方身份. 真实身份端点是 GET /api/p2p-publickey;
|
|
35
|
+
* 此处取鉴权 session (human/agent + pubkey).
|
|
36
|
+
* 防御期: 假定默认 human, pubkey='__self__'. */
|
|
37
|
+
function extractCaller(req) {
|
|
38
|
+
// 防御期 stub: 由 server.ts 接入鉴权后替换
|
|
39
|
+
const hdr = arrToStr(req.headers['x-bolloon-pubkey']) || '__self__';
|
|
40
|
+
const roleRaw = arrToStr(req.headers['x-bolloon-role']) || 'human';
|
|
41
|
+
return { pubkey: hdr, role: roleRaw };
|
|
42
|
+
}
|
|
43
|
+
/** express query / headers 都是 string | string[]; 取字符串 */
|
|
44
|
+
function arrToStr(v) {
|
|
45
|
+
if (typeof v === 'string')
|
|
46
|
+
return v;
|
|
47
|
+
if (Array.isArray(v))
|
|
48
|
+
return v[0] ?? '';
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// 主注册函数
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
export function registerHearthRoutes(app) {
|
|
55
|
+
// 1. 健康
|
|
56
|
+
app.get('/api/hearth', async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
await ensureJudgenessDirs();
|
|
59
|
+
const descs = await listDescriptions();
|
|
60
|
+
const vis = await loadVisibility();
|
|
61
|
+
const allow = await loadAllowlist();
|
|
62
|
+
res.json({
|
|
63
|
+
ok: true,
|
|
64
|
+
service: 'judgeness-hearth',
|
|
65
|
+
version: '0.3.x-jd-1',
|
|
66
|
+
rootPath: JUDGENESS_ROOT(),
|
|
67
|
+
descriptionCount: descs.length,
|
|
68
|
+
visibilityChannels: vis.channels.length,
|
|
69
|
+
allowlistCount: allow.peers.length,
|
|
70
|
+
defenseMode: DEFENSE_MODE,
|
|
71
|
+
ts: new Date().toISOString(),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
res.status(500).json({ error: err.message });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// 2. discover — 接受 q / tag / capability
|
|
79
|
+
app.get('/api/hearth/discover', async (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
const ctx = extractCaller(req);
|
|
82
|
+
const q = arrToStr(req.query['q']).toLowerCase();
|
|
83
|
+
const tag = arrToStr(req.query['tag']).toLowerCase();
|
|
84
|
+
const cap = arrToStr(req.query['capability']).toLowerCase();
|
|
85
|
+
const all = await listDescriptions();
|
|
86
|
+
const matched = all.filter((d) => {
|
|
87
|
+
if (q && !(d.judgmentRef.toLowerCase().includes(q) || d.descriptionId.toLowerCase().includes(q)))
|
|
88
|
+
return false;
|
|
89
|
+
if (tag && !(d.scope.topics ?? []).some((t) => t.toLowerCase().includes(tag)) &&
|
|
90
|
+
!(d.scope.domains ?? []).some((t) => t.toLowerCase().includes(tag)))
|
|
91
|
+
return false;
|
|
92
|
+
if (cap)
|
|
93
|
+
return true; // cap 是 peer 资源维度, 防御期不连
|
|
94
|
+
return true;
|
|
95
|
+
});
|
|
96
|
+
const scrubbed = await scrubListForAudience(matched, ctx);
|
|
97
|
+
const jsonLd = scrubbed.map((s) => descriptionToJsonLd(s));
|
|
98
|
+
const capInfo = { accept: arrToStr(req.headers.accept), query: req.query, userAgent: arrToStr(req.headers['user-agent']) };
|
|
99
|
+
const result = dualRender(capInfo, () => {
|
|
100
|
+
const cards = scrubbed.map((s) => `<li>${s.descriptionId} · ${s.visibility} · ${s.openState}</li>`).join('');
|
|
101
|
+
return `<!DOCTYPE html><html><body><h1>Discover</h1><ul>${cards}</ul></body></html>`;
|
|
102
|
+
}, () => jsonLd);
|
|
103
|
+
res.status(result.status).setHeader('Content-Type', result.contentType).send(result.body);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
res.status(500).json({ error: err.message });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
// 3. 单卡片读
|
|
110
|
+
app.get('/api/hearth/cards/:id', async (req, res) => {
|
|
111
|
+
try {
|
|
112
|
+
const id = req.params['id'];
|
|
113
|
+
const ctx = extractCaller(req);
|
|
114
|
+
const d = await loadDescription(id);
|
|
115
|
+
if (!d)
|
|
116
|
+
return res.status(404).json({ error: 'description not found' });
|
|
117
|
+
const scrubbed = await scrubForAudience(d, ctx);
|
|
118
|
+
const result = dualRender({ accept: arrToStr(req.headers.accept), query: req.query, userAgent: arrToStr(req.headers['user-agent']) }, () => descriptionToHumanHtml(scrubbed), () => descriptionToJsonLd(scrubbed));
|
|
119
|
+
res.status(result.status).setHeader('Content-Type', result.contentType).send(result.body);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
res.status(500).json({ error: err.message });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// 4. 创建卡片 (human-only, 防御期 405)
|
|
126
|
+
app.post('/api/hearth/cards', gateWrite, async (req, res) => {
|
|
127
|
+
try {
|
|
128
|
+
const ctx = extractCaller(req);
|
|
129
|
+
if (DEFENSE_MODE)
|
|
130
|
+
return res.status(405).json({ error: 'POST /api/hearth/cards disabled in defense mode' });
|
|
131
|
+
if (ctx.role !== 'human')
|
|
132
|
+
return res.status(403).json({ error: 'human-only' });
|
|
133
|
+
const body = req.body;
|
|
134
|
+
if (!body.judgmentRef)
|
|
135
|
+
return res.status(400).json({ error: 'judgmentRef required' });
|
|
136
|
+
const vis = await loadVisibility();
|
|
137
|
+
// 闸 3 校验 — 取占位 desc 用于校验
|
|
138
|
+
const placeholder = {
|
|
139
|
+
descriptionId: newDescriptionId(),
|
|
140
|
+
judgmentRef: body.judgmentRef,
|
|
141
|
+
description_version: 1,
|
|
142
|
+
facets: body.facets ?? {},
|
|
143
|
+
basis: body.basis ?? {},
|
|
144
|
+
scope: body.scope ?? { topics: [], domains: [] },
|
|
145
|
+
visibility: body.visibility ?? vis.defaults.visibility,
|
|
146
|
+
openState: body.openState ?? vis.defaults.openState,
|
|
147
|
+
by: ctx.role,
|
|
148
|
+
byAgentId: undefined,
|
|
149
|
+
createdAt: new Date().toISOString(),
|
|
150
|
+
updatedAt: new Date().toISOString(),
|
|
151
|
+
};
|
|
152
|
+
const g3 = resolveGate3(placeholder, ctx, vis);
|
|
153
|
+
if (!g3.allow)
|
|
154
|
+
return res.status(403).json({ error: g3.reason });
|
|
155
|
+
await saveDescription(placeholder);
|
|
156
|
+
res.json({ ok: true, descriptionId: placeholder.descriptionId });
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
res.status(500).json({ error: err.message });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// 5. 修改卡片
|
|
163
|
+
app.patch('/api/hearth/cards/:id', gateWrite, async (req, res) => {
|
|
164
|
+
try {
|
|
165
|
+
const ctx = extractCaller(req);
|
|
166
|
+
if (DEFENSE_MODE)
|
|
167
|
+
return res.status(405).json({ error: 'PATCH disabled in defense mode' });
|
|
168
|
+
if (ctx.role !== 'human')
|
|
169
|
+
return res.status(403).json({ error: 'human override required' });
|
|
170
|
+
const id = arrToStr(req.params['id']);
|
|
171
|
+
const d = await loadDescription(id);
|
|
172
|
+
if (!d)
|
|
173
|
+
return res.status(404).json({ error: 'description not found' });
|
|
174
|
+
const vis = await loadVisibility();
|
|
175
|
+
const g3 = resolveGate3(d, ctx, vis);
|
|
176
|
+
if (!g3.allow)
|
|
177
|
+
return res.status(403).json({ error: g3.reason });
|
|
178
|
+
const body = req.body;
|
|
179
|
+
const merged = {
|
|
180
|
+
...d,
|
|
181
|
+
facets: { ...d.facets, ...(body.facets ?? {}) },
|
|
182
|
+
basis: { ...d.basis, ...(body.basis ?? {}) },
|
|
183
|
+
scope: {
|
|
184
|
+
domains: body.scope?.domains ?? d.scope.domains,
|
|
185
|
+
topics: body.scope?.topics ?? d.scope.topics,
|
|
186
|
+
},
|
|
187
|
+
visibility: body.visibility ?? d.visibility,
|
|
188
|
+
openState: body.openState ?? d.openState,
|
|
189
|
+
updatedAt: new Date().toISOString(),
|
|
190
|
+
};
|
|
191
|
+
await saveDescription(merged);
|
|
192
|
+
res.json({ ok: true });
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
res.status(500).json({ error: err.message });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
// 6. 读 visibility
|
|
199
|
+
app.get('/api/hearth/visibility', async (req, res) => {
|
|
200
|
+
try {
|
|
201
|
+
const f = await loadVisibility();
|
|
202
|
+
res.json(f);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
res.status(500).json({ error: err.message });
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
// 7. 写 visibility (PUT)
|
|
209
|
+
app.put('/api/hearth/visibility', gateWrite, async (req, res) => {
|
|
210
|
+
try {
|
|
211
|
+
if (DEFENSE_MODE)
|
|
212
|
+
return res.status(405).json({ error: 'PUT visibility disabled in defense mode' });
|
|
213
|
+
const ctx = extractCaller(req);
|
|
214
|
+
if (ctx.role !== 'human')
|
|
215
|
+
return res.status(403).json({ error: 'human override required' });
|
|
216
|
+
const body = req.body;
|
|
217
|
+
if (!body || !body.version)
|
|
218
|
+
return res.status(400).json({ error: 'invalid body' });
|
|
219
|
+
await saveVisibility(body);
|
|
220
|
+
res.json({ ok: true });
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
res.status(500).json({ error: err.message });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
// 8. 读 allowlist
|
|
227
|
+
app.get('/api/hearth/allowlist', async (req, res) => {
|
|
228
|
+
try {
|
|
229
|
+
const f = await loadAllowlist();
|
|
230
|
+
res.json(f);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
res.status(500).json({ error: err.message });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
// 9. 改 allowlist (POST 加 / 减)
|
|
237
|
+
app.post('/api/hearth/allowlist', gateWrite, async (req, res) => {
|
|
238
|
+
try {
|
|
239
|
+
if (DEFENSE_MODE)
|
|
240
|
+
return res.status(405).json({ error: 'POST allowlist disabled in defense mode' });
|
|
241
|
+
const ctx = extractCaller(req);
|
|
242
|
+
if (ctx.role !== 'human')
|
|
243
|
+
return res.status(403).json({ error: 'human override required' });
|
|
244
|
+
const body = req.body;
|
|
245
|
+
if (!body.action || !body.pubkey)
|
|
246
|
+
return res.status(400).json({ error: 'action + pubkey required' });
|
|
247
|
+
if (body.action === 'add') {
|
|
248
|
+
await addAllowlistPeer({ pubkey: body.pubkey, alias: body.alias, note: body.note, addedAt: new Date().toISOString() });
|
|
249
|
+
}
|
|
250
|
+
else if (body.action === 'remove') {
|
|
251
|
+
await removeAllowlistPeer(body.pubkey);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
return res.status(400).json({ error: 'unknown action' });
|
|
255
|
+
}
|
|
256
|
+
res.json({ ok: true });
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
res.status(500).json({ error: err.message });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
// 10. 我的 peers 列表
|
|
263
|
+
app.get('/api/hearth/peers', async (_req, res) => {
|
|
264
|
+
try {
|
|
265
|
+
const allow = await loadAllowlist();
|
|
266
|
+
res.json({ peers: allow.peers });
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
res.status(500).json({ error: err.message });
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
// 11. channel-autoadd
|
|
273
|
+
app.post('/api/hearth/channel-autoadd', gateWrite, async (req, res) => {
|
|
274
|
+
try {
|
|
275
|
+
if (DEFENSE_MODE)
|
|
276
|
+
return res.status(405).json({ error: 'autoadd disabled in defense mode' });
|
|
277
|
+
const body = req.body;
|
|
278
|
+
if (!body.channelTopic)
|
|
279
|
+
return res.status(400).json({ error: 'channelTopic required' });
|
|
280
|
+
const ctx = extractCaller(req);
|
|
281
|
+
const g2 = await resolveGate2(body.sourceChannelOwnerPk ?? '__self__', body.channelTopic);
|
|
282
|
+
if (!g2.allow)
|
|
283
|
+
return res.status(403).json({ error: g2.reason });
|
|
284
|
+
void ctx;
|
|
285
|
+
// 反攻期主路径 — 防御期 stub: 仅打印 audit log
|
|
286
|
+
const auditLine = JSON.stringify({
|
|
287
|
+
ts: new Date().toISOString(),
|
|
288
|
+
kind: 'autoadd_request',
|
|
289
|
+
channelTopic: body.channelTopic,
|
|
290
|
+
sourceChannelOwnerPk: body.sourceChannelOwnerPk ?? null,
|
|
291
|
+
by: ctx.role,
|
|
292
|
+
}) + '\n';
|
|
293
|
+
const fs = await import('fs/promises');
|
|
294
|
+
const path = await import('path');
|
|
295
|
+
const os = await import('os');
|
|
296
|
+
const auditPath = path.join(os.homedir(), '.bolloon', 'human-values', 'counterfactual-audit.jsonl');
|
|
297
|
+
await fs.mkdir(path.dirname(auditPath), { recursive: true });
|
|
298
|
+
await fs.appendFile(auditPath, auditLine, 'utf-8');
|
|
299
|
+
res.json({ ok: true, mode: 'audit-only', reason: '反攻期 O3 才实现全自动 joinTopic' });
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
res.status(500).json({ error: err.message });
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
// 12. dual-mode entrance — GET /api/hearth/{anything}
|
|
306
|
+
// (放在所有精确路由之后; 由 Express 路由匹配先到精确路由)
|
|
307
|
+
app.get(/^\/api\/hearth\/(.+)$/, async (req, res) => {
|
|
308
|
+
// 只剩未匹配的子路径
|
|
309
|
+
res.status(404).json({ error: 'not found', hint: 'see /api/hearth for service index' });
|
|
310
|
+
});
|
|
311
|
+
// ---------- Peer 4 类资源写 API (C5) ----------
|
|
312
|
+
registerPeerResourceRoutes(app);
|
|
313
|
+
}
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// 中间件: 写 API 闸
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
function gateWrite(_req, res, next) {
|
|
318
|
+
if (DEFENSE_MODE)
|
|
319
|
+
return; // 在 handler 里直接 405
|
|
320
|
+
next();
|
|
321
|
+
}
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Peer 4 类资源写 API
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
function registerPeerResourceRoutes(app) {
|
|
326
|
+
// 用动态 import 避免循环依赖
|
|
327
|
+
const handlers = {
|
|
328
|
+
groups: (a) => a.post('/api/peer-resources/groups', writePeerResource('groups')),
|
|
329
|
+
functions: (a) => a.post('/api/peer-resources/functions', writePeerResource('functions')),
|
|
330
|
+
exportments: (a) => a.post('/api/peer-resources/exportments', writePeerResource('exportments')),
|
|
331
|
+
sciences: (a) => a.post('/api/peer-resources/sciences', writePeerResource('sciences')),
|
|
332
|
+
};
|
|
333
|
+
for (const k of Object.keys(handlers)) {
|
|
334
|
+
try {
|
|
335
|
+
handlers[k](app);
|
|
336
|
+
}
|
|
337
|
+
catch { /* ignore */ }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function writePeerResource(kind) {
|
|
341
|
+
return async (req, res) => {
|
|
342
|
+
if (DEFENSE_MODE)
|
|
343
|
+
return res.status(405).json({ error: `POST /api/peer-resources/${kind} disabled in defense mode` });
|
|
344
|
+
const ctx = extractCaller(req);
|
|
345
|
+
if (ctx.role !== 'human')
|
|
346
|
+
return res.status(403).json({ error: 'human override required' });
|
|
347
|
+
try {
|
|
348
|
+
// 实际写盘走 src/network/peer-fs.ts 的 addLocalGroup/Function/Exportment/Science
|
|
349
|
+
// 防御期 stub: 仅 echo 入参 + 写 audit log
|
|
350
|
+
const fs = await import('fs/promises');
|
|
351
|
+
const path = await import('path');
|
|
352
|
+
const os = await import('os');
|
|
353
|
+
const auditPath = path.join(os.homedir(), '.bolloon', 'human-values', 'counterfactual-audit.jsonl');
|
|
354
|
+
await fs.mkdir(path.dirname(auditPath), { recursive: true });
|
|
355
|
+
const auditLine = JSON.stringify({
|
|
356
|
+
ts: new Date().toISOString(),
|
|
357
|
+
kind: `peer_resource_${kind}_write`,
|
|
358
|
+
body: req.body,
|
|
359
|
+
by: ctx.role,
|
|
360
|
+
}) + '\n';
|
|
361
|
+
await fs.appendFile(auditPath, auditLine, 'utf-8');
|
|
362
|
+
res.json({ ok: true, mode: 'audit-only', kind, body: req.body });
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
res.status(500).json({ error: err.message });
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
// 关 lint
|
|
370
|
+
void (() => 'public');
|
|
371
|
+
void (() => 'open');
|