@uuaid/agent-commons 0.0.0-stage → 0.2.0-alpha.2

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.
Files changed (69) hide show
  1. package/CONFORMANCE.md +26 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +25 -0
  4. package/PROVENANCE.json +29 -0
  5. package/README.md +316 -2
  6. package/bin/agent-commons.mjs +71 -0
  7. package/examples/Dockerfile +12 -0
  8. package/examples/agent.mjs +17 -0
  9. package/examples/global-policy.json +9 -0
  10. package/examples/run-cohort.mjs +68 -0
  11. package/package.json +31 -4
  12. package/profiles/agentnet.chat.json +11 -0
  13. package/profiles/local.json +11 -0
  14. package/profiles/zilligon.com.json +11 -0
  15. package/profiles/zilligons.com.json +11 -0
  16. package/src/carrier.mjs +79 -0
  17. package/src/channels.d.mts +75 -0
  18. package/src/channels.mjs +158 -0
  19. package/src/cohort.mjs +61 -0
  20. package/src/collaboration.mjs +875 -0
  21. package/src/config.mjs +39 -0
  22. package/src/continuity.d.mts +24 -0
  23. package/src/continuity.mjs +594 -0
  24. package/src/governance-scope.d.mts +46 -0
  25. package/src/governance-scope.mjs +139 -0
  26. package/src/governance.mjs +353 -0
  27. package/src/index.d.ts +109 -0
  28. package/src/index.mjs +15 -0
  29. package/src/loop.mjs +71 -0
  30. package/src/offline.d.mts +7 -0
  31. package/src/offline.mjs +29 -0
  32. package/src/pillar.mjs +59 -0
  33. package/src/profiles.d.mts +12 -0
  34. package/src/profiles.mjs +91 -0
  35. package/src/runtime.mjs +346 -0
  36. package/src/store.mjs +73 -0
  37. package/src/transport.d.mts +76 -0
  38. package/src/transport.mjs +267 -0
  39. package/src/trust.mjs +65 -0
  40. package/src/vendor/pillar/blob/blob.mjs +510 -0
  41. package/src/vendor/pillar/crypto/e2e.mjs +97 -0
  42. package/src/vendor/pillar/identity/jcs.mjs +35 -0
  43. package/src/vendor/pillar/identity/keychain.mjs +192 -0
  44. package/src/vendor/pillar/identity/tier.mjs +130 -0
  45. package/src/vendor/pillar/net/carrier-client.mjs +197 -0
  46. package/src/vendor/pillar/net/delegation.mjs +209 -0
  47. package/src/vendor/pillar/net/envelope.mjs +258 -0
  48. package/src/vendor/pillar/net/refusals.mjs +127 -0
  49. package/src/vendor/pillar/net/seed-tier.mjs +185 -0
  50. package/test/channels.test.mjs +268 -0
  51. package/test/cohort.test.mjs +36 -0
  52. package/test/collaboration.test.mjs +459 -0
  53. package/test/continuity.test.mjs +229 -0
  54. package/test/custody.negative.test.mjs +165 -0
  55. package/test/derivation-parity.test.mjs +19 -0
  56. package/test/fixtures/derivation-vectors.json +26 -0
  57. package/test/fixtures/ed25519-small-order.vectors.json +15 -0
  58. package/test/governance-expected-red.expected-red.mjs +241 -0
  59. package/test/governance-scope.test.mjs +142 -0
  60. package/test/governance-t1-t10.test.mjs +477 -0
  61. package/test/governance.test.mjs +514 -0
  62. package/test/hold-admission.test.mjs +1185 -0
  63. package/test/keychain-strict-ed25519.test.mjs +240 -0
  64. package/test/offline.test.mjs +152 -0
  65. package/test/runtime-channels.test.mjs +1140 -0
  66. package/test/runtime.test.mjs +201 -0
  67. package/test/security-runtime.test.mjs +104 -0
  68. package/test/strict-ed25519-consumers.test.mjs +234 -0
  69. package/test/transport.test.mjs +353 -0
@@ -0,0 +1,1140 @@
1
+ // L3 Slice 2 v6 §3 / §8: Property 1-5 Two-Pole Test Suite
2
+ import test from "node:test";
3
+ import assert from "node:assert/strict";
4
+ import { mkdtempSync, rmSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import {
8
+ AgentCommons,
9
+ CommonsStore,
10
+ Keychain,
11
+ createProfile,
12
+ verifyProfile,
13
+ MemoryLoopbackTransport,
14
+ PillarCarrierTransport,
15
+ classifyThread,
16
+ validateTenantBinding,
17
+ } from "../src/index.mjs";
18
+ import { jcs, envelopeSha, seal } from "../src/pillar.mjs";
19
+ import { encode, digest } from "../src/profiles.mjs";
20
+
21
+ function makeKey() {
22
+ const k = new Keychain("unused");
23
+ k._identity = Keychain.generate();
24
+ return k;
25
+ }
26
+
27
+ function makeEnv(tempDir) {
28
+ const atlasKey = makeKey();
29
+ const lyraKey = makeKey();
30
+ const atlasId = atlasKey._identity.uuaid;
31
+ const lyraId = lyraKey._identity.uuaid;
32
+ const channelUri = "channel://local/community/general";
33
+ const tenantId = "tenant-a";
34
+
35
+ const policy = {
36
+ mode: "local",
37
+ offline: true,
38
+ channelsEnabled: true,
39
+ channels: {
40
+ [channelUri]: {
41
+ members: [atlasId, lyraId],
42
+ allowForwarding: false,
43
+ },
44
+ },
45
+ agents: {
46
+ [atlasId]: {
47
+ kind: "agent",
48
+ publicKey: atlasKey._identity.publicKeyHex,
49
+ capabilities: ["commons:message", "commons:relay"],
50
+ tenantId,
51
+ },
52
+ [lyraId]: {
53
+ kind: "agent",
54
+ publicKey: lyraKey._identity.publicKeyHex,
55
+ capabilities: ["commons:message"],
56
+ tenantId,
57
+ },
58
+ },
59
+ tenantProfiles: {},
60
+ };
61
+
62
+ const profile = createProfile({
63
+ namespace: "local/test",
64
+ name: "Test Profile",
65
+ scope: "local",
66
+ fixtures: ["semantic equivalence A", "semantic equivalence B"],
67
+ });
68
+
69
+ policy.tenantProfiles[profile.id] = tenantId;
70
+
71
+ const dbPath = tempDir ? join(tempDir, "store.db") : ":memory:";
72
+ const atlasStore = new CommonsStore(dbPath);
73
+ const lyraStore = new CommonsStore(":memory:");
74
+ const transport = new MemoryLoopbackTransport();
75
+
76
+ const atlasRuntime = new AgentCommons({
77
+ keychain: atlasKey,
78
+ store: atlasStore,
79
+ policy,
80
+ transport,
81
+ });
82
+ atlasRuntime.addProfile(profile);
83
+
84
+ const lyraRuntime = new AgentCommons({
85
+ keychain: lyraKey,
86
+ store: lyraStore,
87
+ policy,
88
+ transport,
89
+ });
90
+ lyraRuntime.addProfile(profile);
91
+
92
+ return {
93
+ atlasKey,
94
+ lyraKey,
95
+ atlasId,
96
+ lyraId,
97
+ channelUri,
98
+ tenantId,
99
+ policy,
100
+ profile,
101
+ atlasStore,
102
+ lyraStore,
103
+ transport,
104
+ atlasRuntime,
105
+ lyraRuntime,
106
+ };
107
+ }
108
+
109
+ // =========================================================================
110
+ // Property 1: Pluggable Transport Wiring & Receipt Acceptance (§3.1, §3.3, §3.5)
111
+ // =========================================================================
112
+ test("Property 1: CONTROL A - Memory transport auto-flush delivers and consumes outbox row", async () => {
113
+ const env = makeEnv();
114
+ const fixtureReceipt = {
115
+ carrier: "memory://loopback/test",
116
+ seq: 1,
117
+ sha: "fixture-sha",
118
+ duplicate: false,
119
+ all: [],
120
+ accepted: true,
121
+ };
122
+ env.transport.deliver = async (envelope) => fixtureReceipt;
123
+
124
+ const res = await env.atlasRuntime.send({
125
+ recipient: env.lyraId,
126
+ profileId: env.profile.id,
127
+ thread: env.channelUri,
128
+ body: "hello",
129
+ });
130
+
131
+ assert.equal(res.result.length, 1);
132
+ assert.equal(res.result[0].state, "carrier-accepted");
133
+ assert.deepEqual(res.result[0].receipt, fixtureReceipt);
134
+
135
+ const pendingRows = env.atlasStore.pending();
136
+ assert.equal(pendingRows.length, 0);
137
+
138
+ const subsequentFlush = await env.atlasRuntime.flush();
139
+ assert.deepEqual(subsequentFlush, []);
140
+ });
141
+
142
+ test("Property 1: CONTROL B - Omitted transport fallback preserves pending row without error", async () => {
143
+ const atlasKey = makeKey();
144
+ const atlasId = atlasKey._identity.uuaid;
145
+ const lyraKey = makeKey();
146
+ const lyraId = lyraKey._identity.uuaid;
147
+ const channelUri = "channel://local/community/general";
148
+ const tenantId = "tenant-a";
149
+
150
+ const policy = {
151
+ mode: "local",
152
+ offline: true,
153
+ channelsEnabled: true,
154
+ channels: { [channelUri]: { members: [atlasId, lyraId] } },
155
+ agents: {
156
+ [atlasId]: { kind: "agent", publicKey: atlasKey._identity.publicKeyHex, capabilities: ["commons:message"], tenantId },
157
+ [lyraId]: { kind: "agent", publicKey: lyraKey._identity.publicKeyHex, capabilities: ["commons:message"], tenantId },
158
+ },
159
+ tenantProfiles: {},
160
+ };
161
+ const profile = createProfile({
162
+ namespace: "local/test",
163
+ name: "Test Profile",
164
+ scope: "local",
165
+ fixtures: ["semantic equivalence A", "semantic equivalence B"],
166
+ });
167
+ policy.tenantProfiles[profile.id] = tenantId;
168
+
169
+ const atlasStore = new CommonsStore(":memory:");
170
+ const atlasRuntime = new AgentCommons({
171
+ keychain: atlasKey,
172
+ store: atlasStore,
173
+ policy,
174
+ carriers: [], // Omitted transport and empty carriers
175
+ });
176
+ atlasRuntime.addProfile(profile);
177
+
178
+ const res = await atlasRuntime.send({
179
+ recipient: lyraId,
180
+ profileId: profile.id,
181
+ thread: channelUri,
182
+ body: "hello",
183
+ });
184
+
185
+ assert.equal(res.state, "pending");
186
+ assert.equal(res.reason, "No configured carrier");
187
+
188
+ const flushRes = await atlasRuntime.flush();
189
+ assert.deepEqual(flushRes, []);
190
+
191
+ const pendingRows = atlasStore.pending();
192
+ assert.equal(pendingRows.length, 1);
193
+ assert.equal(pendingRows[0].state, "pending");
194
+ });
195
+
196
+ test("Property 1: MUTANT A - Unaccepted receipt refusal retains pending with backoff", async () => {
197
+ const env = makeEnv();
198
+ const rejectedReceipt = {
199
+ carrier: "memory://loopback/test",
200
+ seq: 1,
201
+ sha: "fixture-sha",
202
+ duplicate: false,
203
+ all: [],
204
+ accepted: false,
205
+ };
206
+ env.transport.deliver = async () => rejectedReceipt;
207
+
208
+ const res = await env.atlasRuntime.send({
209
+ recipient: env.lyraId,
210
+ profileId: env.profile.id,
211
+ thread: env.channelUri,
212
+ body: "hello",
213
+ });
214
+
215
+ assert.equal(res.result.length, 1);
216
+ assert.equal(res.result[0].state, "retry-or-quarantine");
217
+ assert.equal(res.result[0].error, "Carrier rejected envelope");
218
+
219
+ const pendingRows = env.atlasStore.pending();
220
+ assert.equal(pendingRows.length, 0); // next_at is in future (now + 2000), so pending() filters it out
221
+
222
+ const allRows = env.atlasStore.db.prepare("SELECT * FROM outbox").all();
223
+ assert.equal(allRows.length, 1);
224
+ assert.equal(allRows[0].state, "pending");
225
+ assert.equal(allRows[0].attempts, 1);
226
+ assert.equal(allRows[0].error, "Carrier rejected envelope");
227
+
228
+ const flushRes = await env.atlasRuntime.flush();
229
+ assert.deepEqual(flushRes, []);
230
+ });
231
+
232
+ test("Property 1: Five-attempt quarantine boundary with controlled clock", async () => {
233
+ const env = makeEnv();
234
+ env.transport.deliver = async () => ({ accepted: false, carrier: "mem", sha: "s" });
235
+
236
+ await env.atlasRuntime.send({
237
+ recipient: env.lyraId,
238
+ profileId: env.profile.id,
239
+ thread: env.channelUri,
240
+ body: "retry-test",
241
+ deferFlush: true,
242
+ });
243
+
244
+ const row = env.atlasStore.pending()[0];
245
+ assert.ok(row);
246
+
247
+ // Simulate attempts 1 to 4 with store.failure()
248
+ for (let i = 1; i <= 4; i++) {
249
+ env.atlasStore.failure(row.id, `error-${i}`);
250
+ const r = env.atlasStore.db.prepare("SELECT * FROM outbox WHERE id = ?").get(row.id);
251
+ assert.equal(r.state, "pending");
252
+ assert.equal(r.attempts, i);
253
+ }
254
+
255
+ // Attempt 5 triggers quarantine
256
+ env.atlasStore.failure(row.id, "error-5");
257
+ const quarantinedRow = env.atlasStore.db.prepare("SELECT * FROM outbox WHERE id = ?").get(row.id);
258
+ assert.equal(quarantinedRow.state, "quarantined");
259
+ assert.equal(quarantinedRow.attempts, 5);
260
+ assert.equal(env.atlasStore.pending().length, 0);
261
+ });
262
+
263
+ test("Property 1: MUTANT B - Offline non-loopback carrier catch returns retry-or-quarantine", async () => {
264
+ const origOffline = process.env.AGENT_COMMONS_OFFLINE;
265
+ process.env.AGENT_COMMONS_OFFLINE = "1";
266
+ try {
267
+ const env = makeEnv();
268
+ const mockCarrierClient = {
269
+ deliver: async () => {
270
+ throw new Error("Carrier refused loopback requirement: https://carrier.example.com");
271
+ },
272
+ };
273
+ const pillarTransport = new PillarCarrierTransport({
274
+ carriers: ["https://carrier.example.com"],
275
+ client: mockCarrierClient,
276
+ });
277
+ env.atlasRuntime.transport = pillarTransport;
278
+
279
+ const res = await env.atlasRuntime.send({
280
+ recipient: env.lyraId,
281
+ profileId: env.profile.id,
282
+ thread: env.channelUri,
283
+ body: "offline-test",
284
+ deferFlush: true,
285
+ });
286
+ assert.equal(res.state, "pending");
287
+
288
+ const flushRes = await env.atlasRuntime.flush();
289
+ assert.equal(flushRes.length, 1);
290
+ assert.equal(flushRes[0].state, "retry-or-quarantine");
291
+ assert.match(flushRes[0].error, /offline|loopback/i);
292
+
293
+ const pendingInDb = env.atlasStore.db.prepare("SELECT * FROM outbox").all();
294
+ assert.equal(pendingInDb.length, 1);
295
+ assert.equal(pendingInDb[0].state, "pending");
296
+ } finally {
297
+ if (origOffline === undefined) delete process.env.AGENT_COMMONS_OFFLINE;
298
+ else process.env.AGENT_COMMONS_OFFLINE = origOffline;
299
+ }
300
+ });
301
+
302
+ // =========================================================================
303
+ // Property 2: Structured Channel Send Admission & Validation Precedence (§4.2, §4.2.3)
304
+ // =========================================================================
305
+ test("Property 2: CONTROL - Structured channel send succeeds when sender and recipient are admitted members", async () => {
306
+ const env = makeEnv();
307
+ const res = await env.atlasRuntime.send({
308
+ recipient: env.lyraId,
309
+ profileId: env.profile.id,
310
+ thread: env.channelUri,
311
+ body: "passing-channel-msg",
312
+ });
313
+ assert.equal(res.result[0].state, "carrier-accepted");
314
+ assert.ok(res.envelope);
315
+ });
316
+
317
+ test("Property 2: MUTANT A - Unauthorized sender throws UNAUTHORIZED_CHANNEL_SENDER with zero writes", async () => {
318
+ const env = makeEnv();
319
+ env.policy.channels[env.channelUri].members = [env.lyraId]; // Atlas removed
320
+
321
+ await assert.rejects(
322
+ () =>
323
+ env.atlasRuntime.send({
324
+ recipient: env.lyraId,
325
+ profileId: env.profile.id,
326
+ thread: env.channelUri,
327
+ body: "unauthorized-sender",
328
+ }),
329
+ (err) => err.code === "UNAUTHORIZED_CHANNEL_SENDER" || /UNAUTHORIZED_CHANNEL_SENDER|Sender atlas is unauthorized/.test(err.message)
330
+ );
331
+
332
+ const outboxRows = env.atlasStore.db.prepare("SELECT COUNT(*) as count FROM outbox").get();
333
+ assert.equal(outboxRows.count, 0);
334
+ });
335
+
336
+ test("Property 2: MUTANT B - Unauthorized recipient throws UNAUTHORIZED_CHANNEL_RECIPIENT with zero writes", async () => {
337
+ const env = makeEnv();
338
+ env.policy.channels[env.channelUri].members = [env.atlasId]; // Lyra removed
339
+
340
+ await assert.rejects(
341
+ () =>
342
+ env.atlasRuntime.send({
343
+ recipient: env.lyraId,
344
+ profileId: env.profile.id,
345
+ thread: env.channelUri,
346
+ body: "unauthorized-recipient",
347
+ }),
348
+ (err) => err.code === "UNAUTHORIZED_CHANNEL_RECIPIENT" || /UNAUTHORIZED_CHANNEL_RECIPIENT|Receiver lyra is unauthorized/.test(err.message)
349
+ );
350
+
351
+ const outboxRows = env.atlasStore.db.prepare("SELECT COUNT(*) as count FROM outbox").get();
352
+ assert.equal(outboxRows.count, 0);
353
+ });
354
+
355
+ test("Property 2: MUTANT C - Reserved-prefix malformed / uppercase URI throws INVALID_CHANNEL_URI", async () => {
356
+ const env = makeEnv();
357
+ env.policy.channelsEnabled = false; // Even when channels disabled, reserved prefix takes precedence
358
+
359
+ for (const badUri of ["channel://LOCAL/COMMUNITY/GENERAL", "channel:malformed"]) {
360
+ await assert.rejects(
361
+ () =>
362
+ env.atlasRuntime.send({
363
+ recipient: env.lyraId,
364
+ profileId: env.profile.id,
365
+ thread: badUri,
366
+ body: "bad-uri",
367
+ }),
368
+ (err) => err.code === "INVALID_CHANNEL_URI" || /INVALID_CHANNEL_URI/.test(err.message)
369
+ );
370
+ }
371
+
372
+ const outboxRows = env.atlasStore.db.prepare("SELECT COUNT(*) as count FROM outbox").get();
373
+ assert.equal(outboxRows.count, 0);
374
+ });
375
+
376
+ test("Property 2: MUTANT D - Unconfigured channel throws UNCONFIGURED_CHANNEL with zero writes", async () => {
377
+ const env = makeEnv();
378
+ await assert.rejects(
379
+ () =>
380
+ env.atlasRuntime.send({
381
+ recipient: env.lyraId,
382
+ profileId: env.profile.id,
383
+ thread: "channel://local/community/unconfigured",
384
+ body: "unconfigured-channel",
385
+ }),
386
+ (err) => err.code === "UNCONFIGURED_CHANNEL" || /UNCONFIGURED_CHANNEL|Channel is unconfigured/.test(err.message)
387
+ );
388
+
389
+ const outboxRows = env.atlasStore.db.prepare("SELECT COUNT(*) as count FROM outbox").get();
390
+ assert.equal(outboxRows.count, 0);
391
+ });
392
+
393
+ test("Property 2: MUTANT E - Strict mode legacy thread throws STRUCTURED_CHANNELS_REQUIRED with zero writes", async () => {
394
+ const env = makeEnv();
395
+ env.policy.channelsEnabled = true;
396
+
397
+ await assert.rejects(
398
+ () =>
399
+ env.atlasRuntime.send({
400
+ recipient: env.lyraId,
401
+ profileId: env.profile.id,
402
+ thread: "commons",
403
+ body: "legacy-thread",
404
+ }),
405
+ (err) => err.code === "STRUCTURED_CHANNELS_REQUIRED" || /STRUCTURED_CHANNELS_REQUIRED/.test(err.message)
406
+ );
407
+
408
+ const outboxRows = env.atlasStore.db.prepare("SELECT COUNT(*) as count FROM outbox").get();
409
+ assert.equal(outboxRows.count, 0);
410
+ });
411
+
412
+ // =========================================================================
413
+ // Property 3: Structured Channel Inbound Receive Admission & Quarantine (§4.2, §4.2.3)
414
+ // =========================================================================
415
+ test("Property 3: CONTROL - Inbound receive and poll succeed for admitted member envelope", async () => {
416
+ const env = makeEnv();
417
+ // 1. Direct receive on envelope 1
418
+ const sent1 = await env.atlasRuntime.send({
419
+ recipient: env.lyraId,
420
+ profileId: env.profile.id,
421
+ thread: env.channelUri,
422
+ body: "hello-lyra-direct",
423
+ deferFlush: true,
424
+ });
425
+
426
+ const recv = await env.lyraRuntime.receive(sent1.envelope);
427
+ assert.equal(recv.receipt.kind, "host-accepted");
428
+ assert.equal(recv.body, "hello-lyra-direct");
429
+ assert.equal(recv.accepted, true);
430
+
431
+ const processedCount1 = env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c;
432
+ assert.equal(processedCount1, 1);
433
+
434
+ // 2. Poll on envelope 2 delivered to transport
435
+ const sent2 = await env.atlasRuntime.send({
436
+ recipient: env.lyraId,
437
+ profileId: env.profile.id,
438
+ thread: env.channelUri,
439
+ body: "hello-lyra-poll",
440
+ deferFlush: true,
441
+ });
442
+ await env.transport.deliver(sent2.envelope);
443
+
444
+ const polled = await env.lyraRuntime.poll();
445
+ assert.equal(polled.length, 1);
446
+ assert.equal(polled[0].accepted, true);
447
+ assert.equal(polled[0].body, "hello-lyra-poll");
448
+
449
+ const processedCount2 = env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c;
450
+ assert.equal(processedCount2, 2);
451
+ });
452
+
453
+ test("Property 3: MUTANT A & B - Unauthorized inbound sender fails receive() and poll()", async () => {
454
+ const env = makeEnv();
455
+ const sent = await env.atlasRuntime.send({
456
+ recipient: env.lyraId,
457
+ profileId: env.profile.id,
458
+ thread: env.channelUri,
459
+ body: "sender-unauth-test",
460
+ deferFlush: true,
461
+ });
462
+
463
+ // Mutate receiver policy only
464
+ env.lyraRuntime.policy.channels[env.channelUri].members = [env.lyraId]; // Atlas removed
465
+
466
+ // MUTANT A: direct receive() throws
467
+ await assert.rejects(
468
+ () => env.lyraRuntime.receive(sent.envelope),
469
+ (err) => err.code === "UNAUTHORIZED_CHANNEL_SENDER" || /UNAUTHORIZED_CHANNEL_SENDER|Sender .* is unauthorized for channel/.test(err.message)
470
+ );
471
+ assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
472
+ assert.equal(env.lyraStore.audit().filter(e => JSON.parse(e.body).kind === "control-received").length, 0);
473
+
474
+ // MUTANT B: poll() quarantines with exact reason and advances cursor
475
+ await env.transport.deliver(sent.envelope);
476
+ const polled = await env.lyraRuntime.poll();
477
+ assert.equal(polled.length, 1);
478
+ assert.equal(polled[0].rejected, true);
479
+ assert.equal(polled[0].reason, `Sender ${env.atlasId} is unauthorized for channel ${env.channelUri}`);
480
+ assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
481
+ assert.equal(env.lyraStore.audit().filter(e => JSON.parse(e.body).kind === "control-received").length, 0);
482
+
483
+ // Second poll returns empty because cursor advanced
484
+ const secondPoll = await env.lyraRuntime.poll();
485
+ assert.equal(secondPoll.length, 0);
486
+ });
487
+
488
+ test("Property 3: MUTANT C & D - Unauthorized inbound receiver fails receive() and poll()", async () => {
489
+ const env = makeEnv();
490
+ const sent = await env.atlasRuntime.send({
491
+ recipient: env.lyraId,
492
+ profileId: env.profile.id,
493
+ thread: env.channelUri,
494
+ body: "recv-unauth-test",
495
+ deferFlush: true,
496
+ });
497
+
498
+ // Mutate receiver policy only: Lyra removed
499
+ env.lyraRuntime.policy.channels[env.channelUri].members = [env.atlasId];
500
+
501
+ // MUTANT C: direct receive() throws
502
+ await assert.rejects(
503
+ () => env.lyraRuntime.receive(sent.envelope),
504
+ (err) => err.code === "UNAUTHORIZED_CHANNEL_RECEIVER" || /UNAUTHORIZED_CHANNEL_RECEIVER|Receiver .* is unauthorized for channel/.test(err.message)
505
+ );
506
+ assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
507
+
508
+ // MUTANT D: poll() quarantines with exact reason and advances cursor
509
+ await env.transport.deliver(sent.envelope);
510
+ const polled = await env.lyraRuntime.poll();
511
+ assert.equal(polled.length, 1);
512
+ assert.equal(polled[0].rejected, true);
513
+ assert.equal(polled[0].reason, `Receiver ${env.lyraId} is unauthorized for channel ${env.channelUri}`);
514
+ assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
515
+
516
+ const secondPoll = await env.lyraRuntime.poll();
517
+ assert.equal(secondPoll.length, 0);
518
+ });
519
+
520
+ test("Property 3: MUTANT E - Strict mode inbound legacy rejection fails receive() and poll()", async () => {
521
+ const env = makeEnv();
522
+ // Sender queues legacy envelope under channelsEnabled: false
523
+ env.atlasRuntime.policy.channelsEnabled = false;
524
+ const sent = await env.atlasRuntime.send({
525
+ recipient: env.lyraId,
526
+ profileId: env.profile.id,
527
+ thread: "commons",
528
+ body: "legacy-inbound",
529
+ deferFlush: true,
530
+ });
531
+
532
+ // Receiver has channelsEnabled: true (strict)
533
+ env.lyraRuntime.policy.channelsEnabled = true;
534
+
535
+ // Direct receive
536
+ await assert.rejects(
537
+ () => env.lyraRuntime.receive(sent.envelope),
538
+ (err) => err.code === "STRUCTURED_CHANNELS_REQUIRED" || /STRUCTURED_CHANNELS_REQUIRED/.test(err.message)
539
+ );
540
+ assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
541
+
542
+ // Poll
543
+ await env.transport.deliver(sent.envelope);
544
+ const polled = await env.lyraRuntime.poll();
545
+ assert.equal(polled.length, 1);
546
+ assert.equal(polled[0].rejected, true);
547
+ assert.match(polled[0].reason, /STRUCTURED_CHANNELS_REQUIRED/);
548
+ assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
549
+ });
550
+
551
+ test("Property 3: MUTANT F - Inbound reserved-malformed rejection fails receive() and poll()", async () => {
552
+ const env = makeEnv();
553
+ const bodyText = "malformed-thread-body";
554
+ const payload = {
555
+ v: "agent-commons/1",
556
+ id: "00000000-0000-0000-0000-000000000001",
557
+ profileId: env.profile.id,
558
+ thread: "channel:malformed",
559
+ kind: "message",
560
+ wire: encode(bodyText, env.profile.lexicon),
561
+ bodyHash: digest(bodyText),
562
+ expiresAt: new Date(Date.now() + 3600000).toISOString(),
563
+ };
564
+ const sealedEnvelope = seal(env.atlasKey, {
565
+ recipient: env.lyraId,
566
+ recipientPublicKey: env.lyraKey._identity.publicKeyHex,
567
+ kind: "agent-commons/1",
568
+ payload,
569
+ });
570
+
571
+ // Direct receive
572
+ await assert.rejects(
573
+ () => env.lyraRuntime.receive(sealedEnvelope),
574
+ (err) => err.code === "INVALID_CHANNEL_URI" || /INVALID_CHANNEL_URI/.test(err.message)
575
+ );
576
+ assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
577
+
578
+ // Poll
579
+ await env.transport.deliver(sealedEnvelope);
580
+ const polled = await env.lyraRuntime.poll();
581
+ assert.equal(polled.length, 1);
582
+ assert.equal(polled[0].rejected, true);
583
+ assert.match(polled[0].reason, /INVALID_CHANNEL_URI/);
584
+ assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
585
+ });
586
+
587
+ // =========================================================================
588
+ // Property 4: Tenant Boundary Fail-Closed Across All Scopes & Durable Flush (§5.1, §5.2)
589
+ // =========================================================================
590
+ test("Property 4: CONTROL A & B - Tenant matching and unbound local exemption", async () => {
591
+ const env = makeEnv();
592
+ // CONTROL A: Local profile bound to tenant-a, matching sender and receiver
593
+ const resA = await env.atlasRuntime.send({
594
+ recipient: env.lyraId,
595
+ profileId: env.profile.id,
596
+ thread: env.channelUri,
597
+ body: "tenant-match",
598
+ });
599
+ assert.equal(resA.result[0].state, "carrier-accepted");
600
+
601
+ // CONTROL B: Unbound local profile exemption
602
+ const unboundLocalProfile = createProfile({
603
+ namespace: "local/unbound",
604
+ name: "Unbound Local Profile",
605
+ scope: "local",
606
+ fixtures: ["fixture one local", "fixture two local"],
607
+ });
608
+ env.atlasRuntime.addProfile(unboundLocalProfile);
609
+ env.lyraRuntime.addProfile(unboundLocalProfile);
610
+ delete env.policy.tenantProfiles[unboundLocalProfile.id];
611
+
612
+ const resB = await env.atlasRuntime.send({
613
+ recipient: env.lyraId,
614
+ profileId: unboundLocalProfile.id,
615
+ thread: env.channelUri,
616
+ body: "unbound-local-pass",
617
+ });
618
+ assert.equal(resB.result[0].state, "carrier-accepted");
619
+ });
620
+
621
+ test("Property 4: MUTANT A & B - Bound local tenant mismatch throws TENANT_NAMESPACE_MISMATCH", async () => {
622
+ const envA = makeEnv();
623
+ envA.policy.agents[envA.lyraId].tenantId = "tenant-b"; // Recipient mismatch
624
+ await assert.rejects(
625
+ () =>
626
+ envA.atlasRuntime.send({
627
+ recipient: envA.lyraId,
628
+ profileId: envA.profile.id,
629
+ thread: envA.channelUri,
630
+ body: "msg",
631
+ }),
632
+ (err) => err.code === "TENANT_NAMESPACE_MISMATCH" || /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i.test(err.message)
633
+ );
634
+
635
+ const envB = makeEnv();
636
+ envB.policy.agents[envB.atlasId].tenantId = "tenant-b"; // Sender mismatch
637
+ await assert.rejects(
638
+ () =>
639
+ envB.atlasRuntime.send({
640
+ recipient: envB.lyraId,
641
+ profileId: envB.profile.id,
642
+ thread: envB.channelUri,
643
+ body: "msg",
644
+ }),
645
+ (err) => err.code === "TENANT_NAMESPACE_MISMATCH" || /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i.test(err.message)
646
+ );
647
+ });
648
+
649
+ test("Property 4: MUTANT C & D - Missing tenant binding throws TENANT_NAMESPACE_MISMATCH", async () => {
650
+ const envC = makeEnv();
651
+ delete envC.policy.agents[envC.atlasId].tenantId; // Missing sender binding
652
+ await assert.rejects(
653
+ () =>
654
+ envC.atlasRuntime.send({
655
+ recipient: envC.lyraId,
656
+ profileId: envC.profile.id,
657
+ thread: envC.channelUri,
658
+ body: "msg",
659
+ }),
660
+ (err) => err.code === "TENANT_NAMESPACE_MISMATCH" || /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i.test(err.message)
661
+ );
662
+
663
+ const envD = makeEnv();
664
+ delete envD.policy.agents[envD.lyraId].tenantId; // Missing recipient binding
665
+ await assert.rejects(
666
+ () =>
667
+ envD.atlasRuntime.send({
668
+ recipient: envD.lyraId,
669
+ profileId: envD.profile.id,
670
+ thread: envD.channelUri,
671
+ body: "msg",
672
+ }),
673
+ (err) => err.code === "TENANT_NAMESPACE_MISMATCH" || /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i.test(err.message)
674
+ );
675
+ });
676
+
677
+ test("Property 4: MUTANT E, F, G - Inbound receive tenant mismatches and missing bindings fail closed", async () => {
678
+ const env = makeEnv();
679
+ const sent = await env.atlasRuntime.send({
680
+ recipient: env.lyraId,
681
+ profileId: env.profile.id,
682
+ thread: env.channelUri,
683
+ body: "inbound-tenant-test",
684
+ deferFlush: true,
685
+ });
686
+
687
+ // MUTANT E: Receiver has tenant-b
688
+ env.lyraRuntime.policy.agents[env.lyraId].tenantId = "tenant-b";
689
+ await assert.rejects(
690
+ () => env.lyraRuntime.receive(sent.envelope),
691
+ (err) => err.code === "TENANT_NAMESPACE_MISMATCH" || /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i.test(err.message)
692
+ );
693
+ assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
694
+
695
+ // Reset Lyra to tenant-a, then MUTANT F: Sender tenantId removed from receiver policy
696
+ env.lyraRuntime.policy.agents[env.lyraId].tenantId = "tenant-a";
697
+ delete env.lyraRuntime.policy.agents[env.atlasId].tenantId;
698
+ await assert.rejects(
699
+ () => env.lyraRuntime.receive(sent.envelope),
700
+ (err) => err.code === "TENANT_NAMESPACE_MISMATCH" || /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i.test(err.message)
701
+ );
702
+ assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
703
+
704
+ // Restore sender tenantId, then MUTANT G: Receiver tenantId removed from receiver policy
705
+ env.lyraRuntime.policy.agents[env.atlasId].tenantId = "tenant-a";
706
+ delete env.lyraRuntime.policy.agents[env.lyraId].tenantId;
707
+ await assert.rejects(
708
+ () => env.lyraRuntime.receive(sent.envelope),
709
+ (err) => err.code === "TENANT_NAMESPACE_MISMATCH" || /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i.test(err.message)
710
+ );
711
+ assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
712
+ });
713
+
714
+ test("Property 4: MUTANT H & I - Unbound tenant and global scope profiles throw UNBOUND_PROFILE_TENANT", async () => {
715
+ const env = makeEnv();
716
+
717
+ // MUTANT H: scope tenant without tenantProfiles entry
718
+ const tenantProfile = createProfile({
719
+ namespace: "tenant/org",
720
+ name: "Org Profile",
721
+ scope: "tenant",
722
+ fixtures: ["tenant fixture alpha", "tenant fixture beta"],
723
+ });
724
+ env.atlasRuntime.addProfile(tenantProfile);
725
+ delete env.policy.tenantProfiles[tenantProfile.id];
726
+
727
+ await assert.rejects(
728
+ () =>
729
+ env.atlasRuntime.send({
730
+ recipient: env.lyraId,
731
+ profileId: tenantProfile.id,
732
+ thread: env.channelUri,
733
+ body: "tenant-unbound",
734
+ }),
735
+ (err) => err.code === "UNBOUND_PROFILE_TENANT" || /UNBOUND_PROFILE_TENANT|Unbound tenant profile/.test(err.message)
736
+ );
737
+
738
+ // MUTANT I: scope global without tenantProfiles entry, with local trust stubs
739
+ const globalProfile = createProfile({
740
+ namespace: "global/standard",
741
+ name: "Standard Global Profile",
742
+ scope: "global",
743
+ fixtures: ["global fixture alpha", "global fixture beta"],
744
+ });
745
+ const pins = env.atlasRuntime.store.get("globalProfilePins", {});
746
+ pins[globalProfile.id] = "IAASO-1001";
747
+ env.atlasRuntime.store.set("globalProfilePins", pins);
748
+ env.atlasRuntime.store.set("profiles", [...env.atlasRuntime.profiles(), globalProfile]);
749
+ delete env.policy.tenantProfiles[globalProfile.id];
750
+ // Local stubs preventing external network
751
+ env.atlasRuntime.trust = {
752
+ publishedProfilePin: async () => ({ code: "IAASO-1001", hash: globalProfile.fixtureHash }),
753
+ authorize: async () => ({ kind: "agent", capabilities: ["commons:message"] }),
754
+ };
755
+
756
+ await assert.rejects(
757
+ () =>
758
+ env.atlasRuntime.send({
759
+ recipient: env.lyraId,
760
+ profileId: globalProfile.id,
761
+ thread: env.channelUri,
762
+ body: "global-unbound",
763
+ }),
764
+ (err) => err.code === "UNBOUND_PROFILE_TENANT" || /UNBOUND_PROFILE_TENANT|Unbound tenant profile/.test(err.message)
765
+ );
766
+ });
767
+
768
+ test("Property 4: Scope Separation Pole (R3) - verifyProfile validator vs validateTenantBinding", () => {
769
+ const base = createProfile({
770
+ namespace: "local/rebuilt",
771
+ name: "Rebuilt Local",
772
+ scope: "local",
773
+ fixtures: ["fixture one local", "fixture two local"],
774
+ });
775
+
776
+ // 1. Unrecognized scope rejected at verifyProfile seam with 'Invalid profile scope'
777
+ assert.throws(
778
+ () => verifyProfile({ ...base, scope: "unknown" }),
779
+ /Invalid profile scope/
780
+ );
781
+
782
+ // 2. Local profile with scope: undefined is rebuilt as 'local' by verifyProfile
783
+ const withoutScope = { ...base };
784
+ delete withoutScope.scope;
785
+ const verified = verifyProfile(withoutScope);
786
+ assert.equal(verified.scope, "local");
787
+
788
+ // 3. At validateTenantBinding with unbound tenant profile, fails closed with UNBOUND_PROFILE_TENANT
789
+ assert.throws(
790
+ () => validateTenantBinding("atlas", "lyra", { id: "some-id", scope: "tenant" }, {}),
791
+ (err) => err.code === "UNBOUND_PROFILE_TENANT" || /UNBOUND_PROFILE_TENANT|Unbound tenant profile/.test(err.message)
792
+ );
793
+ });
794
+
795
+ test("Property 4: Inbound unbound tenant scope refusal via receive()", async () => {
796
+ const env = makeEnv();
797
+ const tenantProfile = createProfile({
798
+ namespace: "tenant/org",
799
+ name: "Org Profile",
800
+ scope: "tenant",
801
+ fixtures: ["tenant fixture 1", "tenant fixture 2"],
802
+ });
803
+ env.policy.tenantProfiles[tenantProfile.id] = "tenant-a";
804
+ env.atlasRuntime.addProfile(tenantProfile);
805
+ env.lyraRuntime.addProfile(tenantProfile);
806
+
807
+ const sent = await env.atlasRuntime.send({
808
+ recipient: env.lyraId,
809
+ profileId: tenantProfile.id,
810
+ thread: env.channelUri,
811
+ body: "unbound-inbound",
812
+ deferFlush: true,
813
+ });
814
+
815
+ // Remove profile from receiver tenantProfiles
816
+ delete env.lyraRuntime.policy.tenantProfiles[tenantProfile.id];
817
+
818
+ await assert.rejects(
819
+ () => env.lyraRuntime.receive(sent.envelope),
820
+ (err) => err.code === "UNBOUND_PROFILE_TENANT" || /UNBOUND_PROFILE_TENANT|Unbound tenant profile/.test(err.message)
821
+ );
822
+ assert.equal(env.lyraStore.db.prepare("SELECT COUNT(*) as c FROM processed").get().c, 0);
823
+ });
824
+
825
+ test("Property 4: Durable Flush Rechecks & Retained-Store Recovery (CONTROL 1 & MUTANTS 1-7)", async () => {
826
+ const tempDir = mkdtempSync(join(tmpdir(), "ac-p4-flush-"));
827
+ try {
828
+ const env = makeEnv(tempDir);
829
+ // Queue message with valid matching policy
830
+ const sent = await env.atlasRuntime.send({
831
+ recipient: env.lyraId,
832
+ profileId: env.profile.id,
833
+ thread: env.channelUri,
834
+ body: "durable-flush-msg",
835
+ deferFlush: true,
836
+ });
837
+ const msgId = sent.envelope.id;
838
+
839
+ // CONTROL 1: Retained Store Recovery
840
+ // Close store and runtime
841
+ env.atlasStore.close();
842
+ const recoveredStore = new CommonsStore(join(tempDir, "store.db"));
843
+ const recoveredRuntime = new AgentCommons({
844
+ keychain: env.atlasKey,
845
+ store: recoveredStore,
846
+ policy: env.policy,
847
+ transport: env.transport,
848
+ });
849
+ recoveredRuntime.addProfile(env.profile);
850
+
851
+ const flushControl = await recoveredRuntime.flush();
852
+ assert.equal(flushControl.length, 1);
853
+ assert.equal(flushControl[0].state, "carrier-accepted");
854
+ recoveredStore.close();
855
+
856
+ // Setup for MUTANT 1: Thread-only corruption
857
+ const storeM1 = new CommonsStore(join(tempDir, "store.db"));
858
+ const runtimeM1 = new AgentCommons({ keychain: env.atlasKey, store: storeM1, policy: env.policy, transport: env.transport });
859
+ runtimeM1.addProfile(env.profile);
860
+ const sentM1 = await runtimeM1.send({
861
+ recipient: env.lyraId,
862
+ profileId: env.profile.id,
863
+ thread: env.channelUri,
864
+ body: "m1-msg",
865
+ deferFlush: true,
866
+ });
867
+ // Tamper meta.thread in SQLite kv table
868
+ const metaM1 = storeM1.get(`outbox_meta:${sentM1.envelope.id}`);
869
+ metaM1.thread = "commons";
870
+ storeM1.set(`outbox_meta:${sentM1.envelope.id}`, metaM1);
871
+
872
+ const flushM1 = await runtimeM1.flush();
873
+ assert.equal(flushM1.length, 1);
874
+ assert.equal(flushM1[0].state, "retry-or-quarantine");
875
+ assert.equal(flushM1[0].error, "INVALID_OUTBOX_METADATA");
876
+ storeM1.close();
877
+
878
+ // Setup for MUTANT 2: Sender Revocation after queueing
879
+ const storeM2 = new CommonsStore(join(tempDir, "store.db"));
880
+ const runtimeM2 = new AgentCommons({ keychain: env.atlasKey, store: storeM2, policy: env.policy, transport: env.transport });
881
+ runtimeM2.addProfile(env.profile);
882
+ await runtimeM2.send({ recipient: env.lyraId, profileId: env.profile.id, thread: env.channelUri, body: "m2-msg", deferFlush: true });
883
+ // Revoke sender
884
+ runtimeM2.policy.channels[env.channelUri].members = [env.lyraId];
885
+ const flushM2 = await runtimeM2.flush();
886
+ assert.equal(flushM2.length, 1);
887
+ assert.equal(flushM2[0].state, "retry-or-quarantine");
888
+ assert.match(flushM2[0].error, /UNAUTHORIZED_CHANNEL_SENDER|Sender atlas is unauthorized/);
889
+ runtimeM2.policy.channels[env.channelUri].members = [env.atlasId, env.lyraId]; // restore
890
+ storeM2.close();
891
+
892
+ // Setup for MUTANT 3: Recipient Revocation after queueing
893
+ const storeM3 = new CommonsStore(join(tempDir, "store.db"));
894
+ const runtimeM3 = new AgentCommons({ keychain: env.atlasKey, store: storeM3, policy: env.policy, transport: env.transport });
895
+ runtimeM3.addProfile(env.profile);
896
+ await runtimeM3.send({ recipient: env.lyraId, profileId: env.profile.id, thread: env.channelUri, body: "m3-msg", deferFlush: true });
897
+ // Revoke recipient
898
+ runtimeM3.policy.channels[env.channelUri].members = [env.atlasId];
899
+ const flushM3 = await runtimeM3.flush();
900
+ assert.equal(flushM3.length, 1);
901
+ assert.equal(flushM3[0].state, "retry-or-quarantine");
902
+ assert.match(flushM3[0].error, /UNAUTHORIZED_CHANNEL_RECIPIENT|Receiver lyra is unauthorized/);
903
+ runtimeM3.policy.channels[env.channelUri].members = [env.atlasId, env.lyraId]; // restore
904
+ storeM3.close();
905
+
906
+ // Setup for MUTANT 4: Peer tenant revocation after queueing
907
+ const storeM4 = new CommonsStore(join(tempDir, "store.db"));
908
+ const runtimeM4 = new AgentCommons({ keychain: env.atlasKey, store: storeM4, policy: env.policy, transport: env.transport });
909
+ runtimeM4.addProfile(env.profile);
910
+ await runtimeM4.send({ recipient: env.lyraId, profileId: env.profile.id, thread: env.channelUri, body: "m4-msg", deferFlush: true });
911
+ runtimeM4.policy.agents[env.lyraId].tenantId = "tenant-b";
912
+ const flushM4 = await runtimeM4.flush();
913
+ assert.equal(flushM4.length, 1);
914
+ assert.equal(flushM4[0].state, "retry-or-quarantine");
915
+ assert.match(flushM4[0].error, /TENANT_NAMESPACE_MISMATCH|tenant mismatch/i);
916
+ runtimeM4.policy.agents[env.lyraId].tenantId = "tenant-a"; // restore
917
+ storeM4.close();
918
+
919
+ // Setup for MUTANT 5: Missing admission record
920
+ const storeM5 = new CommonsStore(join(tempDir, "store.db"));
921
+ const runtimeM5 = new AgentCommons({ keychain: env.atlasKey, store: storeM5, policy: env.policy, transport: env.transport });
922
+ runtimeM5.addProfile(env.profile);
923
+ const sentM5 = await runtimeM5.send({ recipient: env.lyraId, profileId: env.profile.id, thread: env.channelUri, body: "m5-msg", deferFlush: true });
924
+ storeM5.db.prepare("DELETE FROM kv WHERE k = ?").run(`outbox_meta:${sentM5.envelope.id}`);
925
+ const flushM5 = await runtimeM5.flush();
926
+ assert.equal(flushM5.length, 1);
927
+ assert.equal(flushM5[0].state, "retry-or-quarantine");
928
+ assert.equal(flushM5[0].error, "INVALID_OUTBOX_METADATA");
929
+ storeM5.close();
930
+
931
+ // Setup for MUTANT 6: Strict toggle after queueing
932
+ const storeM6 = new CommonsStore(join(tempDir, "store.db"));
933
+ const runtimeM6 = new AgentCommons({ keychain: env.atlasKey, store: storeM6, policy: { ...env.policy, channelsEnabled: false }, transport: env.transport });
934
+ runtimeM6.addProfile(env.profile);
935
+ await runtimeM6.send({ recipient: env.lyraId, profileId: env.profile.id, thread: "commons", body: "m6-msg", deferFlush: true });
936
+ // Toggle strict mode
937
+ runtimeM6.policy.channelsEnabled = true;
938
+ const flushM6 = await runtimeM6.flush();
939
+ assert.equal(flushM6.length, 1);
940
+ assert.equal(flushM6[0].state, "retry-or-quarantine");
941
+ assert.equal(flushM6[0].error, "STRUCTURED_CHANNELS_REQUIRED");
942
+ storeM6.close();
943
+
944
+ // Setup for MUTANT 7: Authenticated reserved-malformed flush refusal (R1b / R3)
945
+ const storeM7 = new CommonsStore(join(tempDir, "store.db"));
946
+ const runtimeM7 = new AgentCommons({ keychain: env.atlasKey, store: storeM7, policy: env.policy, transport: env.transport });
947
+ runtimeM7.addProfile(env.profile);
948
+ // Queue valid message first
949
+ const sentM7 = await runtimeM7.send({ recipient: env.lyraId, profileId: env.profile.id, thread: env.channelUri, body: "m7-msg", deferFlush: true });
950
+ // Construct signed tuple matching runtime.mjs:161 exactly:
951
+ const sha = envelopeSha(sentM7.envelope);
952
+ const tuple7 = {
953
+ id: sentM7.envelope.id,
954
+ sha,
955
+ thread: "channel:malformed",
956
+ profileId: env.profile.id,
957
+ sender: env.atlasId,
958
+ recipient: env.lyraId,
959
+ };
960
+ const sig7 = env.atlasKey.sign(Buffer.from(jcs(tuple7))).toString("hex");
961
+ const meta7 = { ...tuple7, signature: sig7 };
962
+ storeM7.set(`outbox_meta:${sentM7.envelope.id}`, meta7);
963
+
964
+ const flushM7 = await runtimeM7.flush();
965
+ assert.equal(flushM7.length, 1);
966
+ assert.equal(flushM7[0].state, "retry-or-quarantine");
967
+ assert.equal(flushM7[0].error, "INVALID_CHANNEL_URI");
968
+
969
+ const rowM7 = storeM7.db.prepare("SELECT * FROM outbox WHERE id = ?").get(sentM7.envelope.id);
970
+ assert.equal(rowM7.attempts, 1);
971
+ assert.equal(rowM7.error, "INVALID_CHANNEL_URI");
972
+ storeM7.close();
973
+ } finally {
974
+ rmSync(tempDir, { recursive: true, force: true });
975
+ }
976
+ });
977
+
978
+ // =========================================================================
979
+ // Property 5: Legacy Thread Compatibility & Reserved-Prefix Priority (§4.2)
980
+ // =========================================================================
981
+ test("Property 5: CONTROL - Legacy thread succeeds under channelsEnabled !== true", async () => {
982
+ const env = makeEnv();
983
+ env.policy.channelsEnabled = false;
984
+
985
+ const res = await env.atlasRuntime.send({
986
+ recipient: env.lyraId,
987
+ profileId: env.profile.id,
988
+ thread: "commons",
989
+ body: "legacy-pass",
990
+ });
991
+ assert.equal(res.result[0].state, "carrier-accepted");
992
+ });
993
+
994
+ test("Property 5: MUTANT A - Legacy thread denied under channelsEnabled === true", async () => {
995
+ const env = makeEnv();
996
+ env.policy.channelsEnabled = true;
997
+
998
+ await assert.rejects(
999
+ () =>
1000
+ env.atlasRuntime.send({
1001
+ recipient: env.lyraId,
1002
+ profileId: env.profile.id,
1003
+ thread: "commons",
1004
+ body: "legacy-fail",
1005
+ }),
1006
+ (err) => err.code === "STRUCTURED_CHANNELS_REQUIRED" || /STRUCTURED_CHANNELS_REQUIRED/.test(err.message)
1007
+ );
1008
+ });
1009
+
1010
+ test("Property 5: MUTANT B - Reserved prefix takes precedence over disabled channels", async () => {
1011
+ const env = makeEnv();
1012
+ env.policy.channelsEnabled = false;
1013
+
1014
+ // Even though channelsEnabled is false, channel:// is classified as structured and enforces channel configuration
1015
+ await assert.rejects(
1016
+ () =>
1017
+ env.atlasRuntime.send({
1018
+ recipient: env.lyraId,
1019
+ profileId: env.profile.id,
1020
+ thread: "channel://local/community/unconfigured",
1021
+ body: "reserved-precedence",
1022
+ }),
1023
+ (err) => err.code === "UNCONFIGURED_CHANNEL" || /UNCONFIGURED_CHANNEL|Channel is unconfigured/.test(err.message)
1024
+ );
1025
+
1026
+ // And malformed channel URI throws INVALID_CHANNEL_URI
1027
+ await assert.rejects(
1028
+ () =>
1029
+ env.atlasRuntime.send({
1030
+ recipient: env.lyraId,
1031
+ profileId: env.profile.id,
1032
+ thread: "channel:malformed",
1033
+ body: "reserved-malformed",
1034
+ }),
1035
+ (err) => err.code === "INVALID_CHANNEL_URI" || /INVALID_CHANNEL_URI/.test(err.message)
1036
+ );
1037
+ });
1038
+
1039
+ // =========================================================================
1040
+ // Property 4: Reviewer Atomicity and Ordering Fault Oracles
1041
+ // =========================================================================
1042
+ test("Property 4: Atomic rollback oracle on send metadata fault", async () => {
1043
+ const a = new Keychain("unused");
1044
+ const b = new Keychain("unused");
1045
+ a._identity = Keychain.generate();
1046
+ b._identity = Keychain.generate();
1047
+ const store = new CommonsStore(":memory:");
1048
+ const policy = {
1049
+ mode: "local",
1050
+ offline: true,
1051
+ agents: Object.fromEntries(
1052
+ [a, b].map((k) => [
1053
+ k._identity.uuaid,
1054
+ { kind: "agent", publicKey: k._identity.publicKeyHex, capabilities: ["commons:message"] },
1055
+ ])
1056
+ ),
1057
+ };
1058
+ const runtime = new AgentCommons({ keychain: a, store, policy });
1059
+ const profile = createProfile({
1060
+ namespace: "local/review",
1061
+ name: "Atomic rollback",
1062
+ scope: "local",
1063
+ fixtures: ["first fixture", "second fixture"],
1064
+ });
1065
+ runtime.addProfile(profile);
1066
+ const set = store.set.bind(store);
1067
+ let faults = 0;
1068
+ store.set = (key, value) => {
1069
+ if (key.startsWith("outbox_meta:")) {
1070
+ faults++;
1071
+ throw Error("AC2_META_WRITE_FAULT");
1072
+ }
1073
+ return set(key, value);
1074
+ };
1075
+ try {
1076
+ await assert.rejects(
1077
+ runtime.send({ recipient: b._identity.uuaid, profileId: profile.id, body: "atomic", deferFlush: true }),
1078
+ /AC2_META_WRITE_FAULT/
1079
+ );
1080
+ const outbox = store.db.prepare("SELECT COUNT(*) AS n FROM outbox").get().n;
1081
+ const admission = store.audit().filter((e) => JSON.parse(e.body).kind === "outbox-admission").length;
1082
+ assert.equal(faults, 1);
1083
+ assert.equal(outbox, 0);
1084
+ assert.equal(admission, 0);
1085
+ } finally {
1086
+ store.close();
1087
+ }
1088
+ });
1089
+
1090
+ test("Property 4: Actual trust-failure ordering oracle on receive path", async () => {
1091
+ const keys = Array.from({ length: 3 }, () => {
1092
+ const k = new Keychain("unused");
1093
+ k._identity = Keychain.generate();
1094
+ return k;
1095
+ });
1096
+ const policy = {
1097
+ mode: "local",
1098
+ offline: true,
1099
+ agents: Object.fromEntries(
1100
+ keys.map((k) => [
1101
+ k._identity.uuaid,
1102
+ { kind: "agent", publicKey: k._identity.publicKeyHex, capabilities: ["commons:message"] },
1103
+ ])
1104
+ ),
1105
+ };
1106
+ const transport = new MemoryLoopbackTransport();
1107
+ const stores = [new CommonsStore(":memory:"), new CommonsStore(":memory:")];
1108
+ const [a, b] = keys.slice(0, 2).map((keychain, i) => new AgentCommons({ keychain, store: stores[i], policy, transport }));
1109
+ const profile = createProfile({
1110
+ namespace: "local/review",
1111
+ name: "Transient order",
1112
+ scope: "local",
1113
+ fixtures: ["first fixture", "second fixture"],
1114
+ });
1115
+ a.addProfile(profile);
1116
+ b.addProfile(profile);
1117
+ try {
1118
+ for (const recipient of [b.uuaid, b.uuaid, keys[2]._identity.uuaid]) {
1119
+ const { envelope } = await a.send({ recipient, profileId: profile.id, body: "trust-order", deferFlush: true });
1120
+ await transport.deliver(envelope);
1121
+ }
1122
+ let calls = 0;
1123
+ b.trust.authorize = async () => {
1124
+ calls++;
1125
+ const e = Error("AC2_TRANSIENT");
1126
+ e.transient = true;
1127
+ e.code = "trust-unavailable";
1128
+ throw e;
1129
+ };
1130
+ const result = await b.poll();
1131
+ const cursor = b.store.get("cursors", {})[transport.sourceId] ?? 0;
1132
+ assert.equal(result.length, 1);
1133
+ assert.equal(calls, 1);
1134
+ assert.equal(cursor, 0);
1135
+ assert.equal(result[0].deferred, true);
1136
+ } finally {
1137
+ await transport.close();
1138
+ for (const s of stores) s.close();
1139
+ }
1140
+ });