deadbyte-mcp 0.15.18 → 0.15.20
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 +56 -34
- package/PROVENANCE.json +13 -7
- package/README.txt +7 -7
- package/RELEASE.json +30 -27
- package/SBOM.cdx.json +3 -3
- package/TEST-MATRIX.json +27 -7
- package/controller/deadbyte-controller.ps1 +56 -1
- package/docs/COMMANDER_EXHAUSTIVE_SURFACE.json +247 -44
- package/docs/COMMANDER_REFERENCE_0_2_51.json +46 -0
- package/docs/R47-PROVENANCE.json +11 -0
- package/docs/R47-RELEASE.json +58 -0
- package/docs/plans/2026-09-27-desktop-commander-drop-in-parity.md +92 -0
- package/package.json +4 -2
- package/proof/commander-closure-20260927/CONTINUATION.json +43 -0
- package/proof/commander-closure-20260927/RESULT.json +30 -0
- package/proof/commander-closure-20260927/SEARCH-CONTINUATION.json +30 -0
- package/proof/commander-closure-20260927/XLS-REFERENCE-PROBE.json +13 -0
- package/scripts/deploy-r48.mjs +86 -0
- package/scripts/full-regression-r48.mjs +28 -0
- package/src/coding-search.mjs +65 -7
- package/src/coding-task-runtime.mjs +1 -1
- package/src/commander-compat-contracts.mjs +10 -0
- package/src/commander-core-bridge.mjs +2 -2
- package/src/commander-extended-bridge-r47.mjs +116 -12
- package/src/commander-extended-runtime.mjs +33 -6
- package/src/commander-external-process-r48.mjs +125 -0
- package/src/commander-observability.mjs +66 -16
- package/src/commander-office-reader-r48.mjs +548 -0
- package/src/commander-pdf-r48.mjs +448 -0
- package/src/commander-ui-resources.mjs +26 -0
- package/src/commander-url-reader-r48.mjs +201 -0
- package/src/commander-wire-adapter-r47.mjs +595 -101
- package/src/deadbyte-cli.mjs +73 -8
- package/src/mcp-server.mjs +9 -2
- package/src/observation-output-contracts.mjs +2 -1
- package/src/observation-runtime.mjs +4 -2
- package/src/version.mjs +1 -1
- package/test/coding-runtime.test.mjs +7 -0
- package/test/coding-search-r36.test.mjs +71 -0
- package/test/commander-core-bridge-r47.test.mjs +32 -3
- package/test/commander-exhaustive-surface-r47.test.mjs +5 -1
- package/test/commander-extended-runtime-r47.test.mjs +8 -0
- package/test/commander-extended-wire-r48.test.mjs +98 -0
- package/test/commander-external-process-r48.test.mjs +76 -0
- package/test/commander-office-reader-r48.test.mjs +152 -0
- package/test/commander-pdf-r48.test.mjs +103 -0
- package/test/commander-protocol-r47.test.mjs +23 -1
- package/test/commander-reference-contract-r48.test.mjs +26 -0
- package/test/commander-schema-differential-r48.test.mjs +88 -0
- package/test/commander-url-reader-r48.test.mjs +68 -0
- package/test/commander-wire-contract-r47.test.mjs +461 -41
- package/test/controller-r212.test.mjs +23 -2
- package/test/observation-runtime.test.mjs +4 -3
- package/test/prompts-resources.test.mjs +1 -1
- package/test/r47-release-contract.test.mjs +9 -8
- package/test/r48-release-contract.test.mjs +36 -0
- package/test/remote-session-cli.test.mjs +34 -1
package/src/deadbyte-cli.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { closeSync, openSync } from 'node:fs';
|
|
2
3
|
import { lstat, mkdir, open, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { spawn, spawnSync } from 'node:child_process';
|
|
4
5
|
import { createHash, randomBytes } from 'node:crypto';
|
|
@@ -10,6 +11,7 @@ import { defaultPublicKeyPath } from './trust.mjs';
|
|
|
10
11
|
import { verifyAuditChain } from './audit-verify.mjs';
|
|
11
12
|
import { openRemoteResultJournal, parseRemoteResultCursor } from './remote-result-journal.mjs';
|
|
12
13
|
import { DEADBYTE_VERSION } from './version.mjs';
|
|
14
|
+
import { parseActivePointer } from './release-slot.mjs';
|
|
13
15
|
import { printDoctor } from './doctor-r45.mjs';
|
|
14
16
|
import { createLiveTimelineTracker, formatCompactAuditEvent, formatRemoteLifecycle, formatRemoteToolCall, formatRuntimeDiagnosticLine } from './remote-live-log.mjs';
|
|
15
17
|
import { prepareRemoteConsole, remoteDisplayText } from './remote-console.mjs';
|
|
@@ -60,14 +62,62 @@ async function remoteContext(){
|
|
|
60
62
|
const powershell=await requireRegular(path.join(systemRoot,'System32','WindowsPowerShell','v1.0','powershell.exe'),'Windows PowerShell');
|
|
61
63
|
return {root,launcher,powershell};
|
|
62
64
|
}
|
|
63
|
-
function
|
|
65
|
+
function boundedSingleLine(value,maxChars=2048){
|
|
66
|
+
const line=String(value??'').replace(/[\r\n\t]+/g,' ').replace(/\s{2,}/g,' ').trim();
|
|
67
|
+
return line.length<=maxChars?line:`${line.slice(0,maxChars)}...`;
|
|
68
|
+
}
|
|
69
|
+
async function readActiveRuntimeBinding(root){
|
|
70
|
+
const file=path.join(root,'active-release.json');
|
|
71
|
+
try{return parseActivePointer(await readFile(file,'utf8'));}
|
|
72
|
+
catch(error){if(error?.code==='ENOENT') return null;throw new Error(`active runtime pointer invalid: ${boundedSingleLine(error?.message||error,512)}`);}
|
|
73
|
+
}
|
|
74
|
+
async function reportVersionCoherence(root,{displayMode='ascii'}={}){
|
|
75
|
+
const active=await readActiveRuntimeBinding(root);
|
|
76
|
+
if(!active||active.version===DEADBYTE_VERSION) return active;
|
|
77
|
+
writeRemote(process.stderr,`${remoteTag('warn',displayMode)} VERSION SKEW: CLI package ${DEADBYTE_VERSION}; active runtime ${active.version} (${active.release_id})\n`,displayMode);
|
|
78
|
+
return active;
|
|
79
|
+
}
|
|
80
|
+
async function readControllerStartFailure(root,command,{maxChars=2048}={}){
|
|
81
|
+
const file=path.join(root,'runtime','controller-start-failure.json');
|
|
82
|
+
let raw;
|
|
83
|
+
try{raw=await readFile(file,'utf8');}catch(error){if(error?.code==='ENOENT') return null;throw error;}
|
|
84
|
+
let value;
|
|
85
|
+
try{value=JSON.parse(String(raw).replace(/^\uFEFF/,''));}catch{return null;}
|
|
86
|
+
if(value?.schema!=='deadbyte.controller-start-failure.v1'||String(value?.command||'')!==command||typeof value?.message!=='string') return null;
|
|
87
|
+
const message=boundedSingleLine(value.message,maxChars);
|
|
88
|
+
return message?{file,message}:null;
|
|
89
|
+
}
|
|
90
|
+
async function readTailBounded(file,maxBytes=4096){
|
|
91
|
+
try{
|
|
92
|
+
const info=await stat(file);
|
|
93
|
+
const length=Math.min(info.size,maxBytes);
|
|
94
|
+
if(length===0) return '';
|
|
95
|
+
const buffer=Buffer.alloc(length);
|
|
96
|
+
const handle=await open(file,'r');
|
|
97
|
+
try{await handle.read(buffer,0,length,info.size-length);}finally{await handle.close();}
|
|
98
|
+
return boundedSingleLine(buffer.toString('utf8'),maxBytes);
|
|
99
|
+
}catch(error){if(error?.code==='ENOENT') return '';throw error;}
|
|
100
|
+
}
|
|
101
|
+
function invokeController(ctx,command,{capture=false,allowFailure=false,echoCaptured=true,timeoutMs=null,discardOutput=false,redirectOutput=null}={}){
|
|
64
102
|
if(capture&&discardOutput) throw new Error('invokeController cannot capture and discard output together');
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
103
|
+
if(capture&&redirectOutput) throw new Error('invokeController cannot capture and redirect output together');
|
|
104
|
+
if(discardOutput&&redirectOutput) throw new Error('invokeController cannot discard and redirect output together');
|
|
105
|
+
let stdoutFd=null,stderrFd=null,result;
|
|
106
|
+
try{
|
|
107
|
+
let stdio;
|
|
108
|
+
if(discardOutput) stdio=['ignore','ignore','ignore'];
|
|
109
|
+
else if(redirectOutput){
|
|
110
|
+
stdoutFd=openSync(redirectOutput.stdout_path,'w');
|
|
111
|
+
stderrFd=openSync(redirectOutput.stderr_path,'w');
|
|
112
|
+
stdio=['ignore',stdoutFd,stderrFd];
|
|
113
|
+
}else stdio=capture?['ignore','pipe','pipe']:'inherit';
|
|
114
|
+
const options={stdio,encoding:capture?'utf8':undefined,windowsHide:false,shell:false};
|
|
115
|
+
if(Number.isInteger(timeoutMs)&&timeoutMs>0) options.timeout=timeoutMs;
|
|
116
|
+
result=spawnSync(ctx.powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',ctx.launcher,command],options);
|
|
117
|
+
}finally{
|
|
118
|
+
if(stdoutFd!==null) closeSync(stdoutFd);
|
|
119
|
+
if(stderrFd!==null) closeSync(stderrFd);
|
|
120
|
+
}
|
|
71
121
|
if(result.error){
|
|
72
122
|
if(result.error?.code==='ETIMEDOUT') throw new Error(`remote controller '${command}' timed out after ${timeoutMs}ms`);
|
|
73
123
|
throw result.error;
|
|
@@ -295,6 +345,7 @@ async function runRemoteSession({resumeCursor=null,verbose=false}={}){
|
|
|
295
345
|
const display=prepareRemoteConsole();
|
|
296
346
|
const displayMode=display.mode;
|
|
297
347
|
const ctx=await remoteContext();
|
|
348
|
+
await reportVersionCoherence(ctx.root,{displayMode});
|
|
298
349
|
if(displayMode==='ascii') writeRemote(process.stdout,`[DISPLAY] ASCII fallback: ${display.reason}\n`,displayMode);
|
|
299
350
|
if(resumeCursor!==null) await replayRemoteResults(ctx.root,resumeCursor,{displayMode});
|
|
300
351
|
await recoverRemoteSession(ctx,{displayMode});
|
|
@@ -324,7 +375,20 @@ async function runRemoteSession({resumeCursor=null,verbose=false}={}){
|
|
|
324
375
|
}else{
|
|
325
376
|
const recovery=preflight.status===0&&controllerRuntimeDegraded(preflight.stdout);
|
|
326
377
|
process.stdout.write(recovery?'[RECOVERY] Runtime DEGRADED; restarting verified owned components...\n':'[BOOT] Starting bridge/runtime...\n');
|
|
327
|
-
|
|
378
|
+
const startCommand=recovery?'restart':'start';
|
|
379
|
+
const controllerLogDir=path.join(ctx.root,'runtime','logs');
|
|
380
|
+
await mkdir(controllerLogDir,{recursive:true});
|
|
381
|
+
const launchOutput={
|
|
382
|
+
stdout_path:path.join(controllerLogDir,`controller-${startCommand}.stdout.log`),
|
|
383
|
+
stderr_path:path.join(controllerLogDir,`controller-${startCommand}.stderr.log`)
|
|
384
|
+
};
|
|
385
|
+
const launch=invokeController(ctx,startCommand,{allowFailure:true,redirectOutput:launchOutput,timeoutMs:REMOTE_START_TIMEOUT_MS});
|
|
386
|
+
if(launch.status!==0){
|
|
387
|
+
const evidence=await readControllerStartFailure(ctx.root,startCommand);
|
|
388
|
+
const fallback=evidence?'':await readTailBounded(launchOutput.stderr_path,2048);
|
|
389
|
+
const detail=evidence?`; controller-start-failure: ${evidence.message}`:(fallback?`; controller-stderr: ${fallback}`:'');
|
|
390
|
+
throw new Error(`remote controller '${startCommand}' failed exit=${launch.status}${detail}`);
|
|
391
|
+
}
|
|
328
392
|
const started=invokeController(ctx,'status',{capture:true,echoCaptured:false,timeoutMs:REMOTE_STATUS_TIMEOUT_MS});
|
|
329
393
|
if(started.status!==0||!controllerRuntimeReady(started.stdout)) throw new Error(`remote controller ${recovery?'restart':'start'} returned without a verified ONLINE runtime`);
|
|
330
394
|
startText=started.stdout||'';
|
|
@@ -371,6 +435,7 @@ async function runRemote(args){
|
|
|
371
435
|
if(filtered.length>1) throw new Error('remote accepts one controller command or --resume <cursor>');
|
|
372
436
|
const command=filtered[0];if(!REMOTE_COMMANDS.has(command)) throw new Error(`unsupported remote command '${command}'`);
|
|
373
437
|
const ctx=await remoteContext();
|
|
438
|
+
await reportVersionCoherence(ctx.root,{displayMode:'ascii'});
|
|
374
439
|
const result=invokeController(ctx,command,{allowFailure:true});
|
|
375
440
|
process.exitCode=result.status;
|
|
376
441
|
}
|
package/src/mcp-server.mjs
CHANGED
|
@@ -51,11 +51,13 @@ 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 { registerCommanderUiResources } from './commander-ui-resources.mjs';
|
|
54
55
|
import { createOperationHistory } from './operation-history.mjs';
|
|
55
56
|
import { createCommanderObservability } from './commander-observability.mjs';
|
|
56
|
-
import { registerCommanderExtendedBridge } from './commander-extended-bridge-r47.mjs';
|
|
57
|
+
import { registerCommanderExtendedBridge, registerCommanderHiddenUiDispatch } from './commander-extended-bridge-r47.mjs';
|
|
57
58
|
import { createCommanderExtendedRuntime } from './commander-extended-runtime.mjs';
|
|
58
59
|
import { registerCommanderCoreBridge } from './commander-core-bridge.mjs';
|
|
60
|
+
import { createCommanderExternalProcessRuntime } from './commander-external-process-r48.mjs';
|
|
59
61
|
import { registerCommanderRemoteBridge } from './commander-remote-bridge-r47.mjs';
|
|
60
62
|
import { createCommanderRemoteRelayR47 } from './commander-remote-relay-r47.mjs';
|
|
61
63
|
import { createRemoteDevicePlaneR44 } from './remote-device-r44.mjs';
|
|
@@ -246,6 +248,9 @@ const skillRoot=path.join(runtimeStateRoot,'skills');
|
|
|
246
248
|
await mkdir(skillRoot,{recursive:true});
|
|
247
249
|
const skillRuntime=await createSkillPlane({root:skillRoot,verifyKeyPath});
|
|
248
250
|
const commanderExtendedRuntime=createCommanderExtendedRuntime({stateRoot:runtimeStateRoot});
|
|
251
|
+
const commanderExternalProcessRuntime=observationRuntime?createCommanderExternalProcessRuntime({
|
|
252
|
+
observationRuntime,hostPolicy,hostExecInlineFn:hostExecInline
|
|
253
|
+
}):null;
|
|
249
254
|
const commanderHistory=createOperationHistory({maxEvents:1024,maxBytes:1024*1024});
|
|
250
255
|
const commanderObservability=createCommanderObservability({history:commanderHistory});
|
|
251
256
|
const remoteRelayEndpoint=process.env.DEADBYTE_REMOTE_RELAY_ENDPOINT||process.env.DEADBYTE_REMOTE_ENDPOINT||'';
|
|
@@ -604,9 +609,11 @@ serveStdio(() => {
|
|
|
604
609
|
}
|
|
605
610
|
|
|
606
611
|
if (surfaceProfile === 'full') {
|
|
607
|
-
registerCommanderCoreBridge(toolServer,{machineFsRuntime,processRuntime,searchRuntime:codingRuntime?.search??null,extendedRuntime:commanderExtendedRuntime,includeMarkdownEditor:true});
|
|
612
|
+
registerCommanderCoreBridge(toolServer,{machineFsRuntime,processRuntime,searchRuntime:codingRuntime?.search??null,extendedRuntime:commanderExtendedRuntime,externalProcessRuntime:commanderExternalProcessRuntime,includeMarkdownEditor:true});
|
|
608
613
|
registerCommanderExtendedBridge(toolServer,{extendedRuntime:commanderExtendedRuntime,observability:commanderObservability,skillPlane:skillRuntime});
|
|
609
614
|
registerCommanderRemoteBridge(toolServer,{remoteDevicePlane});
|
|
615
|
+
registerCommanderHiddenUiDispatch(server);
|
|
616
|
+
registerCommanderUiResources(server);
|
|
610
617
|
}
|
|
611
618
|
|
|
612
619
|
return server;
|
|
@@ -8,7 +8,8 @@ const evidenceShape={request_sha256:hex64,result_head_sha256:hex64,receipt_id:re
|
|
|
8
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
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)});
|
|
10
10
|
const processSchema=z.strictObject({pid:z.number().int().positive(),parent_pid:z.number().int().nonnegative(),name:z.string().max(260),
|
|
11
|
-
ownership:z.enum(['deadbyte-runtime','deadbyte-session','external']),
|
|
11
|
+
ownership:z.enum(['deadbyte-runtime','deadbyte-session','external']),creation_time_utc:z.string().min(1).nullable(),
|
|
12
|
+
executable_path:z.string().max(2048).nullable(),command_line:z.string().max(4096).nullable()});
|
|
12
13
|
|
|
13
14
|
function statusContract(shape,successRequired=[]){
|
|
14
15
|
return z.strictObject({status:z.enum(['ok','failed']),error:z.string().optional(),...shape}).superRefine((value,ctx)=>{
|
|
@@ -91,12 +91,13 @@ export async function verifyObservationReceiptFile({policy,verifyKeyPath,receipt
|
|
|
91
91
|
function defaultProcessSnapshotProvider(){
|
|
92
92
|
if(process.platform!=='win32') return [];
|
|
93
93
|
const exe=path.join(process.env.SystemRoot||'C:\\Windows','System32','WindowsPowerShell','v1.0','powershell.exe');
|
|
94
|
-
const command="Get-CimInstance Win32_Process |
|
|
94
|
+
const command="$ErrorActionPreference='Stop'; Get-CimInstance Win32_Process | ForEach-Object { $created=$null; try { if ($_.CreationDate) { $created=([DateTime]$_.CreationDate).ToUniversalTime().ToString('o') } } catch {}; [pscustomobject]@{ ProcessId=$_.ProcessId; ParentProcessId=$_.ParentProcessId; Name=$_.Name; CreationTimeUtc=$created; ExecutablePath=$_.ExecutablePath; CommandLine=$_.CommandLine } } | ConvertTo-Json -Compress";
|
|
95
95
|
const result=spawnSync(exe,['-NoLogo','-NoProfile','-Command',command],{encoding:'utf8',windowsHide:true,timeout:15000,maxBuffer:8*1024*1024});
|
|
96
96
|
if(result.status!==0) throw new Error(`process observation failed: ${result.stderr||result.stdout}`);
|
|
97
97
|
const text=(result.stdout??'').trim(); if(!text) return [];
|
|
98
98
|
const parsed=JSON.parse(text); const rows=Array.isArray(parsed)?parsed:[parsed];
|
|
99
99
|
return rows.map(item=>({pid:Number(item.ProcessId),parent_pid:Number(item.ParentProcessId),name:String(item.Name??''),
|
|
100
|
+
creation_time_utc:typeof item.CreationTimeUtc==='string'&&Number.isFinite(Date.parse(item.CreationTimeUtc))?new Date(item.CreationTimeUtc).toISOString():null,
|
|
100
101
|
executable_path:typeof item.ExecutablePath==='string'?item.ExecutablePath:null,
|
|
101
102
|
command_line:typeof item.CommandLine==='string'?item.CommandLine:null}));
|
|
102
103
|
}
|
|
@@ -287,7 +288,8 @@ export function createObservationRuntime({policy,signingKeyPath,verifyKeyPath,pr
|
|
|
287
288
|
const processes=rows.filter(item=>Number.isInteger(item.pid)&&item.pid>0).map(item=>{
|
|
288
289
|
const ownership=sessionPids.has(item.pid)?'deadbyte-session':runtimePids.has(item.pid)?'deadbyte-runtime':'external';
|
|
289
290
|
if(ownership==='external'&&!includeExternal) return null;
|
|
290
|
-
|
|
291
|
+
const creationTime=typeof item.creation_time_utc==='string'&&Number.isFinite(Date.parse(item.creation_time_utc))?new Date(item.creation_time_utc).toISOString():null;
|
|
292
|
+
return {pid:item.pid,parent_pid:Number.isInteger(item.parent_pid)?item.parent_pid:0,name:String(item.name??'').slice(0,260),ownership,creation_time_utc:creationTime,
|
|
291
293
|
executable_path:ownership==='external'?null:(typeof item.executable_path==='string'?item.executable_path.slice(0,2048):null),
|
|
292
294
|
command_line:ownership==='external'?null:(typeof item.command_line==='string'?item.command_line.slice(0,4096):null)};
|
|
293
295
|
}).filter(Boolean).sort((a,b)=>a.pid-b.pid||a.name.localeCompare(b.name));
|
package/src/version.mjs
CHANGED
|
@@ -158,6 +158,13 @@ test('durable coding task chains edit command benchmark and verifies task receip
|
|
|
158
158
|
assert.equal(page.stream_id, submitted.task_id);
|
|
159
159
|
assert.equal(page.events.length, 3);
|
|
160
160
|
assert.equal(page.events[0].seq, 4);
|
|
161
|
+
assert.equal(page.events[0].schema, 'deadbyte.coding-task-event.v1');
|
|
162
|
+
assert.equal(page.events[0].task_id, submitted.task_id);
|
|
163
|
+
assert.match(page.events[0].created_at_utc, /^\d{4}-\d{2}-\d{2}T/);
|
|
164
|
+
assert.match(page.events[0].prev_sha256, /^[0-9a-f]{64}$/);
|
|
165
|
+
assert.equal(page.events[0].attestation.algorithm, 'ed25519');
|
|
166
|
+
assert.match(page.events[0].attestation.key_id, /^[0-9a-f]{64}$/);
|
|
167
|
+
assert.equal(typeof page.events[0].attestation.signature_base64, 'string');
|
|
161
168
|
assert.equal(page.events[2].seq, 6);
|
|
162
169
|
const verified = await f.tasks.verify({ taskId: submitted.task_id });
|
|
163
170
|
assert.equal(verified.ok, true);
|
|
@@ -29,6 +29,77 @@ async function fixture(){
|
|
|
29
29
|
return {temp,state,search,get reads(){return reads;},cleanup:()=>rm(temp,{recursive:true,force:true})};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
test('Commander hidden and file-pattern flags affect native search and cursor identity',async()=>{
|
|
33
|
+
const f=await fixture();
|
|
34
|
+
try{
|
|
35
|
+
f.state.set('src/.hidden.txt',Buffer.from('foo'));
|
|
36
|
+
f.state.set('.cache/item.txt',Buffer.from('foo'));
|
|
37
|
+
const query={rootId:'core',target:'content',mode:'literal',pattern:'foo',maxResults:20,pageSize:20};
|
|
38
|
+
const visible=await f.search({...query,includeHidden:false,filePattern:'*.txt'});
|
|
39
|
+
assert.ok(visible.matches.length>0);
|
|
40
|
+
assert.ok(visible.matches.every(x=>!x.path.split('/').some(p=>p.startsWith('.'))&&x.path.endsWith('.txt')));
|
|
41
|
+
const hidden=await f.search({...query,includeHidden:true,filePattern:'*.txt'});
|
|
42
|
+
assert.ok(hidden.matches.some(x=>x.path==='src/.hidden.txt'));
|
|
43
|
+
const js=await f.search({...query,includeHidden:true,filePattern:'*.js'});
|
|
44
|
+
assert.equal(js.matches.length,0);
|
|
45
|
+
assert.notEqual(visible.query_sha256,hidden.query_sha256);
|
|
46
|
+
}finally{await f.cleanup();}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('native search abort prevents further reads after an in-flight file returns',async()=>{
|
|
50
|
+
const controller=new AbortController();let reads=0;
|
|
51
|
+
const search=createCodingSearch({
|
|
52
|
+
tree:async()=>({entries:[{path:'a.txt',type:'file',bytes:1},{path:'b.txt',type:'file',bytes:1}]}),
|
|
53
|
+
fileRead:async()=>{reads++;controller.abort();return {content:Buffer.from('x').toString('base64'),bytes:1};},
|
|
54
|
+
cursorCodec:{encode:async()=>'',decode:async()=>({})}
|
|
55
|
+
});
|
|
56
|
+
await assert.rejects(()=>search({rootId:'core',pattern:'x',signal:controller.signal}),/cancelled/);
|
|
57
|
+
assert.equal(reads,1);
|
|
58
|
+
await assert.rejects(()=>search({rootId:'core',pattern:'x',signal:controller.signal}),/cancelled/);
|
|
59
|
+
assert.equal(reads,1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('Commander file search treats glob and exact filename as filenames, not content regex',async()=>{
|
|
63
|
+
const f=await fixture();
|
|
64
|
+
try{
|
|
65
|
+
f.state.set('src/not-a.txt.backup',Buffer.from('x'));
|
|
66
|
+
const glob=await f.search({rootId:'core',target:'path',mode:'literal',pathGlob:true,pattern:'*.js',maxResults:20});
|
|
67
|
+
assert.deepEqual(glob.matches.map(x=>x.path),['src/foo-file.js']);
|
|
68
|
+
const exact=await f.search({rootId:'core',target:'path',mode:'literal',pathGlob:true,pattern:'a.txt',maxResults:20});
|
|
69
|
+
assert.deepEqual(exact.matches.map(x=>x.path),['src/a.txt']);
|
|
70
|
+
assert.equal(f.reads,0);
|
|
71
|
+
}finally{await f.cleanup();}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('Commander context lines are ordered, deduplicated and query-bound',async()=>{
|
|
75
|
+
const f=await fixture();
|
|
76
|
+
try{
|
|
77
|
+
f.state.clear();f.state.set('a.txt',Buffer.from('before\r\nfoo foo\r\nafter\r\n'));
|
|
78
|
+
const q={rootId:'core',pattern:'foo',target:'content',mode:'literal',maxResults:20};
|
|
79
|
+
const context=await f.search({...q,contextLines:1});
|
|
80
|
+
assert.deepEqual(context.matches.map(x=>[x.line,x.preview]),[[1,'before'],[2,'foo'],[3,'after']]);
|
|
81
|
+
const zero=await f.search({...q,contextLines:0});
|
|
82
|
+
assert.deepEqual(zero.matches.map(x=>[x.line,x.preview]),[[2,'foo']]);
|
|
83
|
+
assert.notEqual(context.query_sha256,zero.query_sha256);
|
|
84
|
+
}finally{await f.cleanup();}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('Commander exact filename early termination returns the first deterministic match unless disabled',async()=>{
|
|
88
|
+
const f=await fixture();
|
|
89
|
+
try{
|
|
90
|
+
f.state.clear();
|
|
91
|
+
f.state.set('a/target.txt',Buffer.from('a'));
|
|
92
|
+
f.state.set('b/target.txt',Buffer.from('b'));
|
|
93
|
+
f.state.set('c/other.txt',Buffer.from('c'));
|
|
94
|
+
const q={rootId:'core',target:'path',mode:'literal',pathGlob:true,pattern:'target.txt',maxResults:20};
|
|
95
|
+
const early=await f.search({...q,earlyTermination:true});
|
|
96
|
+
assert.deepEqual(early.matches.map(x=>x.path),['a/target.txt']);
|
|
97
|
+
const exhaustive=await f.search({...q,earlyTermination:false});
|
|
98
|
+
assert.deepEqual(exhaustive.matches.map(x=>x.path),['a/target.txt','b/target.txt']);
|
|
99
|
+
assert.notEqual(early.query_sha256,exhaustive.query_sha256);
|
|
100
|
+
}finally{await f.cleanup();}
|
|
101
|
+
});
|
|
102
|
+
|
|
32
103
|
test('R36 regex search returns multiple matches per file with deterministic signed paging',async()=>{
|
|
33
104
|
const f=await fixture();
|
|
34
105
|
try{
|
|
@@ -2,6 +2,19 @@ import test from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { registerCommanderCoreBridge } from '../src/commander-core-bridge.mjs';
|
|
4
4
|
|
|
5
|
+
test('search adapter drains bounded native pages instead of silently losing later matches',async()=>{
|
|
6
|
+
const tools=new Map(), cursors=[];
|
|
7
|
+
const searchRuntime=async query=>{cursors.push(query.cursor);return query.cursor===null
|
|
8
|
+
?{matches:[{path:'a.txt'}],next_cursor:'page-2'}
|
|
9
|
+
:{matches:[{path:'b.txt'}],next_cursor:null};};
|
|
10
|
+
registerCommanderCoreBridge({registerTool(name,config,handler){tools.set(name,{config,handler});}},{searchRuntime});
|
|
11
|
+
const started=await tools.get('start_search').handler({path:'.',pattern:'.',searchType:'files',maxResults:10});
|
|
12
|
+
let page;
|
|
13
|
+
for(let i=0;i<20;i++){page=await tools.get('get_more_search_results').handler({sessionId:started.sessionId,offset:0,length:10});if(page.isComplete)break;await new Promise(r=>setTimeout(r,1));}
|
|
14
|
+
assert.deepEqual(cursors,[null,'page-2']);
|
|
15
|
+
assert.deepEqual(page.results.map(x=>x.file),['a.txt','b.txt']);
|
|
16
|
+
});
|
|
17
|
+
|
|
5
18
|
test('bridge registers all aliases',()=>{
|
|
6
19
|
const names=[]; registerCommanderCoreBridge({registerTool(name){names.push(name);}},{}); assert.equal(names.length,20);
|
|
7
20
|
assert.ok(names.includes('read_file')); assert.ok(names.includes('start_search'));
|
|
@@ -20,12 +33,28 @@ test('search adapter provides Commander progressive session semantics',async()=>
|
|
|
20
33
|
throw new Error('search session did not complete');
|
|
21
34
|
});
|
|
22
35
|
|
|
23
|
-
test('search adapter
|
|
24
|
-
const tools=new Map(); let resolveSearch; const searchRuntime=
|
|
36
|
+
test('search adapter cancellation aborts native work and rejects late results',async()=>{
|
|
37
|
+
const tools=new Map(); let resolveSearch,signal; const searchRuntime=query=>{signal=query.signal;return new Promise(r=>{resolveSearch=r;});};
|
|
25
38
|
registerCommanderCoreBridge({registerTool(name,config,handler){tools.set(name,{config,handler});}},{searchRuntime});
|
|
26
39
|
const started=await tools.get('start_search').handler({path:'.',pattern:'x',searchType:'files'});
|
|
27
40
|
assert.equal(await tools.get('stop_search').handler({sessionId:started.sessionId}),true);
|
|
41
|
+
assert.equal(signal.aborted,true);
|
|
42
|
+
assert.equal(await tools.get('stop_search').handler({sessionId:started.sessionId}),false);
|
|
28
43
|
resolveSearch({matches:[{path:'z.txt'}],truncated:false});
|
|
29
44
|
let page; for(let i=0;i<20;i++){page=await tools.get('get_more_search_results').handler({sessionId:started.sessionId,offset:-1,length:100}); if(page.isComplete) break; await new Promise(r=>setTimeout(r,1));}
|
|
30
|
-
assert.equal(page.isComplete,true); assert.equal(page.results.length,
|
|
45
|
+
assert.equal(page.isComplete,true); assert.equal(page.results.length,0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('search adapter timeout aborts native work, completes cleanly, and rejects late results',async()=>{
|
|
49
|
+
const tools=new Map(); let resolveSearch,signal; const searchRuntime=query=>{signal=query.signal;return new Promise(r=>{resolveSearch=r;});};
|
|
50
|
+
registerCommanderCoreBridge({registerTool(name,config,handler){tools.set(name,{config,handler});}},{searchRuntime});
|
|
51
|
+
const started=await tools.get('start_search').handler({path:'.',pattern:'needle',searchType:'content',timeout_ms:5});
|
|
52
|
+
await new Promise(r=>setTimeout(r,20));
|
|
53
|
+
assert.equal(signal.aborted,true);
|
|
54
|
+
let page=await tools.get('get_more_search_results').handler({sessionId:started.sessionId,offset:0,length:100});
|
|
55
|
+
assert.equal(page.isComplete,true); assert.equal(page.isError,false); assert.equal(page.results.length,0);
|
|
56
|
+
resolveSearch({matches:[{path:'late.txt',line:1,preview:'needle'}],truncated:false});
|
|
57
|
+
await new Promise(r=>setTimeout(r,5));
|
|
58
|
+
page=await tools.get('get_more_search_results').handler({sessionId:started.sessionId,offset:0,length:100});
|
|
59
|
+
assert.equal(page.results.length,0);
|
|
31
60
|
});
|
|
@@ -14,7 +14,11 @@ test('Commander exhaustive surface is internally consistent and natively impleme
|
|
|
14
14
|
assert.equal(new Set(catalog.surface.map(x=>x.name)).size,40);
|
|
15
15
|
assert.ok(catalog.surface.every(x=>x.status==='implemented'));
|
|
16
16
|
assert.ok(catalog.surface.every(x=>typeof x.native==='string'&&x.native.length>0));
|
|
17
|
-
assert.equal(
|
|
17
|
+
assert.equal(catalog.reference.package,'@wonderwhy-er/desktop-commander');
|
|
18
|
+
assert.equal(catalog.reference.version,'0.2.51');
|
|
19
|
+
assert.equal(catalog.reference.license,'MIT');
|
|
20
|
+
assert.equal(catalog.reference.server_sha256,'dfc303bb7dd2e8d01debac50fc43bb8644d00089bffdf2887e149497cff8b305');
|
|
21
|
+
assert.equal(catalog.reference.schemas_sha256,'f342345e34e48769dabaefe1b932f09d41e2e207b422f7794a15ee50ffa8f140');
|
|
18
22
|
});
|
|
19
23
|
|
|
20
24
|
test('Commander extended native source has no Commander dependency',async()=>{
|
|
@@ -9,12 +9,20 @@ test('Commander extended config is hash-preconditioned and bounded',async()=>{
|
|
|
9
9
|
const root=await mkdtemp(path.join(tmpdir(),'deadbyte-ext-'));
|
|
10
10
|
const rt=createCommanderExtendedRuntime({stateRoot:root});
|
|
11
11
|
const initial=await rt.getConfig();
|
|
12
|
+
for(const key of ['blockedCommands','defaultShell','allowedDirectories','fileReadLineLimit','fileWriteLineLimit','telemetryEnabled','currentClient','clientHistory','version','systemInfo'])assert.ok(Object.hasOwn(initial.config,key),`missing compatibility config key ${key}`);
|
|
13
|
+
assert.deepEqual(initial.config.allowedDirectories,[]);
|
|
14
|
+
assert.equal(initial.config.telemetryEnabled,false);
|
|
12
15
|
const committed=await rt.setConfigValue({key:'theme',value:'classic',expected_sha256:initial.config_sha256});
|
|
13
16
|
assert.equal(committed.status,'committed');
|
|
14
17
|
await assert.rejects(()=>rt.setConfigValue({key:'theme',value:'modern',expected_sha256:initial.config_sha256}),/precondition/);
|
|
15
18
|
assert.equal((await rt.getConfig({key:'theme'})).config.theme,'classic');
|
|
16
19
|
});
|
|
17
20
|
|
|
21
|
+
test('Commander prompt runtime accepts reference-valid long IDs within bounded native storage',()=>{
|
|
22
|
+
const id='x'.repeat(256),rt=createCommanderExtendedRuntime({stateRoot:'x',prompts:[{id,title:'Long',prompt:'bounded'}]});
|
|
23
|
+
assert.equal(rt.getPrompt(id).prompt.id,id);
|
|
24
|
+
});
|
|
25
|
+
|
|
18
26
|
test('Commander extended feedback is bounded and durably recorded',async()=>{
|
|
19
27
|
const root=await mkdtemp(path.join(tmpdir(),'deadbyte-ext-'));
|
|
20
28
|
const rt=createCommanderExtendedRuntime({stateRoot:root});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { createCommanderExtendedRuntime } from '../src/commander-extended-runtime.mjs';
|
|
7
|
+
import { registerCommanderExtendedBridge } from '../src/commander-extended-bridge-r47.mjs';
|
|
8
|
+
import { createOperationHistory } from '../src/operation-history.mjs';
|
|
9
|
+
import { createCommanderObservability } from '../src/commander-observability.mjs';
|
|
10
|
+
|
|
11
|
+
function fakeServer(){
|
|
12
|
+
const tools=new Map();
|
|
13
|
+
return {
|
|
14
|
+
registerTool(name,definition,handler){tools.set(name,{definition,handler});},
|
|
15
|
+
tools
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function shape(schema){return schema?._def?.shape??schema?.shape??{};}
|
|
19
|
+
|
|
20
|
+
test('R48 extended bridge exposes Desktop Commander 0.2.51 argument shapes',async()=>{
|
|
21
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r48-extended-schema-'));
|
|
22
|
+
try{
|
|
23
|
+
const runtime=createCommanderExtendedRuntime({stateRoot:root,prompts:[{id:'onb2_01',title:'Explore',prompt:'Inspect the workspace'}]});
|
|
24
|
+
const history=createOperationHistory();
|
|
25
|
+
const observability=createCommanderObservability({history});
|
|
26
|
+
const server=fakeServer();
|
|
27
|
+
registerCommanderExtendedBridge(server,{extendedRuntime:runtime,observability});
|
|
28
|
+
const getConfig=shape(server.tools.get('get_config').definition.inputSchema);
|
|
29
|
+
const setConfig=shape(server.tools.get('set_config_value').definition.inputSchema);
|
|
30
|
+
const getPrompts=shape(server.tools.get('get_prompts').definition.inputSchema);
|
|
31
|
+
const recent=shape(server.tools.get('get_recent_tool_calls').definition.inputSchema);
|
|
32
|
+
const feedback=shape(server.tools.get('give_feedback_to_desktop_commander').definition.inputSchema);
|
|
33
|
+
assert.ok(getConfig.origin); assert.equal(getConfig.key,undefined);
|
|
34
|
+
assert.ok(setConfig.key); assert.ok(setConfig.value); assert.ok(setConfig.origin); assert.equal(setConfig.expected_sha256,undefined);
|
|
35
|
+
assert.ok(getPrompts.action); assert.ok(getPrompts.promptId);
|
|
36
|
+
assert.ok(recent.maxResults); assert.ok(recent.toolName); assert.ok(recent.since);
|
|
37
|
+
assert.deepEqual(Object.keys(feedback),[]);
|
|
38
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('R48 set_config_value performs internal CAS while preserving reference wire',async()=>{
|
|
42
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r48-extended-cas-'));
|
|
43
|
+
try{
|
|
44
|
+
const runtime=createCommanderExtendedRuntime({stateRoot:root});
|
|
45
|
+
const server=fakeServer();
|
|
46
|
+
registerCommanderExtendedBridge(server,{extendedRuntime:runtime,observability:null});
|
|
47
|
+
const set=await server.tools.get('set_config_value').handler({key:'defaultShell',value:'powershell.exe'});
|
|
48
|
+
assert.equal(set.isError,undefined,JSON.stringify(set));
|
|
49
|
+
assert.match(set.content[0].text,/Successfully set defaultShell/);
|
|
50
|
+
const get=await server.tools.get('get_config').handler({});
|
|
51
|
+
assert.equal(get.structuredContent.config.defaultShell,'powershell.exe');
|
|
52
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('R48 prompts, history filters, and feedback accept reference calls',async()=>{
|
|
56
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r48-extended-behavior-'));
|
|
57
|
+
try{
|
|
58
|
+
const runtime=createCommanderExtendedRuntime({stateRoot:root,prompts:[{id:'onb2_01',title:'Explore',prompt:'Inspect the workspace'}]});
|
|
59
|
+
const history=createOperationHistory({clockMs:()=>Date.parse('2026-09-27T00:00:00.000Z')});
|
|
60
|
+
history.append('tool.call',{tool:'read_file'});
|
|
61
|
+
history.append('tool.result',{tool:'read_file'});
|
|
62
|
+
history.append('tool.call',{tool:'start_process'});
|
|
63
|
+
const observability=createCommanderObservability({history});
|
|
64
|
+
const server=fakeServer();
|
|
65
|
+
registerCommanderExtendedBridge(server,{extendedRuntime:runtime,observability});
|
|
66
|
+
const prompt=await server.tools.get('get_prompts').handler({action:'get_prompt',promptId:'onb2_01'});
|
|
67
|
+
assert.equal(prompt.isError,undefined,JSON.stringify(prompt));
|
|
68
|
+
assert.match(prompt.content[0].text,/Inspect the workspace/);
|
|
69
|
+
const recent=await server.tools.get('get_recent_tool_calls').handler({maxResults:10,toolName:'read_file',since:'2026-09-26T00:00:00.000Z'});
|
|
70
|
+
assert.equal(recent.isError,undefined,JSON.stringify(recent));
|
|
71
|
+
assert.ok(recent.structuredContent.events.length>=2);
|
|
72
|
+
assert.ok(recent.structuredContent.events.every(e=>e.payload?.tool==='read_file'));
|
|
73
|
+
const feedback=await server.tools.get('give_feedback_to_desktop_commander').handler({});
|
|
74
|
+
assert.equal(feedback.isError,undefined,JSON.stringify(feedback));
|
|
75
|
+
assert.match(feedback.content[0].text,/feedback/i);
|
|
76
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
test('R48 extended schema matches reference promptId and ISO datetime semantics exactly',async()=>{
|
|
81
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r48-extended-schema-exact-'));
|
|
82
|
+
try{
|
|
83
|
+
const runtime=createCommanderExtendedRuntime({stateRoot:root,prompts:[{id:'x'.repeat(256),title:'Long',prompt:'Long id is legal on reference wire'}]});
|
|
84
|
+
const history=createOperationHistory();
|
|
85
|
+
const observability=createCommanderObservability({history});
|
|
86
|
+
const server=fakeServer();
|
|
87
|
+
registerCommanderExtendedBridge(server,{extendedRuntime:runtime,observability});
|
|
88
|
+
const promptSchema=server.tools.get('get_prompts').definition.inputSchema;
|
|
89
|
+
const recentSchema=server.tools.get('get_recent_tool_calls').definition.inputSchema;
|
|
90
|
+
assert.doesNotThrow(()=>promptSchema.parse({action:'get_prompt',promptId:'x'.repeat(256)}));
|
|
91
|
+
assert.doesNotThrow(()=>recentSchema.parse({since:'2026-09-27T00:00:00.000Z'}));
|
|
92
|
+
assert.doesNotThrow(()=>recentSchema.parse({maxResults:1.5}));
|
|
93
|
+
history.append('tool.call',{tool:'read_file'});history.append('tool.result',{tool:'read_file'});
|
|
94
|
+
const fractional=await server.tools.get('get_recent_tool_calls').handler(recentSchema.parse({maxResults:1.5}));
|
|
95
|
+
assert.equal(fractional.isError,undefined);assert.equal(fractional.structuredContent.events.length,1);
|
|
96
|
+
assert.throws(()=>recentSchema.parse({since:'not-a-datetime'}));
|
|
97
|
+
}finally{await rm(root,{recursive:true,force:true});}
|
|
98
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createCommanderExternalProcessRuntime } from '../src/commander-external-process-r48.mjs';
|
|
4
|
+
|
|
5
|
+
function observationSequence(pages){
|
|
6
|
+
let call=0;
|
|
7
|
+
return {
|
|
8
|
+
calls:()=>call,
|
|
9
|
+
async processList(args){
|
|
10
|
+
const page=pages[Math.min(call,pages.length-1)];
|
|
11
|
+
call++;
|
|
12
|
+
return typeof page==='function'?page(args):structuredClone(page);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('R48 external process list exhausts one signed observation snapshot through cursors',async()=>{
|
|
18
|
+
const obs=observationSequence([
|
|
19
|
+
{status:'ok',snapshot_id:'a'.repeat(64),observed_at_utc:'2026-09-27T00:00:00.000Z',
|
|
20
|
+
processes:[{pid:10,parent_pid:1,name:'a.exe',ownership:'external',creation_time_utc:'2026-09-27T00:00:01.000Z',executable_path:null,command_line:null}],
|
|
21
|
+
next_cursor:'cursor-1',receipt_id:'1'.repeat(24),receipt_sha256:'2'.repeat(64)},
|
|
22
|
+
{status:'ok',snapshot_id:'a'.repeat(64),observed_at_utc:'2026-09-27T00:00:00.000Z',
|
|
23
|
+
processes:[{pid:20,parent_pid:1,name:'b.exe',ownership:'external',creation_time_utc:'2026-09-27T00:00:02.000Z',executable_path:null,command_line:null}],
|
|
24
|
+
next_cursor:null,receipt_id:'1'.repeat(24),receipt_sha256:'2'.repeat(64)}
|
|
25
|
+
]);
|
|
26
|
+
const rt=createCommanderExternalProcessRuntime({observationRuntime:obs,hostPolicy:null,hostExecInlineFn:null});
|
|
27
|
+
const listed=await rt.list();
|
|
28
|
+
assert.deepEqual(listed.processes.map(x=>x.pid),[10,20]);
|
|
29
|
+
assert.equal(listed.snapshot_id,'a'.repeat(64));
|
|
30
|
+
assert.equal(obs.calls(),2);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('R48 external kill binds PID creation time and name then proves the same identity disappeared',async()=>{
|
|
34
|
+
const before={status:'ok',snapshot_id:'a'.repeat(64),observed_at_utc:'2026-09-27T00:00:00.000Z',
|
|
35
|
+
processes:[{pid:222,parent_pid:1,name:"external.exe",ownership:'external',creation_time_utc:'2026-09-27T00:00:02.000Z',executable_path:null,command_line:null}],
|
|
36
|
+
next_cursor:null,receipt_id:'1'.repeat(24),receipt_sha256:'2'.repeat(64)};
|
|
37
|
+
const after={...before,snapshot_id:'b'.repeat(64),processes:[],receipt_id:'3'.repeat(24),receipt_sha256:'4'.repeat(64)};
|
|
38
|
+
const obs=observationSequence([before,after]);
|
|
39
|
+
const hostCalls=[];
|
|
40
|
+
const hostPolicy={commands:{powershell_inline:{kind:'inline',root_ids:['core']}},roots:{core:{}}};
|
|
41
|
+
const rt=createCommanderExternalProcessRuntime({
|
|
42
|
+
observationRuntime:obs,hostPolicy,
|
|
43
|
+
hostExecInlineFn:async(policy,args)=>{hostCalls.push({policy,args});return {exit_code:0,timed_out:false,stdout:'TERMINATED\n',stderr:''};}
|
|
44
|
+
});
|
|
45
|
+
const result=await rt.terminate({pid:222});
|
|
46
|
+
assert.equal(result.status,'terminated');
|
|
47
|
+
assert.equal(result.pid,222);
|
|
48
|
+
assert.equal(result.creation_time_utc,'2026-09-27T00:00:02.000Z');
|
|
49
|
+
assert.equal(hostCalls.length,1);
|
|
50
|
+
const command=hostCalls[0].args.command;
|
|
51
|
+
assert.match(command,/\$pidExpected=222;/);
|
|
52
|
+
assert.match(command,/2026-09-27T00:00:02\.000Z/);
|
|
53
|
+
assert.match(command,/Stop-Process -Id \$pidExpected -Force/);
|
|
54
|
+
assert.doesNotMatch(command,/external\.exe/,'process name must not be interpolated raw');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('R48 external kill fails closed on missing identity and on post-effect same identity',async()=>{
|
|
58
|
+
const noIdentity={status:'ok',snapshot_id:'a'.repeat(64),observed_at_utc:'2026-09-27T00:00:00.000Z',
|
|
59
|
+
processes:[{pid:222,parent_pid:1,name:'external.exe',ownership:'external',creation_time_utc:null,executable_path:null,command_line:null}],
|
|
60
|
+
next_cursor:null,receipt_id:'1'.repeat(24),receipt_sha256:'2'.repeat(64)};
|
|
61
|
+
const hostPolicy={commands:{powershell_inline:{kind:'inline',root_ids:['core']}},roots:{core:{}}};
|
|
62
|
+
let hostCount=0;
|
|
63
|
+
const noIdentityRuntime=createCommanderExternalProcessRuntime({
|
|
64
|
+
observationRuntime:observationSequence([noIdentity]),hostPolicy,
|
|
65
|
+
hostExecInlineFn:async()=>{hostCount++;return {exit_code:0,timed_out:false,stdout:'',stderr:''};}
|
|
66
|
+
});
|
|
67
|
+
await assert.rejects(noIdentityRuntime.terminate({pid:222}),/creation time identity unavailable/);
|
|
68
|
+
assert.equal(hostCount,0);
|
|
69
|
+
|
|
70
|
+
const stable={...noIdentity,processes:[{...noIdentity.processes[0],creation_time_utc:'2026-09-27T00:00:02.000Z'}]};
|
|
71
|
+
const stableRuntime=createCommanderExternalProcessRuntime({
|
|
72
|
+
observationRuntime:observationSequence([stable,stable]),hostPolicy,
|
|
73
|
+
hostExecInlineFn:async()=>({exit_code:0,timed_out:false,stdout:'TERMINATED',stderr:''})
|
|
74
|
+
});
|
|
75
|
+
await assert.rejects(stableRuntime.terminate({pid:222}),/same process identity is still alive/);
|
|
76
|
+
});
|