deadbyte-mcp 0.15.48 → 0.15.49
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/MANIFEST.SHA256 +48 -22
- package/PROVENANCE.json +11 -22
- package/README.txt +18 -18
- package/RELEASE.json +368 -378
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +2 -2
- package/bench/r121-epoch-readhead.mjs +54 -0
- package/bench/r124-epoch-predecessor.mjs +53 -0
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/HARNESS2-ADAPTATION-20261020.md +24 -0
- package/docs/R128-RELEASE-CANDIDATE.md +26 -0
- package/package.json +1 -1
- package/scripts/npm-release-r48.mjs +0 -4
- package/scripts/verify-authority-epoch-history.mjs +38 -0
- package/src/activation-continuity-engine-r65.mjs +9 -15
- package/src/agent-eval-r42.mjs +30 -0
- package/src/authority-continuity-r65.mjs +182 -13
- package/src/autonomous-local-supervisor.mjs +77 -47
- package/src/deadbyte-cli.mjs +18 -28
- package/src/remote-live-log.mjs +15 -8
- package/src/remote-log-decode-r60.mjs +65 -0
- package/src/remote-session-guardian.mjs +4 -5
- package/src/version.mjs +1 -1
- package/test/autonomous-local-supervisor.test.mjs +1 -1
- package/test/harness2-eval-context-isolation.test.mjs +90 -0
- package/test/r108-remote-powershell-path.test.mjs +59 -0
- package/test/r110-supervisor-powershell-path.test.mjs +28 -0
- package/test/r111-supervisor-marker-cas.test.mjs +116 -0
- package/test/r112-supervisor-claim-cas.test.mjs +63 -0
- package/test/r113-supervisor-cleanup-terminality.test.mjs +100 -0
- package/test/r114-supervisor-absence-proof.test.mjs +45 -0
- package/test/r115-posix-continuity-entrypoint.test.mjs +17 -0
- package/test/r116-authority-crash-matrix.test.mjs +83 -0
- package/test/r117-concurrent-orphan-recovery.test.mjs +54 -0
- package/test/r118-cross-process-orphan-recovery.test.mjs +99 -0
- package/test/r119-recovery-crash-after-commit.test.mjs +91 -0
- package/test/r120-authority-history-audit.test.mjs +103 -0
- package/test/r121-signed-transition-invariant.test.mjs +132 -0
- package/test/r122-epoch-bounded-read.test.mjs +58 -0
- package/test/r123-recovery-bounded-read.test.mjs +64 -0
- package/test/r124-live-log-integrity.test.mjs +66 -0
- package/test/r124-live-log-tail-boundaries.test.mjs +57 -0
- package/test/r124-signed-predecessor-shape.test.mjs +66 -0
- package/test/r125-restore-binding-types.test.mjs +63 -0
- package/test/r126-restore-policy-envelope.test.mjs +89 -0
- package/test/r127-restore-append-policy-boundary.test.mjs +25 -0
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/r65-authority-epoch-store.test.mjs +9 -6
- package/test/r74-prepublish-contract.test.mjs +3 -1
- package/scripts/npm-release-readiness-r108.mjs +0 -134
|
@@ -214,10 +214,63 @@ const durablePublicKeys=new Map();
|
|
|
214
214
|
const RESTORE_BINDING_FIELDS=Object.freeze(['prior_grant_sha256','prior_grant_instance_nonce','predecessor_policy_envelope_sha256','successor_policy_envelope_sha256','candidate_manifest_sha256','continuity_subject_sha256']);
|
|
215
215
|
function normalizeRestoreBinding(value,{required=false}={}){
|
|
216
216
|
if(value===undefined||value===null){if(required)throw new Error('authority epoch transaction restore binding required');return null;}
|
|
217
|
-
if(!plain(value)||Object.keys(value).length!==RESTORE_BINDING_FIELDS.length||
|
|
217
|
+
if(!plain(value)||Object.keys(value).length!==RESTORE_BINDING_FIELDS.length||
|
|
218
|
+
RESTORE_BINDING_FIELDS.some(key=>typeof value[key]!=='string'||!SHAPAT.test(value[key])))
|
|
218
219
|
throw new Error('authority epoch transaction restore binding invalid');
|
|
219
220
|
for(const key of Object.keys(value))if(!RESTORE_BINDING_FIELDS.includes(key))throw new Error('authority epoch transaction restore binding invalid');
|
|
220
|
-
return Object.freeze(Object.fromEntries(RESTORE_BINDING_FIELDS.map(key=>[key,
|
|
221
|
+
return Object.freeze(Object.fromEntries(RESTORE_BINDING_FIELDS.map(key=>[key,value[key]])));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function assertEpochActionIdentity(current){
|
|
225
|
+
const action=current?.action;
|
|
226
|
+
if(!DURABLE_EPOCH_ACTIONS.has(action)||
|
|
227
|
+
(action.startsWith('transaction_')?
|
|
228
|
+
typeof current.update_id!=='string'||!/^[A-Za-z0-9._-]{1,96}$/.test(current.update_id):
|
|
229
|
+
current.update_id!==null))
|
|
230
|
+
throw new Error('authority epoch transition invalid');
|
|
231
|
+
if(action==='transaction_restore')normalizeRestoreBinding(current.restore_binding,{required:true});
|
|
232
|
+
else if(current.restore_binding!==null)throw new Error('authority epoch transition invalid');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function assertEpochPredecessorIdentity(event){
|
|
236
|
+
if(event.schema!==DURABLE_EPOCH_SCHEMA||
|
|
237
|
+
typeof event.grant_sha256!=='string'||!SHAPAT.test(event.grant_sha256)||
|
|
238
|
+
typeof event.grant_instance_nonce!=='string'||!SHAPAT.test(event.grant_instance_nonce)||
|
|
239
|
+
(event.seq===1?event.previous_event_sha256!==null:
|
|
240
|
+
typeof event.previous_event_sha256!=='string'||!SHAPAT.test(event.previous_event_sha256))||
|
|
241
|
+
((event.policy_sha256!==undefined||event.policy_envelope_sha256!==undefined)&&
|
|
242
|
+
(typeof event.policy_sha256!=='string'||!SHAPAT.test(event.policy_sha256)||
|
|
243
|
+
typeof event.policy_envelope_sha256!=='string'||!SHAPAT.test(event.policy_envelope_sha256))))
|
|
244
|
+
throw new Error('authority epoch transition invalid');
|
|
245
|
+
assertEpochActionIdentity(event);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function assertEpochTransition(current,previous){
|
|
249
|
+
assertEpochActionIdentity(current);
|
|
250
|
+
const action=current.action;
|
|
251
|
+
if(!previous){
|
|
252
|
+
if(action.startsWith('transaction_'))throw new Error('authority epoch transition invalid');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if(action==='transaction_revoke'&&
|
|
256
|
+
(!['grant','transaction_restore'].includes(previous.action)||
|
|
257
|
+
current.grant_sha256!==previous.grant_sha256||
|
|
258
|
+
current.grant_instance_nonce!==previous.grant_instance_nonce))
|
|
259
|
+
throw new Error('authority epoch transition invalid');
|
|
260
|
+
if(action==='transaction_restore'){
|
|
261
|
+
const binding=normalizeRestoreBinding(current.restore_binding,{required:true});
|
|
262
|
+
if(previous.action!=='transaction_revoke'||current.update_id!==previous.update_id||
|
|
263
|
+
binding.prior_grant_sha256!==previous.grant_sha256||
|
|
264
|
+
binding.prior_grant_instance_nonce!==previous.grant_instance_nonce||
|
|
265
|
+
current.grant_instance_nonce===previous.grant_instance_nonce||
|
|
266
|
+
(previous.policy_envelope_sha256!==undefined&&
|
|
267
|
+
binding.predecessor_policy_envelope_sha256!==previous.policy_envelope_sha256)||
|
|
268
|
+
(current.policy_envelope_sha256!==undefined&&
|
|
269
|
+
binding.successor_policy_envelope_sha256!==current.policy_envelope_sha256))
|
|
270
|
+
throw new Error('authority epoch transition invalid');
|
|
271
|
+
}
|
|
272
|
+
if(action==='grant'&¤t.grant_instance_nonce===previous.grant_instance_nonce)
|
|
273
|
+
throw new Error('authority epoch transition invalid');
|
|
221
274
|
}
|
|
222
275
|
|
|
223
276
|
async function durableEpochKeys(runtimeDir,{privateRequired=false}={}){
|
|
@@ -431,12 +484,40 @@ async function acquireEpochLock(dir,{allowStaleRecovery=true,onLockDispositionSt
|
|
|
431
484
|
}
|
|
432
485
|
throw new Error('authority epoch mutation locked');
|
|
433
486
|
}
|
|
487
|
+
const MAX_DURABLE_EPOCH_RECORD_BYTES=1048576;
|
|
488
|
+
|
|
489
|
+
async function readBoundedEpochHandle(handle,label){
|
|
490
|
+
const stat=await handle.stat();
|
|
491
|
+
if(!stat.isFile()||!Number.isSafeInteger(stat.size)||stat.size>MAX_DURABLE_EPOCH_RECORD_BYTES)
|
|
492
|
+
throw new Error(`authority epoch ${label} oversized`);
|
|
493
|
+
// Read at most the observed size plus one byte. A concurrent extension
|
|
494
|
+
// cannot turn this into an unbounded read, and is rejected rather than
|
|
495
|
+
// silently truncating the record that will be hashed or parsed.
|
|
496
|
+
const bytes=Buffer.alloc(stat.size+1);
|
|
497
|
+
let used=0;
|
|
498
|
+
while(used<bytes.length){
|
|
499
|
+
const {bytesRead}=await handle.read(bytes,used,bytes.length-used,used);
|
|
500
|
+
if(bytesRead===0)break;
|
|
501
|
+
used+=bytesRead;
|
|
502
|
+
}
|
|
503
|
+
if(used!==stat.size)throw new Error(`authority epoch ${label} changed during read`);
|
|
504
|
+
return bytes.subarray(0,used);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function readBoundedEpochRecord(file,label){
|
|
508
|
+
const handle=await open(file,'r');
|
|
509
|
+
try{
|
|
510
|
+
return await readBoundedEpochHandle(handle,label);
|
|
511
|
+
}finally{await handle.close();}
|
|
512
|
+
}
|
|
513
|
+
|
|
434
514
|
async function readDurableHead(runtimeDir,plane,{recoveryOrphanFile=null}={}){
|
|
435
515
|
const dir=durableEpochDir(runtimeDir,plane),headPath=path.join(dir,'head.json');
|
|
436
516
|
let head;
|
|
437
|
-
try{head=JSON.parse(await
|
|
517
|
+
try{head=JSON.parse(decodeUtf8Strict(await readBoundedEpochRecord(headPath,'head')));}
|
|
438
518
|
catch(error){
|
|
439
519
|
if(error?.code==='ENOENT')return null;
|
|
520
|
+
if(String(error?.message??'').includes('authority epoch head oversized'))throw error;
|
|
440
521
|
throw new Error('authority epoch head invalid');
|
|
441
522
|
}
|
|
442
523
|
if(!plain(head)||head.schema!==DURABLE_HEAD_SCHEMA||head.plane!==plane||
|
|
@@ -455,7 +536,8 @@ async function readDurableHead(runtimeDir,plane,{recoveryOrphanFile=null}={}){
|
|
|
455
536
|
// the head. The head remains freshly read on every authority decision.
|
|
456
537
|
const [eventNames,eventRead,keys]=await Promise.all([
|
|
457
538
|
readdir(path.join(dir,'events')),
|
|
458
|
-
|
|
539
|
+
readBoundedEpochRecord(path.join(dir,'events',head.event_file),'event')
|
|
540
|
+
.then(bytes=>({bytes}),error=>({error})),
|
|
459
541
|
durableEpochKeys(runtimeDir)
|
|
460
542
|
]);
|
|
461
543
|
if(eventNames.length>10000)throw new Error('authority epoch event budget exceeded');
|
|
@@ -466,8 +548,11 @@ async function readDurableHead(runtimeDir,plane,{recoveryOrphanFile=null}={}){
|
|
|
466
548
|
if(!Number.isSafeInteger(seq)||(seq>head.seq&&name!==recoveryOrphanFile)||(seq===head.seq&&name!==head.event_file))
|
|
467
549
|
throw new Error('authority epoch head replay or incomplete write');
|
|
468
550
|
}
|
|
551
|
+
if(eventRead.error){
|
|
552
|
+
if(eventRead.error.code==='ENOENT')throw new Error('authority epoch event missing');
|
|
553
|
+
throw eventRead.error;
|
|
554
|
+
}
|
|
469
555
|
const eventBytes=eventRead.bytes;
|
|
470
|
-
if(!eventBytes)throw new Error('authority epoch event missing');
|
|
471
556
|
if(hashBytes(eventBytes)!==head.event_sha256)throw new Error('authority epoch event hash mismatch');
|
|
472
557
|
let event;
|
|
473
558
|
try{event=JSON.parse(decodeUtf8Strict(eventBytes));}catch{throw new Error('authority epoch event invalid');}
|
|
@@ -484,22 +569,92 @@ async function readDurableHead(runtimeDir,plane,{recoveryOrphanFile=null}={}){
|
|
|
484
569
|
(!SHAPAT.test(String(head.policy_sha256))||!SHAPAT.test(String(head.policy_envelope_sha256))||
|
|
485
570
|
head.policy_sha256!==event.policy_sha256||head.policy_envelope_sha256!==event.policy_envelope_sha256))
|
|
486
571
|
throw new Error('authority epoch policy binding invalid');
|
|
487
|
-
if(head.seq===1
|
|
572
|
+
if(head.seq===1){
|
|
573
|
+
if(head.previous_event_sha256!==null)throw new Error('authority epoch genesis chain invalid');
|
|
574
|
+
assertEpochTransition(head,null);
|
|
575
|
+
}
|
|
488
576
|
if(head.seq>1){
|
|
489
577
|
if(!SHAPAT.test(String(head.previous_event_sha256)))throw new Error('authority epoch predecessor missing');
|
|
490
578
|
const priorName=durableEventName(head.seq-1,head.previous_event_sha256);
|
|
491
579
|
let priorBytes;
|
|
492
|
-
try{priorBytes=await
|
|
493
|
-
catch
|
|
580
|
+
try{priorBytes=await readBoundedEpochRecord(path.join(dir,'events',priorName),'predecessor event');}
|
|
581
|
+
catch(error){
|
|
582
|
+
if(error?.code==='ENOENT')throw new Error('authority epoch predecessor event missing');
|
|
583
|
+
throw error;
|
|
584
|
+
}
|
|
494
585
|
if(hashBytes(priorBytes)!==head.previous_event_sha256)throw new Error('authority epoch predecessor hash mismatch');
|
|
495
586
|
let prior;
|
|
496
587
|
try{prior=JSON.parse(decodeUtf8Strict(priorBytes));}catch{throw new Error('authority epoch predecessor invalid');}
|
|
497
588
|
if(!verifyDurableEvent(prior,publicKey,keyId)||prior.seq!==head.seq-1||prior.plane!==plane)
|
|
498
589
|
throw new Error('authority epoch predecessor signature invalid');
|
|
590
|
+
assertEpochPredecessorIdentity(prior);
|
|
591
|
+
if(prior.seq===1)assertEpochTransition(prior,null);
|
|
592
|
+
assertEpochTransition(head,prior);
|
|
499
593
|
}
|
|
500
594
|
return Object.freeze({...head});
|
|
501
595
|
}
|
|
502
596
|
|
|
597
|
+
// Deliberately separate from the per-request authority decision. This is an
|
|
598
|
+
// independent, bounded audit of the complete signed chain, including events
|
|
599
|
+
// older than the immediate predecessor checked by readDurableHead().
|
|
600
|
+
async function auditDurableHistory(runtimeDir,plane){
|
|
601
|
+
const dir=durableEpochDir(runtimeDir,plane),events=path.join(dir,'events');
|
|
602
|
+
const head=await readDurableHead(runtimeDir,plane);
|
|
603
|
+
if(!head){
|
|
604
|
+
let names;
|
|
605
|
+
try{names=await readdir(events);}catch(error){if(error?.code==='ENOENT')return Object.freeze({status:'EMPTY',plane,verified_events:0});throw error;}
|
|
606
|
+
if(names.length)throw new Error('authority epoch history events exist without head');
|
|
607
|
+
return Object.freeze({status:'EMPTY',plane,verified_events:0});
|
|
608
|
+
}
|
|
609
|
+
if(head.seq>10000)throw new Error('authority epoch history event budget exceeded');
|
|
610
|
+
const headPath=path.join(dir,'head.json'),headBytes=await readBoundedEpochRecord(headPath,'head');
|
|
611
|
+
let pinnedHead;
|
|
612
|
+
try{pinnedHead=JSON.parse(decodeUtf8Strict(headBytes));}catch{throw new Error('authority epoch history head changed');}
|
|
613
|
+
if(canonicalJson(pinnedHead)!==canonicalJson(head))throw new Error('authority epoch history head changed');
|
|
614
|
+
const names=await readdir(events);
|
|
615
|
+
if(names.length!==head.seq)throw new Error('authority epoch history event count mismatch');
|
|
616
|
+
const namesSet=new Set(names);
|
|
617
|
+
const {publicKey,keyId}=await durableEpochKeys(runtimeDir);
|
|
618
|
+
let expectedSha=head.event_sha256,totalBytes=0,newer=null;
|
|
619
|
+
for(let seq=head.seq;seq>=1;seq--){
|
|
620
|
+
const name=durableEventName(seq,expectedSha);
|
|
621
|
+
if(!namesSet.has(name))throw new Error('authority epoch history event missing');
|
|
622
|
+
const file=path.join(events,name),st=await lstat(file);
|
|
623
|
+
if(!st.isFile()||st.isSymbolicLink()||st.size>1048576)
|
|
624
|
+
throw new Error('authority epoch history event identity invalid');
|
|
625
|
+
totalBytes+=st.size;
|
|
626
|
+
if(totalBytes>67108864)throw new Error('authority epoch history byte budget exceeded');
|
|
627
|
+
const bytes=await readBoundedEpochRecord(file,'event');
|
|
628
|
+
if(hashBytes(bytes)!==expectedSha)throw new Error('authority epoch history hash mismatch');
|
|
629
|
+
let event;
|
|
630
|
+
try{event=JSON.parse(decodeUtf8Strict(bytes));}catch{throw new Error('authority epoch history event invalid');}
|
|
631
|
+
if(!verifyDurableEvent(event,publicKey,keyId)||event.schema!==DURABLE_EPOCH_SCHEMA||
|
|
632
|
+
event.plane!==plane||event.seq!==seq||!DURABLE_EPOCH_ACTIONS.has(event.action)||
|
|
633
|
+
!SHAPAT.test(String(event.grant_sha256))||!SHAPAT.test(String(event.grant_instance_nonce)))
|
|
634
|
+
throw new Error('authority epoch history event signature or identity invalid');
|
|
635
|
+
normalizeRestoreBinding(event.restore_binding,{required:event.action==='transaction_restore'});
|
|
636
|
+
if((event.action.startsWith('transaction_')?!/^[A-Za-z0-9._-]{1,96}$/.test(String(event.update_id??'')):event.update_id!==null))
|
|
637
|
+
throw new Error('authority epoch history update identity invalid');
|
|
638
|
+
if(event.policy_sha256!==undefined||event.policy_envelope_sha256!==undefined){
|
|
639
|
+
if(!SHAPAT.test(String(event.policy_sha256))||!SHAPAT.test(String(event.policy_envelope_sha256)))
|
|
640
|
+
throw new Error('authority epoch history policy binding invalid');
|
|
641
|
+
}
|
|
642
|
+
if(seq===1){
|
|
643
|
+
if(event.previous_event_sha256!==null)throw new Error('authority epoch history genesis chain invalid');
|
|
644
|
+
}else if(!SHAPAT.test(String(event.previous_event_sha256)))
|
|
645
|
+
throw new Error('authority epoch history predecessor missing');
|
|
646
|
+
if(newer)assertEpochTransition(newer,event);
|
|
647
|
+
else if(seq===1)assertEpochTransition(event,null);
|
|
648
|
+
newer=event;
|
|
649
|
+
expectedSha=event.previous_event_sha256;
|
|
650
|
+
}
|
|
651
|
+
if(newer&&head.seq>1)assertEpochTransition(newer,null);
|
|
652
|
+
if(!(await readBoundedEpochRecord(headPath,'head')).equals(headBytes))throw new Error('authority epoch history head changed during audit');
|
|
653
|
+
if((await durableEpochKeys(runtimeDir)).keyId!==keyId)throw new Error('authority epoch history trust changed during audit');
|
|
654
|
+
return Object.freeze({status:'PASS',plane,seq:head.seq,event_sha256:head.event_sha256,
|
|
655
|
+
verified_events:head.seq,verified_bytes:totalBytes});
|
|
656
|
+
}
|
|
657
|
+
|
|
503
658
|
// R79: An orphan event is not a committed head. Ordinary reads, verification
|
|
504
659
|
// and appends MUST keep rejecting it. Only explicit revocation-only recovery
|
|
505
660
|
// can use the bounded exception to inspect an older, still-signed head.
|
|
@@ -516,7 +671,7 @@ async function inspectOrphanRevocation(runtimeDir,plane,{expectedHeadSha256,expe
|
|
|
516
671
|
const headPath=path.join(dir,'head.json');
|
|
517
672
|
const headStat=await lstat(headPath);
|
|
518
673
|
if(!headStat.isFile()||headStat.isSymbolicLink())throw new Error('authority epoch recovery head identity invalid');
|
|
519
|
-
const rawHead=await
|
|
674
|
+
const rawHead=await readBoundedEpochRecord(headPath,'head');
|
|
520
675
|
let untrusted;
|
|
521
676
|
try{untrusted=JSON.parse(decodeUtf8Strict(rawHead));}catch{throw new Error('authority epoch recovery head invalid');}
|
|
522
677
|
if(!Number.isSafeInteger(untrusted?.seq)||untrusted.seq<1)
|
|
@@ -539,7 +694,7 @@ async function inspectOrphanRevocation(runtimeDir,plane,{expectedHeadSha256,expe
|
|
|
539
694
|
const orphanPath=path.join(events,orphanFile);
|
|
540
695
|
const orphanStat=await lstat(orphanPath);
|
|
541
696
|
if(!orphanStat.isFile()||orphanStat.isSymbolicLink())throw new Error('authority epoch recovery orphan file identity invalid');
|
|
542
|
-
const orphanBytes=await
|
|
697
|
+
const orphanBytes=await readBoundedEpochRecord(orphanPath,'event');
|
|
543
698
|
if(hashBytes(orphanBytes)!==expectedOrphanSha256)
|
|
544
699
|
throw new Error('authority epoch recovery orphan bytes mismatch');
|
|
545
700
|
let event;
|
|
@@ -559,7 +714,7 @@ async function inspectOrphanRevocation(runtimeDir,plane,{expectedHeadSha256,expe
|
|
|
559
714
|
throw new Error('authority epoch recovery orphan revocation binding invalid');
|
|
560
715
|
// Head cannot be promoted unless it is still exactly the same committed
|
|
561
716
|
// predecessor. No authority-creating action is ever eligible.
|
|
562
|
-
const replay=await
|
|
717
|
+
const replay=await readBoundedEpochRecord(headPath,'head');
|
|
563
718
|
if(!replay.equals(rawHead))throw new Error('authority epoch recovery head changed');
|
|
564
719
|
const newHead={schema:DURABLE_HEAD_SCHEMA,plane,seq:event.seq,action:event.action,
|
|
565
720
|
update_id:event.update_id,grant_sha256:event.grant_sha256,
|
|
@@ -583,6 +738,9 @@ export function createAuthorityEpochStore({runtimeDir,policyBytes=null,onDurabil
|
|
|
583
738
|
async readHead({plane}){
|
|
584
739
|
return readDurableHead(root,plane);
|
|
585
740
|
},
|
|
741
|
+
async auditHistory({plane}){
|
|
742
|
+
return auditDurableHistory(root,plane);
|
|
743
|
+
},
|
|
586
744
|
async reconcileOrphanedRevocation({plane,expectedHeadSha256,expectedOrphanSha256,operatorConfirmed=false}){
|
|
587
745
|
// NOT called from grant verification, append, startup or automatic
|
|
588
746
|
// recovery. The operator must supply two observed event identities.
|
|
@@ -610,12 +768,12 @@ export function createAuthorityEpochStore({runtimeDir,policyBytes=null,onDurabil
|
|
|
610
768
|
const st=await fh.stat();
|
|
611
769
|
if(!st.isFile()||st.size!==orphanBytes.length)
|
|
612
770
|
throw new Error('authority epoch recovery pinned orphan identity invalid');
|
|
613
|
-
const pinnedBytes=await fh
|
|
771
|
+
const pinnedBytes=await readBoundedEpochHandle(fh,'event');
|
|
614
772
|
if(!pinnedBytes.equals(orphanBytes)||hashBytes(pinnedBytes)!==expectedOrphanSha256)
|
|
615
773
|
throw new Error('authority epoch recovery orphan changed before sync');
|
|
616
774
|
await fh.sync();
|
|
617
775
|
}finally{await fh.close();}
|
|
618
|
-
if(!(await
|
|
776
|
+
if(!(await readBoundedEpochRecord(path.join(dir,'head.json'),'head')).equals(headBytes))
|
|
619
777
|
throw new Error('authority epoch recovery head changed before commit');
|
|
620
778
|
await release.verifyOwned();
|
|
621
779
|
await atomicJsonWrite(path.join(dir,'head.json'),newHead,{stage});
|
|
@@ -650,6 +808,11 @@ export function createAuthorityEpochStore({runtimeDir,policyBytes=null,onDurabil
|
|
|
650
808
|
if(parsedGrant?.policy_sha256!==policySha256)throw new Error('authority epoch grant policy digest mismatch');
|
|
651
809
|
policyBinding={policy_sha256:policySha256,policy_envelope_sha256:deriveSecurityEnvelope(plane,parsedPolicy).sha256};
|
|
652
810
|
}
|
|
811
|
+
// A mismatched successor envelope is known before acquiring a native
|
|
812
|
+
// lock or writing any signed event. Refuse it without side effects.
|
|
813
|
+
if(action==='transaction_restore'&&policyBinding?.policy_envelope_sha256!==undefined&&
|
|
814
|
+
normalizedRestoreBinding.successor_policy_envelope_sha256!==policyBinding.policy_envelope_sha256)
|
|
815
|
+
throw new Error('authority epoch transaction restore successor policy envelope mismatch');
|
|
653
816
|
const dir=durableEpochDir(root,plane),events=path.join(dir,'events');
|
|
654
817
|
const release=await acquireEpochLock(dir,{onLockDispositionStage});
|
|
655
818
|
try{
|
|
@@ -674,6 +837,12 @@ export function createAuthorityEpochStore({runtimeDir,policyBytes=null,onDurabil
|
|
|
674
837
|
throw new Error('authority epoch transaction restore predecessor grant binding mismatch');
|
|
675
838
|
if(previous.grant_instance_nonce===grantInstanceNonce)
|
|
676
839
|
throw new Error('authority epoch transaction restore requires fresh nonce; revoked nonce cannot be resurrected');
|
|
840
|
+
if(previous.policy_envelope_sha256!==undefined&&
|
|
841
|
+
normalizedRestoreBinding.predecessor_policy_envelope_sha256!==previous.policy_envelope_sha256)
|
|
842
|
+
throw new Error('authority epoch transaction restore predecessor policy envelope mismatch');
|
|
843
|
+
if(policyBinding?.policy_envelope_sha256!==undefined&&
|
|
844
|
+
normalizedRestoreBinding.successor_policy_envelope_sha256!==policyBinding.policy_envelope_sha256)
|
|
845
|
+
throw new Error('authority epoch transaction restore successor policy envelope mismatch');
|
|
677
846
|
}
|
|
678
847
|
if(action==='grant'&&previous?.grant_instance_nonce===grantInstanceNonce)
|
|
679
848
|
throw new Error('authority epoch grant requires fresh nonce; prior nonce cannot be resurrected');
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
|
-
import { access,
|
|
4
|
+
import { access, readFile } from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { startAutonomousSupervisor } from './autonomous-supervisor.mjs';
|
|
7
|
+
import { windowsPowerShellEnvironment } from './windows-powershell-environment.mjs';
|
|
8
|
+
import { readPinnedEpochLockSnapshot, createPinnedEpochLock, deletePinnedRemoteMarker } from './foreground-marker-cas-win32.mjs';
|
|
9
|
+
import { runNativeInspect } from './native-inspector-r67.mjs';
|
|
7
10
|
|
|
8
11
|
const execFileAsync=promisify(execFile);
|
|
12
|
+
const TRUSTED_WINDOWS_ROOT='C:\\Windows';
|
|
9
13
|
export const AUTONOMOUS_SUPERVISOR_OWNER_SCHEMA='deadbyte.autonomous-supervisor-owner.v1';
|
|
10
14
|
const HEX64=/^[0-9a-f]{64}$/;
|
|
11
15
|
const RECOVERY_OPERATIONS=Object.freeze(['status','resume','park','disarmautonomous','disarmcoding','disarmprocess','disarmhost']);
|
|
@@ -33,12 +37,12 @@ function validateMarker(marker){
|
|
|
33
37
|
}
|
|
34
38
|
async function powershellPath(){
|
|
35
39
|
if(process.platform!=='win32') return null;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return
|
|
40
|
+
const candidate=path.join(TRUSTED_WINDOWS_ROOT,'System32','WindowsPowerShell','v1.0','powershell.exe');
|
|
41
|
+
try { await access(candidate); return candidate; } catch { return null; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function trustedPowerShellEnvironment(){
|
|
45
|
+
return windowsPowerShellEnvironment({...process.env,SystemRoot:TRUSTED_WINDOWS_ROOT,WINDIR:TRUSTED_WINDOWS_ROOT});
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
export async function observeCurrentProcessIdentity(pid=process.pid){
|
|
@@ -49,7 +53,7 @@ export async function observeCurrentProcessIdentity(pid=process.pid){
|
|
|
49
53
|
let stdout;
|
|
50
54
|
try {
|
|
51
55
|
({stdout}=await execFileAsync(shell,['-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-Command',script],{
|
|
52
|
-
windowsHide:true,timeout:10_000,maxBuffer:1024*1024,encoding:'utf8'
|
|
56
|
+
windowsHide:true,timeout:10_000,maxBuffer:1024*1024,encoding:'utf8',env:trustedPowerShellEnvironment()
|
|
53
57
|
}));
|
|
54
58
|
} catch { return null; }
|
|
55
59
|
let parsed;
|
|
@@ -64,14 +68,36 @@ export async function observeCurrentProcessIdentity(pid=process.pid){
|
|
|
64
68
|
});
|
|
65
69
|
}
|
|
66
70
|
|
|
71
|
+
async function confirmProcessAbsent(pid){
|
|
72
|
+
const shell=await powershellPath();
|
|
73
|
+
if(!shell) return false;
|
|
74
|
+
const script=`$ErrorActionPreference='Stop'; $p=Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}' -ErrorAction Stop; if($null -eq $p){[Console]::Out.WriteLine('ABSENT')}else{[Console]::Out.WriteLine('PRESENT')}`;
|
|
75
|
+
try {
|
|
76
|
+
const {stdout}=await execFileAsync(shell,['-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-Command',script],{
|
|
77
|
+
windowsHide:true,timeout:10_000,maxBuffer:1024*1024,encoding:'utf8',env:trustedPowerShellEnvironment()
|
|
78
|
+
});
|
|
79
|
+
if(String(stdout).trim()!=='ABSENT') return false;
|
|
80
|
+
const native=await runNativeInspect(['identity',String(pid)]);
|
|
81
|
+
return native.status==='failed'&&native.error_code==='INVALID_PARAMETER'&&
|
|
82
|
+
native.win32_error===87&&native.error==='OpenProcess identity win32=87';
|
|
83
|
+
} catch { return false; }
|
|
84
|
+
}
|
|
85
|
+
|
|
67
86
|
async function readMarker(markerPath){
|
|
68
|
-
let raw;
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
let raw, fileId=null;
|
|
88
|
+
if(process.platform==='win32'){
|
|
89
|
+
const snapshot=readPinnedEpochLockSnapshot(markerPath);
|
|
90
|
+
if(!snapshot) return {exists:false};
|
|
91
|
+
raw=snapshot.bytes;
|
|
92
|
+
fileId=snapshot.fileId;
|
|
93
|
+
} else {
|
|
94
|
+
try { raw=await readFile(markerPath); }
|
|
95
|
+
catch(error){ if(error?.code==='ENOENT') return {exists:false}; throw error; }
|
|
96
|
+
}
|
|
71
97
|
let marker;
|
|
72
98
|
try { marker=JSON.parse(raw.toString('utf8')); }
|
|
73
|
-
catch { return {exists:true,raw,raw_sha256:sha256Bytes(raw),valid:false,marker:null}; }
|
|
74
|
-
return {exists:true,raw,raw_sha256:sha256Bytes(raw),valid:validateMarker(marker),marker};
|
|
99
|
+
catch { return {exists:true,raw,raw_sha256:sha256Bytes(raw),file_id:fileId,valid:false,marker:null}; }
|
|
100
|
+
return {exists:true,raw,raw_sha256:sha256Bytes(raw),file_id:fileId,valid:validateMarker(marker),marker};
|
|
75
101
|
}
|
|
76
102
|
function markerFor(identity){
|
|
77
103
|
return {
|
|
@@ -91,28 +117,28 @@ export async function observeSupervisor(options={}){
|
|
|
91
117
|
const markerPath=assertOptions(options);
|
|
92
118
|
const stored=await readMarker(markerPath);
|
|
93
119
|
if(!stored.exists) return {status:'absent',live:false,reason:'no_owner',marker:null,marker_sha256:null};
|
|
94
|
-
|
|
120
|
+
const binding={marker_sha256:stored.raw_sha256,marker_file_id:stored.file_id};
|
|
121
|
+
if(!stored.valid) return {status:'unverified',live:false,reason:'marker_invalid',marker:stored.marker,...binding};
|
|
95
122
|
const observed=await observeCurrentProcessIdentity(stored.marker.pid);
|
|
96
|
-
if(!observed)
|
|
123
|
+
if(!observed){
|
|
124
|
+
if(await confirmProcessAbsent(stored.marker.pid))
|
|
125
|
+
return {status:'stale',live:false,reason:'process_absent',marker:stored.marker,...binding};
|
|
126
|
+
return {status:'unverified',live:false,reason:'process_unobservable',marker:stored.marker,...binding};
|
|
127
|
+
}
|
|
97
128
|
if(observed.creation_time_utc!==new Date(stored.marker.creation_time_utc).toISOString()) {
|
|
98
|
-
return {status:'stale',live:false,reason:'pid_reused',marker:stored.marker,observation:observed
|
|
129
|
+
return {status:'stale',live:false,reason:'pid_reused',marker:stored.marker,observation:observed,...binding};
|
|
99
130
|
}
|
|
100
131
|
if(!samePath(observed.image_path,stored.marker.image_path)||observed.image_sha256!==stored.marker.image_sha256) {
|
|
101
|
-
return {status:'
|
|
132
|
+
return {status:'unverified',live:false,reason:'identity_mismatch',marker:stored.marker,observation:observed,...binding};
|
|
102
133
|
}
|
|
103
|
-
return {status:'owned',live:true,reason:'live_verified_owner',marker:stored.marker,observation:observed
|
|
134
|
+
return {status:'owned',live:true,reason:'live_verified_owner',marker:stored.marker,observation:observed,...binding};
|
|
104
135
|
}
|
|
105
136
|
|
|
106
137
|
export async function claimSupervisor(options={}){
|
|
107
138
|
const markerPath=assertOptions(options);
|
|
108
139
|
const marker=await currentMarker();
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
await writeFile(markerPath,`${JSON.stringify(marker)}\n`,{flag:'wx',mode:0o600});
|
|
112
|
-
} catch(error){
|
|
113
|
-
if(error?.code==='EEXIST') throw new Error('supervisor owner marker already exists; takeover required');
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
140
|
+
const created=createPinnedEpochLock(markerPath,Buffer.from(`${JSON.stringify(marker)}\n`));
|
|
141
|
+
if(!created) throw new Error('supervisor owner marker already exists; takeover required');
|
|
116
142
|
return {status:'claimed',instance_nonce:marker.instance_nonce,marker};
|
|
117
143
|
}
|
|
118
144
|
|
|
@@ -121,17 +147,13 @@ export async function takeoverSupervisor(options={}){
|
|
|
121
147
|
const prior=await observeSupervisor({markerPath});
|
|
122
148
|
if(prior.live&&prior.reason==='live_verified_owner') throw new Error('supervisor takeover refused: live verified owner');
|
|
123
149
|
if(prior.reason==='no_owner') return claimSupervisor({markerPath});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if(!
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if(!check.exists||check.raw_sha256!==prior.marker_sha256) throw new Error('supervisor ownership changed during takeover');
|
|
132
|
-
await rename(temp,markerPath);
|
|
133
|
-
} finally { await rm(temp,{force:true}).catch(()=>{}); }
|
|
134
|
-
return {status:'claimed',instance_nonce:marker.instance_nonce,marker,taken_over_reason:prior.reason};
|
|
150
|
+
if(!['process_absent','pid_reused'].includes(prior.reason))
|
|
151
|
+
throw new Error(`supervisor takeover refused: ownership unverified (${prior.reason})`);
|
|
152
|
+
if(!prior.marker_file_id) throw new Error('supervisor ownership file identity unavailable for takeover');
|
|
153
|
+
const removed=deletePinnedRemoteMarker(markerPath,prior.marker_sha256,{expectedFileId:prior.marker_file_id});
|
|
154
|
+
if(removed!=='REMOVED') throw new Error(`supervisor ownership changed during takeover: ${removed}`);
|
|
155
|
+
const claimed=await claimSupervisor({markerPath});
|
|
156
|
+
return {...claimed,taken_over_reason:prior.reason};
|
|
135
157
|
}
|
|
136
158
|
|
|
137
159
|
export async function releaseSupervisor(options={}){
|
|
@@ -147,9 +169,9 @@ export async function releaseSupervisor(options={}){
|
|
|
147
169
|
!samePath(current.image_path,observed.observation.image_path)||current.image_sha256!==observed.observation.image_sha256) {
|
|
148
170
|
throw new Error('supervisor ownership current process identity mismatch');
|
|
149
171
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
172
|
+
if(!observed.marker_file_id) throw new Error('supervisor ownership file identity unavailable for release');
|
|
173
|
+
const removed=deletePinnedRemoteMarker(markerPath,observed.marker_sha256,{expectedFileId:observed.marker_file_id});
|
|
174
|
+
if(removed!=='REMOVED') throw new Error(`supervisor ownership changed before release: ${removed}`);
|
|
153
175
|
return {status:'released'};
|
|
154
176
|
}
|
|
155
177
|
|
|
@@ -180,7 +202,7 @@ export async function runPinnedControllerOperation(options={}){
|
|
|
180
202
|
if(!shell) throw new Error('supervisor controller requires Windows PowerShell');
|
|
181
203
|
try {
|
|
182
204
|
const {stdout,stderr}=await execFileAsync(shell,['-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-File',controllerPath,operation],{
|
|
183
|
-
windowsHide:true,timeout:120_000,maxBuffer:4*1024*1024,encoding:'utf8'
|
|
205
|
+
windowsHide:true,timeout:120_000,maxBuffer:4*1024*1024,encoding:'utf8',env:trustedPowerShellEnvironment()
|
|
184
206
|
});
|
|
185
207
|
return {exit_code:0,stdout:String(stdout??''),stderr:String(stderr??'')};
|
|
186
208
|
} catch(error) {
|
|
@@ -228,20 +250,28 @@ export async function startLocalSupervisorHost({runtime,policy=runtime?.policy,m
|
|
|
228
250
|
try {
|
|
229
251
|
supervisor=startAutonomousSupervisor({runtime,policy,idleDelayMs,maxCycles,logger,continuous});
|
|
230
252
|
} catch(error) {
|
|
231
|
-
await releaseSupervisor({markerPath,instanceNonce:claim.instance_nonce})
|
|
253
|
+
try { await releaseSupervisor({markerPath,instanceNonce:claim.instance_nonce}); }
|
|
254
|
+
catch(cleanupError) {
|
|
255
|
+
throw new AggregateError([error,cleanupError],
|
|
256
|
+
'supervisor startup and ownership cleanup both failed',{cause:error});
|
|
257
|
+
}
|
|
232
258
|
throw error;
|
|
233
259
|
}
|
|
234
260
|
let stopped=false;
|
|
261
|
+
let stopPromise=null;
|
|
235
262
|
return Object.freeze({
|
|
236
263
|
marker_path:path.resolve(markerPath),instance_nonce:claim.instance_nonce,supervisor,
|
|
237
|
-
get running(){ return !
|
|
238
|
-
async scanNow(){ if(
|
|
264
|
+
get running(){ return !stopPromise&&supervisor.running; },
|
|
265
|
+
async scanNow(){ if(stopPromise) throw new Error('local supervisor host stopping or stopped'); return supervisor.scanNow(); },
|
|
239
266
|
async stop(){
|
|
240
267
|
if(stopped) return {status:'stopped'};
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
268
|
+
if(!stopPromise) stopPromise=(async()=>{
|
|
269
|
+
await supervisor.stop();
|
|
270
|
+
await releaseSupervisor({markerPath,instanceNonce:claim.instance_nonce});
|
|
271
|
+
stopped=true;
|
|
272
|
+
return {status:'stopped'};
|
|
273
|
+
})();
|
|
274
|
+
return stopPromise;
|
|
245
275
|
}
|
|
246
276
|
});
|
|
247
277
|
}
|
package/src/deadbyte-cli.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import {foregroundIdentityMatches,queryForegroundIdentity,validForegroundIdentit
|
|
|
25
25
|
import {readRemoteMarkerSnapshot,deletePinnedRemoteMarker} from './foreground-marker-cas-win32.mjs';
|
|
26
26
|
import {acquireForegroundSessionLock} from './foreground-session-lock.mjs';
|
|
27
27
|
import { AUTHORITY_PLANES, authorityVector, authorityRestorePlan, parseAuthorityObservation, assertAuthorityVector, assertRuntimeReadyObservation, authorityBanner } from './remote-authority.mjs';
|
|
28
|
-
import { decodeRemoteLogBytes, boundedRemoteDiagnostic,
|
|
28
|
+
import { decodeRemoteLogBytes, boundedRemoteDiagnostic, readRemoteLogAppend } from './remote-log-decode-r60.mjs';
|
|
29
29
|
|
|
30
30
|
const REMOTE_COMMANDS=new Set([
|
|
31
31
|
'menu','start','resume','park','stop','restart','status','url','test','fulltest','toggle','clean','machinetest',
|
|
@@ -43,6 +43,7 @@ const CURRENT_PACKAGE_ROOT=path.resolve(fileURLToPath(new URL('..',import.meta.u
|
|
|
43
43
|
const CURRENT_DEPLOY_SCRIPT=path.join(CURRENT_PACKAGE_ROOT,'scripts','deploy-r48.mjs');
|
|
44
44
|
const REMOTE_UPDATE_TIMEOUT_MS=900000;
|
|
45
45
|
const LIVE_LOG_DISCOVERY_INTERVAL_MS=1000;
|
|
46
|
+
const TRUSTED_WINDOWS_ROOT='C:\\Windows';
|
|
46
47
|
|
|
47
48
|
function stateRoot(){return resolveStateRoot();}
|
|
48
49
|
async function requireRegular(file,label){const info=await lstat(file);if(!info.isFile()||info.isSymbolicLink()) throw new Error(`${label} must be a regular non-symlink file`);return file;}
|
|
@@ -79,8 +80,7 @@ async function remoteContext(){
|
|
|
79
80
|
if(process.platform!=='win32') throw new Error('remote controller is currently Windows-only');
|
|
80
81
|
const root=stateRoot();
|
|
81
82
|
const launcher=await requireRegular(path.join(root,'bin','DEADBYTE-MCP.ps1'),'stable DEADBYTE launcher');
|
|
82
|
-
const
|
|
83
|
-
const powershell=await requireRegular(path.join(systemRoot,'System32','WindowsPowerShell','v1.0','powershell.exe'),'Windows PowerShell');
|
|
83
|
+
const powershell=await requireRegular(path.join(TRUSTED_WINDOWS_ROOT,'System32','WindowsPowerShell','v1.0','powershell.exe'),'Windows PowerShell');
|
|
84
84
|
return {root,launcher,powershell};
|
|
85
85
|
}
|
|
86
86
|
function boundedSingleLine(value,maxChars=2048){
|
|
@@ -203,7 +203,8 @@ function invokeController(ctx,command,{capture=false,allowFailure=false,echoCapt
|
|
|
203
203
|
stdio=['ignore',stdoutFd,stderrFd];
|
|
204
204
|
}else stdio=capture?['ignore','pipe','pipe']:'inherit';
|
|
205
205
|
const options={stdio,encoding:capture?'utf8':undefined,windowsHide:false,shell:false,
|
|
206
|
-
env:windowsPowerShellEnvironment(
|
|
206
|
+
env:windowsPowerShellEnvironment({...process.env,SystemRoot:TRUSTED_WINDOWS_ROOT,WINDIR:TRUSTED_WINDOWS_ROOT,
|
|
207
|
+
...(probeMode?{DEADBYTE_REMOTE_PROBE_MODE:probeMode}:{})})};
|
|
207
208
|
if(Number.isInteger(timeoutMs)&&timeoutMs>0) options.timeout=timeoutMs;
|
|
208
209
|
result=spawnSync(ctx.powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',ctx.launcher,command,...(json?['-Json']:[])],options);
|
|
209
210
|
}finally{
|
|
@@ -309,32 +310,15 @@ async function replayRemoteResults(root,cursor,{displayMode='unicode'}={}){
|
|
|
309
310
|
if(!page.has_more) return next;
|
|
310
311
|
}
|
|
311
312
|
}
|
|
312
|
-
async function readAppend(file,
|
|
313
|
-
|
|
314
|
-
let start=offset;
|
|
315
|
-
const reset=info.size<start;
|
|
316
|
-
if(reset) start=0;
|
|
317
|
-
if(info.size===start) return {offset:start,text:''};
|
|
318
|
-
const handle=await open(file,'r');
|
|
319
|
-
let encoding=null,buffer,bytesRead=0;
|
|
320
|
-
try{
|
|
321
|
-
const header=Buffer.alloc(2);await handle.read(header,0,2,0);
|
|
322
|
-
if(header[0]===0xff&&header[1]===0xfe&&/\.log$/i.test(file)){
|
|
323
|
-
encoding='utf16le';if(start%2!==0)start=Math.max(0,start-1);
|
|
324
|
-
}
|
|
325
|
-
const length=Math.min(1024*1024,info.size-start);
|
|
326
|
-
if(length===0)return {offset:start,text:''};
|
|
327
|
-
buffer=Buffer.alloc(length);
|
|
328
|
-
({bytesRead}=await handle.read(buffer,0,length,start));
|
|
329
|
-
}finally{await handle.close();}
|
|
330
|
-
const complete=safeRemoteLogPrefixLength(buffer.subarray(0,bytesRead),encoding??'utf8');
|
|
331
|
-
return {offset:start+complete,text:decodeRemoteLogBytes(file,buffer.subarray(0,complete),{encoding}),reset};
|
|
313
|
+
async function readAppend(file,cursor){
|
|
314
|
+
return readRemoteLogAppend(file,cursor);
|
|
332
315
|
}
|
|
333
316
|
async function snapshotRemoteLogOffsets(root){
|
|
334
317
|
const offsets=new Map();
|
|
335
318
|
const files=await listLogFiles(root);
|
|
336
319
|
for(const file of files){
|
|
337
|
-
|
|
320
|
+
const snapshot=await readRemoteLogAppend(file,null,{startAtEnd:true});
|
|
321
|
+
if(snapshot.cursor)offsets.set(file,snapshot.cursor);
|
|
338
322
|
}
|
|
339
323
|
return offsets;
|
|
340
324
|
}
|
|
@@ -346,6 +330,7 @@ function timelineFilterEvent(timeline){
|
|
|
346
330
|
return {
|
|
347
331
|
event_type:'tool.result',request_id:timeline.request_id,tool:timeline.tool,
|
|
348
332
|
outcome:{
|
|
333
|
+
status:timeline.side_effect_state==='unknown'?'interrupted':timeline.status,
|
|
349
334
|
is_error:timeline.status==='error',
|
|
350
335
|
...(Number.isFinite(Number(timeline.duration_ms))?{duration_ms:Number(timeline.duration_ms)}:{}),
|
|
351
336
|
...(timeline.side_effect_state?{side_effect_state:timeline.side_effect_state}:{}),
|
|
@@ -368,10 +353,13 @@ async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTra
|
|
|
368
353
|
nextDiscoveryAt=now+LIVE_LOG_DISCOVERY_INTERVAL_MS;
|
|
369
354
|
}
|
|
370
355
|
for(const file of liveFiles){
|
|
371
|
-
const prior=activeOffsets.has(file)?activeOffsets.get(file):
|
|
356
|
+
const prior=activeOffsets.has(file)?activeOffsets.get(file):null;
|
|
372
357
|
const next=await readAppend(file,prior);
|
|
373
|
-
activeOffsets.set(file,next.
|
|
374
|
-
if(next.reset)
|
|
358
|
+
activeOffsets.set(file,next.cursor);
|
|
359
|
+
if(next.reset){
|
|
360
|
+
partialLines.delete(file);
|
|
361
|
+
process.stderr.write('[LIVELOG] STREAM_RESET '+path.basename(file)+' — '+next.resetReason+'; check audit continuity before trusting coverage\n');
|
|
362
|
+
}
|
|
375
363
|
if(!next.text)continue;
|
|
376
364
|
const lines=((partialLines.get(file)??'')+next.text).split(/\r?\n/);
|
|
377
365
|
partialLines.set(file,lines.pop()??'');
|
|
@@ -379,6 +367,8 @@ async function tailRemoteLogs(root,signal,{journal=null,offsets=null,activityTra
|
|
|
379
367
|
for(const line of lines){
|
|
380
368
|
if(Buffer.byteLength(line,'utf8')>1024*1024) throw new Error(`remote log line exceeds bounded decode limit: ${path.basename(file)}`);
|
|
381
369
|
const auditEvent=parseAuditLogLine(file,line);
|
|
370
|
+
if(/^segment-\d{8}\.jsonl$/.test(path.basename(file))&&!auditEvent)
|
|
371
|
+
throw new Error('LIVELOG_AUDIT_RECORD_INVALID '+path.basename(file)+'; refusing to silently skip audit bytes');
|
|
382
372
|
const timeline=auditTimelineRecord(file,line,auditEvent);
|
|
383
373
|
let recorded=null;
|
|
384
374
|
if(timeline&&journal) recorded=timeline.event_type==='tool.call'
|