@uuaid/agent-commons 0.2.0-alpha.2 → 0.2.0-alpha.4
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/CONFORMANCE.md +1 -1
- package/NOTICE +4 -7
- package/PROVENANCE.json +12 -17
- package/README.md +103 -12
- package/bin/agent-commons.mjs +8 -3
- package/examples/Dockerfile +1 -1
- package/package.json +5 -2
- package/src/carrier.mjs +7 -2
- package/src/channels.mjs +1 -1
- package/src/config.mjs +2 -1
- package/src/continuity.mjs +14 -7
- package/src/governance-scope.mjs +1 -1
- package/src/governance.mjs +4 -2
- package/src/index.d.ts +3 -1
- package/src/pillar.mjs +2 -2
- package/src/profiles.mjs +2 -1
- package/src/relay/auth.mjs +294 -0
- package/src/relay/board.mjs +223 -0
- package/src/relay/claims.mjs +25 -0
- package/src/relay/cli.mjs +49 -0
- package/src/relay/client.mjs +437 -0
- package/src/relay/docs/reconnect-v1.md +27 -0
- package/src/relay/docs/working-rules-v1.md +42 -0
- package/src/relay/enroll.mjs +232 -0
- package/src/relay/export-cli.mjs +47 -0
- package/src/relay/governance.mjs +1328 -0
- package/src/relay/mailbox.mjs +103 -0
- package/src/relay/records.mjs +146 -0
- package/src/relay/render.mjs +38 -0
- package/src/relay/roster.mjs +134 -0
- package/src/relay/routes.mjs +87 -0
- package/src/relay/schema.mjs +1081 -0
- package/src/relay/seed.mjs +89 -0
- package/src/relay/serve.mjs +116 -0
- package/src/relay/server.mjs +633 -0
- package/src/relay/signing.mjs +693 -0
- package/src/relay/test-mode.mjs +73 -0
- package/src/runtime.mjs +25 -12
- package/src/spaces/manifest.mjs +245 -0
- package/src/spaces/pillar-group.mjs +12 -0
- package/src/spaces/policy.mjs +229 -0
- package/src/spaces/store.mjs +325 -0
- package/src/trust.mjs +44 -5
- package/src/uuaid-namespace.d.mts +6 -0
- package/src/uuaid-namespace.mjs +48 -0
- package/src/vendor/pillar/blob/blob.mjs +12 -14
- package/src/vendor/pillar/groups-v2.mjs +1108 -0
- package/src/vendor/pillar/groups.mjs +175 -0
- package/src/vendor/pillar/identity/jcs.mjs +4 -5
- package/src/vendor/pillar/identity/keychain.mjs +51 -2
- package/src/vendor/pillar/net/carrier-client.mjs +29 -7
- package/src/vendor/pillar/net/refusals.mjs +4 -2
- package/test/channels.test.mjs +0 -268
- package/test/cohort.test.mjs +0 -36
- package/test/collaboration.test.mjs +0 -459
- package/test/continuity.test.mjs +0 -229
- package/test/custody.negative.test.mjs +0 -165
- package/test/derivation-parity.test.mjs +0 -19
- package/test/fixtures/derivation-vectors.json +0 -26
- package/test/fixtures/ed25519-small-order.vectors.json +0 -15
- package/test/governance-expected-red.expected-red.mjs +0 -241
- package/test/governance-scope.test.mjs +0 -142
- package/test/governance-t1-t10.test.mjs +0 -477
- package/test/governance.test.mjs +0 -514
- package/test/hold-admission.test.mjs +0 -1185
- package/test/keychain-strict-ed25519.test.mjs +0 -240
- package/test/offline.test.mjs +0 -152
- package/test/runtime-channels.test.mjs +0 -1140
- package/test/runtime.test.mjs +0 -201
- package/test/security-runtime.test.mjs +0 -104
- package/test/strict-ed25519-consumers.test.mjs +0 -234
- package/test/transport.test.mjs +0 -353
package/test/continuity.test.mjs
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
|
-
import { UuaidClient, generateVaultKey } from "@uuaid/sdk";
|
|
8
|
-
import { Keychain } from "../src/pillar.mjs";
|
|
9
|
-
import { CommonsStore } from "../src/store.mjs";
|
|
10
|
-
import { ContinuityMemory, ContinuityError, MemoryContinuityStore, verifyContinuity, CONTINUITY_PROTOCOL, DEFAULT_LIMITS } from "../src/continuity.mjs";
|
|
11
|
-
|
|
12
|
-
function key(){const k=new Keychain("unused");k._identity=Keychain.generate();return k}
|
|
13
|
-
|
|
14
|
-
/** In-memory stand-in for api.uuaid.org's vault routes; records every request so tests can prove what crossed the wire. */
|
|
15
|
-
function fakeRegistry(){
|
|
16
|
-
const items=new Map();const calls=[];
|
|
17
|
-
const fetchImpl=async(url,init={})=>{
|
|
18
|
-
const u=new URL(String(url));calls.push({url:u.pathname,method:init.method,body:init.body??null});
|
|
19
|
-
const m=u.pathname.match(/^\/agents\/([^/]+)\/vault\/(.+)$/);
|
|
20
|
-
if(!m)return new Response(JSON.stringify({error:"unexpected route"}),{status:500});
|
|
21
|
-
const agent=decodeURIComponent(m[1]),slot=m[2].split("/").map(decodeURIComponent).join("/");const id=`${agent}|${slot}`;
|
|
22
|
-
if(init.method==="PUT"){const {envelope}=JSON.parse(init.body);const replaced=items.has(id);const content_hash=createHash("sha256").update(JSON.stringify(envelope)).digest("hex");items.set(id,{agent,key:slot,envelope,content_hash,size_bytes:init.body.length});return new Response(JSON.stringify({agent,key:slot,content_hash,size_bytes:init.body.length,replaced}),{status:200})}
|
|
23
|
-
if(init.method==="GET"){const item=items.get(id);return item?new Response(JSON.stringify(item),{status:200}):new Response(JSON.stringify({error:"not found"}),{status:404})}
|
|
24
|
-
return new Response("",{status:405});
|
|
25
|
-
};
|
|
26
|
-
return {items,calls,client:new UuaidClient({apiKey:"uuaid_test_fixture_notasecret",fetchImpl})};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
test("entries are signed, hash-chained, provenance-tagged, and survive a durable reload",()=>{
|
|
30
|
-
const home=mkdtempSync(join(tmpdir(),"ac-cont-"));
|
|
31
|
-
try{
|
|
32
|
-
const k=key();const store=new CommonsStore(join(home,"commons.db"));
|
|
33
|
-
const m=new ContinuityMemory({keychain:k,store});
|
|
34
|
-
const a=m.remember("note",{text:"harbor"});
|
|
35
|
-
const b=m.remember("commitment",{text:"run rollback fixture first"},{source:"operator",origin:"brief#1",evidence:"a".repeat(64),pin:true});
|
|
36
|
-
assert.equal(a.previous,"genesis");assert.equal(b.previous,a.hash);assert.equal(a.seq,0);assert.equal(b.seq,1);
|
|
37
|
-
assert.equal(a.provenance.actor,k._identity.uuaid);assert.equal(b.provenance.source,"operator");assert.equal(b.pin,true);
|
|
38
|
-
assert.match(a.signature,/^[0-9a-f]{128}$/);assert.equal(m.verify(),true);assert.equal(m.head(),b.hash);
|
|
39
|
-
assert.equal(m.status().durable,true);assert.equal(m.status().remote.configured,false);
|
|
40
|
-
store.close();
|
|
41
|
-
const reopened=new ContinuityMemory({keychain:k,store:new CommonsStore(join(home,"commons.db"))});
|
|
42
|
-
assert.deepEqual(reopened.recall().map(e=>e.hash),[a.hash,b.hash]);assert.equal(reopened.recall({kind:"note"}).length,1);
|
|
43
|
-
assert.equal(reopened.remember("note",{text:"after restart"}).seq,2);
|
|
44
|
-
reopened.store.close();
|
|
45
|
-
}finally{rmSync(home,{recursive:true,force:true})}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("ledger is identity-scoped: two agents in one store never see each other and a foreign blob fails closed",()=>{
|
|
49
|
-
const store=new CommonsStore(":memory:");const k1=key(),k2=key();
|
|
50
|
-
const m1=new ContinuityMemory({keychain:k1,store}),m2=new ContinuityMemory({keychain:k2,store});
|
|
51
|
-
m1.remember("note",{text:"mine"});assert.equal(m2.recall().length,0);
|
|
52
|
-
store.set(ContinuityMemory.storeKey(k2._identity.uuaid),store.get(ContinuityMemory.storeKey(k1._identity.uuaid)));
|
|
53
|
-
assert.throws(()=>new ContinuityMemory({keychain:k2,store}),e=>e instanceof ContinuityError&&e.code==="IDENTITY_MISMATCH");
|
|
54
|
-
store.close();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("tampered content, broken links, and forged signatures are refused on load; quarantine is explicit",()=>{
|
|
58
|
-
const store=new MemoryContinuityStore();const k=key();const m=new ContinuityMemory({keychain:k,store});
|
|
59
|
-
m.remember("note",{text:"one"});m.remember("note",{text:"two"});
|
|
60
|
-
const skey=ContinuityMemory.storeKey(k._identity.uuaid);const good=store.get(skey);
|
|
61
|
-
const tampered=structuredClone(good);tampered.entries[0].content.text="ONE";store.set(skey,tampered);
|
|
62
|
-
assert.throws(()=>new ContinuityMemory({keychain:k,store}),e=>e.code==="INTEGRITY");
|
|
63
|
-
const relinked=structuredClone(good);relinked.entries[1].previous="genesis";store.set(skey,relinked);
|
|
64
|
-
assert.throws(()=>new ContinuityMemory({keychain:k,store}),e=>e.code==="INTEGRITY");
|
|
65
|
-
const forged=structuredClone(good);const other=key();forged.entries[1].signature=other.sign(Buffer.from(forged.entries[1].hash,"hex")).toString("hex");store.set(skey,forged);
|
|
66
|
-
assert.throws(()=>new ContinuityMemory({keychain:k,store}),e=>e.code==="INTEGRITY");
|
|
67
|
-
const dropped=structuredClone(good);dropped.entries.pop();store.set(skey,dropped);// truncation without updating nextSeq is still a valid prefix; verify it loads but head differs
|
|
68
|
-
assert.notEqual(new ContinuityMemory({keychain:k,store}).head(),good.entries[1].hash);
|
|
69
|
-
store.set(skey,tampered);
|
|
70
|
-
const q=ContinuityMemory.quarantine(store,k._identity.uuaid);assert.match(q,/:quarantine:/);
|
|
71
|
-
assert.equal(new ContinuityMemory({keychain:k,store}).recall().length,0);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("bounds and retention evict oldest unpinned entries while the chain stays verifiable; budgets fail closed",()=>{
|
|
75
|
-
let t=Date.UTC(2026,0,1);const now=()=>t;const k=key();
|
|
76
|
-
const m=new ContinuityMemory({keychain:k,limits:{maxEntries:3,retentionMs:1000},now});
|
|
77
|
-
const e=[];for(let i=0;i<5;i++){t+=10;e.push(m.remember("note",{i}))}
|
|
78
|
-
assert.equal(m.recall().length,3);assert.equal(m.status().pruned,2);assert.equal(m.status().anchor.previous,e[1].hash);assert.equal(m.recall()[0].previous,e[1].hash);assert.equal(m.verify(),true);
|
|
79
|
-
t+=5000;const late=m.remember("note",{late:true});assert.deepEqual(m.recall().map(x=>x.hash),[late.hash]);assert.equal(m.status().pruned,5);assert.equal(m.status().anchor.previous,e[4].hash);assert.equal(m.verify(),true);
|
|
80
|
-
assert.throws(()=>m.remember("note",{big:"x".repeat(DEFAULT_LIMITS.maxEntryBytes)}),e=>e.code==="ENTRY_TOO_LARGE");
|
|
81
|
-
const pinned=new ContinuityMemory({keychain:k,limits:{maxEntries:2},now});
|
|
82
|
-
pinned.remember("note",{p:1},{pin:true});pinned.remember("note",{p:2},{pin:true});
|
|
83
|
-
assert.throws(()=>pinned.remember("note",{p:3}),e=>e.code==="BUDGET_EXHAUSTED");assert.equal(pinned.recall().length,2);assert.equal(pinned.verify(),true);
|
|
84
|
-
const tiny=new ContinuityMemory({keychain:k,limits:{maxBytes:900},now});tiny.remember("note",{a:1});tiny.remember("note",{b:2});assert.equal(tiny.recall().length,1);assert.equal(tiny.verify(),true);
|
|
85
|
-
assert.throws(()=>m.remember("Note",{}),e=>e.code==="ENTRY_INVALID");
|
|
86
|
-
assert.throws(()=>m.remember("note",{},{source:"peer"}),e=>e.code==="ENTRY_INVALID");
|
|
87
|
-
assert.throws(()=>m.remember("note",{},{source:"oracle"}),e=>e.code==="ENTRY_INVALID");
|
|
88
|
-
assert.throws(()=>m.remember("note",{n:1.5}),e=>e.code==="ENTRY_INVALID");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("explicit push/pull through the official SDK client: ciphertext only, slot bound, no registration",async()=>{
|
|
92
|
-
const reg=fakeRegistry();const k=key();const vaultKey=generateVaultKey();
|
|
93
|
-
const m=new ContinuityMemory({keychain:k,client:reg.client,vaultKey});
|
|
94
|
-
m.remember("note",{text:"I remember the harbor."});
|
|
95
|
-
assert.equal(reg.calls.length,0,"nothing leaves the process until push() is called");
|
|
96
|
-
const pushed=await m.push();
|
|
97
|
-
assert.equal(pushed.relation,"pushed");assert.equal(pushed.head,m.head());assert.match(pushed.contentHash,/^[0-9a-f]{64}$/);
|
|
98
|
-
assert.equal(reg.calls.length,1);assert.equal(reg.calls[0].method,"PUT");
|
|
99
|
-
assert.equal(reg.calls[0].url,`/agents/${encodeURIComponent(k._identity.uuaid)}/vault/continuity/chain`);
|
|
100
|
-
assert.ok(!reg.calls[0].body.includes("harbor"));assert.ok(!reg.calls[0].body.includes(vaultKey));
|
|
101
|
-
assert.equal(JSON.parse(reg.calls[0].body).envelope.aad,`${k._identity.uuaid}/continuity/chain`);
|
|
102
|
-
const fresh=new ContinuityMemory({keychain:k,client:reg.client,vaultKey});
|
|
103
|
-
const pulled=await fresh.pull();assert.equal(pulled.relation,"fast-forwarded");assert.equal(fresh.head(),m.head());assert.equal(fresh.recall()[0].content.text,"I remember the harbor.");
|
|
104
|
-
assert.equal((await fresh.pull()).relation,"in-sync");
|
|
105
|
-
fresh.remember("note",{text:"second session"});
|
|
106
|
-
const {pull,push}=await fresh.sync();assert.equal(pull.relation,"local-ahead");assert.equal(push.relation,"pushed");assert.equal(push.replaced,true);
|
|
107
|
-
assert.equal((await m.pull()).relation,"fast-forwarded");assert.equal(m.head(),fresh.head());
|
|
108
|
-
assert.ok(reg.calls.every(c=>!(c.method==="POST")),"no registerAgent/signup/POST ever issued");
|
|
109
|
-
assert.ok(!JSON.stringify(m.status()).includes(vaultKey));assert.ok(!JSON.stringify(m.snapshot()).includes(vaultKey));
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("remote-empty, divergence, wrong-identity, and wrong-key remote states fail closed",async()=>{
|
|
113
|
-
const reg=fakeRegistry();const k=key();const vaultKey=generateVaultKey();
|
|
114
|
-
const a=new ContinuityMemory({keychain:k,client:reg.client,vaultKey});
|
|
115
|
-
assert.equal((await a.pull()).relation,"remote-empty");
|
|
116
|
-
a.remember("note",{side:"a"});await a.push();
|
|
117
|
-
const b=new ContinuityMemory({keychain:k,client:reg.client,vaultKey});b.remember("note",{side:"b"});
|
|
118
|
-
await assert.rejects(b.pull(),e=>e.code==="DIVERGED");assert.equal(b.recall()[0].content.side,"b");
|
|
119
|
-
await assert.rejects(b.sync(),e=>e.code==="DIVERGED");
|
|
120
|
-
const wrongKey=new ContinuityMemory({keychain:k,client:reg.client,vaultKey:generateVaultKey()});
|
|
121
|
-
await assert.rejects(wrongKey.pull(),e=>e.code==="REMOTE_UNAVAILABLE");
|
|
122
|
-
const other=key();const impostor=new ContinuityMemory({keychain:other,client:reg.client,vaultKey});
|
|
123
|
-
assert.equal((await impostor.pull()).relation,"remote-empty","a different UUAID addresses a different vault");
|
|
124
|
-
const forwarded={saveMemory:reg.client.saveMemory.bind(reg.client),loadMemory:async()=>JSON.stringify(a.snapshot())};
|
|
125
|
-
await assert.rejects(new ContinuityMemory({keychain:other,client:forwarded,vaultKey}).pull(),e=>e.code==="IDENTITY_MISMATCH");
|
|
126
|
-
const relabelled={saveMemory:async(agent,slot)=>({agent,key:"somewhere/else"}),loadMemory:async()=>""};
|
|
127
|
-
await assert.rejects(new ContinuityMemory({keychain:k,client:relabelled,vaultKey}).push(),e=>e.code==="REMOTE_MISMATCH");
|
|
128
|
-
const corrupt={saveMemory:async()=>({}),loadMemory:async()=>"{not json"};
|
|
129
|
-
await assert.rejects(new ContinuityMemory({keychain:k,client:corrupt,vaultKey}).pull(),e=>e.code==="INTEGRITY");
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test("constructor refuses missing identity, non-official clients, bad vault keys, bad slots; remote ops refuse when unconfigured",async()=>{
|
|
133
|
-
const k=key();
|
|
134
|
-
assert.throws(()=>new ContinuityMemory({}),e=>e.code==="IDENTITY_REQUIRED");
|
|
135
|
-
const unbound=key();unbound._identity={...unbound._identity,uuaid:"uuaid:foundation:agent:00000000-0000-4000-8000-000000000000"};
|
|
136
|
-
assert.throws(()=>new ContinuityMemory({keychain:unbound}),e=>e.code==="IDENTITY_REQUIRED");
|
|
137
|
-
assert.throws(()=>new ContinuityMemory({keychain:k,client:{registerAgent(){}}}),e=>e.code==="CLIENT_INVALID");
|
|
138
|
-
assert.throws(()=>new ContinuityMemory({keychain:k,vaultKey:"hunter2"}),e=>e.code==="VAULT_KEY_INVALID");
|
|
139
|
-
assert.throws(()=>new ContinuityMemory({keychain:k,slot:"../etc"}),e=>e.code==="SLOT_INVALID");
|
|
140
|
-
assert.throws(()=>new ContinuityMemory({keychain:k,slot:"a//b"}),e=>e.code==="SLOT_INVALID");
|
|
141
|
-
assert.throws(()=>new ContinuityMemory({keychain:k,limits:{maxEntries:0}}),e=>e.code==="BUDGET_EXHAUSTED");
|
|
142
|
-
const local=new ContinuityMemory({keychain:k});
|
|
143
|
-
await assert.rejects(local.push(),e=>e.code==="REMOTE_NOT_CONFIGURED");await assert.rejects(local.pull(),e=>e.code==="REMOTE_NOT_CONFIGURED");
|
|
144
|
-
const halfConfigured=new ContinuityMemory({keychain:k,client:fakeRegistry().client});
|
|
145
|
-
await assert.rejects(halfConfigured.sync(),e=>e.code==="REMOTE_NOT_CONFIGURED");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("injected minimal store wrapper (get/set only, no transaction) works and snapshots verify standalone",()=>{
|
|
149
|
-
const map=new Map();const wrapper={get:(k,f=null)=>map.has(k)?JSON.parse(map.get(k)):f,set:(k,v)=>map.set(k,JSON.stringify(v))};
|
|
150
|
-
const k=key();const m=new ContinuityMemory({keychain:k,store:wrapper});m.remember("note",{w:1});
|
|
151
|
-
assert.equal(m.status().durable,true);assert.equal([...map.keys()][0],`continuity:${k._identity.uuaid}`);
|
|
152
|
-
const snap=m.snapshot();assert.equal(snap.v,CONTINUITY_PROTOCOL);assert.equal(snap.head,m.head());
|
|
153
|
-
assert.equal(verifyContinuity(snap,{uuaid:k._identity.uuaid,publicKey:k._identity.publicKeyHex}),snap);
|
|
154
|
-
assert.throws(()=>verifyContinuity({...snap,head:"0".repeat(64)},{uuaid:k._identity.uuaid,publicKey:k._identity.publicKeyHex}),e=>e.code==="INTEGRITY");
|
|
155
|
-
const again=new ContinuityMemory({keychain:k,store:wrapper});assert.equal(again.head(),m.head());
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("two instances on one store for one identity: stale writer is refused, nothing lost, reload() required",()=>{
|
|
159
|
-
const store=new CommonsStore(":memory:");const k=key();
|
|
160
|
-
const a=new ContinuityMemory({keychain:k,store}),b=new ContinuityMemory({keychain:k,store});
|
|
161
|
-
const fromA=a.remember("note",{by:"a"});
|
|
162
|
-
assert.throws(()=>b.remember("note",{by:"b"}),e=>e.code==="STALE_WRITER"&&e.details.storedHead===fromA.hash);
|
|
163
|
-
assert.equal(b.recall().length,0,"stale instance keeps its cached view; nothing merged");
|
|
164
|
-
assert.equal(store.get(ContinuityMemory.storeKey(k._identity.uuaid)).entries.length,1,"store still holds A's write");
|
|
165
|
-
assert.deepEqual(b.reload(),{head:fromA.hash,entries:1});
|
|
166
|
-
const fromB=b.remember("note",{by:"b"});assert.equal(fromB.previous,fromA.hash);assert.equal(fromB.seq,1);
|
|
167
|
-
assert.throws(()=>a.remember("note",{by:"a2"}),e=>e.code==="STALE_WRITER");
|
|
168
|
-
assert.equal(a.recall().length,1);a.reload();assert.deepEqual(a.recall().map(e=>e.hash),[fromA.hash,fromB.hash]);
|
|
169
|
-
assert.equal(a.remember("note",{by:"a2"}).seq,2);assert.equal(b.verify(),true);assert.equal(a.verify(),true);
|
|
170
|
-
assert.throws(()=>b.compact(),e=>e.code==="STALE_WRITER");
|
|
171
|
-
store.close();
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test("pull/push race: a sibling write during the remote await is detected after the await, never overwritten",async()=>{
|
|
175
|
-
const store=new CommonsStore(":memory:");const k=key();const vaultKey=generateVaultKey();
|
|
176
|
-
const reg=fakeRegistry();
|
|
177
|
-
const seed=new ContinuityMemory({keychain:k,store:new MemoryContinuityStore(),client:reg.client,vaultKey});seed.remember("note",{remote:1});await seed.push();
|
|
178
|
-
const a=new ContinuityMemory({keychain:k,store,client:reg.client,vaultKey}),b=new ContinuityMemory({keychain:k,store,client:reg.client,vaultKey});
|
|
179
|
-
// sibling B writes while A's loadMemory is in flight
|
|
180
|
-
const racing={saveMemory:reg.client.saveMemory.bind(reg.client),loadMemory:async(...args)=>{b.remember("note",{local:"b"});return reg.client.loadMemory(...args)}};
|
|
181
|
-
const a2=new ContinuityMemory({keychain:k,store,client:racing,vaultKey});
|
|
182
|
-
await assert.rejects(a2.pull(),e=>e.code==="STALE_WRITER");
|
|
183
|
-
assert.equal(store.get(ContinuityMemory.storeKey(k._identity.uuaid)).entries[0].content.local,"b","B's write survived A's pull");
|
|
184
|
-
assert.equal(a2.recall().length,0);
|
|
185
|
-
// A (stale cache) must not push either
|
|
186
|
-
await assert.rejects(a.push(),e=>e.code==="STALE_WRITER");
|
|
187
|
-
// after reload, local (b) and remote (seed) have diverged: refuse, do not merge
|
|
188
|
-
a.reload();await assert.rejects(a.pull(),e=>e.code==="DIVERGED");
|
|
189
|
-
// push race: sibling writes between saveMemory and receipt commit → STALE_WRITER surfaced, not recorded as success
|
|
190
|
-
const c=new ContinuityMemory({keychain:k,store:new MemoryContinuityStore(),client:reg.client,vaultKey});await c.pull();
|
|
191
|
-
const cStore=c.store;const sibling=new ContinuityMemory({keychain:k,store:cStore,client:reg.client,vaultKey});
|
|
192
|
-
const racingSave={loadMemory:reg.client.loadMemory.bind(reg.client),saveMemory:async(...args)=>{sibling.remember("note",{slipped:true});return reg.client.saveMemory(...args)}};
|
|
193
|
-
const c2=new ContinuityMemory({keychain:k,store:cStore,client:racingSave,vaultKey});c2.reload();
|
|
194
|
-
await assert.rejects(c2.push(),e=>e.code==="STALE_WRITER");assert.equal(c2.status().remote.lastPush,null);
|
|
195
|
-
store.close();
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("remote snapshots with valid signatures that exceed this instance's limits are rejected whole, never pruned",async()=>{
|
|
199
|
-
const k=key();const vaultKey=generateVaultKey();let t=Date.UTC(2026,0,1);const now=()=>t;
|
|
200
|
-
const reg=fakeRegistry();
|
|
201
|
-
const writer=new ContinuityMemory({keychain:k,client:reg.client,vaultKey,limits:{maxEntries:10},now});
|
|
202
|
-
t+=10;writer.remember("note",{n:1});t+=10;writer.remember("note",{n:2});await writer.push();
|
|
203
|
-
const tight=new ContinuityMemory({keychain:k,client:reg.client,vaultKey,limits:{maxEntries:1},now});
|
|
204
|
-
await assert.rejects(tight.pull(),e=>e.code==="LIMIT_EXCEEDED"&&e.details.entries===2&&e.details.limit===1);
|
|
205
|
-
assert.equal(tight.recall().length,0);assert.equal(tight.status().remote.lastPull,null);
|
|
206
|
-
await assert.rejects(tight.sync(),e=>e.code==="LIMIT_EXCEEDED");
|
|
207
|
-
const smallEntry=new ContinuityMemory({keychain:k,client:reg.client,vaultKey,limits:{maxEntryBytes:300},now});
|
|
208
|
-
await assert.rejects(smallEntry.pull(),e=>e.code==="LIMIT_EXCEEDED"&&/maxEntryBytes/.test(e.message));
|
|
209
|
-
const smallTotal=new ContinuityMemory({keychain:k,client:reg.client,vaultKey,limits:{maxBytes:700},now});
|
|
210
|
-
await assert.rejects(smallTotal.pull(),e=>e.code==="LIMIT_EXCEEDED"&&/maxBytes/.test(e.message));
|
|
211
|
-
const shortRetention=new ContinuityMemory({keychain:k,client:reg.client,vaultKey,limits:{retentionMs:5},now});
|
|
212
|
-
await assert.rejects(shortRetention.pull(),e=>e.code==="LIMIT_EXCEEDED"&&/retentionMs/.test(e.message));
|
|
213
|
-
// identical limits: accepted
|
|
214
|
-
const peer=new ContinuityMemory({keychain:k,client:reg.client,vaultKey,limits:{maxEntries:10},now});assert.equal((await peer.pull()).relation,"fast-forwarded");
|
|
215
|
-
// owner-signed snapshot whose anchor bookkeeping was edited (signatures still valid) is rejected
|
|
216
|
-
const owner={uuaid:k._identity.uuaid,publicKey:k._identity.publicKeyHex};const snap=writer.snapshot();
|
|
217
|
-
assert.throws(()=>verifyContinuity({...snap,anchor:{...snap.anchor,pruned:3}},owner),e=>e.code==="INTEGRITY");
|
|
218
|
-
assert.throws(()=>verifyContinuity({...snap,anchor:{previous:"f".repeat(64),pruned:1,through:null}},owner),e=>e.code==="INTEGRITY");
|
|
219
|
-
assert.throws(()=>verifyContinuity({...snap,anchor:{...snap.anchor,through:"2030-01-01T00:00:00.000Z"}},owner),e=>e.code==="INTEGRITY");
|
|
220
|
-
// a future-dated but validly signed entry is refused by the envelope
|
|
221
|
-
const future=new ContinuityMemory({keychain:k,client:reg.client,vaultKey,limits:{maxEntries:10},now:()=>Date.UTC(2020,0,1)});
|
|
222
|
-
await assert.rejects(future.pull(),e=>e.code==="LIMIT_EXCEEDED"&&/future/.test(e.message));
|
|
223
|
-
// expired local entries are reported, block push until compact(), and are never shipped
|
|
224
|
-
t+=DEFAULT_LIMITS.retentionMs+1;
|
|
225
|
-
assert.equal(writer.status().expired,2);assert.equal(writer.verify(),true);
|
|
226
|
-
await assert.rejects(writer.push(),e=>e.code==="LIMIT_EXCEEDED");
|
|
227
|
-
assert.equal(writer.compact(),2);assert.equal(writer.status().pruned,2);assert.equal(writer.verify(),true);
|
|
228
|
-
assert.equal((await writer.push()).relation,"pushed");
|
|
229
|
-
});
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { cpSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join, relative } from "node:path";
|
|
6
|
-
import { pathToFileURL } from "node:url";
|
|
7
|
-
import { generateVaultKey } from "@uuaid/sdk";
|
|
8
|
-
import { initialize, loadRuntime } from "../src/config.mjs";
|
|
9
|
-
|
|
10
|
-
const PREFIX = Buffer.from("uvk_");
|
|
11
|
-
// Representations this scanner checks, and only these: the 4-byte uvk_ prefix,
|
|
12
|
-
// the full vault-key string, the 64-byte ASCII hex suffix, and the decoded
|
|
13
|
-
// 32-byte key. Base64, split fragments, and other encodings are not claimed.
|
|
14
|
-
|
|
15
|
-
function filesUnder(dir) {
|
|
16
|
-
const out = [];
|
|
17
|
-
for (const name of readdirSync(dir)) {
|
|
18
|
-
const path = join(dir, name);
|
|
19
|
-
const stat = statSync(path);
|
|
20
|
-
if (stat.isDirectory()) out.push(...filesUnder(path));
|
|
21
|
-
else if (stat.isFile()) out.push(path);
|
|
22
|
-
}
|
|
23
|
-
return out;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function containsPrefix(dir) {
|
|
27
|
-
return filesUnder(dir).some((path) => readFileSync(path).includes(PREFIX));
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function scan(home, vaultKey) {
|
|
31
|
-
const full = Buffer.from(vaultKey);
|
|
32
|
-
const material = Buffer.from(vaultKey.slice(PREFIX.length));
|
|
33
|
-
const raw = Buffer.from(vaultKey.slice(PREFIX.length), "hex");
|
|
34
|
-
assert.equal(material.length, 64);
|
|
35
|
-
assert.equal(raw.length, 32);
|
|
36
|
-
const files = filesUnder(home);
|
|
37
|
-
const rels = files.map((path) => relative(home, path));
|
|
38
|
-
for (const name of ["commons.db", "config.json", "local-secret", "identity.json"]) {
|
|
39
|
-
assert.ok(rels.includes(name), `missing ${name}`);
|
|
40
|
-
}
|
|
41
|
-
for (const path of files) {
|
|
42
|
-
const rel = relative(home, path);
|
|
43
|
-
const buf = readFileSync(path);
|
|
44
|
-
assert.equal(buf.includes(PREFIX), false, `uvk_ persisted in ${rel}`);
|
|
45
|
-
assert.equal(buf.includes(full), false, `vault key persisted in ${rel}`);
|
|
46
|
-
assert.equal(buf.includes(material), false, `vault-key material persisted in ${rel}`);
|
|
47
|
-
assert.equal(buf.includes(raw), false, `vault-key raw material persisted in ${rel}`);
|
|
48
|
-
}
|
|
49
|
-
return rels;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function changedFiles(left, right) {
|
|
53
|
-
const names = filesUnder(left).map((path) => relative(left, path)).sort();
|
|
54
|
-
const rightNames = filesUnder(right).map((path) => relative(right, path)).sort();
|
|
55
|
-
assert.deepEqual(rightNames, names);
|
|
56
|
-
return names.filter((name) => !readFileSync(join(left, name)).equals(readFileSync(join(right, name))));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
test("scratch-home continuity session does not persist the vault key", async () => {
|
|
60
|
-
let home = null;
|
|
61
|
-
let store = null;
|
|
62
|
-
try {
|
|
63
|
-
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
64
|
-
home = mkdtempSync(join(tmpdir(), "l7-custody-"));
|
|
65
|
-
initialize({ home, target: "local", name: "custody fixture" });
|
|
66
|
-
if (!containsPrefix(home)) break;
|
|
67
|
-
rmSync(home, { recursive: true, force: true });
|
|
68
|
-
home = null;
|
|
69
|
-
}
|
|
70
|
-
assert.ok(home, "fixture home contained uvk_ before the continuity session");
|
|
71
|
-
const loaded = loadRuntime(home);
|
|
72
|
-
store = loaded.store;
|
|
73
|
-
const vaultKey = generateVaultKey();
|
|
74
|
-
assert.match(vaultKey, /^uvk_[0-9a-f]{64}$/);
|
|
75
|
-
let saved = null;
|
|
76
|
-
let used = 0;
|
|
77
|
-
const client = {
|
|
78
|
-
async saveMemory(agent, key, text, presented) {
|
|
79
|
-
assert.equal(presented, vaultKey);
|
|
80
|
-
used += 1;
|
|
81
|
-
saved = text;
|
|
82
|
-
return { agent, key, content_hash: "ab".repeat(32), size_bytes: Buffer.byteLength(text), replaced: false };
|
|
83
|
-
},
|
|
84
|
-
async loadMemory(_agent, _key, presented) {
|
|
85
|
-
assert.equal(presented, vaultKey);
|
|
86
|
-
used += 1;
|
|
87
|
-
return saved;
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
const href = process.env.L7_CONTINUITY_MODULE
|
|
91
|
-
? pathToFileURL(process.env.L7_CONTINUITY_MODULE).href
|
|
92
|
-
: new URL("../src/continuity.mjs", import.meta.url).href;
|
|
93
|
-
const { ContinuityMemory } = await import(href);
|
|
94
|
-
const memory = new ContinuityMemory({ keychain: loaded.keychain, store, client, vaultKey });
|
|
95
|
-
memory.remember("note", { text: "custody fixture" });
|
|
96
|
-
const pushed = await memory.push();
|
|
97
|
-
const pulled = await memory.pull();
|
|
98
|
-
assert.equal(pushed.relation, "pushed");
|
|
99
|
-
assert.equal(pulled.relation, "in-sync");
|
|
100
|
-
assert.equal(used, 2);
|
|
101
|
-
assert.equal(memory.status().durable, true);
|
|
102
|
-
assert.equal(JSON.stringify(memory.status()).includes("uvk_"), false);
|
|
103
|
-
assert.equal(JSON.stringify(memory.snapshot()).includes("uvk_"), false);
|
|
104
|
-
const openRels = scan(home, vaultKey);
|
|
105
|
-
assert.ok(openRels.includes("commons.db-wal"), "live commons.db-wal missing from the open custody manifest");
|
|
106
|
-
assert.ok(openRels.includes("commons.db-shm"), "live commons.db-shm missing from the open custody manifest");
|
|
107
|
-
store.close();
|
|
108
|
-
store = null;
|
|
109
|
-
const closedRels = scan(home, vaultKey);
|
|
110
|
-
assert.ok(closedRels.includes("commons.db"));
|
|
111
|
-
assert.ok(closedRels.length >= 4);
|
|
112
|
-
} finally {
|
|
113
|
-
store?.close();
|
|
114
|
-
if (home) rmSync(home, { recursive: true, force: true });
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("scratch-copy custody scanner sees a one-property sidecar leak", () => {
|
|
119
|
-
const leak = process.env.L7_SCRATCH_LEAK ?? "";
|
|
120
|
-
assert.ok(["", "wal", "shm", "raw"].includes(leak), "unknown scratch leak");
|
|
121
|
-
const vaultKey = generateVaultKey();
|
|
122
|
-
assert.match(vaultKey, /^uvk_[0-9a-f]{64}$/);
|
|
123
|
-
const ascii = Buffer.from(vaultKey.slice(PREFIX.length));
|
|
124
|
-
const raw = Buffer.from(vaultKey.slice(PREFIX.length), "hex");
|
|
125
|
-
const benign = Buffer.alloc(ascii.length, 0x61);
|
|
126
|
-
const zeros = Buffer.alloc(raw.length, 0);
|
|
127
|
-
assert.equal(benign.equals(ascii), false, "benign journal collided with the hex suffix");
|
|
128
|
-
assert.equal(zeros.equals(raw), false, "zero raw file collided with the decoded key");
|
|
129
|
-
assert.equal(raw.includes(PREFIX), false, "decoded key collided with the prefix");
|
|
130
|
-
const parent = mkdtempSync(join(tmpdir(), "l7-scratch-"));
|
|
131
|
-
try {
|
|
132
|
-
const control = join(parent, "control");
|
|
133
|
-
const copy = join(parent, "copy");
|
|
134
|
-
mkdirSync(control);
|
|
135
|
-
writeFileSync(join(control, "commons.db"), Buffer.from("not-a-database"));
|
|
136
|
-
writeFileSync(join(control, "config.json"), Buffer.from("{}\n"));
|
|
137
|
-
writeFileSync(join(control, "local-secret"), Buffer.from("scratch-secret"));
|
|
138
|
-
writeFileSync(join(control, "identity.json"), Buffer.from("{}\n"));
|
|
139
|
-
writeFileSync(join(control, "commons.db-wal"), benign);
|
|
140
|
-
writeFileSync(join(control, "commons.db-shm"), benign);
|
|
141
|
-
writeFileSync(join(control, "raw.key"), zeros);
|
|
142
|
-
cpSync(control, copy, { recursive: true });
|
|
143
|
-
const target = { wal: "commons.db-wal", shm: "commons.db-shm", raw: "raw.key" }[leak];
|
|
144
|
-
if (target) {
|
|
145
|
-
const next = leak === "raw" ? raw : ascii;
|
|
146
|
-
assert.equal(next.length, readFileSync(join(copy, target)).length);
|
|
147
|
-
writeFileSync(join(copy, target), next);
|
|
148
|
-
assert.deepEqual(changedFiles(control, copy), [target]);
|
|
149
|
-
} else {
|
|
150
|
-
assert.deepEqual(changedFiles(control, copy), []);
|
|
151
|
-
}
|
|
152
|
-
const rels = scan(copy, vaultKey);
|
|
153
|
-
assert.deepEqual(rels.slice().sort(), [
|
|
154
|
-
"commons.db",
|
|
155
|
-
"commons.db-shm",
|
|
156
|
-
"commons.db-wal",
|
|
157
|
-
"config.json",
|
|
158
|
-
"identity.json",
|
|
159
|
-
"local-secret",
|
|
160
|
-
"raw.key",
|
|
161
|
-
]);
|
|
162
|
-
} finally {
|
|
163
|
-
rmSync(parent, { recursive: true, force: true });
|
|
164
|
-
}
|
|
165
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
|
|
6
|
-
const vectors = JSON.parse(readFileSync(new URL("./fixtures/derivation-vectors.json", import.meta.url), "utf8")).vectors;
|
|
7
|
-
const keychainHref = process.env.L7_KEYCHAIN_MODULE
|
|
8
|
-
? pathToFileURL(process.env.L7_KEYCHAIN_MODULE).href
|
|
9
|
-
: new URL("../src/vendor/pillar/identity/keychain.mjs", import.meta.url).href;
|
|
10
|
-
const { localIdFromKey } = await import(keychainHref);
|
|
11
|
-
|
|
12
|
-
test("recorded vectors match vendored localIdFromKey", () => {
|
|
13
|
-
assert.equal(vectors.length, 3);
|
|
14
|
-
for (const vector of vectors) {
|
|
15
|
-
assert.equal(vector.uuaid, `uuaid:foundation:agent:${vector.localId}`);
|
|
16
|
-
const got = localIdFromKey(Buffer.from(vector.rawPublicHex, "hex"));
|
|
17
|
-
assert.equal(got, vector.localId, `vendored derivation mismatch for ${vector.id}`);
|
|
18
|
-
}
|
|
19
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"note": "Pillar transport identity (key-derived, unregistered). Published fixtures, not operational keys. Oracle is sha256(raw 32-byte Ed25519 public key), first 16 bytes, lowercase hex, laid out 8-4-4-4-12. The expected localId is a literal: the test must not recompute it with either production function. Seeds are the 32-byte Ed25519 private seeds. rawPublicHex is the SPKI DER tail.",
|
|
3
|
-
"vectors": [
|
|
4
|
-
{
|
|
5
|
-
"id": "l7-seed-zero",
|
|
6
|
-
"seedHex": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
7
|
-
"rawPublicHex": "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29",
|
|
8
|
-
"localId": "139e3940-e64b-5491-7220-88d9a0d74162",
|
|
9
|
-
"uuaid": "uuaid:foundation:agent:139e3940-e64b-5491-7220-88d9a0d74162"
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
"id": "l7-seed-ff",
|
|
13
|
-
"seedHex": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
|
14
|
-
"rawPublicHex": "76a1592044a6e4f511265bca73a604d90b0529d1df602be30a19a9257660d1f5",
|
|
15
|
-
"localId": "af822958-f2d7-5afb-91f8-a8f4da253230",
|
|
16
|
-
"uuaid": "uuaid:foundation:agent:af822958-f2d7-5afb-91f8-a8f4da253230"
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"id": "l7-seed-seq",
|
|
20
|
-
"seedHex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
|
|
21
|
-
"rawPublicHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664",
|
|
22
|
-
"localId": "65b60673-d6ed-884b-f01c-2c222d82ada0",
|
|
23
|
-
"uuaid": "uuaid:foundation:agent:65b60673-d6ed-884b-f01c-2c222d82ada0"
|
|
24
|
-
}
|
|
25
|
-
]
|
|
26
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"source": "the shared pillar-group/2 vectors, packages/pillar-client/test/vectors/pillar-group-v2.vectors.json in the uuaid monorepo",
|
|
3
|
-
"sourceSha256": "10c4f3bc58e28ec91f848aaaf35f359c87a4cf7f18eeefa89e0f8a2e97165732",
|
|
4
|
-
"note": "Two of the RFC 8032 strictness vectors, reduced to what pillar's Keychain checks. 1s: a manifest signed by the identity-point key with R = identity and S = 0; OpenSSL's verify accepts it for any message. 3s2: a member row whose key is the identity point.",
|
|
5
|
-
"1s-small-order-signer": {
|
|
6
|
-
"publicKey": "0100000000000000000000000000000000000000000000000000000000000000",
|
|
7
|
-
"keyId": "uuaid:foundation:agent:01d0fabd-251f-cbbe-2b93-b4b927b26ad2",
|
|
8
|
-
"signature": "01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
|
9
|
-
"signingPreimageHex": "7b2265706f6368223a302c22657874223a7b7d2c226964223a226772702d6266313563616638613839326662336638646665353636303365343064336437222c226973737565644174223a22323032362d31302d30365431333a30303a34342e3030305a222c226d656d62657273223a5b7b226b6579223a2230313030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030222c22726f6c65223a2261646d696e222c22737461747573223a22616374697665222c227575616964223a2275756169643a666f756e646174696f6e3a6167656e743a30316430666162642d323531662d636262652d326239332d623462393237623236616432227d2c7b226b6579223a2238313339373730656138376431373566353661333534363663333463376563636362386438613931623465653337613235646636306635623866633962333934222c22726f6c65223a226d656d626572222c22737461747573223a22696e7669746564222c227575616964223a2275756169643a666f756e646174696f6e3a6167656e743a36613338303364352d663035392d393032612d316336642d616662633962613437323932227d5d2c226e616d65223a22536d616c6c2d6f726465722061646d696e222c2270726576223a6e756c6c2c227472616e736974696f6e223a7b22616374696f6e223a22637265617465222c226163746f72223a2275756169643a666f756e646174696f6e3a6167656e743a30316430666162642d323531662d636262652d326239332d623462393237623236616432222c2272657175657374223a6e756c6c2c22746172676574223a6e756c6c7d2c2276223a2270696c6c61722d67726f75702d6d616e69666573742f32222c2276657273696f6e223a317d"
|
|
10
|
-
},
|
|
11
|
-
"3s2-small-order-member-key": {
|
|
12
|
-
"uuaid": "uuaid:foundation:agent:01d0fabd-251f-cbbe-2b93-b4b927b26ad2",
|
|
13
|
-
"key": "0100000000000000000000000000000000000000000000000000000000000000"
|
|
14
|
-
}
|
|
15
|
-
}
|