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.
- package/MANIFEST.SHA256 +65 -27
- package/PROVENANCE.json +6 -6
- package/README.txt +6 -6
- package/RELEASE.json +58 -53
- package/docs/COMMANDER_EXHAUSTIVE_SURFACE.json +49 -0
- package/docs/COMMANDER_REFERENCE_INVENTORY.json +70 -0
- package/docs/R47-FULL-TOOL-CATALOG.json +123 -0
- package/package.json +5 -2
- package/scripts/commander-hardening-gate-r47.mjs +35 -0
- package/scripts/commander-parity-benchmark.mjs +6 -0
- package/scripts/commander-real-performance-benchmark-r47.mjs +41 -0
- package/scripts/deploy-r47.mjs +67 -8
- package/scripts/full-regression-r47.mjs +28 -0
- package/scripts/inventory-reference-dist.mjs +17 -0
- package/scripts/npm-release-r47.mjs +25 -1
- package/scripts/r47-syntax-check.mjs +6 -0
- package/scripts/r47-wire-remote-plane.mjs +23 -0
- package/src/atomic-mutation.mjs +59 -29
- package/src/commander-compat-contracts.mjs +90 -0
- package/src/commander-compat-runtime.mjs +91 -0
- package/src/commander-core-bridge.mjs +30 -0
- package/src/commander-extended-runtime.mjs +74 -0
- package/src/commander-observability.mjs +33 -0
- package/src/commander-remote-bridge-r47.mjs +41 -0
- package/src/commander-remote-relay-r47.mjs +44 -0
- package/src/machine-fs.mjs +108 -22
- package/src/mcp-server.mjs +94 -0
- package/src/observation-output-contracts.mjs +9 -8
- package/src/observation-runtime.mjs +47 -17
- package/src/operation-history.mjs +95 -0
- package/src/operation-lifecycle.mjs +63 -0
- package/src/process-output-contracts.mjs +24 -1
- package/src/process-readiness.mjs +120 -0
- package/src/process-runtime.mjs +178 -6
- package/src/release-slot.mjs +296 -293
- package/src/runtime-update.mjs +61 -7
- package/src/tool-audit.mjs +15 -2
- package/src/version.mjs +1 -2
- package/test/OBSERVED-COMMANDER-TOOLS.json +1 -0
- package/test/agent-runtime.test.mjs +1 -1
- package/test/autonomous-runtime.test.mjs +1 -1
- package/test/coding-plane.test.mjs +1 -1
- package/test/coding-search-r36.test.mjs +14 -0
- package/test/commander-compat-runtime.test.mjs +34 -0
- package/test/commander-core-bridge-r47.test.mjs +31 -0
- package/test/commander-exhaustive-surface-r47.test.mjs +32 -0
- package/test/commander-extended-runtime-r47.test.mjs +46 -0
- package/test/commander-observability-r47.test.mjs +34 -0
- package/test/commander-parity-gate-r47.test.mjs +49 -0
- package/test/commander-protocol-r47.test.mjs +30 -0
- package/test/commander-real-parity-r47.test.mjs +121 -0
- package/test/commander-remote-bridge-r47.test.mjs +39 -0
- package/test/commander-remote-plane-r47.test.mjs +25 -0
- package/test/commander-torture-r47.test.mjs +146 -0
- package/test/full-max-catalog-r46.test.mjs +1 -1
- package/test/full-max-catalog-r47.test.mjs +78 -0
- package/test/machine-fs-runtime.test.mjs +97 -0
- package/test/observation-runtime.test.mjs +28 -0
- package/test/operation-history.test.mjs +61 -0
- package/test/operation-lifecycle.test.mjs +72 -0
- package/test/output-schema.test.mjs +1 -1
- package/test/process-readiness.test.mjs +32 -0
- package/test/process-runtime.test.mjs +147 -0
- package/test/r47-release-contract.test.mjs +37 -0
- package/test/release-activation-gate.test.mjs +30 -0
- package/test/release-version.test.mjs +1 -1
package/src/mcp-server.mjs
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
228
|
-
|
|
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
|
|
246
|
-
if(cursor!==null){
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
+
}
|