deadbyte-mcp 0.9.0 → 0.11.0

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 (136) hide show
  1. package/MANIFEST.SHA256 +134 -92
  2. package/README.txt +19 -15
  3. package/bench/fixtures/corpus.json +32 -13
  4. package/bench/fixtures/r36/case-map.json +8 -0
  5. package/bin/WORKER-REGISTRY.txt +3 -3
  6. package/bin/appcontainer-stage.exe +0 -0
  7. package/bin/appcontainer-stage.obj +0 -0
  8. package/bin/bootstrap-advapi32.exe +0 -0
  9. package/bin/bootstrap-advapi32.obj +0 -0
  10. package/bin/bootstrap-exitcode.exe +0 -0
  11. package/bin/bootstrap-exitcode.obj +0 -0
  12. package/bin/bootstrap-kernel32.exe +0 -0
  13. package/bin/bootstrap-kernel32.obj +0 -0
  14. package/bin/child-control-probe.exe +0 -0
  15. package/bin/child-control-probe.obj +0 -0
  16. package/bin/child-control-stage.exe +0 -0
  17. package/bin/child-control-stage.obj +0 -0
  18. package/bin/contained-reverse-worker.exe +0 -0
  19. package/bin/contained-reverse-worker.obj +0 -0
  20. package/bin/contained-transform-worker.exe +0 -0
  21. package/bin/contained-transform-worker.obj +0 -0
  22. package/bin/containment-probe.exe +0 -0
  23. package/bin/containment-probe.obj +0 -0
  24. package/bin/deadbyte-contain.exe +0 -0
  25. package/bin/deadbyte-contain.obj +0 -0
  26. package/bin/deadbyte-exec.exe +0 -0
  27. package/bin/deadbyte-exec.obj +0 -0
  28. package/bin/deadbyte-process-host.exe +0 -0
  29. package/bin/deadbyte-process-host.obj +0 -0
  30. package/bin/deadbyte-tunnel-host.exe +0 -0
  31. package/bin/deadbyte-tunnel-host.obj +0 -0
  32. package/controller/README.TXT +8 -6
  33. package/controller/deadbyte-controller.ps1 +48 -2
  34. package/controller/deadbyte-process-policy.json +5 -5
  35. package/docs/ARCHITECTURE.md +1 -1
  36. package/docs/R36-CAPABILITY-EVIDENCE.md +43 -0
  37. package/docs/superpowers/plans/2026-09-18-r36-zero-loss-autonomy.md +739 -0
  38. package/docs/superpowers/specs/2026-09-18-r36-zero-loss-autonomy-design.md +640 -0
  39. package/package.json +1 -1
  40. package/proof/CONTAINMENT-BUILD.txt +6 -6
  41. package/scripts/deferred-slot-operation.mjs +53 -15
  42. package/scripts/final-closure-r36.mjs +113 -0
  43. package/scripts/final-closure-verify-r36.mjs +208 -0
  44. package/scripts/final-closure-verify.mjs +88 -39
  45. package/scripts/final-closure.mjs +63 -56
  46. package/scripts/gate-windows-r36.ps1 +42 -0
  47. package/scripts/gate-windows.ps1 +3 -3
  48. package/scripts/release-parity-r36.mjs +200 -0
  49. package/scripts/release-parity-tests.ps1 +9 -0
  50. package/scripts/release-parity.mjs +149 -139
  51. package/scripts/windows-gate-evidence-r36.mjs +52 -0
  52. package/scripts/windows-gate-evidence.mjs +5 -5
  53. package/src/agent-memory-plane.mjs +98 -0
  54. package/src/autonomous-context-engine.mjs +2 -1
  55. package/src/autonomous-mcp-tools.mjs +5 -1
  56. package/src/autonomous-output-contracts.mjs +9 -6
  57. package/src/autonomous-planner-contracts.mjs +36 -1
  58. package/src/autonomous-planner.mjs +25 -3
  59. package/src/autonomous-runtime.mjs +337 -43
  60. package/src/autonomous-timeout-recovery.mjs +27 -0
  61. package/src/capability-benchmark-runner.mjs +86 -1
  62. package/src/capability-benchmark.mjs +45 -0
  63. package/src/capability-ledger.mjs +158 -0
  64. package/src/coding-mcp-tools.mjs +31 -17
  65. package/src/coding-output-contracts.mjs +48 -10
  66. package/src/coding-plane.mjs +45 -9
  67. package/src/coding-search.mjs +130 -47
  68. package/src/compact-gateway-mcp-tools.mjs +128 -0
  69. package/src/compact-gateway-output-contracts.mjs +16 -0
  70. package/src/deadbyte-cli.mjs +62 -78
  71. package/src/effect-ledger.mjs +47 -0
  72. package/src/machine-fs-smoke-client.mjs +1 -1
  73. package/src/machine-fs.mjs +51 -8
  74. package/src/mcp-server.mjs +32 -9
  75. package/src/native-diff-v2.mjs +337 -0
  76. package/src/native-diff.mjs +50 -2
  77. package/src/observation-lease.mjs +32 -0
  78. package/src/observation-smoke-client.mjs +1 -1
  79. package/src/operator-surface.mjs +7 -1
  80. package/src/process-runtime.mjs +98 -11
  81. package/src/process-smoke-client.mjs +11 -3
  82. package/src/r36-compact-disarmed-probe.mjs +50 -0
  83. package/src/r36-full-surface-probe.mjs +56 -0
  84. package/src/remote-live-log.mjs +135 -0
  85. package/src/remote-mcp-autonomous-smoke-client.mjs +22 -12
  86. package/src/remote-mcp-coding-smoke-client.mjs +1 -1
  87. package/src/remote-mcp-r36-parity-client.mjs +175 -0
  88. package/src/remote-mcp-smoke-client.mjs +112 -56
  89. package/src/runtime-update.mjs +51 -1
  90. package/src/search-cursor.mjs +45 -0
  91. package/src/semantic-code.mjs +72 -5
  92. package/src/semantic-lsp-backend.mjs +39 -3
  93. package/src/version.mjs +1 -1
  94. package/src/worker-scheduler.mjs +53 -0
  95. package/test/autonomous-capability-r36.test.mjs +151 -0
  96. package/test/autonomous-output-contract.test.mjs +1 -1
  97. package/test/autonomous-runtime.test.mjs +2 -2
  98. package/test/capability-benchmark-r36.test.mjs +107 -0
  99. package/test/capability-benchmark-semantic-r36.test.mjs +52 -0
  100. package/test/capability-claims-r36.test.mjs +23 -0
  101. package/test/capability-ledger-r36.test.mjs +102 -0
  102. package/test/coding-catalog-consistency.test.mjs +2 -2
  103. package/test/coding-plane.test.mjs +2 -2
  104. package/test/coding-runtime.test.mjs +16 -0
  105. package/test/coding-search-r36.test.mjs +90 -0
  106. package/test/compact-catalog-r36.test.mjs +37 -0
  107. package/test/compact-gateway-r36.test.mjs +66 -0
  108. package/test/contained.test.mjs +1 -1
  109. package/test/core.test.mjs +1 -1
  110. package/test/deferred-slot-operation.test.mjs +9 -0
  111. package/test/fixtures/lsp-framed-server.mjs +2 -0
  112. package/test/helpers/autonomous-r34-fixture.mjs +5 -3
  113. package/test/multi-file-read.test.mjs +3 -0
  114. package/test/native-diff-topology-r36.test.mjs +105 -0
  115. package/test/package-boundary.test.mjs +6 -0
  116. package/test/process-longrun-r36.test.mjs +82 -0
  117. package/test/process-release-gate.test.mjs +9 -0
  118. package/test/production-docs.test.mjs +5 -5
  119. package/test/prompts-resources.test.mjs +7 -1
  120. package/test/r33-closeout-regression.test.mjs +4 -3
  121. package/test/r33-finalization.test.mjs +14 -9
  122. package/test/r36-finalization.test.mjs +104 -0
  123. package/test/r36-release-identity.test.mjs +45 -0
  124. package/test/r37-agent-fabric.test.mjs +92 -0
  125. package/test/r37-autonomous-timeout-recovery.test.mjs +60 -0
  126. package/test/r37-finalization.test.mjs +74 -0
  127. package/test/release-version.test.mjs +42 -33
  128. package/test/remote-live-log-r36.test.mjs +37 -0
  129. package/test/remote-live-log-r37.test.mjs +47 -0
  130. package/test/remote-live-log-recovery-r36.test.mjs +41 -0
  131. package/test/remote-r24-catalog.test.mjs +13 -0
  132. package/test/remote-session-cli.test.mjs +53 -66
  133. package/test/runtime-self-update.test.mjs +58 -1
  134. package/test/semantic-lsp-backend.test.mjs +9 -0
  135. package/test/semantic-r36-rename.test.mjs +82 -0
  136. package/deadbyte-mcp-0.8.9.tgz +0 -0
@@ -0,0 +1,175 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
7
+ import { R34_EXPECTED_TOOLS } from './remote-mcp-r34-parity-client.mjs';
8
+ import { loadCodingPolicy } from './coding-plane.mjs';
9
+ import { verifyMachineFsReceiptFile } from './machine-fs.mjs';
10
+ import { verifyObservationReceiptFile } from './observation-runtime.mjs';
11
+
12
+ export const R36_EXPECTED_TOOLS=Object.freeze([...new Set([
13
+ ...R34_EXPECTED_TOOLS,
14
+ 'coding_symbol_diagnostics','coding_symbol_implementations','coding_symbol_rename_preview'
15
+ ])].sort());
16
+ if(R36_EXPECTED_TOOLS.length !== 76) throw new Error(`R36 full catalog identity mismatch: ${R36_EXPECTED_TOOLS.length}`);
17
+ export const R36_COMPACT_GATEWAYS=Object.freeze(['capability_manifest','coding_inspect','machine_fs','machine_observe','process_session'].sort());
18
+ export const R36_COMPACT_TOOL_LIMIT=42;
19
+
20
+ const here=path.dirname(fileURLToPath(import.meta.url));
21
+ const repoRoot=path.resolve(here,'..');
22
+ const stateRoot=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
23
+ const verifyKeyPath=path.join(stateRoot,'trust','ed25519-public.pem');
24
+ const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
25
+ const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
26
+
27
+ function structured(result,label){
28
+ if(result.isError) throw new Error(`${label} returned error: ${JSON.stringify(result.structuredContent??result.content)}`);
29
+ if(!result.structuredContent||typeof result.structuredContent!=='object') throw new Error(`${label} missing structured output`);
30
+ return result.structuredContent;
31
+ }
32
+ function gateway(result,label){
33
+ const value=structured(result,label);
34
+ if(value.status!=='ok'||!value.result||typeof value.result!=='object') throw new Error(`${label} gateway status=${String(value.status)} error=${String(value.error??'')}`);
35
+ return value.result;
36
+ }
37
+ async function fileSha(relative){return sha256(await readFile(path.join(repoRoot,...relative.split('/'))));}
38
+ async function verifyMachineReceipt(policy,receiptId,operation){
39
+ const checked=await verifyMachineFsReceiptFile({policy,verifyKeyPath,receiptId});
40
+ if(!checked.ok||checked.signature_valid!==true||checked.operation!==operation) throw new Error(`machine-fs receipt verification failed for ${operation}`);
41
+ return checked;
42
+ }
43
+ async function verifyObservationReceipt(policy,receiptId){
44
+ const checked=await verifyObservationReceiptFile({policy,verifyKeyPath,receiptId});
45
+ if(!checked.ok||checked.signature_valid!==true) throw new Error('observation receipt verification failed');
46
+ return checked;
47
+ }
48
+ async function waitSearch(client,searchId){
49
+ for(let attempt=0;attempt<200;attempt+=1){
50
+ const status=gateway(await client.callTool({name:'machine_observe',arguments:{op:'search_status',search_id:searchId}}),'search_status');
51
+ if(['completed','cancelled','failed'].includes(status.state)) return status;
52
+ await sleep(25);
53
+ }
54
+ throw new Error('R36 compact search timeout');
55
+ }
56
+ async function waitProcessOutput(client,sessionId){
57
+ for(let attempt=0;attempt<100;attempt+=1){
58
+ const page=gateway(await client.callTool({name:'process_session',arguments:{op:'read',session_id:sessionId,offset:0,max_bytes:65536}}),'process read');
59
+ if(Buffer.from(page.data_base64??'','base64').toString('utf8').includes('DEADBYTE_ACTION_PING')) return page;
60
+ await sleep(50);
61
+ }
62
+ throw new Error('R36 compact process output timeout');
63
+ }
64
+
65
+ export async function runR36CompactParity(endpointText){
66
+ const endpoint=new URL(endpointText);
67
+ if(endpoint.protocol!=='https:') throw new Error('R36 compact parity requires HTTPS');
68
+ const pointer=JSON.parse(await readFile(path.join(stateRoot,'active-release.json'),'utf8'));
69
+ const policy=await loadCodingPolicy(path.join(pointer.generation_root,'coding-policy.json'));
70
+ const client=new Client({name:'deadbyte-r36-compact-parity',version:'0.10.0'},{versionNegotiation:{mode:{pin:'2026-07-28'}}});
71
+ const transport=new StreamableHTTPClientTransport(endpoint);
72
+ const tag=randomBytes(8).toString('hex');
73
+ const scratchRoot='parity_scratch';
74
+ const target=`.deadbyte-r36-parity-${tag}.txt`;
75
+ let processSession=null,targetPresent=false;
76
+ const machineReceipts=[],observationReceipts=[];
77
+ try{
78
+ await client.connect(transport);
79
+ const listed=await client.listTools();
80
+ const names=listed.tools.map(tool=>tool.name).sort();
81
+ if(names.length>R36_COMPACT_TOOL_LIMIT) throw new Error(`R36 compact catalog exceeds ${R36_COMPACT_TOOL_LIMIT}: ${names.length}`);
82
+ for(const name of R36_COMPACT_GATEWAYS) if(!names.includes(name)) throw new Error(`R36 compact gateway missing ${name}`);
83
+ for(const tool of listed.tools){
84
+ if(tool.outputSchema?.type!=='object'||tool.outputSchema?.additionalProperties!==false||!tool.outputSchema?.properties) throw new Error(`tool ${tool.name} missing strict output schema`);
85
+ }
86
+ const manifest=gateway(await client.callTool({name:'capability_manifest',arguments:{}}),'capability_manifest');
87
+ if(manifest.schema!=='deadbyte.capability-manifest.v1'||manifest.profile!=='compact'||!Array.isArray(manifest.capabilities)||!/^[0-9a-f]{64}$/.test(String(manifest.ledger_sha256??''))) throw new Error('R36 compact capability manifest malformed');
88
+ const required=manifest.capabilities.filter(item=>item?.production===true&&item?.host_only!==true);
89
+ for(const item of required){
90
+ if(typeof item.compact_tool!=='string'||!names.includes(item.compact_tool)) throw new Error(`compact capability route missing: ${String(item.id)}`);
91
+ }
92
+ const coding=structured(await client.callTool({name:'coding_session_status',arguments:{}}),'coding_session_status');
93
+ const autonomous=structured(await client.callTool({name:'autonomous_session_status',arguments:{}}),'autonomous_session_status');
94
+ if(coding.armed!==true||autonomous.coding_armed!==true||autonomous.autonomous_armed!==true) throw new Error('R36 compact parity requires coding and autonomous standing grants');
95
+ const tree=structured(await client.callTool({name:'coding_tree',arguments:{root_id:'core',path:'workspaces/demo',max_depth:2,max_entries:128}}),'coding_tree');
96
+ if(tree.status!=='ok'||!Array.isArray(tree.entries)) throw new Error('coding_tree malformed');
97
+ const stat=gateway(await client.callTool({name:'coding_inspect',arguments:{op:'stat',root_id:'core',path:'package.json',hash:true}}),'coding stat');
98
+ if(stat.type!=='file'||!/^[0-9a-f]{64}$/.test(String(stat.sha256??''))||!/^[0-9a-f]{64}$/.test(String(stat.observation_sha256??''))) throw new Error('coding stat identity missing');
99
+ const search=gateway(await client.callTool({name:'coding_inspect',arguments:{op:'search',root_id:'core',path:'src',pattern:'R36_CAPABILITY_LEDGER',target:'content',mode:'literal',case_sensitive:true,max_results:20,max_file_bytes:262144,max_scan_bytes:4194304,page_size:20,cursor:null}}),'coding search');
100
+ if(!Array.isArray(search.matches)||search.matches.length<1) throw new Error('coding search returned no R36 ledger match');
101
+
102
+ const buildScript='scripts/autonomous-build.ps1';
103
+ const testScript='scripts/release-parity-tests.ps1';
104
+ const build=structured(await client.callTool({name:'coding_command_run',arguments:{root_id:'core',command_id:'powershell_script',script_path:buildScript,script_sha256:await fileSha(buildScript),args:[],cwd:'.',timeout_ms:120000}},{timeout:180000,maxTotalTimeout:180000}),'coding build');
105
+ if(build.status!=='passed'||build.exit_code!==0) throw new Error('R36 coding build failed');
106
+ const buildVerify=structured(await client.callTool({name:'coding_receipt_verify',arguments:{receipt_id:build.receipt_id}}),'coding build receipt');
107
+ if(buildVerify.ok!==true||buildVerify.signature_valid!==true) throw new Error('R36 coding build receipt invalid');
108
+ const tests=structured(await client.callTool({name:'coding_command_run',arguments:{root_id:'core',command_id:'powershell_script',script_path:testScript,script_sha256:await fileSha(testScript),args:[],cwd:'.',timeout_ms:300000}},{timeout:360000,maxTotalTimeout:360000}),'coding tests');
109
+ if(tests.status!=='passed'||tests.exit_code!==0) throw new Error('R36 coding tests failed');
110
+ const testVerify=structured(await client.callTool({name:'coding_receipt_verify',arguments:{receipt_id:tests.receipt_id}}),'coding test receipt');
111
+ if(testVerify.ok!==true||testVerify.signature_valid!==true) throw new Error('R36 coding test receipt invalid');
112
+
113
+ const initial=`needle-${tag}\n`;
114
+ const write=gateway(await client.callTool({name:'machine_fs',arguments:{op:'write',root_id:scratchRoot,path:target,content:initial,encoding:'utf8',expected_sha256:'absent'}}),'machine_fs write');
115
+ targetPresent=true;machineReceipts.push(write.receipt_id);await verifyMachineReceipt(policy,write.receipt_id,'machine_fs_write');
116
+ const bounded=gateway(await client.callTool({name:'machine_fs',arguments:{op:'read',root_id:scratchRoot,path:target,offset:0,max_bytes:128,hash_file:true}}),'machine_fs read');
117
+ if(Buffer.from(bounded.data_base64,'base64').toString('utf8')!==initial||bounded.file_sha256!==write.sha256) throw new Error('R36 bounded machine read mismatch');
118
+ const edit=gateway(await client.callTool({name:'machine_fs',arguments:{op:'edit_exact',root_id:scratchRoot,path:target,expected_sha256:write.sha256,old_string:'needle',new_string:'beta',expected_replacements:1}}),'machine_fs edit');
119
+ machineReceipts.push(edit.receipt_id);await verifyMachineReceipt(policy,edit.receipt_id,'machine_fs_edit_exact');
120
+ const stale=await client.callTool({name:'machine_fs',arguments:{op:'edit_exact',root_id:scratchRoot,path:target,expected_sha256:write.sha256,old_string:'beta',new_string:'bad',expected_replacements:1}});
121
+ const staleValue=stale.structuredContent;
122
+ if(!staleValue||typeof staleValue!=='object') throw new Error('R36 stale machine-fs denial missing structured output');
123
+ if(stale.isError!==true||staleValue.status!=='denied'||!/precondition mismatch/i.test(String(staleValue.error??''))) {
124
+ throw new Error(`R36 stale machine-fs precondition was not denied: ${JSON.stringify(staleValue)}`);
125
+ }
126
+
127
+ const searchStart=gateway(await client.callTool({name:'machine_observe',arguments:{op:'search_start',root_id:scratchRoot,path:'.',pattern:tag,case_sensitive:true,max_results:20,max_files:200,max_bytes:262144,max_file_bytes:16384}}),'observe search start');
128
+ const searchStatus=await waitSearch(client,searchStart.search_id);
129
+ if(searchStatus.state!=='completed'||!searchStatus.receipt_id) throw new Error('R36 compact observation search failed');
130
+ observationReceipts.push(searchStatus.receipt_id);await verifyObservationReceipt(policy,searchStatus.receipt_id);
131
+ const page=gateway(await client.callTool({name:'machine_observe',arguments:{op:'search_page',search_id:searchStart.search_id,limit:20,cursor:null}}),'observe search page');
132
+ if(!page.results.some(item=>item.path===target)) throw new Error('R36 compact search did not return parity file');
133
+ const system=gateway(await client.callTool({name:'machine_observe',arguments:{op:'system_status'}}),'system status');
134
+ observationReceipts.push(system.receipt_id);await verifyObservationReceipt(policy,system.receipt_id);
135
+ const machineStatus=gateway(await client.callTool({name:'machine_observe',arguments:{op:'machine_status'}}),'machine status');
136
+ if(machineStatus.release_id!==pointer.release_id||machineStatus.release_manifest_sha256!==pointer.manifest_sha256) throw new Error('R36 compact machine status release mismatch');
137
+
138
+ const started=gateway(await client.callTool({name:'process_session',arguments:{op:'start',profile_id:'ping_hold',root_id:'core',cwd:'.',args:[]}}),'process start');
139
+ processSession=started.session_id??started.session?.session_id;
140
+ if(!/^[0-9a-f]{64}$/.test(String(processSession??''))) throw new Error('R36 compact process session id missing');
141
+ const action=gateway(await client.callTool({name:'process_session',arguments:{op:'action',session_id:processSession,action_id:'ping'}}),'process action');
142
+ const processRead=await waitProcessOutput(client,processSession);
143
+ const terminated=gateway(await client.callTool({name:'process_session',arguments:{op:'terminate',session_id:processSession}}),'process terminate');
144
+ const terminalState=terminated.state??terminated.session?.state;
145
+ if(action.action_id!=='ping'||terminalState!=='terminated') throw new Error('R36 compact process lifecycle failed');
146
+
147
+ const fsStat=gateway(await client.callTool({name:'machine_fs',arguments:{op:'stat',root_id:scratchRoot,path:target,hash:true}}),'machine_fs stat');
148
+ const deleted=gateway(await client.callTool({name:'machine_fs',arguments:{op:'delete',root_id:scratchRoot,path:target,expected_source_identity:fsStat.identity_sha256}}),'machine_fs delete');
149
+ machineReceipts.push(deleted.receipt_id);await verifyMachineReceipt(policy,deleted.receipt_id,'machine_fs_delete');targetPresent=false;
150
+
151
+ return {status:'passed',protocol_revision:client.getNegotiatedProtocolVersion(),profile:'compact',tool_count:names.length,tools:names,
152
+ catalog_sha256:sha256(Buffer.from(JSON.stringify(names),'utf8')),strict_output_schemas:true,ledger_sha256:manifest.ledger_sha256,
153
+ capability_count:manifest.capabilities.length,production_non_host_capabilities:required.length,capability_closure:true,
154
+ gateway_count:R36_COMPACT_GATEWAYS.length,tree_entries:tree.entries.length,coding_stat_verified:true,coding_search_result_count:search.matches.length,
155
+ machine_fs_receipts_verified:machineReceipts.length,observation_receipts_verified:observationReceipts.length,
156
+ observation_search_result_count:page.results.length,process_session_id:processSession,process_output_bytes:processRead.stream_head,
157
+ process_journal_head_sha256:terminated.journal_head_sha256??terminated.session?.journal_head_sha256,
158
+ build_receipt_id:build.receipt_id,test_receipt_id:tests.receipt_id,coding_receipts_verified:2,
159
+ execution_provider:'deadbyte_mcp',rdc_operation_count:0};
160
+ } finally {
161
+ if(processSession) await client.callTool({name:'process_session',arguments:{op:'terminate',session_id:processSession}}).catch(()=>{});
162
+ if(targetPresent){
163
+ try{const fsStat=gateway(await client.callTool({name:'machine_fs',arguments:{op:'stat',root_id:scratchRoot,path:target,hash:false}}),'cleanup stat');
164
+ if(fsStat.identity_sha256) await client.callTool({name:'machine_fs',arguments:{op:'delete',root_id:scratchRoot,path:target,expected_source_identity:fsStat.identity_sha256}}).catch(()=>{});
165
+ }catch{}
166
+ }
167
+ await client.close().catch(()=>{});
168
+ }
169
+ }
170
+
171
+ if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
172
+ const endpoint=process.argv[2];
173
+ if(!endpoint) throw new Error('usage: node src/remote-mcp-r36-parity-client.mjs https://host/mcp/path');
174
+ runR36CompactParity(endpoint).then(result=>console.log(JSON.stringify(result,null,2))).catch(error=>{console.error(error instanceof Error?error.stack:String(error));process.exitCode=1;});
175
+ }
@@ -30,7 +30,7 @@ const agentTools = [
30
30
  const codingTools = [
31
31
  'coding_benchmark_run', 'coding_command_run', 'coding_file_read', 'coding_file_write', 'coding_files_read',
32
32
  'coding_receipt_verify', 'coding_search', 'coding_session_status', 'coding_stat',
33
- 'coding_symbol_definition', 'coding_symbol_outline', 'coding_symbol_references', 'coding_symbol_replace',
33
+ 'coding_symbol_definition', 'coding_symbol_diagnostics', 'coding_symbol_implementations', 'coding_symbol_outline', 'coding_symbol_references', 'coding_symbol_rename_preview', 'coding_symbol_replace',
34
34
  'coding_task_approve', 'coding_task_cancel', 'coding_task_events', 'coding_task_run',
35
35
  'coding_task_status', 'coding_task_submit', 'coding_task_verify', 'coding_text_replace', 'coding_tree'
36
36
  ];
@@ -39,6 +39,18 @@ const autonomousTools = [
39
39
  'autonomous_goal_status', 'autonomous_goal_submit', 'autonomous_goal_verify', 'autonomous_release_gate',
40
40
  'autonomous_session_status', 'native_diff_apply', 'native_diff_preview', 'native_diff_verify'
41
41
  ];
42
+ const r36CompactTools = [
43
+ 'autonomous_goal_approve','autonomous_goal_cancel','autonomous_goal_events','autonomous_goal_run','autonomous_goal_status','autonomous_goal_submit','autonomous_goal_verify','autonomous_release_gate','autonomous_session_status',
44
+ 'capability_manifest',
45
+ 'coding_benchmark_run','coding_command_run','coding_file_read','coding_file_write','coding_inspect','coding_receipt_verify','coding_session_status','coding_symbol_replace',
46
+ 'coding_task_approve','coding_task_cancel','coding_task_events','coding_task_run','coding_task_status','coding_task_submit','coding_task_verify','coding_text_replace','coding_tree',
47
+ 'evidence_verify',
48
+ 'host_exec','host_file_read','host_file_write',
49
+ 'machine_fs','machine_list','machine_observe','machine_ping','machine_revoke','machine_status',
50
+ 'native_diff_apply','native_diff_preview','native_diff_verify',
51
+ 'process_session'
52
+ ].sort();
53
+ if (r36CompactTools.length !== 41) throw new Error(`R36 compact catalog constant drift: ${r36CompactTools.length}`);
42
54
  const allowedCatalogs = [];
43
55
  for (const machine of [false, true]) for (const host of [false, true]) for (const agent of [false, true]) {
44
56
  for (const processPlane of [false, true]) for (const machineFs of [false, true]) for (const observation of [false, true]) for (const codingMode of ['none', 'coding', 'autonomous']) {
@@ -68,13 +80,21 @@ function structured(result, label) {
68
80
  if (!result.structuredContent || typeof result.structuredContent !== 'object') throw new Error(`${label} returned no structured object`);
69
81
  return result.structuredContent;
70
82
  }
83
+ function gateway(result, label) {
84
+ const value = structured(result, label);
85
+ if (value.status !== 'ok' || !value.result || typeof value.result !== 'object') {
86
+ throw new Error(`${label} gateway status=${String(value.status)} error=${String(value.error ?? '')}`);
87
+ }
88
+ return value.result;
89
+ }
71
90
 
72
91
  try {
73
92
  await client.connect(transport);
74
93
  if (client.getProtocolEra() !== 'modern') throw new Error(`expected modern MCP era, got ${String(client.getProtocolEra())}`);
75
94
  const listed = await client.listTools();
76
95
  const names = listed.tools.map(t => t.name).sort();
77
- const catalogOk = allowedCatalogs.some(expected => JSON.stringify(names) === JSON.stringify(expected));
96
+ const compactProfile = JSON.stringify(names) === JSON.stringify(r36CompactTools);
97
+ const catalogOk = compactProfile || allowedCatalogs.some(expected => JSON.stringify(names) === JSON.stringify(expected));
78
98
  if (!catalogOk) throw new Error(`tool catalog mismatch: ${JSON.stringify(names)}`);
79
99
  for (const tool of listed.tools) {
80
100
  if (tool.outputSchema?.type !== 'object' || tool.outputSchema?.additionalProperties !== false || !tool.outputSchema?.properties) {
@@ -90,72 +110,108 @@ try {
90
110
  const codingRuntime = codingTools.every(name => names.includes(name));
91
111
  const autonomousRuntime = autonomousTools.every(name => names.includes(name));
92
112
 
93
- const opened = structured(await client.callTool({ name: 'workspace_open', arguments: { workspace_id: 'demo' } }), 'workspace_open');
94
- if (opened.status !== 'ok') throw new Error(`workspace_open status=${String(opened.status)}`);
95
-
96
- const proofOutput = `remote-proof.${randomBytes(8).toString('hex')}.txt`;
97
- const proof = structured(await client.callTool({
98
- name: 'proof_execute', arguments: { workspace_id: 'demo', input: 'input.txt', output: proofOutput }
99
- }), 'proof_execute');
100
- if (proof.status !== 'passed' || !/^[0-9a-f]{64}$/.test(proof.request_sha256)) throw new Error('remote proof request identity missing');
101
- const proofVerify = structured(await client.callTool({
102
- name: 'evidence_verify', arguments: { workspace_id: 'demo', receipt_path: proof.receipt_path }
103
- }), 'evidence_verify proof');
104
- if (proofVerify.ok !== true || proofVerify.signature_valid !== true || proofVerify.receipt_schema !== 'deadbyte.receipt.v2' ||
105
- proofVerify.request_sha256 !== proof.request_sha256) throw new Error(`remote proof verify failed: ${JSON.stringify(proofVerify)}`);
106
-
113
+ let compact = null;
114
+ let proofOutput = null;
115
+ let proof = null;
107
116
  let contained = null;
108
- if (mode === 'full') {
109
- const output = `remote-contained.${randomBytes(8).toString('hex')}.txt`;
110
- const exec = structured(await client.callTool({
111
- name: 'contained_transform',
112
- arguments: { workspace_id: 'demo', input: 'input.txt', output, operation: 'uppercase_ascii' }
113
- }), 'contained_transform');
114
- if (exec.status !== 'passed' || exec.output_sha256 !== '3e740f6fcf2c6659bc5f99e0706b09f3c127407bbabe74d05285d2e3276946c2' ||
115
- !/^[0-9a-f]{64}$/.test(exec.request_sha256)) throw new Error(`remote contained result mismatch: ${JSON.stringify(exec)}`);
116
- const verify = structured(await client.callTool({
117
- name: 'evidence_verify', arguments: { workspace_id: 'demo', receipt_path: exec.receipt_path }
118
- }), 'evidence_verify contained');
119
- if (verify.ok !== true || verify.signature_valid !== true || verify.containment_verified !== true ||
120
- verify.active_network_denial_verified !== true || verify.receipt_schema !== 'deadbyte.receipt.v2' ||
121
- verify.request_sha256 !== exec.request_sha256) throw new Error(`remote contained verification failed: ${JSON.stringify(verify)}`);
122
- contained = { output, receipt_sha256: exec.receipt_sha256, request_sha256: exec.request_sha256, output_sha256: exec.output_sha256 };
123
- }
124
117
  let agent = null;
125
- if (mode === 'full' && agentRuntime) {
126
- const submissionId = `remote-gate-${randomBytes(8).toString('hex')}`;
127
- const submitted = structured(await client.callTool({
128
- name: 'agent_task_submit', arguments: {
129
- workspace_id: 'demo', submission_id: submissionId, title: 'remote R22 gate',
130
- steps: [
131
- { step_id: 'proof', kind: 'proof_execute', input: { kind: 'workspace', path: 'input.txt' }, retry_limit: 0 },
132
- { step_id: 'contained', kind: 'contained_transform', operation: 'uppercase_ascii', input: { kind: 'step_output', step_id: 'proof' }, retry_limit: 0 }
133
- ]
118
+ if (compactProfile) {
119
+ const manifest = gateway(await client.callTool({ name: 'capability_manifest', arguments: {} }), 'capability_manifest');
120
+ if (manifest.schema !== 'deadbyte.capability-manifest.v1' || manifest.profile !== 'compact' ||
121
+ !Array.isArray(manifest.capabilities) || !/^[0-9a-f]{64}$/.test(String(manifest.ledger_sha256 ?? ''))) {
122
+ throw new Error('R36 compact capability manifest malformed');
123
+ }
124
+ const required = manifest.capabilities.filter(item => item?.production === true && item?.host_only !== true);
125
+ for (const item of required) {
126
+ if (typeof item.compact_tool !== 'string' || !names.includes(item.compact_tool)) {
127
+ throw new Error(`R36 compact capability route missing: ${String(item.id)}`);
128
+ }
129
+ }
130
+ const machine = gateway(await client.callTool({ name: 'machine_observe', arguments: { op: 'machine_status' } }), 'machine_status');
131
+ const nonce = `remote-smoke-${randomBytes(16).toString('hex')}`;
132
+ const ping = gateway(await client.callTool({ name: 'machine_observe', arguments: { op: 'machine_ping', nonce } }), 'machine_ping');
133
+ if (ping.nonce !== nonce || ping.machine_id !== machine.machine_id ||
134
+ ping.release_id !== machine.release_id || ping.release_manifest_sha256 !== machine.release_manifest_sha256) {
135
+ throw new Error('R36 compact machine ping binding mismatch');
136
+ }
137
+ compact = {
138
+ profile: 'compact',
139
+ ledger_sha256: manifest.ledger_sha256,
140
+ capability_count: manifest.capabilities.length,
141
+ production_non_host_capabilities: required.length,
142
+ capability_closure: true,
143
+ machine_id: machine.machine_id,
144
+ release_id: machine.release_id,
145
+ release_manifest_sha256: machine.release_manifest_sha256,
146
+ ping_signature_valid: ping.signature_valid === true
147
+ };
148
+ } else {
149
+ const opened = structured(await client.callTool({ name: 'workspace_open', arguments: { workspace_id: 'demo' } }), 'workspace_open');
150
+ if (opened.status !== 'ok') throw new Error(`workspace_open status=${String(opened.status)}`);
151
+
152
+ proofOutput = `remote-proof.${randomBytes(8).toString('hex')}.txt`;
153
+ proof = structured(await client.callTool({
154
+ name: 'proof_execute', arguments: { workspace_id: 'demo', input: 'input.txt', output: proofOutput }
155
+ }), 'proof_execute');
156
+ if (proof.status !== 'passed' || !/^[0-9a-f]{64}$/.test(proof.request_sha256)) throw new Error('remote proof request identity missing');
157
+ const proofVerify = structured(await client.callTool({
158
+ name: 'evidence_verify', arguments: { workspace_id: 'demo', receipt_path: proof.receipt_path }
159
+ }), 'evidence_verify proof');
160
+ if (proofVerify.ok !== true || proofVerify.signature_valid !== true || proofVerify.receipt_schema !== 'deadbyte.receipt.v2' ||
161
+ proofVerify.request_sha256 !== proof.request_sha256) throw new Error(`remote proof verify failed: ${JSON.stringify(proofVerify)}`);
162
+
163
+ if (mode === 'full') {
164
+ const output = `remote-contained.${randomBytes(8).toString('hex')}.txt`;
165
+ const exec = structured(await client.callTool({
166
+ name: 'contained_transform',
167
+ arguments: { workspace_id: 'demo', input: 'input.txt', output, operation: 'uppercase_ascii' }
168
+ }), 'contained_transform');
169
+ if (exec.status !== 'passed' || exec.output_sha256 !== '3e740f6fcf2c6659bc5f99e0706b09f3c127407bbabe74d05285d2e3276946c2' ||
170
+ !/^[0-9a-f]{64}$/.test(exec.request_sha256)) throw new Error(`remote contained result mismatch: ${JSON.stringify(exec)}`);
171
+ const verify = structured(await client.callTool({
172
+ name: 'evidence_verify', arguments: { workspace_id: 'demo', receipt_path: exec.receipt_path }
173
+ }), 'evidence_verify contained');
174
+ if (verify.ok !== true || verify.signature_valid !== true || verify.containment_verified !== true ||
175
+ verify.active_network_denial_verified !== true || verify.receipt_schema !== 'deadbyte.receipt.v2' ||
176
+ verify.request_sha256 !== exec.request_sha256) throw new Error(`remote contained verification failed: ${JSON.stringify(verify)}`);
177
+ contained = { output, receipt_sha256: exec.receipt_sha256, request_sha256: exec.request_sha256, output_sha256: exec.output_sha256 };
178
+ }
179
+ if (mode === 'full' && agentRuntime) {
180
+ const submissionId = `remote-gate-${randomBytes(8).toString('hex')}`;
181
+ const submitted = structured(await client.callTool({
182
+ name: 'agent_task_submit', arguments: {
183
+ workspace_id: 'demo', submission_id: submissionId, title: 'remote R22 gate',
184
+ steps: [
185
+ { step_id: 'proof', kind: 'proof_execute', input: { kind: 'workspace', path: 'input.txt' }, retry_limit: 0 },
186
+ { step_id: 'contained', kind: 'contained_transform', operation: 'uppercase_ascii', input: { kind: 'step_output', step_id: 'proof' }, retry_limit: 0 }
187
+ ]
188
+ }
189
+ }), 'agent_task_submit');
190
+ const run = structured(await client.callTool({
191
+ name: 'agent_task_run', arguments: { workspace_id: 'demo', task_id: submitted.task_id, max_steps: 8 }
192
+ }), 'agent_task_run');
193
+ if (run.state !== 'succeeded' || run.completed_steps !== 2) throw new Error(`agent task run failed: ${JSON.stringify(run)}`);
194
+ const verified = structured(await client.callTool({
195
+ name: 'agent_task_verify', arguments: { workspace_id: 'demo', task_id: submitted.task_id }
196
+ }), 'agent_task_verify');
197
+ if (verified.ok !== true || verified.signature_valid !== true || verified.step_receipts_verified !== 2) {
198
+ throw new Error(`agent task verify failed: ${JSON.stringify(verified)}`);
134
199
  }
135
- }), 'agent_task_submit');
136
- const run = structured(await client.callTool({
137
- name: 'agent_task_run', arguments: { workspace_id: 'demo', task_id: submitted.task_id, max_steps: 8 }
138
- }), 'agent_task_run');
139
- if (run.state !== 'succeeded' || run.completed_steps !== 2) throw new Error(`agent task run failed: ${JSON.stringify(run)}`);
140
- const verified = structured(await client.callTool({
141
- name: 'agent_task_verify', arguments: { workspace_id: 'demo', task_id: submitted.task_id }
142
- }), 'agent_task_verify');
143
- if (verified.ok !== true || verified.signature_valid !== true || verified.step_receipts_verified !== 2) {
144
- throw new Error(`agent task verify failed: ${JSON.stringify(verified)}`);
200
+ agent = { task_id: submitted.task_id, plan_sha256: submitted.plan_sha256,
201
+ task_receipt_sha256: verified.task_receipt_sha256, step_receipts_verified: verified.step_receipts_verified };
145
202
  }
146
- agent = { task_id: submitted.task_id, plan_sha256: submitted.plan_sha256,
147
- task_receipt_sha256: verified.task_receipt_sha256, step_receipts_verified: verified.step_receipts_verified };
148
203
  }
149
204
  console.log(JSON.stringify({
150
- status: 'passed', mode,
205
+ status: 'passed', mode, profile: compactProfile ? 'compact' : 'legacy-full',
151
206
  protocol_revision: '2026-07-28', protocol_era: 'modern', tools: names,
152
207
  output_schemas_strict: true,
153
208
  machine_runtime: machineRuntime, privileged_host_plane: privilegedHostPlane,
154
209
  agent_runtime: agentRuntime, process_runtime: processRuntime,
155
210
  machine_fs_runtime: machineFsRuntime, observation_runtime: observationRuntime,
156
211
  coding_runtime: codingRuntime, autonomous_runtime: autonomousRuntime,
157
- proof: { output: proofOutput, receipt_sha256: proof.receipt_sha256,
158
- request_sha256: proof.request_sha256, signature_valid: true },
212
+ compact,
213
+ proof: proof ? { output: proofOutput, receipt_sha256: proof.receipt_sha256,
214
+ request_sha256: proof.request_sha256, signature_valid: true } : null,
159
215
  contained, agent
160
216
  }, null, 2));
161
217
  } finally {
@@ -497,6 +497,34 @@ export function runControllerStartNoCapture(ctx,timeoutMs=45000){
497
497
  const call=controllerInvocation(ctx,'start');
498
498
  return controlResult(call.exe,call.args,{cwd:call.cwd,timeoutMs,stdio:'ignore'});
499
499
  }
500
+ export async function readControllerStartFailure(ctx){
501
+ const file=path.join(ctx.runtime_root,'controller-start-failure.json');
502
+ let bytes;
503
+ try { bytes=await readFile(file); }
504
+ catch(error){ if(error?.code==='ENOENT') return null; throw error; }
505
+ const text=bytes.toString('utf8').replace(/^\uFEFF/,'');
506
+ let value;
507
+ try { value=JSON.parse(text); } catch(error){ throw new Error(`controller start failure evidence JSON invalid: ${error.message}`); }
508
+ if(value?.schema!=='deadbyte.controller-start-failure.v1'||!['start','resume','restart'].includes(String(value?.command??''))||
509
+ typeof value?.message!=='string'||value.message.length<1||typeof value?.failed_at_utc!=='string') {
510
+ throw new Error('controller start failure evidence schema invalid');
511
+ }
512
+ return {path:file,sha256:sha256Bytes(bytes),...value};
513
+ }
514
+ export async function preserveControllerStartFailure(ctx,evidenceDir){
515
+ const evidence=await readControllerStartFailure(ctx);
516
+ if(!evidence) return null;
517
+ const source=path.join(ctx.runtime_root,'controller-start-failure.json');
518
+ const bytes=await readFile(source);
519
+ const sourceSha256=sha256Bytes(bytes);
520
+ if(sourceSha256!==evidence.sha256) throw new Error('controller start failure evidence changed before preservation');
521
+ const destination=path.join(path.resolve(evidenceDir),'controller-start-failure.json');
522
+ await atomicBytes(destination,bytes);
523
+ const persistedSha256=sha256Bytes(await readFile(destination));
524
+ if(persistedSha256!==sourceSha256) throw new Error('preserved controller start failure evidence SHA-256 mismatch');
525
+ return {path:destination,sha256:persistedSha256,schema:evidence.schema,command:evidence.command,
526
+ failed_at_utc:evidence.failed_at_utc,message:evidence.message};
527
+ }
500
528
  async function readLedger(ctx,name){
501
529
  const file=path.join(ctx.runtime_root,`${name}.instance.json`);
502
530
  const value=JSON.parse((await readFile(file,'utf8')).replace(/^\uFEFF/,''));
@@ -554,12 +582,20 @@ async function waitCoreGone(runtime,timeoutMs=10000){
554
582
  }
555
583
  async function startAndVerify(ctx,oldRuntime){
556
584
  let start=null;
585
+ await rm(path.join(ctx.runtime_root,'controller-start-failure.json'),{force:true});
557
586
  for(let attempt=1;attempt<=3;attempt+=1){
558
587
  start=runControllerStartNoCapture(ctx,45000);
559
588
  if(!start.error&&start.status===0) break;
560
589
  if(attempt<3) await sleep(1500);
561
590
  }
562
- if(start.error||start.status!==0) throw new Error(`controller start transport failed status=${start.status} error=${start.error?.message??'none'}`);
591
+ if(start.error||start.status!==0){
592
+ let detail='';
593
+ try {
594
+ const evidence=await readControllerStartFailure(ctx);
595
+ if(evidence) detail=` controller=${evidence.message} evidence_sha256=${evidence.sha256}`;
596
+ } catch(error) { detail=` controller_evidence_error=${error.message}`; }
597
+ throw new Error(`controller start transport failed status=${start.status} error=${start.error?.message??'none'}${detail}`);
598
+ }
563
599
  let statusText=''; let status=null;
564
600
  const started=Date.now();
565
601
  while(Date.now()-started<45000){
@@ -879,6 +915,18 @@ export async function performSlotUpdate({stage,developmentRoot,expectedVersion,e
879
915
  return {ok:true,update_id:updateId,state_path:statePath,receipt_path:receiptPath,receipt_sha256:receipt.receipt_sha256,
880
916
  pointer:newPointer,new_runtime:newRuntime,final_authority:finalAuthority,workspace_root:workspace.target};
881
917
  } catch(error){
918
+ let controllerStartFailure=null,controllerStartFailurePreservationError=null;
919
+ if(pointerCommitted&&newCtx){
920
+ try {
921
+ controllerStartFailure=await preserveControllerStartFailure(newCtx,evidenceDir);
922
+ const claimed=/evidence_sha256=([0-9a-f]{64})/.exec(String(error?.message??error));
923
+ if(claimed&&(!controllerStartFailure||controllerStartFailure.sha256!==claimed[1])) {
924
+ controllerStartFailurePreservationError=`controller start failure evidence claim mismatch expected=${claimed[1]} preserved=${controllerStartFailure?.sha256??'missing'}`;
925
+ }
926
+ } catch(preserveError) {
927
+ controllerStartFailurePreservationError=String(preserveError?.message??preserveError);
928
+ }
929
+ }
882
930
  const rollback={attempted:false,new_runtime_stopped:false,pointer_restored:false,launchers_restored:false,workspace_restored:false,
883
931
  mcp_url_restored:false,old_runtime_restarted:false,authority_restored:false,errors:[]};
884
932
  try {
@@ -916,6 +964,8 @@ export async function performSlotUpdate({stage,developmentRoot,expectedVersion,e
916
964
  } catch(rollbackError){ rollback.errors.push(`rollback:${rollbackError.message}`); }
917
965
  const failure={schema:FAILURE_SCHEMA,update_id:updateId,release_version:expectedVersion,candidate_manifest_sha256:expectedManifest,
918
966
  failed_at_utc:new Date().toISOString(),phase:journal.phase??'unknown',error:{name:error?.name??'Error',code:error?.code??null,message:String(error?.message??error)},
967
+ controller_start_failure:controllerStartFailure,
968
+ controller_start_failure_preservation_error:controllerStartFailurePreservationError,
919
969
  predecessor_mode:predecessor?.mode??null,prior_pointer:publicPointer(pointerInfo?.pointer??null),attempted_pointer:publicPointer(newPointer),old_runtime:generationTuple(oldRuntime),rollback};
920
970
  await writeState(statePath,journal,'failed',{failure,rollback}).catch(()=>{});
921
971
  let failureReceipt=null;
@@ -0,0 +1,45 @@
1
+ import { sign as cryptoSign, verify as cryptoVerify } from 'node:crypto';
2
+ import { canonicalJson } from './canonical-json.mjs';
3
+ import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
4
+
5
+ const DOMAIN=Buffer.from('DEADBYTE-SEARCH-CURSOR-V1\0','utf8');
6
+ const HEX64=/^[0-9a-f]{64}$/;
7
+ const SCHEMA='deadbyte.search-cursor.v1';
8
+
9
+ function validateBody(body){
10
+ if(!body||typeof body!=='object'||Array.isArray(body)||body.schema!==SCHEMA) throw new Error('search cursor body invalid');
11
+ if(!HEX64.test(body.query_sha256??'')) throw new Error('search cursor query identity invalid');
12
+ if(!HEX64.test(body.snapshot_sha256??'')) throw new Error('search cursor snapshot identity invalid');
13
+ if(!Number.isInteger(body.offset)||body.offset<0||body.offset>1_000_000) throw new Error('search cursor offset invalid');
14
+ return {schema:SCHEMA,query_sha256:body.query_sha256,snapshot_sha256:body.snapshot_sha256,offset:body.offset};
15
+ }
16
+
17
+ export function createSearchCursorCodec({signingKeyPath,verifyKeyPath}){
18
+ if(!signingKeyPath||!verifyKeyPath) throw new Error('search cursor codec requires signing and verify keys');
19
+ return Object.freeze({
20
+ async encode(input){
21
+ const body=validateBody({...input,schema:SCHEMA});
22
+ const signing=await loadPrivateSigningKey(signingKeyPath);
23
+ const bytes=Buffer.from(canonicalJson(body),'utf8');
24
+ const signature=cryptoSign(null,Buffer.concat([DOMAIN,bytes]),signing.privateKey).toString('base64');
25
+ const envelope={body,attestation:{algorithm:'ed25519',key_id:signing.keyId,signature_base64:signature}};
26
+ return Buffer.from(canonicalJson(envelope),'utf8').toString('base64url');
27
+ },
28
+ async decode(cursor){
29
+ if(typeof cursor!=='string'||cursor.length<1||cursor.length>8192) throw new Error('search cursor invalid');
30
+ let envelope;
31
+ try{envelope=JSON.parse(Buffer.from(cursor,'base64url').toString('utf8'));}catch{throw new Error('search cursor invalid');}
32
+ const body=validateBody(envelope?.body);
33
+ const verifyKey=await loadPublicVerifyKey(verifyKeyPath);
34
+ const att=envelope?.attestation;
35
+ if(att?.algorithm!=='ed25519'||att?.key_id!==verifyKey.keyId||typeof att?.signature_base64!=='string') throw new Error('search cursor attestation invalid');
36
+ const signature=Buffer.from(att.signature_base64,'base64');
37
+ if(signature.length!==64||signature.toString('base64')!==att.signature_base64) throw new Error('search cursor signature encoding invalid');
38
+ const bytes=Buffer.from(canonicalJson(body),'utf8');
39
+ if(!cryptoVerify(null,Buffer.concat([DOMAIN,bytes]),verifyKey.publicKey,signature)) throw new Error('search cursor signature invalid');
40
+ return body;
41
+ }
42
+ });
43
+ }
44
+
45
+ export const SEARCH_CURSOR_SCHEMA=SCHEMA;