@ramex-labs/continuity-remote 0.3.0-preview.1

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.
@@ -0,0 +1,180 @@
1
+ import http from 'node:http';
2
+ import { openDestinationStore } from './durable-store.mjs';
3
+ import { stateOf } from '@ramex-labs/continuity/adapter';
4
+ import { captureHistory } from '@ramex-labs/continuity/adapter';
5
+ import { capture, exact, textId, hashId, integer, canonicalDigest, encodeTransport, decodeTransport,
6
+ publicKeyIdentity, verifyRequest, signResponse, validateOperationShape, validateReport, MAX_WIRE_BYTES } from './wire.mjs';
7
+
8
+ const refuse=code=>{throw Object.assign(new Error(code),{code});};
9
+ const same=(a,b)=>canonicalDigest(a)===canonicalDigest(b);
10
+ const response=(state,code)=>({state,code});
11
+ const refusals=['NO_CHECKPOINT','CHECKPOINT_CONFLICT','CHECKPOINT_CHANGED','OPERATION_CONFLICT','BUSINESS_KEY_CONFLICT',
12
+ 'AUTHORIZATION_REFUSED','CLOCK_INVALID','CAPACITY_EXHAUSTED','INVALID_OPERATION'];
13
+ const requestBytes=request=>new Promise((resolve,reject)=>{
14
+ const parts=[];let size=0,settled=false;
15
+ const timer=setTimeout(()=>{if(!settled){settled=true;reject(Error('REQUEST_TIMEOUT'));request.destroy();}},5000);
16
+ const end=()=>clearTimeout(timer);
17
+ request.on('data',chunk=>{if(settled)return;size+=chunk.length;if(size>MAX_WIRE_BYTES){settled=true;end();reject(Error('REQUEST_LIMIT'));request.destroy();}else parts.push(chunk);});
18
+ request.on('end',()=>{if(settled)return;settled=true;end();resolve(Buffer.concat(parts));});
19
+ request.on('error',error=>{if(settled)return;settled=true;end();reject(error);});
20
+ request.on('aborted',()=>{if(settled)return;settled=true;end();reject(Error('REQUEST_ABORTED'));});
21
+ });
22
+
23
+ /** Synthetic effects only: effect and deduplication record share one commit. */
24
+ export async function createCooperativeDestination({directory,domain,serviceId,coordinatorPublicKey,servicePrivateKey,now,validateOperation}) {
25
+ domain=capture(domain);textId(serviceId);
26
+ if(typeof now!=='function'||typeof validateOperation!=='function'||servicePrivateKey.type!=='private'||coordinatorPublicKey.type!=='public')throw Error('INVALID_CONFIGURATION');
27
+ const coordinatorKey=publicKeyIdentity(coordinatorPublicKey),serviceKey=publicKeyIdentity(servicePrivateKey);
28
+ const store=openDestinationStore({directory,initial:{version:'continuity-cooperative-destination/1',domain,serviceId,
29
+ coordinatorKey,serviceKey,sequence:0,lastTime:0,checkpoint:null,attempts:[],businessKeys:[],effects:[]}});
30
+ const clock=state=>{
31
+ let at;try{at=integer(now());}catch{refuse('CLOCK_INVALID');}
32
+ if(at<state.lastTime||(state.checkpoint&&at<state.checkpoint.head.canonicalTime))refuse('CLOCK_INVALID');
33
+ state.lastTime=at;
34
+ return at;
35
+ };
36
+ const normalized=(events,operation,at,state)=>{
37
+ let result;
38
+ try{result=capture(validateOperation(events,operation,at,capture(state)));}
39
+ catch{refuse('AUTHORIZATION_REFUSED');}
40
+ exact(result,['intentId','idempotencyKey','submissionFingerprint','tool','arguments','businessKey','contractId']);
41
+ hashId(result.idempotencyKey);hashId(result.submissionFingerprint);
42
+ for(const name of ['intentId','tool','businessKey','contractId'])textId(result[name]);
43
+ if(result.intentId!==operation.intentId||result.tool!==operation.tool||result.businessKey!==operation.businessKey||
44
+ result.contractId!==operation.contractId||!same(result.arguments,operation.arguments))refuse('INVALID_OPERATION');
45
+ return result;
46
+ };
47
+ const validateSaved=state=>{
48
+ const heads=new Map();
49
+ if(state.checkpoint!==null){
50
+ exact(state.checkpoint,['head','events']);const events=captureHistory(state.checkpoint.events),replayed=stateOf(events);
51
+ if(!same(replayed.genesis.domain,domain)||!same(replayed.head,state.checkpoint.head))throw Error('STORAGE_UNAVAILABLE');
52
+ heads.set(replayed.head.position,replayed.head);
53
+ }
54
+ const keys=new Set(),business=new Set(),effects=new Set();
55
+ for(const item of state.attempts){
56
+ exact(item,['key','fingerprint','intentId','operation','normalized','checkpointHead','preparedAt','report']);
57
+ hashId(item.key);hashId(item.fingerprint);integer(item.preparedAt);validateOperationShape(item.operation);validateReport(item.report);
58
+ exact(item.normalized,['intentId','idempotencyKey','submissionFingerprint','tool','arguments','businessKey','contractId']);
59
+ exact(item.checkpointHead,['hash','position','canonicalTime']);hashId(item.checkpointHead.hash);
60
+ integer(item.checkpointHead.position);integer(item.checkpointHead.canonicalTime);
61
+ if(!state.checkpoint||item.checkpointHead.position>state.checkpoint.head.position||
62
+ item.preparedAt<item.checkpointHead.canonicalTime)throw Error('STORAGE_UNAVAILABLE');
63
+ if(!heads.has(item.checkpointHead.position))heads.set(item.checkpointHead.position,
64
+ stateOf(state.checkpoint.events.slice(0,item.checkpointHead.position+1)).head);
65
+ if(!same(heads.get(item.checkpointHead.position),item.checkpointHead))throw Error('STORAGE_UNAVAILABLE');
66
+ if(keys.has(item.key)||item.normalized.idempotencyKey!==item.key||item.normalized.submissionFingerprint!==item.fingerprint||
67
+ !same(item.normalized,{...item.operation,idempotencyKey:item.key,submissionFingerprint:item.fingerprint})||
68
+ item.intentId!==item.operation.intentId||item.report.key!==item.key||item.report.fingerprint!==item.fingerprint||
69
+ item.report.intentId!==item.intentId||item.report.tool!==item.operation.tool||item.report.businessKey!==item.operation.businessKey||
70
+ item.report.contractId!==item.operation.contractId||item.report.checkpointHash!==item.checkpointHead.hash)throw Error('STORAGE_UNAVAILABLE');
71
+ keys.add(item.key);
72
+ }
73
+ for(const binding of state.businessKeys){exact(binding,['businessKey','key','intentId']);if(business.has(binding.businessKey)||
74
+ !state.attempts.some(item=>item.key===binding.key&&item.intentId===binding.intentId&&item.operation.businessKey===binding.businessKey))throw Error('STORAGE_UNAVAILABLE');business.add(binding.businessKey);}
75
+ if(business.size!==state.attempts.length)throw Error('STORAGE_UNAVAILABLE');
76
+ for(const effect of state.effects){
77
+ exact(effect,['effectId','key','fingerprint','tool','arguments','businessKey','contractId']);
78
+ const attempt=state.attempts.find(item=>item.key===effect.key);
79
+ if(effects.has(effect.key)||!attempt||attempt.report.state!=='APPLIED'||attempt.report.effectId!==effect.effectId||
80
+ effect.fingerprint!==attempt.fingerprint||effect.tool!==attempt.normalized.tool||
81
+ !same(effect.arguments,attempt.normalized.arguments)||effect.businessKey!==attempt.normalized.businessKey||effect.contractId!==attempt.normalized.contractId)throw Error('STORAGE_UNAVAILABLE');
82
+ effects.add(effect.key);
83
+ }
84
+ if(state.attempts.some(item=>(item.report.state==='APPLIED')!==effects.has(item.key)))throw Error('STORAGE_UNAVAILABLE');
85
+ return state;
86
+ };
87
+ try{validateSaved(store.read());}catch(error){store.close();throw error;}
88
+ const handle=body=>{
89
+ try{
90
+ const committed=store.transact(state=>{
91
+ validateSaved(state);
92
+ const before=capture(state);
93
+ try {
94
+ const {operation,payload}=body;
95
+ let result;
96
+ if(operation==='checkpoint'){
97
+ const at=clock(state);let events,replayed;
98
+ try{events=captureHistory(payload.events);replayed=stateOf(events);}catch{refuse('CHECKPOINT_CONFLICT');}
99
+ if(!same(replayed.genesis.domain,domain)||at<replayed.head.canonicalTime)refuse('CHECKPOINT_CONFLICT');
100
+ if(state.checkpoint){
101
+ const prior=state.checkpoint.events;
102
+ if(events.length<prior.length||!same(events.slice(0,prior.length),prior))refuse('CHECKPOINT_CONFLICT');
103
+ if(events.length===prior.length)return{state,result:{state:'CHECKPOINTED',head:state.checkpoint.head}};
104
+ }
105
+ state.checkpoint={events,head:replayed.head};state.lastTime=at;
106
+ result={state:'CHECKPOINTED',head:replayed.head};
107
+ }else if(operation==='prepare'){
108
+ if(!state.checkpoint)refuse('NO_CHECKPOINT');
109
+ const at=clock(state),op=payload.operation,norm=normalized(state.checkpoint.events,op,at,state);
110
+ const prior=state.attempts.find(item=>item.key===norm.idempotencyKey);
111
+ if(prior){
112
+ if(!same(prior.operation,op)||!same(prior.normalized,norm))refuse('OPERATION_CONFLICT');
113
+ if(!same(prior.checkpointHead,state.checkpoint.head))refuse('CHECKPOINT_CHANGED');
114
+ return{state,result:prior.report};
115
+ }
116
+ if(state.businessKeys.some(item=>item.businessKey===norm.businessKey))refuse('BUSINESS_KEY_CONFLICT');
117
+ if(state.attempts.length>=256)refuse('CAPACITY_EXHAUSTED');
118
+ const report={state:'PENDING',key:norm.idempotencyKey,fingerprint:norm.submissionFingerprint,
119
+ intentId:norm.intentId,tool:norm.tool,businessKey:norm.businessKey,contractId:norm.contractId,checkpointHash:state.checkpoint.head.hash};
120
+ state.attempts.push({key:norm.idempotencyKey,fingerprint:norm.submissionFingerprint,intentId:norm.intentId,
121
+ operation:op,normalized:norm,checkpointHead:state.checkpoint.head,preparedAt:at,report});
122
+ state.businessKeys.push({businessKey:norm.businessKey,key:norm.idempotencyKey,intentId:norm.intentId});
123
+ state.lastTime=at;result=report;
124
+ }else{
125
+ const prior=state.attempts.find(item=>item.key===payload.key);
126
+ if(!prior)return{state,result:{state:'UNKNOWN',key:payload.key}};
127
+ if(operation==='status')return{state,result:prior.report};
128
+ if(operation==='cancel'){
129
+ if(prior.report.state==='APPLIED')return{state,result:{state:'TOO_LATE',report:prior.report}};
130
+ if(prior.report.state==='CANCELLED')return{state,result:prior.report};
131
+ // Cancellation reduces authority. A faulty clock cannot block it
132
+ // or lower the floor used to admit future work.
133
+ try{state.lastTime=Math.max(state.lastTime,integer(now()));}catch{}
134
+ prior.report={...prior.report,state:'CANCELLED'};result=prior.report;
135
+ }else if(operation==='commit'){
136
+ if(prior.report.state!=='PENDING')return{state,result:prior.report};
137
+ if(!state.checkpoint||!same(prior.checkpointHead,state.checkpoint.head))refuse('CHECKPOINT_CHANGED');
138
+ const at=clock(state),norm=normalized(state.checkpoint.events,prior.operation,at,state);
139
+ if(!same(norm,prior.normalized))refuse('OPERATION_CONFLICT');
140
+ const effectId='effect:'+prior.key.slice(2);
141
+ state.effects.push({effectId,key:prior.key,fingerprint:prior.fingerprint,tool:norm.tool,
142
+ arguments:norm.arguments,businessKey:norm.businessKey,contractId:norm.contractId});
143
+ prior.report={...prior.report,state:'APPLIED',effectId};state.lastTime=at;result=prior.report;
144
+ }else refuse('INVALID_OPERATION');
145
+ }
146
+ validateSaved(state);return{state,result};
147
+ }catch(error){
148
+ if(!refusals.includes(error.code))throw error;
149
+ // An observed later trusted time survives a refusal. Otherwise a
150
+ // subsequent backward clock could revive an expired permission.
151
+ return{state:{...before,lastTime:Math.max(before.lastTime,state.lastTime)},result:response('REFUSED',error.code)};
152
+ }
153
+ });
154
+ return{sequence:committed.state.sequence,result:committed.result};
155
+ }catch(error){
156
+ const code=refusals.includes(error.code)?error.code:'STORAGE_UNAVAILABLE';
157
+ return{sequence:store.read().sequence,result:response('REFUSED',code)};
158
+ }
159
+ };
160
+ const sockets=new Set();let closing=false;
161
+ const server=http.createServer(async(request,res)=>{
162
+ if(request.method!=='POST'||request.url!=='/cooperative'){res.writeHead(404);res.end();return;}
163
+ try{
164
+ const envelope=decodeTransport(await requestBytes(request));
165
+ const body=verifyRequest(envelope,coordinatorPublicKey,{serviceId});
166
+ const result=handle(body);
167
+ const receipt=signResponse({version:'continuity-cooperative-response/1',serviceId,requestDigest:canonicalDigest(body),...result},servicePrivateKey);
168
+ res.writeHead(200,{'content-type':'application/json'});res.end(encodeTransport(receipt));
169
+ }catch{if(!res.destroyed){res.writeHead(400);res.end();}}
170
+ });
171
+ server.on('connection',socket=>{sockets.add(socket);socket.on('close',()=>sockets.delete(socket));});
172
+ server.requestTimeout=5000;server.headersTimeout=5000;
173
+ try{await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',()=>{server.off('error',reject);resolve();});});}
174
+ catch(error){store.close();throw error;}
175
+ return Object.freeze({
176
+ url:`http://127.0.0.1:${server.address().port}`,
177
+ inspect(){return capture(validateSaved(store.read()));},
178
+ async close(){if(closing)return;closing=true;for(const socket of sockets)socket.destroy();await new Promise(resolve=>server.close(resolve));store.close();},
179
+ });
180
+ }
@@ -0,0 +1,232 @@
1
+ /** Single-host cooperative destination store; checksums are not rollback authentication. */
2
+ import {
3
+ constants, openSync, closeSync, writeFileSync, readSync, fsyncSync, fstatSync,
4
+ lstatSync, realpathSync, readdirSync, renameSync, unlinkSync, existsSync,
5
+ } from 'node:fs';
6
+ import {join,resolve} from 'node:path';
7
+ import {hostname} from 'node:os';
8
+ import {randomUUID} from 'node:crypto';
9
+ import {types} from 'node:util';
10
+ import {encodeTransport,decodeTransport,canonicalDigest} from './wire.mjs';
11
+
12
+ const FILE_LIMIT=16*1024*1024, RECORD_LIMIT=1024;
13
+ const STATE_VERSION='continuity-cooperative-destination/1';
14
+ const SNAPSHOT_VERSION='continuity-destination-snapshot/1';
15
+ const IDENTITY_VERSION='continuity-destination-identity/1';
16
+ const LOCK_VERSION='continuity-destination-lock/1';
17
+ const STATE_KEYS=['version','domain','serviceId','coordinatorKey','serviceKey','sequence','lastTime','checkpoint','attempts','businessKeys','effects'];
18
+ const IDENTITY_KEYS=['version','domain','serviceId','coordinatorKey','serviceKey'];
19
+ const HASH=/^0x[0-9a-f]{64}$/;
20
+ const fail=code=>{throw Object.assign(new Error(code),{code})};
21
+ const check=(condition,code='DESTINATION_STATE_INVALID')=>{if(!condition)fail(code)};
22
+ const equal=(a,b)=>canonicalDigest(a)===canonicalDigest(b);
23
+ const exact=(value,keys)=>value!==null&&typeof value==='object'&&!Array.isArray(value)&&
24
+ Object.keys(value).length===keys.length&&keys.every(key=>Object.hasOwn(value,key));
25
+ const integer=value=>Number.isSafeInteger(value)&&value>=0&&!Object.is(value,-0);
26
+ const identifier=value=>typeof value==='string'&&value.length>0&&value.length<=256&&!/[\u0000-\u001f\u007f]/u.test(value);
27
+ const capture=value=>decodeTransport(encodeTransport(value));
28
+ const identityOf=state=>capture(Object.fromEntries(IDENTITY_KEYS.map(key=>[key,state[key]])));
29
+
30
+ function checkedDirectory(value) {
31
+ check(typeof value==='string'&&value.length>0,'DIRECTORY_INVALID');
32
+ const path=resolve(value);
33
+ check(realpathSync(path)===path&&lstatSync(path).isDirectory(),'DIRECTORY_INVALID');
34
+ return path;
35
+ }
36
+ function syncDirectory(path) {
37
+ const fd=openSync(path,constants.O_RDONLY|constants.O_DIRECTORY|constants.O_NOFOLLOW);
38
+ try {fsyncSync(fd)} finally {closeSync(fd)}
39
+ }
40
+ function readFile(path,maximum=FILE_LIMIT) {
41
+ const before=lstatSync(path);
42
+ check(before.isFile()&&!before.isSymbolicLink()&&before.nlink===1&&before.size<=maximum,'DESTINATION_FILE_INVALID');
43
+ const fd=openSync(path,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);
44
+ try {
45
+ const opened=fstatSync(fd);
46
+ check(opened.isFile()&&opened.nlink===1&&opened.dev===before.dev&&opened.ino===before.ino&&opened.size<=maximum,'DESTINATION_FILE_INVALID');
47
+ const buffer=Buffer.alloc(opened.size+1);let offset=0;
48
+ while(offset<buffer.length){const count=readSync(fd,buffer,offset,buffer.length-offset,null);if(!count)break;offset+=count}
49
+ const after=fstatSync(fd),named=lstatSync(path);
50
+ check(offset===opened.size&&after.size===opened.size&&after.mtimeMs===opened.mtimeMs&&after.ctimeMs===opened.ctimeMs&&
51
+ after.nlink===1&&!named.isSymbolicLink()&&named.dev===opened.dev&&named.ino===opened.ino,'DESTINATION_FILE_CHANGED');
52
+ const bytes=buffer.subarray(0,offset),value=decodeTransport(bytes);
53
+ // Only the writer's exact serialization is a valid stored record.
54
+ check(bytes.equals(encodeTransport(value)),'DESTINATION_ENCODING_INVALID');
55
+ return value;
56
+ } finally {closeSync(fd)}
57
+ }
58
+ function createFile(path,value,maximum=FILE_LIMIT) {
59
+ const bytes=encodeTransport(value);check(bytes.length<=maximum,'DESTINATION_FILE_LIMIT');
60
+ const fd=openSync(path,constants.O_WRONLY|constants.O_CREAT|constants.O_EXCL|constants.O_NOFOLLOW,0o600);
61
+ try {writeFileSync(fd,bytes);fsyncSync(fd)} finally {closeSync(fd)}
62
+ }
63
+ function identityEnvelope(identity) {
64
+ const body={version:IDENTITY_VERSION,identity};
65
+ return {...body,checksum:canonicalDigest(body)};
66
+ }
67
+ function readIdentity(path) {
68
+ const value=readFile(path,65536);
69
+ check(exact(value,['version','identity','checksum'])&&value.version===IDENTITY_VERSION&&
70
+ exact(value.identity,IDENTITY_KEYS)&&value.checksum===canonicalDigest({version:value.version,identity:value.identity}),'DESTINATION_IDENTITY_INVALID');
71
+ return value.identity;
72
+ }
73
+ function validateState(input,identity) {
74
+ const state=capture(input);
75
+ check(exact(state,STATE_KEYS)&&state.version===STATE_VERSION&&integer(state.sequence)&&integer(state.lastTime));
76
+ check(exact(state.domain,['protocol','version','deploymentId','chainId','verifyingContract'])&&
77
+ state.domain.protocol==='continuity'&&state.domain.version==='0.2'&&identifier(state.domain.deploymentId)&&
78
+ typeof state.domain.chainId==='string'&&/^[1-9][0-9]*$/.test(state.domain.chainId)&&state.domain.chainId.length<=78&&
79
+ typeof state.domain.verifyingContract==='string'&&/^0x[0-9a-fA-F]{40}$/.test(state.domain.verifyingContract));
80
+ check(identifier(state.serviceId)&&typeof state.coordinatorKey==='string'&&HASH.test(state.coordinatorKey)&&
81
+ typeof state.serviceKey==='string'&&HASH.test(state.serviceKey));
82
+ if(identity)check(equal(identityOf(state),identity),'DESTINATION_IDENTITY_MISMATCH');
83
+ for(const key of ['attempts','businessKeys','effects'])check(Array.isArray(state[key])&&state[key].length<=RECORD_LIMIT,'DESTINATION_RECORD_LIMIT');
84
+ if(state.checkpoint!==null){
85
+ check(exact(state.checkpoint,['head','events'])&&Array.isArray(state.checkpoint.events)&&state.checkpoint.events.length>0&&state.checkpoint.events.length<=256);
86
+ const head=state.checkpoint.head;
87
+ check(exact(head,['hash','position','canonicalTime'])&&typeof head.hash==='string'&&HASH.test(head.hash)&&integer(head.position)&&integer(head.canonicalTime)&&
88
+ head.position===state.checkpoint.events.length-1&&head.canonicalTime<=state.lastTime);
89
+ }
90
+ const attemptKeys=new Set(),businessKeys=new Set(),effectKeys=new Set();
91
+ for(const attempt of state.attempts){
92
+ check(exact(attempt,['key','fingerprint','intentId','operation','normalized','checkpointHead','preparedAt','report']));
93
+ check(typeof attempt.key==='string'&&HASH.test(attempt.key)&&typeof attempt.fingerprint==='string'&&HASH.test(attempt.fingerprint)&&
94
+ identifier(attempt.intentId)&&integer(attempt.preparedAt)&&attempt.preparedAt<=state.lastTime&&!attemptKeys.has(attempt.key));
95
+ attemptKeys.add(attempt.key);
96
+ }
97
+ for(const record of state.businessKeys){
98
+ check(exact(record,['businessKey','key','intentId'])&&identifier(record.businessKey)&&typeof record.key==='string'&&HASH.test(record.key)&&
99
+ identifier(record.intentId)&&!businessKeys.has(record.businessKey)&&attemptKeys.has(record.key));
100
+ businessKeys.add(record.businessKey);
101
+ }
102
+ for(const effect of state.effects){
103
+ check(exact(effect,['effectId','key','fingerprint','tool','arguments','businessKey','contractId'])&&identifier(effect.effectId)&&
104
+ typeof effect.key==='string'&&HASH.test(effect.key)&&typeof effect.fingerprint==='string'&&HASH.test(effect.fingerprint)&&
105
+ identifier(effect.tool)&&identifier(effect.businessKey)&&identifier(effect.contractId)&&!effectKeys.has(effect.key)&&attemptKeys.has(effect.key));
106
+ effectKeys.add(effect.key);
107
+ }
108
+ return state;
109
+ }
110
+ function monotonic(previous,next) {
111
+ check(next.sequence===previous.sequence,'DESTINATION_SEQUENCE_OWNED_BY_STORE');
112
+ check(next.lastTime>=previous.lastTime,'DESTINATION_TIME_ROLLBACK');
113
+ for(const key of ['attempts','businessKeys','effects'])check(next[key].length>=previous[key].length,'DESTINATION_PRUNING_FORBIDDEN');
114
+ for(let index=0;index<previous.attempts.length;index++){
115
+ const {report:oldReport,...oldIdentity}=previous.attempts[index];
116
+ const {report:newReport,...newIdentity}=next.attempts[index];
117
+ check(equal(oldIdentity,newIdentity),'DESTINATION_ATTEMPT_IDENTITY_CHANGED');
118
+ if(oldReport?.state==='APPLIED'||oldReport?.state==='CANCELLED')check(equal(oldReport,newReport),'DESTINATION_TERMINAL_CHANGED');
119
+ }
120
+ for(const key of ['businessKeys','effects'])for(let index=0;index<previous[key].length;index++)check(equal(previous[key][index],next[key][index]),'DESTINATION_RETAINED_RECORD_CHANGED');
121
+ if(previous.checkpoint!==null){
122
+ check(next.checkpoint!==null&&next.checkpoint.events.length>=previous.checkpoint.events.length,'DESTINATION_CHECKPOINT_ROLLBACK');
123
+ for(let index=0;index<previous.checkpoint.events.length;index++)check(equal(previous.checkpoint.events[index],next.checkpoint.events[index]),'DESTINATION_CHECKPOINT_FORK');
124
+ }
125
+ }
126
+ function snapshotEnvelope(state,identityHash) {
127
+ const body={version:SNAPSHOT_VERSION,identityHash,state};
128
+ return {...body,checksum:canonicalDigest(body)};
129
+ }
130
+ function loadSnapshot(path,identity) {
131
+ const value=readFile(path);
132
+ check(exact(value,['version','identityHash','state','checksum'])&&value.version===SNAPSHOT_VERSION&&
133
+ value.identityHash===canonicalDigest(identity)&&value.checksum===canonicalDigest({version:value.version,identityHash:value.identityHash,state:value.state}),'DESTINATION_INTEGRITY_INVALID');
134
+ return validateState(value.state,identity);
135
+ }
136
+ function lockRecord() {return {version:LOCK_VERSION,hostname:hostname(),pid:process.pid,instance:randomUUID()}}
137
+ function validateLock(value) {
138
+ check(exact(value,['version','hostname','pid','instance'])&&value.version===LOCK_VERSION&&identifier(value.hostname)&&
139
+ Number.isSafeInteger(value.pid)&&value.pid>0&&typeof value.instance==='string'&&/^[0-9a-f-]{36}$/.test(value.instance),'DESTINATION_LOCK_INVALID');
140
+ return value;
141
+ }
142
+ function assertOwnLock(path,lock) {check(equal(validateLock(readFile(path,65536)),lock),'DESTINATION_LOCK_CHANGED')}
143
+
144
+ /** Read-only evidence for a separate, explicit operator recovery step. */
145
+ export function inspectDestinationLock({directory}) {
146
+ return validateLock(readFile(join(checkedDirectory(directory),'destination.lock'),65536));
147
+ }
148
+
149
+ /** Exact lock token + same host + OS-confirmed absent PID. Never age-based. */
150
+ export function recoverDestinationLock({directory,expectedLock}) {
151
+ const root=checkedDirectory(directory),expected=validateLock(capture(expectedLock));
152
+ check(expected.hostname===hostname(),'DESTINATION_FOREIGN_LOCK');
153
+ const lockPath=join(root,'destination.lock'),recoveryPath=join(root,'recovery.lock'),recovery=lockRecord();
154
+ try{createFile(recoveryPath,recovery,65536);syncDirectory(root)}catch(error){if(error.code==='EEXIST')fail('DESTINATION_RECOVERY_LOCKED');throw error}
155
+ try{
156
+ assertOwnLock(lockPath,expected);
157
+ let absent=false;
158
+ try{process.kill(expected.pid,0)}catch(error){if(error.code==='ESRCH')absent=true;else fail('DESTINATION_LOCK_OWNER_UNVERIFIED')}
159
+ check(absent,'DESTINATION_LOCK_OWNER_ALIVE');
160
+ assertOwnLock(lockPath,expected);
161
+ const retained=join(root,`recovered-${expected.instance}.lock`);
162
+ check(!existsSync(retained),'DESTINATION_RECOVERY_EVIDENCE_EXISTS');
163
+ renameSync(lockPath,retained);syncDirectory(root);
164
+ return Object.freeze({recovered:true,retainedLock:retained,lock:expected});
165
+ } finally {
166
+ assertOwnLock(recoveryPath,recovery);unlinkSync(recoveryPath);syncDirectory(root);
167
+ }
168
+ }
169
+
170
+ /** One lifetime lock, no pruning or implicit unlock/reinitialization. */
171
+ export function openDestinationStore({directory,initial}) {
172
+ const root=checkedDirectory(directory),initialState=validateState(initial);
173
+ check(initialState.sequence===0&&initialState.lastTime===0&&initialState.checkpoint===null&&
174
+ initialState.attempts.length===0&&initialState.businessKeys.length===0&&initialState.effects.length===0,'DESTINATION_INITIAL_INVALID');
175
+ const identity=identityOf(initialState),lock=lockRecord();
176
+ const lockPath=join(root,'destination.lock'),identityPath=join(root,'identity.bin'),snapshotPath=join(root,'snapshot.bin');
177
+ check(!existsSync(join(root,'recovery.lock')),'DESTINATION_RECOVERY_LOCKED');
178
+ const before=readdirSync(root);
179
+ try {createFile(lockPath,lock,65536);syncDirectory(root)}catch(error){if(error.code==='EEXIST')fail('DESTINATION_LOCKED');throw error}
180
+ let closed=false,busy=false,poisoned=false,current;
181
+ const release=()=>{assertOwnLock(lockPath,lock);unlinkSync(lockPath);syncDirectory(root)};
182
+ const writeSnapshot=state=>{
183
+ const temporary=join(root,`snapshot-${randomUUID()}.tmp`);
184
+ createFile(temporary,snapshotEnvelope(state,canonicalDigest(identity)));
185
+ renameSync(temporary,snapshotPath);syncDirectory(root);
186
+ };
187
+ try{
188
+ check(!existsSync(join(root,'recovery.lock')),'DESTINATION_RECOVERY_LOCKED');
189
+ const hasIdentity=existsSync(identityPath),hasSnapshot=existsSync(snapshotPath);
190
+ if(!hasIdentity&&!hasSnapshot){
191
+ check(before.length===0,'DESTINATION_INITIALIZATION_REFUSED');
192
+ // Persist the identity first: interruption may block availability, never silently reset history.
193
+ poisoned=true;createFile(identityPath,identityEnvelope(identity),65536);syncDirectory(root);
194
+ writeSnapshot(initialState);poisoned=false;
195
+ } else check(hasIdentity&&hasSnapshot,'DESTINATION_STATE_MISSING');
196
+ check(equal(readIdentity(identityPath),identity),'DESTINATION_IDENTITY_MISMATCH');
197
+ current=loadSnapshot(snapshotPath,identity);
198
+ }catch(error){if(!poisoned)release();throw error}
199
+ const active=()=>{check(!closed,'DESTINATION_STORE_CLOSED');check(!poisoned,'DESTINATION_STORE_UNCERTAIN');check(!busy,'DESTINATION_STORE_BUSY')};
200
+ const verify=()=>{
201
+ try{
202
+ assertOwnLock(lockPath,lock);check(equal(readIdentity(identityPath),identity),'DESTINATION_IDENTITY_MISMATCH');
203
+ const stored=loadSnapshot(snapshotPath,identity);
204
+ check(equal(stored,current),'DESTINATION_EXTERNAL_STATE_CHANGE');return stored;
205
+ }catch(error){poisoned=true;throw error}
206
+ };
207
+ return Object.freeze({
208
+ read(){active();return verify()},
209
+ transact(fn){
210
+ active();check(typeof fn==='function','DESTINATION_TRANSACTION_INVALID');
211
+ const previous=verify();busy=true;
212
+ try{
213
+ const answer=fn(structuredClone(previous));
214
+ check(!types.isPromise(answer),'DESTINATION_ASYNC_TRANSACTION');
215
+ const captured=capture(answer);
216
+ check(exact(captured,['state','result']),'DESTINATION_TRANSACTION_INVALID');
217
+ let next=validateState(captured.state,identity);monotonic(previous,next);
218
+ if(equal(previous,next))return Object.freeze({state:previous,result:captured.result});
219
+ check(previous.sequence<Number.MAX_SAFE_INTEGER,'DESTINATION_SEQUENCE_LIMIT');
220
+ next=validateState({...next,sequence:previous.sequence+1},identity);
221
+ try{writeSnapshot(next);current=next}catch(error){poisoned=true;throw Object.assign(new Error('DESTINATION_COMMIT_UNCERTAIN'),{code:'DESTINATION_COMMIT_UNCERTAIN',cause:error})}
222
+ return Object.freeze({state:current,result:captured.result});
223
+ }finally{busy=false}
224
+ },
225
+ close(){
226
+ if(closed)return;
227
+ check(!busy,'DESTINATION_STORE_BUSY');closed=true;
228
+ // An uncertain write requires explicit recovery after the owning process is gone.
229
+ if(!poisoned)release();
230
+ },
231
+ });
232
+ }
@@ -0,0 +1,67 @@
1
+ import {createHash} from 'node:crypto';
2
+ import * as core from '@ramex-labs/continuity/adapter';
3
+ import {PortableFileEventStore} from '@ramex-labs/continuity/adapter';
4
+ import {openLocalExecution} from '@ramex-labs/continuity/adapter';
5
+ import {record,identifier,requireCondition} from '@ramex-labs/continuity/adapter';
6
+
7
+ /** Trusted application factory. Model arguments cannot select identity or client. */
8
+ export function createCooperativeExecutor({local,client,registry,role,tenure}) {
9
+ requireCondition(local.additionalPolicy === undefined, "UNSUPPORTED_ADDITIONAL_POLICY");
10
+ const store=new PortableFileEventStore(local.historyFile);
11
+ const profile=core.approvedPortableAdapterProfileForPolicy(core.PORTABLE_ADAPTER_POLICY_E5_HASH,core.REMOTE_SERVICE_REPORT_ADAPTER_ID);
12
+ role=identifier(role);tenure=identifier(tenure);
13
+ const select=input=>{
14
+ const value=record(input,['operationId','businessKey','tool','arguments']);
15
+ const selected=registry.capture({intentId:identifier(value.operationId),businessKey:value.businessKey,
16
+ tool:value.tool,arguments:value.arguments,contractId:registry.contractId});
17
+ return {...selected,op:{id:selected.wire.intentId,action:selected.action,resource:selected.resource,role,tenure,termsCommitment:selected.termsCommitment,...selected.quantities}};
18
+ };
19
+ const retained=new Map();
20
+ const adapterFor=selected=>{
21
+ const unknown=i=>({status:'OUTCOME_UNKNOWN',idempotencyKey:i.idempotencyKey,submissionFingerprint:i.submissionFingerprint});
22
+ const convert=(reply,i,status)=>{
23
+ const report=reply.result;
24
+ if(report.state!=='APPLIED') {
25
+ requireCondition(report.key === undefined || report.key === i.idempotencyKey, "REPORT_MISMATCH");
26
+ retained.set(i.intentId,reply);
27
+ return unknown(i);
28
+ }
29
+ requireCondition(report.key===i.idempotencyKey&&report.fingerprint===i.submissionFingerprint&&report.intentId===i.intentId&&
30
+ report.tool===selected.wire.tool&&report.businessKey===selected.wire.businessKey&&report.contractId===registry.contractId,'REPORT_MISMATCH');
31
+ retained.set(i.intentId,reply);
32
+ const digest='0x'+createHash('sha256').update(core.canonicalEncode(report)).digest('hex');
33
+ const acknowledgment=core.createRemoteServiceReportAcknowledgment(i,digest);
34
+ const evidence=core.portableAdapterAcknowledgmentEvidence(i,acknowledgment);
35
+ return {status,idempotencyKey:i.idempotencyKey,submissionFingerprint:i.submissionFingerprint,
36
+ acknowledgment,...(status==='RETRY'?{retainedEvidence:evidence}:{evidence})};
37
+ };
38
+ return {adapterProfile:profile,
39
+ async submit(submission) {
40
+ const i=core.derivePortableAdapterIdentity(submission);
41
+ try {
42
+ const checkpoint=await client.checkpoint(store.readAll());
43
+ if(checkpoint.result.state!=='CHECKPOINTED')return unknown(i);
44
+ const prepared=await client.prepare(selected.wire);
45
+ if(prepared.result.state==='APPLIED')return convert(prepared,i,'RETRY');
46
+ if(prepared.result.state!=='PENDING')return convert(prepared,i,'RETRY');
47
+ return convert(await client.commit(i.idempotencyKey),i,'SUBMITTED');
48
+ } catch {return unknown(i);}
49
+ },
50
+ async reconcile(submission) {
51
+ const i=core.derivePortableAdapterIdentity(submission);
52
+ try {return convert(await client.status(i.idempotencyKey),i,'RETRY');}
53
+ catch {return unknown(i);}
54
+ },
55
+ };
56
+ };
57
+ return Object.freeze({
58
+ async run(input) {
59
+ const selected=select(input);
60
+ const execution=await openLocalExecution(local,adapterFor(selected),'REMOTE_REPORT').run(selected.op);
61
+ return Object.freeze({execution,serviceReport:retained.get(selected.wire.intentId)??null,
62
+ externalOutcome:'NOT_PROVEN',revocationBoundary:'DESTINATION_ACKNOWLEDGED_CHECKPOINT'});
63
+ },
64
+ // Publishing a checkpoint is an owner/application action, not an agent tool.
65
+ checkpoint(){return client.checkpoint(store.readAll());},
66
+ });
67
+ }
@@ -0,0 +1,106 @@
1
+ import type {KeyObject} from 'node:crypto';
2
+ import type {PortableAuthorizationDomain, PortableCanonicalEvent, PortableHistoryHead, RemoteServiceReportAcknowledgment} from '@ramex-labs/continuity/adapter';
3
+ import type {LocalExecutionOptions, openLocalExecution} from '@ramex-labs/continuity/adapter';
4
+
5
+ export type Hash = `0x${string}`;
6
+ export type Data = null | boolean | number | string | bigint | readonly Data[] | {readonly [key: string]: Data};
7
+ export type Arguments = Readonly<Record<string, Data>>;
8
+ export type Field = 'text' | 'integer' | 'boolean' | 'amount' | 'identifier' | readonly string[];
9
+ export type ToolDefinition = Readonly<{id: string; action: string; resource: string; fields: Readonly<Record<string, Field>>;
10
+ projection?: Readonly<{amount?: string; counterparty?: string; unit?: string}>;
11
+ compensates?: Readonly<{tool: string; businessKeyField: string}>}>;
12
+ export type Operation = Readonly<{intentId: string; tool: string; arguments: Arguments; businessKey: string; contractId: string}>;
13
+ export type NormalizedOperation = Operation & Readonly<{idempotencyKey: Hash; submissionFingerprint: Hash}>;
14
+ export type ReportIdentity = Readonly<{key: Hash; fingerprint: Hash; intentId: string; tool: string; businessKey: string; contractId: string; checkpointHash: Hash}>;
15
+ export type PendingReport = ReportIdentity & Readonly<{state: 'PENDING'}>;
16
+ export type CancelledReport = ReportIdentity & Readonly<{state: 'CANCELLED'}>;
17
+ export type AppliedReport = ReportIdentity & Readonly<{state: 'APPLIED'; effectId: string}>;
18
+ export type AttemptReport = PendingReport | CancelledReport | AppliedReport;
19
+ export type RefusalCode = 'NO_CHECKPOINT' | 'CHECKPOINT_CONFLICT' | 'CHECKPOINT_CHANGED' | 'OPERATION_CONFLICT' |
20
+ 'BUSINESS_KEY_CONFLICT' | 'AUTHORIZATION_REFUSED' | 'CLOCK_INVALID' | 'CAPACITY_EXHAUSTED' | 'INVALID_OPERATION' | 'STORAGE_UNAVAILABLE';
21
+ export type Refusal = Readonly<{state: 'REFUSED'; code: RefusalCode}>;
22
+ export type UnknownReport = Readonly<{state: 'UNKNOWN'; key: Hash}>;
23
+ export type CheckpointReport = Readonly<{state: 'CHECKPOINTED'; head: PortableHistoryHead}>;
24
+ export type TooLateReport = Readonly<{state: 'TOO_LATE'; report: AppliedReport}>;
25
+ export type ServiceResult = AttemptReport | Refusal | UnknownReport | CheckpointReport | TooLateReport;
26
+ export type SignedResponse<R extends ServiceResult = ServiceResult> = Readonly<{
27
+ body: Readonly<{version: 'continuity-cooperative-response/1'; serviceId: string; requestDigest: Hash; sequence: number; result: R}>;
28
+ signature: string;
29
+ }>;
30
+ export type CooperativeReply<R extends ServiceResult = ServiceResult> = Readonly<{sequence: number; result: R; receipt: SignedResponse<R>}>;
31
+
32
+ export interface ToolRegistry {
33
+ readonly contractId: Hash;
34
+ readonly serviceId: string;
35
+ readonly account: string;
36
+ readonly tools: readonly ToolDefinition[];
37
+ capture(operation: Operation): Readonly<{wire: Operation; action: string; resource: string; termsCommitment: Hash; quantities: Readonly<{amount?: bigint; counterparty?: string}>}>;
38
+ validateOperation(events: readonly PortableCanonicalEvent[], operation: Operation, now: number, destinationState?: DestinationState): NormalizedOperation;
39
+ }
40
+ export declare const REFERENCE_TOOLS: readonly ToolDefinition[];
41
+ export declare function createToolRegistry(options: Readonly<{serviceId: string; account: string; tools: readonly ToolDefinition[]}>): ToolRegistry;
42
+
43
+ export interface CooperativeClient {
44
+ checkpoint(events: readonly PortableCanonicalEvent[]): Promise<CooperativeReply<CheckpointReport | Refusal>>;
45
+ prepare(operation: Operation): Promise<CooperativeReply<AttemptReport | Refusal>>;
46
+ commit(key: Hash): Promise<CooperativeReply<AttemptReport | UnknownReport | Refusal>>;
47
+ status(key: Hash): Promise<CooperativeReply<AttemptReport | UnknownReport | Refusal>>;
48
+ cancel(key: Hash): Promise<CooperativeReply<AttemptReport | UnknownReport | TooLateReport | Refusal>>;
49
+ inspectResponse(envelope: unknown, expectedRequestDigest: Hash): CooperativeReply;
50
+ }
51
+ /** Trusted application keys and pinned destination; never expose this whole handle to an agent. */
52
+ export declare function createCooperativeClient(options: Readonly<{
53
+ url: string; serviceId: string; coordinatorPrivateKey: KeyObject; servicePublicKey: KeyObject; timeoutMs?: number;
54
+ }>): CooperativeClient;
55
+
56
+ export type StoredAttempt = Readonly<{
57
+ key: Hash; fingerprint: Hash; intentId: string; operation: Operation; normalized: NormalizedOperation;
58
+ checkpointHead: PortableHistoryHead; preparedAt: number; report: AttemptReport;
59
+ }>;
60
+ export type DestinationState = Readonly<{
61
+ version: 'continuity-cooperative-destination/1'; domain: PortableAuthorizationDomain; serviceId: string;
62
+ coordinatorKey: Hash; serviceKey: Hash; sequence: number; lastTime: number;
63
+ checkpoint: null | Readonly<{head: PortableHistoryHead; events: readonly PortableCanonicalEvent[]}>;
64
+ attempts: readonly StoredAttempt[];
65
+ businessKeys: readonly Readonly<{businessKey: string; key: Hash; intentId: string}>[];
66
+ effects: readonly Readonly<{effectId: string; key: Hash; fingerprint: Hash; tool: string; arguments: Arguments; businessKey: string; contractId: string}>[];
67
+ }>;
68
+ export interface CooperativeDestination {
69
+ readonly url: string;
70
+ inspect(): DestinationState;
71
+ close(): Promise<void>;
72
+ }
73
+ /** Local synthetic destination: no real external effect or arbitrary execution callback. */
74
+ export declare function createCooperativeDestination(options: Readonly<{
75
+ directory: string; domain: PortableAuthorizationDomain; serviceId: string;
76
+ coordinatorPublicKey: KeyObject; servicePrivateKey: KeyObject; now: () => number;
77
+ validateOperation: (events: readonly PortableCanonicalEvent[], operation: Operation, now: number, destinationState?: DestinationState) => NormalizedOperation;
78
+ }>): Promise<CooperativeDestination>;
79
+
80
+ export type ToolRequest = Readonly<{operationId: string; businessKey: string; tool: string; arguments: Arguments}>;
81
+ export type ExecutionResult = Awaited<ReturnType<ReturnType<typeof openLocalExecution>['run']>>;
82
+ export type CooperativeExecutionResult = Readonly<{
83
+ execution: ExecutionResult;
84
+ serviceReport: CooperativeReply | null;
85
+ externalOutcome: 'NOT_PROVEN';
86
+ revocationBoundary: 'DESTINATION_ACKNOWLEDGED_CHECKPOINT';
87
+ }>;
88
+ export interface CooperativeExecutor {
89
+ run(request: ToolRequest): Promise<CooperativeExecutionResult>;
90
+ checkpoint(): Promise<CooperativeReply<CheckpointReport | Refusal>>;
91
+ }
92
+ /** Root-selected local identity and policy; request data cannot choose them. */
93
+ export declare function createCooperativeExecutor(options: Readonly<{
94
+ local: Omit<LocalExecutionOptions, 'additionalPolicy'> & Readonly<{additionalPolicy?: never}>;
95
+ client: CooperativeClient; registry: ToolRegistry; role: string; tenure: string;
96
+ }>): CooperativeExecutor;
97
+
98
+ export type RecoveryResult = Readonly<{serviceReport: CooperativeReply; observationAcknowledgment: RemoteServiceReportAcknowledgment | null; externalOutcome: 'NOT_PROVEN';
99
+ dispatchPerformed: false; retryPolicy: 'NO_AUTOMATIC_REDELIVERY'}>;
100
+ export interface CooperativeRecovery {lookup(request: ToolRequest): Promise<RecoveryResult>; cancel(request: ToolRequest): Promise<RecoveryResult>}
101
+ export declare function createCooperativeRecovery(options: Readonly<{local: Pick<LocalExecutionOptions,'historyFile'|'domain'>;
102
+ client: CooperativeClient; registry: ToolRegistry}>): CooperativeRecovery;
103
+ export type DestinationLock = Readonly<{version: 'continuity-destination-lock/1'; hostname: string; pid: number; instance: string}>;
104
+ export declare function inspectDestinationLock(options: Readonly<{directory: string}>): DestinationLock;
105
+ export declare function recoverDestinationLock(options: Readonly<{directory: string; expectedLock: DestinationLock}>):
106
+ Readonly<{recovered: true; retainedLock: string; lock: DestinationLock}>;
@@ -0,0 +1,7 @@
1
+ export {createToolRegistry, REFERENCE_TOOLS} from './validation.mjs';
2
+ export {createCooperativeClient} from './client.mjs';
3
+ export {createCooperativeDestination} from './destination.mjs';
4
+ export {createCooperativeExecutor} from './executor.mjs';
5
+
6
+ export {createCooperativeRecovery} from './recovery.mjs';
7
+ export {inspectDestinationLock,recoverDestinationLock} from './durable-store.mjs';
@@ -0,0 +1,7 @@
1
+ import type {DynamicStructuredTool} from '@langchain/core/tools';
2
+ import type {ToolRegistry,CooperativeExecutor} from './index.mjs';
3
+ /** The application fixes identity. Only the configured arguments become model input. */
4
+ export declare function createContinuityTool(options: Readonly<{registry: ToolRegistry; executor: CooperativeExecutor;
5
+ tool: string; operationId: string; businessKey: string; name?: string}>): DynamicStructuredTool;
6
+ export declare function createContinuityTools(options: Readonly<{registry: ToolRegistry; executor: CooperativeExecutor;
7
+ operations: readonly Readonly<{tool: string; operationId: string; businessKey: string; name?: string}>[]}>): readonly DynamicStructuredTool[];