@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,346 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Keychain, seal, open, decrypt, envelopeSha, CarrierClient, jcs } from "./pillar.mjs";
3
+ import { CommonsStore } from "./store.mjs";
4
+ import { RegistryTrust } from "./trust.mjs";
5
+ import { BASE_PROTOCOL, createProfile, verifyProfile, proposeAlias, encode, decode, digest, signDocument, verifyDocument, contribution } from "./profiles.mjs";
6
+ // L3 Slice 2 v6 §2.1: Pluggable transport and channel/tenant admission
7
+ import { PillarCarrierTransport } from "./transport.mjs";
8
+ import { classifyThread, assertChannelSendAdmission, assertChannelReceiveAdmission, validateTenantBinding } from "./channels.mjs";
9
+
10
+ export class AgentCommons {
11
+ // L3 Slice 2 v6 §2.1: options.transport and channel/tenant policy flags
12
+ constructor({keychain,store=new CommonsStore(),policy={mode:"local",agents:{}},carriers=[],transport,trust}={}){
13
+ if(!keychain?._identity)throw new Error("A loaded Pillar keychain is required");
14
+ this.keychain=keychain;this.store=store;this.policy=policy;this.trust=trust??new RegistryTrust({policy});this.carriers=carriers;
15
+ for(const carrier of carriers){const url=new URL(carrier);if(policy.mode==="global"&&url.protocol!=="https:")throw new Error("Global carrier URLs must use HTTPS");if(url.username||url.password||url.search||url.hash)throw new Error("Carrier URLs must not embed credentials, queries, or fragments")}
16
+ if(!store.verify())throw new Error("Audit integrity failure: runtime is fail-closed");
17
+ this.transport=transport??(carriers.length?new PillarCarrierTransport({keychain,carriers}):null);
18
+ this.channelsEnabled=policy?.channelsEnabled===true;
19
+ this._applyTail=Promise.resolve();this._receiveTail=Promise.resolve();this._flushTail=Promise.resolve();
20
+ }
21
+ get uuaid(){return this.keychain._identity.uuaid}
22
+ get publicKey(){return this.keychain._identity.publicKeyHex}
23
+ profiles(){return this.store.get("profiles",[])}
24
+ addProfile(profile){verifyProfile(profile);if(profile.scope==="global")throw new Error("Use importGlobal for published global profiles");const profiles=this.profiles();if(!profiles.some(p=>p.id===profile.id)){profiles.push(profile);this.store.set("profiles",profiles)}const active=this.store.get("active",{});if(!active[profile.namespace]){active[profile.namespace]=profile.id;this.store.set("active",active)}return profile}
25
+ profile(id){const profile=this.profiles().find(p=>p.id===id);if(!profile)throw new Error("Unknown local profile digest");verifyProfile(profile);return profile}
26
+ activeProfile(namespace){const active=this.store.get("active",{});const id=active[namespace]??this.profiles().filter(p=>p.namespace===namespace).sort((a,b)=>b.revision-a.revision)[0]?.id;if(!id)throw new Error("Unknown active profile namespace");return this.profile(id)}
27
+ async authorizedProfile(id){const profile=this.profile(id);if(profile.scope==="global"){const code=this.store.get("globalProfilePins",{})[id];if(!code)throw new Error("Global profile publication binding is missing");await this.trust.publishedProfilePin(code,id)}return profile}
28
+ async importGlobal(profile,code){verifyProfile(profile);if(profile.scope!=="global")throw new Error("Global profile scope required");await this.trust.publishedProfilePin(code,profile.id);const profiles=this.profiles();if(!profiles.some(p=>p.id===profile.id)){profiles.push(profile);this.store.set("profiles",profiles)}const pins=this.store.get("globalProfilePins",{});pins[profile.id]=code;this.store.set("globalProfilePins",pins);const active=this.store.get("active",{});active[profile.namespace]=profile.id;this.store.set("active",active);return profile}
29
+ async propose(profileId,phrase){
30
+ await this.trust.authorize(this.uuaid,this.publicKey,"commons:evolve");
31
+ const proposal=proposeAlias(this.profile(profileId),phrase);
32
+ const document=signDocument(this.keychain,"profile-proposal",{proposal});
33
+ await this.apply(document);return document;
34
+ }
35
+ async vote(proposalId,approve,rationale=""){
36
+ if(typeof approve!=="boolean"||typeof rationale!=="string"||rationale.length>2000)throw new Error("Invalid vote");
37
+ await this.trust.authorize(this.uuaid,this.publicKey,"commons:evolve");
38
+ return signDocument(this.keychain,"profile-vote",{proposalId,approve,rationale});
39
+ }
40
+ // Replay keys are namespaced (doc:/msg:sender:) so a peer-chosen message ID cannot poison a signed control document ID.
41
+ seenScoped(key,legacyId,hash){if(this.store.seen(key,hash))return true;try{return this.store.seen(legacyId,hash)}catch{return false}}
42
+ apply(document){const next=this._applyTail.then(()=>this.applyInternal(document));this._applyTail=next.catch(()=>{});return next}
43
+ async applyInternal(document){
44
+ const body=verifyDocument(document);
45
+ if(this.seenScoped(`doc:${document.id}`,document.id,digest(document)))return {duplicate:true};
46
+ if(!Number.isFinite(Date.parse(body.createdAt))||Math.abs(Date.now()-Date.parse(body.createdAt))>86400000)throw new Error("Stale profile document");
47
+ const capability=body.kind==="profile-contribution"?"commons:contribute":body.kind==="profile-recovery"?"commons:recover":"commons:evolve";
48
+ await this.trust.authorize(body.issuer,body.publicKey,capability);
49
+ const proposals=this.store.get("proposals",[]);
50
+ let result;
51
+ if(body.kind==="profile-proposal"){
52
+ const p=body.payload.proposal;
53
+ const base=this.profile(p.parent);
54
+ const recalculated=proposeAlias(base,p.phrase);
55
+ if(p.candidate?.id!==recalculated.candidate.id||p.alias!==recalculated.alias||p.status!=="pending"||Object.keys(p.votes??{}).length)throw new Error("Proposal evidence is not reproducible");
56
+ if(proposals.some(x=>x.id===p.id))throw new Error("Conflicting proposal ID");
57
+ if(typeof p.id!=="string"||!/^[0-9a-f-]{36}$/.test(p.id))throw new Error("Invalid proposal ID");
58
+ proposals.push({...p,author:body.issuer,authorPublicKey:body.publicKey,documentId:document.id});
59
+ result={proposalId:p.id,status:"pending"};
60
+ } else if(body.kind==="profile-vote"){
61
+ const {proposalId,approve}=body.payload;
62
+ const p=proposals.find(p=>p.id===proposalId);
63
+ if(!p||p.status!=="pending"||p.author===body.issuer||p.authorPublicKey===body.publicKey||Object.hasOwn(p.votes,body.issuer)||Object.values(p.votes).some(v=>v.document?.publicKey===body.publicKey)||typeof approve!=="boolean")throw new Error("Ineligible or duplicate peer vote");
64
+ p.votes[body.issuer]={approve,document};
65
+ const parent=this.profile(p.parent);
66
+ await this.trust.authorize(p.author,p.authorPublicKey,"commons:evolve");
67
+ // Recheck every previously approving peer: revoked or unadmitted votes
68
+ // cannot remain valid just because their old signature still verifies.
69
+ const approvals=[],approvalKeys=new Set();
70
+ for(const vote of Object.values(p.votes)){
71
+ if(!vote.approve)continue;
72
+ const voter=verifyDocument(vote.document);
73
+ await this.trust.authorize(voter.issuer,voter.publicKey,"commons:evolve");
74
+ // Quorum counts distinct public keys, never UUAID spellings, and never the proposer's own key.
75
+ if(voter.publicKey===p.authorPublicKey||approvalKeys.has(voter.publicKey))continue;
76
+ approvalKeys.add(voter.publicKey);approvals.push(voter.issuer);
77
+ }
78
+ if(approvals.length>=parent.quorum){
79
+ const latest=this.activeProfile(parent.namespace);
80
+ if(latest.id!==p.parent)throw new Error("Stale proposal cannot replace a newer local profile");
81
+ verifyProfile(p.candidate);
82
+ if(parent.scope==="global")throw new Error("Local votes cannot ratify global profile changes");
83
+ p.status="adopted";
84
+ }
85
+ result={proposalId:p.id,status:p.status,approvals:approvals.length};
86
+ } else if(body.kind==="profile-contribution"){
87
+ const candidate=body.payload.contribution;
88
+ if(candidate?.stage!=="candidate"||candidate?.base!==BASE_PROTOCOL)throw new Error("Invalid global contribution stage");
89
+ if(candidate.profile){verifyProfile(candidate.profile);if(candidate.profile.id!==candidate.profileId)throw new Error("Contribution digest mismatch")}
90
+ const queue=this.store.get("contributions",[]);
91
+ queue.push({id:document.id,issuer:body.issuer,candidate,stage:"awaiting-independent-review",ratified:false});
92
+ result={stage:"awaiting-independent-review",ratified:false};
93
+ this.store.transaction(()=>{this.store.set("contributions",queue);this.store.mark(`doc:${document.id}`,digest(document));this.store.append(document.id,{kind:body.kind,issuer:body.issuer,digest:digest(document),result})});return result;
94
+ } else if(body.kind==="profile-recovery"){
95
+ await this.trust.authorize(body.issuer,body.publicKey,"commons:recover");
96
+ const profile=this.profile(body.payload.profileId);
97
+ if(profile.scope==="global")throw new Error("Global recovery needs independent IAASO disposition, not a local rollback");
98
+ if(typeof body.payload.reason!=="string"||body.payload.reason.length<3)throw new Error("Recovery rationale required");
99
+ this.store.transaction(()=>{const active=this.store.get("active",{});active[profile.namespace]=profile.id;this.store.set("active",active);this.store.mark(`doc:${document.id}`,digest(document));this.store.append(document.id,{kind:body.kind,issuer:body.issuer,profileId:profile.id,reason:body.payload.reason})});
100
+ return {status:"local-profile-pinned",profileId:profile.id};
101
+ } else throw new Error("Unsupported profile control document");
102
+ this.store.transaction(()=>{
103
+ this.store.set("proposals",proposals);
104
+ const adopted=proposals.find(p=>p.id===result.proposalId&&p.status==="adopted");
105
+ if(adopted){if(!this.profiles().some(p=>p.id===adopted.candidate.id)){const profiles=this.profiles();profiles.push(adopted.candidate);this.store.set("profiles",profiles)}const active=this.store.get("active",{});active[adopted.candidate.namespace]=adopted.candidate.id;this.store.set("active",active)}
106
+ this.store.mark(`doc:${document.id}`,digest(document));this.store.append(document.id,{kind:body.kind,issuer:body.issuer,digest:digest(document),result});
107
+ });
108
+ return result;
109
+ }
110
+ async prepareContribution(profileId,options={}){
111
+ await this.trust.authorize(this.uuaid,this.publicKey,"commons:contribute");
112
+ return signDocument(this.keychain,"profile-contribution",{contribution:contribution(this.profile(profileId),options)});
113
+ }
114
+ // L3 Slice 2 v6 §2.1 / §4.2, §4.2.3, §5.2: channel/tenant admission, atomic authenticated admission record
115
+ async send({recipient,profileId,namespace,body,thread="commons",kind="message",deferFlush=false}){
116
+ if(typeof body!=="string"||Buffer.byteLength(body)>32000||typeof thread!=="string"||thread.length>120||!["message","profile-control"].includes(kind))throw new Error("Invalid bounded message");
117
+ const classified=classifyThread(thread,this.policy);
118
+ if(classified.ok!==true)throw new Error(classified.error);
119
+ if(classified.classification==="structured"){
120
+ assertChannelSendAdmission(classified.channelUri,this.uuaid,[recipient],this.policy);
121
+ }
122
+ const recipientKey=this.policy.agents?.[recipient]?.publicKey;
123
+ await this.trust.authorize(this.uuaid,this.publicKey,"commons:message");
124
+ await this.trust.authorize(recipient,recipientKey,"commons:message");
125
+ if(!profileId)profileId=this.activeProfile(namespace).id;
126
+ const profile=await this.authorizedProfile(profileId);
127
+ validateTenantBinding(this.uuaid,recipient,profile,this.policy);
128
+ const payload={v:BASE_PROTOCOL,id:randomUUID(),kind,thread,profileId:profile.id,wire:encode(body,profile.lexicon),bodyHash:digest(body),expiresAt:new Date(Date.now()+3600000).toISOString()};
129
+ const envelope=seal(this.keychain,{recipient,recipientPublicKey:recipientKey,kind:BASE_PROTOCOL,payload});
130
+ const tuple={id:envelope.id,sha:envelopeSha(envelope),thread,profileId:profile.id,sender:this.uuaid,recipient};
131
+ const signature=this.keychain.sign(Buffer.from(jcs(tuple))).toString("hex");
132
+ const meta={...tuple,signature};
133
+ this.store.transaction(()=>{
134
+ this.store.queue(envelope,profile.id);
135
+ this.store.set(`outbox_meta:${envelope.id}`,meta);
136
+ this.store.append(`admission-${envelope.id}`,{kind:"outbox-admission",id:envelope.id,sha:meta.sha,thread,profileId:profile.id,sender:this.uuaid,recipient,signature});
137
+ });
138
+ if(!this.transport||deferFlush)return {envelope,state:"pending",reason:deferFlush?"Queued for bounded batch delivery":"No configured carrier"};
139
+ const result=await this.flush();return {envelope,result};
140
+ }
141
+ async broadcast({recipients,...message}){
142
+ if(!Array.isArray(recipients)||!recipients.length||recipients.length>32||new Set(recipients).size!==recipients.length)throw new Error("Broadcast requires 1–32 distinct agent recipients");
143
+ for(const recipient of recipients)await this.trust.authorize(recipient,this.policy.agents?.[recipient]?.publicKey,"commons:message");
144
+ const queued=[];
145
+ for(const recipient of recipients)queued.push(await this.send({...message,recipient,deferFlush:true}));
146
+ return {queued:queued.map(x=>({recipient:x.envelope.recipient,id:x.envelope.id})),deliveries:await this.flush(),groupPrivacy:"individually-sealed-recipient-copies"};
147
+ }
148
+ flush(){const next=this._flushTail.then(()=>this.flushInternal());this._flushTail=next.catch(()=>{});return next}
149
+ // L3 Slice 2 v6 §2.1 / §3.1, §4.2, §4.2.3, §5.2: pre-delivery rechecks, signature verification, and delivery
150
+ async flushInternal(){
151
+ if(!this.transport)return [];
152
+ const results=[];
153
+ for(let pageIndex=0;pageIndex<20;pageIndex++){
154
+ const page=this.store.pending();
155
+ if(!page.length)break;
156
+ let deliveredInPass=0;
157
+ for(const row of page){
158
+ try{
159
+ const envelope=JSON.parse(row.body);
160
+ if(!open(envelope).ok||envelope.sender!==this.uuaid||envelope.id!==row.id)throw new Error("Outbox integrity or sender mismatch");
161
+ const meta=this.store.get(`outbox_meta:${row.id}`);
162
+ if(!meta||meta.id!==row.id||meta.sha!==envelopeSha(envelope)||meta.profileId!==row.profile_id||meta.sender!==envelope.sender||meta.recipient!==envelope.recipient||typeof meta.thread!=="string"){
163
+ throw new Error("INVALID_OUTBOX_METADATA");
164
+ }
165
+ const signedTuple={id:row.id,sha:meta.sha,thread:meta.thread,profileId:meta.profileId,sender:meta.sender,recipient:meta.recipient};
166
+ const okSig=Keychain.verifyDetached(this.publicKey,Buffer.from(jcs(signedTuple)),Buffer.from(meta.signature,"hex"));
167
+ if(!okSig)throw new Error("INVALID_OUTBOX_METADATA");
168
+ const classified=classifyThread(meta.thread,this.policy);
169
+ if(classified.ok!==true)throw new Error(classified.error);
170
+ if(classified.classification==="structured"){
171
+ assertChannelSendAdmission(classified.channelUri,envelope.sender,[envelope.recipient],this.policy);
172
+ }
173
+ const profile=meta.profileId?await this.authorizedProfile(meta.profileId):(row.profile_id?await this.authorizedProfile(row.profile_id):null);
174
+ validateTenantBinding(envelope.sender,envelope.recipient,profile,this.policy);
175
+ await this.trust.authorize(this.uuaid,this.publicKey,"commons:message");
176
+ await this.trust.authorize(envelope.recipient,this.policy.agents?.[envelope.recipient]?.publicKey,"commons:message");
177
+ const receipt=await this.transport.deliver(envelope,{timeoutMs:10000});
178
+ if(receipt?.accepted!==true)throw new Error("Carrier rejected envelope");
179
+ this.store.delivered(row.id);
180
+ this.store.set(`outbox_meta:${row.id}`,null);
181
+ this.store.append(`outbox-${row.id}`,{kind:"carrier-accepted",id:row.id,sha:envelopeSha(envelope),carrier:receipt.carrier});
182
+ results.push({id:row.id,state:"carrier-accepted",receipt});
183
+ deliveredInPass++;
184
+ }
185
+ catch(error){
186
+ this.store.failure(row.id,error.message);
187
+ results.push({id:row.id,state:"retry-or-quarantine",error:error.code??error.message});
188
+ }
189
+ }
190
+ if(deliveredInPass===0)break;
191
+ }
192
+ return results;
193
+ }
194
+ receive(envelope){const next=this._receiveTail.then(()=>this.receiveInternal(envelope));this._receiveTail=next.catch(()=>{});return next}
195
+ // L3 Slice 2 v6 §2.1 / §4.2, §4.2.3, §5.2: inbound channel and tenant admission
196
+ async receiveInternal(envelope){
197
+ const valid=open(envelope);if(!valid.ok)throw new Error(`Invalid Pillar envelope: ${valid.reason}`);
198
+ if(envelope.delegation!==undefined)throw new Error("Delegated host admission requires a configured principal-status adapter; not yet enabled");
199
+ await this.trust.authorize(envelope.sender,envelope.transportSignature.publicKey,"commons:message");
200
+ const payload=decrypt(this.keychain,envelope);
201
+ if(payload?.v!==BASE_PROTOCOL||payload.profileId===undefined||!["message","profile-control"].includes(payload.kind)||typeof payload.wire!=="string"||Buffer.byteLength(payload.wire)>64000||typeof payload.id!=="string"||!/^[0-9a-f-]{36}$/.test(payload.id)||!Number.isFinite(Date.parse(payload.expiresAt))||Date.parse(payload.expiresAt)<=Date.now())throw new Error("Invalid or expired Agent Commons payload");
202
+ const msgKey=`msg:${envelope.sender}:${payload.id}`;if(this.seenScoped(msgKey,payload.id,digest(payload)))return {duplicate:true};
203
+ const profile=await this.authorizedProfile(payload.profileId),body=decode(payload.wire,profile.lexicon);
204
+ if(digest(body)!==payload.bodyHash)throw new Error("Lossless body digest mismatch");
205
+ const classified=classifyThread(payload.thread,this.policy);
206
+ if(classified.ok!==true)throw new Error(classified.error);
207
+ if(classified.classification==="structured"){
208
+ try{
209
+ assertChannelReceiveAdmission(classified.channelUri,envelope.sender,this.uuaid,this.policy);
210
+ }catch(err){
211
+ if(this.policy.holdBeforeAdmission!==true)throw err;
212
+ if(err.code!=="UNAUTHORIZED_CHANNEL_SENDER"&&err.code!=="UNAUTHORIZED_CHANNEL_RECEIVER")throw err;
213
+ if(payload.kind!=="message")throw err;
214
+ const agent=this.policy.agents?.[envelope.sender];
215
+ const admitted=agent&&agent.kind==="agent"&&Array.isArray(agent.capabilities)&&agent.capabilities.includes("commons:message")&&agent.publicKey===envelope.transportSignature?.publicKey;
216
+ if(!admitted)throw err;
217
+ const expired=this.store.pruneExpiredHeld();
218
+ for(const exp of expired){
219
+ this.store.append(randomUUID(),{kind:"hold-expired",id:exp.id,sender:exp.sender,channel:exp.channel,sha:exp.sha,reason:"expired"});
220
+ }
221
+ // a re-run of a still-ineligible held entry returns {held:true} here, so release keeps it without counting it again (C4)
222
+ if(this.store.isHeld(envelope.id,msgKey)){
223
+ return {held:true,id:envelope.id,sender:envelope.sender,channel:classified.channelUri};
224
+ }
225
+ const counts=this.store.holdCounts(classified.channelUri,envelope.sender);
226
+ let violatedBound=null;
227
+ if(counts.channelSender>=20)violatedBound="channel-sender";
228
+ else if(counts.sender>=20)violatedBound="sender";
229
+ else if(counts.channel>=100)violatedBound="channel";
230
+ else if(counts.total>=500)violatedBound="total";
231
+ if(violatedBound){
232
+ this.store.append(randomUUID(),{kind:"hold-refused",id:envelope.id,sender:envelope.sender,channel:classified.channelUri,sha:envelopeSha(envelope),reason:violatedBound});
233
+ throw err;
234
+ }
235
+ const payloadExpires=Date.parse(payload.expiresAt);
236
+ const expiresAt=Math.min(Date.now()+600000,payloadExpires);
237
+ this.store.hold(envelope,msgKey,classified.channelUri,envelope.sender,envelopeSha(envelope),expiresAt);
238
+ this.store.append(randomUUID(),{kind:"hold-added",id:envelope.id,sender:envelope.sender,channel:classified.channelUri,sha:envelopeSha(envelope)});
239
+ return {held:true,id:envelope.id,sender:envelope.sender,channel:classified.channelUri};
240
+ }
241
+ }
242
+ validateTenantBinding(envelope.sender,this.uuaid,profile,this.policy);
243
+ if(payload.kind==="profile-control"){
244
+ const result=await this.apply(JSON.parse(body));
245
+ this.store.transaction(()=>{this.store.mark(msgKey,digest(payload));this.store.append(`ctl:${envelope.sender}:${payload.id}`,{kind:"control-received",sender:envelope.sender,envelopeId:envelope.id,result})});
246
+ return {id:payload.id,kind:payload.kind,result};
247
+ }
248
+ this.store.transaction(()=>{this.store.mark(msgKey,digest(payload));this.store.append(`msg:${envelope.sender}:${payload.id}`,{kind:"message-received",sender:envelope.sender,envelopeId:envelope.id,profileId:profile.id,bodyHash:payload.bodyHash,wireBytes:Buffer.byteLength(payload.wire)})});
249
+ return {id:payload.id,sender:envelope.sender,thread:payload.thread,body,profileId:profile.id,accepted:true,receipt:{kind:"host-accepted",notProofOfTaskCompletion:true}};
250
+ }
251
+ releaseHeld(){const next=this._receiveTail.then(()=>this.releaseHeldInternal());this._receiveTail=next.catch(()=>{});return next}
252
+ async releaseHeldInternal(){
253
+ const heldEntries=this.store.allHeld();
254
+ const results=[];
255
+ const now=Date.now();
256
+ for(const entry of heldEntries){
257
+ if(entry.expires_at<=now){
258
+ this.store.removeHeld(entry.id);
259
+ this.store.append(randomUUID(),{kind:"hold-expired",id:entry.id,sender:entry.sender,channel:entry.channel,sha:entry.sha,reason:"expired"});
260
+ continue;
261
+ }
262
+ const envelope=JSON.parse(entry.envelope);
263
+ const agent=this.policy.agents?.[entry.sender];
264
+ const eligible=agent&&agent.kind==="agent"&&Array.isArray(agent.capabilities)&&agent.capabilities.includes("commons:message")&&agent.publicKey===envelope.transportSignature?.publicKey;
265
+ if(!eligible){
266
+ this.store.removeHeld(entry.id);
267
+ this.store.append(randomUUID(),{kind:"hold-dropped",id:entry.id,sender:entry.sender,channel:entry.channel,sha:entry.sha,reason:"sender-not-admitted"});
268
+ continue;
269
+ }
270
+ try{
271
+ const res=await this.receiveInternal(envelope);
272
+ if(res?.duplicate===true){
273
+ this.store.removeHeld(entry.id);
274
+ this.store.append(randomUUID(),{kind:"hold-released",id:entry.id,sender:entry.sender,channel:entry.channel,sha:entry.sha,reason:"duplicate-of-accepted"});
275
+ results.push(res);
276
+ } else if(res?.accepted===true){
277
+ this.store.removeHeld(entry.id);
278
+ this.store.append(randomUUID(),{kind:"hold-released",id:entry.id,sender:entry.sender,channel:entry.channel,sha:entry.sha,reason:"released"});
279
+ results.push(res);
280
+ }
281
+ }catch(error){
282
+ if(error?.transient===true){
283
+ this.store.append(randomUUID(),{kind:"hold-deferred",id:entry.id,sender:entry.sender,channel:entry.channel,sha:entry.sha,reason:error.code??error.message??"transient-error"});
284
+ break;
285
+ }
286
+ this.store.removeHeld(entry.id);
287
+ this.store.append(randomUUID(),{kind:"hold-dropped",id:entry.id,sender:entry.sender,channel:entry.channel,sha:entry.sha,reason:error.code??error.message});
288
+ }
289
+ }
290
+ return results;
291
+ }
292
+ // L3 Slice 2 v6 §2.1 / §3.1, §8 (R2): foreign item skip with cursor advance and recipient isolation
293
+ async poll(){
294
+ if(!this.transport)throw new Error("No carrier configured");
295
+ const cursors=this.store.get("cursors",{}),health=this.store.get("carrierHealth",{}),results=[];
296
+ const sources=this.transport?.sources?.()??this.carriers;
297
+ for(const carrier of sources){
298
+ if(health[carrier]?.nextProbeAt>Date.now()){results.push({carrier,rejected:true,state:"bounded-backoff"});continue}
299
+ let inbox;
300
+ try{inbox=await this.transport.fetchInbox(carrier,{since:cursors[carrier]??0,waitS:0,timeoutMs:10000});health[carrier]={failures:0,nextProbeAt:0};this.store.set("carrierHealth",health)}
301
+ catch(error){const failures=(health[carrier]?.failures??0)+1;health[carrier]={failures,nextProbeAt:Date.now()+Math.min(60000,1000*2**Math.min(failures,6)),error:error.code??"carrier-unavailable"};this.store.set("carrierHealth",health);this.store.append(randomUUID(),{kind:"carrier-poll-failed",carrier,failures,reason:error.code??"carrier-unavailable"});results.push({carrier,rejected:true,state:failures>=3?"circuit-backoff":"retry-backoff"});continue}
302
+ let last=cursors[carrier]??0;
303
+ for(const item of inbox.envelopes){
304
+ // A carrier may not rewind or repeat sequence numbers within a response.
305
+ if(!Number.isSafeInteger(item?.seq)||item.seq<=last){results.push({carrier,rejected:true,reason:"non-monotonic-carrier-sequence"});continue}
306
+ const valid=open(item?.envelope);
307
+ if(!valid.ok){
308
+ results.push({id:item?.envelope?.id,rejected:true,reason:`Invalid Pillar envelope: ${valid.reason}`});
309
+ this.store.append(`rejected-${item?.envelope?.id}`,{kind:"quarantined-inbound",id:item?.envelope?.id,reason:`Invalid Pillar envelope: ${valid.reason}`});
310
+ last=item.seq;cursors[carrier]=item.seq;this.store.set("cursors",cursors);
311
+ continue;
312
+ }
313
+ if(item.envelope.recipient!==this.uuaid){
314
+ results.push({id:item.envelope.id,skipped:true,recipient:item.envelope.recipient});
315
+ last=item.seq;cursors[carrier]=item.seq;this.store.set("cursors",cursors);
316
+ continue;
317
+ }
318
+ // Held envelopes return { held: true }, advancing the cursor without writing quarantined-inbound (R6)
319
+ try{results.push(await this.receive(item.envelope))}
320
+ catch(error){
321
+ if(error?.transient===true){
322
+ // Trust backend outage is not a verdict on the sender: keep the cursor so the envelope is retried, and stop to preserve order.
323
+ const failures=(health[carrier]?.failures??0)+1;health[carrier]={failures,nextProbeAt:Date.now()+Math.min(60000,1000*2**Math.min(failures,6)),error:error.code??"trust-unavailable"};this.store.set("carrierHealth",health);
324
+ this.store.append(`deferred-${item.envelope?.id}`,{kind:"inbound-deferred-trust-unavailable",id:item.envelope?.id});
325
+ results.push({id:item.envelope?.id,rejected:false,deferred:true,state:"trust-unavailable-retry",reason:error.code??"trust-unavailable"});break;
326
+ }
327
+ results.push({id:item.envelope?.id,rejected:true,reason:error.message});this.store.append(`rejected-${item.envelope?.id}`,{kind:"quarantined-inbound",id:item.envelope?.id,reason:error.message});
328
+ }
329
+ last=item.seq;cursors[carrier]=item.seq;this.store.set("cursors",cursors);
330
+ }
331
+ }
332
+ return results;
333
+ }
334
+ async retryOutbox(envelopeId){
335
+ await this.trust.authorize(this.uuaid,this.publicKey,"commons:recover");
336
+ const row=this.store.db.prepare("SELECT * FROM outbox WHERE id=?").get(envelopeId);
337
+ if(!row||row.state!=="quarantined")throw new Error("Only a known quarantined outbox envelope can be retried");
338
+ const envelope=JSON.parse(row.body);
339
+ if(row.profile_id)await this.authorizedProfile(row.profile_id);
340
+ if(!open(envelope).ok||envelope.sender!==this.uuaid||Date.parse(envelope.createdAt)<Date.now()-3500000)throw new Error("Envelope is invalid or expired; create a fresh authorized message instead");
341
+ await this.trust.authorize(envelope.recipient,this.policy.agents?.[envelope.recipient]?.publicKey,"commons:message");
342
+ this.store.transaction(()=>{this.store.db.prepare("UPDATE outbox SET attempts=0,next_at=0,state='pending',error=NULL WHERE id=?").run(envelopeId);this.store.append(randomUUID(),{kind:"outbox-retry-authorized",envelopeId,issuer:this.uuaid})});
343
+ return {id:envelopeId,state:"pending",networkCallMade:false};
344
+ }
345
+ status(){return {name:"Agent Commons",protocol:BASE_PROTOCOL,uuaid:this.uuaid,publicKey:this.publicKey,mode:this.policy.mode??"local",profiles:this.profiles().map(p=>({id:p.id,name:p.name,namespace:p.namespace,scope:p.scope,revision:p.revision})),activeProfiles:this.store.get("active",{}),carriers:this.carriers,pending:this.store.pending().length,proposals:this.store.get("proposals",[]).length,contributions:this.store.get("contributions",[]).length,auditVerified:this.store.verify(),standardization:"draft-profile-not-IAASO-certified"}}
346
+ }
package/src/store.mjs ADDED
@@ -0,0 +1,73 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { mkdirSync, chmodSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { digest } from "./profiles.mjs";
5
+
6
+ export class CommonsStore {
7
+ constructor(path=":memory:") {
8
+ if(path!==":memory:")mkdirSync(dirname(path),{recursive:true,mode:0o700});
9
+ this.db=new DatabaseSync(path);
10
+ this.db.exec(`PRAGMA journal_mode=WAL; CREATE TABLE IF NOT EXISTS kv(k TEXT PRIMARY KEY,v TEXT NOT NULL);
11
+ CREATE TABLE IF NOT EXISTS envelopes(seq INTEGER PRIMARY KEY AUTOINCREMENT,id TEXT NOT NULL UNIQUE,sender TEXT NOT NULL,recipient TEXT NOT NULL,sha TEXT NOT NULL,body TEXT NOT NULL,expires INTEGER NOT NULL);
12
+ CREATE INDEX IF NOT EXISTS inbox_recipient ON envelopes(recipient,seq);
13
+ CREATE TABLE IF NOT EXISTS events(seq INTEGER PRIMARY KEY AUTOINCREMENT,id TEXT NOT NULL UNIQUE,previous TEXT NOT NULL,hash TEXT NOT NULL,body TEXT NOT NULL);
14
+ CREATE TABLE IF NOT EXISTS outbox(id TEXT PRIMARY KEY,body TEXT NOT NULL,attempts INTEGER NOT NULL DEFAULT 0,next_at INTEGER NOT NULL DEFAULT 0,state TEXT NOT NULL DEFAULT 'pending',error TEXT);
15
+ CREATE TABLE IF NOT EXISTS processed(id TEXT PRIMARY KEY,hash TEXT NOT NULL);
16
+ CREATE TABLE IF NOT EXISTS held(id TEXT PRIMARY KEY,msg_key TEXT NOT NULL UNIQUE,channel TEXT NOT NULL,sender TEXT NOT NULL,envelope TEXT NOT NULL,sha TEXT NOT NULL,expires_at INTEGER NOT NULL,created_at INTEGER NOT NULL);
17
+ CREATE INDEX IF NOT EXISTS held_channel ON held(channel);
18
+ CREATE INDEX IF NOT EXISTS held_sender ON held(sender);
19
+ CREATE INDEX IF NOT EXISTS held_channel_sender ON held(channel,sender);
20
+ `);
21
+ if(!this.db.prepare("PRAGMA table_info(outbox)").all().some(c=>c.name==="profile_id"))this.db.exec("ALTER TABLE outbox ADD COLUMN profile_id TEXT");
22
+ if(path!==":memory:")chmodSync(path,0o600);
23
+ }
24
+ get(key,fallback=null){const row=this.db.prepare("SELECT v FROM kv WHERE k=?").get(key);return row?JSON.parse(row.v):fallback}
25
+ set(key,value){this.db.prepare("INSERT INTO kv(k,v) VALUES(?,?) ON CONFLICT(k) DO UPDATE SET v=excluded.v").run(key,JSON.stringify(value))}
26
+ transaction(fn){this.db.exec("BEGIN IMMEDIATE");try{const result=fn();this.db.exec("COMMIT");return result}catch(e){this.db.exec("ROLLBACK");throw e}}
27
+ append(id,event){
28
+ const existing=this.db.prepare("SELECT * FROM events WHERE id=?").get(id);if(existing){if(digest(JSON.parse(existing.body))!==digest(event))throw new Error("Audit event ID conflicts with content");return {...existing,duplicate:true}};
29
+ const previous=this.db.prepare("SELECT hash FROM events ORDER BY seq DESC LIMIT 1").get()?.hash??"genesis";
30
+ const hash=digest({previous,event});
31
+ this.db.prepare("INSERT INTO events(id,previous,hash,body) VALUES(?,?,?,?)").run(id,previous,hash,JSON.stringify(event));
32
+ return {id,previous,hash};
33
+ }
34
+ verify(){let previous="genesis";return this.db.prepare("SELECT * FROM events ORDER BY seq").all().every(r=>{const ok=r.previous===previous&&r.hash===digest({previous,event:JSON.parse(r.body)});previous=r.hash;return ok})}
35
+ audit(){return this.db.prepare("SELECT * FROM events ORDER BY seq DESC LIMIT 100").all()}
36
+ seen(id,hash=null){const row=this.db.prepare("SELECT hash FROM processed WHERE id=?").get(id);if(row&&hash&&row.hash!==hash)throw new Error("Replay ID conflicts with signed content");return !!row}
37
+ mark(id,hash){this.db.prepare("INSERT INTO processed(id,hash) VALUES(?,?)").run(id,hash)}
38
+ retain(envelope,sha,ttlMs=14*86400000){
39
+ const existing=this.db.prepare("SELECT seq,sha FROM envelopes WHERE id=?").get(envelope.id);
40
+ if(existing){if(existing.sha!==sha)throw new Error("Envelope ID conflicts with existing content");return {...existing,duplicate:true}}
41
+ this.db.prepare("DELETE FROM envelopes WHERE expires<?").run(Date.now());
42
+ if(this.db.prepare("SELECT COUNT(*) AS n FROM envelopes").get().n>=10000||this.db.prepare("PRAGMA page_count").get().page_count*this.db.prepare("PRAGMA page_size").get().page_size>=256*1024*1024)throw new Error("Carrier storage budget exhausted");
43
+ const r=this.db.prepare("INSERT INTO envelopes(id,sender,recipient,sha,body,expires) VALUES(?,?,?,?,?,?)").run(envelope.id,envelope.sender,envelope.recipient,sha,JSON.stringify(envelope),Date.now()+ttlMs);
44
+ return {seq:Number(r.lastInsertRowid),sha,duplicate:false};
45
+ }
46
+ inbox(recipient,since=0){return this.db.prepare("SELECT seq,body FROM envelopes WHERE recipient=? AND seq>? AND expires>? ORDER BY seq LIMIT 100").all(recipient,since,Date.now()).map(r=>({seq:r.seq,envelope:JSON.parse(r.body)}))}
47
+ queue(envelope,profileId=null){this.db.prepare("INSERT OR IGNORE INTO outbox(id,body,profile_id) VALUES(?,?,?)").run(envelope.id,JSON.stringify(envelope),profileId)}
48
+ pending(){return this.db.prepare("SELECT * FROM outbox WHERE state='pending' AND next_at<=? ORDER BY rowid LIMIT 50").all(Date.now())}
49
+ delivered(id){this.db.prepare("UPDATE outbox SET state='accepted',error=NULL WHERE id=?").run(id)}
50
+ failure(id,error){const row=this.db.prepare("SELECT attempts FROM outbox WHERE id=?").get(id);const attempts=row.attempts+1;const state=attempts>=5?"quarantined":"pending";this.db.prepare("UPDATE outbox SET attempts=?,next_at=?,state=?,error=? WHERE id=?").run(attempts,Date.now()+Math.min(300000,1000*2**attempts),state,error.slice(0,300),id)}
51
+ hold(envelope,msgKey,channel,sender,sha,expiresAt){this.db.prepare("INSERT INTO held(id,msg_key,channel,sender,envelope,sha,expires_at,created_at) VALUES(?,?,?,?,?,?,?,?)").run(envelope.id,msgKey,channel,sender,JSON.stringify(envelope),sha,expiresAt,Date.now())}
52
+ isHeld(id,msgKey){return !!this.db.prepare("SELECT 1 FROM held WHERE id=? OR msg_key=?").get(id,msgKey)}
53
+ removeHeld(id){this.db.prepare("DELETE FROM held WHERE id=?").run(id)}
54
+ allHeld(){return this.db.prepare("SELECT * FROM held ORDER BY created_at ASC, rowid ASC").all()}
55
+ holdCounts(channel,sender){
56
+ const total=this.db.prepare("SELECT COUNT(*) AS n FROM held").get().n;
57
+ const channelCount=this.db.prepare("SELECT COUNT(*) AS n FROM held WHERE channel=?").get(channel).n;
58
+ const senderCount=this.db.prepare("SELECT COUNT(*) AS n FROM held WHERE sender=?").get(sender).n;
59
+ const channelSender=this.db.prepare("SELECT COUNT(*) AS n FROM held WHERE channel=? AND sender=?").get(channel,sender).n;
60
+ return {total,channel:channelCount,sender:senderCount,channelSender};
61
+ }
62
+ pruneExpiredHeld(now=Date.now()){
63
+ return this.transaction(()=>{
64
+ const expired=this.db.prepare("SELECT * FROM held WHERE expires_at<=?").all(now);
65
+ if(expired.length){
66
+ const del=this.db.prepare("DELETE FROM held WHERE id=?");
67
+ for(const row of expired)del.run(row.id);
68
+ }
69
+ return expired;
70
+ });
71
+ }
72
+ close(){this.db.close()}
73
+ }
@@ -0,0 +1,76 @@
1
+ export interface TransportCapabilities {
2
+ readonly name: string;
3
+ readonly canPush: boolean; // Can receive inbound pushes
4
+ readonly canPoll: boolean; // Supports inbox polling
5
+ readonly supportsStreaming: boolean; // Supports SSE / live streams
6
+ readonly maxMessageBytes: number; // Transport-specific message size ceiling
7
+ readonly loopbackSafe: boolean; // Works on 127.0.0.1 without external network
8
+ }
9
+
10
+ export interface DeliveryResult {
11
+ readonly carrier: string; // Identifying URI of the accepting carrier
12
+ readonly seq?: number; // Monotonic sequence assigned by carrier
13
+ readonly sha: string; // Envelope SHA-256
14
+ readonly duplicate: boolean; // Whether the carrier already held this envelope
15
+ readonly all?: any[]; // Multi-carrier replication or refusal details
16
+ readonly accepted: boolean; // Must be true for outbox consumption
17
+ readonly retryable?: boolean;
18
+ readonly quarantine?: boolean;
19
+ readonly reason?: string;
20
+ }
21
+
22
+ export interface InboxEnvelopeItem {
23
+ readonly sourceId: string;
24
+ readonly seq: number;
25
+ readonly envelope: any;
26
+ }
27
+
28
+ export interface InboxBatch {
29
+ readonly sourceId: string;
30
+ readonly envelopes: InboxEnvelopeItem[];
31
+ readonly now: number;
32
+ }
33
+
34
+ export interface MessageTransport {
35
+ readonly capabilities: TransportCapabilities;
36
+ sources(): readonly string[];
37
+ deliver(envelope: any, options?: { timeoutMs?: number }): Promise<DeliveryResult>;
38
+ fetchInbox(sourceId: string, options?: { since?: number; waitS?: number; timeoutMs?: number }): Promise<InboxBatch>;
39
+ subscribe?(sourceId: string, onEnvelope: (item: InboxEnvelopeItem) => Promise<void>): () => void;
40
+ close(): Promise<void>;
41
+ }
42
+
43
+ export interface MemoryLoopbackOptions {
44
+ instanceId?: string;
45
+ capabilities?: Partial<TransportCapabilities>;
46
+ }
47
+
48
+ export class MemoryLoopbackTransport implements MessageTransport {
49
+ constructor(options?: MemoryLoopbackOptions);
50
+ readonly instanceId: string;
51
+ readonly sourceId: string;
52
+ readonly capabilities: TransportCapabilities;
53
+ sources(): readonly string[];
54
+ deliver(envelope: any, options?: { timeoutMs?: number }): Promise<DeliveryResult>;
55
+ fetchInbox(sourceId: string, options?: { since?: number; waitS?: number; timeoutMs?: number }): Promise<InboxBatch>;
56
+ subscribe(sourceId: string, onEnvelope: (item: InboxEnvelopeItem) => Promise<void>): () => void;
57
+ close(): Promise<void>;
58
+ }
59
+
60
+ export interface PillarCarrierOptions {
61
+ keychain?: any;
62
+ client?: any;
63
+ carriers?: string[];
64
+ env?: Record<string, string | undefined>;
65
+ capabilities?: Partial<TransportCapabilities>;
66
+ }
67
+
68
+ export class PillarCarrierTransport implements MessageTransport {
69
+ constructor(options?: PillarCarrierOptions);
70
+ readonly client: any;
71
+ readonly capabilities: TransportCapabilities;
72
+ sources(): readonly string[];
73
+ deliver(envelope: any, options?: { timeoutMs?: number }): Promise<DeliveryResult>;
74
+ fetchInbox(sourceId: string, options?: { since?: number; waitS?: number; timeoutMs?: number }): Promise<InboxBatch>;
75
+ close(): Promise<void>;
76
+ }