@twinsearth/agent-universe 2.5.8 → 2.6.0
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/README.md +3 -1
- package/js/index.js +1 -1
- package/js/lib/aca.js +1 -1
- package/js/lib/market.js +205 -2
- package/js/lib/mcp.js +1 -1
- package/js/lib/models.js +27 -2
- package/js/package-lock.json +2 -2
- package/js/package.json +1 -1
- package/js/test/test.js +162 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -273,7 +273,7 @@ npm config set @twinsearth:registry https://npm.pkg.github.com
|
|
|
273
273
|
npm install @twinsearth/agent-universe
|
|
274
274
|
```
|
|
275
275
|
|
|
276
|
-
已发布版本:1.0.0 / 2.0.0 / 2.2.0 / 2.3.0 / 2.3.1 / 2.3.4 / 2.3.5 / 2.3.6 / 2.4.0 ~ 2.
|
|
276
|
+
已发布版本:1.0.0 / 2.0.0 / 2.2.0 / 2.3.0 / 2.3.1 / 2.3.4 / 2.3.5 / 2.3.6 / 2.4.0 ~ 2.6.0,详见 [Releases](https://github.com/TwinsEarth/agent-universe/releases)。
|
|
277
277
|
|
|
278
278
|
## 版本谱系
|
|
279
279
|
|
|
@@ -309,6 +309,8 @@ npm install @twinsearth/agent-universe
|
|
|
309
309
|
| v2.5.6 | **Regression** | **历史 Bug 回归套件:test/ 14 条 + Rust 网络回归,CI 强制全量回归,修新 bug 先加用例** |
|
|
310
310
|
| v2.5.7 | **Identity** | **跨实现身份一致性:统一 DID 派生口径 + conformance 签名向量逐字节命中上游 + 客户端构建链路根治(file: 依赖)** |
|
|
311
311
|
| v2.5.8 | **Ledger** | **精确整数账本 Money(i64):守恒精确相等无容差 + 发布即托管 __escrow__ 防铸币 + 三端共享金额向量逐值一致** |
|
|
312
|
+
| v2.5.9 | **Auth QA** | **认证式 BFT(固定委员集 Ed25519 签名票,替代调用方合成)+ 可失败独立审计(只信任流水独立重放)+ nonce/时间窗重放保护 + 只追加结算流水** |
|
|
313
|
+
| v2.6.0 | **State Machine** | **状态机恢复边(no_quorum→open、rework→running,消除 NoQuorum 吸收态)+ 证据分级作为结算强制闸门(is_trustworthy 此前零调用点)+ policy=None 提交即验收** |
|
|
312
314
|
|
|
313
315
|
详见 [RELEASES.md](RELEASES.md) 和 [releases/](releases/) 目录。
|
|
314
316
|
|
package/js/index.js
CHANGED
|
@@ -9,7 +9,7 @@ const { AgentMarket, Bid, Reputation, MIN_STAKE } = require('./lib/market');
|
|
|
9
9
|
const { McpHttpClient, McpError, MCP_PROTOCOL_VERSION } = require('./lib/mcp');
|
|
10
10
|
const aca = require('./lib/aca');
|
|
11
11
|
|
|
12
|
-
const version = '2.
|
|
12
|
+
const version = '2.6.0';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* AgentUniverse 门面:聚合身份、网络、市场能力
|
package/js/lib/aca.js
CHANGED
|
@@ -129,7 +129,7 @@ function buildManifest(identity, name, capabilities, opts = {}) {
|
|
|
129
129
|
const manifest = {
|
|
130
130
|
did: identity.did,
|
|
131
131
|
name,
|
|
132
|
-
version: opts.version || '2.
|
|
132
|
+
version: opts.version || '2.6.0',
|
|
133
133
|
capabilities: [...capabilities],
|
|
134
134
|
endpoints: opts.endpoints || [],
|
|
135
135
|
hardware: {
|
package/js/lib/market.js
CHANGED
|
@@ -38,7 +38,15 @@ class AgentMarket {
|
|
|
38
38
|
this.bids = new Map(); // taskId → Bid[]
|
|
39
39
|
this.balances = new Map(); // 账户 → 余额
|
|
40
40
|
this.totalSlashed = 0;
|
|
41
|
+
this.totalDeposits = 0;
|
|
41
42
|
this.paidTasks = new Set(); // 已结算任务(防重复支付)
|
|
43
|
+
this.records = []; // 只追加结算流水(独立审计的唯一信任源)
|
|
44
|
+
this._seenNonces = new Set(); // 已用投票 nonce(防重放)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 追加一条结算流水(与 Rust SettlementRecord 对齐) */
|
|
48
|
+
_record(from, to, amount, reason) {
|
|
49
|
+
this.records.push({ from, to, amount, reason });
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
/** 金额必须是安全整数(与 Rust Money(i64) 对齐,杜绝浮点导致的跨语言守恒失效) */
|
|
@@ -55,6 +63,8 @@ class AgentMarket {
|
|
|
55
63
|
if (amount < 0) throw new Error('金额不能为负');
|
|
56
64
|
const cur = this.balances.get(account) || 0;
|
|
57
65
|
this.balances.set(account, cur + amount);
|
|
66
|
+
this.totalDeposits += amount;
|
|
67
|
+
this._record('', account, amount, 'Deposited');
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
balance(account) {
|
|
@@ -88,6 +98,7 @@ class AgentMarket {
|
|
|
88
98
|
const stakeAcc = this._stakeAccount(owner);
|
|
89
99
|
this.balances.set(owner, this.balance(owner) - stake);
|
|
90
100
|
this.balances.set(stakeAcc, this.balance(stakeAcc) + stake);
|
|
101
|
+
this._record(owner, stakeAcc, stake, 'Staked');
|
|
91
102
|
|
|
92
103
|
this.agents.set(card.did, {
|
|
93
104
|
card,
|
|
@@ -110,6 +121,7 @@ class AgentMarket {
|
|
|
110
121
|
this.balances.set(requester, this.balance(requester) - budget);
|
|
111
122
|
const escrowAcc = this._escrowAccount(taskId);
|
|
112
123
|
this.balances.set(escrowAcc, this.balance(escrowAcc) + budget);
|
|
124
|
+
this._record(requester, escrowAcc, budget, 'Escrowed');
|
|
113
125
|
|
|
114
126
|
const task = new Task(taskId, goal);
|
|
115
127
|
task.transition(TaskStatus.OPEN);
|
|
@@ -163,11 +175,33 @@ class AgentMarket {
|
|
|
163
175
|
}
|
|
164
176
|
|
|
165
177
|
/** 执行 → 完成 → 验证 */
|
|
166
|
-
completeTask(taskId, passed = true) {
|
|
178
|
+
completeTask(taskId, passed = true, evidenceGrade = 'CpuProto') {
|
|
167
179
|
const task = this.tasks.get(taskId);
|
|
168
180
|
task.start();
|
|
169
181
|
task.complete();
|
|
170
|
-
task.
|
|
182
|
+
task.evidenceGrade = evidenceGrade;
|
|
183
|
+
if (task.verificationPolicy === 'None') {
|
|
184
|
+
// 无需 QA:提交结果即验收(证据门禁亦豁免)
|
|
185
|
+
task.transition(TaskStatus.VERIFIED);
|
|
186
|
+
} else {
|
|
187
|
+
task.verify(passed);
|
|
188
|
+
}
|
|
189
|
+
return task;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 恢复边:返工任务回到执行中(v2.6.0) */
|
|
193
|
+
resumeAfterRework(taskId) {
|
|
194
|
+
const task = this.tasks.get(taskId);
|
|
195
|
+
if (!task) throw new Error('任务不存在');
|
|
196
|
+
task.resumeAfterRework();
|
|
197
|
+
return task;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 恢复边:无共识任务重新开放(v2.6.0,消除吸收态) */
|
|
201
|
+
reopenTask(taskId) {
|
|
202
|
+
const task = this.tasks.get(taskId);
|
|
203
|
+
if (!task) throw new Error('任务不存在');
|
|
204
|
+
task.reopenAfterNoQuorum();
|
|
171
205
|
return task;
|
|
172
206
|
}
|
|
173
207
|
|
|
@@ -183,6 +217,14 @@ class AgentMarket {
|
|
|
183
217
|
return { paid: 0, reason: 'rejected' };
|
|
184
218
|
}
|
|
185
219
|
|
|
220
|
+
// 证据分级强制闸门(v2.6.0):需要验证的任务,结果证据必须可信;
|
|
221
|
+
// policy='None' 时豁免。
|
|
222
|
+
if (task.verificationPolicy !== 'None') {
|
|
223
|
+
if (task.evidenceGrade !== 'Verified' && task.evidenceGrade !== 'CpuProto') {
|
|
224
|
+
return { paid: 0, reason: 'untrusted_evidence' };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
186
228
|
const price = task.winnerPrice || task.budget;
|
|
187
229
|
const pay = Math.min(price, task.budget);
|
|
188
230
|
|
|
@@ -191,11 +233,13 @@ class AgentMarket {
|
|
|
191
233
|
const assignee = task.assignee;
|
|
192
234
|
this.balances.set(escrowAcc, this.balance(escrowAcc) - pay);
|
|
193
235
|
this.balances.set(assignee, this.balance(assignee) + pay);
|
|
236
|
+
this._record(escrowAcc, assignee, pay, 'Completed');
|
|
194
237
|
// 未用完的预算退还需求方
|
|
195
238
|
const refund = task.budget - pay;
|
|
196
239
|
if (refund > 0) {
|
|
197
240
|
this.balances.set(escrowAcc, this.balance(escrowAcc) - refund);
|
|
198
241
|
this.balances.set(task.requester, this.balance(task.requester) + refund);
|
|
242
|
+
this._record(escrowAcc, task.requester, refund, 'Refunded');
|
|
199
243
|
}
|
|
200
244
|
|
|
201
245
|
task.settle();
|
|
@@ -215,6 +259,7 @@ class AgentMarket {
|
|
|
215
259
|
const stakeAcc = this._stakeAccount(agentId);
|
|
216
260
|
this.balances.set(stakeAcc, this.balance(stakeAcc) - actual);
|
|
217
261
|
this.totalSlashed += actual;
|
|
262
|
+
this._record(stakeAcc, '', actual, 'Slashed');
|
|
218
263
|
entry.status = entry.stake < MIN_STAKE ? 'suspended' : entry.status;
|
|
219
264
|
return { slashed: actual, reason, remainingStake: entry.stake };
|
|
220
265
|
}
|
|
@@ -260,6 +305,164 @@ class AgentMarket {
|
|
|
260
305
|
expected,
|
|
261
306
|
};
|
|
262
307
|
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* 认证式 QA 验证(v2.5.9,与 Rust verify_result_authenticated 对齐)
|
|
311
|
+
* members: [{did, public_key(hex32)}] 固定委员集
|
|
312
|
+
* signedVotes: [SignedQaVote],vote 为 Stop/Continue
|
|
313
|
+
*/
|
|
314
|
+
verifyResultAuthenticated(
|
|
315
|
+
taskId,
|
|
316
|
+
members,
|
|
317
|
+
signedVotes,
|
|
318
|
+
round = 0,
|
|
319
|
+
now = Math.floor(Date.now() / 1000),
|
|
320
|
+
) {
|
|
321
|
+
if (!Array.isArray(members) || members.length === 0) {
|
|
322
|
+
throw new Error('members 必须为非空固定委员集');
|
|
323
|
+
}
|
|
324
|
+
const n = members.length;
|
|
325
|
+
const f = Math.floor((n - 1) / 3);
|
|
326
|
+
const pubkeys = new Map();
|
|
327
|
+
for (const m of members) {
|
|
328
|
+
const pk = m.public_key || m.publicKey;
|
|
329
|
+
if (!m.did || !pk) throw new Error('委员需含 did 与 public_key');
|
|
330
|
+
if (!/^[0-9a-fA-F]{64}$/.test(pk)) throw new Error('委员公钥须为 32 字节 hex');
|
|
331
|
+
pubkeys.set(m.did, pk.toLowerCase());
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 逐票校验;voterVotes 记录每个委员最终立场(equivocated 票作废)
|
|
335
|
+
const voterVotes = new Map();
|
|
336
|
+
for (const sv of signedVotes || []) {
|
|
337
|
+
const pk = pubkeys.get(sv.voter);
|
|
338
|
+
if (!pk) throw new Error(`投票者 ${sv.voter} 不在固定委员集`);
|
|
339
|
+
if (sv.task_id !== taskId) throw new Error('投票任务不匹配');
|
|
340
|
+
if (Number(sv.round) !== round) throw new Error('投票轮次不匹配');
|
|
341
|
+
const issued = Number(sv.issued_at);
|
|
342
|
+
const expires = Number(sv.expires_at);
|
|
343
|
+
if (!(expires > issued) || now < issued || now > expires) {
|
|
344
|
+
throw new Error(`投票 ${sv.nonce} 不在有效时间窗`);
|
|
345
|
+
}
|
|
346
|
+
const voteTag = String(sv.vote).toLowerCase();
|
|
347
|
+
if (voteTag === 'silent') throw new Error('Silent 票不计入');
|
|
348
|
+
// 先验签,避免无效票污染 nonce
|
|
349
|
+
if (!AgentMarket._verifyEd25519(pk, AgentMarket._voteSigningBytes(sv), sv.signature)) {
|
|
350
|
+
throw new Error(`投票 ${sv.nonce} 签名无效`);
|
|
351
|
+
}
|
|
352
|
+
if (!sv.nonce) throw new Error('nonce 不能为空');
|
|
353
|
+
if (this._seenNonces.has(sv.nonce)) throw new Error(`投票 ${sv.nonce} 重放`);
|
|
354
|
+
this._seenNonces.add(sv.nonce);
|
|
355
|
+
const prev = voterVotes.get(sv.voter);
|
|
356
|
+
if (prev && prev !== voteTag) voterVotes.set(sv.voter, 'equivocated');
|
|
357
|
+
else if (!prev) voterVotes.set(sv.voter, voteTag);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let stops = 0;
|
|
361
|
+
let continues = 0;
|
|
362
|
+
for (const v of voterVotes.values()) {
|
|
363
|
+
if (v === 'stop') stops += 1;
|
|
364
|
+
else if (v === 'continue') continues += 1;
|
|
365
|
+
}
|
|
366
|
+
const quorum = 2 * f + 1;
|
|
367
|
+
let decision;
|
|
368
|
+
if (stops >= quorum) decision = 'stop';
|
|
369
|
+
else if (continues >= quorum) decision = 'continue';
|
|
370
|
+
else decision = 'no_quorum';
|
|
371
|
+
const accepted = decision === 'stop';
|
|
372
|
+
const task = this.tasks.get(taskId);
|
|
373
|
+
if (task && decision === 'stop') task.verify(true);
|
|
374
|
+
return { decision, accepted, stops, continues, quorum, n, f };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 独立审计(v2.5.9,与 Rust independent_audit 对齐)
|
|
379
|
+
* 只信任 records,逐笔重放出期望余额,再与当前 balances 逐户比对;
|
|
380
|
+
* 能发现守恒检查无法察觉的账实不符(如拆账、幽灵账户)。
|
|
381
|
+
*/
|
|
382
|
+
independentAudit() {
|
|
383
|
+
const expected = new Map();
|
|
384
|
+
const add = (acct, delta) => {
|
|
385
|
+
const cur = expected.get(acct) || 0;
|
|
386
|
+
const v = cur + delta;
|
|
387
|
+
if (!Number.isSafeInteger(v)) throw new Error('重放溢出');
|
|
388
|
+
expected.set(acct, v);
|
|
389
|
+
};
|
|
390
|
+
let replayedDeposits = 0;
|
|
391
|
+
let replayedSlashed = 0;
|
|
392
|
+
for (const r of this.records) {
|
|
393
|
+
switch (r.reason) {
|
|
394
|
+
case 'Deposited':
|
|
395
|
+
add(r.to, r.amount);
|
|
396
|
+
replayedDeposits += r.amount;
|
|
397
|
+
break;
|
|
398
|
+
case 'Slashed':
|
|
399
|
+
add(r.from, -r.amount);
|
|
400
|
+
replayedSlashed += r.amount;
|
|
401
|
+
break;
|
|
402
|
+
default:
|
|
403
|
+
if (r.amount > 0) {
|
|
404
|
+
add(r.from, -r.amount);
|
|
405
|
+
add(r.to, r.amount);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const accounts = new Set([...expected.keys(), ...this.balances.keys()]);
|
|
411
|
+
const mismatches = [];
|
|
412
|
+
for (const a of accounts) {
|
|
413
|
+
const exp = expected.get(a) || 0;
|
|
414
|
+
const act = this.balances.get(a) || 0;
|
|
415
|
+
if (exp !== act) mismatches.push({ account: a, expected: exp, actual: act });
|
|
416
|
+
}
|
|
417
|
+
let balanceSum = 0;
|
|
418
|
+
for (const v of this.balances.values()) balanceSum += v;
|
|
419
|
+
const aggregateMatches =
|
|
420
|
+
replayedDeposits === this.totalDeposits &&
|
|
421
|
+
replayedSlashed === this.totalSlashed;
|
|
422
|
+
const expectedTotal = replayedDeposits - replayedSlashed;
|
|
423
|
+
return {
|
|
424
|
+
passed:
|
|
425
|
+
mismatches.length === 0 &&
|
|
426
|
+
aggregateMatches &&
|
|
427
|
+
expectedTotal === balanceSum,
|
|
428
|
+
replayedRecords: this.records.length,
|
|
429
|
+
replayedDeposits,
|
|
430
|
+
replayedSlashed,
|
|
431
|
+
expectedTotal,
|
|
432
|
+
actualTotal: balanceSum,
|
|
433
|
+
aggregateMatches,
|
|
434
|
+
mismatches,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** 投票待签名规范字节(与 Rust SignedQaVote::signing_bytes 逐字一致) */
|
|
439
|
+
static _voteSigningBytes(sv) {
|
|
440
|
+
const tag = String(sv.vote).toUpperCase();
|
|
441
|
+
return (
|
|
442
|
+
`AU-QA-VOTE\ntask_id=${sv.task_id}\nround=${sv.round}\nvoter=${sv.voter}` +
|
|
443
|
+
`\nvote=${tag}\nnonce=${sv.nonce}\nissued_at=${sv.issued_at}` +
|
|
444
|
+
`\nexpires_at=${sv.expires_at}`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Ed25519 验签;无 crypto 的环境(WKWebView)抛明确错误而非静默通过 */
|
|
449
|
+
static _verifyEd25519(pubkeyHex, message, signatureHex) {
|
|
450
|
+
let crypto;
|
|
451
|
+
try {
|
|
452
|
+
crypto = require('crypto');
|
|
453
|
+
} catch (e) {
|
|
454
|
+
throw new Error('当前环境无 Ed25519 验签能力;签名验证须在 Node/Rust 侧进行');
|
|
455
|
+
}
|
|
456
|
+
const SPKI = '302a300506032b6570032100';
|
|
457
|
+
const der = Buffer.from(SPKI + pubkeyHex, 'hex');
|
|
458
|
+
const key = crypto.createPublicKey({ key: der, format: 'der', type: 'spki' });
|
|
459
|
+
return crypto.verify(
|
|
460
|
+
null,
|
|
461
|
+
Buffer.from(message, 'utf8'),
|
|
462
|
+
key,
|
|
463
|
+
Buffer.from(signatureHex, 'hex'),
|
|
464
|
+
);
|
|
465
|
+
}
|
|
263
466
|
}
|
|
264
467
|
|
|
265
468
|
module.exports = { AgentMarket, Bid, Reputation, MIN_STAKE };
|
package/js/lib/mcp.js
CHANGED
|
@@ -60,7 +60,7 @@ class McpHttpClient {
|
|
|
60
60
|
method: 'initialize',
|
|
61
61
|
params: {
|
|
62
62
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
63
|
-
clientInfo: { name: 'agent-universe-js', version: '2.
|
|
63
|
+
clientInfo: { name: 'agent-universe-js', version: '2.6.0' },
|
|
64
64
|
capabilities: {},
|
|
65
65
|
},
|
|
66
66
|
});
|
package/js/lib/models.js
CHANGED
|
@@ -13,6 +13,7 @@ const TaskStatus = {
|
|
|
13
13
|
VERIFIED: 'verified',
|
|
14
14
|
SETTLED: 'settled',
|
|
15
15
|
REWORK: 'rework',
|
|
16
|
+
NO_QUORUM: 'no_quorum',
|
|
16
17
|
DISPUTED: 'disputed',
|
|
17
18
|
REJECTED: 'rejected',
|
|
18
19
|
};
|
|
@@ -24,9 +25,10 @@ const TRANSITIONS = {
|
|
|
24
25
|
[TaskStatus.MATCHED]: [TaskStatus.ASSIGNED],
|
|
25
26
|
[TaskStatus.ASSIGNED]: [TaskStatus.RUNNING],
|
|
26
27
|
[TaskStatus.RUNNING]: [TaskStatus.COMPLETED, TaskStatus.REWORK],
|
|
27
|
-
[TaskStatus.COMPLETED]: [TaskStatus.VERIFIED, TaskStatus.DISPUTED, TaskStatus.REWORK],
|
|
28
|
+
[TaskStatus.COMPLETED]: [TaskStatus.VERIFIED, TaskStatus.DISPUTED, TaskStatus.REWORK, TaskStatus.NO_QUORUM],
|
|
28
29
|
[TaskStatus.VERIFIED]: [TaskStatus.SETTLED],
|
|
29
|
-
[TaskStatus.REWORK]: [TaskStatus.RUNNING],
|
|
30
|
+
[TaskStatus.REWORK]: [TaskStatus.RUNNING], // 恢复边:返工 → 继续执行
|
|
31
|
+
[TaskStatus.NO_QUORUM]: [TaskStatus.OPEN], // 恢复边:无共识 → 重新开放
|
|
30
32
|
[TaskStatus.DISPUTED]: [TaskStatus.SETTLED, TaskStatus.REJECTED],
|
|
31
33
|
[TaskStatus.SETTLED]: [],
|
|
32
34
|
[TaskStatus.REJECTED]: [],
|
|
@@ -78,6 +80,8 @@ class Task {
|
|
|
78
80
|
this.goal = goal;
|
|
79
81
|
this.status = TaskStatus.PENDING;
|
|
80
82
|
this.assignee = null;
|
|
83
|
+
this.verificationPolicy = 'BftLite'; // 验证策略;'None' 时提交即验收、豁免证据门禁
|
|
84
|
+
this.evidenceGrade = null; // 结果信封证据等级
|
|
81
85
|
this.history = [TaskStatus.PENDING];
|
|
82
86
|
}
|
|
83
87
|
|
|
@@ -116,6 +120,27 @@ class Task {
|
|
|
116
120
|
throw new Error('只有验证通过的任务才能结算');
|
|
117
121
|
}
|
|
118
122
|
|
|
123
|
+
/** 本轮验收无共识(v2.6.0) */
|
|
124
|
+
markNoQuorum() {
|
|
125
|
+
return this.transition(TaskStatus.NO_QUORUM);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 恢复边:无共识任务重新开放(v2.6.0,消除吸收态) */
|
|
129
|
+
reopenAfterNoQuorum() {
|
|
130
|
+
if (this.status !== TaskStatus.NO_QUORUM) {
|
|
131
|
+
throw new Error(`只有 NO_QUORUM 状态才能重新开放,当前 ${this.status}`);
|
|
132
|
+
}
|
|
133
|
+
return this.transition(TaskStatus.OPEN);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 恢复边:返工任务回到执行中(v2.6.0) */
|
|
137
|
+
resumeAfterRework() {
|
|
138
|
+
if (this.status !== TaskStatus.REWORK) {
|
|
139
|
+
throw new Error(`只有 REWORK 状态才能恢复执行,当前 ${this.status}`);
|
|
140
|
+
}
|
|
141
|
+
return this.transition(TaskStatus.RUNNING);
|
|
142
|
+
}
|
|
143
|
+
|
|
119
144
|
get isTerminal() {
|
|
120
145
|
return this.status === TaskStatus.SETTLED || this.status === TaskStatus.REJECTED;
|
|
121
146
|
}
|
package/js/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinsearth/agent-universe",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@twinsearth/agent-universe",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.6.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"engines": {
|
|
12
12
|
"node": ">=18"
|
package/js/package.json
CHANGED
package/js/test/test.js
CHANGED
|
@@ -43,8 +43,8 @@ function test(name, fn) {
|
|
|
43
43
|
console.log('Agent Universe JS SDK 测试\n');
|
|
44
44
|
|
|
45
45
|
// 1. 版本号
|
|
46
|
-
test('版本号为 2.
|
|
47
|
-
assert.strictEqual(version, '2.
|
|
46
|
+
test('版本号为 2.6.0', () => {
|
|
47
|
+
assert.strictEqual(version, '2.6.0');
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
// 2. 密钥对 + DID + 签名验证
|
|
@@ -195,6 +195,101 @@ test('跨语言金额向量:逐账户余额命中 conformance 向量', () => {
|
|
|
195
195
|
assert.strictEqual(report.conserved, vector.expected_report.conserved);
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
+
// 8d. 认证式 QA:固定委员集 + 签名票(v2.5.9,与 Rust 认证式 BFT 对齐)
|
|
199
|
+
test('认证式 QA:固定委员签名票通过、伪造与重放拒绝', () => {
|
|
200
|
+
const market = new AgentMarket();
|
|
201
|
+
const owner = 'did:au:qa-owner';
|
|
202
|
+
const requester = 'did:au:qa-req';
|
|
203
|
+
market.deposit(owner, 100);
|
|
204
|
+
market.deposit(requester, 50);
|
|
205
|
+
const card = AgentCard.new({ did: owner, name: 'worker' }).withSkill('writing');
|
|
206
|
+
market.registerAgent(card, MIN_STAKE);
|
|
207
|
+
market.publishTask('task-qa', 'goal', 50, requester);
|
|
208
|
+
market.submitBid('task-qa', owner, 40);
|
|
209
|
+
market.matchTask('task-qa');
|
|
210
|
+
// 执行完成,停在 COMPLETED 等待认证式验收(不走本地 verify)
|
|
211
|
+
const t = market.tasks.get('task-qa');
|
|
212
|
+
t.start();
|
|
213
|
+
t.complete();
|
|
214
|
+
|
|
215
|
+
// 固定 4 委员(n=4, f=1, quorum=3)
|
|
216
|
+
const now = Math.floor(Date.now() / 1000);
|
|
217
|
+
const kps = [];
|
|
218
|
+
const members = [];
|
|
219
|
+
for (let i = 0; i < 4; i++) {
|
|
220
|
+
const kp = Keypair.generate();
|
|
221
|
+
kps.push(kp);
|
|
222
|
+
members.push({ did: kp.did, public_key: kp.rawPublicKey().toString('hex') });
|
|
223
|
+
}
|
|
224
|
+
const makeVote = (kp, vote, nonce, round = 0) => {
|
|
225
|
+
const sv = {
|
|
226
|
+
task_id: 'task-qa', round, voter: kp.did, vote, nonce,
|
|
227
|
+
issued_at: now - 60, expires_at: now + 3600, signature: '',
|
|
228
|
+
};
|
|
229
|
+
sv.signature = kp.sign(AgentMarket._voteSigningBytes(sv));
|
|
230
|
+
return sv;
|
|
231
|
+
};
|
|
232
|
+
// 前 3 委员签 Stop
|
|
233
|
+
const votes = [
|
|
234
|
+
makeVote(kps[0], 'Stop', 'n1'),
|
|
235
|
+
makeVote(kps[1], 'Stop', 'n2'),
|
|
236
|
+
makeVote(kps[2], 'Stop', 'n3'),
|
|
237
|
+
];
|
|
238
|
+
const r = market.verifyResultAuthenticated('task-qa', members, votes, 0, now);
|
|
239
|
+
assert.strictEqual(r.decision, 'stop');
|
|
240
|
+
assert.strictEqual(r.accepted, true);
|
|
241
|
+
|
|
242
|
+
// 伪造签名:委员3 签的票冒充委员0(round 1)→ 验签失败
|
|
243
|
+
const forged = makeVote(kps[3], 'Stop', 'nX', 1);
|
|
244
|
+
forged.voter = kps[0].did;
|
|
245
|
+
assert.throws(
|
|
246
|
+
() => market.verifyResultAuthenticated('task-qa', members, [forged], 1, now),
|
|
247
|
+
/签名无效/,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// 重放:委员3 在 round1 复用已用 nonce n1 → 拒绝
|
|
251
|
+
const replay = makeVote(kps[3], 'Stop', 'n1', 1);
|
|
252
|
+
assert.throws(
|
|
253
|
+
() => market.verifyResultAuthenticated('task-qa', members, [replay], 1, now),
|
|
254
|
+
/重放/,
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// 8e. 独立审计:完整生命周期通过、守恒无法发现的拆账篡改被捕获(v2.5.9)
|
|
259
|
+
test('独立审计:完整生命周期通过并捕获账实篡改', () => {
|
|
260
|
+
const market = new AgentMarket();
|
|
261
|
+
const owner = 'did:au:au-owner';
|
|
262
|
+
const requester = 'did:au:au-req';
|
|
263
|
+
market.deposit(owner, 100);
|
|
264
|
+
market.deposit(requester, 50);
|
|
265
|
+
const card = AgentCard.new({ did: owner, name: 'w' }).withSkill('writing');
|
|
266
|
+
market.registerAgent(card, MIN_STAKE);
|
|
267
|
+
market.publishTask('task-au', 'goal', 50, requester);
|
|
268
|
+
market.submitBid('task-au', owner, 40);
|
|
269
|
+
market.matchTask('task-au');
|
|
270
|
+
market.completeTask('task-au', true);
|
|
271
|
+
market.settle('task-au');
|
|
272
|
+
|
|
273
|
+
const good = market.independentAudit();
|
|
274
|
+
assert.ok(good.passed, `审计应通过: ${JSON.stringify(good.mismatches)}`);
|
|
275
|
+
|
|
276
|
+
// 拆账篡改:质押账户 100 → 50,另开幽灵账户 50(总额不变)
|
|
277
|
+
const stakeAcc = `__stake__:${owner}`;
|
|
278
|
+
const before = market.balance(stakeAcc);
|
|
279
|
+
market.balances.set(stakeAcc, before - 50);
|
|
280
|
+
market.balances.set('__ghost__:split', 50);
|
|
281
|
+
// 守恒检查被蒙蔽(余额总和不变)
|
|
282
|
+
const cons = market.conservationCheck(150);
|
|
283
|
+
assert.ok(cons.conserved, '拆账不改总额,守恒被蒙蔽');
|
|
284
|
+
// 独立审计捕获
|
|
285
|
+
const bad = market.independentAudit();
|
|
286
|
+
assert.ok(!bad.passed, '独立审计应发现账实不符');
|
|
287
|
+
assert.ok(
|
|
288
|
+
bad.mismatches.some((m) => m.account === '__ghost__:split'),
|
|
289
|
+
'应列出幽灵拆账账户',
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
198
293
|
// 9. AipIdentity DID 与签名往返
|
|
199
294
|
test('AipIdentity 生成 did:aip 且签名可验证', () => {
|
|
200
295
|
const id = AipIdentity.generate();
|
|
@@ -257,6 +352,71 @@ test('ACA receipt 签名、结果哈希正反例', () => {
|
|
|
257
352
|
assert.ok(!verifyReceiptResult(receipt, Buffer.from('other')));
|
|
258
353
|
});
|
|
259
354
|
|
|
355
|
+
// 13. v2.6.0 状态机恢复边:rework→running、no_quorum→open
|
|
356
|
+
test('v2.6.0 状态机恢复边', () => {
|
|
357
|
+
const market = new AgentMarket();
|
|
358
|
+
market.deposit('did:au:req', 50);
|
|
359
|
+
market.publishTask('task-1', 'goal', 50, 'did:au:req');
|
|
360
|
+
const t = market.tasks.get('task-1');
|
|
361
|
+
t.assign('did:au:worker'); // OPEN→MATCHED→ASSIGNED
|
|
362
|
+
t.start(); // ASSIGNED→RUNNING
|
|
363
|
+
t.complete(); // RUNNING→COMPLETED
|
|
364
|
+
|
|
365
|
+
// 返工 → 恢复执行
|
|
366
|
+
t.transition(TaskStatus.REWORK);
|
|
367
|
+
assert.strictEqual(t.status, TaskStatus.REWORK);
|
|
368
|
+
market.resumeAfterRework('task-1');
|
|
369
|
+
assert.strictEqual(t.status, TaskStatus.RUNNING);
|
|
370
|
+
// 非 REWORK 不能恢复
|
|
371
|
+
assert.throws(() => market.resumeAfterRework('task-1'), /REWORK/);
|
|
372
|
+
|
|
373
|
+
// 无共识 → 重新开放
|
|
374
|
+
t.complete(); // RUNNING→COMPLETED
|
|
375
|
+
t.markNoQuorum(); // COMPLETED→NO_QUORUM
|
|
376
|
+
assert.strictEqual(t.status, TaskStatus.NO_QUORUM);
|
|
377
|
+
market.reopenTask('task-1'); // NO_QUORUM→OPEN
|
|
378
|
+
assert.strictEqual(t.status, TaskStatus.OPEN);
|
|
379
|
+
// 非 NO_QUORUM 不能 reopen
|
|
380
|
+
assert.throws(() => market.reopenTask('task-1'), /NO_QUORUM/);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// 14. v2.6.0 Unverified 证据拒绝结算
|
|
384
|
+
test('v2.6.0 Unverified 证据拒绝结算', () => {
|
|
385
|
+
const market = new AgentMarket();
|
|
386
|
+
const owner = 'did:au:owner';
|
|
387
|
+
market.deposit(owner, 100);
|
|
388
|
+
market.deposit('did:au:req', 50);
|
|
389
|
+
market.registerAgent(AgentCard.new({ did: owner, name: 'w' }), MIN_STAKE);
|
|
390
|
+
market.publishTask('task-1', 'goal', 50, 'did:au:req');
|
|
391
|
+
market.submitBid('task-1', owner, 40);
|
|
392
|
+
market.matchTask('task-1');
|
|
393
|
+
// 验收通过但证据 Unverified
|
|
394
|
+
market.completeTask('task-1', true, 'Unverified');
|
|
395
|
+
assert.strictEqual(market.tasks.get('task-1').status, TaskStatus.VERIFIED);
|
|
396
|
+
const r = market.settle('task-1');
|
|
397
|
+
assert.strictEqual(r.reason, 'untrusted_evidence');
|
|
398
|
+
assert.strictEqual(r.paid, 0);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// 15. v2.6.0 policy=None 提交即验收、豁免证据门禁
|
|
402
|
+
test('v2.6.0 policy=None 提交即验收并豁免证据', () => {
|
|
403
|
+
const market = new AgentMarket();
|
|
404
|
+
const owner = 'did:au:owner';
|
|
405
|
+
market.deposit(owner, 100);
|
|
406
|
+
market.deposit('did:au:req', 50);
|
|
407
|
+
market.registerAgent(AgentCard.new({ did: owner, name: 'w' }), MIN_STAKE);
|
|
408
|
+
const t = market.publishTask('task-1', 'goal', 50, 'did:au:req');
|
|
409
|
+
t.verificationPolicy = 'None';
|
|
410
|
+
market.submitBid('task-1', owner, 40);
|
|
411
|
+
market.matchTask('task-1');
|
|
412
|
+
// 无 QA 投票,提交(证据 Unverified)即验收
|
|
413
|
+
market.completeTask('task-1', true, 'Unverified');
|
|
414
|
+
assert.strictEqual(t.status, TaskStatus.VERIFIED);
|
|
415
|
+
const r = market.settle('task-1');
|
|
416
|
+
assert.strictEqual(r.reason, 'settled');
|
|
417
|
+
assert.strictEqual(r.paid, 40);
|
|
418
|
+
});
|
|
419
|
+
|
|
260
420
|
console.log(`\n${passed} 项测试通过`);
|
|
261
421
|
if (process.exitCode === 1) {
|
|
262
422
|
console.error('存在失败测试');
|