deadbyte-mcp 0.15.48 → 0.15.49
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 +48 -22
- package/PROVENANCE.json +11 -22
- package/README.txt +18 -18
- package/RELEASE.json +368 -378
- package/SBOM.cdx.json +2 -2
- package/TEST-MATRIX.json +2 -2
- package/bench/r121-epoch-readhead.mjs +54 -0
- package/bench/r124-epoch-predecessor.mjs +53 -0
- package/controller/deadbyte-process-policy.json +4 -4
- package/docs/HARNESS2-ADAPTATION-20261020.md +24 -0
- package/docs/R128-RELEASE-CANDIDATE.md +26 -0
- package/package.json +1 -1
- package/scripts/npm-release-r48.mjs +0 -4
- package/scripts/verify-authority-epoch-history.mjs +38 -0
- package/src/activation-continuity-engine-r65.mjs +9 -15
- package/src/agent-eval-r42.mjs +30 -0
- package/src/authority-continuity-r65.mjs +182 -13
- package/src/autonomous-local-supervisor.mjs +77 -47
- package/src/deadbyte-cli.mjs +18 -28
- package/src/remote-live-log.mjs +15 -8
- package/src/remote-log-decode-r60.mjs +65 -0
- package/src/remote-session-guardian.mjs +4 -5
- package/src/version.mjs +1 -1
- package/test/autonomous-local-supervisor.test.mjs +1 -1
- package/test/harness2-eval-context-isolation.test.mjs +90 -0
- package/test/r108-remote-powershell-path.test.mjs +59 -0
- package/test/r110-supervisor-powershell-path.test.mjs +28 -0
- package/test/r111-supervisor-marker-cas.test.mjs +116 -0
- package/test/r112-supervisor-claim-cas.test.mjs +63 -0
- package/test/r113-supervisor-cleanup-terminality.test.mjs +100 -0
- package/test/r114-supervisor-absence-proof.test.mjs +45 -0
- package/test/r115-posix-continuity-entrypoint.test.mjs +17 -0
- package/test/r116-authority-crash-matrix.test.mjs +83 -0
- package/test/r117-concurrent-orphan-recovery.test.mjs +54 -0
- package/test/r118-cross-process-orphan-recovery.test.mjs +99 -0
- package/test/r119-recovery-crash-after-commit.test.mjs +91 -0
- package/test/r120-authority-history-audit.test.mjs +103 -0
- package/test/r121-signed-transition-invariant.test.mjs +132 -0
- package/test/r122-epoch-bounded-read.test.mjs +58 -0
- package/test/r123-recovery-bounded-read.test.mjs +64 -0
- package/test/r124-live-log-integrity.test.mjs +66 -0
- package/test/r124-live-log-tail-boundaries.test.mjs +57 -0
- package/test/r124-signed-predecessor-shape.test.mjs +66 -0
- package/test/r125-restore-binding-types.test.mjs +63 -0
- package/test/r126-restore-policy-envelope.test.mjs +89 -0
- package/test/r127-restore-append-policy-boundary.test.mjs +25 -0
- package/test/r59-tool-surface-compat.test.mjs +1 -1
- package/test/r65-authority-epoch-store.test.mjs +9 -6
- package/test/r74-prepublish-contract.test.mjs +3 -1
- package/scripts/npm-release-readiness-r108.mjs +0 -134
package/src/remote-live-log.mjs
CHANGED
|
@@ -254,13 +254,20 @@ function traceText(meta,displayMode='unicode'){
|
|
|
254
254
|
}
|
|
255
255
|
function outcomeClass(event){
|
|
256
256
|
const outcome=event?.outcome;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if(
|
|
260
|
-
if(outcome
|
|
261
|
-
|
|
262
|
-
if(
|
|
263
|
-
|
|
257
|
+
// A terminal record is not evidence of success without a classified
|
|
258
|
+
// outcome. Never upgrade missing or new statuses to OK.
|
|
259
|
+
if(!outcome||typeof outcome!=='object'||Array.isArray(outcome))return 'unknown';
|
|
260
|
+
if(outcome.side_effect_state==='unknown')return 'unknown';
|
|
261
|
+
const status=outcome.status;
|
|
262
|
+
if(status==='timeout'||status==='timed_out'||status==='interrupted')return 'unknown';
|
|
263
|
+
if(outcome.is_error===true||
|
|
264
|
+
['failed','threw','error','denied','rejected','blocked','observed_failure','cancelled','canceled','unavailable','unsupported'].includes(status))return 'error';
|
|
265
|
+
if(['pending','running','input_required','paused','processing','accepted'].includes(status))return 'pending';
|
|
266
|
+
if(['ok','success','succeeded','completed','committed','verified','passed','done'].includes(status))return 'ok';
|
|
267
|
+
// Legacy audit records with explicit is_error:false but no status remain
|
|
268
|
+
// supported; an unrecognized status is not evidence of success.
|
|
269
|
+
if((status===undefined||status===null)&&outcome.is_error===false)return 'ok';
|
|
270
|
+
return 'unknown';
|
|
264
271
|
}
|
|
265
272
|
export function classifyRemoteAuditOutcome(event){return outcomeClass(event);}
|
|
266
273
|
function parseFilterExpression(expression){
|
|
@@ -391,7 +398,7 @@ export function createLiveTimelineTracker({clock=()=>Date.now(),verbose=false,fo
|
|
|
391
398
|
const category=categoryForTool(displayTool);
|
|
392
399
|
const receipt=Array.isArray(event.outcome?.receipt_sha256s)&&typeof event.outcome.receipt_sha256s[0]==='string'
|
|
393
400
|
?shortValue(event.outcome.receipt_sha256s[0],8):'';
|
|
394
|
-
const marker=unknown?'FAILED_AFTER_UNKNOWN_EFFECT':
|
|
401
|
+
const marker=unknown?(event.outcome?.side_effect_state==='unknown'?'FAILED_AFTER_UNKNOWN_EFFECT':'OUTCOME_UNKNOWN'):
|
|
395
402
|
error?(displayMode==='ascii'?'ERR':'✗'):
|
|
396
403
|
pendingResult?'PENDING':
|
|
397
404
|
['write','edit','run'].includes(category)?(displayMode==='ascii'?'SUCCEEDED':'✅ SUCCEEDED'):
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import {createHash} from 'node:crypto';
|
|
3
|
+
import {open} from 'node:fs/promises';
|
|
2
4
|
|
|
3
5
|
export function safeRemoteLogPrefixLength(bytes,encoding='utf8'){
|
|
4
6
|
if(!Buffer.isBuffer(bytes))throw new Error('remote log prefix bytes invalid');
|
|
@@ -30,3 +32,66 @@ export function boundedRemoteDiagnostic(value,maxChars=2048){
|
|
|
30
32
|
const line=String(value??'').replace(/\x1b\[[0-9;]*[A-Za-z]/g,'').replace(/[\x00-\x1f\x7f]+/g,' ').replace(/\s{2,}/g,' ').trim();
|
|
31
33
|
return line.length<=maxChars?line:`${line.slice(0,maxChars)}...`;
|
|
32
34
|
}
|
|
35
|
+
|
|
36
|
+
// Cursor pinned to the file handle's observed identity. We never use a path
|
|
37
|
+
// stat's size to allocate or seek on a separately opened file.
|
|
38
|
+
export async function readRemoteLogAppend(file,cursor=null,{startAtEnd=false,maxBytes=1048576}={}){
|
|
39
|
+
if(typeof file!=='string'||!file||!Number.isInteger(maxBytes)||maxBytes<4||maxBytes>1048576)
|
|
40
|
+
throw new Error('remote live log append arguments invalid');
|
|
41
|
+
if(cursor!==null&&(!Number.isSafeInteger(cursor.offset)||cursor.offset<0))
|
|
42
|
+
throw new Error('remote live log cursor invalid');
|
|
43
|
+
let handle;
|
|
44
|
+
try{handle=await open(file,'r');}
|
|
45
|
+
catch(error){
|
|
46
|
+
if(error?.code==='ENOENT')return {cursor:null,offset:0,text:'',reset:cursor!==null,resetReason:'file disappeared'};
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
try{
|
|
50
|
+
const st=await handle.stat({bigint:true});
|
|
51
|
+
if(!st.isFile()||st.size<0n||st.size>BigInt(Number.MAX_SAFE_INTEGER))
|
|
52
|
+
throw new Error('remote live log regular file/size invalid');
|
|
53
|
+
const size=Number(st.size);
|
|
54
|
+
const identity=st.ino!==0n?st.dev.toString()+':'+st.ino.toString():null;
|
|
55
|
+
const mtimeNs=(st.mtimeNs??BigInt(Math.trunc(Number(st.mtimeMs)*1000000))).toString();
|
|
56
|
+
const gone=cursor!==null&&cursor.identity&&identity&&cursor.identity!==identity;
|
|
57
|
+
const shrunk=cursor!==null&&size<cursor.offset;
|
|
58
|
+
// NTFS may report the same timestamp for rapid same-size rewrites.
|
|
59
|
+
// Compare a bounded suffix fingerprint on the idle cursor as well.
|
|
60
|
+
// This is best-effort rewrite detection, NOT complete file integrity.
|
|
61
|
+
async function tailFingerprint(offset){
|
|
62
|
+
if(offset===0)return null;
|
|
63
|
+
const length=Math.min(offset,64),bytes=Buffer.allocUnsafe(length);
|
|
64
|
+
const {bytesRead}=await handle.read(bytes,0,length,offset-length);
|
|
65
|
+
if(bytesRead!==length)throw new Error('remote live log changed during fingerprint');
|
|
66
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
67
|
+
}
|
|
68
|
+
const priorTailChanged=cursor!==null&&!gone&&!shrunk&&size===cursor.offset&&
|
|
69
|
+
typeof cursor.tail_sha256==='string'&&
|
|
70
|
+
(await tailFingerprint(cursor.offset))!==cursor.tail_sha256;
|
|
71
|
+
const rewritten=cursor!==null&&!gone&&!shrunk&&size===cursor.offset&&
|
|
72
|
+
((typeof cursor.mtimeNs==='string'&&cursor.mtimeNs!==mtimeNs)||priorTailChanged);
|
|
73
|
+
const reset=Boolean(gone||shrunk||rewritten);
|
|
74
|
+
let start=reset?0:(cursor?.offset??0);
|
|
75
|
+
const makeCursor=async offset=>Object.freeze({offset,identity,mtimeNs,tail_sha256:await tailFingerprint(offset)});
|
|
76
|
+
if(startAtEnd&&cursor===null)
|
|
77
|
+
return {cursor:await makeCursor(size),offset:size,text:'',reset:false,resetReason:null};
|
|
78
|
+
let encoding=null;
|
|
79
|
+
if(/\.log$/i.test(file)){
|
|
80
|
+
const bom=Buffer.allocUnsafe(2);
|
|
81
|
+
const header=await handle.read(bom,0,2,0);
|
|
82
|
+
if(header.bytesRead===2&&bom[0]===0xff&&bom[1]===0xfe){
|
|
83
|
+
encoding='utf16le';
|
|
84
|
+
if(start%2!==0)start--;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if(start>=size)
|
|
88
|
+
return {cursor:await makeCursor(start),offset:start,text:'',reset,resetReason:gone?'file identity changed':shrunk?'file shrank':rewritten?'same-size rewrite':null};
|
|
89
|
+
const buffer=Buffer.allocUnsafe(Math.min(maxBytes,size-start));
|
|
90
|
+
const {bytesRead}=await handle.read(buffer,0,buffer.length,start);
|
|
91
|
+
const complete=safeRemoteLogPrefixLength(buffer.subarray(0,bytesRead),encoding??'utf8');
|
|
92
|
+
const next=start+complete;
|
|
93
|
+
return {cursor:await makeCursor(next),offset:next,
|
|
94
|
+
text:decodeRemoteLogBytes(file,buffer.subarray(0,complete),{encoding}),
|
|
95
|
+
reset,resetReason:gone?'file identity changed':shrunk?'file shrank':rewritten?'same-size rewrite':null};
|
|
96
|
+
}finally{await handle.close();}
|
|
97
|
+
}
|
|
@@ -10,6 +10,7 @@ import { AUTHORITY_PLANES, parseAuthorityObservation, assertAuthorityVector } fr
|
|
|
10
10
|
import { readRemoteMarkerSnapshot, deletePinnedRemoteMarker } from './foreground-marker-cas-win32.mjs';
|
|
11
11
|
|
|
12
12
|
const CLEANUP=['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost'];
|
|
13
|
+
const TRUSTED_WINDOWS_ROOT='C:\\Windows';
|
|
13
14
|
// This inherited IPC channel is an OS-owned parent lifetime handle, not a PID
|
|
14
15
|
// poll. A reused PID cannot keep a new foreground guardian alive.
|
|
15
16
|
let ownerDisconnected=false;
|
|
@@ -44,7 +45,7 @@ function markerMatches(marker,{ownerPid,nonce}){
|
|
|
44
45
|
}
|
|
45
46
|
function invokeCleanup(powershell,launcher,command,stateRoot,probeMode=null){
|
|
46
47
|
const result=spawnSync(powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',launcher,command],{
|
|
47
|
-
stdio:'ignore',windowsHide:true,shell:false,env:windowsPowerShellEnvironment({...process.env,DEADBYTE_STATE_ROOT:stateRoot,
|
|
48
|
+
stdio:'ignore',windowsHide:true,shell:false,env:windowsPowerShellEnvironment({...process.env,SystemRoot:TRUSTED_WINDOWS_ROOT,WINDIR:TRUSTED_WINDOWS_ROOT,DEADBYTE_STATE_ROOT:stateRoot,
|
|
48
49
|
...(probeMode?{DEADBYTE_REMOTE_PROBE_MODE:probeMode}:{})}),timeout:probeMode?150000:30000
|
|
49
50
|
});
|
|
50
51
|
return !result.error&&result.status===0;
|
|
@@ -52,7 +53,7 @@ function invokeCleanup(powershell,launcher,command,stateRoot,probeMode=null){
|
|
|
52
53
|
function verifyRevoked(powershell,launcher,stateRoot){
|
|
53
54
|
const result=spawnSync(powershell,['-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',launcher,'status','-Json'],{
|
|
54
55
|
stdio:['ignore','pipe','pipe'],encoding:'utf8',windowsHide:true,shell:false,
|
|
55
|
-
env:windowsPowerShellEnvironment({...process.env,DEADBYTE_STATE_ROOT:stateRoot}),timeout:30000
|
|
56
|
+
env:windowsPowerShellEnvironment({...process.env,SystemRoot:TRUSTED_WINDOWS_ROOT,WINDIR:TRUSTED_WINDOWS_ROOT,DEADBYTE_STATE_ROOT:stateRoot}),timeout:30000
|
|
56
57
|
});
|
|
57
58
|
if(result.error||result.status!==0) return null;
|
|
58
59
|
try{
|
|
@@ -73,9 +74,7 @@ async function main(){
|
|
|
73
74
|
if(!/^[0-9a-f]{64}$/.test(nonce)) throw new Error('guardian session nonce invalid');
|
|
74
75
|
|
|
75
76
|
const launcher=await requireRegular(path.join(root,'bin','DEADBYTE-MCP.ps1'),'stable DEADBYTE launcher');
|
|
76
|
-
const
|
|
77
|
-
if(!systemRoot) throw new Error('SystemRoot/WINDIR missing');
|
|
78
|
-
const powershell=await requireRegular(path.join(systemRoot,'System32','WindowsPowerShell','v1.0','powershell.exe'),'Windows PowerShell');
|
|
77
|
+
const powershell=await requireRegular(path.join(TRUSTED_WINDOWS_ROOT,'System32','WindowsPowerShell','v1.0','powershell.exe'),'Windows PowerShell');
|
|
79
78
|
const markerPath=path.join(root,'runtime','remote-session.json');
|
|
80
79
|
|
|
81
80
|
for(;;){
|
package/src/version.mjs
CHANGED
|
@@ -365,7 +365,7 @@ test('continuous standalone supervisor remains alive after READY and hard-stop o
|
|
|
365
365
|
await new Promise(resolve=>child.once('exit',()=>resolve()));
|
|
366
366
|
const stale=await observeSupervisor({markerPath:f.markerPath});
|
|
367
367
|
assert.equal(stale.live,false);
|
|
368
|
-
assert.match(stale.reason,/
|
|
368
|
+
assert.match(stale.reason,/process_absent|pid_reused|identity_mismatch/);
|
|
369
369
|
const claimed=await takeoverSupervisor({markerPath:f.markerPath});
|
|
370
370
|
assert.equal(claimed.status,'claimed');
|
|
371
371
|
await releaseSupervisor({markerPath:f.markerPath,instanceNonce:claimed.instance_nonce});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { runIsolatedEvaluation } from '../src/agent-eval-r42.mjs';
|
|
4
|
+
|
|
5
|
+
const caseBase = {
|
|
6
|
+
schema: 'deadbyte.agent-eval-case.v1',
|
|
7
|
+
case_id: 'harness2-context-boundary',
|
|
8
|
+
task: 'Return a bounded plan.',
|
|
9
|
+
acceptance: ['Bounded plan']
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
test('judge-only oracle token nested in allowed context is rejected before generator invocation', async () => {
|
|
13
|
+
const token = 'JUDGE_ONLY_38d0f04a8a7b4ae8';
|
|
14
|
+
let generatorCalls = 0;
|
|
15
|
+
await assert.rejects(runIsolatedEvaluation({
|
|
16
|
+
caseSpec: {
|
|
17
|
+
...caseBase,
|
|
18
|
+
allowed_context: {files: [{path: 'context.md', contents: `visible notes: ${token}`}]},
|
|
19
|
+
hidden_oracle: {graded: {token}}
|
|
20
|
+
},
|
|
21
|
+
generator: async () => { generatorCalls++; return {artifact: {plan: ['safe']}}; },
|
|
22
|
+
judge: async () => ({verdict: 'pass', findings: [], evidence_refs: ['a'.repeat(64)]})
|
|
23
|
+
}), /judge-only.*context|oracle.*leak/i);
|
|
24
|
+
assert.equal(generatorCalls, 0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('judge-only secret in task or acceptance fails closed without echoing secret', async () => {
|
|
28
|
+
const token = 'SECRET_4f725e0e1cbe4a15';
|
|
29
|
+
for (const visible of [
|
|
30
|
+
{task: `Return ${token}`, acceptance: ['Bounded plan']},
|
|
31
|
+
{task: 'Return a bounded plan.', acceptance: [`Must not reveal ${token}`]}
|
|
32
|
+
]) {
|
|
33
|
+
let generatorCalls = 0;
|
|
34
|
+
await assert.rejects(runIsolatedEvaluation({
|
|
35
|
+
caseSpec: {...caseBase, ...visible, allowed_context: {}, hidden_oracle: {secret: token}},
|
|
36
|
+
generator: async () => { generatorCalls++; return {artifact: {plan: ['safe']}}; },
|
|
37
|
+
judge: async () => ({verdict: 'pass', findings: [], evidence_refs: ['a'.repeat(64)]})
|
|
38
|
+
}), error => {
|
|
39
|
+
assert.match(error.message, /judge-only.*context/i);
|
|
40
|
+
assert.equal(error.message.includes(token), false);
|
|
41
|
+
return true;
|
|
42
|
+
});
|
|
43
|
+
assert.equal(generatorCalls, 0);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('short explicit judge-only secret is not exempt from context isolation', async () => {
|
|
48
|
+
let generatorCalls = 0;
|
|
49
|
+
await assert.rejects(runIsolatedEvaluation({
|
|
50
|
+
caseSpec: {
|
|
51
|
+
...caseBase,
|
|
52
|
+
allowed_context: {note: 'Visible value K9 is forbidden here'},
|
|
53
|
+
hidden_oracle: {secret: 'K9'}
|
|
54
|
+
},
|
|
55
|
+
generator: async () => { generatorCalls++; return {artifact: {plan: ['safe']}}; },
|
|
56
|
+
judge: async () => ({verdict: 'pass', findings: [], evidence_refs: ['a'.repeat(64)]})
|
|
57
|
+
}), /judge-only.*context/i);
|
|
58
|
+
assert.equal(generatorCalls, 0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('nested judge verdict objects cannot enter generator-visible context', async () => {
|
|
62
|
+
for (const forbidden of ['hidden_oracle', 'judge_result', 'grader_verdict', 'deterministic_eval_result']) {
|
|
63
|
+
let generatorCalls = 0;
|
|
64
|
+
await assert.rejects(runIsolatedEvaluation({
|
|
65
|
+
caseSpec: {
|
|
66
|
+
...caseBase,
|
|
67
|
+
allowed_context: {fixture: {metadata: {[forbidden]: {verdict: 'pass'}}}},
|
|
68
|
+
hidden_oracle: {expected: 'bounded'}
|
|
69
|
+
},
|
|
70
|
+
generator: async () => { generatorCalls++; return {artifact: {plan: ['safe']}}; },
|
|
71
|
+
judge: async () => ({verdict: 'pass', findings: [], evidence_refs: ['a'.repeat(64)]})
|
|
72
|
+
}), /judge-only.*context|grader.*context/i);
|
|
73
|
+
assert.equal(generatorCalls, 0, `${forbidden} must reject before generator dispatch`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('ordinary shared acceptance terms do not trigger oracle isolation tripwire', async () => {
|
|
78
|
+
let generatorCalls = 0;
|
|
79
|
+
const result = await runIsolatedEvaluation({
|
|
80
|
+
caseSpec: {
|
|
81
|
+
...caseBase,
|
|
82
|
+
allowed_context: {visible: 'bounded plan'},
|
|
83
|
+
hidden_oracle: {expected: 'bounded plan'}
|
|
84
|
+
},
|
|
85
|
+
generator: async () => { generatorCalls++; return {artifact: {plan: ['safe']}}; },
|
|
86
|
+
judge: async () => ({verdict: 'pass', findings: [], evidence_refs: ['a'.repeat(64)]})
|
|
87
|
+
});
|
|
88
|
+
assert.equal(result.verdict, 'pass');
|
|
89
|
+
assert.equal(generatorCalls, 1);
|
|
90
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdtemp,mkdir,writeFile,rm} from 'node:fs/promises';
|
|
4
|
+
import {spawnSync} from 'node:child_process';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import {fileURLToPath,pathToFileURL} from 'node:url';
|
|
8
|
+
|
|
9
|
+
const root=path.resolve(fileURLToPath(new URL('..',import.meta.url)));
|
|
10
|
+
const systemPowerShell='C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
|
|
11
|
+
const winTest=process.platform==='win32'?test:test.skip;
|
|
12
|
+
function poisonedRuntimeImport(source,args){
|
|
13
|
+
return `process.env.SystemRoot='C:\\\\untrusted-root';process.env.WINDIR='C:\\\\untrusted-root';`+
|
|
14
|
+
`process.argv=[process.execPath,${JSON.stringify(source)},...${JSON.stringify(args)}];`+
|
|
15
|
+
`await import(${JSON.stringify(pathToFileURL(source).href)});`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
winTest('remote supervisor dispatch ignores poisoned caller Windows roots',async()=>{
|
|
19
|
+
const scratch=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r108-cli-'));
|
|
20
|
+
try{
|
|
21
|
+
await mkdir(path.join(scratch,'bin'));
|
|
22
|
+
await writeFile(path.join(scratch,'bin','DEADBYTE-MCP.ps1'),
|
|
23
|
+
"Write-Output ('R108_CONTROLLER_EXECUTED root=' + $env:SystemRoot + ' windir=' + $env:WINDIR)\nexit 0\n",{flag:'wx'});
|
|
24
|
+
const source=path.join(root,'src','deadbyte-cli.mjs');
|
|
25
|
+
const result=spawnSync(process.execPath,['--input-type=module','-e',
|
|
26
|
+
poisonedRuntimeImport(source,['supervisor','status'])],{
|
|
27
|
+
cwd:root,encoding:'utf8',timeout:15000,windowsHide:true,
|
|
28
|
+
env:{...process.env,DEADBYTE_STATE_ROOT:scratch}
|
|
29
|
+
});
|
|
30
|
+
assert.equal(result.error,undefined,String(result.error));
|
|
31
|
+
assert.equal(result.status,0,result.stderr);
|
|
32
|
+
assert.match(result.stdout,/R108_CONTROLLER_EXECUTED root=C:\\Windows windir=C:\\Windows/);
|
|
33
|
+
}finally{await rm(scratch,{recursive:true,force:true});}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
winTest('guardian startup ignores poisoned caller Windows roots before marker lookup',async()=>{
|
|
37
|
+
const scratch=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r108-guardian-'));
|
|
38
|
+
try{
|
|
39
|
+
await mkdir(path.join(scratch,'bin'));
|
|
40
|
+
await writeFile(path.join(scratch,'bin','DEADBYTE-MCP.ps1'),"exit 0\n",{flag:'wx'});
|
|
41
|
+
const source=path.join(root,'src','remote-session-guardian.mjs');
|
|
42
|
+
const result=spawnSync(process.execPath,['--input-type=module','-e',
|
|
43
|
+
poisonedRuntimeImport(source,['--state-root',scratch,'--owner-pid',String(process.pid),'--session-nonce','a'.repeat(64)])],{
|
|
44
|
+
cwd:root,encoding:'utf8',timeout:15000,windowsHide:true,
|
|
45
|
+
env:process.env
|
|
46
|
+
});
|
|
47
|
+
assert.equal(result.error,undefined,String(result.error));
|
|
48
|
+
assert.equal(result.status,0,`guardian startup failed under poisoned roots; stderr=${result.stderr}`);
|
|
49
|
+
}finally{await rm(scratch,{recursive:true,force:true});}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
winTest('trusted Windows PowerShell binary exists for remote lifecycle tests',()=>{
|
|
53
|
+
const result=spawnSync(systemPowerShell,['-NoLogo','-NoProfile','-Command','exit 0'],{
|
|
54
|
+
encoding:'utf8',timeout:15000,windowsHide:true,
|
|
55
|
+
env:{...process.env,SystemRoot:'C:\\Windows',WINDIR:'C:\\Windows'}
|
|
56
|
+
});
|
|
57
|
+
assert.equal(result.error,undefined,String(result.error));
|
|
58
|
+
assert.equal(result.status,0);
|
|
59
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {copyFile, mkdir, mkdtemp, rm} from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {observeCurrentProcessIdentity} from '../src/autonomous-local-supervisor.mjs';
|
|
7
|
+
|
|
8
|
+
test('supervisor identity observation ignores an inherited untrusted Windows root', {skip:process.platform!=='win32'}, async () => {
|
|
9
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r110-poisoned-root-'));
|
|
10
|
+
const shellDir=path.join(root,'System32','WindowsPowerShell','v1.0');
|
|
11
|
+
const priorRoot=process.env.SystemRoot;
|
|
12
|
+
const priorWindir=process.env.WINDIR;
|
|
13
|
+
try {
|
|
14
|
+
await mkdir(shellDir,{recursive:true});
|
|
15
|
+
await copyFile('C:\\Windows\\System32\\where.exe',path.join(shellDir,'powershell.exe'));
|
|
16
|
+
process.env.SystemRoot=root;
|
|
17
|
+
process.env.WINDIR=root;
|
|
18
|
+
const identity=await observeCurrentProcessIdentity();
|
|
19
|
+
assert.ok(identity,'a caller-provided Windows root must not redirect the trusted identity probe');
|
|
20
|
+
assert.equal(identity.pid,process.pid);
|
|
21
|
+
} finally {
|
|
22
|
+
if(priorRoot===undefined) delete process.env.SystemRoot;
|
|
23
|
+
else process.env.SystemRoot=priorRoot;
|
|
24
|
+
if(priorWindir===undefined) delete process.env.WINDIR;
|
|
25
|
+
else process.env.WINDIR=priorWindir;
|
|
26
|
+
await rm(root,{recursive:true,force:true});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {randomBytes} from 'node:crypto';
|
|
4
|
+
import childProcess from 'node:child_process';
|
|
5
|
+
import {syncBuiltinESMExports} from 'node:module';
|
|
6
|
+
import {promisify} from 'node:util';
|
|
7
|
+
import {existsSync, readFileSync, renameSync, writeFileSync} from 'node:fs';
|
|
8
|
+
import {mkdtemp, readFile, rm, writeFile} from 'node:fs/promises';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
const moduleUrl=new URL('../src/autonomous-local-supervisor.mjs',import.meta.url);
|
|
13
|
+
const freshModule=()=>import(`${moduleUrl.href}?race=${randomBytes(8).toString('hex')}`);
|
|
14
|
+
|
|
15
|
+
async function withIdenticalReplacement(markerPath,run){
|
|
16
|
+
const original=childProcess.execFile;
|
|
17
|
+
let replaced=0;
|
|
18
|
+
const replace=()=>{
|
|
19
|
+
if(replaced===0){
|
|
20
|
+
const before=readFileSync(markerPath);
|
|
21
|
+
const successor=`${markerPath}.${randomBytes(6).toString('hex')}.successor`;
|
|
22
|
+
writeFileSync(successor,before,{flag:'wx'});
|
|
23
|
+
renameSync(successor,markerPath);
|
|
24
|
+
assert.deepEqual(readFileSync(markerPath),before,'replacement must be byte-identical');
|
|
25
|
+
replaced++;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const wrapped=function(...args){replace();return original.apply(this,args);};
|
|
29
|
+
wrapped[promisify.custom]=function(...args){replace();return original[promisify.custom].apply(original,args);};
|
|
30
|
+
childProcess.execFile=wrapped;
|
|
31
|
+
syncBuiltinESMExports();
|
|
32
|
+
try {
|
|
33
|
+
const module=await freshModule();
|
|
34
|
+
await run(module);
|
|
35
|
+
assert.equal(replaced,1,'replacement must occur after the initial marker observation');
|
|
36
|
+
} finally {
|
|
37
|
+
childProcess.execFile=original;
|
|
38
|
+
syncBuiltinESMExports();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('owned supervisor release cannot remove a byte-identical successor marker', {skip:process.platform!=='win32'}, async()=>{
|
|
43
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r111-release-'));
|
|
44
|
+
const markerPath=path.join(root,'owner.json');
|
|
45
|
+
try {
|
|
46
|
+
const baseline=await freshModule();
|
|
47
|
+
const claim=await baseline.claimSupervisor({markerPath});
|
|
48
|
+
const original=await readFile(markerPath);
|
|
49
|
+
await withIdenticalReplacement(markerPath,async({releaseSupervisor})=>{
|
|
50
|
+
await assert.rejects(releaseSupervisor({markerPath,instanceNonce:claim.instance_nonce}),/changed before release: CHANGED/i);
|
|
51
|
+
});
|
|
52
|
+
assert.equal(existsSync(markerPath),true,'successor must survive release');
|
|
53
|
+
assert.deepEqual(await readFile(markerPath),original);
|
|
54
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('stale supervisor takeover cannot overwrite a byte-identical successor marker', {skip:process.platform!=='win32'}, async()=>{
|
|
58
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r111-takeover-'));
|
|
59
|
+
const markerPath=path.join(root,'owner.json');
|
|
60
|
+
try {
|
|
61
|
+
const baseline=await freshModule();
|
|
62
|
+
const claim=await baseline.claimSupervisor({markerPath});
|
|
63
|
+
const stale={...claim.marker,creation_time_utc:'2000-01-01T00:00:00.000Z'};
|
|
64
|
+
await writeFile(markerPath,`${JSON.stringify(stale)}\n`);
|
|
65
|
+
const original=await readFile(markerPath);
|
|
66
|
+
await withIdenticalReplacement(markerPath,async({takeoverSupervisor})=>{
|
|
67
|
+
await assert.rejects(takeoverSupervisor({markerPath}),/changed during takeover: CHANGED/i);
|
|
68
|
+
});
|
|
69
|
+
assert.equal(existsSync(markerPath),true,'successor must survive takeover');
|
|
70
|
+
assert.deepEqual(await readFile(markerPath),original);
|
|
71
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('unobservable process identity cannot authorize takeover or remove its marker', {skip:process.platform!=='win32'}, async()=>{
|
|
75
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r111-unobservable-'));
|
|
76
|
+
const markerPath=path.join(root,'owner.json');
|
|
77
|
+
const originalExec=childProcess.execFile;
|
|
78
|
+
try {
|
|
79
|
+
const baseline=await freshModule();
|
|
80
|
+
await baseline.claimSupervisor({markerPath});
|
|
81
|
+
const originalMarker=await readFile(markerPath);
|
|
82
|
+
const denied=function(...args){
|
|
83
|
+
const callback=args.at(-1);
|
|
84
|
+
if(typeof callback==='function') callback(new Error('process lookup denied'));
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
87
|
+
denied[promisify.custom]=()=>Promise.reject(new Error('process lookup denied'));
|
|
88
|
+
childProcess.execFile=denied;
|
|
89
|
+
syncBuiltinESMExports();
|
|
90
|
+
const module=await freshModule();
|
|
91
|
+
await assert.rejects(module.takeoverSupervisor({markerPath}),/unverified|unobservable|lookup|identity/i);
|
|
92
|
+
assert.deepEqual(await readFile(markerPath),originalMarker,'uncertain lookup must not delete a live owner marker');
|
|
93
|
+
} finally {
|
|
94
|
+
childProcess.execFile=originalExec;
|
|
95
|
+
syncBuiltinESMExports();
|
|
96
|
+
await rm(root,{recursive:true,force:true});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('same live process with mismatched image evidence cannot be taken over', {skip:process.platform!=='win32'}, async()=>{
|
|
101
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r111-image-drift-'));
|
|
102
|
+
const markerPath=path.join(root,'owner.json');
|
|
103
|
+
try {
|
|
104
|
+
const module=await freshModule();
|
|
105
|
+
const claim=await module.claimSupervisor({markerPath});
|
|
106
|
+
const mismatched={...claim.marker,image_sha256:'f'.repeat(64)};
|
|
107
|
+
await writeFile(markerPath,`${JSON.stringify(mismatched)}\n`);
|
|
108
|
+
const before=await readFile(markerPath);
|
|
109
|
+
const observation=await module.observeSupervisor({markerPath});
|
|
110
|
+
assert.equal(observation.reason,'identity_mismatch');
|
|
111
|
+
assert.equal(observation.observation.pid,process.pid);
|
|
112
|
+
assert.equal(observation.observation.creation_time_utc,claim.marker.creation_time_utc);
|
|
113
|
+
await assert.rejects(module.takeoverSupervisor({markerPath}),/unverified|live|identity/i);
|
|
114
|
+
assert.deepEqual(await readFile(markerPath),before,'live owner with image drift must retain marker');
|
|
115
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
116
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {access, mkdir, mkdtemp, readFile, rm, symlink, unlink, writeFile} from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {claimSupervisor, observeSupervisor} from '../src/autonomous-local-supervisor.mjs';
|
|
7
|
+
|
|
8
|
+
async function withJunctionFixture(nested,run){
|
|
9
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r112-claim-'));
|
|
10
|
+
const outside=path.join(root,'outside');
|
|
11
|
+
const parent=nested?path.join(root,'state','runtime'):path.join(root,'state');
|
|
12
|
+
const link=path.join(parent,'redirect');
|
|
13
|
+
await mkdir(parent,{recursive:true});
|
|
14
|
+
await mkdir(outside);
|
|
15
|
+
try {
|
|
16
|
+
await symlink(outside,link,'junction');
|
|
17
|
+
return await run({markerPath:path.join(link,'owner.json'),outside});
|
|
18
|
+
} finally {
|
|
19
|
+
await unlink(link).catch(()=>{});
|
|
20
|
+
await rm(root,{recursive:true,force:true});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for(const nested of [false,true]){
|
|
25
|
+
test(`supervisor claim rejects ${nested?'nested ':' '}parent junction before creating marker`,{skip:process.platform!=='win32'},async()=>{
|
|
26
|
+
await withJunctionFixture(nested,async({markerPath,outside})=>{
|
|
27
|
+
await assert.rejects(claimSupervisor({markerPath}),/reparse|pinned|failed closed|ancestor/i);
|
|
28
|
+
await assert.rejects(access(path.join(outside,'owner.json')),{code:'ENOENT'});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('supervisor claim does not create a missing directory through a parent junction',{skip:process.platform!=='win32'},async()=>{
|
|
34
|
+
await withJunctionFixture(false,async({markerPath,outside})=>{
|
|
35
|
+
const redirected=path.join(path.dirname(markerPath),'new-child','owner.json');
|
|
36
|
+
await assert.rejects(claimSupervisor({markerPath:redirected}),/reparse|pinned|failed closed|ancestor|missing/i);
|
|
37
|
+
await assert.rejects(access(path.join(outside,'new-child')),{code:'ENOENT'});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('supervisor claim requires an existing trusted marker parent',{skip:process.platform!=='win32'},async()=>{
|
|
42
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r112-missing-parent-'));
|
|
43
|
+
const parent=path.join(root,'new-runtime');
|
|
44
|
+
try {
|
|
45
|
+
await assert.rejects(claimSupervisor({markerPath:path.join(parent,'owner.json')}),/parent|pinned|failed closed|missing/i);
|
|
46
|
+
await assert.rejects(access(parent),{code:'ENOENT'});
|
|
47
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('live image mismatch is unverified, not a stale-owner assertion',{skip:process.platform!=='win32'},async()=>{
|
|
51
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r112-status-'));
|
|
52
|
+
const markerPath=path.join(root,'owner.json');
|
|
53
|
+
try {
|
|
54
|
+
const claim=await claimSupervisor({markerPath});
|
|
55
|
+
const modified={...claim.marker,image_sha256:'f'.repeat(64)};
|
|
56
|
+
await writeFile(markerPath,`${JSON.stringify(modified)}\n`);
|
|
57
|
+
const observed=await observeSupervisor({markerPath});
|
|
58
|
+
assert.equal(observed.reason,'identity_mismatch');
|
|
59
|
+
assert.equal(observed.observation.pid,process.pid);
|
|
60
|
+
assert.equal(observed.status,'unverified');
|
|
61
|
+
assert.deepEqual(JSON.parse(await readFile(markerPath,'utf8')),modified);
|
|
62
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
63
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {randomBytes} from 'node:crypto';
|
|
4
|
+
import childProcess from 'node:child_process';
|
|
5
|
+
import {syncBuiltinESMExports} from 'node:module';
|
|
6
|
+
import {promisify} from 'node:util';
|
|
7
|
+
import {readFileSync, renameSync, writeFileSync} from 'node:fs';
|
|
8
|
+
import {mkdtemp, readFile, rm} from 'node:fs/promises';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
const moduleUrl=new URL('../src/autonomous-local-supervisor.mjs',import.meta.url);
|
|
13
|
+
|
|
14
|
+
async function withReleaseReplacement(markerPath,run){
|
|
15
|
+
const original=childProcess.execFile;
|
|
16
|
+
let armed=false;
|
|
17
|
+
let replaced=0;
|
|
18
|
+
const replace=()=>{
|
|
19
|
+
if(!armed||replaced) return;
|
|
20
|
+
const before=readFileSync(markerPath);
|
|
21
|
+
const successor=`${markerPath}.${randomBytes(6).toString('hex')}.successor`;
|
|
22
|
+
writeFileSync(successor,before,{flag:'wx'});
|
|
23
|
+
renameSync(successor,markerPath);
|
|
24
|
+
assert.deepEqual(readFileSync(markerPath),before);
|
|
25
|
+
replaced++;
|
|
26
|
+
};
|
|
27
|
+
const wrapped=function(...args){replace();return original.apply(this,args);};
|
|
28
|
+
wrapped[promisify.custom]=function(...args){replace();return original[promisify.custom].apply(original,args);};
|
|
29
|
+
childProcess.execFile=wrapped;
|
|
30
|
+
syncBuiltinESMExports();
|
|
31
|
+
try {
|
|
32
|
+
const module=await import(`${moduleUrl.href}?cleanup=${randomBytes(8).toString('hex')}`);
|
|
33
|
+
await run({module,arm:()=>{armed=true;},replacementCount:()=>replaced});
|
|
34
|
+
assert.equal(replaced,1,'test must replace the marker at cleanup time');
|
|
35
|
+
} finally {
|
|
36
|
+
childProcess.execFile=original;
|
|
37
|
+
syncBuiltinESMExports();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('startup failure retains both primary and cleanup failure and preserves successor marker', {skip:process.platform!=='win32'}, async()=>{
|
|
42
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r113-start-'));
|
|
43
|
+
const markerPath=path.join(root,'owner.json');
|
|
44
|
+
try {
|
|
45
|
+
await withReleaseReplacement(markerPath,async({module,arm})=>{
|
|
46
|
+
const policy={get planner(){arm();throw new Error('PRIMARY_START_FAILURE');}};
|
|
47
|
+
const runtime={policy};
|
|
48
|
+
await assert.rejects(module.startLocalSupervisorHost({runtime,policy,markerPath}),error=>{
|
|
49
|
+
assert.ok(error instanceof AggregateError,'cleanup failure must not be discarded');
|
|
50
|
+
assert.match(String(error.errors[0]),/PRIMARY_START_FAILURE/);
|
|
51
|
+
assert.match(String(error.errors[1]),/changed before release: CHANGED/i);
|
|
52
|
+
return true;
|
|
53
|
+
});
|
|
54
|
+
assert.ok((await readFile(markerPath)).length>0,'uncertain successor marker must survive');
|
|
55
|
+
});
|
|
56
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('failed stop never reports stopped on retry or deletes a changed marker', {skip:process.platform!=='win32'}, async()=>{
|
|
60
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r113-stop-'));
|
|
61
|
+
const markerPath=path.join(root,'owner.json');
|
|
62
|
+
try {
|
|
63
|
+
await withReleaseReplacement(markerPath,async({module,arm,replacementCount})=>{
|
|
64
|
+
const policy={evidence_dir:root,planner:{backend:'client_sampling'}};
|
|
65
|
+
const runtime={policy};
|
|
66
|
+
const host=await module.startLocalSupervisorHost({runtime,policy,markerPath,continuous:false});
|
|
67
|
+
arm();
|
|
68
|
+
await assert.rejects(host.stop(),/changed before release: CHANGED/i);
|
|
69
|
+
assert.equal(replacementCount(),1);
|
|
70
|
+
const survivor=await readFile(markerPath);
|
|
71
|
+
await assert.rejects(host.stop(),/changed before release: CHANGED|cleanup|uncertain/i);
|
|
72
|
+
assert.deepEqual(await readFile(markerPath),survivor,'retry must not remove the byte-identical successor');
|
|
73
|
+
});
|
|
74
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('ordinary startup rejection releases owned marker and retains the primary error', {skip:process.platform!=='win32'}, async()=>{
|
|
78
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r113-start-clean-'));
|
|
79
|
+
const markerPath=path.join(root,'owner.json');
|
|
80
|
+
try {
|
|
81
|
+
const module=await import(`${moduleUrl.href}?clean=${randomBytes(8).toString('hex')}`);
|
|
82
|
+
const policy={evidence_dir:root,planner:{backend:'client_sampling'}};
|
|
83
|
+
await assert.rejects(module.startLocalSupervisorHost({runtime:{policy},policy,markerPath,idleDelayMs:0}),
|
|
84
|
+
/idleDelayMs out of range/);
|
|
85
|
+
assert.equal((await module.observeSupervisor({markerPath})).reason,'no_owner');
|
|
86
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('concurrent stop calls share one verified cleanup outcome', {skip:process.platform!=='win32'}, async()=>{
|
|
90
|
+
const root=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r113-stop-concurrent-'));
|
|
91
|
+
const markerPath=path.join(root,'owner.json');
|
|
92
|
+
try {
|
|
93
|
+
const module=await import(`${moduleUrl.href}?concurrent=${randomBytes(8).toString('hex')}`);
|
|
94
|
+
const policy={evidence_dir:root,planner:{backend:'client_sampling'}};
|
|
95
|
+
const host=await module.startLocalSupervisorHost({runtime:{policy},policy,markerPath,continuous:false});
|
|
96
|
+
const results=await Promise.all([host.stop(),host.stop()]);
|
|
97
|
+
assert.deepEqual(results,[{status:'stopped'},{status:'stopped'}]);
|
|
98
|
+
assert.equal((await module.observeSupervisor({markerPath})).reason,'no_owner');
|
|
99
|
+
} finally { await rm(root,{recursive:true,force:true}); }
|
|
100
|
+
});
|