@bolloon/bolloon-agent 0.4.32 → 0.4.33

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.
@@ -71,6 +71,10 @@ const OPTIONS_WITH_VALUE = new Set([
71
71
  '--confirmation-window', '--proof-version', '--from-block', '--gate',
72
72
  // 2026-09-22 (P6b): 链上**写**操作的**授权意图**声明 (MCP 写 tool 必填; 只可能收紧, 不可能放权)
73
73
  '--payment-mode',
74
+ // 2026-09-23 (C1): 任务公告板 `bolloon task publish|board|claim` 的显式公告号
75
+ '--announcement-id',
76
+ // 2026-09-23 (C7): 群聊通道 `bolloon task announce|trail|post` 的群链接/groupId + 极短事实字段
77
+ '--group', '--round', '--criteria', '--kind', '--hash', '--bytes', '--checks', '--verdict', '--limit', '--from',
74
78
  ]);
75
79
  export function parseFlags(args) {
76
80
  const flags = { json: false, quiet: false, positionals: [], options: new Map(), raw: args };
package/dist/cli-entry.js CHANGED
@@ -30,7 +30,7 @@ import { legacyJson } from './cli/protocol-envelope.js';
30
30
  import { createRequire } from 'module';
31
31
  const _require = createRequire(import.meta.url);
32
32
  /** P3 `bolloon task <子命令>` 的已知子命令 —— 其它一律当 M1 任务正文 (既有体验不动) */
33
- const TASK_SUBCOMMANDS = new Set(['send', 'list', 'status', 'cancel', 'retry', 'result', 'inbox', 'accept', 'reject', 'run', 'complete']);
33
+ const TASK_SUBCOMMANDS = new Set(['send', 'list', 'status', 'cancel', 'retry', 'result', 'inbox', 'accept', 'reject', 'run', 'complete', 'publish', 'board', 'claim']);
34
34
  const isWindows = process.platform === 'win32';
35
35
  // ANSI 颜色 — Bolloon Web UI 配色 truecolor
36
36
  function _fg(r, g, b) { return `\x1b[38;2;${r};${g};${b}m`; }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * explorer.ts — 区块浏览器映射 (chainId → 公网浏览器) + **交易**链接构造
4
+ *
5
+ * 只回答一件事: **这条链有没有已知的公网浏览器**。
6
+ * · 有 → `https://<explorer>/tx/0x…` (公开可核验的链上事实)
7
+ * · 认不出 / 本机隔离开发链 (31337) → **null** —— 没有公网浏览器就**不给链接**,
8
+ * 绝不编造 `href="#"`、死链或猜一个域名 (宁缺勿错)。
9
+ *
10
+ * 隐私红线 (2026-09-23 leo 拍板; 同日收窄):
11
+ * · 允许出现在公开快照里的 0x 字符串**只有两种**: 交易哈希 (0x+64 hex) 与 escrow **合约**地址
12
+ * (0x+40 hex) —— 它们本来就是公开链上事实。
13
+ * · 但**页面只可点交易**: escrow 合约地址保留在行数据里供索引/诊断, **不渲染、不生成合约链接**
14
+ * (所以本文件**没有** address 链接构造器 —— 少一个能凭空造出合约链接的入口)。
15
+ * · EOA / 钱包地址 (买方·卖方·payTo 之类)、taskKey 原文、taskId、args 里的地址、DID、
16
+ * peer IP / multiaddr、私钥 —— **一个都不许**经这里或别处进快照。
17
+ * · 本文件只构造 URL, 不读盘不发 RPC, 也从不接受「调用方说它是合约地址」的自我声明。
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.EXPLORER_URL_RE = exports.ADDRESS_RE = exports.TX_HASH_RE = exports.EXPLORER_BY_CHAIN = void 0;
21
+ exports.explorerBaseUrl = explorerBaseUrl;
22
+ exports.explorerTxUrl = explorerTxUrl;
23
+ exports.isExplorerUrl = isExplorerUrl;
24
+ /** chainId → 公网浏览器 base URL (白名单; 认不出的一律没有) */
25
+ exports.EXPLORER_BY_CHAIN = Object.freeze({
26
+ 8453: 'https://basescan.org', // Base 主网
27
+ 84532: 'https://sepolia.basescan.org', // Base Sepolia 测试网
28
+ 1: 'https://etherscan.io', // Ethereum 主网
29
+ 11155111: 'https://sepolia.etherscan.io', // Sepolia 测试网
30
+ });
31
+ /** 交易哈希: 小写 0x + 64 位十六进制 (词法唯一形状) */
32
+ exports.TX_HASH_RE = /^0x[0-9a-f]{64}$/;
33
+ /** 地址: 小写 0x + 40 位十六进制 (合约地址同样长这样 —— 是**谁**由调用方的白名单决定) */
34
+ exports.ADDRESS_RE = /^0x[0-9a-f]{40}$/;
35
+ /**
36
+ * 浏览器**交易**链接形状 (反向校验用): https + 已知浏览器域名 + `/tx/0x64hex`。
37
+ * 给守卫 / 单测一把「这不是我们造的链接」的尺子 —— 端口 / 凭据 / 别的路径 (含 `/address/`) 一律不认。
38
+ */
39
+ exports.EXPLORER_URL_RE = /^https:\/\/(?:[a-z0-9-]+\.)*(?:basescan\.org|etherscan\.io)\/tx\/0x[0-9a-f]{64}$/;
40
+ /** chainId 有没有已知公网浏览器 → base URL; 认不出 (含本机 31337) → null */
41
+ function explorerBaseUrl(chainId) {
42
+ const id = Number(chainId);
43
+ if (!Number.isInteger(id) || id < 0)
44
+ return null;
45
+ return exports.EXPLORER_BY_CHAIN[id] ?? null;
46
+ }
47
+ /** 交易链接; 链没浏览器 / 哈希非法 → null (调用方**不要**填 null 之外的占位) */
48
+ function explorerTxUrl(chainId, txHash) {
49
+ const base = explorerBaseUrl(chainId);
50
+ const h = String(txHash ?? '').toLowerCase();
51
+ if (!base || !exports.TX_HASH_RE.test(h))
52
+ return null;
53
+ return `${base}/tx/${h}`;
54
+ }
55
+ /** 这个字符串是不是我们亲手造的那种**交易**浏览器链接 (守卫 / 单测用) */
56
+ function isExplorerUrl(url) {
57
+ return typeof url === 'string' && exports.EXPLORER_URL_RE.test(url);
58
+ }
59
+ //# sourceMappingURL=explorer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explorer.js","sourceRoot":"","sources":["../../../../src/agents/chain/explorer.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;;;;AAEH,+CAA+C;AAClC,QAAA,iBAAiB,GAAqC,MAAM,CAAC,MAAM,CAAC;IAC/E,IAAI,EAAE,sBAAsB,EAAa,UAAU;IACnD,KAAK,EAAE,8BAA8B,EAAI,mBAAmB;IAC5D,CAAC,EAAE,sBAAsB,EAAgB,cAAc;IACvD,QAAQ,EAAE,8BAA8B,EAAE,cAAc;CACzD,CAAC,CAAC;AAEH,sCAAsC;AACzB,QAAA,UAAU,GAAG,kBAAkB,CAAC;AAC7C,2DAA2D;AAC9C,QAAA,UAAU,GAAG,kBAAkB,CAAC;AAE7C;;;GAGG;AACU,QAAA,eAAe,GAAG,kFAAkF,CAAC;AAElH,4DAA4D;AAC5D,yBAAgC,OAAe;IAC7C,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC3B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACjD,OAAO,QAAA,iBAAiB,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC;AACvC,CAAC;AAED,wDAAwD;AACxD,uBAA8B,OAAe,EAAE,MAAc;IAC3D,MAAM,IAAI,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IAC7C,IAAI,CAAC,IAAI,IAAI,CAAC,QAAA,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9C,OAAO,GAAG,IAAI,OAAO,CAAC,EAAE,CAAC;AAC3B,CAAC;AAED,6CAA6C;AAC7C,uBAA8B,GAAY;IACxC,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,QAAA,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ /**
3
+ * local-signer.ts — 本机 DID 身份签名器 (P3, 2026-09-21)
4
+ *
5
+ * `bolloon-task/1` 的每一个信封 (request / accept / reject / result) 都必须带签名。
6
+ * 签名用的是本机 DIAP 身份 (`~/.bolloon/identity.json`, ed25519) —— **私钥只在本机进程内存里**,
7
+ * 本模块**只返回可用于签名的 keypair 对象**, 绝不返回私钥字符串/hex, 也不打印任何密钥材料。
8
+ *
9
+ * 红线 (P1 §5.5):
10
+ * · 私钥永不进 stdout/日志/审计账本;
11
+ * · 公钥 (hex) 可以随帧一起走 P2P —— 对端需要它来验签, 它是公开材料;
12
+ * · 没有本机身份 → 返回 null, 调用方必须**如实失败**, 不许用假签名糊过去。
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.identityFile = identityFile;
49
+ exports.loadLocalSigner = loadLocalSigner;
50
+ exports.localPublicKeyHex = localPublicKeyHex;
51
+ exports.verifierFor = verifierFor;
52
+ exports.fingerprintOf = fingerprintOf;
53
+ exports.payloadDigest = payloadDigest;
54
+ const os = __importStar(require("os"));
55
+ const path = __importStar(require("path"));
56
+ const crypto = __importStar(require("crypto"));
57
+ /** 本机身份文件 (与 network-pulse 快照签名同源: ~/.bolloon/identity.json) */
58
+ function identityFile(home) {
59
+ return path.join(home || process.env.HOME || os.homedir(), '.bolloon', 'identity.json');
60
+ }
61
+ /**
62
+ * 加载本机签名器。文件缺失/损坏 → null (调用方如实报"没有可签名的身份")。
63
+ */
64
+ async function loadLocalSigner(home) {
65
+ try {
66
+ const file = identityFile(home);
67
+ const { KeyManager } = (await Promise.resolve().then(() => __importStar(require('@diap/sdk'))));
68
+ const kp = await KeyManager.fromFile(file);
69
+ if (!kp?.privateKey || !kp?.publicKey)
70
+ return null;
71
+ return { did: String(kp.did || ''), publicKeyHex: Buffer.from(kp.publicKey).toString('hex'), keypair: kp };
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ }
77
+ /** 本机公钥 hex (给帧的信封用; 没有身份 → null) */
78
+ async function localPublicKeyHex(home) {
79
+ const s = await loadLocalSigner(home);
80
+ return s?.publicKeyHex ?? null;
81
+ }
82
+ /**
83
+ * 从公钥 hex 造一个 `KeyManager.verify` 能吃的 verifier。
84
+ * (@diap/sdk 的 verify 只用到 `publicKey` 字段 —— 它改不了密钥的用途, 这里不涉及任何私钥。)
85
+ */
86
+ function verifierFor(publicKeyHex) {
87
+ if (!/^[0-9a-f]{64}$/i.test(String(publicKeyHex || '')))
88
+ return null;
89
+ return { publicKey: new Uint8Array(Buffer.from(publicKeyHex, 'hex')) };
90
+ }
91
+ /**
92
+ * 指纹 (sha256 前 12 位 hex) —— 只用于日志/审计的"是谁签的", **不可逆, 不含密钥材料**。
93
+ */
94
+ function fingerprintOf(input) {
95
+ return `sha256:${crypto.createHash('sha256').update(String(input || '')).digest('hex').slice(0, 12)}`;
96
+ }
97
+ /** payload 摘要 (审计账本只记摘要, 不记正文) */
98
+ function payloadDigest(payload) {
99
+ return crypto.createHash('sha256').update(String(payload ?? '')).digest('hex');
100
+ }
101
+ //# sourceMappingURL=local-signer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-signer.js","sourceRoot":"","sources":["../../../src/agents/local-signer.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEH,MAAY,EAAE,+BAAW;AACzB,MAAY,IAAI,iCAAa;AAC7B,MAAY,MAAM,mCAAe;AAWjC,gEAAgE;AAChE,sBAA6B,IAAa;IACxC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,eAAe,CAAC,CAAC;AAC1F,CAAC;AAED;;GAEG;AACI,KAAK,0BAA0B,IAAa;IACjD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAChC,MAAM,EAAE,UAAU,EAAE,GAAG,CAAC,wDAAa,WAAW,GAAC,CAAQ,CAAC;QAC1D,MAAM,EAAE,GAAQ,MAAO,UAAkB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACzD,IAAI,CAAC,EAAE,EAAE,UAAU,IAAI,CAAC,EAAE,EAAE,SAAS;YAAE,OAAO,IAAI,CAAC;QACnD,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,SAAuB,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAC3H,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,qCAAqC;AAC9B,KAAK,4BAA4B,IAAa;IACnD,MAAM,CAAC,GAAG,MAAM,eAAe,CAAC,IAAI,CAAC,CAAC;IACtC,OAAO,CAAC,EAAE,YAAY,IAAI,IAAI,CAAC;AACjC,CAAC;AAED;;;GAGG;AACH,qBAA4B,YAAoB;IAC9C,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACrE,OAAO,EAAE,SAAS,EAAE,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;AACzE,CAAC;AAED;;GAEG;AACH,uBAA8B,KAAa;IACzC,OAAO,UAAU,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AACxG,CAAC;AAED,kCAAkC;AAClC,uBAA8B,OAAe;IAC3C,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACjF,CAAC"}
@@ -11,10 +11,19 @@
11
11
  * · 快照过期 → `stale` (不许伪装实时); 观察层不可用 → `unavailable`
12
12
  * · 类别小于隐私阈值 → 合并进 other
13
13
  * · 事件数 / 时间窗 / capability 数都有上限
14
- * · **冻结形状 `confirmed_activity`** (2026-09-22): 公开页面要能列出「哪个任务 · 什么状态 ·
15
- * 哪个块 · 多少确认」, 而不是只有数字 —— 数据源优先 P5 链上索引, 不可用时退回脉冲事件并
16
- * 用 `confirmed_activity_source` 标注; 标识一律 sha256 短写 (前 8 位), 绝不落 taskKey /
17
- * taskId / txHash 原文; 不够确认门槛的行只报 observed, 不冒充 confirmed。
14
+ * · **冻结形状 `confirmed_activity`** (2026-09-22; 2026-09-23 加链上可核验字段): 公开页面要能列出
15
+ * 「哪个任务 · 什么状态 · 哪个块 · 多少确认」, 而不是只有数字 —— 数据源优先 P5 链上索引, 不可用时
16
+ * 退回脉冲事件并用 `confirmed_activity_source` 标注; 任务标识一律 sha256 短写 (前 8 位), 绝不落
17
+ * taskKey / taskId 原文; 不够确认门槛的行只报 observed, 不冒充 confirmed。
18
+ * 2026-09-23 隐私决定**变更一处** (只放开公开链上事实, 别的一律不变): 交易哈希 (`tx_hash`) 与
19
+ * escrow **合约**地址 (`contract`, 只给索引/诊断) 允许出现; 页面可点的只有**交易**链接 `explorer_tx`
20
+ * (仅当该链有已知公网浏览器)。EOA / 钱包地址 (买方·卖方)、taskKey 原文、taskId、args 里的地址、
21
+ * DID、peer IP/multiaddr、私钥 —— 仍然一个都不许出现; 老字段 `tx` (sha256 短写) 保留不删。
22
+ * · **冻结形状 `open_tasks[]`** (2026-09-23): 公开页要能列「本节点公告板上哪些任务还没被接单」。
23
+ * 数据源 = `~/.bolloon/tasks/board/*.json` 里**未认领且未过期**的公告, 每行只含白名单 7 个字段:
24
+ * `capability` / `budget`(原子单位) / `currency` / `network` / `deadline` / `claimed`(恒 false) /
25
+ * `announcementId`(前 8 位)。**任务正文与它的摘要/预览、买方 DID 与公钥、认领者、公告签名
26
+ * 一律不进公开投影** —— 行由 `buildOpenTaskRow` 逐字段拷贝构造, 不做对象展平。
18
27
  *
19
28
  * 借鉴 (只借产品与工程思想, 不借 Go/Postgres/API 项目):
20
29
  * EigenFlux 的 "注册总量 / 当前活跃 / 最近出现" 时间窗统计、服务端生成匿名活动文本、
@@ -54,7 +63,7 @@ var __importStar = (this && this.__importStar) || (function () {
54
63
  };
55
64
  })();
56
65
  Object.defineProperty(exports, "__esModule", { value: true });
57
- exports.FORBIDDEN_PUBLIC_KEYS = exports.CHAIN_LABELS = exports.PUBLIC_MAINNET_CHAIN_ID = exports.PUBLIC_TESTNET_CHAIN_ID = exports.PULSE_EVENT_ACTIVITY = exports.CHAIN_EVENT_ACTIVITY = exports.CONFIRMED_ACTIVITY_LIMIT = exports.MAX_AGENT_SITES = exports.pulseDir = exports.NETWORK_EVENT_TYPES = exports.PULSE_LIMITS = void 0;
66
+ exports.PUBLIC_HEX_KEYS = exports.FORBIDDEN_PUBLIC_KEYS = exports.OPEN_TASK_KEYS = exports.CHAIN_LABELS = exports.PUBLIC_MAINNET_CHAIN_ID = exports.PUBLIC_TESTNET_CHAIN_ID = exports.PULSE_EVENT_ACTIVITY = exports.CHAIN_EVENT_ACTIVITY = exports.CONFIRMED_ACTIVITY_LIMIT = exports.MAX_OPEN_TASKS = exports.MAX_AGENT_SITES = exports.pulseDir = exports.NETWORK_EVENT_TYPES = exports.PULSE_LIMITS = void 0;
58
67
  exports.nodeDigest = nodeDigest;
59
68
  exports.capabilityGroup = capabilityGroup;
60
69
  exports.bucketOf = bucketOf;
@@ -62,6 +71,7 @@ exports.recordNetworkEvent = recordNetworkEvent;
62
71
  exports.renderActivityText = renderActivityText;
63
72
  exports.normalizeIpns = normalizeIpns;
64
73
  exports.readAgentSites = readAgentSites;
74
+ exports.readOpenTasks = readOpenTasks;
65
75
  exports.emitTradePulse = emitTradePulse;
66
76
  exports.anonShortRef = anonShortRef;
67
77
  exports.isoSeconds = isoSeconds;
@@ -76,6 +86,7 @@ exports.isPublicChainId = isPublicChainId;
76
86
  exports.summarizeActivityRows = summarizeActivityRows;
77
87
  exports.buildChainIdScope = buildChainIdScope;
78
88
  exports.snapshotConsistencyIssues = snapshotConsistencyIssues;
89
+ exports.openTasksIssues = openTasksIssues;
79
90
  exports.computeSnapshot = computeSnapshot;
80
91
  exports.getNetworkPulse = getNetworkPulse;
81
92
  exports.snapshotStatus = snapshotStatus;
@@ -84,12 +95,18 @@ exports.lastSnapshotSignError = lastSnapshotSignError;
84
95
  exports.signSnapshot = signSnapshot;
85
96
  exports.verifySnapshotSignature = verifySnapshotSignature;
86
97
  exports.assertNoPrivateFields = assertNoPrivateFields;
98
+ exports.auditPublicHexLeaks = auditPublicHexLeaks;
87
99
  const fs = __importStar(require("fs"));
88
100
  const os = __importStar(require("os"));
89
101
  const path = __importStar(require("path"));
90
102
  const crypto = __importStar(require("crypto"));
91
103
  // 确认数门槛来自 chain-config (单一实现; 默认 confirmed=1 / finalized=12) —— 只借常量, 不借 RPC
92
104
  const chain_config_js_1 = require("./chain/chain-config.js");
105
+ // 区块浏览器映射 (chainId → 公网浏览器); 认不出的链没有链接 —— 宁缺勿错, 不编死链
106
+ const explorer_js_1 = require("./chain/explorer.js");
107
+ // 「待接单任务」的数据源 = 本机公告板目录 (~/.bolloon/tasks/board)。只借**路径常量**,
108
+ // 公告的解析/发布/接单一律留在 task-board.ts (这里不做第二套实现)。
109
+ const task_board_js_1 = require("./task-board.js");
93
110
  // ── 常量 (上限 + 时间边界) ─────────────────────────────────────────────────
94
111
  exports.PULSE_LIMITS = {
95
112
  /** 最多留多少条事件 (超出丢最旧) */
@@ -121,6 +138,8 @@ exports.NETWORK_EVENT_TYPES = [
121
138
  // 经济事件 (P6 扩展; 原五类保持兼容, 老节点发来的事件仍被接受)
122
139
  'task_posted', 'task_accepted', 'task_completed', 'trade_settled', 'trade_verified',
123
140
  'wallet_signed',
141
+ // 2026-09-23 (C1): 任务对外发布 (公告板 publish) —— 只记"有节点公告了一个待接单任务", 正文/ID 原文不落盘
142
+ 'task_announced',
124
143
  ];
125
144
  // ── 存储 ────────────────────────────────────────────────────────────────────
126
145
  const home = (h) => h || process.env.HOME || os.homedir() || '/tmp';
@@ -233,6 +252,7 @@ function renderActivityText(kind) {
233
252
  case 'capability_announced': return { zh: '有节点公开声明了新能力', en: 'A node announced a capability' };
234
253
  case 'peer_connected': return { zh: '两个节点建立了连接', en: 'Two nodes connected' };
235
254
  case 'delegation_completed': return { zh: '一次能力委派完成', en: 'A delegation completed' };
255
+ case 'task_announced': return { zh: '有节点公告了一个待接单任务', en: 'A node announced an open task' };
236
256
  default: return { zh: '网络有活动', en: 'Network activity' };
237
257
  }
238
258
  }
@@ -282,6 +302,82 @@ function readAgentSites(h) {
282
302
  return [];
283
303
  }
284
304
  }
305
+ /** 上限: 公开页最多列几条待接单任务 (超出按最近截止排序取前 N) */
306
+ exports.MAX_OPEN_TASKS = 25;
307
+ /** 公开页只列这条公告的这 7 个字段; 其余(正文/买方/摘要/签名)一律不出这个函数 */
308
+ function buildOpenTaskRow(a) {
309
+ const capability = String(a?.capability ?? '').trim();
310
+ const id = String(a?.announcementId ?? '').trim();
311
+ const deadline = Number(a?.deadline);
312
+ if (!capability || !id || !Number.isFinite(deadline) || deadline <= 0)
313
+ return null;
314
+ const b = a?.budget && typeof a.budget === 'object' ? a.budget : null;
315
+ const maxAmount = b ? String(b.maxAmount ?? '').trim() : '';
316
+ const currency = b ? String(b.currency ?? '').trim() : '';
317
+ const network = b ? String(b.network ?? '').trim() : '';
318
+ return {
319
+ capability,
320
+ budget: maxAmount || null,
321
+ currency: currency || null,
322
+ network: network || null,
323
+ deadline,
324
+ claimed: (Array.isArray(a?.claims) ? a.claims.length : 0) > 0,
325
+ announcementId: id.slice(0, 8), // 前 8 位 (按字面, 不重新格式化)
326
+ };
327
+ }
328
+ /**
329
+ * 读本节点公告板上**未认领且未过期**的公告, 投影成公开「待接单任务」行。
330
+ *
331
+ * 三条筛选 (缺一个都会把不该上的东西露出来):
332
+ * · `protocol= bolloon-task/1` + `kind= task_announcement` (同目录还有 `remote-claims.json`,
333
+ * 它是个数组 —— 不按 kind 判会把认领台账当公告解析)
334
+ * · `status === 'open'` (已认领 / 已取消 → 不是「待接单」)
335
+ * · `deadline > now` (过期只是不能再接单, 文件不删 → 必须在这里判掉)
336
+ *
337
+ * 读不到目录 / 坏 JSON / 缺字段 → 该条跳过 (一条都不编); 整个目录不可读 → 空数组 + 不抛。
338
+ */
339
+ function readOpenTasks(h, now = Date.now()) {
340
+ const out = [];
341
+ try {
342
+ const dir = (0, task_board_js_1.boardDir)(h);
343
+ let names;
344
+ try {
345
+ names = fs.readdirSync(dir);
346
+ }
347
+ catch {
348
+ return [];
349
+ }
350
+ for (const name of names.slice().sort()) {
351
+ if (!name.endsWith('.json') || name === `${task_board_js_1.REMOTE_CLAIMS_FILE}`)
352
+ continue;
353
+ let raw;
354
+ try {
355
+ raw = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf-8'));
356
+ }
357
+ catch {
358
+ continue;
359
+ }
360
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
361
+ continue; // 认领台账是数组 → 这里就出局
362
+ if (raw.protocol !== 'bolloon-task/1' || raw.kind !== 'task_announcement')
363
+ continue;
364
+ if (String(raw.status) !== 'open')
365
+ continue;
366
+ const deadline = Number(raw.deadline);
367
+ if (!Number.isFinite(deadline) || deadline <= now)
368
+ continue;
369
+ const row = buildOpenTaskRow(raw);
370
+ if (row)
371
+ out.push(row);
372
+ }
373
+ }
374
+ catch {
375
+ return [];
376
+ }
377
+ // 稳定排序: 快到期在前 (读者最需要知道的先看); 同截止按短 id 定序 (排序必须确定, 否则快照每次都"变")
378
+ out.sort((x, y) => (x.deadline - y.deadline) || (x.announcementId < y.announcementId ? -1 : x.announcementId > y.announcementId ? 1 : 0));
379
+ return out.slice(0, exports.MAX_OPEN_TASKS);
380
+ }
285
381
  /**
286
382
  * 把**真实交易活动**投影成脉冲事件 (由交易写路径调用, fire-and-forget)。
287
383
  * 只按事实发: 交付 → task_completed · 真验真 → trade_verified · **链上口径**结算 → trade_settled。
@@ -314,10 +410,18 @@ async function emitTradePulse(before, after, h) {
314
410
  // ── 冻结形状: confirmed_activity (真实智能体任务 / 链上活动行) ────────────────
315
411
  //
316
412
  // 公开网页要能列出「哪个任务 · 什么状态 · 哪个块 · 多少确认」, 而不是只有数字。这块就是那批行。
317
- // 形状**冻结** (字段名与取值域不许改), 内容一律**匿名短写**:
318
- // · task = `sha256:` + sha256(域标签|taskKey) 的前 8 位十六进制 —— 绝不落 taskKey / taskId 原文
319
- // · tx = `sha256:` + sha256(域标签|txHash) 的前 8 位十六进制 —— 绝不落 txHash 原文
320
- // · 不出现 DID / peerId / IP / 完整钱包地址 / 任务正文 —— `assertNoPrivateFields` 兜底
413
+ // 形状**冻结** (老字段名与取值域不许改), 内容按用途分两类:
414
+ // · **匿名短写** (老口径, 保留不删): task = `sha256:` + sha256(域标签|taskKey) 的前 8 位;
415
+ // tx = `sha256:` + sha256(域标签|txHash) 的前 8 位 —— 绝不落 taskKey / taskId 原文
416
+ // · **公开链上事实** (2026-09-23 新增; 同日按 leo 拍板收窄): tx_hash (真交易哈希) /
417
+ // explorer_tx (交易浏览器链接, 仅当该链有已知公网浏览器) —— 网页上的行因此**可核验地**跳回区块
418
+ // 浏览器; contract (escrow 合约地址) 仍保留在行里**供索引/诊断**, 但**不上页面、不生成合约链接**
419
+ // 隐私红线 (违反即失败, **只放开上面那两种 0x 字符串**):
420
+ // · 允许: 交易哈希 (0x+64 hex) 与 escrow 合约地址 (0x+40 hex), 且只出现在 `tx_hash`/`contract`/`explorer_tx` 这 3 个键下
421
+ // · 禁止: EOA / 钱包地址 (买方/卖方/payTo)、taskKey 原文、taskId、args 里的地址、DID、peerId /
422
+ // IP / multiaddr、私钥 —— `assertNoPrivateFields`(键名) + `auditPublicHexLeaks`(越界 0x) 兜底
423
+ // · 拿不到链配置里的 escrow 地址, 或这条索引记录的 address 不是它 → **不填** contract
424
+ // (宁缺勿错: 宁可少一个字段, 也不把可能是别人的地址写成合约)
321
425
  // 数据源优先级: ① P5 链上索引 (真链上事实) → ② 索引不可用 → 退回脉冲事件, 并在快照里用
322
426
  // `confirmed_activity_source` 如实标注 (chain-index / pulse-events / none)。
323
427
  // 确认数口径走 chain-config 门槛 (默认 confirmed=1 / finalized=12): 够不着门槛的行只报
@@ -401,6 +505,8 @@ function activityLimit(limit) {
401
505
  * · 不认识的事件名 / 非法 taskKey·txHash / 缺观察时间 → 跳过 (不猜、不臆造)
402
506
  * · confirmations: 有 headBlock → head - block + 1 复算 (无 RPC, 用索引快照里的 head); 否则用记录值
403
507
  * · finality: 按 chain-config 门槛复算 (不够 → observed)
508
+ * · 新增: `tx_hash` (真交易哈希) 恒给; `contract` 仅当 opts.escrowAddress 合法且 === 本条 address;
509
+ * `explorer_tx` 仅当该 chainId 有**已知公网浏览器** (否则键整个不存在, 不填 null)
404
510
  * · 最新在前 (blockNumber desc, logIndex desc), 上限 25 行
405
511
  */
406
512
  function buildConfirmedActivityFromIndex(entries, opts = {}) {
@@ -408,6 +514,9 @@ function buildConfirmedActivityFromIndex(entries, opts = {}) {
408
514
  const limit = activityLimit(opts.limit);
409
515
  const chainId = Number.isInteger(Number(opts.chainId)) && Number(opts.chainId) >= 0 ? Number(opts.chainId) : 0;
410
516
  const head = Number.isInteger(Number(opts.headBlock)) && Number(opts.headBlock) >= 0 ? Number(opts.headBlock) : null;
517
+ // escrow 合约地址白名单 (唯一权威): 只有索引文件自己记的这个地址算数, 别的地址一律不当合约
518
+ const escrow = typeof opts.escrowAddress === 'string' ? opts.escrowAddress.toLowerCase() : '';
519
+ const escrowOk = explorer_js_1.ADDRESS_RE.test(escrow) ? escrow : '';
411
520
  const out = [];
412
521
  for (const e of Array.isArray(entries) ? entries : []) {
413
522
  if (!e || e.suspect === true)
@@ -428,6 +537,12 @@ function buildConfirmedActivityFromIndex(entries, opts = {}) {
428
537
  if (!at)
429
538
  continue; // 没观察时间 → 不臆造时间, 不成行
430
539
  const confirmations = head == null ? Math.max(0, Number(e.confirmations) || 0) : Math.max(0, head - block + 1);
540
+ // 合约地址: 只有「本条记录的 address 就是链配置里的 escrow 地址」才填 (宁缺勿错)
541
+ const entryAddr = String(e.address || '').toLowerCase();
542
+ const contract = escrowOk && entryAddr === escrowOk ? escrowOk : '';
543
+ // 浏览器链接: 该链没有已知公网浏览器 → 返回 null → 键整个不出现 (不编 href="#")
544
+ // 只有**交易**链接 (explorer_tx); 合约地址只在数据里 (contract), 不生成链接 (2026-09-23 收窄)
545
+ const explorerTx = (0, explorer_js_1.explorerTxUrl)(chainId, txHash);
431
546
  out.push({
432
547
  block, logIndex,
433
548
  row: {
@@ -440,6 +555,10 @@ function buildConfirmedActivityFromIndex(entries, opts = {}) {
440
555
  confirmations,
441
556
  finality: finalityFromConfirmations(confirmations, gates),
442
557
  at,
558
+ // 公开链上事实 (白名单键; 老字段 tx 已在上方保留)
559
+ tx_hash: txHash,
560
+ ...(contract ? { contract } : {}),
561
+ ...(explorerTx ? { explorer_tx: explorerTx } : {}),
443
562
  },
444
563
  });
445
564
  }
@@ -537,6 +656,7 @@ async function resolveConfirmedActivity(q) {
537
656
  headBlock: file?.headBlock,
538
657
  chainId: file?.chainId,
539
658
  limit,
659
+ escrowAddress: file?.escrowAddress, // 合约地址白名单 = 索引文件自己的部署身份
540
660
  });
541
661
  if (rows.length > 0)
542
662
  return { source: 'chain-index', rows, gates };
@@ -675,6 +795,8 @@ function buildChainIdScope(rows) {
675
795
  * ④ `activity_totals.source` === `confirmed_activity_source` (两块来源标注必须一致)
676
796
  * ⑤ 行里有任务 而 `totals.tasks === 0` 时, 必须有口径说明 note
677
797
  * (页面不许出现「0 个任务」与「N 行任务」并存而**不解释**)
798
+ * ⑥ 0x 长 hex 越界 (2026-09-23): 只许出现在 tx_hash/contract/explorer_* 四个白名单键下 ——
799
+ * EOA 地址跑到别的键 (或白名单键形状不对) 就拒绝导出
678
800
  */
679
801
  function snapshotConsistencyIssues(snap) {
680
802
  const issues = [];
@@ -701,8 +823,50 @@ function snapshotConsistencyIssues(snap) {
701
823
  if (!explained)
702
824
  issues.push(`totals.tasks=0 与 ${rows.length} 行任务并存, 却没有口径说明 note`);
703
825
  }
826
+ issues.push(...auditPublicHexLeaks(snap)); // ⑥ 0x 长 hex 越界 → 拒绝导出 (不静默放行)
827
+ issues.push(...openTasksIssues(snap)); // ⑦ 待接单任务的字段白名单与「只含未认领」不变式
704
828
  return issues;
705
829
  }
830
+ /** open_tasks 行的字段白名单 (与 `OpenTaskRow` 逐字一致; 多一个键 = 可能把正文/身份带出去了) */
831
+ exports.OPEN_TASK_KEYS = ['capability', 'budget', 'currency', 'network', 'deadline', 'claimed', 'announcementId'];
832
+ /**
833
+ * 待接单任务行的自检 (空 = 通过)。这不是格式洁癖, 每条都对着一个真泄漏路径:
834
+ * · 键集 ≠ 白名单 → 有人把 instruction / buyerDid / claims 之类**展平**进公开投影了;
835
+ * · `claimed !== false` → 「只导出未认领」这个筛选坏了 (页面会把已接单的当待接单);
836
+ * · `announcementId` 长于 8 位 → 短写丢了, 原始公告 id 上公开页 (页面上的 id 会被读者当完整 id 用);
837
+ * · capability 空 / deadline 非正 → 页面上会出现一条说不出是什么、也不知道何时截止的行。
838
+ */
839
+ function openTasksIssues(snap) {
840
+ const out = [];
841
+ const rows = snap?.open_tasks;
842
+ if (!Array.isArray(rows))
843
+ return ['open_tasks 缺失 (公开「待接单任务」必须是数组, 没有就给空数组)'];
844
+ const allow = exports.OPEN_TASK_KEYS;
845
+ rows.forEach((r, i) => {
846
+ const at = `open_tasks[${i}]`;
847
+ if (!r || typeof r !== 'object' || Array.isArray(r)) {
848
+ out.push(`${at} 不是对象`);
849
+ return;
850
+ }
851
+ const extra = Object.keys(r).filter((k) => !allow.includes(k));
852
+ if (extra.length)
853
+ out.push(`${at} 出现白名单外的键: ${extra.join(', ')} (公开投影只许 ${allow.join('/')})`);
854
+ if (typeof r.capability !== 'string' || !r.capability.trim())
855
+ out.push(`${at}.capability 缺失/为空`);
856
+ if (r.claimed !== false)
857
+ out.push(`${at}.claimed=${JSON.stringify(r.claimed)} —— 公开页只许列**未认领**的公告`);
858
+ if (typeof r.announcementId !== 'string' || r.announcementId.length === 0 || r.announcementId.length > 8) {
859
+ out.push(`${at}.announcementId 必须是 announcementId 的**前 8 位**(1..8 字符), 实得 ${JSON.stringify(r.announcementId)}`);
860
+ }
861
+ if (!Number.isFinite(r.deadline) || r.deadline <= 0)
862
+ out.push(`${at}.deadline 必须是正的 ms 时间戳`);
863
+ for (const k of ['budget', 'currency', 'network']) {
864
+ if (r[k] !== null && typeof r[k] !== 'string')
865
+ out.push(`${at}.${k} 只许字符串或 null (不替公告猜值)`);
866
+ }
867
+ });
868
+ return out;
869
+ }
706
870
  function computeSnapshot(events, opts) {
707
871
  const now = opts.now;
708
872
  const fresh_until = now + exports.PULSE_LIMITS.snapshotTtlMs;
@@ -730,6 +894,9 @@ function computeSnapshot(events, opts) {
730
894
  confirmed_activity_source: 'none',
731
895
  activity_totals: summarizeActivityRows(emptyRows, { source: 'none', gates }),
732
896
  chain_id_scope: buildChainIdScope(emptyRows),
897
+ // 观察层不可用 ≠ 公告板读不到: 这一支只说明「脉冲事件层」挂了, 待接单任务照实带出来
898
+ // (调用方没给就空数组 —— 不拿空数组冒充「板上没有」: 页面空态说的是「暂未观察到」)
899
+ open_tasks: Array.isArray(opts.openTasks) ? opts.openTasks : [],
733
900
  notes: ['观察层暂不可用 — 这不是"网络为空"'],
734
901
  };
735
902
  }
@@ -850,6 +1017,7 @@ function computeSnapshot(events, opts) {
850
1017
  confirmed_activity_source: activity.source,
851
1018
  activity_totals,
852
1019
  chain_id_scope,
1020
+ open_tasks: Array.isArray(opts.openTasks) ? opts.openTasks : [],
853
1021
  notes,
854
1022
  };
855
1023
  }
@@ -868,7 +1036,8 @@ async function getNetworkPulse(opts = {}) {
868
1036
  && typeof cached?.confirmed_activity_source === 'string'
869
1037
  && !!cached?.totals_scope
870
1038
  && !!cached?.activity_totals
871
- && !!cached?.chain_id_scope;
1039
+ && !!cached?.chain_id_scope
1040
+ && Array.isArray(cached?.open_tasks); // 老缓存没有待接单任务 → 过期形状, 重算
872
1041
  if (shapeOk && cached && Number.isFinite(cached.generated_at) && cached.generated_at + exports.PULSE_LIMITS.snapshotTtlMs > now)
873
1042
  return cached;
874
1043
  }
@@ -883,7 +1052,8 @@ async function getNetworkPulse(opts = {}) {
883
1052
  }
884
1053
  // 活动行优先取 P5 链上索引 (真链上事实); 索引不可用 → 退回脉冲事件, 并在快照里标出来源
885
1054
  const confirmedActivity = await resolveConfirmedActivity({ home: opts.home, events, now });
886
- const snap = computeSnapshot(events, { now, confirmedActivity });
1055
+ // 待接单任务: 只读本机公告板目录 (未认领且未过期), 投影成白名单 7 字段 —— 与脉冲事件层无关
1056
+ const snap = computeSnapshot(events, { now, confirmedActivity, openTasks: readOpenTasks(opts.home, now) });
887
1057
  try {
888
1058
  fs.mkdirSync((0, exports.pulseDir)(opts.home), { recursive: true });
889
1059
  fs.writeFileSync(snapshotFile(opts.home), JSON.stringify(snap), 'utf8');
@@ -976,4 +1146,49 @@ function assertNoPrivateFields(value, at = '$') {
976
1146
  walk(value, at);
977
1147
  return issues;
978
1148
  }
1149
+ /**
1150
+ * 允许出现 0x 长 hex 的键 (**唯一白名单**, 2026-09-23 leo 拍板; 同日收窄): 交易哈希 / escrow 合约地址 /
1151
+ * 交易浏览器链接 —— 都是公开链上事实。除了这 3 个键, 别处出现 0x 长 hex (尤其 EOA 钱包地址) 一律算泄露。
1152
+ * 注意 `contract` **只在数据里**(供索引/诊断), 页面不渲染合约地址也不给合约链接 ⇒ 没有 `explorer_contract` 这个键。
1153
+ */
1154
+ exports.PUBLIC_HEX_KEYS = ['tx_hash', 'contract', 'explorer_tx'];
1155
+ /**
1156
+ * 0x 长 hex 越界审计 (**粒度比 `assertNoPrivateFields` 更细**: 那个只看键名, 这个看值形态)。
1157
+ * · 白名单键下: 形状必须精确 —— `tx_hash` = 0x+64 hex; `contract` = 0x+40 hex;
1158
+ * `explorer_tx` = 我们造的**交易**浏览器链接形状 (`EXPLORER_URL_RE`)
1159
+ * · **其他任何键**的值里出现 `0x` + 40/64 位 hex → 报出来 (买方·卖方 EOA、taskKey 原文、
1160
+ * args 里的地址都属这一类); `explorer_contract` 之类不在白名单的键**同样**按泄露处理
1161
+ * 返回越界说明列表 (空 = 干净)。这条尺子**只收紧不放松**: 老口径下「任何 0x 长 hex 都不许」
1162
+ * 现在等价于「除了这 3 个白名单键, 任何 0x 长 hex 都不许」。
1163
+ */
1164
+ function auditPublicHexLeaks(value, at = '$') {
1165
+ const issues = [];
1166
+ const allow = exports.PUBLIC_HEX_KEYS;
1167
+ const walk = (v, p) => {
1168
+ if (Array.isArray(v))
1169
+ return v.forEach((x, i) => walk(x, `${p}[${i}]`));
1170
+ if (v && typeof v === 'object') {
1171
+ for (const [k, val] of Object.entries(v))
1172
+ walk(val, `${p}.${k}`);
1173
+ return;
1174
+ }
1175
+ if (typeof v !== 'string')
1176
+ return;
1177
+ const s = v;
1178
+ const key = p.slice(p.lastIndexOf('.') + 1);
1179
+ // 白名单键: 形状必须精确 —— 不管值长短都查 (拿短串/怪串冒充同样拒绝)
1180
+ if (allow.includes(key)) {
1181
+ const ok = (key === 'tx_hash' && explorer_js_1.TX_HASH_RE.test(s))
1182
+ || (key === 'contract' && explorer_js_1.ADDRESS_RE.test(s))
1183
+ || (key === 'explorer_tx' && explorer_js_1.EXPLORER_URL_RE.test(s));
1184
+ if (!ok)
1185
+ issues.push(`${p} = 白名单键 ${key} 下的形状不对 (只认精确形状, 不认"含 0x 就算")`);
1186
+ return;
1187
+ }
1188
+ if (/0x[0-9a-fA-F]{40}/.test(s))
1189
+ issues.push(`${p} = 0x 长 hex (只许出现在 ${allow.join('/')} 下) → 按泄露处理`);
1190
+ };
1191
+ walk(value, at);
1192
+ return issues;
1193
+ }
979
1194
  //# sourceMappingURL=network-pulse.js.map