deadbyte-mcp 0.15.35 → 0.15.37

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 (92) hide show
  1. package/MANIFEST.SHA256 +91 -46
  2. package/PROVENANCE.json +76 -30
  3. package/README.txt +62 -8
  4. package/RELEASE.json +78 -23
  5. package/SBOM.cdx.json +10 -25
  6. package/TEST-MATRIX.json +109 -44
  7. package/bridge/package.json +4 -2
  8. package/bridge/src/bridge-health-r59.mjs +53 -0
  9. package/bridge/src/bridge.mjs +158 -26
  10. package/bridge/src/canonical-json-r60.mjs +13 -0
  11. package/bridge/src/r40-governor.mjs +56 -10
  12. package/bridge/src/reconnect-delay-r60.mjs +31 -0
  13. package/bridge/src/remote-call-claims-r60.mjs +157 -0
  14. package/bridge/src/remote-dispatch-r60.mjs +75 -0
  15. package/controller/deadbyte-controller.ps1 +76 -13
  16. package/controller/deadbyte-process-policy.json +4 -4
  17. package/docs/R59-PROVENANCE.json +87 -0
  18. package/docs/R59-RELEASE.json +187 -0
  19. package/docs/R59-SBOM.cdx.json +68 -0
  20. package/docs/R59-TASK-CONTRACT.md +26 -0
  21. package/docs/R59-TEST-MATRIX.json +258 -0
  22. package/docs/R59-bridge-package.json +13 -0
  23. package/docs/R59-package-lock.json +184 -0
  24. package/docs/R59-package.json +106 -0
  25. package/docs/R59-version.mjs +6 -0
  26. package/docs/R60-TASK-CONTRACT.md +22 -0
  27. package/package.json +7 -3
  28. package/proof/r59-full-regression-current.json +13 -0
  29. package/proof/r59-full-regression-current.stderr.log +0 -0
  30. package/proof/r59-full-regression-current.stdout.log +1268 -0
  31. package/scripts/full-regression-r60.mjs +32 -0
  32. package/scripts/gate-windows-r59-prepublish.ps1 +41 -0
  33. package/scripts/gate-windows-r60-prepublish.ps1 +74 -0
  34. package/scripts/r59-full-regression-capture.mjs +35 -0
  35. package/src/capability-resolver.mjs +25 -7
  36. package/src/capability-snapshot.mjs +56 -0
  37. package/src/compact-gateway-mcp-tools.mjs +10 -11
  38. package/src/deadbyte-cli.mjs +80 -34
  39. package/src/mcp-server.mjs +179 -118
  40. package/src/r40-compact-disarmed-probe.mjs +1 -1
  41. package/src/r44-compact-disarmed-probe.mjs +1 -1
  42. package/src/r45-compact-disarmed-probe.mjs +1 -1
  43. package/src/remote-authority.mjs +56 -0
  44. package/src/remote-enforcement-probe-r60.mjs +17 -0
  45. package/src/remote-live-log.mjs +19 -2
  46. package/src/remote-log-decode-r60.mjs +32 -0
  47. package/src/remote-mcp-machine-smoke-client.mjs +19 -1
  48. package/src/remote-mcp-r40-parity-client.mjs +1 -1
  49. package/src/remote-mcp-r44-parity-client.mjs +1 -1
  50. package/src/remote-mcp-r45-parity-client.mjs +1 -1
  51. package/src/remote-mcp-smoke-client.mjs +1 -1
  52. package/src/remote-session-guardian.mjs +20 -3
  53. package/src/runtime-plane-registry.mjs +74 -0
  54. package/src/tool-audit.mjs +5 -0
  55. package/src/version.mjs +1 -1
  56. package/test/bridge-runtime-r59.test.mjs +60 -0
  57. package/test/capability-resolver.test.mjs +16 -0
  58. package/test/capability-snapshot-r59.test.mjs +39 -0
  59. package/test/compact-gateway-r36.test.mjs +22 -2
  60. package/test/compact-max-catalog-r44.test.mjs +1 -1
  61. package/test/compact-max-catalog-r45.test.mjs +1 -1
  62. package/test/contained.test.mjs +2 -1
  63. package/test/controller-policy-preflight-r59.test.mjs +39 -0
  64. package/test/fixtures/r59-compact-surface-v0.15.35.json +222 -0
  65. package/test/observation-integration.test.mjs +2 -3
  66. package/test/optional-plane-degraded-r59.test.mjs +44 -0
  67. package/test/r48-release-contract.test.mjs +1 -1
  68. package/test/r49-windows-gate.test.mjs +1 -1
  69. package/test/r51-runtime-coherence.test.mjs +2 -2
  70. package/test/r58-bundled-bridge.test.mjs +3 -1
  71. package/test/r58-cleanup-retention.test.mjs +9 -3
  72. package/test/r58-concurrent-recovery.test.mjs +9 -3
  73. package/test/r58-controller-live-local.test.mjs +36 -4
  74. package/test/r58-controller-state-coherence.test.mjs +1 -1
  75. package/test/r58-release-contract.test.mjs +3 -2
  76. package/test/r58-stale-owner.test.mjs +2 -2
  77. package/test/r58-windows-gate.test.mjs +2 -1
  78. package/test/r59-release-contract.test.mjs +61 -0
  79. package/test/r59-tool-surface-compat.test.mjs +90 -0
  80. package/test/r60-authority-coherence.test.mjs +36 -0
  81. package/test/r60-authority-enforcement.test.mjs +28 -0
  82. package/test/r60-bridge-dispatch.test.mjs +119 -0
  83. package/test/r60-clock-discipline.test.mjs +15 -0
  84. package/test/r60-live-timeline.test.mjs +29 -0
  85. package/test/r60-reconnect-errors.test.mjs +21 -0
  86. package/test/r60-reconnect-jitter.test.mjs +18 -0
  87. package/test/r60-release-contract.test.mjs +81 -0
  88. package/test/r60-remote-call-recovery.test.mjs +170 -0
  89. package/test/r60-windows-log-decode.test.mjs +25 -0
  90. package/test/remote-live-log-r37.test.mjs +1 -1
  91. package/test/remote-session-cli.test.mjs +200 -43
  92. package/test/runtime-plane-registry-r59.test.mjs +40 -0
@@ -0,0 +1,90 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createHash } from 'node:crypto';
4
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { Client } from '@modelcontextprotocol/client';
8
+ import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/client/stdio';
9
+ import { createR34Fixture } from './helpers/autonomous-r34-fixture.mjs';
10
+ import { snapshotToolsListResult, diffToolSurface } from '../src/tool-surface-ledger.mjs';
11
+
12
+ const here=path.dirname(fileURLToPath(import.meta.url));
13
+ const repoRoot=path.resolve(here,'..');
14
+ const fixturePath=path.join(here,'fixtures','r59-compact-surface-v0.15.35.json');
15
+ const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
16
+ const absent=async p=>{try{await access(p);return false;}catch(e){if(e?.code==='ENOENT')return true;throw e;}};
17
+
18
+ async function captureCurrentSurface(){
19
+ const f=await createR34Fixture({authority:'disarmed'});
20
+ const processPolicyPath=path.join(f.root,'process-policy.json');
21
+ const processGrant=path.join(f.root,'process-grant.json');
22
+ const processState=path.join(f.root,'process-state');
23
+ const hostPolicyPath=path.join(f.root,'host-policy.json');
24
+ const hostArm=path.join(f.root,'host-arm.json');
25
+ const desktopPolicyPath=path.join(f.root,'desktop-policy.json');
26
+ const desktopGrant=path.join(f.root,'desktop-grant.json');
27
+ const nodeSha=sha(await readFile(process.execPath));
28
+ await mkdir(processState,{recursive:true});
29
+ await writeFile(processPolicyPath,JSON.stringify({
30
+ schema:'deadbyte.process-policy.v1',enabled:true,grant_file:processGrant,state_dir:processState,
31
+ native_host:{path:process.execPath,sha256:nodeSha},roots:{core:{path:f.project,deny_paths:[]}},
32
+ profiles:{node:{executable:process.execPath,executable_sha256:nodeSha,root_ids:['core'],prefix_args:[],
33
+ transport:'pipe',max_args:8,max_arg_bytes:1024,max_sessions:2,max_input_bytes:65536,
34
+ max_retained_output_bytes:262144,env_allow:['PATH','TEMP','TMP'],input_actions:{ping:'PING\\r\\n'}}}
35
+ },null,2));
36
+ await writeFile(hostPolicyPath,JSON.stringify({
37
+ schema:'deadbyte.host-policy.v1',enabled:true,arm_file:hostArm,max_read_bytes:262144,max_write_bytes:1048576,
38
+ roots:{core:{path:f.project,read:true,write:true,exec:true}},commands:{node_script:{kind:'script',
39
+ executable:process.execPath,executable_sha256:nodeSha,root_ids:['core'],prefix_args:[],extensions:['.mjs'],
40
+ timeout_ms:5000,max_output_bytes:65536}}
41
+ },null,2));
42
+ const desktopTemplate=JSON.parse(await readFile(path.join(repoRoot,'controller','deadbyte-desktop-policy.json'),'utf8'));
43
+ desktopTemplate.grant_file=desktopGrant;
44
+ desktopTemplate.evidence_dir=path.join(f.root,'desktop-evidence');
45
+ await writeFile(desktopPolicyPath,JSON.stringify(desktopTemplate,null,2));
46
+
47
+ const authorityFiles=[processGrant,hostArm,desktopGrant,f.codingPolicy.lease_file,f.policy.lease_file];
48
+ for(const p of authorityFiles) assert.equal(await absent(p),true,'authority unexpectedly armed: '+p);
49
+
50
+ const client=new Client({name:'deadbyte-r59-surface-compat',version:'0.15.35'},{versionNegotiation:{mode:{pin:'2026-07-28'}}});
51
+ const env={...getDefaultEnvironment(),DEADBYTE_CAPABILITY_PROFILE:'compact',DEADBYTE_AUDIT_RUNTIME:'0',
52
+ DEADBYTE_STATE_ROOT:path.join(f.root,'state'),DEADBYTE_WORKSPACE_ROOT:f.root,
53
+ DEADBYTE_SIGNING_KEY:f.keys.privateKeyPath,DEADBYTE_VERIFY_KEY:f.keys.publicKeyPath,
54
+ DEADBYTE_PRIVILEGED_POLICY:hostPolicyPath,DEADBYTE_MACHINE_RUNTIME:'1',DEADBYTE_AGENT_RUNTIME:'0',
55
+ DEADBYTE_PROCESS_RUNTIME:'1',DEADBYTE_PROCESS_POLICY:processPolicyPath,DEADBYTE_CODING_RUNTIME:'1',
56
+ DEADBYTE_CODING_POLICY:path.join(f.root,'coding-policy.json'),DEADBYTE_MACHINE_FS_RUNTIME:'1',
57
+ DEADBYTE_OBSERVATION_RUNTIME:'1',DEADBYTE_DESKTOP_RUNTIME:'1',DEADBYTE_DESKTOP_POLICY:desktopPolicyPath,
58
+ DEADBYTE_AUTONOMOUS_RUNTIME:'1',DEADBYTE_AUTONOMOUS_POLICY:path.join(f.root,'autonomous-policy.json'),
59
+ DEADBYTE_AUTONOMOUS_SUPERVISOR_MODE:'external'};
60
+ const transport=new StdioClientTransport({command:process.execPath,args:[path.join(repoRoot,'src','mcp-server.mjs')],
61
+ cwd:repoRoot,env,stderr:'pipe'});
62
+ try{
63
+ await client.connect(transport);
64
+ const listed=await client.listTools();
65
+ assert.equal(listed.tools.length,43,'candidate expected compact 43-tool surface');
66
+ return snapshotToolsListResult(listed);
67
+ } finally {
68
+ await client.close().catch(()=>{});
69
+ for(const p of authorityFiles) assert.equal(await absent(p),true,'authority file appeared: '+p);
70
+ await f.cleanup();
71
+ }
72
+ }
73
+
74
+ test('R59 compact client-visible tool names and schemas match sealed 0.15.35 baseline',async()=>{
75
+ const raw=JSON.parse(await readFile(fixturePath,'utf8'));
76
+ assert.equal(raw.schema,'deadbyte.tool-surface-baseline.v1');
77
+ assert.equal(raw.release_id,'v0.15.35-1b5715b276facbc0');
78
+ assert.match(raw.surface_sha256,/^[0-9a-f]{64}$/);
79
+ assert.equal(raw.tools.length,43);
80
+ const baseline={schema:'deadbyte.tool-surface.v1',tools:raw.tools,surface_sha256:raw.surface_sha256};
81
+ const current=await captureCurrentSurface();
82
+ const diff=diffToolSurface(baseline,current);
83
+ assert.deepEqual(diff.added,[]);
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);
88
+ console.log(JSON.stringify({baseline_release:raw.release_id,surface_sha256:current.surface_sha256,
89
+ unchanged:diff.unchanged.length,added:diff.added,removed:diff.removed,schema_changed:diff.schema_changed}));
90
+ });
@@ -0,0 +1,36 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { authorityVector, parseAuthorityObservation, assertAuthorityVector, assertRuntimeReadyObservation } from '../src/remote-authority.mjs';
4
+
5
+ const observed = (armed={}) => ({
6
+ schema:'deadbyte.controller-observation.v1',
7
+ authority:Object.fromEntries(['process','coding','autonomous','desktop','host'].map(name=>[
8
+ name,{configured:true,armed:Boolean(armed[name]),reason:armed[name]?'standing-grant':'disarmed'}
9
+ ]))
10
+ });
11
+
12
+ test('remote modes have exact non-widening authority vectors',()=>{
13
+ assert.deepEqual(authorityVector('default'),{process:true,coding:true,autonomous:true,desktop:false,host:false});
14
+ assert.deepEqual(authorityVector('desktop'),{process:true,coding:true,autonomous:true,desktop:true,host:false});
15
+ assert.deepEqual(authorityVector('host'),{process:true,coding:true,autonomous:true,desktop:false,host:true});
16
+ assert.deepEqual(authorityVector('full'),{process:true,coding:true,autonomous:true,desktop:true,host:true});
17
+ });
18
+
19
+ test('observed standing Host grant rejects bounded READY',()=>{
20
+ const snapshot=parseAuthorityObservation(JSON.stringify(observed({process:true,coding:true,autonomous:true,host:true})));
21
+ assert.throws(()=>assertAuthorityVector(snapshot,authorityVector('default')),/host.*expected DISARMED/);
22
+ });
23
+
24
+ test('malformed or incomplete controller observation fails closed',()=>{
25
+ assert.throws(()=>parseAuthorityObservation('{}'),/observation/);
26
+ assert.throws(()=>parseAuthorityObservation(JSON.stringify({schema:'deadbyte.controller-observation.v1',authority:{process:{armed:true}}})),/authority/);
27
+ });
28
+
29
+ test('readiness requires verified Bridge Core Cloud and local MCP, not just ONLINE text',()=>{
30
+ const value={...observed({process:true,coding:true,autonomous:true}),state:'ONLINE',local_ready:true,
31
+ bridge:{alive:true,reason:'verified'},core:{alive:true,reason:'verified'},cloud:{alive:true,reason:'verified'},
32
+ supervisor:{running:true},core_root:'D:\\exact-release'};
33
+ assertRuntimeReadyObservation(value,{coreRoot:'D:\\exact-release'});
34
+ assert.throws(()=>assertRuntimeReadyObservation({...value,core:{alive:false,reason:'wrong-core-generation'}}),/core/);
35
+ assert.throws(()=>assertRuntimeReadyObservation({...value,core_root:'D:\\wrong'} ,{coreRoot:'D:\\exact-release'}),/core root/);
36
+ });
@@ -0,0 +1,28 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { probeRemoteEnforcement } from '../src/remote-enforcement-probe-r60.mjs';
4
+
5
+ function client({hostArmed=false,desktopArmed=false}={}){
6
+ return {callTool:async ({name})=>name==='desktop_control'
7
+ ? {isError:false,structuredContent:{status:'ok',op:'status',result:{armed:desktopArmed}}}
8
+ : hostArmed
9
+ ? {isError:false,structuredContent:{status:'ok',content:'{}',sha256:'a'.repeat(64)}}
10
+ : {isError:true,structuredContent:{status:'denied'}}};
11
+ }
12
+
13
+ test('default remote verifies Host denial and Desktop disarm through actual MCP tools',async()=>{
14
+ await probeRemoteEnforcement({client:client(),mode:'default'});
15
+ await assert.rejects(probeRemoteEnforcement({client:client({hostArmed:true}),mode:'default'}),/Host.*enforcement/);
16
+ await assert.rejects(probeRemoteEnforcement({client:client({desktopArmed:true}),mode:'default'}),/Desktop.*enforcement/);
17
+ });
18
+
19
+ test('full remote requires both Host read and Desktop armed state',async()=>{
20
+ await probeRemoteEnforcement({client:client({hostArmed:true,desktopArmed:true}),mode:'full'});
21
+ await assert.rejects(probeRemoteEnforcement({client:client({hostArmed:false,desktopArmed:true}),mode:'full'}),/Host.*enforcement/);
22
+ });
23
+
24
+ test('post-session revoked probe independently rejects lingering Host and Desktop grants',async()=>{
25
+ await probeRemoteEnforcement({client:client(),mode:'revoked'});
26
+ await assert.rejects(probeRemoteEnforcement({client:client({hostArmed:true}),mode:'revoked'}),/Host.*enforcement/);
27
+ await assert.rejects(probeRemoteEnforcement({client:client({desktopArmed:true}),mode:'revoked'}),/Desktop.*enforcement/);
28
+ });
@@ -0,0 +1,119 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {mkdtemp,rm} from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import {openRemoteCallClaims} from '../bridge/src/remote-call-claims-r60.mjs';
7
+ import {dispatchBridgeCall,classifyRemoteOperation} from '../bridge/src/remote-dispatch-r60.mjs';
8
+ import {createCapabilitySnapshotTracker} from '../src/capability-snapshot.mjs';
9
+
10
+ const base={device_id:'device-1',session_id:'stateless',request_id:'call-1',tool:'coding_file_write',
11
+ arguments:{root_id:'fixture',path:'a.txt',content:'A'},expires_at_utc:'2030-01-01T00:00:00.000Z',
12
+ release_id:'v0.15.37-test',generation_manifest_sha256:'a'.repeat(64),machine_id:'d'.repeat(64),online_epoch:'c'.repeat(64),classification:'mutating'};
13
+
14
+ async function fixture(run){
15
+ const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r60-bridge-dispatch-'));
16
+ try{await run(openRemoteCallClaims({root,clock:()=>Date.parse('2029-01-01T00:00:00.000Z')}));}
17
+ finally{await rm(root,{recursive:true,force:true});}
18
+ }
19
+
20
+ test('remote operation classifier marks only pinned read-only forms safe for recovery',()=>{
21
+ assert.equal(classifyRemoteOperation({name:'machine_status'},{}),'read_only');
22
+ assert.equal(classifyRemoteOperation({name:'machine_fs'},{op:'read',root_id:'fixture',path:'a.txt'}),'read_only');
23
+ assert.equal(classifyRemoteOperation({name:'machine_fs'},{op:'write',root_id:'fixture',path:'a.txt'}),'mutating');
24
+ assert.equal(classifyRemoteOperation({name:'coding_file_write'},{}),'mutating');
25
+ assert.equal(classifyRemoteOperation({name:'unknown_future_tool'},{}),'unknown');
26
+ });
27
+
28
+ test('capability snapshot describes the Bridge idempotency boundary without adding signed capabilities',()=>{
29
+ const snapshot=createCapabilitySnapshotTracker({clock:()=>1000}).snapshot({profile:'compact',
30
+ ledger:{ledger_sha256:'a'.repeat(64),entries:[]},toolCatalog:['capability_manifest'],catalogSha256:'b'.repeat(64),
31
+ resolution:{capabilities:[]},bindings:{},planes:{}});
32
+ assert.equal(snapshot.remote_dispatch_contract.scope,'bridge_ingress_only');
33
+ assert.equal(snapshot.remote_dispatch_contract.keyed_mutation,'same stable key and canonical payload: no duplicate dispatch; unknown post-dispatch outcome is not retried');
34
+ assert.equal(snapshot.remote_dispatch_contract.keyless_mutation,'cross-reconnect logical retry safety unavailable; ambiguous outcome is not auto-replayed');
35
+ assert.deepEqual(snapshot.capabilities,[]);
36
+ });
37
+
38
+ test('Bridge keyed mutation duplicate executes the downstream side effect once and replays result',async()=>fixture(async claims=>{
39
+ let effects=0;
40
+ const binding={...base,idempotency_key:'stable-write-1'};
41
+ const invoke=async()=>{effects++;return {structuredContent:{status:'ok',receipt_sha256:'b'.repeat(64)}};};
42
+ const first=await dispatchBridgeCall({claims,binding,classification:'mutating',invoke});
43
+ const duplicate=await dispatchBridgeCall({claims,binding:{...binding,session_id:'new-session',request_id:'call-99'},classification:'mutating',invoke});
44
+ assert.deepEqual(duplicate,first);
45
+ assert.equal(effects,1);
46
+ await assert.rejects(dispatchBridgeCall({claims,binding:{...binding,arguments:{...binding.arguments,content:'B'}},classification:'mutating',invoke}),/idempotency_key_conflict/);
47
+ assert.equal(effects,1);
48
+ }));
49
+
50
+ test('Bridge keyless exact duplicate does not replay and uncertain post-dispatch mutation stays unknown',async()=>fixture(async claims=>{
51
+ let effects=0;
52
+ const first=await dispatchBridgeCall({claims,binding:base,classification:'mutating',invoke:async()=>{effects++;throw new Error('transport lost');}}).catch(error=>error);
53
+ assert.match(first.message,/unknown/i);
54
+ const second=await dispatchBridgeCall({claims,binding:base,classification:'mutating',invoke:async()=>{effects++;}}).catch(error=>error);
55
+ assert.match(second.message,/unknown/i);
56
+ assert.equal(effects,1);
57
+ }));
58
+
59
+ test('keyless JSON-RPC ids do not become cross-delivery mutation identities',async()=>fixture(async claims=>{
60
+ let effects=0;
61
+ const invoke=async()=>({structuredContent:{status:'ok',effect_number:++effects}});
62
+ const first=await dispatchBridgeCall({claims,binding:{...base,session_id:'delivery-a'},classification:'mutating',invoke});
63
+ const second=await dispatchBridgeCall({claims,binding:{...base,session_id:'delivery-b'},classification:'mutating',invoke});
64
+ assert.equal(first.structuredContent.effect_number,1);
65
+ assert.equal(second.structuredContent.effect_number,2);
66
+ assert.equal(effects,2,'legacy calls do not acquire exactly-once semantics from a reused JSON-RPC id');
67
+ }));
68
+
69
+ test('concurrent Bridge deliveries race one durable dispatch marker and one side effect',async()=>fixture(async claims=>{
70
+ let effects=0;
71
+ const binding={...base,idempotency_key:'concurrent-write-1'};
72
+ const outcomes=await Promise.allSettled(Array.from({length:12},()=>dispatchBridgeCall({claims,binding,classification:'mutating',invoke:async()=>{
73
+ effects++;
74
+ return {structuredContent:{status:'ok',receipt_sha256:'b'.repeat(64)}};
75
+ }})));
76
+ assert.equal(effects,1);
77
+ assert.ok(outcomes.some(item=>item.status==='fulfilled'));
78
+ assert.ok(outcomes.every(item=>item.status==='fulfilled'||/in flight|unknown|no replay/i.test(item.reason.message)));
79
+ }));
80
+
81
+ test('two Bridge claim-store instances sharing one root still allow only one keyed side effect',async()=>{
82
+ const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r60-two-bridges-'));
83
+ try{
84
+ const one=openRemoteCallClaims({root,clock:()=>Date.parse('2029-01-01T00:00:00.000Z')});
85
+ const two=openRemoteCallClaims({root,clock:()=>Date.parse('2029-01-01T00:00:00.000Z')});
86
+ const binding={...base,idempotency_key:'two-bridge-race'};
87
+ let effects=0;
88
+ const invoke=async()=>{effects++;return {structuredContent:{status:'ok'}};};
89
+ const outcomes=await Promise.allSettled([
90
+ dispatchBridgeCall({claims:one,binding,classification:'mutating',invoke}),
91
+ dispatchBridgeCall({claims:two,binding:{...binding,session_id:'second',request_id:'second'},classification:'mutating',invoke})
92
+ ]);
93
+ assert.equal(effects,1);
94
+ assert.ok(outcomes.some(item=>item.status==='fulfilled'));
95
+ assert.ok(outcomes.every(item=>item.status==='fulfilled'||/in flight|unknown|no replay/i.test(item.reason.message)));
96
+ }finally{await rm(root,{recursive:true,force:true});}
97
+ });
98
+
99
+ test('a keyed result is replayable after the reply is lost, without invoking Core again',async()=>fixture(async claims=>{
100
+ const binding={...base,idempotency_key:'lost-reply'};
101
+ let effects=0;
102
+ await dispatchBridgeCall({claims,binding,classification:'mutating',invoke:async()=>({structuredContent:{status:'ok',ordinal:++effects}})});
103
+ const replay=await dispatchBridgeCall({claims,binding:{...binding,session_id:'reattached',request_id:'new-rpc-id'},classification:'mutating',
104
+ invoke:async()=>{throw new Error('must not reach Core');}});
105
+ assert.equal(replay.structuredContent.ordinal,1);
106
+ assert.equal(effects,1);
107
+ }));
108
+
109
+ test('read-only post-crash claim can re-execute after recovery without mutating retry',async()=>fixture(async claims=>{
110
+ const binding={...base,tool:'machine_status',classification:'read_only',arguments:{},idempotency_key:'read-status-1'};
111
+ const first=await claims.claim(binding);
112
+ await claims.dispatch(first.claim_id);
113
+ await claims.recover({limit:100});
114
+ let calls=0;
115
+ const result=await dispatchBridgeCall({claims,binding,classification:'read_only',invoke:async()=>{calls++;return {structuredContent:{status:'ok'}};}});
116
+ assert.equal(result.structuredContent.status,'ok');
117
+ assert.equal(calls,1);
118
+ assert.equal((await claims.state(first.claim_id)).state,'TERMINAL');
119
+ }));
@@ -0,0 +1,15 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createBridgeHealth } from '../bridge/src/bridge-health-r59.mjs';
4
+
5
+ test('backward wall-clock jump cannot extend capability freshness',()=>{
6
+ let wall=100000,elapsed=0;
7
+ const health=createBridgeHealth({clock:()=>wall,elapsedClock:()=>elapsed,maxCapabilityAgeMs:15000});
8
+ health.coreConnected({childIdentity:'core',protocolEra:'modern',catalogSha256:'a'.repeat(64)});
9
+ health.capabilityObserved({schema:'deadbyte.capability-manifest.v3',snapshot_id:'b'.repeat(64),state_epoch:1,
10
+ observed_at_utc:new Date(wall).toISOString(),valid_until_utc:new Date(wall+60000).toISOString(),planes:{}});
11
+ assert.equal(health.snapshot().state,'READY');
12
+ wall-=60000;
13
+ elapsed+=16000;
14
+ assert.equal(health.snapshot().state,'DEGRADED');
15
+ });
@@ -0,0 +1,29 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createLiveTimelineTracker } from '../src/remote-live-log.mjs';
4
+
5
+ test('capability polling prints initial/changed identity and suppresses only unchanged observations',()=>{
6
+ let now=1000;
7
+ const tracker=createLiveTimelineTracker({clock:()=>now,heartbeatBurstLimit:3,heartbeatWindowMs:3000});
8
+ const lines=[];
9
+ for(let i=0;i<3;i++){
10
+ const request_id=`cap-${i}`;
11
+ lines.push(tracker.observe({event_type:'tool.request',tool:'capability_manifest',request_id,args:{}},{sequence:i*2+1}));
12
+ now+=10;
13
+ lines.push(tracker.observe({event_type:'tool.result',tool:'capability_manifest',request_id,outcome:{is_error:false,duration_ms:2,
14
+ snapshot_id:i===2?'b'.repeat(64):'a'.repeat(64),state_epoch:i===2?2:1}},{sequence:i*2+2}));
15
+ }
16
+ assert.match(lines[1],/capability_manifest.*epoch=1/);
17
+ assert.equal(lines[3],null);
18
+ assert.match(lines[5],/capability_manifest.*epoch=2/);
19
+ assert.match(tracker.reconcileInterrupted().join('\n'),/capability_manifest ×1 unchanged suppressed/);
20
+ assert.match(tracker.summary(),/3 calls \| 3 ok/);
21
+ });
22
+
23
+ test('machine status and ping remain visible in R60 human timeline',()=>{
24
+ const tracker=createLiveTimelineTracker({clock:()=>1000});
25
+ for(const tool of ['machine_status','machine_ping']){
26
+ tracker.observe({event_type:'tool.request',tool,request_id:tool,args:{}},{});
27
+ assert.match(tracker.observe({event_type:'tool.result',tool,request_id:tool,outcome:{is_error:false,duration_ms:1}},{}),new RegExp(tool));
28
+ }
29
+ });
@@ -0,0 +1,21 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { classifyReconnectFailure } from '../bridge/src/reconnect-delay-r60.mjs';
4
+
5
+ test('identity, catalog and protocol drift terminate reconnect budget immediately',()=>{
6
+ for(const message of ['core script identity drift','replacement core catalog drift','replacement core protocol is not modern'])
7
+ assert.equal(classifyReconnectFailure(new Error(message)),'TERMINAL');
8
+ assert.equal(classifyReconnectFailure(Object.assign(new Error('connection reset'),{code:'ECONNRESET'})),'TRANSIENT');
9
+ assert.equal(classifyReconnectFailure(Object.assign(new Error('permission denied'),{code:'EACCES'})),'TERMINAL');
10
+ });
11
+
12
+ test('typed terminal and transient failures are not inferred from incidental message text',()=>{
13
+ assert.equal(classifyReconnectFailure(Object.assign(new Error('connection reset'),{kind:'terminal',code:'DEADBYTE_GENERATION_MISMATCH'})),'TERMINAL');
14
+ assert.equal(classifyReconnectFailure(Object.assign(new Error('generation mismatch'),{kind:'transient',code:'ECONNRESET'})),'TRANSIENT');
15
+ for(const code of ['DEADBYTE_AUTHORITY_REVOKED','DEADBYTE_POLICY_DENIED','DEADBYTE_SCHEMA_INVALID',
16
+ 'DEADBYTE_RELEASE_MISMATCH','DEADBYTE_GENERATION_MISMATCH','DEADBYTE_IDEMPOTENCY_CONFLICT',
17
+ 'DEADBYTE_CLAIM_CORRUPT','DEADBYTE_CREDENTIAL_EXPIRED'])
18
+ assert.equal(classifyReconnectFailure(Object.assign(new Error('arbitrary'),{code})),'TERMINAL');
19
+ for(const code of ['ECONNRESET','ECONNREFUSED','EPIPE','ETIMEDOUT','EHOSTUNREACH'])
20
+ assert.equal(classifyReconnectFailure(Object.assign(new Error('arbitrary'),{code})),'TRANSIENT');
21
+ });
@@ -0,0 +1,18 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { reconnectDelayMs, waitForReconnect } from '../bridge/src/reconnect-delay-r60.mjs';
4
+
5
+ test('bounded reconnect delays jitter each attempt without exceeding the restart budget',()=>{
6
+ const low=[0,1,2,3,4].map(index=>reconnectDelayMs(index,()=>0));
7
+ const high=[0,1,2,3,4].map(index=>reconnectDelayMs(index,()=>1));
8
+ assert.deepEqual(low,[188,375,750,1500,3000]);
9
+ assert.deepEqual(high,[313,625,1250,2500,5000]);
10
+ assert.throws(()=>reconnectDelayMs(5),/attempt/);
11
+ });
12
+
13
+ test('shutdown aborts a scheduled reconnect before another child can start',async()=>{
14
+ const controller=new AbortController();
15
+ const pending=waitForReconnect(5000,controller.signal);
16
+ controller.abort();
17
+ assert.equal(await pending,false);
18
+ });
@@ -0,0 +1,81 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {readFile} from 'node:fs/promises';
4
+
5
+ const read=rel=>readFile(new URL('../'+rel,import.meta.url),'utf8');
6
+ const json=rel=>read(rel).then(raw=>JSON.parse(raw.replace(/^\uFEFF/,'')));
7
+
8
+ const PARENT=Object.freeze({
9
+ version:'0.15.36',
10
+ release_id:'v0.15.36-090d9db3e0679c0d',
11
+ manifest_sha256:'090d9db3e0679c0d4943f3229ecab7b9fcccbe4f767348a568c9f3bb60d1b431',
12
+ generation_manifest_sha256:'6134f9f26b680e13ed545da4ab5f3020f411ea2569062fb9c154c8a7e036c9dc'
13
+ });
14
+
15
+ test('R60 identity is coherent and binds the exact production-accepted 0.15.36 predecessor',async()=>{
16
+ const [pkg,lock,release,prov,matrix,sbom,version,bridgePkg,contract]=await Promise.all([
17
+ json('package.json'),json('package-lock.json'),json('RELEASE.json'),json('PROVENANCE.json'),
18
+ json('TEST-MATRIX.json'),json('SBOM.cdx.json'),read('src/version.mjs'),json('bridge/package.json'),
19
+ read('docs/R60-TASK-CONTRACT.md')
20
+ ]);
21
+ assert.equal(pkg.version,'0.15.37');
22
+ assert.equal(lock.version,pkg.version);
23
+ assert.equal(lock.packages[''].version,pkg.version);
24
+ assert.match(version,/DEADBYTE_VERSION = '0\.15\.37'/);
25
+ assert.equal(bridgePkg.version,pkg.version);
26
+ assert.equal(release.version,pkg.version);
27
+ assert.equal(release.train,'R60');
28
+ assert.equal(release.label,'REMOTE TRUTH + RESILIENCE HARDENING');
29
+ assert.equal(prov.version,pkg.version);
30
+ assert.equal(matrix.version,pkg.version);
31
+ assert.equal(matrix.train,'R60');
32
+ assert.equal(sbom.metadata.component.version,pkg.version);
33
+ assert.equal(sbom.serialNumber,'urn:cdx:deadbyte-mcp:r60:0.15.37');
34
+ assert.equal(release.parent.version,PARENT.version);
35
+ assert.equal(release.parent.release_id,PARENT.release_id);
36
+ assert.equal(release.parent.manifest_sha256,PARENT.manifest_sha256);
37
+ assert.equal(release.parent.generation_manifest_sha256,PARENT.generation_manifest_sha256);
38
+ assert.equal(release.runtime_predecessor.release_id,PARENT.release_id);
39
+ assert.equal(release.runtime_predecessor.manifest_sha256,PARENT.manifest_sha256);
40
+ assert.equal(release.runtime_predecessor.generation_manifest_sha256,PARENT.generation_manifest_sha256);
41
+ assert.equal(prov.runtime_predecessor.release_id,PARENT.release_id);
42
+ assert.equal(prov.runtime_predecessor.manifest_sha256,PARENT.manifest_sha256);
43
+ assert.equal(prov.runtime_predecessor.generation_manifest_sha256,PARENT.generation_manifest_sha256);
44
+ assert.match(contract,/TASK_ID: deadbyte-r60-remote-truth-resilience/);
45
+ assert.match(contract,/0\.15\.36/);
46
+ assert.match(contract,/0\.15\.37/);
47
+ });
48
+
49
+ test('R60 release contract exposes the proven remote truth semantics without widening dependencies',async()=>{
50
+ const [pkg,release,matrix]=await Promise.all([json('package.json'),json('RELEASE.json'),json('TEST-MATRIX.json')]);
51
+ assert.equal(Object.keys(pkg.dependencies??{}).length,4);
52
+ assert.equal(release.r60_remote_hardening.authority_truth_coherence,true);
53
+ assert.equal(release.r60_remote_hardening.execution_backed_readiness,true);
54
+ assert.equal(release.r60_remote_hardening.keyed_mutation_dedup,true);
55
+ assert.equal(release.r60_remote_hardening.keyless_ambiguous_mutation_fail_closed,true);
56
+ assert.equal(release.r60_remote_hardening.read_only_recovery,true);
57
+ assert.equal(release.r60_remote_hardening.bounded_reconnect_jitter,true);
58
+ assert.equal(release.r60_remote_hardening.terminal_transient_failure_taxonomy,true);
59
+ assert.equal(release.r60_remote_hardening.capability_manifest_human_suppression,true);
60
+ assert.equal(release.r60_remote_hardening.windows_log_bom_decode,true);
61
+ assert.equal(release.r60_remote_hardening.public_surface_tool_count,43);
62
+ assert.equal(release.r60_remote_hardening.stable_final_claim,false);
63
+ assert.equal(matrix.observed_pre_release_evidence.r60_behavioral_regression.status,'PASS');
64
+ assert.equal(matrix.observed_pre_release_evidence.r60_behavioral_regression.tests,1241);
65
+ assert.equal(matrix.observed_pre_release_evidence.r60_behavioral_regression.passed,1210);
66
+ assert.equal(matrix.observed_pre_release_evidence.r60_behavioral_regression.skipped,31);
67
+ assert.equal(matrix.observed_pre_release_evidence.r60_behavioral_regression.failed,0);
68
+ assert.equal(matrix.observed_pre_release_evidence.r60_windows_gate.status,'PASS_STAGED_WINDOWS_DOCTOR_WARN');
69
+ assert.equal(matrix.observed_pre_release_evidence.r60_package.status,'PASS_VERIFY_ONLY_PRE_FINAL_SEAL');
70
+ });
71
+
72
+ test('R60 package scripts bind the new focused, full-regression and Windows-gate entrypoints',async()=>{
73
+ const [pkg,gate]=await Promise.all([json('package.json'),read('scripts/gate-windows-r60-prepublish.ps1')]);
74
+ assert.match(pkg.scripts['r60:focused:test'],/r60-authority-coherence\.test\.mjs/);
75
+ assert.match(pkg.scripts['r60:focused:test'],/r60-remote-call-recovery\.test\.mjs/);
76
+ assert.equal(pkg.scripts['r60:full:test'],'node scripts/full-regression-r60.mjs');
77
+ assert.equal(pkg.scripts['gate:windows'],'powershell -NoProfile -ExecutionPolicy Bypass -File scripts/gate-windows-r60-prepublish.ps1');
78
+ assert.match(gate,/ValidateSet\('all','preflight','focused','full-batch','manifest','doctor'\)/);
79
+ assert.match(gate,/R60 PREPUBLISH WINDOWS GATE STAGE: PASS/);
80
+ assert.match(gate,/--batch=\$Batch/);
81
+ });
@@ -0,0 +1,170 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, rm, writeFile, readFile } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { openRemoteCallClaims } from '../bridge/src/remote-call-claims-r60.mjs';
7
+ import {canonicalJson as bridgeCanonicalJson} from '../bridge/src/canonical-json-r60.mjs';
8
+ import {canonicalJson as coreCanonicalJson} from '../src/canonical-json.mjs';
9
+
10
+ const binding={device_id:'device-1',session_id:'session-1',request_id:'call-1',tool:'coding_file_write',
11
+ arguments:{root_id:'fixture',path:'a.txt',content:'A'},expires_at_utc:'2030-01-01T00:00:00.000Z',
12
+ release_id:'v0.15.37-test',generation_manifest_sha256:'a'.repeat(64),machine_id:'d'.repeat(64),online_epoch:'c'.repeat(64),classification:'mutating'};
13
+
14
+ async function withStore(fn){
15
+ const dir=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r60-claims-'));
16
+ try{await fn(openRemoteCallClaims({root:dir,clock:()=>Date.parse('2029-01-01T00:00:00.000Z')}));}
17
+ finally{await rm(dir,{recursive:true,force:true});}
18
+ }
19
+
20
+ test('Bridge and Core canonical argument encoding remain byte-identical',()=>{
21
+ const value={z:[{b:2,a:1}],a:{thai:'ทดสอบ',x:null}};
22
+ assert.equal(bridgeCanonicalJson(value),coreCanonicalJson(value));
23
+ });
24
+
25
+ test('claim refuses missing machine/epoch/classification identity before execution',async()=>withStore(async store=>{
26
+ await assert.rejects(store.claim({...binding,machine_id:undefined}),/machine_id/);
27
+ await assert.rejects(store.claim({...binding,online_epoch:undefined}),/online epoch/);
28
+ await assert.rejects(store.claim({...binding,classification:undefined}),/classification/);
29
+ }));
30
+
31
+ test('duplicate request identity with same canonical payload never receives a second execution claim',async()=>withStore(async store=>{
32
+ const first=await store.claim(binding);
33
+ assert.equal(first.owner,true);
34
+ const second=await store.claim({...binding,arguments:{content:'A',path:'a.txt',root_id:'fixture'}});
35
+ assert.equal(second.owner,false);
36
+ assert.equal(second.state,'CLAIMED');
37
+ await store.dispatch(first.claim_id);
38
+ const uncertain=await store.claim(binding);
39
+ assert.equal(uncertain.owner,false);
40
+ assert.equal(uncertain.state,'EXECUTING');
41
+ assert.equal(uncertain.side_effect_state,'may_have_started');
42
+ }));
43
+
44
+ test('changed payload and expired request fail closed',async()=>withStore(async store=>{
45
+ await store.claim(binding);
46
+ await assert.rejects(store.claim({...binding,arguments:{...binding.arguments,content:'B'}}),/request_id_conflict/);
47
+ await assert.rejects(store.claim({...binding,request_id:'expired',expires_at_utc:'2020-01-01T00:00:00.000Z'}),/expired/);
48
+ }));
49
+
50
+ test('same JSON-RPC request id in a distinct session is not a duplicate',async()=>withStore(async store=>{
51
+ const first=await store.claim(binding);
52
+ const second=await store.claim({...binding,session_id:'session-2'});
53
+ assert.equal(first.owner,true);
54
+ assert.equal(second.owner,true);
55
+ assert.notEqual(first.claim_id,second.claim_id);
56
+ }));
57
+
58
+ test('an already claimed call cannot acquire a fresh deadline after its original expiry',async()=>{
59
+ const dir=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r60-expiry-'));
60
+ let now=Date.parse('2029-01-01T00:00:00.000Z');
61
+ try{
62
+ const store=openRemoteCallClaims({root:dir,clock:()=>now});
63
+ await store.claim({...binding,expires_at_utc:'2029-01-01T00:00:01.000Z'});
64
+ now+=2000;
65
+ await assert.rejects(store.claim({...binding,expires_at_utc:'2029-01-02T00:00:00.000Z'}),/expired/);
66
+ }finally{await rm(dir,{recursive:true,force:true});}
67
+ });
68
+
69
+ test('stable idempotency key survives transport session and JSON-RPC id changes',async()=>withStore(async store=>{
70
+ const first=await store.claim({...binding,idempotency_key:'goal-write-1'});
71
+ await store.dispatch(first.claim_id);
72
+ await store.terminal(first.claim_id,{status:'ok',receipt_sha256:'b'.repeat(64)});
73
+ const replay=await store.claim({...binding,idempotency_key:'goal-write-1',session_id:'session-2',request_id:'call-99'});
74
+ assert.equal(replay.owner,false);
75
+ assert.equal(replay.state,'TERMINAL');
76
+ assert.equal(replay.claim_id,first.claim_id);
77
+ assert.deepEqual(replay.result,{status:'ok',receipt_sha256:'b'.repeat(64)});
78
+ await assert.rejects(store.claim({...binding,idempotency_key:'goal-write-1',session_id:'session-3',request_id:'call-100',arguments:{...binding.arguments,content:'B'}}),/idempotency_key_conflict/);
79
+ }));
80
+
81
+ test('same JSON-RPC id with a different stable key is a distinct logical mutation',async()=>withStore(async store=>{
82
+ const first=await store.claim({...binding,idempotency_key:'goal-write-1'});
83
+ const second=await store.claim({...binding,idempotency_key:'goal-write-2'});
84
+ assert.equal(first.owner,true);
85
+ assert.equal(second.owner,true);
86
+ assert.notEqual(first.claim_id,second.claim_id);
87
+ }));
88
+
89
+ test('concurrent duplicate claims have one owner and terminal result is replayable without dispatch',async()=>withStore(async store=>{
90
+ const claims=await Promise.all(Array.from({length:12},()=>store.claim(binding)));
91
+ assert.equal(claims.filter(item=>item.owner).length,1);
92
+ const id=claims[0].claim_id;
93
+ await store.dispatch(id);
94
+ await assert.rejects(store.dispatch(id),/already dispatched/);
95
+ await store.terminal(id,{status:'ok',receipt_sha256:'b'.repeat(64)});
96
+ const replay=await store.claim(binding);
97
+ assert.equal(replay.owner,false);
98
+ assert.equal(replay.state,'TERMINAL');
99
+ assert.deepEqual(replay.result,{status:'ok',receipt_sha256:'b'.repeat(64)});
100
+ }));
101
+
102
+ test('corrupted terminal bytes fail closed instead of replaying altered success',async()=>{
103
+ const dir=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r60-corrupt-'));
104
+ try{
105
+ const store=openRemoteCallClaims({root:dir,clock:()=>Date.parse('2029-01-01T00:00:00.000Z')});
106
+ const first=await store.claim(binding);
107
+ await store.dispatch(first.claim_id);
108
+ await store.terminal(first.claim_id,{status:'ok',receipt_sha256:'b'.repeat(64)});
109
+ const file=path.join(dir,first.claim_id.slice(0,2),`${first.claim_id}.terminal.json`);
110
+ const original=await readFile(file,'utf8');
111
+ await writeFile(file,original.replace('"status":"ok"','"status":"forged"'));
112
+ await assert.rejects(store.claim(binding),/terminal.*hash|corrupt/i);
113
+ }finally{await rm(dir,{recursive:true,force:true});}
114
+ });
115
+
116
+ test('corrupted required claim record fails closed before any duplicate dispatch',async()=>{
117
+ const dir=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r60-corrupt-'));
118
+ try{
119
+ const store=openRemoteCallClaims({root:dir,clock:()=>Date.parse('2029-01-01T00:00:00.000Z')});
120
+ const first=await store.claim(binding);
121
+ const file=path.join(dir,first.claim_id.slice(0,2),`${first.claim_id}.claim.json`);
122
+ const original=await readFile(file,'utf8');
123
+ await writeFile(file,original.replace('"tool":"coding_file_write"','"tool":"host_exec"'));
124
+ await assert.rejects(store.claim(binding),/claim.*hash|corrupt/i);
125
+ }finally{await rm(dir,{recursive:true,force:true});}
126
+ });
127
+
128
+ test('startup recovery marks only post-dispatch mutations interrupted and leaves pre-effect claims recoverable',async()=>withStore(async store=>{
129
+ const pending=await store.claim({...binding,request_id:'pre-effect'});
130
+ const uncertain=await store.claim({...binding,request_id:'post-dispatch'});
131
+ await store.dispatch(uncertain.claim_id);
132
+ const batch=await store.recover({limit:100});
133
+ assert.equal(batch.records.length,2);
134
+ assert.equal((await store.state(pending.claim_id)).state,'CLAIMED');
135
+ const after=await store.state(uncertain.claim_id);
136
+ assert.equal(after.state,'INTERRUPTED');
137
+ assert.equal(after.side_effect_state,'unknown');
138
+ }));
139
+
140
+
141
+ test('recovery inventory excludes terminal and expired claims while retaining eligible pre-effect work',async()=>{
142
+ const dir=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r60-recovery-filter-'));
143
+ let now=Date.parse('2029-01-01T00:00:00.000Z');
144
+ try{
145
+ const store=openRemoteCallClaims({root:dir,clock:()=>now});
146
+ const terminal=await store.claim({...binding,request_id:'terminal-history',expires_at_utc:'2030-01-01T00:00:00.000Z'});
147
+ await store.dispatch(terminal.claim_id);
148
+ await store.terminal(terminal.claim_id,{status:'ok',receipt_sha256:'b'.repeat(64)});
149
+ const expired=await store.claim({...binding,request_id:'expired-history',expires_at_utc:'2029-01-01T00:00:01.000Z'});
150
+ const pending=await store.claim({...binding,request_id:'eligible-pending',expires_at_utc:'2030-01-01T00:00:00.000Z'});
151
+ now+=2000;
152
+ const batch=await store.recover({limit:100});
153
+ const ids=new Set(batch.records.map(item=>item.claim_id));
154
+ assert.equal(ids.has(terminal.claim_id),false,'terminal history is not recovery work');
155
+ assert.equal(ids.has(expired.claim_id),false,'expired work must not consume recovery budget');
156
+ assert.equal(ids.has(pending.claim_id),true,'eligible pre-effect claim must remain recoverable');
157
+ }finally{await rm(dir,{recursive:true,force:true});}
158
+ });
159
+
160
+
161
+ test('stable idempotency key cannot replay execution evidence across machine online epoch rotation',async()=>withStore(async store=>{
162
+ const keyed={...binding,idempotency_key:'epoch-bound-write'};
163
+ const first=await store.claim(keyed);
164
+ await store.dispatch(first.claim_id);
165
+ await store.terminal(first.claim_id,{status:'ok',receipt_sha256:'b'.repeat(64)});
166
+ await assert.rejects(
167
+ store.claim({...keyed,session_id:'session-after-revoke',request_id:'retry-after-revoke',online_epoch:'e'.repeat(64)}),
168
+ /epoch|identity drift/i
169
+ );
170
+ }));