deadbyte-mcp 0.15.14 → 0.15.16

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 (66) hide show
  1. package/MANIFEST.SHA256 +65 -27
  2. package/PROVENANCE.json +6 -6
  3. package/README.txt +6 -6
  4. package/RELEASE.json +58 -53
  5. package/docs/COMMANDER_EXHAUSTIVE_SURFACE.json +49 -0
  6. package/docs/COMMANDER_REFERENCE_INVENTORY.json +70 -0
  7. package/docs/R47-FULL-TOOL-CATALOG.json +123 -0
  8. package/package.json +5 -2
  9. package/scripts/commander-hardening-gate-r47.mjs +35 -0
  10. package/scripts/commander-parity-benchmark.mjs +6 -0
  11. package/scripts/commander-real-performance-benchmark-r47.mjs +41 -0
  12. package/scripts/deploy-r47.mjs +67 -8
  13. package/scripts/full-regression-r47.mjs +28 -0
  14. package/scripts/inventory-reference-dist.mjs +17 -0
  15. package/scripts/npm-release-r47.mjs +25 -1
  16. package/scripts/r47-syntax-check.mjs +6 -0
  17. package/scripts/r47-wire-remote-plane.mjs +23 -0
  18. package/src/atomic-mutation.mjs +59 -29
  19. package/src/commander-compat-contracts.mjs +90 -0
  20. package/src/commander-compat-runtime.mjs +91 -0
  21. package/src/commander-core-bridge.mjs +30 -0
  22. package/src/commander-extended-runtime.mjs +74 -0
  23. package/src/commander-observability.mjs +33 -0
  24. package/src/commander-remote-bridge-r47.mjs +41 -0
  25. package/src/commander-remote-relay-r47.mjs +44 -0
  26. package/src/machine-fs.mjs +108 -22
  27. package/src/mcp-server.mjs +94 -0
  28. package/src/observation-output-contracts.mjs +9 -8
  29. package/src/observation-runtime.mjs +47 -17
  30. package/src/operation-history.mjs +95 -0
  31. package/src/operation-lifecycle.mjs +63 -0
  32. package/src/process-output-contracts.mjs +24 -1
  33. package/src/process-readiness.mjs +120 -0
  34. package/src/process-runtime.mjs +178 -6
  35. package/src/release-slot.mjs +296 -293
  36. package/src/runtime-update.mjs +61 -7
  37. package/src/tool-audit.mjs +15 -2
  38. package/src/version.mjs +1 -2
  39. package/test/OBSERVED-COMMANDER-TOOLS.json +1 -0
  40. package/test/agent-runtime.test.mjs +1 -1
  41. package/test/autonomous-runtime.test.mjs +1 -1
  42. package/test/coding-plane.test.mjs +1 -1
  43. package/test/coding-search-r36.test.mjs +14 -0
  44. package/test/commander-compat-runtime.test.mjs +34 -0
  45. package/test/commander-core-bridge-r47.test.mjs +31 -0
  46. package/test/commander-exhaustive-surface-r47.test.mjs +32 -0
  47. package/test/commander-extended-runtime-r47.test.mjs +46 -0
  48. package/test/commander-observability-r47.test.mjs +34 -0
  49. package/test/commander-parity-gate-r47.test.mjs +49 -0
  50. package/test/commander-protocol-r47.test.mjs +30 -0
  51. package/test/commander-real-parity-r47.test.mjs +121 -0
  52. package/test/commander-remote-bridge-r47.test.mjs +39 -0
  53. package/test/commander-remote-plane-r47.test.mjs +25 -0
  54. package/test/commander-torture-r47.test.mjs +146 -0
  55. package/test/full-max-catalog-r46.test.mjs +1 -1
  56. package/test/full-max-catalog-r47.test.mjs +78 -0
  57. package/test/machine-fs-runtime.test.mjs +97 -0
  58. package/test/observation-runtime.test.mjs +28 -0
  59. package/test/operation-history.test.mjs +61 -0
  60. package/test/operation-lifecycle.test.mjs +72 -0
  61. package/test/output-schema.test.mjs +1 -1
  62. package/test/process-readiness.test.mjs +32 -0
  63. package/test/process-runtime.test.mjs +147 -0
  64. package/test/r47-release-contract.test.mjs +37 -0
  65. package/test/release-activation-gate.test.mjs +30 -0
  66. package/test/release-version.test.mjs +1 -1
@@ -51,6 +51,11 @@ import { registerAutonomousTools } from './autonomous-mcp-tools.mjs';
51
51
  import { createToolAudit, installToolAudit } from './tool-audit.mjs';
52
52
  import { createAuditContextProvider } from './audit-context.mjs';
53
53
  import { registerOperatorSurface } from './operator-surface.mjs';
54
+ import { createCommanderExtendedRuntime } from './commander-extended-runtime.mjs';
55
+ import { registerCommanderCoreBridge } from './commander-core-bridge.mjs';
56
+ import { registerCommanderRemoteBridge } from './commander-remote-bridge-r47.mjs';
57
+ import { createCommanderRemoteRelayR47 } from './commander-remote-relay-r47.mjs';
58
+ import { createRemoteDevicePlaneR44 } from './remote-device-r44.mjs';
54
59
  import { capabilityProfileFromEnv, COMPACT_SURFACE_TOOL_LIMIT, R36_CAPABILITY_LEDGER } from './capability-ledger.mjs';
55
60
  import { registerCompactGatewayTools } from './compact-gateway-mcp-tools.mjs';
56
61
  import { createSkillPlane } from './skill-plane-r44.mjs';
@@ -238,6 +243,17 @@ const skillRoot=path.join(runtimeStateRoot,'skills');
238
243
  await mkdir(skillRoot,{recursive:true});
239
244
  const skillRuntime=await createSkillPlane({root:skillRoot,verifyKeyPath});
240
245
 
246
+ const remoteRelayEndpoint=process.env.DEADBYTE_REMOTE_RELAY_ENDPOINT||process.env.DEADBYTE_REMOTE_ENDPOINT||'';
247
+ const remoteRelayCapabilities=new Set(String(process.env.DEADBYTE_REMOTE_CAPABILITIES||'').split(',').map(value=>value.trim()).filter(Boolean));
248
+ const remoteRelay=remoteRelayEndpoint ? await createCommanderRemoteRelayR47({endpoint:remoteRelayEndpoint}) : null;
249
+ const remoteDevicePlane=remoteRelay
250
+ ? await createRemoteDevicePlaneR44({
251
+ stateDir:path.join(runtimeStateRoot,'remote-device-r44'),
252
+ relay:remoteRelay,
253
+ authorizeCapability:async({capabilityId})=>remoteRelayCapabilities.has(capabilityId)
254
+ })
255
+ : null;
256
+
241
257
  installStdioConsoleGuard();
242
258
  if (process.env.DEADBYTE_STDIO_CONSOLE_GUARD_PROBE === '1') console.log('DEADBYTE_STDIO_CONSOLE_GUARD_PROBE');
243
259
 
@@ -276,9 +292,83 @@ serveStdio(() => {
276
292
  return server.registerTool(name,...args);
277
293
  }};
278
294
  registerOperatorSurface(server,{packageRoot,surfaceProfile,compactToolLimit:COMPACT_SURFACE_TOOL_LIMIT});
295
+ const commanderExtendedRuntime=createCommanderExtendedRuntime({
296
+ stateRoot:runtimeStateRoot,
297
+ history:null,
298
+ prompts:[
299
+ {name:'deadbyte.review',title:'Review'},
300
+ {name:'deadbyte.patch',title:'Patch'},
301
+ {name:'deadbyte.diagnose',title:'Diagnose'},
302
+ {name:'deadbyte.run-tests',title:'Run tests'}
303
+ ],
304
+ resources:[
305
+ {uri:'deadbyte://context',mime_type:'text/markdown'},
306
+ {uri:'deadbyte://architecture',mime_type:'text/markdown'},
307
+ {uri:'deadbyte://surface-profile',mime_type:'application/json'}
308
+ ]
309
+ });
279
310
  if (toolAudit) installToolAudit(server,toolAudit);
280
311
 
281
312
  if (surfaceProfile === 'full') {
313
+ toolServer.registerTool('get_config',{
314
+ description:'Read bounded DEADBYTE Commander-compatible configuration.',
315
+ inputSchema:z.strictObject({key:z.string().min(1).max(128).nullable().default(null)})
316
+ },async args=>commanderExtendedRuntime.getConfig(args));
317
+
318
+ toolServer.registerTool('set_config_value',{
319
+ description:'Atomically update one bounded configuration scalar with an exact hash precondition.',
320
+ inputSchema:z.strictObject({key:z.string().min(1).max(128),value:z.union([z.string().max(16384),z.number(),z.boolean(),z.null()]),expected_sha256:z.string().regex(/^[0-9a-f]{64}$/)})
321
+ },async args=>commanderExtendedRuntime.setConfigValue(args));
322
+
323
+ toolServer.registerTool('give_feedback_to_desktop_commander',{
324
+ description:'Persist bounded operator feedback locally; no external network transmission.',
325
+ inputSchema:z.strictObject({message:z.string().min(1).max(4096),category:z.string().min(1).max(64).default('general')})
326
+ },async args=>commanderExtendedRuntime.feedback(args));
327
+
328
+ toolServer.registerTool('get_prompts',{
329
+ description:'Return the bounded native prompt catalog.',
330
+ inputSchema:z.strictObject({})
331
+ },async()=>commanderExtendedRuntime.getPrompts());
332
+
333
+ toolServer.registerTool('resources',{
334
+ description:'Return the bounded native resource catalog.',
335
+ inputSchema:z.strictObject({})
336
+ },async()=>commanderExtendedRuntime.getResources());
337
+
338
+ toolServer.registerTool('file_preview',{
339
+ description:'Return a bounded preview slice from an explicitly rooted file.',
340
+ inputSchema:z.strictObject({root_id:z.string().min(1).max(512),path:z.string().min(1).max(512),offset:z.number().int().min(0).default(0),max_bytes:z.number().int().min(1).max(65536).default(65536),mime_type:z.string().max(128).default('text/plain')})
341
+ },async({root_id,path:relativePath,offset,max_bytes,mime_type})=>commanderExtendedRuntime.filePreview({rootId:root_id,path:relativePath,offset,maxBytes:max_bytes,mimeType:mime_type}));
342
+
343
+ toolServer.registerTool('write_pdf',{
344
+ description:'Generate a bounded self-contained PDF document from text.',
345
+ inputSchema:z.strictObject({text:z.string().max(65536),metadata:z.record(z.string(),z.unknown()).default({})})
346
+ },async args=>commanderExtendedRuntime.writePdf(args));
347
+
348
+ toolServer.registerTool('markdown_editor',{
349
+ description:'Hash-preconditioned bounded Markdown exact edit using the native machine filesystem transaction.',
350
+ inputSchema:z.strictObject({root_id:z.string().min(1).max(64),path:z.string().min(1).max(512),expected_sha256:z.string().regex(/^[0-9a-f]{64}$/),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)})
351
+ },async args=>{
352
+ if(!machineFsRuntime) return textResult(z.any(),{status:'denied',error:'machine filesystem runtime is disabled'},true);
353
+ return machineFsRuntime.editExact({rootId:args.root_id,relativePath:args.path,expectedSha256:args.expected_sha256,oldString:args.old_string,newString:args.new_string,expectedReplacements:args.expected_replacements});
354
+ });
355
+
356
+ toolServer.registerTool('get_usage_stats',{
357
+ description:'Return bounded audit/runtime usage status.',
358
+ inputSchema:z.strictObject({})
359
+ },async()=>{
360
+ if(!toolAudit) return {status:'unavailable',reason:'audit runtime is disabled'};
361
+ return {status:'ok',audit:await toolAudit.status()};
362
+ });
363
+
364
+ toolServer.registerTool('get_recent_tool_calls',{
365
+ description:'Return a bounded, cursorable recent signed audit event window.',
366
+ inputSchema:z.strictObject({limit:z.number().int().min(1).max(256).default(50),after_seq:z.number().int().min(0).default(0)})
367
+ },async({limit,after_seq})=>{
368
+ if(!toolAudit) return {status:'unavailable',reason:'audit runtime is disabled'};
369
+ return {status:'ok',audit:await toolAudit.recent({limit,after_seq})};
370
+ });
371
+
282
372
  toolServer.registerTool(
283
373
  'workspace_open',
284
374
  {
@@ -589,5 +679,9 @@ serveStdio(() => {
589
679
  );
590
680
  }
591
681
 
682
+ if (surfaceProfile === 'full') {
683
+ registerCommanderCoreBridge(toolServer,{machineFsRuntime,processRuntime,searchRuntime:codingRuntime?.search??null});
684
+ registerCommanderRemoteBridge(toolServer,{remoteDevicePlane});
685
+ }
592
686
  return server;
593
687
  }, { legacy: 'reject' });
@@ -5,7 +5,8 @@ const hex64=z.string().regex(/^[0-9a-f]{64}$/);
5
5
  const receiptId=z.string().regex(/^[0-9a-f]{24}$/);
6
6
  const searchState=z.enum(['pending','running','completed','cancelled','failed']);
7
7
  const evidenceShape={request_sha256:hex64,result_head_sha256:hex64,receipt_id:receiptId.nullable(),receipt_sha256:hex64.nullable()};
8
- const resultSchema=z.strictObject({path:z.string().min(1).max(512),line:z.number().int().min(1),column:z.number().int().min(1),preview:z.string().max(512)});
8
+ const overflowSchema=z.strictObject({occurred:z.boolean(),reason:z.enum(['result_cap','byte_cap','time_limit','producer_failure']).nullable(),emitted:z.number().int().nonnegative(),dropped:z.union([z.number().int().nonnegative(),z.literal('unknown')]),resume_supported:z.boolean()});
9
+ const resultSchema=z.strictObject({result_id:hex64,path:z.string().min(1).max(512),file_sha256:hex64,line:z.number().int().min(1),column:z.number().int().min(1),preview:z.string().max(512)});
9
10
  const processSchema=z.strictObject({pid:z.number().int().positive(),parent_pid:z.number().int().nonnegative(),name:z.string().max(260),
10
11
  ownership:z.enum(['deadbyte-runtime','deadbyte-session','external']),executable_path:z.string().max(2048).nullable(),command_line:z.string().max(4096).nullable()});
11
12
 
@@ -17,18 +18,18 @@ function statusContract(shape,successRequired=[]){
17
18
  }
18
19
 
19
20
  export const observationSearchStartOutputSchema=z.strictObject({
20
- status:z.enum(['accepted','failed']),error:z.string().optional(),search_id:hex64.optional(),state:searchState.optional(),request_sha256:hex64.optional()
21
- }).superRefine((v,ctx)=>{if(v.status==='accepted'){for(const k of ['search_id','state','request_sha256']) if(v[k]===undefined) ctx.addIssue({code:'custom',path:[k],message:`${k} required`});} else if(!v.error) ctx.addIssue({code:'custom',path:['error'],message:'error required'});});
21
+ status:z.enum(['accepted','failed']),error:z.string().optional(),search_id:hex64.optional(),snapshot_id:hex64.optional(),state:searchState.optional(),request_sha256:hex64.optional()
22
+ }).superRefine((v,ctx)=>{if(v.status==='accepted'){for(const k of ['search_id','snapshot_id','state','request_sha256']) if(v[k]===undefined) ctx.addIssue({code:'custom',path:[k],message:`${k} required`});} else if(!v.error) ctx.addIssue({code:'custom',path:['error'],message:'error required'});});
22
23
  export const observationSearchPageOutputSchema=statusContract({
23
- search_id:hex64.optional(),state:searchState.optional(),results:z.array(resultSchema).max(500).optional(),next_cursor:z.string().max(512).nullable().optional(),
24
+ search_id:hex64.optional(),snapshot_id:hex64.optional(),state:searchState.optional(),results:z.array(resultSchema).max(500).optional(),next_cursor:z.string().max(1024).nullable().optional(),sequence:z.number().int().min(0).optional(),overflow:overflowSchema.optional(),
24
25
  ...Object.fromEntries(Object.entries(evidenceShape).map(([k,v])=>[k,v.optional()]))
25
- },['search_id','state','results','next_cursor','request_sha256','result_head_sha256','receipt_id','receipt_sha256']);
26
+ },['search_id','snapshot_id','state','results','next_cursor','sequence','overflow','request_sha256','result_head_sha256','receipt_id','receipt_sha256']);
26
27
 
27
28
  export const observationSearchStatusOutputSchema=statusContract({
28
- search_id:hex64.optional(),state:searchState.optional(),result_count:z.number().int().nonnegative().optional(),files_scanned:z.number().int().nonnegative().optional(),
29
- bytes_scanned:z.number().int().nonnegative().optional(),truncated:z.boolean().optional(),search_error:z.string().nullable().optional(),
29
+ search_id:hex64.optional(),snapshot_id:hex64.optional(),state:searchState.optional(),result_count:z.number().int().nonnegative().optional(),files_scanned:z.number().int().nonnegative().optional(),
30
+ bytes_scanned:z.number().int().nonnegative().optional(),truncated:z.boolean().optional(),overflow:overflowSchema.optional(),search_error:z.string().nullable().optional(),
30
31
  ...Object.fromEntries(Object.entries(evidenceShape).map(([k,v])=>[k,v.optional()]))
31
- },['search_id','state','result_count','files_scanned','bytes_scanned','truncated','search_error','request_sha256','result_head_sha256','receipt_id','receipt_sha256']);
32
+ },['search_id','snapshot_id','state','result_count','files_scanned','bytes_scanned','truncated','overflow','search_error','request_sha256','result_head_sha256','receipt_id','receipt_sha256']);
32
33
 
33
34
  export const observationSearchCancelOutputSchema=statusContract({
34
35
  search_id:hex64.optional(),state:searchState.optional(),cancel_requested:z.boolean().optional()
@@ -51,6 +51,8 @@ function decodeCursor(value){
51
51
  try { return JSON.parse(Buffer.from(String(value),'base64url').toString('utf8')); }
52
52
  catch { throw new Error('invalid observation cursor'); }
53
53
  }
54
+ function cursorTag(secret, body){ return sha256Bytes(Buffer.concat([secret,Buffer.from(canonicalJson(body),'utf8')])); }
55
+ function makeStableResultId({path,fileSha256,line,column}){ return sha256Object({schema:'deadbyte.search-result.v1',path:path.replaceAll('\\\\','/'),file_sha256:fileSha256,line,column}); }
54
56
  async function writeReceipt(runtime,kind,requestSha,resultHead,summary){
55
57
  const receiptId=randomBytes(12).toString('hex');
56
58
  const body={schema:OBSERVATION_RECEIPT_SCHEMA,receipt_id:receiptId,kind,
@@ -140,6 +142,9 @@ export function createObservationRuntime({policy,signingKeyPath,verifyKeyPath,pr
140
142
  if(!Number.isInteger(maxSearchSessions)||maxSearchSessions<1||maxSearchSessions>4096) throw new Error('maxSearchSessions out of range');
141
143
  if(typeof clockMs!=='function') throw new Error('clockMs must be a function');
142
144
  const searches=new Map(),snapshots=new Map();
145
+ const searchCursorSecrets=new Map();
146
+ const SEARCH_CURSOR_SCHEMA='deadbyte.search-cursor.v1';
147
+ const overflowReason=Object.freeze({result_cap:'result_cap',byte_cap:'byte_cap',time_limit:'time_limit',producer_failure:'producer_failure'});
143
148
  let signingPromise=null;
144
149
  const runtime={policy,signing:()=>signingPromise??=(loadPrivateSigningKey(signingKeyPath))};
145
150
  const terminalSearchState=state=>['completed','cancelled','failed'].includes(state);
@@ -175,7 +180,7 @@ export function createObservationRuntime({policy,signingKeyPath,verifyKeyPath,pr
175
180
  session.state='running';
176
181
  try {
177
182
  const start=await resolveObservationRoot(policy,session.root_id,session.path);
178
- const files=[]; const queue=[{dir:start.resolved,rel:start.reported==='.'?'':start.reported}];
183
+ const snapshotId=session.snapshot_id; const queue=[{dir:start.resolved,rel:start.reported==='.'?'':start.reported}]; const files=[];
179
184
  while(queue.length&&files.length<session.max_files&&!session.cancel_requested){
180
185
  const current=queue.shift(); const children=await readdir(current.dir,{withFileTypes:true});
181
186
  children.sort((a,b)=>a.name.localeCompare(b.name));
@@ -189,28 +194,30 @@ export function createObservationRuntime({policy,signingKeyPath,verifyKeyPath,pr
189
194
  }
190
195
  }
191
196
  files.sort((a,b)=>a.path.localeCompare(b.path));
192
- if(queue.length) session.truncated=true;
197
+ if(queue.length){ session.truncated=true; session.overflow={occurred:true,reason:overflowReason.result_cap,emitted:session.results.length,dropped:'unknown',resume_supported:false}; }
193
198
  const needle=session.case_sensitive?session.pattern:session.pattern.toLowerCase();
194
199
  for(const file of files){
195
200
  if(session.cancel_requested) break;
196
201
  if(file.bytes>session.max_file_bytes) continue;
197
- if(session.bytes_scanned+file.bytes>session.max_bytes){ session.truncated=true; break; }
198
- const bytes=await readFile(file.full); let text;
202
+ if(session.bytes_scanned+file.bytes>session.max_bytes){ session.truncated=true; session.overflow={occurred:true,reason:overflowReason.byte_cap,emitted:session.results.length,dropped:'unknown',resume_supported:false}; break; }
203
+ const bytes=await readFile(file.full); const fileSha256=sha256Bytes(bytes); let text;
199
204
  try { text=decodeUtf8Strict(bytes); } catch { continue; }
200
205
  session.files_scanned++; session.bytes_scanned+=bytes.length;
201
206
  const lines=text.split(/\r?\n/);
202
207
  for(let i=0;i<lines.length;i++){
203
208
  const hay=session.case_sensitive?lines[i]:lines[i].toLowerCase(); const index=hay.indexOf(needle);
204
209
  if(index<0) continue;
205
- const result={path:file.path,line:i+1,column:index+1,preview:lines[i].slice(Math.max(0,index-40),index+Math.min(200,session.pattern.length+120))};
210
+ const resultBase={path:file.path,file_sha256:fileSha256,line:i+1,column:index+1,preview:lines[i].slice(Math.max(0,index-40),index+Math.min(200,session.pattern.length+120))};
211
+ const result={...resultBase,result_id:makeStableResultId(resultBase)};
206
212
  session.results.push(result); session.result_head_sha256=sha256Object({previous_result_sha256:session.result_head_sha256,result});
207
- if(session.results.length>=session.max_results){ session.truncated=true; break; }
213
+ if(session.results.length>=session.max_results){ session.truncated=true; session.overflow={occurred:true,reason:overflowReason.result_cap,emitted:session.results.length,dropped:'unknown',resume_supported:false}; break; }
208
214
  }
209
215
  if(session.results.length>=session.max_results) break;
210
216
  await new Promise(resolve=>setImmediate(resolve));
211
217
  }
218
+ if(session.cancel_requested){ session.overflow={occurred:session.truncated,reason:session.truncated?overflowReason.result_cap:null,emitted:session.results.length,dropped:session.truncated?'unknown':0,resume_supported:false}; }
212
219
  await finalizeSearch(session,session.cancel_requested?'cancelled':'completed');
213
- } catch(error){ await finalizeSearch(session,'failed',error); }
220
+ } catch(error){ session.overflow={occurred:true,reason:overflowReason.producer_failure,emitted:session.results.length,dropped:'unknown',resume_supported:false}; await finalizeSearch(session,'failed',error); }
214
221
  }
215
222
  async function searchStart({rootId,relativePath='.',pattern,caseSensitive=false,maxResults=200,maxFiles=2000,maxBytes=4*1024*1024,maxFileBytes=262144}){
216
223
  if(typeof pattern!=='string'||pattern.length<1||pattern.length>256) throw new Error('search pattern must be 1-256 characters');
@@ -224,14 +231,16 @@ export function createObservationRuntime({policy,signingKeyPath,verifyKeyPath,pr
224
231
  const args={root_id:rootId,path:relativePath,pattern,case_sensitive:caseSensitive,max_results:maxResults,max_files:maxFiles,max_bytes:maxBytes,max_file_bytes:maxFileBytes};
225
232
  const session={search_id:randomBytes(32).toString('hex'),root_id:rootId,path:relativePath,pattern,case_sensitive:caseSensitive,
226
233
  max_results:maxResults,max_files:maxFiles,max_bytes:maxBytes,max_file_bytes:maxFileBytes,state:'pending',cancel_requested:false,
227
- results:[],files_scanned:0,bytes_scanned:0,truncated:false,result_head_sha256:ZERO,error:null,receipt_id:null,receipt_sha256:null,
228
- created_at_ms:Number(clockMs()),completed_at_ms:null,request_sha256:requestSha(policy,'machine_search_start',args)};
234
+ snapshot_id:randomBytes(32).toString('hex'),snapshot_expires_at_ms:Number(clockMs())+searchSessionTtlMs,
235
+ results:[],files_scanned:0,bytes_scanned:0,truncated:false,overflow:{occurred:false,reason:null,emitted:0,dropped:0,resume_supported:true},result_head_sha256:ZERO,error:null,receipt_id:null,receipt_sha256:null,
236
+ created_at_ms:Number(clockMs()),completed_at_ms:null,max_ack_position:0,last_cursor:null,last_cursor_response:null,request_sha256:requestSha(policy,'machine_search_start',args)};
237
+ searchCursorSecrets.set(session.search_id,randomBytes(32));
229
238
  searches.set(session.search_id,session); setImmediate(()=>{ void executeSearch(session); });
230
- return {status:'accepted',search_id:session.search_id,state:session.state,request_sha256:session.request_sha256};
239
+ return {status:'accepted',search_id:session.search_id,snapshot_id:session.snapshot_id,state:session.state,request_sha256:session.request_sha256};
231
240
  }
232
241
  async function searchStatus({searchId}){
233
- const s=searchFor(searchId); return {status:'ok',search_id:s.search_id,state:s.state,result_count:s.results.length,files_scanned:s.files_scanned,
234
- bytes_scanned:s.bytes_scanned,truncated:s.truncated,request_sha256:s.request_sha256,result_head_sha256:s.result_head_sha256,
242
+ const s=searchFor(searchId); return {status:'ok',search_id:s.search_id,snapshot_id:s.snapshot_id,state:s.state,result_count:s.results.length,files_scanned:s.files_scanned,
243
+ bytes_scanned:s.bytes_scanned,truncated:s.truncated,overflow:s.overflow,request_sha256:s.request_sha256,result_head_sha256:s.result_head_sha256,
235
244
  receipt_id:s.receipt_id,receipt_sha256:s.receipt_sha256,error:s.error};
236
245
  }
237
246
  async function searchCancel({searchId}){
@@ -241,12 +250,33 @@ export function createObservationRuntime({policy,signingKeyPath,verifyKeyPath,pr
241
250
  }
242
251
  async function searchPage({searchId,limit=100,cursor=null}){
243
252
  const s=searchFor(searchId);
253
+ const now=Number(clockMs());
254
+ if(now>=s.snapshot_expires_at_ms) throw new Error('search snapshot expired');
244
255
  if(!Number.isInteger(limit)||limit<1||limit>500) throw new Error('search page limit must be in [1,500]');
245
- let index=0;
246
- if(cursor!==null){ const parsed=decodeCursor(cursor); if(parsed.search_id!==searchId||!Number.isInteger(parsed.index)||parsed.index<0) throw new Error('search cursor mismatch'); index=parsed.index; }
247
- const results=s.results.slice(index,index+limit); const next=index+results.length<s.results.length?encodeCursor({search_id:searchId,index:index+results.length}):null;
248
- return {status:'ok',search_id:s.search_id,state:s.state,results,next_cursor:next,result_head_sha256:s.result_head_sha256,
249
- request_sha256:s.request_sha256,receipt_id:s.receipt_id,receipt_sha256:s.receipt_sha256};
256
+ let position=0, nextSequence=1;
257
+ if(cursor!==null){
258
+ const parsed=decodeCursor(cursor);
259
+ const body={schema:SEARCH_CURSOR_SCHEMA,search_id:parsed.search_id,snapshot_id:parsed.snapshot_id,next_sequence:parsed.next_sequence,position:parsed.position};
260
+ const secret=searchCursorSecrets.get(searchId);
261
+ if(!secret||parsed.search_id!==searchId||parsed.snapshot_id!==s.snapshot_id||!Number.isInteger(parsed.next_sequence)||!Number.isInteger(parsed.position)||parsed.position<0||parsed.next_sequence<1||parsed.next_sequence!==parsed.position+1||parsed.integrity!==cursorTag(secret,body)){
262
+ throw new Error('invalid, tampered, rewound, or mismatched search cursor');
263
+ }
264
+ position=parsed.position; nextSequence=parsed.next_sequence;
265
+ }
266
+ if(position>s.results.length) throw new Error('search cursor is ahead of producer boundary');
267
+ if(position<s.max_ack_position){
268
+ if(cursor!==null && cursor===s.last_cursor && s.last_cursor_response) return s.last_cursor_response;
269
+ throw new Error('rewound search cursor rejected');
270
+ }
271
+ const results=s.results.slice(position,position+limit);
272
+ const nextPosition=position+results.length;
273
+ s.max_ack_position=Math.max(s.max_ack_position,nextPosition);
274
+ const next=nextPosition<s.results.length ? (()=>{ const body={schema:SEARCH_CURSOR_SCHEMA,search_id:s.search_id,snapshot_id:s.snapshot_id,next_sequence:nextPosition+1,position:nextPosition}; const integrity=cursorTag(searchCursorSecrets.get(searchId),body); return encodeCursor({...body,integrity}); })() : null;
275
+ const response=Object.freeze({status:'ok',search_id:s.search_id,snapshot_id:s.snapshot_id,state:s.state,results,next_cursor:next,
276
+ sequence:nextSequence+results.length-1,overflow:s.overflow,result_head_sha256:s.result_head_sha256,
277
+ request_sha256:s.request_sha256,receipt_id:s.receipt_id,receipt_sha256:s.receipt_sha256});
278
+ if(cursor!==null) { s.last_cursor=cursor; s.last_cursor_response=response; }
279
+ return response;
250
280
  }
251
281
  async function captureProcessSnapshot(includeExternal){
252
282
  const observedAt=nowUtc(); const rows=await processSnapshotProvider();
@@ -0,0 +1,95 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { canonicalJson } from './canonical-json.mjs';
3
+
4
+ const ZERO_HASH='0'.repeat(64);
5
+ const sha256=value=>createHash('sha256').update(value).digest('hex');
6
+ const objectHash=value=>sha256(Buffer.from(canonicalJson(value),'utf8'));
7
+
8
+ export const OPERATION_HISTORY_SCHEMA='deadbyte.operation-history.v1';
9
+
10
+ export function createOperationHistory({maxEvents=1024,maxBytes=1024*1024,clockMs=()=>Date.now(),operationId=randomBytes(16).toString('hex')}={}){
11
+ if(!Number.isInteger(maxEvents)||maxEvents<1||maxEvents>100000) throw new Error('maxEvents out of range');
12
+ if(!Number.isInteger(maxBytes)||maxBytes<1024||maxBytes>64*1024*1024) throw new Error('maxBytes out of range');
13
+ if(typeof clockMs!=='function') throw new Error('clockMs must be a function');
14
+ if(typeof operationId!=='string'||operationId.length<1||operationId.length>128) throw new Error('invalid operationId');
15
+ const events=[];
16
+ let bytes=0,sequence=0,head=ZERO_HASH;
17
+ let overflow={occurred:false,reason:null,dropped:0,dropped_bytes:0,resume_supported:true};
18
+
19
+ function append(eventType,payload={}){
20
+ if(typeof eventType!=='string'||eventType.length<1||eventType.length>128) throw new Error('invalid eventType');
21
+ const payloadDigest=objectHash(payload);
22
+ const body={
23
+ schema:OPERATION_HISTORY_SCHEMA,
24
+ operation_id:operationId,
25
+ sequence:++sequence,
26
+ event_type:eventType,
27
+ at_ms:Number(clockMs()),
28
+ payload_digest:payloadDigest,
29
+ previous_event_hash:head
30
+ };
31
+ const eventHash=objectHash(body);
32
+ const frozenPayload=JSON.parse(canonicalJson(payload));
33
+ const event=Object.freeze({...body,event_hash:eventHash,payload:Object.freeze(frozenPayload)});
34
+ const size=Buffer.byteLength(canonicalJson(event),'utf8');
35
+ if(size>maxBytes) throw new Error('operation history event exceeds byte cap');
36
+ while(events.length&&bytes+size>maxBytes){
37
+ const dropped=events.shift();
38
+ bytes-=dropped.__bytes;
39
+ overflow={occurred:true,reason:'byte_cap',dropped:overflow.dropped+1,dropped_bytes:overflow.dropped_bytes+dropped.__bytes,resume_supported:false};
40
+ }
41
+ events.push(Object.freeze({...event,__bytes:size}));
42
+ bytes+=size; head=eventHash;
43
+ if(events.length>maxEvents){
44
+ const dropped=events.shift();
45
+ bytes-=dropped.__bytes;
46
+ overflow={occurred:true,reason:'event_cap',dropped:overflow.dropped+1,dropped_bytes:overflow.dropped_bytes+dropped.__bytes,resume_supported:false};
47
+ }
48
+ return event;
49
+ }
50
+
51
+ function read({afterSequence=0,limit=100}={}){
52
+ if(!Number.isInteger(afterSequence)||afterSequence<0) throw new Error('invalid history cursor');
53
+ if(!Number.isInteger(limit)||limit<1||limit>1000) throw new Error('history limit out of range');
54
+ const earliest=events[0]?.sequence??sequence+1;
55
+ if(afterSequence<earliest-1&&events.length) throw new Error('history cursor expired by bounded retention');
56
+ const selected=events.filter(e=>e.sequence>afterSequence).slice(0,limit).map(({__bytes,...e})=>e);
57
+ return {
58
+ schema:OPERATION_HISTORY_SCHEMA,operation_id:operationId,events:selected,
59
+ next_sequence:selected.length?selected.at(-1).sequence:afterSequence,
60
+ head_event_hash:head,earliest_sequence:earliest,
61
+ overflow:Object.freeze({...overflow})
62
+ };
63
+ }
64
+
65
+ function verify(){
66
+ let previous=events.length&&overflow.occurred ? events[0].previous_event_hash : ZERO_HASH;
67
+ for(let i=0;i<events.length;i++){
68
+ const {event_hash,payload,__bytes,...body}=events[i];
69
+ if(body.operation_id!==operationId||body.sequence!==events[i].sequence||body.previous_event_hash!==previous) return {ok:false,reason:'continuity_mismatch'};
70
+ if(body.payload_digest!==objectHash(payload)||event_hash!==objectHash(body)) return {ok:false,reason:'event_hash_mismatch'};
71
+ previous=event_hash;
72
+ }
73
+ return {ok:true,head_event_hash:head,earliest_sequence:events[0]?.sequence??sequence+1,next_sequence:sequence+1,overflow:{...overflow}};
74
+ }
75
+
76
+ return Object.freeze({
77
+ operationId,
78
+ append,
79
+ read,
80
+ verify,
81
+ snapshot:()=>read({afterSequence:Math.max(0,(events[0]?.sequence??sequence+1)-1),limit:Math.min(maxEvents,1000)})
82
+ });
83
+ }
84
+ export function operationHistoryReceipt(history){
85
+ const verified=history.verify();
86
+ if(!verified.ok) throw new Error('operation history integrity verification failed');
87
+ return Object.freeze({
88
+ schema:OPERATION_HISTORY_SCHEMA,
89
+ operation_id:history.operationId,
90
+ head_event_hash:verified.head_event_hash,
91
+ earliest_sequence:verified.earliest_sequence,
92
+ next_sequence:verified.next_sequence,
93
+ overflow:verified.overflow
94
+ });
95
+ }
@@ -0,0 +1,63 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { createOperationHistory, operationHistoryReceipt } from './operation-history.mjs';
3
+
4
+ export const OPERATION_LIFECYCLE_SCHEMA='deadbyte.operation-lifecycle.v1';
5
+ const TERMINAL=new Set(['success','failure','cancelled','timed_out']);
6
+ const ACTIVE=new Set(['start','running','cancel_requested','canceling','output','error']);
7
+ const ALLOWED=new Map([
8
+ ['start',new Set(['running','cancel_requested','output','error','success','failure','cancelled','timed_out'])],
9
+ ['running',new Set(['running','cancel_requested','canceling','output','error','success','failure','cancelled','timed_out'])],
10
+ ['cancel_requested',new Set(['canceling','cancelled','timed_out','output','error'])],
11
+ ['canceling',new Set(['cancelled','timed_out','output','error'])],
12
+ ['output',new Set(['running','cancel_requested','canceling','output','error','success','failure','cancelled','timed_out'])],
13
+ ['error',new Set(['running','cancel_requested','canceling','success','failure','cancelled','timed_out','error'])],
14
+ ['success',new Set()], ['failure',new Set()], ['cancelled',new Set()], ['timed_out',new Set()]
15
+ ]);
16
+
17
+ export function createOperationLifecycle({operationId=randomBytes(32).toString('hex'),clockMs=()=>Date.now(),maxEvents=1024,maxBytes=1024*1024}={}){
18
+ const history=createOperationHistory({operationId,clockMs,maxEvents,maxBytes});
19
+ let state='new';
20
+ let terminalSequence=null;
21
+ let terminalEventHash=null;
22
+ let terminalType=null;
23
+
24
+ function emit(type,payload={}){
25
+ if(typeof type!=='string'||(!ACTIVE.has(type)&&!TERMINAL.has(type))) throw new Error('invalid lifecycle event');
26
+ if(terminalType) {
27
+ if(TERMINAL.has(type)) throw new Error('terminal transition already committed');
28
+ throw new Error('late operation event rejected after terminal');
29
+ }
30
+ if(type==='start' && state!=='new') throw new Error('duplicate operation start');
31
+ const allowed=state==='new' ? new Set(['start']) : ALLOWED.get(state);
32
+ if(!allowed?.has(type)) throw new Error('invalid operation transition '+state+' -> '+type);
33
+ const event=history.append(type,payload);
34
+ state=type;
35
+ if(TERMINAL.has(type)){ terminalSequence=event.sequence; terminalEventHash=event.event_hash; terminalType=type; }
36
+ return Object.freeze({schema:OPERATION_LIFECYCLE_SCHEMA,operation_id:operationId,state,event});
37
+ }
38
+
39
+ function terminal(type,payload={}) {
40
+ if(!TERMINAL.has(type)) throw new Error('terminal type required');
41
+ return emit(type,payload);
42
+ }
43
+
44
+ function snapshot(){
45
+ const verified=history.verify();
46
+ return Object.freeze({
47
+ schema:OPERATION_LIFECYCLE_SCHEMA,operation_id:operationId,state,
48
+ terminal:{type:terminalType,sequence:terminalSequence,event_hash:terminalEventHash},
49
+ history_head_sha256:verified.head_event_hash,
50
+ next_sequence:verified.next_sequence,
51
+ overflow:verified.overflow
52
+ });
53
+ }
54
+
55
+ function receipt(){
56
+ if(!terminalType) throw new Error('terminal receipt unavailable before terminal transition');
57
+ const h=operationHistoryReceipt(history);
58
+ if(h.next_sequence!==terminalSequence+1 || h.head_event_hash!==terminalEventHash) throw new Error('terminal receipt/history mismatch');
59
+ return Object.freeze({...h,terminal_type:terminalType,terminal_sequence:terminalSequence,terminal_event_hash:terminalEventHash});
60
+ }
61
+
62
+ return Object.freeze({operationId,history,emit,terminal,snapshot,receipt,isTerminal:()=>Boolean(terminalType)});
63
+ }
@@ -4,6 +4,17 @@ import { structuredToolResult } from './output-contracts.mjs';
4
4
  const hex64 = z.string().regex(/^[0-9a-f]{64}$/);
5
5
  const digits = z.string().regex(/^[0-9]+$/);
6
6
  const processState = z.enum(['running','exited','terminated']);
7
+ const operationSnapshot = z.strictObject({
8
+ schema:z.literal('deadbyte.operation-lifecycle.v1'), operation_id:hex64,
9
+ state:z.string().min(1).max(32),
10
+ terminal:z.strictObject({type:z.enum(['success','failure','cancelled','timed_out']).nullable(),sequence:z.number().int().positive().nullable(),event_hash:hex64.nullable()}),
11
+ history_head_sha256:hex64,next_sequence:z.number().int().positive(),overflow:z.object({}).passthrough()
12
+ });
13
+ const operationReceipt = z.strictObject({
14
+ schema:z.literal('deadbyte.operation-terminal-receipt.v1'),operation_id:hex64,session_id:hex64,
15
+ terminal:z.object({}).passthrough(),root_id:z.string().min(1).max(64),policy_sha256:hex64,
16
+ key_id:z.string().min(1).max(128),signature:z.string().min(1).max(512)
17
+ });
7
18
  const termination = z.strictObject({
8
19
  deadline_exceeded:z.boolean(), cancellation_requested:z.boolean(), physical_termination:z.boolean(), verified:z.boolean(),
9
20
  terminal_semantics:z.string().min(1).max(64)
@@ -11,6 +22,12 @@ const termination = z.strictObject({
11
22
  const deadline = z.strictObject({
12
23
  schema:z.literal('deadbyte.deadline.v1'), id:hex64.nullable(), created_at_ms:z.number(), timeout_ms:z.number().int().positive().nullable(), deadline_ms:z.number().nullable()
13
24
  });
25
+ const readiness = z.strictObject({
26
+ state:z.enum(['running','ready_for_input','finished']),
27
+ evidence:z.string().min(1).max(64),
28
+ prompt:z.strictObject({profile:z.string().min(1).max(32),prompt:z.string().min(1).max(64)}).nullable(),
29
+ exit_code:z.number().int().nullable()
30
+ });
14
31
  const streamCoverage = z.strictObject({
15
32
  logical_first_seq:z.number().int().min(1), logical_last_seq:z.number().int().min(0),
16
33
  retained_first_seq:z.number().int().min(1), retained_last_seq:z.number().int().min(0),
@@ -40,7 +57,13 @@ export const processSessionRecordSchema = z.strictObject({
40
57
  stream_coverage: streamCoverage.nullable(),
41
58
  termination: termination.nullable(),
42
59
  deadline: deadline.nullable(),
43
- journal_head_sha256: hex64
60
+ readiness: readiness.optional().nullable(),
61
+ journal_head_sha256: hex64,
62
+ operation_id: hex64.nullable().optional(),
63
+ operation: operationSnapshot.nullable().optional(),
64
+ operation_history_head_sha256: hex64.nullable().optional(),
65
+ operation_receipt: operationReceipt.nullable().optional(),
66
+ operation_history_error: z.string().max(4096).nullable().optional()
44
67
  });
45
68
 
46
69
  function resultWithSession(okStatus = 'ok') {
@@ -0,0 +1,120 @@
1
+ const PROMPTS = Object.freeze({
2
+ python: ['>>> ', '... '],
3
+ node: ['> ', '... '],
4
+ powershell: ['PS> ', 'PS C:\\', '>> '],
5
+ cmd: ['> ', 'More? '],
6
+ shell: ['$ ', '# ', '% '],
7
+ mysql: ['mysql> ', ' -> '],
8
+ postgres: ['=# ', '-# '],
9
+ redis: ['redis> ']
10
+ });
11
+
12
+ const COMPLETION = [
13
+ /(?:^|\n)Process finished(?:\r?\n|$)/i,
14
+ /(?:^|\n)Command completed(?:\r?\n|$)/i,
15
+ /(?:^|\n)\[Process completed\]/i,
16
+ /(?:^|\n)Program terminated(?:\r?\n|$)/i,
17
+ /(?:^|\n)Exit code:\s*-?\d+/i
18
+ ];
19
+
20
+ const ERROR_TERMINAL = [
21
+ /^\s*SyntaxError:/m,
22
+ /^\s*TypeError:/m,
23
+ /^\s*ReferenceError:/m,
24
+ /^\s*NameError:/m,
25
+ /^\s*Traceback/m,
26
+ /^\s*Unhandled exception/m
27
+ ];
28
+
29
+ const stripAnsi = value => String(value ?? '').replace(/\x1b\[[0-?]*[ -\/]*[@-~]/g, '');
30
+
31
+ export function normalizeProcessText(value) {
32
+ return stripAnsi(value).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
33
+ }
34
+
35
+ export function detectPrompt(text, profiles = PROMPTS) {
36
+ const normalized = normalizeProcessText(text);
37
+ const lastLine = normalized.split('\n').at(-1) ?? '';
38
+ for (const [profile, prompts] of Object.entries(profiles)) {
39
+ for (const prompt of prompts) {
40
+ if (lastLine.endsWith(prompt) || lastLine === prompt.trimEnd()) {
41
+ return { profile, prompt };
42
+ }
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ export function analyzeReadiness({stdout='', stderr='', state='running', exitCode=null} = {}) {
49
+ const out = normalizeProcessText(stdout);
50
+ const err = normalizeProcessText(stderr);
51
+ if (state === 'exited' || state === 'terminated') {
52
+ return Object.freeze({ state: 'finished', evidence: 'process_terminal', prompt: null, exit_code: exitCode });
53
+ }
54
+ const prompt = detectPrompt(out);
55
+ if (prompt) {
56
+ return Object.freeze({ state: 'ready_for_input', evidence: 'prompt', prompt, exit_code: null });
57
+ }
58
+ if (COMPLETION.some(pattern => pattern.test(out)) || ERROR_TERMINAL.some(pattern => pattern.test(err || out))) {
59
+ return Object.freeze({ state: 'finished', evidence: 'terminal_output', prompt: null, exit_code: exitCode });
60
+ }
61
+ if (out.length === 0 && err.length === 0) {
62
+ return Object.freeze({ state: 'running', evidence: 'no_output', prompt: null, exit_code: null });
63
+ }
64
+ return Object.freeze({ state: 'running', evidence: 'output_without_terminal_marker', prompt: null, exit_code: null });
65
+ }
66
+
67
+ export function lineEndingProfile(text) {
68
+ const value = String(text ?? '');
69
+ let crlf = 0, lf = 0, cr = 0;
70
+ for (let i = 0; i < value.length; i += 1) {
71
+ if (value[i] === '\r') {
72
+ if (value[i + 1] === '\n') { crlf += 1; i += 1; } else cr += 1;
73
+ } else if (value[i] === '\n') lf += 1;
74
+ }
75
+ const total = crlf + lf + cr;
76
+ const styles = [crlf > 0, lf > 0, cr > 0].filter(Boolean).length;
77
+ const style = crlf >= lf && crlf >= cr && crlf > 0 ? '\r\n' : lf > 0 ? '\n' : cr > 0 ? '\r' : null;
78
+ return Object.freeze({ style, crlf, lf, cr, total, mixed: styles > 1 });
79
+ }
80
+
81
+ export function normalizeLineEndings(text, target) {
82
+ const lf = String(text ?? '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
83
+ if (target === '\r\n') return lf.replace(/\n/g, '\r\n');
84
+ if (target === '\r') return lf.replace(/\n/g, '\r');
85
+ return lf;
86
+ }
87
+
88
+ export function createReadinessTracker({maxEvidenceBytes=16384}={}) {
89
+ if (!Number.isInteger(maxEvidenceBytes) || maxEvidenceBytes < 256) {
90
+ throw new Error('maxEvidenceBytes must be >= 256');
91
+ }
92
+ let stdout = '';
93
+ let stderr = '';
94
+ let sequence = 0;
95
+ let last = analyzeReadiness();
96
+
97
+ const append = (channel, chunk) => {
98
+ const value = normalizeProcessText(chunk);
99
+ if (channel === 'stderr') stderr = (stderr + value).slice(-maxEvidenceBytes);
100
+ else stdout = (stdout + value).slice(-maxEvidenceBytes);
101
+ sequence += 1;
102
+ last = analyzeReadiness({stdout, stderr});
103
+ return Object.freeze({sequence, channel, readiness: last});
104
+ };
105
+
106
+ const updateTerminal = (state, exitCode) => {
107
+ last = analyzeReadiness({stdout, stderr, state, exitCode});
108
+ sequence += 1;
109
+ return Object.freeze({sequence, readiness: last});
110
+ };
111
+
112
+ const snapshot = () => Object.freeze({
113
+ sequence,
114
+ readiness: last,
115
+ stdout_tail: stdout.slice(-2048),
116
+ stderr_tail: stderr.slice(-2048)
117
+ });
118
+
119
+ return Object.freeze({append, updateTerminal, snapshot});
120
+ }