@letterblack/lbe-sdk 0.4.2 → 0.4.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/dist/index.js CHANGED
@@ -1,678 +1,12 @@
1
- import crypto from'crypto';import fs from'fs';import os from'os';import path from'path';
2
- import nacl from'tweetnacl';import{canonicalize}from'json-canonicalize';
3
- import{runValidationPipeline,checkNonce,checkRateLimit,computeAuditHash,classifyRisk,shouldRollback}from'./engine.js';
4
-
5
- // ── Ed25519 sign / verify ─────────────────────────────────────────────────────
6
- function b64d(s){return Buffer.from(s,'base64')}
7
- function b64e(b){return Buffer.from(b).toString('base64')}
8
- function verifyEd25519({payloadObj,sigB64,pubKeyB64}){
9
- try{
10
- let msg=Buffer.from(canonicalize(payloadObj),'utf8');
11
- let ok=nacl.sign.detached.verify(new Uint8Array(msg),new Uint8Array(b64d(sigB64)),new Uint8Array(b64d(pubKeyB64)));
12
- return{valid:ok,message:ok?'Signature verified':'Signature verification failed'}
13
- }catch(e){return{valid:false,message:`Signature verification error: ${e.message}`}}
14
- }
15
- function generateKeyPair(){let k=nacl.sign.keyPair();return{publicKey:b64e(k.publicKey),secretKey:b64e(k.secretKey)}}
16
- function signPayload({payloadObj,secretKeyB64}){
17
- try{
18
- let msg=Buffer.from(canonicalize(payloadObj),'utf8');
19
- let sig=nacl.sign.detached(new Uint8Array(msg),new Uint8Array(b64d(secretKeyB64)));
20
- return{signature:b64e(sig),error:null}
21
- }catch(e){return{signature:null,error:`Signing failed: ${e.message}`}}
22
- }
23
-
24
- // ── Atomic file write ─────────────────────────────────────────────────────────
25
- var LOCK_OPTS={timeoutMs:5000,pollMs:15,staleMs:30000};
26
- function lockPath(p){return p+'.lock'}
27
- function tryLock(p){
28
- try{let fd=fs.openSync(p,'wx');fs.writeSync(fd,`pid:${process.pid}:${Date.now()}`);fs.closeSync(fd);return true}
29
- catch(e){if(e.code==='EEXIST'||e.code==='EPERM'||e.code==='EBUSY'||e.code==='EACCES')return false;throw e}
30
- }
31
- function stalePurge(p,ms){try{if(Date.now()-fs.statSync(p).mtimeMs>ms)fs.unlinkSync(p)}catch{}}
32
- function busySleep(ms){let end=Date.now()+ms;while(Date.now()<end)try{Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,Math.max(1,end-Date.now()))}catch{}}
33
- function withLock(p,opts,fn){
34
- let{timeoutMs,pollMs,staleMs}={...LOCK_OPTS,...(typeof opts==='function'?{}:opts)};
35
- let cb=typeof opts==='function'?opts:fn;
36
- let lp=lockPath(p);fs.mkdirSync(path.dirname(p),{recursive:true});
37
- let deadline=Date.now()+timeoutMs,acquired=false;
38
- while(!acquired){acquired=tryLock(lp);if(!acquired){if(Date.now()>=deadline){stalePurge(lp,staleMs);acquired=tryLock(lp);if(acquired)break;let e=new Error(`withLock: timeout ${lp}`);e.code='ELOCKTIMEOUT';throw e}stalePurge(lp,staleMs);busySleep(pollMs+Math.floor(Math.random()*pollMs))}}
39
- try{return cb()}finally{try{fs.unlinkSync(lp)}catch{}}
40
- }
41
- function atomicWrite(p,data,opts={}){
42
- fs.mkdirSync(path.dirname(p),{recursive:true});
43
- let tmp=path.join(path.dirname(p),`.tmp-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`);
44
- try{fs.writeFileSync(tmp,data,opts);fs.renameSync(tmp,p)}
45
- catch(e){try{fs.existsSync(tmp)&&fs.unlinkSync(tmp)}catch{}throw e}
46
- }
47
- function readJson(p){
48
- try{if(!fs.existsSync(p))return null;return JSON.parse(fs.readFileSync(p,'utf8'))}
49
- catch{return null}
50
- }
51
-
52
- // ── Flag extraction ───────────────────────────────────────────────────────────
53
- // Only field presence + format checks. No governance logic.
54
-
55
- function schemaFlags(cmd){
56
- let has=k=>cmd!=null&&Object.prototype.hasOwnProperty.call(cmd,k);
57
- let isStr=v=>typeof v==='string';
58
- let p=cmd?.payload,sig=cmd?.signature;
59
- return{
60
- hasId:has('id'),idValid:isStr(cmd?.id)&&/^[A-Z_]+$/.test(cmd.id)&&cmd.id.length>=1&&cmd.id.length<=50,
61
- hasCommandId:has('commandId'),commandIdValid:isStr(cmd?.commandId)&&/^[a-f0-9-]+$/.test(cmd.commandId)&&cmd.commandId.length===36,
62
- hasRequesterId:has('requesterId'),requesterIdValid:isStr(cmd?.requesterId)&&cmd.requesterId.length>=3&&cmd.requesterId.length<=100,
63
- hasSessionId:has('sessionId'),sessionIdValid:isStr(cmd?.sessionId)&&cmd.sessionId.length>=3,
64
- hasTimestamp:has('timestamp'),timestampValid:typeof cmd?.timestamp==='number'&&cmd.timestamp>=1e9,
65
- hasNonce:has('nonce'),nonceValid:isStr(cmd?.nonce)&&cmd.nonce.length>=32&&cmd.nonce.length<=128,
66
- hasRequires:has('requires'),requiresValid:Array.isArray(cmd?.requires)&&cmd.requires.length>=1&&cmd.requires.every(isStr),
67
- hasPayload:has('payload')&&typeof p==='object'&&p!==null&&!Array.isArray(p),
68
- hasPayloadAdapter:p!=null&&Object.prototype.hasOwnProperty.call(p,'adapter'),payloadAdapterValid:isStr(p?.adapter),
69
- hasSignature:has('signature')&&typeof sig==='object'&&sig!==null&&!Array.isArray(sig),
70
- hasSignatureAlg:sig!=null&&Object.prototype.hasOwnProperty.call(sig,'alg'),signatureAlgValid:sig?.alg==='ed25519',
71
- hasSignatureKeyId:sig!=null&&Object.prototype.hasOwnProperty.call(sig,'keyId'),
72
- hasSignatureSig:sig!=null&&Object.prototype.hasOwnProperty.call(sig,'sig'),signatureSigValid:isStr(sig?.sig)&&sig.sig.length>=10,
73
- hasRisk:has('risk'),riskValid:['LOW','MEDIUM','HIGH','CRITICAL'].includes(cmd?.risk),
74
- }
75
- }
76
-
77
- function policyFlags(policy,cmd){
78
- let hasP=!!(policy&&policy.default==='DENY'&&policy.requesters&&typeof policy.requesters==='object');
79
- let rp=policy?.requesters?.[cmd.requesterId];
80
- let cmdId=(cmd?.id||'').toLowerCase();
81
- let commandAllowed=!!(rp?.allowCommands?.some(c=>c.toLowerCase()===cmdId));
82
- let adapterAllowed=!!(rp?.allowAdapters?.includes(cmd.payload?.adapter));
83
- let filesystemRequired=!!(cmd.payload?.cwd);
84
- let filesystemRootsDefined=false,filesystemOk=false,pathDenied=false;
85
- if(filesystemRequired){
86
- let roots=rp?.filesystem?.roots??[];
87
- filesystemRootsDefined=roots.length>0;
88
- if(filesystemRootsDefined){
89
- let cwd=path.resolve(cmd.payload.cwd);
90
- filesystemOk=roots.some(r=>{let rr=path.resolve(r);return cwd===rr||cwd.startsWith(rr+path.sep)});
91
- pathDenied=(rp?.filesystem?.denyPatterns??[]).some(pat=>new RegExp('^'+pat.replace(/\./g,'\\.').replace(/\*\*/g,'.*').replace(/\*/g,'[^/]*')+'$').test(cwd));
92
- }
93
- }
94
- let shellRequired=false,shellCommandOk=true;
95
- if(cmd.id==='RUN_SHELL'){
96
- shellRequired=true;
97
- let allow=rp?.exec?.allowCmds??[],deny=rp?.exec?.denyCmds??[],sc=cmd.payload?.cmd;
98
- if(deny.includes(sc))shellCommandOk=false;
99
- else shellCommandOk=allow.length===0||allow.includes(sc);
100
- }
101
- return{policyConfigured:hasP,requesterConfigured:!!rp,commandAllowed,adapterAllowed,filesystemRequired,filesystemRootsDefined,filesystemOk,pathDenied,shellRequired,shellCommandOk}
102
- }
103
-
104
- // ── Key lifecycle ─────────────────────────────────────────────────────────────
105
- var KEY_ID_RE=/^[A-Za-z0-9:_-]{3,128}$/;
106
- function isValidKeyId(id){return typeof id==='string'&&KEY_ID_RE.test(id)&&id!=='default'}
107
-
108
- function loadKeyStore(p){
109
- let rp=path.resolve(p);
110
- if(!fs.existsSync(rp))return{ok:false,reason:'KEY_STORE_MISSING',message:`Key store not found: ${rp}`,store:null};
111
- try{
112
- let s=JSON.parse(fs.readFileSync(rp,'utf-8'));
113
- if(!s||typeof s!='object'||typeof s.trustedKeys!='object')return{ok:false,reason:'KEY_STORE_INVALID',message:`Invalid key store format: ${rp}`,store:null};
114
- return{ok:true,reason:null,message:'Key store loaded',store:s}
115
- }catch(e){return{ok:false,reason:'KEY_STORE_INVALID_JSON',message:`Unable to parse key store: ${e.message}`,store:null}}
116
- }
117
-
118
- function resolveKeyFlags(keyStore,keyId,requesterId,now=new Date()){
119
- let base={keyIdFormatValid:false,keyFound:false,keyNotDeprecated:false,keyRequesterMatches:false,keyNotBeforeOk:false,keyNotExpired:false,keyLifecycleFieldsPresent:false,publicKey:null};
120
- if(!keyStore||!keyId)return base;
121
- if(!isValidKeyId(keyId))return base;
122
- base.keyIdFormatValid=true;
123
- let entry=keyStore.trustedKeys?.[keyId];
124
- if(!entry)return base;
125
- base.keyFound=true;
126
- base.keyNotDeprecated=!entry.deprecated;
127
- base.keyRequesterMatches=!entry.requesterId||entry.requesterId===requesterId;
128
- let nb=entry.notBefore||entry.validFrom,exp=entry.expiresAt||entry.validUntil;
129
- base.keyLifecycleFieldsPresent=typeof nb==='string'&&typeof exp==='string';
130
- if(base.keyLifecycleFieldsPresent){
131
- let nd=new Date(nb),ed=new Date(exp);
132
- if(!isNaN(nd.getTime())&&!isNaN(ed.getTime())&&nd<ed){base.keyNotBeforeOk=now>=nd;base.keyNotExpired=now<ed}
133
- }
134
- base.publicKey=entry.publicKey??null;
135
- return base
136
- }
137
-
138
- // ── Policy version guard ──────────────────────────────────────────────────────
139
- function parseVer(v){
140
- if(typeof v==='number'&&Number.isFinite(v))return{ok:true,kind:'int',parts:[Math.floor(v)],raw:String(v)};
141
- if(typeof v!=='string'||!v.trim())return{ok:false,reason:'POLICY_VERSION_INVALID',message:'Policy version required'};
142
- let r=v.trim();
143
- if(/^\d+$/.test(r))return{ok:true,kind:'int',parts:[Number(r)],raw:r};
144
- let s=r.replace(/^v/i,'');
145
- if(/^\d+(\.\d+){0,2}$/.test(s)){let p=s.split('.').map(Number);while(p.length<3)p.push(0);return{ok:true,kind:'semver',parts:p,raw:r}}
146
- return{ok:false,reason:'POLICY_VERSION_INVALID',message:`Unsupported policy version format '${v}'`}
147
- }
148
- function cmpVer(a,b){let n=Math.max(a.parts.length,b.parts.length);for(let i=0;i<n;i++){let x=a.parts[i]??0,y=b.parts[i]??0;if(x>y)return 1;if(x<y)return-1}return 0}
149
- function parseTs(v){
150
- if(typeof v==='number'&&Number.isFinite(v))return{ok:true,epochSec:v>1e12?Math.floor(v/1e3):Math.floor(v)};
151
- if(typeof v!=='string'||!v.trim())return{ok:false,reason:'POLICY_CREATED_AT_INVALID',message:'Policy createdAt required'};
152
- let ms=Date.parse(v);return isNaN(ms)?{ok:false,reason:'POLICY_CREATED_AT_INVALID',message:`Invalid createdAt '${v}'`}:{ok:true,epochSec:Math.floor(ms/1e3)}
153
- }
154
- function readPolicyState(p){
155
- if(!fs.existsSync(p))return{schemaVersion:'1',lastAccepted:null,updatedAt:null};
156
- try{let s=JSON.parse(fs.readFileSync(p,'utf8'));if(!s||typeof s!=='object')throw new Error('invalid');return{schemaVersion:String(s.schemaVersion||'1'),lastAccepted:s.lastAccepted&&typeof s.lastAccepted==='object'?s.lastAccepted:null,updatedAt:s.updatedAt||null}}
157
- catch(e){throw new Error(`Policy state at ${p} is corrupt: ${e.message}`)}
158
- }
159
- function validatePolicyVersionGuard({policyObj,statePath,persist=true,nowSec=Math.floor(Date.now()/1e3),maxSkewSec=31536000}){
160
- let av=parseVer(policyObj?.version);if(!av.ok)return{ok:false,...av,updated:false};
161
- let at2=parseTs(policyObj?.createdAt);if(!at2.ok)return{ok:false,...at2,updated:false};
162
- if(Math.abs(nowSec-at2.epochSec)>maxSkewSec)return{ok:false,reason:'POLICY_CREATED_AT_SKEW_EXCEEDED',message:`Policy createdAt skew exceeds ${maxSkewSec}s`,updated:false};
163
- let state;try{state=readPolicyState(statePath)}catch(e){return{ok:false,reason:'POLICY_STATE_CORRUPT',message:e.message,updated:false}}
164
- let prev=state.lastAccepted,pv=null,pt2=null,cmp=0;
165
- if(prev){pv=parseVer(prev.version);pt2=parseTs(prev.createdAt);
166
- if(pv.ok&&pt2.ok){cmp=cmpVer(av,pv);
167
- if(cmp<0)return{ok:false,reason:'POLICY_VERSION_REGRESSION',message:`Version regression: '${av.raw}' < '${pv.raw}'`,updated:false};
168
- if(cmp===0&&at2.epochSec<pt2.epochSec)return{ok:false,reason:'POLICY_CREATED_AT_REGRESSION',message:'Policy createdAt must increase',updated:false};
169
- if(cmp>0&&at2.epochSec<pt2.epochSec)return{ok:false,reason:'POLICY_CREATED_AT_REGRESSION',message:'Policy createdAt must increase with version',updated:false};
170
- }
171
- }
172
- let changed=!prev||!pv?.ok||!pt2?.ok||cmp>0||(cmp===0&&at2.epochSec>pt2.epochSec);
173
- if(persist&&changed)atomicWrite(statePath,JSON.stringify({schemaVersion:'1',lastAccepted:{version:policyObj.version,createdAt:policyObj.createdAt,environment:policyObj.environment||null},updatedAt:new Date().toISOString()},null,2),{encoding:'utf8'});
174
- return{ok:true,reason:null,message:'Policy version guard passed',updated:changed}
175
- }
176
-
177
- // ── Policy signature verification ─────────────────────────────────────────────
178
- function verifyPolicySig({policyObj,keyStore,policySigPath='./config/policy.sig.json',allowUnsigned=false}){
179
- let p=path.resolve(policySigPath);
180
- if(!fs.existsSync(p))return allowUnsigned?{ok:true,skipped:true,reason:'POLICY_SIGNATURE_SKIPPED',message:`Policy sig not found: ${p} (allowed by flag)`}:{ok:false,skipped:false,reason:'POLICY_SIGNATURE_MISSING',message:`Policy sig not found: ${p}`};
181
- let env;try{env=JSON.parse(fs.readFileSync(p,'utf-8'))}catch(e){return{ok:false,skipped:false,reason:'POLICY_SIGNATURE_INVALID',message:`Cannot parse policy sig: ${e.message}`}};
182
- if(!env||env.alg!=='ed25519'||typeof env.keyId!=='string'||typeof env.sig!=='string')return{ok:false,skipped:false,reason:'POLICY_SIGNATURE_INVALID',message:'Policy sig must include {alg,keyId,sig}'};
183
- if(!keyStore)return{ok:false,skipped:false,reason:'POLICY_SIGNER_KEY_STORE_UNAVAILABLE',message:'Key store required for policy sig verification'};
184
- let kf=resolveKeyFlags(keyStore,env.keyId,undefined,new Date());
185
- if(!kf.keyFound||!kf.keyNotExpired)return{ok:false,skipped:false,reason:'POLICY_SIGNER_NOT_TRUSTED',message:`Key '${env.keyId}' not trusted or expired`};
186
- let v=verifyEd25519({payloadObj:policyObj,sigB64:env.sig,pubKeyB64:kf.publicKey});
187
- return v.valid?{ok:true,skipped:false,reason:null,message:'Policy signature verified',keyId:env.keyId}:{ok:false,skipped:false,reason:'POLICY_SIGNATURE_INVALID',message:v.message}
188
- }
189
-
190
- // ── Invariant gate ────────────────────────────────────────────────────────────
191
- class InvariantGateError extends Error{constructor(msg,checks,failures){super(msg);this.name='InvariantGateError';this.checks=checks;this.failures=failures}}
192
- function dirWritable(d){try{fs.accessSync(d,fs.constants.W_OK);return true}catch{try{fs.mkdirSync(d,{recursive:true});return true}catch{return false}}}
193
- function runInvariantGate(paths,policy,keyStore){
194
- let c={},f=[];
195
- c.policy_structure=!!(policy&&policy.default==='DENY'&&policy.requesters&&typeof policy.requesters==='object'&&typeof policy.version!=='undefined');
196
- if(!c.policy_structure)f.push('Policy missing required fields: version, requesters, default=DENY');
197
- c.keys_available=!!(keyStore&&typeof keyStore==='object'&&Object.keys(keyStore).length>0);
198
- if(!c.keys_available)f.push('No trusted keys loaded — provide config/keys.json');
199
- for(let[k,p2]of[['audit_log_writable',paths.auditLog],['nonce_db_writable',paths.nonceDb],['rate_limit_writable',paths.rateLimit]]){c[k]=dirWritable(path.dirname(p2));if(!c[k])f.push(`${k} directory not writable: ${path.dirname(p2)}`)}
200
- c.secret_key_present=!!paths.secretKey;if(!c.secret_key_present)f.push('secretKey not provided to createLBE()');
201
- return{ok:f.length===0,checks:c,failures:f}
202
- }
203
- function assertInvariantGate(paths,policy,keyStore){
204
- let r=runInvariantGate(paths,policy,keyStore);
205
- if(!r.ok){let n=r.failures.length;throw new InvariantGateError(`Invariant gate: ${n} violation${n===1?'':'s'} — ${r.failures.join(' | ')}`,r.checks,r.failures)}
206
- return r
207
- }
208
-
209
- // ── Logger ────────────────────────────────────────────────────────────────────
210
- var LEVELS={DEBUG:0,INFO:1,WARN:2,ERROR:3};
211
- function createLogger({level='INFO',maxHistory=500,silent=false}={}){
212
- let threshold=LEVELS[level]??LEVELS.INFO,history=[];
213
- function emit(lvl,scope,msg,meta){
214
- let e={ts:new Date().toISOString(),level:lvl,scope,message:msg,...(meta!==undefined?{meta}:{})};
215
- if(history.length>=maxHistory)history.shift();history.push(e);
216
- if(!silent&&LEVELS[lvl]>=threshold)process.stderr.write((meta!==undefined?`[${lvl}] [${scope}] ${msg} ${JSON.stringify(meta)}`:`[${lvl}] [${scope}] ${msg}`)+'\n')
217
- }
218
- function scope(s){return{debug:(m,d)=>emit('DEBUG',s,m,d),info:(m,d)=>emit('INFO',s,m,d),warn:(m,d)=>emit('WARN',s,m,d),error:(m,d)=>emit('ERROR',s,m,d)}}
219
- return{scope,debug:(m,d)=>emit('DEBUG','lbe',m,d),info:(m,d)=>emit('INFO','lbe',m,d),warn:(m,d)=>emit('WARN','lbe',m,d),error:(m,d)=>emit('ERROR','lbe',m,d),exportLogs:()=>[...history],clearHistory:()=>{history.length=0},get historyLength(){return history.length}}
220
- }
221
-
222
- // ── Nonce / rate-limit IO helpers ─────────────────────────────────────────────
223
- // WASM owns the logic. These helpers serialize/deserialize the text-format DB.
224
- function loadNonceEntries(dbPath){
225
- try{if(!fs.existsSync(dbPath))return[];let db=JSON.parse(fs.readFileSync(dbPath,'utf8'));return(db?.entries??[]).map(e=>`${e.key}:${e.timestamp}`)}
226
- catch{return[]}
227
- }
228
- function saveNonceEntries(dbPath,text){
229
- let entries=text.split('\n').filter(Boolean).map(line=>{let i=line.lastIndexOf(':');return{key:line.slice(0,i),timestamp:parseInt(line.slice(i+1),10)||0}});
230
- atomicWrite(dbPath,JSON.stringify({entries},null,2),{encoding:'utf8'})
231
- }
232
- function loadRateEntries(dbPath){
233
- try{if(!fs.existsSync(dbPath))return[];let db=JSON.parse(fs.readFileSync(dbPath,'utf8'));return(db?.entries??[]).map(e=>`${e.requesterId}:${e.timestamp}`)}
234
- catch{return[]}
235
- }
236
- function saveRateEntries(dbPath,text){
237
- let entries=text.split('\n').filter(Boolean).map(line=>{let i=line.lastIndexOf(':');return{requesterId:line.slice(0,i),timestamp:parseInt(line.slice(i+1),10)||0}});
238
- atomicWrite(dbPath,JSON.stringify({entries},null,2),{encoding:'utf8'})
239
- }
240
-
241
- // ── Validation — thin orchestrator; all decisions delegate to WASM ────────────
242
- function validate({commandObj,pubKeyB64,keyStore,nonceDbPath,rateLimitDbPath,policy,policyStatePath}){
243
- let result={valid:false,commandId:commandObj?.commandId,checks:{},errors:[]};
244
- let nowSec=Math.floor(Date.now()/1e3);
245
- let maxClockSkewSec=Number.isFinite(policy?.security?.maxClockSkewSec)?policy.security.maxClockSkewSec:600;
246
-
247
- // Policy version guard (meta-check on the policy itself, not the command)
248
- if(policyStatePath&&policy?.version!==undefined){
249
- try{
250
- let vg=validatePolicyVersionGuard({policyObj:policy,statePath:policyStatePath});
251
- result.checks.policyVersion=vg.ok;
252
- if(!vg.ok){result.errors.push({type:'POLICY_VERSION_INVALID',message:vg.message});return result}
253
- }catch{result.checks.policyVersion=true}
254
- }else{result.checks.policyVersion=true}
255
-
256
- // Extract structured flag sets for WASM
257
- let sf=schemaFlags(commandObj);
258
- let keyId=commandObj?.signature?.keyId;
259
- let kf=resolveKeyFlags(keyStore,keyId,commandObj?.requesterId,new Date());
260
-
261
- // Ed25519 signature verification (stays in JS — well-known algorithm via tweetnacl)
262
- let signatureValid=false,effectivePubKey=kf.publicKey||pubKeyB64;
263
- if(effectivePubKey){
264
- let body={...commandObj};delete body.signature;
265
- signatureValid=verifyEd25519({payloadObj:body,sigB64:commandObj?.signature?.sig,pubKeyB64:effectivePubKey}).valid;
266
- }
267
-
268
- // Rate limit — WASM sliding-window logic, state stored in JS file
269
- let rateLimitOk=true,rateLimitRetryAfterSec=0;
270
- if(signatureValid&&rateLimitDbPath){
271
- let rateCfg=policy?.requesters?.[commandObj.requesterId]?.rateLimit||{};
272
- let dflt=policy?.security?.defaultRateLimit||{};
273
- let rr=checkRateLimit({windowSec:rateCfg.windowSec??dflt.windowSec??60,maxRequests:rateCfg.maxRequests??dflt.maxRequests??30,nowSec,requesterId:commandObj.requesterId,existingEntries:loadRateEntries(rateLimitDbPath)});
274
- rateLimitOk=rr.ok;rateLimitRetryAfterSec=rr.retryAfterSec;
275
- saveRateEntries(rateLimitDbPath,rr.updatedEntriesText??'');
276
- }
277
-
278
- // Nonce check — WASM dedup logic, state stored in JS file
279
- let nonceOk=true;
280
- let nonceKey=`${commandObj?.requesterId}|${commandObj?.sessionId}|${commandObj?.nonce}`;
281
- if(signatureValid&&rateLimitOk&&nonceDbPath){
282
- let nr2=checkNonce({ttlSec:3600,nowSec,newKey:nonceKey,existingEntries:loadNonceEntries(nonceDbPath)});
283
- nonceOk=nr2.ok;
284
- if(nr2.ok)saveNonceEntries(nonceDbPath,nr2.updatedEntriesText??'');
285
- }
286
-
287
- // Policy flags
288
- let pf=policyFlags(policy,commandObj??{});
289
-
290
- // WASM pipeline — single call owns all governance decisions
291
- let pipe=runValidationPipeline({...sf,cmdTimestamp:commandObj?.timestamp??0,nowSec,maxClockSkewSec,...kf,signatureValid,rateLimitOk,rateLimitRetryAfterSec,nonceOk,...pf});
292
-
293
- // Map pipeline result to the checks surface
294
- result.checks.schema=pipe.ok||pipe.stage>0;
295
- result.checks.timestamp=pipe.ok||pipe.stage>1;
296
- result.checks.keyId=pipe.ok||pipe.stage>2;
297
- result.checks.signature=pipe.ok||pipe.stage>3;
298
- result.checks.rateLimit=pipe.ok||pipe.stage>4;
299
- result.checks.nonce=pipe.ok||pipe.stage>5;
300
- result.checks.policy=pipe.ok;
301
-
302
- if(!pipe.ok){
303
- let s=pipe.stageLabel;
304
- if(s==='schema'){result.checks.schema=false;result.errors.push({type:'SCHEMA_ERROR',message:pipe.schemaError||'Schema invalid'})}
305
- else if(s==='timestamp'){result.checks.timestamp=false;result.errors.push({type:'TIMESTAMP_SKEW_EXCEEDED',message:`Command timestamp skew ${pipe.skewSec}s exceeds allowed ${maxClockSkewSec}s`})}
306
- else if(s==='key'){result.checks.keyId=false;result.checks.signature=false;let kr=pipe.keyReason||'KEY_ERROR';result.errors.push({type:kr,message:kr})}
307
- else if(s==='signature'){result.checks.signature=false;result.errors.push({type:'SIGNATURE_INVALID',message:effectivePubKey?'Signature verification failed':'No public key available'})}
308
- else if(s==='rate_limit'){result.checks.rateLimit=false;result.errors.push({type:'RATE_LIMIT_EXCEEDED',message:`Rate limit exceeded. Retry after ${pipe.retryAfterSec}s`})}
309
- else if(s==='nonce'){result.checks.nonce=false;result.errors.push({type:'REPLAY_NONCE',message:'Nonce has already been used'})}
310
- else if(s==='policy'&&pipe.policyResult){result.checks.policy=false;result.errors.push({type:pipe.policyResult.reason,message:pipe.policyResult.message})}
311
- else result.errors.push({type:'VALIDATION_FAILED',message:`Failed at stage: ${s}`});
312
- return result
313
- }
314
-
315
- result.valid=true;
316
- result.risk=classifyRisk(commandObj.id,commandObj.payload?.cmd==='rm');
317
- result.message='Command validation successful';
318
- return result
319
- }
320
-
321
- // ── Audit log ─────────────────────────────────────────────────────────────────
322
- // Hash-chain computation delegates to WASM (SHA-256). File IO stays in JS.
323
- function getLastHash(auditPath){
324
- try{
325
- if(!fs.existsSync(auditPath))return'GENESIS';
326
- let lines=fs.readFileSync(auditPath,'utf8').trim().split('\n').filter(Boolean);
327
- if(!lines.length)return'GENESIS';
328
- return JSON.parse(lines[lines.length-1]).hash||'GENESIS'
329
- }catch{return'GENESIS'}
330
- }
331
- function appendAuditEntry(auditPath,entry){
332
- let dir=path.dirname(auditPath);fs.mkdirSync(dir,{recursive:true});
333
- return withLock(auditPath,()=>{
334
- let prevHash=getLastHash(auditPath);
335
- let body={...entry,prevHash,timestamp:new Date().toISOString()};delete body.hash;
336
- let entryJson=JSON.stringify(body);
337
- let hash=computeAuditHash(prevHash,entryJson);
338
- let line=JSON.stringify({...body,hash})+'\n';
339
- let existing=fs.existsSync(auditPath)?fs.readFileSync(auditPath,'utf8'):'';
340
- atomicWrite(auditPath,existing+line,{encoding:'utf8'});
341
- return{success:true,hash,prevHash,message:'Audit entry appended'}
342
- })
343
- }
344
-
345
- // ── Adapters (readable — execution only, no governance) ───────────────────────
346
- async function noopAdapter(cmd){
347
- return{adapter:'noop',commandId:cmd.commandId,command:cmd.id,status:'completed',output:`[NOOP] Would execute: ${cmd.id} on adapter: ${cmd.payload?.adapter}`,exitCode:0,timestamp:new Date().toISOString()}
348
- }
349
-
350
- import{spawnSync}from'child_process';
351
- function parseArgs(args){
352
- if(args===undefined)return{ok:true,args:[]};
353
- if(!Array.isArray(args))return{ok:false,error:'payload.args must be an array'};
354
- let out=[];for(let a of args){if(typeof a!=='string'&&typeof a!=='number'&&typeof a!=='boolean')return{ok:false,error:'payload.args may only contain string, number, or boolean'};out.push(String(a))}
355
- return{ok:true,args:out}
356
- }
357
- async function shellAdapter(cmd,_policy,rp){
358
- let t=cmd.payload,allowed=rp?.exec?.allowCmds??[],denied=rp?.exec?.denyCmds??[];
359
- if(t.adapter!=='shell')return{adapter:'shell',commandId:cmd.commandId,status:'error',error:'Adapter mismatch',exitCode:1};
360
- if(denied.includes(t.cmd))return{adapter:'shell',commandId:cmd.commandId,status:'blocked',error:`Command '${t.cmd}' is denied`,exitCode:2};
361
- if(allowed.length>0&&!allowed.includes(t.cmd))return{adapter:'shell',commandId:cmd.commandId,status:'blocked',error:`Command '${t.cmd}' not in allowlist`,exitCode:2};
362
- let roots=rp?.filesystem?.roots??[];
363
- if(!roots.some(r=>{let rp2=path.resolve(r),cwd2=path.resolve(t.cwd);return cwd2===rp2||cwd2.startsWith(rp2+path.sep)}))
364
- return{adapter:'shell',commandId:cmd.commandId,status:'blocked',error:`CWD '${t.cwd}' not authorized`,exitCode:2};
365
- let pa=parseArgs(t.args);if(!pa.ok)return{adapter:'shell',commandId:cmd.commandId,status:'blocked',error:pa.error,exitCode:2};
366
- try{
367
- let r=spawnSync(t.cmd,pa.args,{cwd:t.cwd,timeout:30000,encoding:'utf8',maxBuffer:1024*1024,stdio:['pipe','pipe','pipe'],shell:false});
368
- if(r.error)throw r.error;
369
- let out=`${r.stdout||''}${r.stderr||''}`,code=r.status??1;
370
- if(code!==0)return{adapter:'shell',commandId:cmd.commandId,command:t.cmd,status:'error',error:out||`Exit ${code}`,exitCode:code,timestamp:new Date().toISOString()};
371
- return{adapter:'shell',commandId:cmd.commandId,command:t.cmd,status:'completed',output:out,exitCode:0,timestamp:new Date().toISOString()}
372
- }catch(e){return{adapter:'shell',commandId:cmd.commandId,command:t.cmd,status:'error',error:e.message,exitCode:1,timestamp:new Date().toISOString()}}
373
- }
374
-
375
- var MAX_READ_BYTES=10*1024*1024;
376
- function absTarget(t,cwd){return t?path.isAbsolute(t)?path.resolve(t):path.resolve(cwd||process.cwd(),t):null}
377
- function inRoots(p2,roots){return roots.some(r=>{let rr=path.resolve(r);return p2===rr||p2.startsWith(rr+path.sep)})}
378
- function matchDeny(p2,patterns){for(let pat of patterns||[])if(new RegExp('^'+pat.replace(/\./g,'\\.').replace(/\*\*/g,'.*').replace(/\*/g,'[^/\\\\]*')+'$').test(p2))return pat;return null}
379
- function fileBlocked(cmd,code,msg){return{adapter:'file',commandId:cmd.commandId,status:'blocked',errorCode:code,error:msg,exitCode:2}}
380
- function fileError(cmd,code,msg,bk=null){return{adapter:'file',commandId:cmd.commandId,status:'error',errorCode:code,error:msg,backup:bk?{path:bk.backupPath,existed:bk.existed,hash:bk.hash,createdAt:bk.createdAt}:null,exitCode:1}}
381
- function makeBackup(target,backupDir){
382
- fs.mkdirSync(backupDir,{recursive:true});
383
- let tp=path.resolve(target),existed=fs.existsSync(tp),data=existed?fs.readFileSync(tp):null;
384
- let hash=data?crypto.createHash('sha256').update(data).digest('hex'):null;
385
- let bname=path.basename(tp).replace(/[^a-zA-Z0-9._-]/g,'_');
386
- let bpath=existed?path.join(backupDir,`${Date.now()}-${hash.slice(0,8)}-${bname}`):null;
387
- if(existed&&data!==null)atomicWrite(bpath,data);
388
- return{originalPath:tp,backupPath:bpath,existed,hash,createdAt:new Date().toISOString()}
389
- }
390
- function restoreBackup(bk){
391
- if(!bk)return{restored:false,error:'No backup metadata'};
392
- if(!bk.existed)try{fs.existsSync(bk.originalPath)&&fs.unlinkSync(bk.originalPath);return{restored:true,action:'deleted'}}catch(e){return{restored:false,error:e.message}}
393
- if(!bk.backupPath||!fs.existsSync(bk.backupPath))return{restored:false,error:'Backup file missing: '+bk.backupPath};
394
- try{atomicWrite(bk.originalPath,fs.readFileSync(bk.backupPath));return{restored:true,action:'restored'}}catch(e){return{restored:false,error:e.message}}
395
- }
396
- async function fileAdapter(cmd,_policy,rp){
397
- let t=cmd.payload,action=t.action,cwd=t.cwd||process.cwd(),target=absTarget(t.target,cwd);
398
- if(!action)return fileBlocked(cmd,'FILE_NO_ACTION','payload.action required');
399
- if(!target&&action!=='noop')return fileBlocked(cmd,'FILE_NO_TARGET','payload.target required');
400
- let roots=rp?.filesystem?.roots??[];if(roots.length===0)return fileBlocked(cmd,'FILE_NO_ROOTS','No filesystem roots defined');
401
- if(!inRoots(target,roots))return fileBlocked(cmd,'FILE_OUTSIDE_ROOT',`'${target}' outside allowed roots`);
402
- let denied=matchDeny(target,rp?.filesystem?.denyPatterns);if(denied)return fileBlocked(cmd,'FILE_PATH_DENIED',`'${target}' matches deny pattern: ${denied}`);
403
- if(action==='read'){
404
- if(!fs.existsSync(target))return fileError(cmd,'FILE_NOT_FOUND',`Not found: ${target}`);
405
- try{let stat=fs.statSync(target);if(stat.size>MAX_READ_BYTES)return fileError(cmd,'FILE_TOO_LARGE','File exceeds 10 MB');let out=fs.readFileSync(target,'utf8');return{adapter:'file',action,commandId:cmd.commandId,status:'completed',target,output:out,bytesRead:stat.size,exitCode:0}}
406
- catch(e){return fileError(cmd,'FILE_READ_ERROR',e.message)}
407
- }
408
- let bk=null;try{bk=makeBackup(target,rp?.backupDir||path.join(os.homedir(),'.lbe','backups'))}catch{}
409
- if(action==='write'||action==='patch'){
410
- if(t.content==null)return fileError(cmd,`FILE_MISSING_CONTENT`,'payload.content required');
411
- try{atomicWrite(target,t.content,{encoding:'utf8'});return{adapter:'file',action,commandId:cmd.commandId,status:'completed',target,backup:bk?{path:bk.backupPath,existed:bk.existed,hash:bk.hash}:null,output:`Wrote ${Buffer.byteLength(t.content,'utf8')} bytes to ${target}`,exitCode:0}}
412
- catch(e){restoreBackup(bk);return fileError(cmd,`FILE_${action.toUpperCase()}_ERROR`,e.message,bk)}
413
- }
414
- if(action==='delete'){
415
- if(!fs.existsSync(target))return fileError(cmd,'FILE_NOT_FOUND',`Not found: ${target}`);
416
- try{fs.unlinkSync(target);return{adapter:'file',action,commandId:cmd.commandId,status:'completed',target,backup:bk?{path:bk.backupPath,existed:bk.existed,hash:bk.hash}:null,output:`Deleted ${target}`,exitCode:0}}
417
- catch(e){return fileError(cmd,'FILE_DELETE_ERROR',e.message,bk)}
418
- }
419
- return fileBlocked(cmd,'FILE_UNKNOWN_ACTION',`Unknown action: '${action}'`)
420
- }
421
-
422
- async function observerAdapter(cmd){
423
- if(!(cmd.id||'').toUpperCase().startsWith('OBSERVE'))return{adapter:'observer',commandId:cmd.commandId,status:'error',error:`Observer adapter only handles OBSERVE_* commands`,exitCode:1};
424
- let{source,context,issueType,description,severity,metadata}=cmd.payload||{};
425
- if(!issueType||!description)return{adapter:'observer',commandId:cmd.commandId,status:'error',error:'Observer payload must include issueType and description',exitCode:1};
426
- let levels=['low','medium','high','critical'];
427
- if(severity&&!levels.includes(severity))return{adapter:'observer',commandId:cmd.commandId,status:'error',error:`Invalid severity '${severity}'`,exitCode:1};
428
- return{adapter:'observer',commandId:cmd.commandId,status:'recorded',timestamp:new Date().toISOString(),requesterId:cmd.requesterId,observation:{source:source||'unknown',context:context||'unknown',issueType,description,severity:severity||'info',metadata:metadata||{}},exitCode:0}
429
- }
430
-
431
- var ADAPTERS={noop:noopAdapter,shell:shellAdapter,file:fileAdapter,observer:observerAdapter};
432
- var ADAPTER_NAMES=Object.keys(ADAPTERS);
433
- async function runAdapter(name,cmd,policy,rp){
434
- let fn=ADAPTERS[name];
435
- if(!fn)return{adapter:name,commandId:cmd.commandId,status:'error',error:`Adapter '${name}' not found`,exitCode:1};
436
- try{return await fn(cmd,policy,rp)}catch(e){return{adapter:name,commandId:cmd.commandId,status:'error',error:`Adapter execution failed: ${e.message}`,exitCode:9}}
437
- }
438
-
439
- // ── Approval token store ──────────────────────────────────────────────────────
440
- var _checkpointStore=null;
441
- class CheckpointStore{
442
- constructor(p){this.dbPath=p||path.resolve('data/checkpoints.db.json');this.store={checkpoints:{},tokens:{}};this._load()}
443
- _load(){let r=readJson(this.dbPath);if(r){this.store=r;this.store.checkpoints=this.store.checkpoints||{};this.store.tokens=this.store.tokens||{}}}
444
- _save(){atomicWrite(this.dbPath,JSON.stringify(this.store,null,2),{encoding:'utf8'})}
445
- saveToken(id,data){this.store.tokens[id]={tokenId:id,...data,createdAt:Date.now()};this._save()}
446
- getToken(id){return this.store.tokens[id]||null}
447
- removeToken(id){if(this.store.tokens[id]){delete this.store.tokens[id];this._save();return true}return false}
448
- }
449
- function getCheckpointStore(p){return _checkpointStore||(_checkpointStore=new CheckpointStore(p))}
450
-
451
- var _approvalGate=null;
452
- class ApprovalGate{
453
- constructor(p){this.store=getCheckpointStore(p);this._pending=new Map()}
454
- createToken(jobId,ctx={}){
455
- let id=crypto.randomBytes(16).toString('hex');
456
- this.store.saveToken(id,{jobId,context:ctx,status:'pending',expiresAt:Date.now()+86400000});
457
- return id
458
- }
459
- awaitApproval(id){
460
- let t=this.store.getToken(id);
461
- if(!t)return Promise.reject(new Error(`Approval token ${id} not found`));
462
- if(t.status!=='pending')return Promise.reject(new Error(`Token ${id} not pending (${t.status})`));
463
- if(Date.now()>t.expiresAt){this.store.removeToken(id);return Promise.reject(new Error(`Token ${id} expired`))}
464
- return new Promise((res,rej)=>{this._pending.set(id,{resolve:res,reject:rej})})
465
- }
466
- approve(id,data={}){
467
- let t=this.store.getToken(id);if(!t)throw new Error('Token not found');if(t.status!=='pending')throw new Error('Token not pending');
468
- this.store.saveToken(id,{...t,status:'approved',approverData:data,resolvedAt:Date.now()});
469
- let p=this._pending.get(id);if(p){p.resolve({approved:true,approverData:data});this._pending.delete(id)}return true
470
- }
471
- deny(id,reason='Manually denied'){
472
- let t=this.store.getToken(id);if(!t)throw new Error('Token not found');if(t.status!=='pending')throw new Error('Token not pending');
473
- this.store.saveToken(id,{...t,status:'denied',reason,resolvedAt:Date.now()});
474
- let p=this._pending.get(id);if(p){p.reject(new Error(`Approval denied: ${reason}`));this._pending.delete(id)}return true
475
- }
476
- }
477
- function getApprovalGate(p){return _approvalGate||(_approvalGate=new ApprovalGate(p))}
478
-
479
- // ── State directory resolver ──────────────────────────────────────────────────
480
- function resolveStateDir(rootDir,state){
481
- let rp=path.resolve(rootDir||process.cwd());
482
- if(!state||state==='local'){let h=crypto.createHash('sha256').update(rp).digest('hex').slice(0,16);return path.join(os.homedir(),'.lbe','workspaces',h)}
483
- if(state==='workspace')return path.join(rp,'.lbe');
484
- if(state&&typeof state==='object'&&state.adapter)return null;
485
- throw new Error(`createLBE: unknown state option: ${JSON.stringify(state)}`)
486
- }
487
-
488
- // ── Risk helpers ──────────────────────────────────────────────────────────────
489
- var INTENT_TO_CMD={patch_file:'PATCH_FILE',write_file:'WRITE_FILE',read_file:'READ_FILE',delete_file:'DELETE_FILE',run_shell:'RUN_SHELL',echo:'ECHO'};
490
- var INTENT_TO_ADAPTER={PATCH_FILE:'file',WRITE_FILE:'file',READ_FILE:'file',DELETE_FILE:'file',RUN_SHELL:'shell',ECHO:'noop'};
491
- var HIGH_RISK=new Set(['HIGH','CRITICAL']);
492
- function needsApproval(risk,rp){
493
- if(!rp?.requireApproval)return false;
494
- let r=rp.requireApproval;
495
- if(r===true)return true;
496
- if(Array.isArray(r))return r.includes(risk)||r.includes('*')||(HIGH_RISK.has(risk)&&r.includes('HIGH+'));
497
- return false
498
- }
499
-
500
- // ── Helpers ───────────────────────────────────────────────────────────────────
501
- function deepFreeze(obj){
502
- if(obj===null||typeof obj!=='object'||Object.isFrozen(obj))return obj;
503
- Object.freeze(obj);
504
- for(let k of Object.getOwnPropertyNames(obj)){let v=obj[k];if(typeof v==='object'&&v!==null&&!Object.isFrozen(v))deepFreeze(v)}
505
- return obj
506
- }
507
- function payloadHash(obj){return crypto.createHash('sha256').update(JSON.stringify(obj)).digest('hex')}
508
-
509
- // ── createLBE ─────────────────────────────────────────────────────────────────
510
- export function createLBE(options={}){
511
- // Auto-provision keypair when rootDir is provided without secretKey
512
- if(options.rootDir&&!options.secretKey){
513
- let kp=generateKeyPair(),kid=options.keyId||'sdk-auto-key';
514
- options={defaultActor:'agent:sdk',logLevel:'WARN',...options,secretKey:kp.secretKey,keyId:kid,
515
- keyStore:options.keyStore||makeKeyStore({publicKey:kp.publicKey,keyId:kid}),
516
- policy:options.policy||{version:1,default:'DENY',requesters:{'agent:sdk':{allowCommands:['write_file','read_file','patch_file','delete_file'],allowAdapters:['file'],filesystem:{roots:[options.rootDir],denyPatterns:['*.key','*.env','*.secret']}}}}
517
- }
518
- }
519
- let{secretKey,keyId='sdk-key-v1',sessionId,defaultActor='agent:sdk',policy:inlinePolicy,keyStore:inlineKeyStore,
520
- policyPath,keysStorePath,policySigPath,policyStatePath,nonceDbPath,rateLimitDbPath,auditLogPath,backupDir,
521
- allowUnsignedPolicy=false,state='local',rootDir,
522
- logLevel=process.env.LBE_LOG_LEVEL||'INFO',logSilent=process.env.LBE_LOG_SILENT==='1'}=options;
523
-
524
- let sd=resolveStateDir(rootDir,state);
525
- let paths={
526
- secretKey,
527
- policy:policyPath||path.resolve('config/policy.default.json'),
528
- keys:keysStorePath||path.resolve('config/keys.json'),
529
- policySig:policySigPath||path.resolve('config/policy.sig.json'),
530
- policyState:policyStatePath||path.join(sd,'policy.state.json'),
531
- nonceDb:nonceDbPath||path.join(sd,'nonce.db.json'),
532
- rateLimit:rateLimitDbPath||path.join(sd,'rate-limit.db.json'),
533
- auditLog:auditLogPath||path.join(sd,'audit.log.jsonl'),
534
- backupDir:backupDir||path.join(sd,'backups'),
535
- };
536
-
537
- let log=createLogger({level:logLevel,silent:logSilent});
538
- let exec_log=log.scope('Executor'),val_log=log.scope('Validator'),pol_log=log.scope('Policy');
539
-
540
- async function execute({actor,intent,target,content,args=[],transaction={}}){
541
- let tx={validate:true,backup:true,rollbackOnFailure:true,audit:true,...transaction};
542
- exec_log.info('execute() called',{actor,intent,target});
543
-
544
- // Load policy
545
- let policy;
546
- if(inlinePolicy){policy=inlinePolicy;pol_log.debug('Using inline policy',{version:policy.version})}
547
- else{try{policy=JSON.parse(fs.readFileSync(paths.policy,'utf8'));pol_log.debug('Policy loaded',{version:policy.version})}
548
- catch(e){pol_log.error('Policy load failed',{error:e.message});return{ok:false,stage:'policy_load',error:'POLICY_LOAD_FAILED',message:e.message}}}
549
-
550
- // Load key store
551
- let keyStore;
552
- if(inlineKeyStore){keyStore=inlineKeyStore;val_log.debug('Using inline keyStore')}
553
- else{let kr=loadKeyStore(paths.keys);keyStore=kr.ok?kr.store:null;val_log.debug('Keys loaded',{ok:kr.ok})}
554
-
555
- deepFreeze(policy);if(keyStore)deepFreeze(keyStore);
556
-
557
- // Invariant gate
558
- try{let ig=assertInvariantGate(paths,policy,keyStore);val_log.debug('Invariant gate passed',ig.checks)}
559
- catch(e){if(e instanceof InvariantGateError)return{ok:false,stage:'invariant_gate',error:'INVARIANT_GATE_FAILED',message:e.message,checks:e.checks,failures:e.failures};throw e}
560
-
561
- // Policy signature check (only for file-loaded policy)
562
- if(!inlinePolicy){
563
- let ps=verifyPolicySig({policyObj:policy,keyStore,policySigPath:paths.policySig,allowUnsigned:allowUnsignedPolicy});
564
- if(!ps.ok){pol_log.error('Policy sig invalid',{reason:ps.reason});return{ok:false,stage:'policy_sig',error:ps.reason,message:ps.message}}
565
- let vg=validatePolicyVersionGuard({policyObj:policy,statePath:paths.policyState});
566
- if(!vg.ok){pol_log.error('Policy version guard failed',{reason:vg.reason});return{ok:false,stage:'policy_version',error:vg.reason,message:vg.message}}
567
- pol_log.debug('Policy sig and version valid')
568
- }
569
-
570
- if(!secretKey)return{ok:false,stage:'sign',error:'NO_SECRET_KEY',message:'createLBE requires secretKey'};
571
-
572
- // Build and sign command
573
- let nowSec=Math.floor(Date.now()/1e3),nonce=crypto.randomBytes(32).toString('hex');
574
- let sid=sessionId||`sdk-${Date.now()}`,commandId=crypto.randomUUID();
575
- let cmdName=INTENT_TO_CMD[intent]||intent.toUpperCase().replace(/-/g,'_');
576
- let adapter=INTENT_TO_ADAPTER[cmdName]||'noop';
577
- exec_log.debug('Command built',{commandId,cmd:cmdName,adapter});
578
-
579
- let body={id:cmdName,commandId,requesterId:actor,sessionId:sid,timestamp:nowSec,nonce,requires:['policy','signature'],
580
- payload:{adapter,action:intent.includes('_')?intent.split('_')[0]:intent,target:target?path.resolve(target):null,content:content||null,args,cwd:target?path.dirname(path.resolve(target)):process.cwd()}};
581
- let signed=signPayload({payloadObj:body,secretKeyB64:secretKey});
582
- if(signed.error){exec_log.error('Signing failed',{error:signed.error});return{ok:false,stage:'sign',commandId,error:'SIGN_FAILED',message:signed.error}}
583
- let cmd={...body,signature:{alg:'ed25519',keyId,sig:signed.signature}};
584
-
585
- // Validate — thin JS orchestrator + WASM decisions
586
- let val=validate({commandObj:cmd,keyStore,nonceDbPath:paths.nonceDb,rateLimitDbPath:paths.rateLimit,policy,policyStatePath:inlinePolicy?null:paths.policyState});
587
- if(!val.valid){
588
- val_log.warn('Validation failed',{error:val.errors[0]?.type,checks:val.checks});
589
- if(tx.audit)appendAuditEntry(paths.auditLog,{commandId,status:'rejected',requesterId:actor,payloadHash:payloadHash(cmd),reason:val.errors[0]?.type,intent});
590
- return{ok:false,stage:'validate',commandId,error:val.errors[0]?.type,message:val.errors[0]?.message,checks:val.checks,operationLog:log.exportLogs()}
591
- }
592
- val_log.info('Validation passed',{risk:val.risk,checks:val.checks});
593
-
594
- // Approval gate
595
- let risk=val.risk||'LOW';
596
- let rp=policy.requesters?.[actor];
597
- if(needsApproval(risk,rp)){
598
- exec_log.warn('Approval required',{risk,commandId});
599
- let token=getApprovalGate(paths.policyState).createToken(commandId,{actor,intent,target,risk,commandId:cmdName});
600
- return{ok:false,stage:'approval_pending',commandId,approvalToken:token,risk,message:`${risk} risk operation requires approval. Token: ${token}`,operationLog:log.exportLogs()}
601
- }
602
-
603
- // Backup
604
- let bk=null;
605
- if(tx.backup&&target){
606
- let isWrite=['write','patch','delete'].includes(body.payload.action);
607
- try{bk=makeBackup(path.resolve(target),paths.backupDir);exec_log.debug('Backup created',{existed:bk.existed})}
608
- catch(e){if(isWrite){exec_log.error('Backup failed — aborting',{error:e.message});return{ok:false,stage:'backup',error:'BACKUP_FAILED',message:e.message}};exec_log.warn('Backup failed (non-fatal)',{error:e.message})}
609
- }
610
-
611
- // Execute
612
- exec_log.info('Executing adapter',{adapter,target});
613
- let adResult;
614
- try{adResult=await runAdapter(adapter,cmd,policy,rp)}
615
- catch(e){adResult={adapter,commandId,status:'error',error:e.message,exitCode:1}}
616
- exec_log.debug('Adapter returned',{status:adResult.status,exitCode:adResult.exitCode});
617
-
618
- // Post-execution validation
619
- let failed=adResult.status==='error'||(adResult.exitCode!==0&&adResult.exitCode!==undefined);
620
- let postCheck=null;
621
- if(tx.validate&&target&&!failed){
622
- let writeActions=['write','patch'];
623
- if(writeActions.includes(body.payload.action)){
624
- let exists=fs.existsSync(path.resolve(target));
625
- postCheck={ok:exists,check:'target_exists',target};
626
- if(!exists){exec_log.error('Post-exec validation failed',{target});adResult.status='error'}
627
- }
628
- }
629
-
630
- // Rollback — WASM decides whether to rollback
631
- let rollbackResult=null;
632
- if((failed||(postCheck&&!postCheck.ok))&&tx.rollbackOnFailure&&bk){
633
- let doRollback=shouldRollback({execFailed:failed,postCheckFailed:postCheck&&!postCheck.ok,backupExists:!!bk.backupPath,rollbackEnabled:true});
634
- if(doRollback)try{rollbackResult=restoreBackup(bk);exec_log.warn('Rollback executed',rollbackResult)}catch(e){rollbackResult={restored:false,error:e.message};exec_log.error('Rollback failed',{error:e.message})}
635
- }
636
-
637
- if(tx.audit)appendAuditEntry(paths.auditLog,{commandId,status:rollbackResult?.restored?'rolled_back':adResult.status||'completed',requesterId:actor,payloadHash:payloadHash(cmd),executionHash:payloadHash(adResult),adapter,intent,riskLevel:risk,exitCode:adResult.exitCode||0,rolledBack:rollbackResult?.restored||false});
638
-
639
- let ok=!failed&&(!postCheck||postCheck.ok);
640
- exec_log.info('execute() complete',{ok,risk});
641
- return{ok,commandId,intent,actor,target,risk,stage:ok?'executed':'failed',status:adResult.status,output:adResult.output||null,exitCode:adResult.exitCode??0,checks:val.checks,backup:bk?{path:bk.backupPath,existed:bk.existed,hash:bk.hash}:null,rollback:rollbackResult,postValidation:postCheck,operationLog:logLevel==='DEBUG'?log.exportLogs():undefined}
642
- }
643
-
644
- return{
645
- execute,
646
- exportLogs:()=>log.exportLogs(),
647
- async writeFile(target,content){
648
- let r=await execute({actor:defaultActor,intent:'write_file',target,content,transaction:{backup:true,rollbackOnFailure:true,audit:true}});
649
- if(!r.ok){let e=new Error(`LBE write failed [${r.error||r.stage}]${r.message?': '+r.message:''}`);e.lbeResult=r;throw e}return r
650
- },
651
- async readFile(target){
652
- let r=await execute({actor:defaultActor,intent:'read_file',target,transaction:{audit:true}});
653
- if(!r.ok){let e=new Error(`LBE read failed [${r.error||r.stage}]${r.message?': '+r.message:''}`);e.lbeResult=r;throw e}return r.output
654
- },
655
- }
656
- }
657
-
658
- // ── sandbox ───────────────────────────────────────────────────────────────────
659
- export function sandbox(rootDir,options={}){
660
- let{audit=false,rollback=false}=options;
661
- let rp=path.resolve(rootDir);
662
- let lbe=createLBE({rootDir:rp,state:options.state||'local',logSilent:true});
663
- let abs=p=>path.isAbsolute(p)?p:path.join(rp,p);
664
- let tx={backup:rollback,rollbackOnFailure:rollback,audit};
665
- function blocked(method,r){let e=new Error(`sandbox.${method} blocked [${r.error}]${r.message?': '+r.message:''}`);e.lbeResult=r;return e}
666
- return{
667
- async write(p,content){let r=await lbe.execute({actor:'agent:sdk',intent:'write_file',target:abs(p),content,transaction:tx});if(!r.ok)throw blocked('write',r)},
668
- async read(p){let r=await lbe.execute({actor:'agent:sdk',intent:'read_file',target:abs(p),transaction:{audit}});if(!r.ok)throw blocked('read',r);return r.output},
669
- async patch(p,content){let r=await lbe.execute({actor:'agent:sdk',intent:'patch_file',target:abs(p),content,transaction:tx});if(!r.ok)throw blocked('patch',r)},
670
- lbe,
671
- }
672
- }
673
-
674
- // ── makeKeyStore convenience helper ──────────────────────────────────────────
675
- function makeKeyStore({publicKey,keyId,validDays=365}){
676
- let now=new Date(),exp=new Date(now.getTime()+validDays*24*3600*1e3);
677
- return{defaultKeyId:keyId,trustedKeys:{[keyId]:{publicKey,notBefore:now.toISOString(),expiresAt:exp.toISOString()}}}
678
- }
1
+ import Le from"crypto";import Ct from"fs";import qr from"os";import _ from"path";import Pe from"tweetnacl";import{canonicalize as et}from"json-canonicalize";function Ce(e){return Buffer.from(e,"base64")}function Te(e){return Buffer.from(e).toString("base64")}function z({payloadObj:e,sigB64:t,pubKeyB64:r}){try{let s=Buffer.from(et(e),"utf8"),o=Ce(t),n=Ce(r),a=Pe.sign.detached.verify(new Uint8Array(s),new Uint8Array(o),new Uint8Array(n));return{valid:a,message:a?"Signature verified":"Signature verification failed"}}catch(s){return{valid:!1,message:`Signature verification error: ${s.message}`}}}function ge(){let e=Pe.sign.keyPair();return{publicKey:Te(e.publicKey),secretKey:Te(e.secretKey)}}function se({payloadObj:e,secretKeyB64:t}){try{let r=Buffer.from(et(e),"utf8"),s=Ce(t),o=Pe.sign.detached(new Uint8Array(r),new Uint8Array(s));return{signature:Te(o),error:null}}catch(r){return{signature:null,error:`Signing failed: ${r.message}`}}}import nt from"fs";import Ht from"path";import v from"fs";import $e from"path";import Bt from"crypto";var Mt={timeoutMs:5e3,pollMs:15,staleMs:3e4};function Yt(e){return e+".lock"}function tt(e){try{let t=v.openSync(e,"wx");return v.writeSync(t,`pid:${process.pid}:${Date.now()}`),v.closeSync(t),!0}catch(t){if(t.code==="EEXIST"||t.code==="EPERM"||t.code==="EBUSY"||t.code==="EACCES")return!1;throw t}}function rt(e,t){try{let r=v.statSync(e);if(Date.now()-r.mtimeMs>t)try{v.unlinkSync(e)}catch{}}catch{}}function Ut(e){let t=Date.now()+e;for(;Date.now()<t;)try{Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,Math.max(1,t-Date.now()))}catch{}}function st(e,t,r){let s=typeof t=="function"?t:r,o=typeof t=="function"?{}:t||{},{timeoutMs:n,pollMs:a,staleMs:i}={...Mt,...o},c=$e.dirname(e);v.existsSync(c)||v.mkdirSync(c,{recursive:!0});let l=Yt(e),d=Date.now()+n,u=!1;for(;!u&&(u=tt(l),!u);){if(Date.now()>=d){if(rt(l,i),u=tt(l),u)break;let m=new Error(`withFileLock: timeout acquiring ${l} after ${n}ms`);throw m.code="ELOCKTIMEOUT",m}rt(l,i);let f=Math.floor(Math.random()*a);Ut(a+f)}try{return s()}finally{try{v.unlinkSync(l)}catch{}}}function A(e,t,r={}){let s=$e.dirname(e);v.existsSync(s)||v.mkdirSync(s,{recursive:!0});let o=$e.join(s,`.tmp-${Date.now()}-${Bt.randomBytes(4).toString("hex")}`);try{v.writeFileSync(o,t,r),v.renameSync(o,e)}catch(n){try{v.existsSync(o)&&v.unlinkSync(o)}catch{}throw n}}function ot(e){try{if(!v.existsSync(e))return null;let t=v.readFileSync(e,"utf8");return JSON.parse(t)}catch(t){return console.error(`[atomicWrite] Failed to read JSON from ${e}:`,t.message),null}}function it(e){if(typeof e=="number"&&Number.isFinite(e))return{ok:!0,kind:"int",parts:[Math.floor(e)],raw:String(e)};if(typeof e!="string"||!e.trim())return{ok:!1,reason:"POLICY_VERSION_INVALID",message:"Policy version is required"};let t=e.trim();if(/^\d+$/.test(t))return{ok:!0,kind:"int",parts:[Number(t)],raw:t};let r=t.replace(/^v/i,"");if(/^\d+(\.\d+){0,2}$/.test(r)){let s=r.split(".").map(o=>Number(o));for(;s.length<3;)s.push(0);return{ok:!0,kind:"semver",parts:s,raw:t}}return{ok:!1,reason:"POLICY_VERSION_INVALID",message:`Unsupported policy version format '${e}' (use integer or semver)`}}function Wt(e,t){let r=Math.max(e.parts.length,t.parts.length);for(let s=0;s<r;s++){let o=e.parts[s]??0,n=t.parts[s]??0;if(o>n)return 1;if(o<n)return-1}return 0}function at(e){if(typeof e=="number"&&Number.isFinite(e))return{ok:!0,epochSec:e>1e12?Math.floor(e/1e3):Math.floor(e)};if(typeof e!="string"||!e.trim())return{ok:!1,reason:"POLICY_CREATED_AT_INVALID",message:"Policy createdAt is required"};let t=Date.parse(e);return Number.isNaN(t)?{ok:!1,reason:"POLICY_CREATED_AT_INVALID",message:`Invalid policy createdAt '${e}'`}:{ok:!0,epochSec:Math.floor(t/1e3)}}function Gt(e){if(!nt.existsSync(e))return{schemaVersion:"1",lastAccepted:null,updatedAt:null};try{let t=JSON.parse(nt.readFileSync(e,"utf8"));if(!t||typeof t!="object")throw new Error("Policy state file has invalid structure");return{schemaVersion:String(t.schemaVersion||"1"),lastAccepted:t.lastAccepted&&typeof t.lastAccepted=="object"?t.lastAccepted:null,updatedAt:t.updatedAt||null}}catch(t){throw new Error(`Policy state at ${e} is corrupt or unreadable: ${t.message}`)}}function jt(e,t){let r=JSON.stringify(t,null,2);A(e,r,{encoding:"utf8"})}function ke({policyObj:e,statePath:t=Ht.resolve("data/policy.state.json"),maxCreatedAtSkewSec:r=31536e3,nowSec:s=Math.floor(Date.now()/1e3),persist:o=!0}){let n=it(e?.version);if(!n.ok)return{ok:!1,reason:n.reason,message:n.message,updated:!1};let a=at(e?.createdAt);if(!a.ok)return{ok:!1,reason:a.reason,message:a.message,updated:!1};let i=Math.abs(s-a.epochSec),c=Number.isFinite(r)&&r>0?Math.floor(r):31536e3;if(i>c)return{ok:!1,reason:"POLICY_CREATED_AT_SKEW_EXCEEDED",message:`Policy createdAt skew ${i}s exceeds allowed ${c}s`,updated:!1};let l;try{l=Gt(t)}catch(S){return{ok:!1,reason:"POLICY_STATE_CORRUPT",message:S.message,updated:!1}}let d=l.lastAccepted,u=null,f=null,m=0;if(d&&(u=it(d.version),f=at(d.createdAt),u.ok&&f.ok)){if(m=Wt(n,u),m<0)return{ok:!1,reason:"POLICY_VERSION_REGRESSION",message:`Policy version regression: current '${n.raw}' < last '${u.raw}'`,updated:!1};if(m===0&&a.epochSec<f.epochSec)return{ok:!1,reason:"POLICY_CREATED_AT_REGRESSION",message:`Policy createdAt regression: current '${e.createdAt}' < last '${d.createdAt}'`,updated:!1};if(m>0&&a.epochSec<f.epochSec)return{ok:!1,reason:"POLICY_CREATED_AT_REGRESSION",message:"Policy createdAt must be monotonic when version increases",updated:!1}}let I=!d||!u?.ok||!f?.ok||m>0||m===0&&a.epochSec>f.epochSec;if(o&&I){let S={schemaVersion:"1",lastAccepted:{version:e.version,createdAt:e.createdAt,environment:e.environment||null},updatedAt:new Date().toISOString()};jt(t,S)}return{ok:!0,reason:null,message:"Policy version guard passed",updated:I}}import Fe from"fs";import lt from"path";import{fileURLToPath as Jt}from"url";var zt=lt.dirname(Jt(import.meta.url)),oe=lt.join(zt,"lbe_engine.wasm"),ct={0:{allowed:!0,reason:null,message:"Policy check passed"},1:{allowed:!1,reason:"POLICY_NOT_CONFIGURED",message:"No policy configured"},2:{allowed:!1,reason:"REQUESTER_NOT_ALLOWED",message:"Requester not in policy"},3:{allowed:!1,reason:"COMMAND_NOT_ALLOWED",message:"Command not allowed for requester"},4:{allowed:!1,reason:"ADAPTER_NOT_ALLOWED",message:"Adapter not allowed"},5:{allowed:!1,reason:"NO_FILESYSTEM_ROOTS_DEFINED",message:"No filesystem roots defined for requester"},6:{allowed:!1,reason:"CWD_OUTSIDE_ALLOWED_ROOT",message:"Path not under allowed roots"},7:{allowed:!1,reason:"PATH_DENIED_BY_PATTERN",message:"Path matches deny pattern"},8:{allowed:!1,reason:"SHELL_CMD_DENIED",message:"Shell command not allowed"}},Xt={0:{valid:!0,error:null},1:{valid:!1,error:"Missing required field: id"},2:{valid:!1,error:"Missing required field: commandId"},3:{valid:!1,error:"Missing required field: requesterId"},4:{valid:!1,error:"Missing required field: sessionId"},5:{valid:!1,error:"Missing required field: timestamp"},6:{valid:!1,error:"Missing required field: nonce"},7:{valid:!1,error:"Missing required field: requires"},8:{valid:!1,error:"Missing required field: payload"},9:{valid:!1,error:"Missing required field: signature"},10:{valid:!1,error:"Field 'id' is invalid"},11:{valid:!1,error:"Field 'commandId' is invalid"},12:{valid:!1,error:"Field 'requesterId' is invalid"},13:{valid:!1,error:"Field 'sessionId' is invalid"},14:{valid:!1,error:"Field 'timestamp' is invalid"},15:{valid:!1,error:"Field 'nonce' is invalid"},16:{valid:!1,error:"Field 'requires' is invalid"},17:{valid:!1,error:"payload: missing required field: adapter"},18:{valid:!1,error:"payload: field 'adapter' is invalid"},19:{valid:!1,error:"signature: missing required field: alg"},20:{valid:!1,error:"signature: missing required field: keyId"},21:{valid:!1,error:"signature: missing required field: sig"},22:{valid:!1,error:"signature: field 'alg' must be ed25519"},23:{valid:!1,error:"signature: field 'sig' is invalid"},24:{valid:!1,error:"Field 'risk' is invalid"}},Qt={1:"KEY_ID_INVALID",2:"KEY_NOT_TRUSTED",3:"KEY_DEPRECATED",4:"KEY_REQUESTER_MISMATCH",5:"KEY_LIFECYCLE_INVALID",6:"KEY_NOT_YET_VALID",7:"KEY_EXPIRED"},Zt={0:"schema",1:"timestamp",2:"key",3:"signature",4:"rate_limit",5:"nonce",6:"policy",255:"ok"},er=["LOW","MEDIUM","HIGH","CRITICAL"],tr={ECHO:0,READ_FILE:1,WRITE_FILE:2,PATCH_FILE:3,DELETE_FILE:4,RUN_SHELL:5},Ee=null;function $(){if(Ee)return Ee;if(!Fe.existsSync(oe))throw new Error(`LBE engine missing: ${oe}`);let e=Fe.readFileSync(oe);return Ee=new WebAssembly.Instance(new WebAssembly.Module(e),{}),Ee}function Ie(){return new Uint8Array($().exports.memory.buffer)}function dt(){return $().exports.lbe_in_ptr()}function ut(){return $().exports.lbe_out_ptr()}function rr(){return $().exports.lbe_buf_size()}function ft(e){let t=new TextEncoder().encode(e),r=Ie(),s=dt();r.set(t,s),r[s+t.length]=0}function pt(){let e=Ie(),t=ut(),r=t;for(;e[r]!==0&&r-t<rr();)r++;return new TextDecoder().decode(e.slice(t,r))}function sr(e){let t=Ie(),r=dt(),s=new DataView(t.buffer,r);e.forEach((o,n)=>s.setUint32(n*4,o>>>0,!0))}function or(){let e=Ie(),t=ut(),r=new DataView(e.buffer,t);return{stage:r.getUint32(0,!0),code:r.getUint32(4,!0)}}function nr(){return{mode:"wasm",available:Fe.existsSync(oe),wasmPath:oe,localFirst:!0}}async function ir(){return{ok:!0,mode:"wasm",version:$().exports.lbe_engine_version()}}function mt(e){sr([e.hasId?1:0,e.idValid?1:0,e.hasCommandId?1:0,e.commandIdValid?1:0,e.hasRequesterId?1:0,e.requesterIdValid?1:0,e.hasSessionId?1:0,e.sessionIdValid?1:0,e.hasTimestamp?1:0,e.timestampValid?1:0,e.hasNonce?1:0,e.nonceValid?1:0,e.hasRequires?1:0,e.requiresValid?1:0,e.hasPayload?1:0,e.hasPayloadAdapter?1:0,e.payloadAdapterValid?1:0,e.hasSignature?1:0,e.hasSignatureAlg?1:0,e.signatureAlgValid?1:0,e.hasSignatureKeyId?1:0,e.hasSignatureSig?1:0,e.signatureSigValid?1:0,e.hasRisk?1:0,e.riskValid?1:0,e.cmdTimestamp>>>0,e.nowSec>>>0,e.maxClockSkewSec>>>0,e.keyIdFormatValid?1:0,e.keyFound?1:0,e.keyNotDeprecated?1:0,e.keyRequesterMatches?1:0,e.keyNotBeforeOk?1:0,e.keyNotExpired?1:0,e.keyLifecycleFieldsPresent?1:0,e.signatureValid?1:0,e.rateLimitOk?1:0,e.rateLimitRetryAfterSec>>>0,e.nonceOk?1:0,e.policyConfigured?1:0,e.requesterConfigured?1:0,e.commandAllowed?1:0,e.adapterAllowed?1:0,e.filesystemRequired?1:0,e.filesystemRootsDefined?1:0,e.filesystemOk?1:0,e.pathDenied?1:0,e.shellRequired?1:0,e.shellCommandOk?1:0]),$().exports.lbe_validate_pipeline();let{stage:t,code:r}=or();return{ok:t===255,stage:t,stageLabel:Zt[t]||"unknown",code:r,schemaError:t===0?Xt[r]?.error||"Schema invalid":null,keyReason:t===2?Qt[r]||"KEY_ERROR":null,policyResult:t===6?{...ct[r]||ct[1],code:r}:null,retryAfterSec:t===4?r:0,skewSec:t===1?r:0}}function Ke({ttlSec:e,nowSec:t,newKey:r,existingEntries:s}){let o=[`${e}:${t}`,r,...s].join(`
2
+ `)+`
3
+ `;if(ft(o),$().exports.lbe_nonce_check()!==0)return{ok:!1,updatedEntriesText:null};let a=pt();return{ok:!0,updatedEntriesText:a.startsWith(`OK
4
+ `)?a.slice(3):a}}function yt({windowSec:e,maxRequests:t,nowSec:r,requesterId:s,existingEntries:o}){let n=[`${e}:${t}:${r}`,s,...o].join(`
5
+ `)+`
6
+ `;ft(n);let a=$().exports.lbe_rate_check()!==0,i=pt();if(a){let c=parseInt(i.match(/^EXCEEDED:(\d+)/)?.[1]??"1",10),l=i.replace(/^EXCEEDED:\d+\n/,"");return{ok:!1,retryAfterSec:c,updatedEntriesText:l}}return{ok:!0,retryAfterSec:0,updatedEntriesText:i.startsWith(`OK
7
+ `)?i.slice(3):i}}function ht(e,t=!1){let r=tr[e]??0,s=$().exports.lbe_classify_risk(r,t?1:0);return er[s]??"LOW"}import qe from"path";function ar(e){let t=n=>e!=null&&Object.prototype.hasOwnProperty.call(e,n),r=n=>typeof n=="string",s=e?.payload,o=e?.signature;return{hasId:t("id"),idValid:r(e?.id)&&/^[A-Z_]+$/.test(e.id)&&e.id.length>=1&&e.id.length<=50,hasCommandId:t("commandId"),commandIdValid:r(e?.commandId)&&/^[a-f0-9-]+$/.test(e.commandId)&&e.commandId.length===36,hasRequesterId:t("requesterId"),requesterIdValid:r(e?.requesterId)&&e.requesterId.length>=3&&e.requesterId.length<=100,hasSessionId:t("sessionId"),sessionIdValid:r(e?.sessionId)&&e.sessionId.length>=3,hasTimestamp:t("timestamp"),timestampValid:typeof e?.timestamp=="number"&&e.timestamp>=1e9,hasNonce:t("nonce"),nonceValid:r(e?.nonce)&&e.nonce.length>=32&&e.nonce.length<=128,hasRequires:t("requires"),requiresValid:Array.isArray(e?.requires)&&e.requires.length>=1&&e.requires.every(r),hasPayload:t("payload")&&typeof s=="object"&&s!==null&&!Array.isArray(s),hasPayloadAdapter:s!=null&&Object.prototype.hasOwnProperty.call(s,"adapter"),payloadAdapterValid:r(s?.adapter),hasSignature:t("signature")&&typeof o=="object"&&o!==null&&!Array.isArray(o),hasSignatureAlg:o!=null&&Object.prototype.hasOwnProperty.call(o,"alg"),signatureAlgValid:o?.alg==="ed25519",hasSignatureKeyId:o!=null&&Object.prototype.hasOwnProperty.call(o,"keyId"),hasSignatureSig:o!=null&&Object.prototype.hasOwnProperty.call(o,"sig"),signatureSigValid:r(o?.sig)&&o.sig.length>=10,hasRisk:t("risk"),riskValid:["LOW","MEDIUM","HIGH","CRITICAL"].includes(e?.risk)}}function cr(e,t){let r=!!(e&&e.default==="DENY"&&e.requesters&&typeof e.requesters=="object"),s=e?.requesters?.[t.requesterId],o=t.id?.toLowerCase()??"",n=!!s?.allowCommands?.some(m=>m.toLowerCase()===o),a=!!s?.allowAdapters?.includes(t.payload?.adapter),i=!!t.payload?.cwd,c=!1,l=!1,d=!1;if(i){let m=s?.filesystem?.roots??[];if(c=m.length>0,c){let I=qe.resolve(t.payload.cwd);l=m.some(x=>{let T=qe.resolve(x);return I===T||I.startsWith(T+qe.sep)}),d=(s?.filesystem?.denyPatterns??[]).some(x=>new RegExp("^"+x.replace(/\./g,"\\.").replace(/\*\*/g,".*").replace(/\*/g,"[^/]*")+"$").test(I))}}let u=!1,f=!0;if(t.id==="RUN_SHELL"){u=!0;let m=s?.exec?.allowCmds??[],I=s?.exec?.denyCmds??[],S=t.payload?.cmd;I.includes(S)?f=!1:f=m.length===0||m.includes(S)}return{policyConfigured:r,requesterConfigured:!!s,commandAllowed:n,adapterAllowed:a,filesystemRequired:i,filesystemRootsDefined:c,filesystemOk:l,pathDenied:d,shellRequired:u,shellCommandOk:f}}function lr(e,t,r,s=new Date){if(!e||!t)return{keyIdFormatValid:!1,keyFound:!1,keyNotDeprecated:!1,keyRequesterMatches:!1,keyNotBeforeOk:!1,keyNotExpired:!1,keyLifecycleFieldsPresent:!1,publicKey:null};let n=/^[A-Za-z0-9:_-]{3,128}$/.test(t)&&t!=="default";if(!n)return{keyIdFormatValid:n,keyFound:!1,keyNotDeprecated:!1,keyRequesterMatches:!1,keyNotBeforeOk:!1,keyNotExpired:!1,keyLifecycleFieldsPresent:!1,publicKey:null};let a=e.trustedKeys?.[t],i=!!a;if(!i)return{keyIdFormatValid:n,keyFound:i,keyNotDeprecated:!1,keyRequesterMatches:!1,keyNotBeforeOk:!1,keyNotExpired:!1,keyLifecycleFieldsPresent:!1,publicKey:null};let c=!a.deprecated,l=!a.requesterId||a.requesterId===r,d=a.notBefore||a.validFrom,u=a.expiresAt||a.validUntil,f=typeof d=="string"&&typeof u=="string",m=!1,I=!1;if(f){let S=new Date(d),x=new Date(u);!isNaN(S.getTime())&&!isNaN(x.getTime())&&S<x&&(m=s>=S,I=s<x)}return{keyIdFormatValid:n,keyFound:i,keyNotDeprecated:c,keyRequesterMatches:l,keyNotBeforeOk:m,keyNotExpired:I,keyLifecycleFieldsPresent:f,publicKey:a.publicKey??null}}function gt(e){return(e?.entries??[]).map(t=>`${t.key}:${t.timestamp}`)}function kt(e){return e.split(`
8
+ `).filter(Boolean).map(t=>{let r=t.lastIndexOf(":");return{key:t.slice(0,r),timestamp:parseInt(t.slice(r+1),10)||0}})}function dr(e){return(e?.entries??[]).map(t=>`${t.requesterId}:${t.timestamp}`)}function ur(e){return e.split(`
9
+ `).filter(Boolean).map(t=>{let r=t.lastIndexOf(":");return{requesterId:t.slice(0,r),timestamp:parseInt(t.slice(r+1),10)||0}})}function Se({commandObj:e,pubKeyB64:t,keyStore:r,nonceDb:s,policy:o,rateLimiter:n,policyStatePath:a}){let i={valid:!1,commandId:e?.commandId,checks:{},errors:[]},c=Math.floor(Date.now()/1e3),l=new Date,d=Number.isFinite(o?.security?.maxClockSkewSec)?o.security.maxClockSkewSec:600;if(a&&o?.version!==void 0)try{let p=ke({policyObj:o,statePath:a});if(i.checks.policyVersion=p.ok,!p.ok)return i.errors.push({type:"POLICY_VERSION_INVALID",message:p.message}),i}catch{i.checks.policyVersion=!0}else i.checks.policyVersion=!0;let u=ar(e),f=e?.signature?.keyId,m=lr(r,f,e?.requesterId,l),I=!1,S=m.publicKey;if(!S&&t&&(S=t),S){let p={...e};delete p.signature,I=z({payloadObj:p,sigB64:e?.signature?.sig,pubKeyB64:S}).valid}let x=!0,T=0;if(I&&n&&typeof n.db<"u"){let p=o?.requesters?.[e.requesterId]?.rateLimit||{},g=o?.security?.defaultRateLimit||{},L=p.windowSec??g.windowSec??60,K=p.maxRequests??g.maxRequests??30,q=yt({windowSec:L,maxRequests:K,nowSec:c,requesterId:e.requesterId,existingEntries:dr(n.db)});x=q.ok,T=q.retryAfterSec,q.ok&&(n.db.entries=ur(q.updatedEntriesText))}else if(I&&n&&typeof n.checkAndRecord=="function"){let p=o?.requesters?.[e.requesterId]?.rateLimit||{},g=o?.security?.defaultRateLimit||{},L=n.checkAndRecord({requesterId:e.requesterId,nowSec:c,windowSec:p.windowSec??g.windowSec??60,maxRequests:p.maxRequests??g.maxRequests??30});x=L.ok,T=L.retryAfterSec??0}let Y=!0,ce=`${e?.requesterId}|${e?.sessionId}|${e?.nonce}`,U=3600;if(I&&x&&s)if(typeof s.checkAndRecord=="function")if(s.db){let p=Ke({ttlSec:U,nowSec:c,newKey:ce,existingEntries:gt(s.db)});Y=p.ok,p.ok&&(s.db.entries=kt(p.updatedEntriesText))}else Y=s.checkAndRecord({requesterId:e.requesterId,sessionId:e.sessionId,nonce:e.nonce}).ok;else{let p=Ke({ttlSec:U,nowSec:c,newKey:ce,existingEntries:gt(s)});Y=p.ok,p.ok&&(s.entries=kt(p.updatedEntriesText))}let le=cr(o,e??{}),w=mt({...u,cmdTimestamp:e?.timestamp??0,nowSec:c,maxClockSkewSec:d,...m,signatureValid:I,rateLimitOk:x,rateLimitRetryAfterSec:T,nonceOk:Y,...le}),h=w.stage;if(i.checks.schema=h!==0,h>=1&&(i.checks.timestamp=h!==1),h>=2&&(i.checks.keyId=h!==2),h>=2&&(i.checks.signature=h!==2&&h!==3),h>=4&&(i.checks.rateLimit=h!==4),h>=5&&(i.checks.nonce=h!==5),(h>=6||w.ok)&&(i.checks.policy=h!==6),!w.ok){let p=w.stageLabel;if(p==="schema")i.errors.push({type:"SCHEMA_ERROR",message:w.schemaError||"Schema invalid"});else if(p==="timestamp")i.errors.push({type:"TIMESTAMP_SKEW_EXCEEDED",message:`Command timestamp skew ${w.skewSec}s exceeds allowed ${d}s`});else if(p==="key"){let g=w.keyReason||"KEY_ERROR",L={KEY_ID_INVALID:`Invalid keyId '${f}'`,KEY_NOT_TRUSTED:`Key '${f}' is not in trusted key store`,KEY_DEPRECATED:`Key '${f}' is deprecated`,KEY_REQUESTER_MISMATCH:`Key '${f}' is not authorized for requester '${e?.requesterId}'`,KEY_LIFECYCLE_INVALID:`Key '${f}' must define notBefore and expiresAt`,KEY_NOT_YET_VALID:`Key '${f}' is not yet valid`,KEY_EXPIRED:`Key '${f}' has expired`};i.errors.push({type:g,message:L[g]||g})}else p==="signature"?i.errors.push({type:"SIGNATURE_INVALID",message:S?"Signature verification failed":"No public key available"}):p==="rate_limit"?i.errors.push({type:"RATE_LIMIT_EXCEEDED",message:`Rate limit exceeded. Retry after ${w.retryAfterSec}s`}):p==="nonce"?i.errors.push({type:"REPLAY_NONCE",message:"Nonce has already been used"}):p==="policy"&&w.policyResult?i.errors.push({type:w.policyResult.reason,message:w.policyResult.message}):i.errors.push({type:"VALIDATION_FAILED",message:`Failed at stage: ${p}`});return i}return i.valid=!0,i.risk=ht(e.id,e.payload?.cmd==="rm"),i.message="Command validation successful",i}import _e from"fs";import fr from"path";var we=class{constructor(t,r=3600){this.dbPath=t,this.ttlSec=r,this.db={entries:[]}}async load(){if(!_e.existsSync(this.dbPath)){this.db={entries:[]};return}try{let t=_e.readFileSync(this.dbPath,"utf8");this.db=JSON.parse(t),this.prune()}catch(t){throw new Error(`Nonce DB at ${this.dbPath} is corrupt or unreadable: ${t.message}`)}}async save(){try{let t=fr.dirname(this.dbPath);_e.existsSync(t)||_e.mkdirSync(t,{recursive:!0}),A(this.dbPath,JSON.stringify(this.db,null,2),{encoding:"utf8"})}catch(t){throw new Error(`Failed to save nonce DB: ${t.message}`)}}checkAndRecord({requesterId:t,sessionId:r,nonce:s}){let o=Math.floor(Date.now()/1e3);this.db.entries=this.db.entries.filter(a=>o-a.timestamp<=this.ttlSec);let n=`${t}|${r}|${s}`;return this.db.entries.some(a=>a.key===n)?{ok:!1,reason:"REPLAY_NONCE",message:"Nonce has already been used"}:(this.db.entries.push({key:n,timestamp:o}),{ok:!0,reason:null,message:"Nonce accepted"})}prune(){let t=Math.floor(Date.now()/1e3),r=this.db.entries.length;this.db.entries=this.db.entries.filter(o=>t-o.timestamp<=this.ttlSec);let s=this.db.entries.length;return{prunedCount:r-s,remainingCount:s}}};import ve from"fs";import pr from"path";var Ae=class{constructor(t){this.dbPath=t,this.db={entries:[]}}async load(){try{if(!ve.existsSync(this.dbPath)){this.db={entries:[]};return}let t=ve.readFileSync(this.dbPath,"utf8");this.db=JSON.parse(t),Array.isArray(this.db.entries)||(this.db={entries:[]})}catch{this.db={entries:[]}}}async save(){let t=pr.dirname(this.dbPath);ve.existsSync(t)||ve.mkdirSync(t,{recursive:!0}),A(this.dbPath,JSON.stringify(this.db,null,2),{encoding:"utf8"})}checkAndRecord({requesterId:t,nowSec:r,windowSec:s,maxRequests:o}){let n=Number.isFinite(r)?r:Math.floor(Date.now()/1e3),a=Number.isFinite(s)&&s>0?s:60,i=Number.isFinite(o)&&o>0?o:30,c=n-a;this.db.entries=this.db.entries.filter(d=>d.timestamp>=c);let l=this.db.entries.filter(d=>d.requesterId===t);if(l.length>=i){let d=l.sort((f,m)=>f.timestamp-m.timestamp)[0],u=Math.max(1,a-(n-d.timestamp));return{ok:!1,reason:"RATE_LIMIT_EXCEEDED",message:`Rate limit exceeded for '${t}' (${i}/${a}s)`,retryAfterSec:u}}return this.db.entries.push({requesterId:t,timestamp:n}),{ok:!0,reason:null,message:"Rate limit check passed",retryAfterSec:0}}};import X from"fs";import mr from"path";import yr from"crypto";function hr(e){return yr.createHash("sha256").update(e).digest("hex")}function gr(e){try{if(!X.existsSync(e))return"GENESIS";let t=X.readFileSync(e,"utf8").trim();if(!t)return"GENESIS";let r=t.split(`
10
+ `),s=r[r.length-1];try{return JSON.parse(s).hash||"GENESIS"}catch{return"GENESIS"}}catch{return"GENESIS"}}function ne(e,t){let r=mr.dirname(e);X.existsSync(r)||X.mkdirSync(r,{recursive:!0});let s;return st(e,()=>{let o=gr(e),n={...t,prevHash:o,timestamp:new Date().toISOString()};delete n.hash;let a=JSON.stringify(n),i=hr(a),c=JSON.stringify({...n,hash:i}),l="";X.existsSync(e)&&(l=X.readFileSync(e,"utf8"));try{A(e,l+c+`
11
+ `,{encoding:"utf8"})}catch(d){throw new Error(`Audit log write failed: ${d.message}`)}s={success:!0,hash:i,prevHash:o,message:"Audit entry appended"}}),s}async function Et(e){return{adapter:"noop",commandId:e.commandId||"unknown",command:e.id||"unknown",status:"completed",output:`[NOOP] Would execute: ${e.id||"unknown"} on adapter: ${e.payload?.adapter||"unknown"}`,exitCode:0,timestamp:new Date().toISOString()}}import{spawnSync as kr}from"child_process";import Ve from"path";function Er(e){if(e===void 0)return{ok:!0,args:[]};if(!Array.isArray(e))return{ok:!1,error:"payload.args must be an array"};let t=[];for(let r of e){if(typeof r!="string"&&typeof r!="number"&&typeof r!="boolean")return{ok:!1,error:"payload.args may only contain string, number, or boolean values"};t.push(String(r))}return{ok:!0,args:t}}async function It(e,t,r){let s=e.payload,o=3e4,n=1024*1024;if(s.adapter!=="shell")return{adapter:"shell",commandId:e.commandId,status:"error",error:"Adapter mismatch",exitCode:1};let a=r?.exec?.allowCmds||[];if((r?.exec?.denyCmds||[]).includes(s.cmd))return{adapter:"shell",commandId:e.commandId,status:"blocked",error:`Command '${s.cmd}' is denied`,exitCode:2};if(a.length>0&&!a.includes(s.cmd))return{adapter:"shell",commandId:e.commandId,status:"blocked",error:`Command '${s.cmd}' not in allowlist`,exitCode:2};if(!(r?.filesystem?.roots||[]).some(u=>{let f=Ve.resolve(u),m=Ve.resolve(s.cwd);return m===f||m.startsWith(f+Ve.sep)}))return{adapter:"shell",commandId:e.commandId,status:"blocked",error:`CWD '${s.cwd}' not authorized`,exitCode:2};let d=Er(s.args);if(!d.ok)return{adapter:"shell",commandId:e.commandId,status:"blocked",error:d.error,exitCode:2};try{let u=kr(s.cmd,d.args,{cwd:s.cwd,timeout:o,encoding:"utf8",maxBuffer:n,stdio:["pipe","pipe","pipe"],shell:!1});if(u.error)throw u.error;let f=`${u.stdout||""}${u.stderr||""}`,m=u.status??1;return m!==0?{adapter:"shell",commandId:e.commandId,command:s.cmd,status:"error",error:f.substring(0,n)||`Command exited with code ${m}`,exitCode:m,timestamp:new Date().toISOString()}:{adapter:"shell",commandId:e.commandId,command:s.cmd,status:"completed",output:f.substring(0,n),exitCode:0,timestamp:new Date().toISOString()}}catch(u){return{adapter:"shell",commandId:e.commandId,command:s.cmd,status:"error",error:u.message,exitCode:u.status||1,timestamp:new Date().toISOString()}}}import ie from"fs";import ee from"path";import B from"fs";import xe from"path";import Ir from"crypto";function Q(e,t){let r=t||xe.resolve("data/backups");B.existsSync(r)||B.mkdirSync(r,{recursive:!0});let s=xe.resolve(e),o=B.existsSync(s),n=null,a=null;o&&(n=B.readFileSync(s),a=Ir.createHash("sha256").update(n).digest("hex"));let i=xe.basename(s).replace(/[^a-zA-Z0-9._-]/g,"_"),c=`${Date.now()}-${a?a.slice(0,8):"new"}-${i}`,l=o?xe.join(r,c):null;return o&&n!==null&&A(l,n),{originalPath:s,backupPath:l,existed:o,hash:a,createdAt:new Date().toISOString()}}function M(e){if(!e)return{restored:!1,error:"No backup metadata"};let{originalPath:t,backupPath:r,existed:s}=e;if(!s)try{return B.existsSync(t)&&B.unlinkSync(t),{restored:!0,action:"deleted"}}catch(o){return{restored:!1,error:o.message}}if(!r||!B.existsSync(r))return{restored:!1,error:"Backup file not found at: "+r};try{let o=B.readFileSync(r);return A(t,o),{restored:!0,action:"restored"}}catch(o){return{restored:!1,error:o.message}}}var Sr=10*1024*1024;function _r(e,t){return e?ee.isAbsolute(e)?ee.resolve(e):ee.resolve(t||process.cwd(),e):null}function wr(e,t){let r=ee.resolve(e);return t.some(s=>{let o=ee.resolve(s);return r===o||r.startsWith(o+ee.sep)})}function vr(e,t){for(let r of t||[])if(new RegExp("^"+r.replace(/\./g,"\\.").replace(/\*\*/g,".*").replace(/\*/g,"[^/\\\\]*")+"$").test(e))return r;return null}function Z(e,t,r,s=2){return{adapter:"file",commandId:e.commandId,status:"blocked",errorCode:t,error:r,exitCode:s}}function F(e,t,r,s=null,o=1){return{adapter:"file",commandId:e.commandId,status:"error",errorCode:t,error:r,backup:s?be(s):null,exitCode:o}}function be(e){return e?{path:e.backupPath,existed:e.existed,hash:e.hash,createdAt:e.createdAt}:null}async function St(e,t,r){let s=e.payload,o=s.action,n=s.cwd||process.cwd(),a=_r(s.target,n);if(!o)return Z(e,"FILE_NO_ACTION","payload.action is required");if(!a&&o!=="noop")return Z(e,"FILE_NO_TARGET","payload.target is required");let i=r?.filesystem?.roots||[];if(i.length===0)return Z(e,"FILE_NO_ROOTS","No filesystem roots defined for requester");if(!wr(a,i))return Z(e,"FILE_OUTSIDE_ROOT",`'${a}' is outside allowed roots`);let c=vr(a,r?.filesystem?.denyPatterns);if(c)return Z(e,"FILE_PATH_DENIED",`'${a}' matches deny pattern: ${c}`);switch(o){case"read":return Ar(e,a);case"write":return xr(e,a,s);case"patch":return br(e,a,s);case"delete":return Rr(e,a);default:return Z(e,"FILE_UNKNOWN_ACTION",`Unknown action: '${o}'`)}}function Ar(e,t){if(!ie.existsSync(t))return F(e,"FILE_NOT_FOUND",`Not found: ${t}`);try{let r=ie.statSync(t);if(r.size>Sr)return F(e,"FILE_TOO_LARGE","File exceeds 10 MB read limit");let s=ie.readFileSync(t,"utf8");return{adapter:"file",action:"read",commandId:e.commandId,status:"completed",target:t,output:s,bytesRead:r.size,exitCode:0}}catch(r){return F(e,"FILE_READ_ERROR",r.message)}}function xr(e,t,r){let s=r.content;if(s==null)return F(e,"FILE_MISSING_CONTENT","payload.content is required for write");let o=Be(t);try{return A(t,s,{encoding:"utf8"}),{adapter:"file",action:"write",commandId:e.commandId,status:"completed",target:t,backup:be(o),output:`Wrote ${Buffer.byteLength(s,"utf8")} bytes to ${t}`,exitCode:0}}catch(n){return M(o),F(e,"FILE_WRITE_ERROR",n.message,o)}}function br(e,t,r){let s=r.content;if(s==null)return F(e,"FILE_MISSING_CONTENT","payload.content is required for patch");let o=Be(t);try{return A(t,s,{encoding:"utf8"}),{adapter:"file",action:"patch",commandId:e.commandId,status:"completed",target:t,backup:be(o),output:`Patched ${t} (${Buffer.byteLength(s,"utf8")} bytes)`,exitCode:0}}catch(n){return M(o),F(e,"FILE_PATCH_ERROR",n.message,o)}}function Rr(e,t){if(!ie.existsSync(t))return F(e,"FILE_NOT_FOUND",`Not found: ${t}`);let r=Be(t);try{return ie.unlinkSync(t),{adapter:"file",action:"delete",commandId:e.commandId,status:"completed",target:t,backup:be(r),output:`Deleted ${t}`,exitCode:0}}catch(s){return M(r),F(e,"FILE_DELETE_ERROR",s.message,r)}}function Be(e){try{return Q(e)}catch{return null}}async function _t(e,t,r){let s=Date.now();try{let o=e.id||"";if(!o.toUpperCase().startsWith("OBSERVE"))return{adapter:"observer",commandId:e.commandId,status:"error",error:`Observer adapter only handles OBSERVE_* commands, got '${o}'`,exitCode:1};let{source:n,context:a,issueType:i,description:c,severity:l,metadata:d}=e.payload||{};if(!i||!c)return{adapter:"observer",commandId:e.commandId,status:"error",error:"Observer payload must include issueType and description",exitCode:1};let u=["low","medium","high","critical"];return l&&!u.includes(l)?{adapter:"observer",commandId:e.commandId,status:"error",error:`Invalid severity '${l}'. Must be one of: ${u.join(", ")}`,exitCode:1}:{adapter:"observer",commandId:e.commandId,status:"recorded",timestamp:new Date().toISOString(),requesterId:e.requesterId,observation:{source:n||"unknown",context:a||"unknown",issueType:i,description:c,severity:l||"info",metadata:d||{}},duration_ms:Date.now()-s,exitCode:0}}catch(o){return{adapter:"observer",commandId:e.commandId,status:"error",error:`Observer execution failed: ${o.message}`,exitCode:9}}}var wt={noop:Et,shell:It,file:St,observer:_t};function Lr(e){return wt[e]}async function vt(e,t,r,s){let o=Lr(e);if(!o)return{adapter:e,commandId:t.commandId,status:"error",error:`Adapter '${e}' not found`,exitCode:1};try{return await o(t,r,s)}catch(n){return{adapter:e,commandId:t.commandId,status:"error",error:`Adapter execution failed: ${n.message}`,exitCode:9}}}var At=Object.keys(wt);import Lt from"fs";import Cr from"path";import xt from"fs";import Nr from"path";var Dr=/^[A-Za-z0-9:_-]{3,128}$/;function Or(e){return typeof e=="string"&&Dr.test(e)&&e!=="default"}function bt(e){let t=Nr.resolve(e);if(!xt.existsSync(t))return{ok:!1,reason:"KEY_STORE_MISSING",message:`Key store not found: ${t}`,store:null};try{let r=xt.readFileSync(t,"utf-8"),s=JSON.parse(r);return!s||typeof s!="object"||typeof s.trustedKeys!="object"?{ok:!1,reason:"KEY_STORE_INVALID",message:`Invalid key store format: ${t}`,store:null}:{ok:!0,reason:null,message:"Key store loaded",store:s}}catch(r){return{ok:!1,reason:"KEY_STORE_INVALID_JSON",message:`Unable to parse key store: ${r.message}`,store:null}}}function Rt({keyStore:e,keyId:t,requesterId:r,now:s=new Date}){if(!e||typeof e!="object")return{ok:!1,reason:"KEY_STORE_UNAVAILABLE",message:"Trusted key store is not available",publicKey:null};if(!Or(t))return{ok:!1,reason:"KEY_ID_INVALID",message:`Invalid keyId '${t}'. Use versioned key IDs like 'agent:gpt-v1-2026Q1'`,publicKey:null};let o=e.trustedKeys?.[t];if(!o)return{ok:!1,reason:"KEY_NOT_TRUSTED",message:`Key '${t}' is not in trusted key store`,publicKey:null};if(o.deprecated)return{ok:!1,reason:"KEY_DEPRECATED",message:`Key '${t}' is deprecated`,publicKey:null};if(o.requesterId&&o.requesterId!==r)return{ok:!1,reason:"KEY_REQUESTER_MISMATCH",message:`Key '${t}' is not authorized for requester '${r}'`,publicKey:null};let n=o.notBefore||o.validFrom,a=o.expiresAt||o.validUntil;if(typeof n!="string"||typeof a!="string")return{ok:!1,reason:"KEY_LIFECYCLE_INVALID",message:`Key '${t}' must define lifecycle fields 'notBefore' and 'expiresAt'`,publicKey:null};let i=new Date(n),c=new Date(a);return Number.isNaN(i.getTime())||Number.isNaN(c.getTime())?{ok:!1,reason:"KEY_LIFECYCLE_INVALID",message:`Key '${t}' has invalid lifecycle timestamp(s)`,publicKey:null}:i>=c?{ok:!1,reason:"KEY_LIFECYCLE_INVALID",message:`Key '${t}' has notBefore >= expiresAt`,publicKey:null}:s<i?{ok:!1,reason:"KEY_NOT_YET_VALID",message:`Key '${t}' not valid until ${n}`,publicKey:null}:s>c?{ok:!1,reason:"KEY_EXPIRED",message:`Key '${t}' expired on ${a}`,publicKey:null}:!o.publicKey||typeof o.publicKey!="string"?{ok:!1,reason:"KEY_CONFIG_INVALID",message:`Trusted key '${t}' is missing publicKey`,publicKey:null}:{ok:!0,reason:null,message:"Trusted key resolved",publicKey:o.publicKey}}function Nt({policyObj:e,keyStore:t,policySigPath:r="./config/policy.sig.json",allowUnsigned:s=!1}){let o=Cr.resolve(r);if(!Lt.existsSync(o))return s?{ok:!0,skipped:!0,reason:"POLICY_SIGNATURE_SKIPPED",message:`Policy signature not found: ${o} (allowed by flag)`}:{ok:!1,skipped:!1,reason:"POLICY_SIGNATURE_MISSING",message:`Policy signature file not found: ${o}`};let n;try{n=JSON.parse(Lt.readFileSync(o,"utf-8"))}catch(c){return{ok:!1,skipped:!1,reason:"POLICY_SIGNATURE_INVALID",message:`Unable to parse policy signature file: ${c.message}`}}if(!n||n.alg!=="ed25519"||typeof n.keyId!="string"||typeof n.sig!="string")return{ok:!1,skipped:!1,reason:"POLICY_SIGNATURE_INVALID",message:"Policy signature envelope must include {alg, keyId, sig}"};if(!t)return{ok:!1,skipped:!1,reason:"POLICY_SIGNER_KEY_STORE_UNAVAILABLE",message:"Trusted key store is required for policy signature verification"};let a=Rt({keyStore:t,keyId:n.keyId,requesterId:void 0});if(!a.ok)return{ok:!1,skipped:!1,reason:"POLICY_SIGNER_NOT_TRUSTED",message:a.message};let i=z({payloadObj:e,sigB64:n.sig,pubKeyB64:a.publicKey});return i.valid?{ok:!0,skipped:!1,reason:null,message:"Policy signature verified",keyId:n.keyId}:{ok:!1,skipped:!1,reason:"POLICY_SIGNATURE_INVALID",message:i.message}}import Pr from"crypto";import Tr from"path";var Ye=class{constructor(t){this.dbPath=t||Tr.resolve("data/checkpoints.db.json"),this.store={checkpoints:{},tokens:{}},this._load()}_load(){let t=ot(this.dbPath);t&&(this.store=t,this.store.checkpoints=this.store.checkpoints||{},this.store.tokens=this.store.tokens||{})}_save(){let t=JSON.stringify(this.store,null,2);A(this.dbPath,t,{encoding:"utf8"})}saveCheckpoint(t,r){this.store.checkpoints[t]={jobId:t,...r,updatedAt:Date.now()},this._save()}getCheckpoint(t){return this.store.checkpoints[t]||null}getAllCheckpoints(){return Object.values(this.store.checkpoints)}removeCheckpoint(t){return this.store.checkpoints[t]?(delete this.store.checkpoints[t],this._save(),!0):!1}saveToken(t,r){this.store.tokens[t]={tokenId:t,...r,createdAt:Date.now()},this._save()}getToken(t){return this.store.tokens[t]||null}getAllTokens(){return Object.values(this.store.tokens)}removeToken(t){return this.store.tokens[t]?(delete this.store.tokens[t],this._save(),!0):!1}},Me=null;function Dt(e){return Me||(Me=new Ye(e)),Me}var He=class{constructor(t){this.store=Dt(t),this._pendingResolvers=new Map}createToken(t,r={}){let s=Pr.randomBytes(16).toString("hex"),o={jobId:t,context:r,status:"pending",expiresAt:Date.now()+1440*60*1e3};return this.store.saveToken(s,o),s}awaitApproval(t){let r=this.store.getToken(t);return r?r.status!=="pending"?Promise.reject(new Error(`Approval token ${t} is no longer pending (status: ${r.status})`)):Date.now()>r.expiresAt?(this.store.removeToken(t),Promise.reject(new Error(`Approval token ${t} expired`))):new Promise((s,o)=>{this._pendingResolvers.set(t,{resolve:s,reject:o})}):Promise.reject(new Error(`Approval token ${t} not found`))}approve(t,r={}){let s=this.store.getToken(t);if(!s)throw new Error("Token not found");if(s.status!=="pending")throw new Error("Token not pending");this.store.saveToken(t,{...s,status:"approved",approverData:r,resolvedAt:Date.now()});let o=this._pendingResolvers.get(t);return o&&(o.resolve({approved:!0,approverData:r}),this._pendingResolvers.delete(t)),!0}deny(t,r="Manually denied"){let s=this.store.getToken(t);if(!s)throw new Error("Token not found");if(s.status!=="pending")throw new Error("Token not pending");this.store.saveToken(t,{...s,status:"denied",reason:r,resolvedAt:Date.now()});let o=this._pendingResolvers.get(t);return o&&(o.reject(new Error(`Approval denied: ${r}`)),this._pendingResolvers.delete(t)),!0}},Ue=null;function Ot(e){return Ue||(Ue=new He(e)),Ue}var We={DEBUG:0,INFO:1,WARN:2,ERROR:3};function ae({level:e="INFO",maxHistory:t=500,silent:r=!1}={}){let s=We[e]??We.INFO,o=[];function n(i,c,l,d){let u={ts:new Date().toISOString(),level:i,scope:c,message:l,...d!==void 0?{meta:d}:{}};if(o.length>=t&&o.shift(),o.push(u),!r&&We[i]>=s){let f=d!==void 0?`[${i}] [${c}] ${l} ${JSON.stringify(d)}`:`[${i}] [${c}] ${l}`;process.stderr.write(f+`
12
+ `)}}function a(i){return{debug:(c,l)=>n("DEBUG",i,c,l),info:(c,l)=>n("INFO",i,c,l),warn:(c,l)=>n("WARN",i,c,l),error:(c,l)=>n("ERROR",i,c,l)}}return{scope:a,debug:(i,c)=>n("DEBUG","lbe",i,c),info:(i,c)=>n("INFO","lbe",i,c),warn:(i,c)=>n("WARN","lbe",i,c),error:(i,c)=>n("ERROR","lbe",i,c),exportLogs:()=>[...o],clearHistory:()=>{o.length=0},get historyLength(){return o.length}}}var $r=process.env.LBE_LOG_LEVEL||"INFO",Fr=process.env.LBE_LOG_SILENT==="1",io=ae({level:$r,silent:Fr});function te(e){if(e===null||typeof e!="object"||Object.isFrozen(e))return e;Object.freeze(e);for(let t of Object.getOwnPropertyNames(e)){let r=e[t];typeof r=="object"&&r!==null&&!Object.isFrozen(r)&&te(r)}return e}import Je from"fs";import Ge from"path";var W=class extends Error{constructor(t,r,s){super(t),this.name="InvariantGateError",this.checks=r,this.failures=s}};function ze(e,t,r){let s={},o=[],n=!!(t&&typeof t.version<"u"&&t.requesters&&typeof t.requesters=="object"&&t.default==="DENY");s.policy_structure=n,n||o.push("Policy missing required fields: version, requesters, default=DENY");let a=!!(r&&typeof r=="object"&&Object.keys(r).length>0);s.keys_available=a,a||o.push("No trusted keys loaded \u2014 provide config/keys.json");let i=Ge.dirname(e.auditLog);s.audit_log_writable=je(i),s.audit_log_writable||o.push(`Audit log directory not writable: ${i}`);let c=Ge.dirname(e.nonceDb);s.nonce_db_writable=je(c),s.nonce_db_writable||o.push(`Nonce DB directory not writable: ${c}`);let l=Ge.dirname(e.rateLimit);if(s.rate_limit_writable=je(l),s.rate_limit_writable||o.push(`Rate-limit DB directory not writable: ${l}`),t&&t.requesters){let d=[];for(let[u,f]of Object.entries(t.requesters))for(let m of f.allowAdapters||[])At.includes(m)||d.push(`${u}\u2192${m}`);s.adapter_chain_valid=d.length===0,s.adapter_chain_warnings=d.length>0?d:[]}else s.adapter_chain_valid=!0,s.adapter_chain_warnings=[];return s.secret_key_present=!!e.secretKey,s.secret_key_present||o.push("secretKey not provided to createLBE()"),{ok:o.length===0,checks:s,failures:o}}function Re(e,t,r){let s=ze(e,t,r);if(!s.ok){let o=s.failures.length;throw new W(`Invariant gate: ${o} violation${o===1?"":"s"} \u2014 ${s.failures.join(" | ")}`,s.checks,s.failures)}return s}function Kr(e){try{return Je.accessSync(e,Je.constants.W_OK),!0}catch{return!1}}function je(e){if(Kr(e))return!0;try{return Je.mkdirSync(e,{recursive:!0}),!0}catch{return!1}}function Vr(e,t){let r=_.resolve(e||process.cwd());if(!t||t==="local"){let s=Le.createHash("sha256").update(r).digest("hex").slice(0,16);return _.join(qr.homedir(),".lbe","workspaces",s)}if(t==="workspace")return _.join(r,".lbe");if(t&&typeof t=="object"&&t.adapter)return null;throw new Error(`createLBE: unknown state option: ${JSON.stringify(t)}`)}var Br={patch_file:"PATCH_FILE",write_file:"WRITE_FILE",read_file:"READ_FILE",delete_file:"DELETE_FILE",run_shell:"RUN_SHELL",echo:"ECHO"},Mr={PATCH_FILE:"file",WRITE_FILE:"file",READ_FILE:"file",DELETE_FILE:"file",RUN_SHELL:"shell",ECHO:"noop"},Yr=new Set(["HIGH","CRITICAL"]);function Xe(e){return Le.createHash("sha256").update(JSON.stringify(e)).digest("hex")}function Ur(e,t){if(!t?.requireApproval)return!1;let r=t.requireApproval;return r===!0?!0:Array.isArray(r)?r.includes(e)||r.includes("*")||Yr.has(e)&&r.includes("HIGH+"):!1}function Tt(e={}){if(e.rootDir&&!e.secretKey){let b=ge(),E=e.keyId||"sdk-auto-key";e={defaultActor:"agent:sdk",logLevel:"WARN",...e,secretKey:b.secretKey,keyId:E,keyStore:e.keyStore||Pt({publicKey:b.publicKey,keyId:E}),policy:e.policy||{version:1,default:"DENY",requesters:{"agent:sdk":{allowCommands:["write_file","read_file","patch_file","delete_file"],allowAdapters:["file"],filesystem:{roots:[e.rootDir],denyPatterns:["*.key","*.env","*.secret"]}}}}}}let{secretKey:t,keyId:r="sdk-key-v1",sessionId:s,defaultActor:o="agent:sdk",policy:n,keyStore:a,policyPath:i,keysStorePath:c,policySigPath:l,policyStatePath:d,nonceDbPath:u,rateLimitDbPath:f,auditLogPath:m,backupDir:I,allowUnsignedPolicy:S=!1,state:x="local",rootDir:T,logLevel:Y=process.env.LBE_LOG_LEVEL||"INFO",logSilent:ce=process.env.LBE_LOG_SILENT==="1"}=e,U=n&&typeof n=="object"?n:null,le=a&&typeof a=="object"?a:null,w=Vr(T,x),h={secretKey:t,policy:i||_.resolve("config/policy.default.json"),keys:c||_.resolve("config/keys.json"),policySig:l||_.resolve("config/policy.sig.json"),policyState:d||_.join(w,"policy.state.json"),nonceDb:u||_.join(w,"nonce.db.json"),rateLimit:f||_.join(w,"rate-limit.db.json"),auditLog:m||_.join(w,"audit.log.jsonl"),backupDir:I||_.join(w,"backups")},p=ae({level:Y,silent:ce}),g=p.scope("Executor"),L=p.scope("Validator"),K=p.scope("Policy");async function q({actor:b,intent:E,target:k,content:de,args:$t=[],transaction:Ft={}}){let re={validate:!0,backup:!0,rollbackOnFailure:!0,audit:!0,...Ft};g.info("execute() called",{actor:b,intent:E,target:k});let ue=Br[E]||E.toUpperCase().replace(/-/g,"_"),G=Mr[ue]||"noop",D;if(U)D=U,K.debug("Using inline policy",{version:D.version});else try{D=JSON.parse(Ct.readFileSync(h.policy,"utf8")),K.debug("Policy loaded from file",{version:D.version})}catch(y){return K.error("Policy load failed",{error:y.message}),{ok:!1,stage:"policy_load",error:"POLICY_LOAD_FAILED",message:y.message}}let H;if(le)H=le,L.debug("Using inline keyStore");else{let y=bt(h.keys);H=y.ok?y.store:null,L.debug("Keys loaded from file",{ok:y.ok})}te(D),H&&te(H);try{let y=Re(h,D,H);L.debug("Invariant gate passed",y.checks)}catch(y){if(y instanceof W)return L.error("Invariant gate failed",{failures:y.failures}),{ok:!1,stage:"invariant_gate",error:"INVARIANT_GATE_FAILED",message:y.message,checks:y.checks,failures:y.failures};throw y}if(!U){let y=Nt({policyObj:D,keyStore:H,policySigPath:h.policySig,allowUnsigned:S});if(!y.ok)return K.error("Policy signature invalid",{reason:y.reason}),{ok:!1,stage:"policy_sig",error:y.reason,message:y.message};let N=ke({policyObj:D,statePath:h.policyState});if(!N.ok)return K.error("Policy version guard failed",{reason:N.reason}),{ok:!1,stage:"policy_version",error:N.reason,message:N.message};K.debug("Policy signature and version valid")}if(!t)return{ok:!1,stage:"sign",error:"NO_SECRET_KEY",message:"createLBE requires secretKey"};let Kt=Math.floor(Date.now()/1e3),qt=Le.randomBytes(32).toString("hex"),Vt=s||`sdk-${Date.now()}`,O=Le.randomUUID();g.debug("Proposal built",{commandId:O,commandId_str:ue,adapter:G});let fe={id:ue,commandId:O,requesterId:b,sessionId:Vt,timestamp:Kt,nonce:qt,requires:["policy","signature"],payload:{adapter:G,action:E.includes("_")?E.split("_")[0]:E,target:k?_.resolve(k):null,content:de||null,args:$t,cwd:k?_.dirname(_.resolve(k)):process.cwd()}},pe=se({payloadObj:fe,secretKeyB64:t});if(pe.error)return g.error("Signing failed",{error:pe.error}),{ok:!1,stage:"sign",commandId:O,error:"SIGN_FAILED",message:pe.error};let me={...fe,signature:{alg:"ed25519",keyId:r,sig:pe.signature}},Ne=new we(h.nonceDb);await Ne.load();let De=new Ae(h.rateLimit);await De.load();let C=Se({commandObj:me,keyStore:H,nonceDb:Ne,policy:D,rateLimiter:De,policyStatePath:U?null:h.policyState}),ye=async()=>{await Ne.save().catch(()=>{}),await De.save().catch(()=>{})};if(!C.valid)return L.warn("Validation failed",{error:C.errors[0]?.type,checks:C.checks}),await ye(),re.audit&&ne(h.auditLog,{commandId:O,status:"rejected",requesterId:b,payloadHash:Xe(me),reason:C.errors[0]?.type,intent:E}),{ok:!1,stage:"validate",commandId:O,error:C.errors[0]?.type,message:C.errors[0]?.message,checks:C.checks,operationLog:p.exportLogs()};L.info("Validation passed",{risk:C.risk,checks:C.checks});let V=C.risk||"LOW",Qe=D.requesters?.[b];if(Ur(V,Qe)){g.warn("Approval required",{risk:V,commandId:O}),await ye();let N=Ot(h.policyState).createToken(O,{actor:b,intent:E,target:k,risk:V,commandId:ue});return{ok:!1,stage:"approval_pending",commandId:O,approvalToken:N,risk:V,message:`${V} risk operation requires approval. Token: ${N}`,operationLog:p.exportLogs()}}let P=null;if(re.backup&&k){let y=["write","patch","delete"].includes(fe?.payload?.action??E.split("_")[0]);try{P=Q(_.resolve(k),h.backupDir),g.debug("Backup created",{existed:P.existed,path:P.backupPath})}catch(N){if(y)return g.error("Backup failed \u2014 aborting write transaction",{error:N.message}),await ye(),{ok:!1,stage:"backup",error:"BACKUP_FAILED",message:N.message};g.warn("Backup failed (non-fatal for read)",{error:N.message})}}g.info("Executing adapter",{adapter:G,target:k});let R;try{R=await vt(G,me,D,Qe)}catch(y){R={adapter:G,commandId:O,status:"error",error:y.message,exitCode:1}}g.debug("Adapter returned",{status:R.status,exitCode:R.exitCode});let Oe=R.status==="error"||R.exitCode!==0&&R.exitCode!==void 0,j=null;if(re.validate&&k&&!Oe){let y=["write","patch"],N=fe.payload.action;if(y.includes(N)){let Ze=Ct.existsSync(_.resolve(k));j={ok:Ze,check:"target_exists",target:k},Ze?g.debug("Post-execution validation passed"):(g.error("Post-execution validation failed \u2014 target missing after write",{target:k}),R.status="error")}}let J=null;if((Oe||j&&!j.ok)&&re.rollbackOnFailure&&P)try{J=M(P),g.warn("Rollback executed",J)}catch(y){J={restored:!1,error:y.message},g.error("Rollback failed",{error:y.message})}re.audit&&ne(h.auditLog,{commandId:O,status:J?.restored?"rolled_back":R.status||"completed",requesterId:b,payloadHash:Xe(me),executionHash:Xe(R),adapter:G,intent:E,riskLevel:V,exitCode:R.exitCode||0,rolledBack:J?.restored||!1}),await ye();let he=!Oe&&(!j||j.ok);return g.info("execute() complete",{ok:he,stage:he?"executed":"failed",risk:V}),{ok:he,commandId:O,intent:E,actor:b,target:k,risk:V,stage:he?"executed":"failed",status:R.status,output:R.output||null,exitCode:R.exitCode??0,checks:C.checks,backup:P?{path:P.backupPath,existed:P.existed,hash:P.hash}:null,rollback:J,postValidation:j,operationLog:Y==="DEBUG"?p.exportLogs():void 0}}return{execute:q,exportLogs:()=>p.exportLogs(),async writeFile(b,E){let k=await q({actor:o,intent:"write_file",target:b,content:E,transaction:{backup:!0,rollbackOnFailure:!0,audit:!0}});if(!k.ok){let de=new Error(`LBE write failed [${k.error||k.stage}]${k.message?": "+k.message:""}`);throw de.lbeResult=k,de}return k},async readFile(b){let E=await q({actor:o,intent:"read_file",target:b,transaction:{audit:!0}});if(!E.ok){let k=new Error(`LBE read failed [${E.error||E.stage}]${E.message?": "+E.message:""}`);throw k.lbeResult=E,k}return E.output}}}function Hr(e,t={}){let{audit:r=!1,rollback:s=!1}=t,o=_.resolve(e),n=Tt({rootDir:o,state:t.state||"local",logSilent:!0}),a=l=>_.isAbsolute(l)?l:_.join(o,l),i={backup:s,rollbackOnFailure:s,audit:r};function c(l,d){let u=new Error(`sandbox.${l} blocked [${d.error}]${d.message?": "+d.message:""}`);return u.lbeResult=d,u}return{async write(l,d){let u=await n.execute({actor:"agent:sdk",intent:"write_file",target:a(l),content:d,transaction:i});if(!u.ok)throw c("write",u)},async read(l){let d=await n.execute({actor:"agent:sdk",intent:"read_file",target:a(l),transaction:{audit:r}});if(!d.ok)throw c("read",d);return d.output},async patch(l,d){let u=await n.execute({actor:"agent:sdk",intent:"patch_file",target:a(l),content:d,transaction:i});if(!u.ok)throw c("patch",u)},lbe:n}}function Pt({publicKey:e,keyId:t,validDays:r=365}){let s=new Date,o=new Date(s.getTime()+r*24*3600*1e3);return{defaultKeyId:t,trustedKeys:{[t]:{publicKey:e,notBefore:s.toISOString(),expiresAt:o.toISOString()}}}}export{W as InvariantGateError,ne as appendAudit,Re as assertInvariants,ze as checkInvariants,Q as createBackup,Pt as createKeyStore,Tt as createLBE,ae as createLogger,te as deepFreeze,ge as generateKeyPair,nr as getRuntimeInfo,ir as loadWasmEngine,M as restoreBackup,Hr as sandbox,se as signEd25519,Se as validateCommand,z as verifyEd25519};