deadbyte-mcp 0.15.38 → 0.15.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/MANIFEST.SHA256 +41 -21
  2. package/PROVENANCE.json +75 -21
  3. package/README.txt +30 -18
  4. package/RELEASE.json +76 -22
  5. package/SBOM.cdx.json +2 -2
  6. package/TEST-MATRIX.json +78 -26
  7. package/bridge/package.json +1 -1
  8. package/controller/deadbyte-process-policy.json +4 -4
  9. package/docs/R64-EVIDENCE.json +95 -0
  10. package/docs/R64-REFERENCE-GAP-MATRIX.json +98 -0
  11. package/docs/R64-SCOPE.json +41 -0
  12. package/package.json +6 -3
  13. package/scripts/gate-windows-r62-prepublish.ps1 +45 -0
  14. package/scripts/gate-windows-r63-prepublish.ps1 +45 -0
  15. package/scripts/gate-windows-r64-prepublish.ps1 +68 -0
  16. package/scripts/npm-release-r48.mjs +6 -0
  17. package/scripts/r62-focused.ps1 +9 -0
  18. package/scripts/r62-full.ps1 +9 -0
  19. package/scripts/r62-live-transaction-audit.mjs +17 -0
  20. package/scripts/r62-unresolved-transaction-inspect.mjs +54 -0
  21. package/scripts/r64-predecessor-reproducer.mjs +32 -0
  22. package/scripts/r64-read-benchmark.mjs +84 -0
  23. package/src/compact-gateway-mcp-tools.mjs +1 -1
  24. package/src/deadbyte-cli.mjs +16 -2
  25. package/src/doctor-transactions-r58.mjs +79 -8
  26. package/src/machine-fs-mcp-tools.mjs +1 -1
  27. package/src/machine-fs.mjs +7 -4
  28. package/src/remote-authority.mjs +10 -0
  29. package/src/runtime-update.mjs +15 -3
  30. package/src/version.mjs +1 -1
  31. package/test/r58-doctor-transactions.test.mjs +2 -2
  32. package/test/r59-tool-surface-compat.test.mjs +14 -4
  33. package/test/r61-release-contract.test.mjs +2 -1
  34. package/test/r62-doctor-receipt-semantics.test.mjs +50 -0
  35. package/test/r62-doctor-transactions-reconcile.test.mjs +208 -0
  36. package/test/r62-release-contract.test.mjs +60 -0
  37. package/test/r62-rollback-failure-receipt.test.mjs +19 -0
  38. package/test/r63-one-shot-coherence.test.mjs +38 -0
  39. package/test/r63-release-contract.test.mjs +59 -0
  40. package/test/r64-machine-fs-large-read.test.mjs +136 -0
  41. package/test/r64-release-contract.test.mjs +51 -0
  42. package/test/remote-session-cli.test.mjs +7 -5
@@ -0,0 +1,9 @@
1
+ $ErrorActionPreference='Stop'
2
+ $root=Split-Path -Parent $PSScriptRoot
3
+ Push-Location $root
4
+ try {
5
+ & npm.cmd test
6
+ $code=$LASTEXITCODE
7
+ Write-Host ("R62_FULL_EXIT="+$code)
8
+ exit $code
9
+ } finally { Pop-Location }
@@ -0,0 +1,17 @@
1
+ import {inspectDoctorTransactions} from '../src/doctor-transactions-r58.mjs';
2
+
3
+ const result=await inspectDoctorTransactions();
4
+ const summary=result.findings.find(item=>item.evidence?.receipt_verification_performed===true)?.evidence??null;
5
+ const unresolved=result.findings
6
+ .filter(item=>item.level==='WARN'&&item.evidence?.recorded_phase)
7
+ .map(item=>({schema:item.evidence.schema,phase:item.evidence.recorded_phase,state_sha256:item.evidence.state_sha256}));
8
+ const phases={};
9
+ for(const item of unresolved) phases[item.phase]=(phases[item.phase]??0)+1;
10
+ console.log(JSON.stringify({
11
+ status:result.status,
12
+ summary,
13
+ unresolved_count:unresolved.length,
14
+ unresolved_phases:phases,
15
+ fail_count:result.findings.filter(item=>item.level==='FAIL').length,
16
+ inventory_warning:result.findings.some(item=>item.evidence?.inventory_complete===false)
17
+ },null,2));
@@ -0,0 +1,54 @@
1
+ import {opendir,lstat,readFile} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import {createHash} from 'node:crypto';
5
+
6
+ const state=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
7
+ const root=path.join(state,'evidence','updates');
8
+ const sha=b=>createHash('sha256').update(b).digest('hex');
9
+ const terminal=new Set(['completed','superseded','failed_receipted']);
10
+ const rows=[];
11
+ for await(const entry of await opendir(root)){
12
+ if(!entry.isDirectory()||entry.isSymbolicLink()) continue;
13
+ const dir=path.join(root,entry.name),stateFile=path.join(dir,'state.json');
14
+ let stateBytes,body;
15
+ try{
16
+ const info=await lstat(stateFile);
17
+ if(!info.isFile()||info.isSymbolicLink()||info.size>1048576) continue;
18
+ stateBytes=await readFile(stateFile);
19
+ body=JSON.parse(stateBytes.toString('utf8').replace(/^\uFEFF/,''));
20
+ }catch{continue;}
21
+ if(!['deadbyte.r27-slot-update-state.v1','deadbyte.r31-slot-rollback-state.v1'].includes(body?.schema)) continue;
22
+ const has=async name=>{try{const st=await lstat(path.join(dir,name));return st.isFile()&&!st.isSymbolicLink();}catch{return false;}};
23
+ if(terminal.has(body.phase)||await has('receipt.json')||await has('failure-receipt.json')) continue;
24
+ const handoff=await has('handoff.json');
25
+ let handoffSummary=null;
26
+ if(handoff){
27
+ try{
28
+ const h=JSON.parse((await readFile(path.join(dir,'handoff.json'),'utf8')).replace(/^\uFEFF/,''));
29
+ handoffSummary={schema:h?.body?.schema??h?.schema??null,body:h?.body??h};
30
+ }catch(error){handoffSummary={parse_error:String(error?.message??error)};}
31
+ }
32
+ rows.push({
33
+ entry:entry.name,
34
+ entry_sha256:sha(Buffer.from(entry.name,'utf8')),
35
+ state_sha256:sha(stateBytes),
36
+ schema:body.schema,
37
+ phase:body.phase,
38
+ update_id:body.update_id??null,
39
+ rollback_id:body.rollback_id??null,
40
+ prior_pointer:body.prior_pointer??null,
41
+ new_pointer:body.new_pointer??null,
42
+ active_pointer:body.active_pointer??null,
43
+ old_runtime:body.old_runtime??null,
44
+ new_runtime:body.new_runtime??null,
45
+ error:body.error??null,
46
+ recovery:body.recovery??null,
47
+ has_handoff:handoff,
48
+ handoff:handoffSummary
49
+ });
50
+ }
51
+ rows.sort((a,b)=>a.entry.localeCompare(b.entry));
52
+ let active=null;
53
+ try{active=JSON.parse(await readFile(path.join(state,'active-release.json'),'utf8'));}catch{}
54
+ console.log(JSON.stringify({state_root:state,active_pointer:active,unresolved_count:rows.length,rows},null,2));
@@ -0,0 +1,32 @@
1
+ import {mkdtemp,mkdir,writeFile,rm} from 'node:fs/promises';
2
+ import {tmpdir} from 'node:os';
3
+ import path from 'node:path';
4
+ import {pathToFileURL} from 'node:url';
5
+
6
+ const sourceRoot=process.argv[2];
7
+ if(!sourceRoot||!path.isAbsolute(sourceRoot))throw new Error('absolute predecessor root required');
8
+ const load=name=>import(pathToFileURL(path.join(sourceRoot,name)).href);
9
+ const [{loadCodingPolicy},{createMachineFsRuntime},{generateTrustKeypair},{machineFsGatewayInputSchema},{registerMachineFsTools}]=await Promise.all([
10
+ load('src/coding-plane.mjs'),load('src/machine-fs.mjs'),load('src/trust.mjs'),
11
+ load('src/compact-gateway-mcp-tools.mjs'),load('src/machine-fs-mcp-tools.mjs')
12
+ ]);
13
+ const root=await mkdtemp(path.join(tmpdir(),'deadbyte-r64-red-'));
14
+ try{
15
+ const project=path.join(root,'project');await mkdir(project);
16
+ await writeFile(path.join(project,'large.bin'),Buffer.alloc(4*1024*1024+64,0x52));
17
+ const policyPath=path.join(root,'policy.json');
18
+ await writeFile(policyPath,JSON.stringify({schema:'deadbyte.coding-policy.v1',enabled:true,lease_file:path.join(root,'grant.json'),evidence_dir:path.join(root,'evidence'),roots:{project:{path:project,read:true,write:false,exec:false,deny_paths:[]}},commands:{},max_read_bytes:4*1024*1024,max_write_bytes:65536}));
19
+ const policy=await loadCodingPolicy(policyPath);
20
+ const keys=await generateTrustKeypair({privateKeyPath:path.join(root,'private.pem'),publicKeyPath:path.join(root,'public.pem')});
21
+ const runtime=createMachineFsRuntime({policy,signingKeyPath:keys.privateKeyPath,verifyKeyPath:keys.publicKeyPath});
22
+ const first=await runtime.read({rootId:'project',relativePath:'large.bin',offset:2,maxBytes:4*1024*1024});
23
+ const args={root_id:'project',path:'large.bin',offset:first.next_offset,max_bytes:64,hash_file:false,timeout_ms:30000};
24
+ const server={registerTool(name,definition){if(name==='machine_fs_read')this.read=definition;}};
25
+ registerMachineFsTools(server,{runtime:{}});
26
+ let runtimeError=null;
27
+ try{await runtime.read({rootId:'project',relativePath:'large.bin',offset:first.next_offset,maxBytes:64});}
28
+ catch(error){runtimeError=String(error?.message||error);}
29
+ const result={source_root:sourceRoot,page_two_offset:first.next_offset,full_schema_accepts:server.read.inputSchema.safeParse(args).success,compact_schema_accepts:machineFsGatewayInputSchema.safeParse({op:'read',...args}).success,runtime_error:runtimeError};
30
+ console.log(JSON.stringify(result));
31
+ if(result.full_schema_accepts||result.compact_schema_accepts||!runtimeError)process.exitCode=1;
32
+ }finally{await rm(root,{recursive:true,force:true});}
@@ -0,0 +1,84 @@
1
+ import {createHash} from 'node:crypto';
2
+ import {mkdtemp,mkdir,readFile,rm,writeFile} from 'node:fs/promises';
3
+ import {performance} from 'node:perf_hooks';
4
+ import path from 'node:path';
5
+ import {pathToFileURL} from 'node:url';
6
+
7
+ const [beforeRoot,afterRoot,scratchRoot,outputPath]=process.argv.slice(2);
8
+ if([beforeRoot,afterRoot,scratchRoot,outputPath].some(value=>!value||!path.isAbsolute(value)))throw new Error('four absolute paths required: R63 R64 scratch output');
9
+ const rounds=7,operations=400,concurrency=4;
10
+ const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
11
+ const percentile=(values,p)=>[...values].sort((a,b)=>a-b)[Math.ceil(values.length*p)-1];
12
+ await mkdir(scratchRoot,{recursive:true});
13
+ const scratch=await mkdtemp(path.join(scratchRoot,'read-bench-'));
14
+ try{
15
+ const project=path.join(scratch,'project');
16
+ await mkdir(project);
17
+ const file=path.join(project,'fixture.bin');
18
+ const content=Buffer.alloc(4*1024*1024+131072,0x52);
19
+ await writeFile(file,content);
20
+ const policyPath=path.join(scratch,'policy.json');
21
+ await writeFile(policyPath,JSON.stringify({schema:'deadbyte.coding-policy.v1',enabled:true,
22
+ lease_file:path.join(scratch,'grant.json'),evidence_dir:path.join(scratch,'evidence'),
23
+ roots:{project:{path:project,read:true,write:false,exec:false,deny_paths:[]}},
24
+ commands:{},max_read_bytes:4*1024*1024,max_write_bytes:65536}));
25
+ const rows=[];
26
+ for(let round=0;round<rounds;round++){
27
+ for(const [label,root] of (round%2?[['after',afterRoot],['before',beforeRoot]]:[['before',beforeRoot],['after',afterRoot]])){
28
+ const url=name=>pathToFileURL(path.join(root,name)).href;
29
+ const [{loadCodingPolicy},{createMachineFsRuntime},{generateTrustKeypair},{createBridgeGovernor}]=await Promise.all([
30
+ import(url('src/coding-plane.mjs')),import(url('src/machine-fs.mjs')),
31
+ import(url('src/trust.mjs')),import(url('bridge/src/r40-governor.mjs'))
32
+ ]);
33
+ const keys=await generateTrustKeypair({privateKeyPath:path.join(scratch,`private-${label}-${round}.pem`),publicKeyPath:path.join(scratch,`public-${label}-${round}.pem`)});
34
+ const policy=await loadCodingPolicy(policyPath);
35
+ const runtime=createMachineFsRuntime({policy,signingKeyPath:keys.privateKeyPath,verifyKeyPath:keys.publicKeyPath});
36
+ const governor=createBridgeGovernor();
37
+ const latencies=[],control=[],errors=[];
38
+ let next=0,peakRss=process.memoryUsage().rss;
39
+ const started=performance.now();
40
+ async function bulkWorker(){
41
+ for(;;){
42
+ const index=next++;
43
+ if(index>=operations)return;
44
+ const mark=performance.now();
45
+ const ticket=await governor.acquire('machine_fs',{op:'read'});
46
+ try{
47
+ const result=await runtime.read({rootId:'project',relativePath:'fixture.bin',offset:(index%32)*65536,maxBytes:65536});
48
+ if(result.status!=='ok'||Buffer.from(result.data_base64,'base64').length!==65536)throw new Error('read oracle failed');
49
+ }catch(error){errors.push(String(error));}
50
+ finally{ticket.release();latencies.push(performance.now()-mark);peakRss=Math.max(peakRss,process.memoryUsage().rss);}
51
+ }
52
+ }
53
+ async function controlWorker(){
54
+ for(let index=0;index<operations;index++){
55
+ const mark=performance.now();
56
+ const ticket=await governor.acquire(index%2?'machine_status':'machine_ping');
57
+ try{if(index%2)await readFile(policyPath);else sha(Buffer.from(`ping-${index}`));}
58
+ finally{ticket.release();control.push(performance.now()-mark);}
59
+ }
60
+ }
61
+ await Promise.all([...Array.from({length:concurrency},()=>bulkWorker()),controlWorker()]);
62
+ const elapsed=performance.now()-started;
63
+ await governor.close();
64
+ const boundaryStart=performance.now();
65
+ let boundary={status:'unknown'};
66
+ try{
67
+ const first=await runtime.read({rootId:'project',relativePath:'fixture.bin',offset:65536,maxBytes:4*1024*1024});
68
+ const second=await runtime.read({rootId:'project',relativePath:'fixture.bin',offset:first.next_offset,maxBytes:65536});
69
+ if(first.next_offset<=4*1024*1024||second.eof!==true||Buffer.from(second.data_base64,'base64').length!==65536)throw new Error('cross-boundary oracle failed');
70
+ boundary={status:'pass',page_two_offset:first.next_offset,elapsed_ms:performance.now()-boundaryStart};
71
+ }catch(error){boundary={status:'rejected',reason:String(error?.message||error),elapsed_ms:performance.now()-boundaryStart};}
72
+ if(label==='before'&&boundary.status!=='rejected')errors.push('R63 unexpectedly accepted page two');
73
+ if(label==='after'&&boundary.status!=='pass')errors.push('R64 cross-boundary paging failed');
74
+ rows.push({round,label,source_sha256:sha(await readFile(path.join(root,'src/machine-fs.mjs'))),
75
+ ops:latencies.length,ops_per_second:operations*1000/elapsed,p50_ms:percentile(latencies,.5),p95_ms:percentile(latencies,.95),
76
+ control_p95_ms:percentile(control,.95),peak_rss_bytes:peakRss,boundary,errors});
77
+ }
78
+ }
79
+ const report={schema:'deadbyte.r64.read-benchmark.v1',node:process.version,rounds,operations_per_round:operations,concurrency,
80
+ source_roots:{before:beforeRoot,after:afterRoot},rows,
81
+ limitations:['same-host sequential alternating runs; OS scheduling noise remains','comparable throughput rows use inherited sub-4-MiB reads; boundary timings are separate because R63 cannot execute page two','control probe uses governor and local read/hash, not public MCP machine_ping or machine_status']};
82
+ await writeFile(outputPath,JSON.stringify(report,null,2)+'\n',{flag:'wx'});
83
+ console.log(JSON.stringify({output_path:outputPath,rows:rows.length,errors:rows.reduce((sum,row)=>sum+row.errors.length,0)}));
84
+ }finally{await rm(scratch,{recursive:true,force:true});}
@@ -28,7 +28,7 @@ export const codingInspectInputSchema=z.discriminatedUnion('op',[
28
28
  export const machineFsGatewayInputSchema=z.discriminatedUnion('op',[
29
29
  z.strictObject({op:z.literal('stat'),root_id:id,path:rel.default('.'),hash:z.boolean().default(false)}),
30
30
  z.strictObject({op:z.literal('list'),root_id:id,path:rel.default('.'),cursor,limit:z.number().int().min(1).max(1000).default(100),hash_files:z.boolean().default(false)}),
31
- z.strictObject({op:z.literal('read'),root_id:id,path:rel,offset:z.number().int().min(-4*1024*1024).max(4*1024*1024).default(0),max_bytes:z.number().int().min(1).max(4*1024*1024).default(65536),hash_file:z.boolean().default(false),timeout_ms:z.number().int().min(100).max(180000).default(30000)}),
31
+ z.strictObject({op:z.literal('read'),root_id:id,path:rel,offset:z.number().int().min(-4*1024*1024).max(Number.MAX_SAFE_INTEGER).default(0),max_bytes:z.number().int().min(1).max(4*1024*1024).default(65536),hash_file:z.boolean().default(false),timeout_ms:z.number().int().min(100).max(180000).default(30000)}),
32
32
  z.strictObject({op:z.literal('write'),root_id:id,path:rel,content:z.string().max(12*1024*1024),encoding:z.enum(['utf8','base64']).default('utf8'),expected_sha256:z.string().regex(/^(?:absent|[0-9a-f]{64})$/)}),
33
33
  z.strictObject({op:z.literal('edit_exact'),root_id:id,path:rel,expected_sha256:hex64,old_string:z.string().min(1).max(262144),new_string:z.string().max(524288),expected_replacements:z.number().int().min(1).max(64).default(1)}),
34
34
  z.strictObject({op:z.literal('mkdir'),root_id:id,path:rel}),
@@ -20,7 +20,7 @@ import { windowsPowerShellEnvironment } from './runtime-update.mjs';
20
20
  import { resolveStateRoot } from './state-root.mjs';
21
21
  import {foregroundIdentityMatches,queryForegroundIdentity,validForegroundIdentity} from './foreground-process-identity.mjs';
22
22
  import {acquireForegroundSessionLock} from './foreground-session-lock.mjs';
23
- import { AUTHORITY_PLANES, authorityVector, parseAuthorityObservation, assertAuthorityVector, assertRuntimeReadyObservation, authorityBanner } from './remote-authority.mjs';
23
+ import { AUTHORITY_PLANES, authorityVector, authorityRestorePlan, parseAuthorityObservation, assertAuthorityVector, assertRuntimeReadyObservation, authorityBanner } from './remote-authority.mjs';
24
24
  import { decodeRemoteLogBytes, boundedRemoteDiagnostic, safeRemoteLogPrefixLength } from './remote-log-decode-r60.mjs';
25
25
 
26
26
  const REMOTE_COMMANDS=new Set([
@@ -137,6 +137,20 @@ async function synchronizeForegroundRuntime(ctx,{displayMode='unicode'}={}){
137
137
  }
138
138
  });
139
139
  }
140
+ async function synchronizeOneShotRuntime(ctx,{displayMode='ascii'}={}){
141
+ const active=await readActiveRuntimeBinding(ctx.root);
142
+ if(!active||active.version===DEADBYTE_VERSION) return synchronizeForegroundRuntime(ctx,{displayMode});
143
+ const before=observedAuthority(ctx);
144
+ const expected=Object.fromEntries(AUTHORITY_PLANES.map(plane=>[plane,before.authority[plane].armed]));
145
+ const result=await synchronizeForegroundRuntime(ctx,{displayMode});
146
+ if(result.status!=='upgraded') return result;
147
+ for(const command of authorityRestorePlan(before)){
148
+ const restored=invokeController(ctx,command,{capture:true,allowFailure:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
149
+ if(restored.status!==0) throw new Error(`runtime coherence authority restore '${command}' failed exit=${restored.status}`);
150
+ }
151
+ assertAuthorityVector(observedAuthority(ctx),expected);
152
+ return result;
153
+ }
140
154
  async function readControllerStartFailure(root,command,{maxChars=2048}={}){
141
155
  const file=path.join(root,'runtime','controller-start-failure.json');
142
156
  let raw;
@@ -629,7 +643,7 @@ async function runRemote(args){
629
643
  if(positional.length>1) throw new Error('remote accepts one controller command');
630
644
  const command=positional[0];if(!REMOTE_COMMANDS.has(command)) throw new Error(`unsupported remote command '${command}'`);
631
645
  const ctx=await remoteContext();
632
- await reportVersionCoherence(ctx.root,{displayMode:'ascii'});
646
+ await synchronizeOneShotRuntime(ctx,{displayMode:'ascii'});
633
647
  const result=invokeController(ctx,command,{allowFailure:true});
634
648
  process.exitCode=result.status;
635
649
  }
@@ -1,10 +1,37 @@
1
1
  import {lstat,opendir,readFile} from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import {createHash} from 'node:crypto';
3
+ import {createHash,createPublicKey} from 'node:crypto';
4
4
  import {resolveStateRoot} from './state-root.mjs';
5
+ import {verifySlotUpdateEnvelope} from './runtime-update.mjs';
5
6
 
6
- export async function inspectDoctorTransactions(explicitRoot,{maxEntries=256}={}){
7
- if(!Number.isSafeInteger(maxEntries)||maxEntries<1||maxEntries>256)throw new Error('doctor transaction budget invalid');
7
+ const UPDATE_RECEIPT='deadbyte.r27-slot-update-receipt.v1';
8
+ const FAILURE_RECEIPT='deadbyte.r27-slot-update-failure.v1';
9
+ const ROLLBACK_RECEIPT='deadbyte.r31-slot-rollback-receipt.v1';
10
+ const ROLLBACK_FAILURE_RECEIPT='deadbyte.r31-slot-rollback-failure.v1';
11
+
12
+ async function readReceipt(file){
13
+ const info=await lstat(file);
14
+ if(!info.isFile()||info.isSymbolicLink()||info.size>1048576)throw new Error('unsafe transaction receipt');
15
+ const bytes=await readFile(file);
16
+ if(bytes.length>1048576)throw new Error('transaction receipt budget');
17
+ return JSON.parse(bytes.toString('utf8').replace(/^\uFEFF/,''));
18
+ }
19
+
20
+ function receiptSubjectMatches(state,envelope,kind){
21
+ const body=envelope?.body;
22
+ if(state.schema==='deadbyte.r27-slot-update-state.v1'){
23
+ if(kind==='success')return body?.schema===UPDATE_RECEIPT&&body?.update_id===state.update_id&&body?.rollback_performed===false;
24
+ return body?.schema===FAILURE_RECEIPT&&body?.update_id===state.update_id;
25
+ }
26
+ if(state.schema==='deadbyte.r31-slot-rollback-state.v1'){
27
+ if(kind==='success')return body?.schema===ROLLBACK_RECEIPT&&body?.rollback_id===state.rollback_id&&body?.rollback_performed===true;
28
+ return body?.schema===ROLLBACK_FAILURE_RECEIPT&&body?.rollback_id===state.rollback_id;
29
+ }
30
+ return false;
31
+ }
32
+
33
+ export async function inspectDoctorTransactions(explicitRoot,{maxEntries=1024}={}){
34
+ if(!Number.isSafeInteger(maxEntries)||maxEntries<1||maxEntries>1024)throw new Error('doctor transaction budget invalid');
8
35
  const state=resolveStateRoot(explicitRoot),root=path.join(state,'evidence','updates'),findings=[];
9
36
  const next='Retain transaction evidence; inspect the exact signed receipt/handoff and current pointer before any supported recovery. Doctor never launches recovery.';
10
37
  const add=(level,what,evidence)=>findings.push({id:'transaction_residue',level,what,evidence,next_action:level==='PASS'?null:next});
@@ -17,24 +44,68 @@ export async function inspectDoctorTransactions(explicitRoot,{maxEntries=256}={}
17
44
  else add('FAIL','Update evidence directory cannot be safely inspected.',{error_code:error?.code??'VALIDATION_FAILED'});
18
45
  return {status:findings[0].level,findings};
19
46
  }
20
- let entries=0,checked=0,terminal=0;
47
+ let publicKey=null;
48
+ const loadPublicKey=async()=>{
49
+ if(publicKey)return publicKey;
50
+ const keyFile=path.join(state,'trust','ed25519-public.pem');
51
+ const info=await lstat(keyFile);
52
+ if(!info.isFile()||info.isSymbolicLink()||info.size>65536)throw new Error('unsafe transaction trust key');
53
+ publicKey=createPublicKey(await readFile(keyFile));
54
+ if(publicKey.asymmetricKeyType!=='ed25519')throw new Error('transaction trust key must be Ed25519');
55
+ return publicKey;
56
+ };
57
+ let entries=0,checked=0,terminal=0,verifiedTerminalReceipts=0,verifiedSuccessReceipts=0,verifiedFailureReceipts=0,unresolved=0,inventoryComplete=true;
21
58
  try{
22
59
  for await(const entry of directory){
23
- if(++entries>maxEntries){add('WARN','Transaction inventory budget reached; unobserved entries remain.',{max_entries:maxEntries,states_checked:checked,inventory_complete:false});break;}
60
+ if(++entries>maxEntries){inventoryComplete=false;add('WARN','Transaction inventory budget reached; unobserved entries remain.',{max_entries:maxEntries,states_checked:checked,inventory_complete:false});break;}
24
61
  if(entry.isSymbolicLink()){add('FAIL','Transaction evidence contains a link; it was not followed.',{entry_name_sha256:createHash('sha256').update(entry.name).digest('hex')});continue;}
25
62
  if(!entry.isDirectory())continue;
26
- const file=path.join(root,entry.name,'state.json');let bytes;
63
+ const dir=path.join(root,entry.name),file=path.join(dir,'state.json');let bytes;
27
64
  try{
28
65
  const info=await lstat(file);if(!info.isFile()||info.isSymbolicLink()||info.size>1048576)throw new Error('unsafe transaction state');
29
66
  bytes=await readFile(file);if(bytes.length>1048576)throw new Error('transaction state budget');
30
67
  const body=JSON.parse(bytes.toString('utf8').replace(/^\uFEFF/,''));checked++;
31
68
  const digest=createHash('sha256').update(bytes).digest('hex');
32
69
  if(!['deadbyte.r27-slot-update-state.v1','deadbyte.r31-slot-rollback-state.v1'].includes(body?.schema)||typeof body.phase!=='string'||!/^[a-z_]{1,64}$/.test(body.phase)){add('FAIL','Transaction state schema or phase is invalid.',{state_sha256:digest,check:'transaction_schema'});continue;}
33
- if(['completed','superseded','failed_receipted'].includes(body.phase)){terminal++;continue;}
70
+ const receiptPath=path.join(dir,'receipt.json'),failurePath=path.join(dir,'failure-receipt.json');
71
+ let receiptExists=false,failureExists=false;
72
+ try{const st=await lstat(receiptPath);receiptExists=st.isFile()&&!st.isSymbolicLink();}catch(error){if(error?.code!=='ENOENT')throw error;}
73
+ try{const st=await lstat(failurePath);failureExists=st.isFile()&&!st.isSymbolicLink();}catch(error){if(error?.code!=='ENOENT')throw error;}
74
+ if(receiptExists&&failureExists){add('FAIL','Transaction contains conflicting terminal receipts.',{state_sha256:digest,check:'terminal_receipt_conflict'});continue;}
75
+ const terminalPath=receiptExists?receiptPath:failureExists?failurePath:null;
76
+ if(terminalPath){
77
+ const kind=receiptExists?'success':'failure';
78
+ const envelope=await readReceipt(terminalPath);
79
+ const key=await loadPublicKey();
80
+ if(!verifySlotUpdateEnvelope(envelope,key)||!receiptSubjectMatches(body,envelope,kind)){
81
+ add('FAIL','Transaction terminal receipt is invalid or not subject-bound to its journal.',{state_sha256:digest,check:'terminal_receipt_verification',receipt_kind:kind});
82
+ continue;
83
+ }
84
+ terminal++;verifiedTerminalReceipts++;
85
+ if(kind==='success')verifiedSuccessReceipts++;else verifiedFailureReceipts++;
86
+ continue;
87
+ }
88
+
89
+ if(body.phase==='superseded'){terminal++;continue;}
90
+ if(body.phase==='failed_receipted'){
91
+ add('FAIL','Transaction journal claims a signed failure receipt but the receipt is missing.',{state_sha256:digest,check:'missing_failure_receipt',recorded_phase:body.phase});
92
+ continue;
93
+ }
94
+ unresolved++;
34
95
  add('WARN','Nonterminal or unresolved transaction record remains; liveness/outcome is not inferred from its age or PID.',{state_sha256:digest,schema:body.schema,recorded_phase:body.phase});
35
96
  }catch(error){if(error?.code==='ENOENT')continue;add('FAIL','Transaction state cannot be safely parsed or inspected.',{error_code:error?.code??'VALIDATION_FAILED'});}
36
97
  }
37
98
  }catch(error){add('FAIL','Transaction inventory observation failed.',{error_code:error?.code??'OBSERVATION_FAILED'});}
38
- add('PASS','Recorded terminal transaction phases counted; signatures and desired outcomes are not established by this inventory.',{states_checked:checked,recorded_terminal_states:terminal,receipt_verification_performed:false});
99
+ add('PASS','Recorded terminal transaction outcomes counted; signed receipts are verified when they reconcile stale journal phases.',{
100
+ states_checked:checked,
101
+ recorded_terminal_states:terminal,
102
+ verified_terminal_receipts:verifiedTerminalReceipts,
103
+ verified_success_receipts:verifiedSuccessReceipts,
104
+ verified_failure_receipts:verifiedFailureReceipts,
105
+ unresolved_transactions:unresolved,
106
+ inventory_complete:inventoryComplete,
107
+ max_entries:maxEntries,
108
+ receipt_verification_performed:true
109
+ });
39
110
  return {status:findings.some(f=>f.level==='FAIL')?'FAIL':findings.some(f=>f.level==='WARN')?'WARN':'PASS',findings};
40
111
  }
@@ -38,7 +38,7 @@ export function registerMachineFsTools(server, { runtime }) {
38
38
  });
39
39
  server.registerTool('machine_fs_read', {
40
40
  description:'Read one bounded byte range from a root-relative regular file.',
41
- inputSchema:z.strictObject({ root_id:id, path:rel, offset:z.number().int().min(-4*1024*1024).max(4*1024*1024).default(0), max_bytes:z.number().int().min(1).max(4*1024*1024).default(65536), hash_file:z.boolean().default(false), timeout_ms:z.number().int().min(100).max(180000).default(30000) }),
41
+ inputSchema:z.strictObject({ root_id:id, path:rel, offset:z.number().int().min(-4*1024*1024).max(Number.MAX_SAFE_INTEGER).default(0), max_bytes:z.number().int().min(1).max(4*1024*1024).default(65536), hash_file:z.boolean().default(false), timeout_ms:z.number().int().min(100).max(180000).default(30000) }),
42
42
  outputSchema:machineFsReadOutputSchema
43
43
  }, async ({root_id,path,offset,max_bytes,hash_file,timeout_ms}) => {
44
44
  try { return ok(machineFsReadOutputSchema, await runtime.read({rootId:root_id,relativePath:path,offset,maxBytes:max_bytes,hashFile:hash_file,timeoutMs:timeout_ms})); }
@@ -21,10 +21,10 @@ const MAX_DIRECTORY_IDENTITY_ENTRIES = 4096;
21
21
  const sha256Bytes = bytes => createHash('sha256').update(bytes).digest('hex');
22
22
  const sha256Object = value => sha256Bytes(Buffer.from(canonicalJson(value), 'utf8'));
23
23
  const nowUtc = () => new Date().toISOString();
24
- async function sha256File(file) {
24
+ async function sha256File(file, signal = undefined) {
25
25
  const hash = createHash('sha256');
26
26
  await new Promise((resolve, reject) => {
27
- const stream = createReadStream(file);
27
+ const stream = createReadStream(file, { signal });
28
28
  stream.on('data', chunk => hash.update(chunk));
29
29
  stream.once('error', reject);
30
30
  stream.once('end', resolve);
@@ -35,11 +35,13 @@ async function sha256File(file) {
35
35
  const machineFsReadWorkerPath = fileURLToPath(new URL('./machine-fs-read-worker.mjs', import.meta.url));
36
36
  async function boundedReadInProcess({ file, offset, length, hashFile, timeoutMs }) {
37
37
  const handle = await open(file, 'r');
38
+ const abortController = new AbortController();
38
39
  let timer;
39
40
  let settled = false;
40
41
  const abort = new Promise((_, reject) => {
41
42
  timer = setTimeout(() => {
42
43
  settled = true;
44
+ abortController.abort();
43
45
  void handle.close().catch(() => {});
44
46
  reject(new Error(`machine fs read timed out after ${timeoutMs}ms`));
45
47
  }, timeoutMs);
@@ -50,7 +52,7 @@ async function boundedReadInProcess({ file, offset, length, hashFile, timeoutMs
50
52
  const buffer = Buffer.alloc(length);
51
53
  const result = length ? await handle.read(buffer, 0, length, offset) : { bytesRead: 0 };
52
54
  let fileSha256 = null;
53
- if (hashFile) fileSha256 = await sha256File(file);
55
+ if (hashFile) fileSha256 = await sha256File(file, abortController.signal);
54
56
  return { schema: 'deadbyte.machine-fs-read-worker.v1', bytes_read: result.bytesRead,
55
57
  data_base64: buffer.subarray(0, result.bytesRead).toString('base64'), file_sha256: fileSha256 };
56
58
  })();
@@ -408,13 +410,14 @@ export function createMachineFsRuntime({ policy, signingKeyPath, verifyKeyPath,
408
410
  return { status: 'ok', root_id: rootId, path: found.relative, entries, next_cursor: nextCursor };
409
411
  }
410
412
  async function readRange({ rootId, relativePath, offset = 0, maxBytes = 65536, hashFile = false, timeoutMs = 30000 }) {
411
- if (!Number.isInteger(offset) || offset < -policy.max_read_bytes || offset > policy.max_read_bytes) throw new Error(`machine fs read offset must be in [-${policy.max_read_bytes}, ${policy.max_read_bytes}]`);
413
+ if (!Number.isSafeInteger(offset) || offset < -policy.max_read_bytes) throw new Error(`machine fs read offset must be a safe integer >= -${policy.max_read_bytes}`);
412
414
  if (!Number.isInteger(maxBytes) || maxBytes < 1 || maxBytes > policy.max_read_bytes) throw new Error(`machine fs max_bytes must be in [1, ${policy.max_read_bytes}]`);
413
415
  if (!Number.isInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 180000) throw new Error('machine fs read timeout_ms must be in [100, 180000]');
414
416
  const grant = await assertCodingReadAllowed(policy, rootId);
415
417
  const found = await resolveExisting(policy, grant, rootId, relativePath, 'read', false);
416
418
  const info = await lstat(found.resolved);
417
419
  if (!info.isFile()) throw new Error('machine fs read target must be a regular file');
420
+ if (!Number.isSafeInteger(info.size)) throw new Error('machine fs read file size exceeds safe integer range');
418
421
  const effectiveOffset = offset < 0 ? Math.max(0, info.size + offset) : offset;
419
422
  if (effectiveOffset > info.size) throw new Error('machine fs read offset exceeds file size');
420
423
  const length = Math.min(maxBytes, info.size - effectiveOffset);
@@ -11,6 +11,16 @@ export function authorityVector(mode='default'){
11
11
  });
12
12
  }
13
13
 
14
+ export function authorityRestorePlan(snapshot){
15
+ const disarm=[],arm=[];
16
+ for(const plane of AUTHORITY_PLANES){
17
+ const entry=snapshot?.authority?.[plane];
18
+ if(!entry||typeof entry.armed!=='boolean') throw new Error(`authority ${plane} observation invalid`);
19
+ (entry.armed?arm:disarm).push(`${entry.armed?'arm':'disarm'}${plane}`);
20
+ }
21
+ return Object.freeze([...disarm,...arm]);
22
+ }
23
+
14
24
  export function parseAuthorityObservation(raw){
15
25
  let value;
16
26
  try{value=typeof raw==='string'?JSON.parse(raw.replace(/^\uFEFF/,'')):raw;}
@@ -26,6 +26,7 @@ const UPDATE_SCHEMA='deadbyte.r27-slot-update-receipt.v1';
26
26
  const HANDOFF_SCHEMA='deadbyte.r27-slot-update-handoff.v1';
27
27
  const FAILURE_SCHEMA='deadbyte.r27-slot-update-failure.v1';
28
28
  const ROLLBACK_SCHEMA='deadbyte.r31-slot-rollback-receipt.v1';
29
+ const ROLLBACK_FAILURE_SCHEMA='deadbyte.r31-slot-rollback-failure.v1';
29
30
  const DEFAULT_STATE_ROOT=undefined;
30
31
  const TOP_LAUNCHER=path.join(os.homedir(),'Downloads','DEADBYTE-MCP.cmd');
31
32
  const RUNTIME_STABILITY_MS=12000;
@@ -1762,7 +1763,7 @@ export async function performSlotRollback({activationReceipt,stateRoot=DEFAULT_S
1762
1763
  const currentCtx=slotContext(state,pointerInfo.pointer),currentRuntime=await captureRuntime(currentCtx);
1763
1764
  const revoked=await revokeAuthority(currentCtx,currentRuntime.status);
1764
1765
  const rollbackId=`r31-rollback-${activationEnvelope.receipt_sha256.slice(0,16)}-${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`;
1765
- const evidenceDir=path.join(state,'evidence','updates',rollbackId),statePath=path.join(evidenceDir,'state.json'),receiptPath=path.join(evidenceDir,'receipt.json');
1766
+ const evidenceDir=path.join(state,'evidence','updates',rollbackId),statePath=path.join(evidenceDir,'state.json'),receiptPath=path.join(evidenceDir,'receipt.json'),failurePath=path.join(evidenceDir,'failure-receipt.json');
1766
1767
  await mkdir(evidenceDir,{recursive:true});
1767
1768
  const journal={schema:'deadbyte.r31-slot-rollback-state.v1',rollback_id:rollbackId,activation_receipt:path.resolve(receiptFile),
1768
1769
  activation_receipt_sha256:activationEnvelope.receipt_sha256,handoff_path:handoffPath,
@@ -1807,8 +1808,19 @@ export async function performSlotRollback({activationReceipt,stateRoot=DEFAULT_S
1807
1808
  }
1808
1809
  try { await startAndVerify(currentCtx,currentRuntime); recovery.successor_runtime_restarted=true; } catch(recoveryError){ recovery.errors.push(`restart-successor:${recoveryError.message}`); }
1809
1810
  }
1810
- await writeState(statePath,journal,'failed',{error:{name:error?.name??'Error',message:String(error?.message??error)},recovery}).catch(()=>{});
1811
- const wrapped=new Error(`R31 slot rollback failed: ${error?.message??error}`); wrapped.cause=error; wrapped.statePath=statePath; throw wrapped;
1811
+ const failure={schema:ROLLBACK_FAILURE_SCHEMA,rollback_id:rollbackId,activation_receipt_sha256:activationEnvelope.receipt_sha256,
1812
+ failed_at_utc:new Date().toISOString(),phase:journal.phase??'unknown',from_pointer:publicPointer(activePointer),to_pointer:publicPointer(priorPointer),
1813
+ error:{name:error?.name??'Error',message:String(error?.message??error)},recovery};
1814
+ await writeState(statePath,journal,'failed',{error:failure.error,recovery}).catch(()=>{});
1815
+ let failureReceipt=null;
1816
+ try{
1817
+ failureReceipt=await writeEnvelope(failurePath,failure,key);
1818
+ await writeState(statePath,journal,'failed_receipted',{failure_receipt_path:failurePath,failure_receipt_sha256:failureReceipt.receipt_sha256});
1819
+ }catch(receiptError){
1820
+ recovery.errors.push('failure-receipt:'+receiptError.message);
1821
+ await writeState(statePath,journal,'failed_unreceipted',{recovery,failure_receipt_error:String(receiptError?.message??receiptError)}).catch(()=>{});
1822
+ }
1823
+ const wrapped=new Error(`R31 slot rollback failed: ${error?.message??error}`); wrapped.cause=error; wrapped.statePath=statePath; wrapped.failureReceiptPath=failureReceipt?failurePath:null; throw wrapped;
1812
1824
  }
1813
1825
  }
1814
1826
 
package/src/version.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.15.38';
1
+ export const DEADBYTE_VERSION = '0.15.41';
2
2
  export const MCP_PROTOCOL_VERSION = '2026-07-28';
3
3
  export const RECEIPT_SCHEMA_V1 = 'deadbyte.receipt.v1';
4
4
  export const RECEIPT_SCHEMA_V2 = 'deadbyte.receipt.v2';
@@ -10,12 +10,12 @@ test('R58 bounded transaction inventory flags residue without recovering or over
10
10
  assert.equal((await inspectDoctorTransactions(root)).status,'PASS');assert.deepEqual(await readdir(root),[]);
11
11
  const transactions=path.join(root,'evidence','updates');await mkdir(transactions,{recursive:true});
12
12
  const write=async(name,body)=>{const dir=path.join(transactions,name);await mkdir(dir);const file=path.join(dir,'state.json'),bytes=JSON.stringify(body);await writeFile(file,bytes);return {file,bytes};};
13
- const done=await write('done',{schema:'deadbyte.r27-slot-update-state.v1',phase:'completed'});
13
+ const done=await write('done',{schema:'deadbyte.r27-slot-update-state.v1',phase:'superseded'});
14
14
  assert.equal((await inspectDoctorTransactions(root)).status,'PASS');
15
15
  const pending=await write('pending',{schema:'deadbyte.r31-slot-rollback-state.v1',phase:'prior_pointer_restored',supervisor_pid:process.pid});
16
16
  const result=await inspectDoctorTransactions(root);assert.equal(result.status,'WARN');assert.ok(result.findings.some(f=>f.evidence.recorded_phase==='prior_pointer_restored'));
17
17
  for(const item of [done,pending])assert.equal(await readFile(item.file,'utf8'),item.bytes);
18
18
  const bounded=await inspectDoctorTransactions(root,{maxEntries:1});assert.equal(bounded.status,'WARN');assert.ok(bounded.findings.some(f=>f.evidence.inventory_complete===false));
19
19
  const corrupt=await write('corrupt',{schema:'INVALID',phase:'completed'});assert.equal((await inspectDoctorTransactions(root)).status,'FAIL');assert.equal(await readFile(corrupt.file,'utf8'),corrupt.bytes);
20
- await assert.rejects(inspectDoctorTransactions(root,{maxEntries:257}),/budget invalid/);
20
+ await assert.rejects(inspectDoctorTransactions(root,{maxEntries:1025}),/budget invalid/);
21
21
  });
@@ -71,7 +71,7 @@ async function captureCurrentSurface(){
71
71
  }
72
72
  }
73
73
 
74
- test('R59 compact client-visible tool names and schemas match sealed 0.15.35 baseline',async()=>{
74
+ test('R59 compact baseline changes only at the explicitly authorized R64 read-offset bound',async()=>{
75
75
  const raw=JSON.parse(await readFile(fixturePath,'utf8'));
76
76
  assert.equal(raw.schema,'deadbyte.tool-surface-baseline.v1');
77
77
  assert.equal(raw.release_id,'v0.15.35-1b5715b276facbc0');
@@ -82,9 +82,19 @@ test('R59 compact client-visible tool names and schemas match sealed 0.15.35 bas
82
82
  const diff=diffToolSurface(baseline,current);
83
83
  assert.deepEqual(diff.added,[]);
84
84
  assert.deepEqual(diff.removed,[]);
85
- assert.deepEqual(diff.schema_changed,[]);
86
- assert.equal(diff.unchanged.length,43);
87
- assert.equal(current.surface_sha256,baseline.surface_sha256);
85
+ const packageVersion=JSON.parse(await readFile(path.join(repoRoot,'package.json'),'utf8')).version;
86
+ if(packageVersion==='0.15.41'){
87
+ assert.deepEqual(diff.schema_changed,['machine_fs']);
88
+ assert.equal(diff.unchanged.length,42);
89
+ const prior=baseline.tools.find(tool=>tool.name==='machine_fs');
90
+ const present=current.tools.find(tool=>tool.name==='machine_fs');
91
+ assert.equal(present.output_sha256,prior.output_sha256,'output contract must remain unchanged');
92
+ assert.notEqual(present.input_sha256,prior.input_sha256,'R64 must expose the widened read offset');
93
+ }else{
94
+ assert.deepEqual(diff.schema_changed,[]);
95
+ assert.equal(diff.unchanged.length,43);
96
+ assert.equal(current.surface_sha256,baseline.surface_sha256);
97
+ }
88
98
  console.log(JSON.stringify({baseline_release:raw.release_id,surface_sha256:current.surface_sha256,
89
99
  unchanged:diff.unchanged.length,added:diff.added,removed:diff.removed,schema_changed:diff.schema_changed}));
90
100
  });
@@ -4,13 +4,14 @@ import {readFile} from 'node:fs/promises';
4
4
 
5
5
  const read=rel=>readFile(new URL('../'+rel,import.meta.url),'utf8');
6
6
  const json=rel=>read(rel).then(raw=>JSON.parse(raw.replace(/^\uFEFF/,'')));
7
+ const currentPackage=await json('package.json');
7
8
  const PARENT={
8
9
  release_id:'v0.15.37-1cfecd7889419da7',
9
10
  manifest_sha256:'1cfecd7889419da7800814e3a2a4ffa93d48dfdb0c344a5b7b91fe8d093230aa',
10
11
  generation_manifest_sha256:'54842021c75f01bf7ecbd5b10ab8a482ecf107765b45c46f9d39b12bab65eac4'
11
12
  };
12
13
 
13
- test('R61 identity binds exact sealed 0.15.37 predecessor without a public surface expansion',async()=>{
14
+ test('R61 identity binds exact sealed 0.15.37 predecessor without a public surface expansion',{skip:currentPackage.version!=='0.15.38'},async()=>{
14
15
  const [pkg,lock,bridge,release,provenance,matrix,sbom,version,gate]=await Promise.all([
15
16
  json('package.json'),json('package-lock.json'),json('bridge/package.json'),json('RELEASE.json'),
16
17
  json('PROVENANCE.json'),json('TEST-MATRIX.json'),json('SBOM.cdx.json'),read('src/version.mjs'),
@@ -0,0 +1,50 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {mkdir,mkdtemp,writeFile} from 'node:fs/promises';
4
+ import {generateKeyPairSync,createHash,sign} from 'node:crypto';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import {inspectDoctorTransactions} from '../src/doctor-transactions-r58.mjs';
8
+
9
+ const sha=b=>createHash('sha256').update(b).digest('hex');
10
+ const canonical=value=>Array.isArray(value)?value.map(canonical):value&&typeof value==='object'
11
+ ?Object.fromEntries(Object.keys(value).sort().map(key=>[key,canonical(value[key])])):value;
12
+ const canonicalJson=value=>JSON.stringify(canonical(value));
13
+ const domain=Buffer.from('DEADBYTE-R27-SLOT-UPDATE-V1\0');
14
+ function envelope(body,keys){
15
+ const bodyBytes=Buffer.from(canonicalJson(body),'utf8');
16
+ const unsigned={body,body_sha256:sha(bodyBytes),attestation:{
17
+ algorithm:'ed25519',domain:'DEADBYTE-R27-SLOT-UPDATE-V1',
18
+ key_id:sha(keys.publicKey.export({type:'spki',format:'der'})),
19
+ signature_base64:sign(null,Buffer.concat([domain,bodyBytes]),keys.privateKey).toString('base64')
20
+ }};
21
+ return {...unsigned,receipt_sha256:sha(Buffer.from(canonicalJson(unsigned),'utf8'))};
22
+ }
23
+
24
+ test('R62 doctor rejects signed success receipts whose rollback semantics contradict their schema',async()=>{
25
+ const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r62-doctor-receipt-semantics-'));
26
+ const trust=path.join(root,'trust'),updates=path.join(root,'evidence','updates');
27
+ await mkdir(trust,{recursive:true});await mkdir(updates,{recursive:true});
28
+ const keys=generateKeyPairSync('ed25519');
29
+ await writeFile(path.join(trust,'ed25519-public.pem'),keys.publicKey.export({type:'spki',format:'pem'}));
30
+
31
+ const updateDir=path.join(updates,'bad-update-success');await mkdir(updateDir);
32
+ await writeFile(path.join(updateDir,'state.json'),JSON.stringify({
33
+ schema:'deadbyte.r27-slot-update-state.v1',update_id:'bad-update-success',phase:'completed'
34
+ }));
35
+ await writeFile(path.join(updateDir,'receipt.json'),JSON.stringify(envelope({
36
+ schema:'deadbyte.r27-slot-update-receipt.v1',update_id:'bad-update-success',rollback_performed:true
37
+ },keys)));
38
+
39
+ const rollbackDir=path.join(updates,'bad-rollback-success');await mkdir(rollbackDir);
40
+ await writeFile(path.join(rollbackDir,'state.json'),JSON.stringify({
41
+ schema:'deadbyte.r31-slot-rollback-state.v1',rollback_id:'bad-rollback-success',phase:'completed'
42
+ }));
43
+ await writeFile(path.join(rollbackDir,'receipt.json'),JSON.stringify(envelope({
44
+ schema:'deadbyte.r31-slot-rollback-receipt.v1',rollback_id:'bad-rollback-success',rollback_performed:false
45
+ },keys)));
46
+
47
+ const result=await inspectDoctorTransactions(root);
48
+ assert.equal(result.status,'FAIL');
49
+ assert.equal(result.findings.filter(f=>f.evidence?.check==='terminal_receipt_verification').length,2);
50
+ });