deadbyte-mcp 0.15.28 → 0.15.29

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.
@@ -11,6 +11,7 @@ import { loadPrivateSigningKey, loadPublicVerifyKey } from './trust.mjs';
11
11
  import { createCodingSearch } from './coding-search.mjs';
12
12
  import { createSearchCursorCodec } from './search-cursor.mjs';
13
13
  import { atomicMutation } from './atomic-mutation.mjs';
14
+ import { fileURLToPath } from 'node:url';
14
15
 
15
16
  export const CODING_POLICY_SCHEMA = 'deadbyte.coding-policy.v1';
16
17
  export const CODING_LEASE_SCHEMA = 'deadbyte.coding-lease.v1';
@@ -591,7 +592,7 @@ export function createCodingRuntime({ policy, signingKeyPath, verifyKeyPath }) {
591
592
  receipt_sha256: signed.receipt.receipt_sha256, request_sha256: request };
592
593
  }
593
594
  async function runCommandOnce({ rootId, commandId, scriptPath, scriptSha256, args = [], cwd = '.', timeoutMs = null,
594
- executionDeadlineUtc = null, preExecutionCheck = null }) {
595
+ executionDeadlineUtc = null, preExecutionCheck = null, cancellationCheck = null }) {
595
596
  const lease = await assertCodingArmed(policy, rootId);
596
597
  if (!ID_RE.test(commandId ?? '')) throw new Error('invalid coding command id');
597
598
  const command = policy.commands[commandId];
@@ -633,28 +634,58 @@ export function createCodingRuntime({ policy, signingKeyPath, verifyKeyPath }) {
633
634
  if (deadlineBound < 100) throw new Error('coding execution deadline expires too soon to execute command');
634
635
  const effectiveTimeout = Math.min(requestedTimeout, command.timeout_ms, grantBound, deadlineBound);
635
636
  const argv = [...command.prefix_args, script.resolved, ...args];
637
+ // The internal Windows host owns a kill-on-close Job Object. Its identity
638
+ // is package-pinned, never selected by a caller or granted public PID input.
639
+ let executable=command.executable, launchArgs=argv;
640
+ if(process.platform==='win32'){
641
+ const host=fileURLToPath(new URL('../bin/deadbyte-process-host.exe',import.meta.url));
642
+ const hostPolicy=JSON.parse(await readFile(new URL('../controller/deadbyte-process-policy.json',import.meta.url),'utf8'));
643
+ if(await sha256File(host)!==hostPolicy.native_host?.sha256) throw new Error('coding process host hash changed');
644
+ executable=host;
645
+ launchArgs=[command.executable,cwdResolved,randomBytes(32).toString('hex'),'--deadbyte-transport=pipe-quiet',...argv];
646
+ }
647
+ if(cancellationCheck!==null&&typeof cancellationCheck!=='function') throw new Error('cancellation check must be internal function');
648
+ if(cancellationCheck&&await cancellationCheck()) throw new Error('coding command cancelled before dispatch');
649
+ await recheckCodingAuthority(policy,activeLease,rootId);
636
650
  const startMs = performance.now();
637
- const child = spawn(command.executable, argv, {
651
+ const child = spawn(executable, launchArgs, {
638
652
  cwd: cwdResolved, env: codingEnvironment(), shell: false, windowsHide: true,
639
- detached: false, stdio: ['ignore', 'pipe', 'pipe']
653
+ detached: process.platform!=='win32', stdio: ['ignore', 'pipe', 'pipe']
640
654
  });
655
+ const killOwned=()=>{
656
+ if(child.exitCode!==null||child.signalCode!==null)return;
657
+ if(process.platform==='win32') child.kill('SIGKILL');
658
+ else {try{process.kill(-child.pid,'SIGKILL');}catch(error){if(error.code!=='ESRCH')throw error;}}
659
+ };
641
660
  const stdoutState = { chunks: [], bytes: 0, exceeded: null };
642
661
  const stderrState = { chunks: [], bytes: 0, exceeded: null };
643
- collectBounded(child.stdout, command.max_output_bytes, stdoutState, 'stdout', () => child.kill('SIGKILL'));
644
- collectBounded(child.stderr, command.max_output_bytes, stderrState, 'stderr', () => child.kill('SIGKILL'));
662
+ collectBounded(child.stdout, command.max_output_bytes, stdoutState, 'stdout', killOwned);
663
+ collectBounded(child.stderr, command.max_output_bytes, stderrState, 'stderr', killOwned);
645
664
  let timedOut = false;
646
665
  let timer;
666
+ let cancelTimer,checking=false,cancellationError=null,finished=false;
667
+ if(cancellationCheck){
668
+ cancelTimer=setInterval(async()=>{
669
+ if(checking||finished)return;
670
+ checking=true;
671
+ try{if(await cancellationCheck())killOwned();}
672
+ catch(error){cancellationError=error;killOwned();}
673
+ finally{checking=false;}
674
+ },50);
675
+ cancelTimer.unref?.();
676
+ }
647
677
  const timerDelay = deadlineAtMs === null
648
678
  ? effectiveTimeout
649
679
  : Math.max(1, Math.min(effectiveTimeout, Math.floor(deadlineAtMs - Date.now())));
650
680
  const completion = new Promise((resolve, reject) => {
651
681
  child.once('error', reject);
652
682
  child.once('close', (code, signal) => resolve({ code, signal }));
653
- timer = setTimeout(() => { timedOut = true; child.kill('SIGKILL'); }, timerDelay);
683
+ timer = setTimeout(() => { timedOut = true; killOwned(); }, timerDelay);
654
684
  timer.unref?.();
655
685
  });
656
686
  let result;
657
- try { result = await completion; } finally { if (timer) clearTimeout(timer); }
687
+ try { result = await completion; } finally { finished=true;if (timer) clearTimeout(timer);if(cancelTimer)clearInterval(cancelTimer); }
688
+ if(cancellationError)throw cancellationError;
658
689
  if (stdoutState.exceeded || stderrState.exceeded) throw new Error(stdoutState.exceeded ?? stderrState.exceeded);
659
690
  const stdoutBytes = Buffer.concat(stdoutState.chunks);
660
691
  const stderrBytes = Buffer.concat(stderrState.chunks);
@@ -669,7 +700,7 @@ export function createCodingRuntime({ policy, signingKeyPath, verifyKeyPath }) {
669
700
  }
670
701
 
671
702
  async function commandRun({ rootId, commandId, scriptPath, scriptSha256, args = [], cwd = '.', timeoutMs = null,
672
- executionDeadlineUtc = null, preExecutionCheck = null, receiptId = null, requestSha256 = null }) {
703
+ executionDeadlineUtc = null, preExecutionCheck = null, cancellationCheck = null, receiptId = null, requestSha256 = null }) {
673
704
  const request = requestSha256 ?? requestSha('coding_command_run', {
674
705
  root_id: rootId, command_id: commandId, script_path: scriptPath, script_sha256: scriptSha256,
675
706
  args, cwd, timeout_ms: timeoutMs
@@ -704,7 +735,7 @@ export function createCodingRuntime({ policy, signingKeyPath, verifyKeyPath }) {
704
735
  }
705
736
  const execution = (async () => {
706
737
  const started = new Date().toISOString();
707
- const result = await runCommandOnce({ rootId, commandId, scriptPath, scriptSha256, args, cwd, timeoutMs, executionDeadlineUtc, preExecutionCheck });
738
+ const result = await runCommandOnce({ rootId, commandId, scriptPath, scriptSha256, args, cwd, timeoutMs, executionDeadlineUtc, preExecutionCheck, cancellationCheck });
708
739
  const signed = await signCodingReceipt(policy, signingKeyPath, result.lease, {
709
740
  operation: 'coding_command_run', request_sha256: request, root_id: rootId, command_id: commandId,
710
741
  script_path: scriptPath, executable_sha256: result.executable_sha256, script_sha256: result.script_sha256,
@@ -751,7 +782,7 @@ export function createCodingRuntime({ policy, signingKeyPath, verifyKeyPath }) {
751
782
 
752
783
  async function benchmarkRun({ rootId, commandId, scriptPath, scriptSha256, args = [], cwd = '.', timeoutMs = null,
753
784
  executionDeadlineUtc = null, preExecutionCheck = null, warmups = 1, iterations = 5, baselineMedianMs = null, maxRegressionPct = 5,
754
- receiptId = null, requestSha256 = null }) {
785
+ receiptId = null, requestSha256 = null, cancellationCheck = null }) {
755
786
  if (!Number.isInteger(warmups) || warmups < 0 || warmups > 3) throw new Error('warmups must be in [0, 3]');
756
787
  if (!Number.isInteger(iterations) || iterations < 1 || iterations > 20) throw new Error('iterations must be in [1, 20]');
757
788
  if (baselineMedianMs !== null && (!Number.isFinite(baselineMedianMs) || baselineMedianMs <= 0)) throw new Error('baseline_median_ms must be positive or null');
@@ -762,13 +793,13 @@ export function createCodingRuntime({ policy, signingKeyPath, verifyKeyPath }) {
762
793
  max_regression_pct: maxRegressionPct
763
794
  });
764
795
  for (let i = 0; i < warmups; i += 1) {
765
- const warm = await runCommandOnce({ rootId, commandId, scriptPath, scriptSha256, args, cwd, timeoutMs, executionDeadlineUtc, preExecutionCheck });
796
+ const warm = await runCommandOnce({ rootId, commandId, scriptPath, scriptSha256, args, cwd, timeoutMs, executionDeadlineUtc, preExecutionCheck, cancellationCheck });
766
797
  if (warm.timed_out || warm.exit_code !== 0) throw new Error(`benchmark warmup failed exit=${String(warm.exit_code)} timed_out=${warm.timed_out}`);
767
798
  }
768
799
  const samples = [];
769
800
  let last = null;
770
801
  for (let i = 0; i < iterations; i += 1) {
771
- last = await runCommandOnce({ rootId, commandId, scriptPath, scriptSha256, args, cwd, timeoutMs, executionDeadlineUtc, preExecutionCheck });
802
+ last = await runCommandOnce({ rootId, commandId, scriptPath, scriptSha256, args, cwd, timeoutMs, executionDeadlineUtc, preExecutionCheck, cancellationCheck });
772
803
  if (last.timed_out || last.exit_code !== 0) throw new Error(`benchmark iteration ${i + 1} failed exit=${String(last.exit_code)} timed_out=${last.timed_out}`);
773
804
  samples.push(last.elapsed_ms);
774
805
  }
@@ -474,7 +474,7 @@ async function reconcileInterrupted(ctx, plan, events, derived, codingRuntime, s
474
474
  return deriveState(plan, events);
475
475
  }
476
476
 
477
- async function executeStep(codingRuntime, plan, step, attempt) {
477
+ async function executeStep(codingRuntime, plan, step, attempt, cancellationCheck = null) {
478
478
  const receiptId = receiptIdFor(plan.task_id, step.step_id, attempt, plan.plan_sha256);
479
479
  const requestSha256 = expectedRequest(codingRuntime, step);
480
480
  if (step.kind === 'write') {
@@ -486,12 +486,12 @@ async function executeStep(codingRuntime, plan, step, attempt) {
486
486
  } else if (step.kind === 'command') {
487
487
  await codingRuntime.commandRun({ rootId: step.root_id, commandId: step.command_id, scriptPath: step.script_path,
488
488
  scriptSha256: step.script_sha256, args: step.args, cwd: step.cwd, timeoutMs: step.timeout_ms,
489
- receiptId, requestSha256 });
489
+ receiptId, requestSha256, cancellationCheck });
490
490
  } else if (step.kind === 'benchmark') {
491
491
  await codingRuntime.benchmarkRun({ rootId: step.root_id, commandId: step.command_id, scriptPath: step.script_path,
492
492
  scriptSha256: step.script_sha256, args: step.args, cwd: step.cwd, timeoutMs: step.timeout_ms,
493
493
  warmups: step.warmups, iterations: step.iterations, baselineMedianMs: step.baseline_median_ms,
494
- maxRegressionPct: step.max_regression_pct, receiptId, requestSha256 });
494
+ maxRegressionPct: step.max_regression_pct, receiptId, requestSha256, cancellationCheck });
495
495
  } else {
496
496
  throw new Error(`coding step '${step.step_id}' is not executable`);
497
497
  }
@@ -670,7 +670,10 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
670
670
  try {
671
671
  const remainingMs = timeoutMs === null ? null : Math.max(100, Math.floor(deadlineState(deadline).remaining_ms));
672
672
  const boundedStep = remainingMs === null ? next : { ...next, timeout_ms: next.timeout_ms === null || next.timeout_ms === undefined ? remainingMs : Math.min(next.timeout_ms, remainingMs) };
673
- const executed = await executeStep(codingRuntime, plan, boundedStep, attempt);
673
+ const executed = await executeStep(codingRuntime, plan, boundedStep, attempt,()=>readCancel(ctx,verifyKeyPath));
674
+ if(await consumeCancel(ctx,journal,signingKeyPath,verifyKeyPath)){
675
+ derived=deriveState(plan,journal);return {status:'cancelled',task_id:taskId,...derived};
676
+ }
674
677
  if (!executed.outcome.valid) {
675
678
  await appendEvent(ctx, journal, signingKeyPath, 'task_paused', {
676
679
  stepId: next.step_id, payload: { reason: 'post_execute_receipt_mismatch', detail: executed.outcome.reason }
@@ -700,6 +703,9 @@ export function createCodingTaskRuntime({ codingRuntime, signingKeyPath, verifyK
700
703
  }
701
704
  derived = deriveState(plan, journal);
702
705
  } catch (error) {
706
+ if(await consumeCancel(ctx,journal,signingKeyPath,verifyKeyPath)){
707
+ derived=deriveState(plan,journal);return {status:'cancelled',task_id:taskId,...derived};
708
+ }
703
709
  if (deadlineExpired()) {
704
710
  derived = await failDeadline(ctx, journal, plan, next.step_id);
705
711
  return { status: 'failed', task_id: taskId, ...derived, deadline };
@@ -704,7 +704,7 @@ async function captureRuntime(ctx){
704
704
  const bridge=await readLedger(ctx,'bridge'); const cloud=await readLedger(ctx,'cloud'); const core=coreProcess(ctx.core_root,bridge);
705
705
  return {status,status_text:statusText.trim(),bridge,cloud,core};
706
706
  }
707
- async function capturePredecessor(ctx,recoverOfflinePredecessor){
707
+ export async function capturePredecessor(ctx,recoverOfflinePredecessor){
708
708
  let statusText,status;
709
709
  try{
710
710
  statusText=controllerCaptured(ctx,'status',15000);
@@ -738,7 +738,13 @@ async function capturePredecessor(ctx,recoverOfflinePredecessor){
738
738
  }
739
739
  if(!recoverOfflinePredecessor) throw new Error(`slot update requires ONLINE runtime: ${statusText}`);
740
740
  if(!status.offline||status.bridge_pid||status.cloud_pid||status.local_ready) throw new Error(`offline predecessor recovery requires clean OFFLINE controller state: ${statusText}`);
741
- if(status.host_armed) throw new Error('offline predecessor recovery requires host plane DISARMED');
741
+ if(status.host_armed){
742
+ controllerCaptured(ctx,'disarmhost',15000);
743
+ statusText=controllerCaptured(ctx,'status',15000);
744
+ status=parseControllerStatus(statusText);
745
+ if(!status.offline||status.bridge_pid||status.cloud_pid||status.local_ready) throw new Error(`offline predecessor recovery changed controller state while revoking Host authority: ${statusText}`);
746
+ if(status.host_armed) throw new Error('offline predecessor recovery requires host plane DISARMED');
747
+ }
742
748
  if(status.autonomous_armed&&!status.coding_armed) throw new Error('invalid runtime authority: autonomous armed without coding');
743
749
  return {mode:'offline-recovery',status,status_text:statusText.trim(),runtime:null};
744
750
  }
package/src/version.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.15.28';
1
+ export const DEADBYTE_VERSION = '0.15.29';
2
2
  export const MCP_PROTOCOL_VERSION = '2026-07-28';
3
3
  export const RECEIPT_SCHEMA_V1 = 'deadbyte.receipt.v1';
4
4
  export const RECEIPT_SCHEMA_V2 = 'deadbyte.receipt.v2';
@@ -7,6 +7,75 @@ import path from 'node:path';
7
7
  import { generateTrustKeypair } from '../src/trust.mjs';
8
8
  import { assertCodingArmed, createCodingRuntime, loadCodingPolicy, recheckCodingAuthority } from '../src/coding-plane.mjs';
9
9
  import { createCodingTaskRuntime } from '../src/coding-task-runtime.mjs';
10
+ import { fork } from 'node:child_process';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ test('R52 caller disconnect retains one durable execution, status, attempts and terminal receipt',async()=>{
14
+ const f=await fixture();
15
+ let server;
16
+ try{
17
+ const script=path.join(f.project,'disconnect.mjs');
18
+ await writeFile(script,"import {appendFileSync} from 'node:fs';appendFileSync('disconnect-runs.txt',process.pid+'\\n');await new Promise(r=>setTimeout(r,750));process.stdout.write('survived-disconnect');");
19
+ const submission={submissionId:'caller-disconnect',steps:[{step_id:'gate',kind:'command',root_id:'project',command_id:'node_script',script_path:'disconnect.mjs',script_sha256:await sha256File(script),timeout_ms:10000}]};
20
+ const task=await f.tasks.submit(submission);
21
+ server=fork(fileURLToPath(new URL('./helpers/coding-disconnect-server.mjs',import.meta.url)),
22
+ [f.policyPath,f.keys.privateKeyPath,f.keys.publicKeyPath],{stdio:['ignore','ignore','ignore','ipc'],windowsHide:true,execArgv:[]});
23
+ const closed=new Promise(resolve=>server.once('exit',resolve));
24
+ server.send({taskId:task.task_id});
25
+ const deadline=Date.now()+10000;
26
+ let started=false;
27
+ while(Date.now()<deadline){
28
+ try{await readFile(path.join(f.project,'disconnect-runs.txt'));started=true;break;}catch{}
29
+ await new Promise(r=>setTimeout(r,20));
30
+ }
31
+ assert.ok(started,'independent runtime did not start');
32
+ server.disconnect(); // Caller transport goes away; execution owner stays live.
33
+ assert.doesNotThrow(()=>process.kill(server.pid,0));
34
+ const duplicate=await f.tasks.submit(submission);
35
+ assert.equal(duplicate.task_id,task.task_id);
36
+ assert.equal(duplicate.status,'existing');
37
+ assert.equal((await f.tasks.status({taskId:task.task_id})).steps[0].attempts,1);
38
+ await closed;
39
+ assert.equal(server.exitCode,0);
40
+ const recovered=createCodingTaskRuntime({codingRuntime:f.runtime,signingKeyPath:f.keys.privateKeyPath,verifyKeyPath:f.keys.publicKeyPath});
41
+ const status=await recovered.status({taskId:task.task_id});
42
+ assert.equal(status.state,'succeeded');
43
+ assert.equal(status.steps[0].attempts,1);
44
+ assert.equal((await recovered.verify({taskId:task.task_id})).ok,true);
45
+ assert.equal((await recovered.run({taskId:task.task_id})).status,'terminal');
46
+ assert.equal((await readFile(path.join(f.project,'disconnect-runs.txt'),'utf8')).trim().split(/\r?\n/).length,1);
47
+ }finally{if(server?.exitCode===null){server.kill();}await rm(f.root,{recursive:true,force:true});}
48
+ });
49
+
50
+ test('R52 cancelling a running durable command stops its owned descendants before terminal state',async()=>{
51
+ const f=await fixture();
52
+ let running;
53
+ try{
54
+ const script=path.join(f.project,'cancel-tree.mjs');
55
+ await writeFile(script,"import {spawn} from 'node:child_process'; import {writeFileSync} from 'node:fs'; const child=spawn(process.execPath,['-e',\"setTimeout(()=>require('node:fs').writeFileSync('escaped.txt','escaped'),4000)\"],{stdio:'inherit'}); writeFileSync('started.txt',String(child.pid)); setTimeout(()=>{},4000);");
56
+ const submission={submissionId:'cancel-running-tree',steps:[{step_id:'gate',kind:'command',root_id:'project',command_id:'node_script',script_path:'cancel-tree.mjs',script_sha256:await sha256File(script),timeout_ms:10000}]};
57
+ const task=await f.tasks.submit(submission);
58
+ running=f.tasks.run({taskId:task.task_id});
59
+ let descendant;
60
+ const deadline=Date.now()+3000;
61
+ while(Date.now()<deadline){
62
+ try{descendant=Number(await readFile(path.join(f.project,'started.txt'),'utf8'));break;}catch{}
63
+ await new Promise(r=>setTimeout(r,20));
64
+ }
65
+ assert.ok(descendant,'fixture did not start descendant');
66
+ const cancelledAt=Date.now();
67
+ await f.tasks.cancel({taskId:task.task_id,reason:'owned-tree-test'});
68
+ const result=await running;
69
+ assert.equal(result.status,'cancelled');
70
+ assert.ok(Date.now()-cancelledAt<2000,'cancellation waited for command to finish');
71
+ assert.throws(()=>process.kill(descendant,0),'owned descendant is still alive after terminal cancellation');
72
+ await assert.rejects(readFile(path.join(f.project,'escaped.txt')),e=>e.code==='ENOENT');
73
+ const status=await f.tasks.status({taskId:task.task_id});
74
+ assert.equal(status.steps[0].attempts,1);
75
+ const executionReceipt=await f.runtime.receiptVerify({receiptId:status.steps[0].receipt_id});
76
+ assert.equal(executionReceipt.ok,true,'cancelled execution must retain its signed terminal observation');
77
+ }finally{await running?.catch(()=>{});await rm(f.root,{recursive:true,force:true});}
78
+ });
10
79
 
11
80
  function sha256Bytes(bytes) { return createHash('sha256').update(bytes).digest('hex'); }
12
81
  async function sha256File(file) { return sha256Bytes(await readFile(file)); }
@@ -39,7 +108,7 @@ async function fixture({ armed = true } = {}) {
39
108
  }
40
109
  const runtime = createCodingRuntime({ policy, signingKeyPath: keys.privateKeyPath, verifyKeyPath: keys.publicKeyPath });
41
110
  const tasks = createCodingTaskRuntime({ codingRuntime: runtime, signingKeyPath: keys.privateKeyPath, verifyKeyPath: keys.publicKeyPath });
42
- return { root, project, policy, runtime, tasks, keys };
111
+ return { root, project, policyPath, policy, runtime, tasks, keys };
43
112
  }
44
113
  test('coding plane edits files under lease and verifies signed action receipts', async () => {
45
114
  const f = await fixture();
@@ -0,0 +1,11 @@
1
+ import { loadCodingPolicy,createCodingRuntime } from '../../src/coding-plane.mjs';
2
+ import { createCodingTaskRuntime } from '../../src/coding-task-runtime.mjs';
3
+ const [policyPath,signingKeyPath,verifyKeyPath]=process.argv.slice(2);
4
+ const policy=await loadCodingPolicy(policyPath);
5
+ const codingRuntime=createCodingRuntime({policy,signingKeyPath,verifyKeyPath});
6
+ const tasks=createCodingTaskRuntime({codingRuntime,signingKeyPath,verifyKeyPath});
7
+ process.once('message',async({taskId})=>{
8
+ try{await tasks.run({taskId});process.exitCode=0;}
9
+ catch(error){console.error(error);process.exitCode=1;}
10
+ finally{if(process.connected)process.disconnect();}
11
+ });
@@ -4,9 +4,11 @@ import test from 'node:test';
4
4
 
5
5
  const read = path => readFile(new URL(`../${path}`, import.meta.url), 'utf8');
6
6
 
7
- test('R51 canonical Windows gate is successor-bound and preserves R50/R49 historical gates', async () => {
8
- const [pkgText, gate, evidence, historicalR50, historicalR49, deferred] = await Promise.all([
7
+ test('R52 canonical Windows gate is successor-bound and preserves R51/R50/R49 historical gates', async () => {
8
+ const [pkgText, gate, evidence, historicalR51, historicalEvidenceR51, historicalR50, historicalR49, deferred] = await Promise.all([
9
9
  read('package.json'),
10
+ read('scripts/gate-windows-r52.ps1'),
11
+ read('scripts/windows-gate-evidence-r52.mjs'),
10
12
  read('scripts/gate-windows-r51.ps1'),
11
13
  read('scripts/windows-gate-evidence-r51.mjs'),
12
14
  read('scripts/gate-windows-r50.ps1'),
@@ -14,16 +16,19 @@ test('R51 canonical Windows gate is successor-bound and preserves R50/R49 histor
14
16
  read('scripts/deferred-slot-operation-r46.mjs')
15
17
  ]);
16
18
  const pkg = JSON.parse(pkgText);
17
- assert.match(pkg.scripts['gate:windows'], /gate-windows-r51\.ps1$/);
18
- assert.match(gate, /DEADBYTE 0\.15\.28 \/ R51 WINDOWS GATE: PASS/);
19
- assert.match(gate, /r51-package-boundary/);
20
- assert.match(gate, /r51-focused-contracts/);
19
+ assert.match(pkg.scripts['gate:windows'], /gate-windows-r52\.ps1$/);
20
+ assert.match(gate, /DEADBYTE 0\.15\.29 \/ R52 WINDOWS GATE: PASS/);
21
+ assert.match(gate, /r52-package-boundary/);
22
+ assert.match(gate, /r52-focused-contracts/);
21
23
  assert.match(gate, /r49-remote-control-donors\.test\.mjs/);
22
24
  assert.match(gate, /desktop-runtime-r40\.test\.mjs/);
23
25
  assert.match(gate, /manifest-final-verify/);
24
- assert.match(evidence, /deadbyte\.r51-windows-gate\.v1/);
25
- assert.match(evidence, /pkg\.version === '0\.15\.28'/);
26
- assert.match(evidence, /release\.train === 'R51'/);
26
+ assert.match(evidence, /deadbyte\.r52-windows-gate\.v1/);
27
+ assert.match(evidence, /pkg\.version === '0\.15\.29'/);
28
+ assert.match(evidence, /release\.train === 'R52'/);
29
+ assert.match(evidence, /source_predecessor\.version === '0\.15\.28'/);
30
+ assert.match(historicalR51, /DEADBYTE 0\.15\.28 \/ R51 WINDOWS GATE: PASS/);
31
+ assert.match(historicalEvidenceR51, /deadbyte\.r51-windows-gate\.v1/);
27
32
  assert.match(historicalR50, /DEADBYTE 0\.15\.27 \/ R50 WINDOWS GATE: PASS/);
28
33
  assert.match(historicalR49, /DEADBYTE 0\.15\.26 \/ R49 WINDOWS GATE: PASS/);
29
34
  assert.match(deferred, /const expectedVersion=packageIdentity\.version/);
@@ -1,31 +1,34 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
+ import { createHash } from 'node:crypto';
3
4
  import { readFile } from 'node:fs/promises';
4
5
 
5
6
  const root=new URL('../',import.meta.url);
6
- const read=rel=>readFile(new URL(rel,root),'utf8');
7
- const json=rel=>read(rel).then(JSON.parse);
7
+ const read=rel=>readFile(new URL(rel,root));
8
+ const json=rel=>read(rel).then(bytes=>JSON.parse(bytes.toString('utf8')));
9
+ const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
8
10
 
9
- test('R51 release identity is coherent across package lock source metadata SBOM and gate',async()=>{
10
- const [pkg,lock,release,provenance,matrix,sbom,versionSource]=await Promise.all([
11
- json('package.json'),json('package-lock.json'),json('RELEASE.json'),json('PROVENANCE.json'),json('TEST-MATRIX.json'),json('SBOM.cdx.json'),read('src/version.mjs')
11
+ test('R51 historical release metadata remains byte-frozen at published 0.15.28',async()=>{
12
+ const [releaseBytes,provenanceBytes,matrixBytes]=await Promise.all([
13
+ read('docs/R51-RELEASE.json'),
14
+ read('docs/R51-PROVENANCE.json'),
15
+ read('docs/R51-TEST-MATRIX.json')
12
16
  ]);
13
- assert.equal(pkg.version,'0.15.28');
14
- assert.equal(lock.version,pkg.version);
15
- assert.equal(lock.packages[''].version,pkg.version);
16
- assert.match(versionSource,/DEADBYTE_VERSION = '0\.15\.28'/);
17
- assert.equal(release.version,pkg.version);
17
+ assert.equal(sha256(releaseBytes),'b89d0eaf4f07d704d2170782f10e4647daec55b5c5bdc70473865c0dae88650b');
18
+ assert.equal(sha256(provenanceBytes),'78e6798e92c2d9134b2e66557b2de7863367124efbcecdd5c91012bfed19a48f');
19
+ assert.equal(sha256(matrixBytes),'eb4d8047fe4ead69a73b3d89f2d443b6fdd88bd39bd8455b80ecaf512cb49abf');
20
+ const release=JSON.parse(releaseBytes.toString('utf8'));
21
+ const provenance=JSON.parse(provenanceBytes.toString('utf8'));
22
+ const matrix=JSON.parse(matrixBytes.toString('utf8'));
23
+ assert.equal(release.version,'0.15.28');
18
24
  assert.equal(release.train,'R51');
19
- assert.equal(provenance.version,pkg.version);
20
- assert.equal(matrix.version,pkg.version);
25
+ assert.equal(provenance.version,'0.15.28');
26
+ assert.equal(matrix.version,'0.15.28');
21
27
  assert.equal(matrix.train,'R51');
22
- assert.equal(sbom.metadata.component.version,pkg.version);
23
- assert.equal(sbom.serialNumber,'urn:cdx:deadbyte-mcp:r51:0.15.28');
24
- assert.match(pkg.scripts['gate:windows'],/gate-windows-r51\.ps1/);
25
28
  });
26
29
 
27
- test('R51 lineage binds exact immutable published 0.15.27 parent',async()=>{
28
- const [release,provenance]=await Promise.all([json('RELEASE.json'),json('PROVENANCE.json')]);
30
+ test('R51 historical lineage binds exact 0.15.27 parent and 0.15.27 source predecessor',async()=>{
31
+ const [release,provenance]=await Promise.all([json('docs/R51-RELEASE.json'),json('docs/R51-PROVENANCE.json')]);
29
32
  const expected={
30
33
  release_id:'v0.15.27-1b1dbe24b45ad06a',
31
34
  manifest_sha256:'1b1dbe24b45ad06a62771d15f510662952f3d7c3e5aa08053c6c61511c523e47',
@@ -38,11 +41,10 @@ test('R51 lineage binds exact immutable published 0.15.27 parent',async()=>{
38
41
  assert.equal(provenance.parent_release_id,expected.release_id);
39
42
  assert.equal(provenance.parent_manifest_sha256,expected.manifest_sha256);
40
43
  assert.equal(provenance.parent_generation_manifest_sha256,expected.generation_manifest_sha256);
41
- assert.equal(provenance.source_predecessor_tarball_sha256,release.source_predecessor.tarball_sha256);
42
44
  });
43
45
 
44
- test('R51 release advertises only implemented coherence single-flight authority and livelog hardening',async()=>{
45
- const release=await json('RELEASE.json');
46
+ test('R51 historical capability claims remain frozen',async()=>{
47
+ const release=await json('docs/R51-RELEASE.json');
46
48
  for(const key of [
47
49
  'runtime_package_auto_coherence',
48
50
  'coding_command_singleflight',
@@ -0,0 +1,65 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { spawnSync } from 'node:child_process';
5
+
6
+ async function runRevoke(t,prelude){
7
+ if(process.platform!=='win32'){t.skip('Windows controller behavior');return;}
8
+ const source=await readFile(new URL('../controller/deadbyte-controller.ps1',import.meta.url),'utf8');
9
+ const begin=source.indexOf('function Disarm-Host(');
10
+ const end=source.indexOf('function Show-HostStatus(',begin);
11
+ assert.ok(begin>=0&&end>begin);
12
+ // Execute the maintained function, not a copied implementation. Simulate a
13
+ // filesystem that refuses deletion while Test-Path still sees the grant.
14
+ const script=`$ErrorActionPreference='Stop'
15
+ function Get-HostPolicyMetadata($Cfg) { return @{ArmFile='fixture-host-grant'} }
16
+ ${prelude}
17
+ ${source.slice(begin,end)}
18
+ try { Disarm-Host @{}; exit 0 } catch { Write-Output $_.Exception.Message; exit 7 }
19
+ `;
20
+ return spawnSync('C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
21
+ ['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',Buffer.from(script,'utf16le').toString('base64')],
22
+ {encoding:'utf8',windowsHide:true,timeout:15000});
23
+ }
24
+
25
+ test('R52 controller never reports successful Host revoke while grant removal failed',async t=>{
26
+ const result=await runRevoke(t,`
27
+ function Remove-Item { param($LiteralPath,[switch]$Force,$ErrorAction) if($ErrorAction -eq 'Stop'){throw 'fixture access denied'} }
28
+ function Test-Path { param($LiteralPath) return $true }
29
+ `);
30
+ if(!result)return;
31
+ assert.equal(result.status,7,`Host revoke falsely succeeded: ${result.stdout} ${result.stderr}`);
32
+ assert.doesNotMatch(result.stdout,/PRIVILEGED HOST PLANE DISARMED/);
33
+ });
34
+
35
+ test('R52 controller rejects a grant that remains after nominally successful removal',async t=>{
36
+ const result=await runRevoke(t,`
37
+ function Remove-Item { param($LiteralPath,[switch]$Force,$ErrorAction) }
38
+ function Test-Path { param($LiteralPath) return $true }
39
+ `);
40
+ if(!result)return;
41
+ assert.equal(result.status,7);
42
+ assert.match(result.stdout,/grant remains present/);
43
+ assert.doesNotMatch(result.stdout,/PRIVILEGED HOST PLANE DISARMED/);
44
+ });
45
+
46
+ test('R52 controller revocation is idempotent when no grant exists',async t=>{
47
+ const result=await runRevoke(t,`
48
+ function Remove-Item { throw 'must not remove absent grant' }
49
+ function Test-Path { param($LiteralPath) return $false }
50
+ `);
51
+ if(!result)return;
52
+ assert.equal(result.status,0);
53
+ assert.match(result.stdout,/PRIVILEGED HOST PLANE DISARMED/);
54
+ });
55
+
56
+ test('R52 controller reports disarmed only after observing successful removal',async t=>{
57
+ const result=await runRevoke(t,`
58
+ $script:present=$true
59
+ function Remove-Item { param($LiteralPath,[switch]$Force,$ErrorAction) $script:present=$false }
60
+ function Test-Path { param($LiteralPath) return $script:present }
61
+ `);
62
+ if(!result)return;
63
+ assert.equal(result.status,0);
64
+ assert.match(result.stdout,/PRIVILEGED HOST PLANE DISARMED/);
65
+ });
@@ -0,0 +1,77 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import * as runtimeUpdate from '../src/runtime-update.mjs';
7
+
8
+ async function fixture(t,{revoke='success',armed=true}={}) {
9
+ const base=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r52-offline-host-'));
10
+ t.after(()=>rm(base,{recursive:true,force:true}));
11
+ const controllerDir=path.join(base,'controller');
12
+ const runtimeDir=path.join(controllerDir,'runtime');
13
+ await mkdir(runtimeDir,{recursive:true});
14
+ const marker=path.join(runtimeDir,'host-grant.json');
15
+ if(armed) await writeFile(marker,'stale-host-grant\n','utf8');
16
+ const revokeSource=revoke==='failure' ? 'exit 7' : revoke==='unchanged' ? 'exit 0' : 'Remove-Item -LiteralPath $marker -Force -ErrorAction SilentlyContinue; exit 0';
17
+ const controller=[
18
+ 'param([string]$Command)',
19
+ '$marker = Join-Path $PSScriptRoot "runtime\\host-grant.json"',
20
+ `if($Command -eq "disarmhost"){ ${revokeSource} }`,
21
+ 'if($Command -eq "status"){',
22
+ ' if(Test-Path -LiteralPath $marker){ $hostState = "ARMED [standing-grant]" } else { $hostState = "DISARMED [disarmed]" }',
23
+ ' Write-Output "DEADBYTE MCP = OFFLINE"',
24
+ ' Write-Output "Bridge PID : -"',
25
+ ' Write-Output "Cloud PID : -"',
26
+ ' Write-Output "Local ready : False"',
27
+ ' Write-Output "Process plane : DISARMED [disarmed]"',
28
+ ' Write-Output "Coding plane : DISARMED [disarmed]"',
29
+ ' Write-Output "Autonomous : DISARMED [disarmed]"',
30
+ ' Write-Output "Desktop plane : DISARMED [disarmed]"',
31
+ ' Write-Output ("Host plane : {0}" -f $hostState)',
32
+ ' exit 0',
33
+ '}',
34
+ 'exit 9'
35
+ ].join('\r\n');
36
+ await writeFile(path.join(controllerDir,'deadbyte-controller.ps1'),controller,'utf8');
37
+ return {base,marker};
38
+ }
39
+
40
+ test('R52 offline predecessor recovery revokes stale Host authority before capture', async t => {
41
+ if(process.platform!=='win32'){ t.skip('Windows controller regression'); return; }
42
+ assert.equal(typeof runtimeUpdate.capturePredecessor,'function','capturePredecessor must be behavior-testable');
43
+ const {base,marker}=await fixture(t);
44
+ const result=await runtimeUpdate.capturePredecessor(runtimeUpdate.legacyContext(base),true);
45
+ assert.equal(result.mode,'offline-recovery');
46
+ assert.equal(result.status.host_armed,false);
47
+ await assert.rejects(readFile(marker,'utf8'),error=>error?.code==='ENOENT');
48
+ });
49
+
50
+ test('R52 offline predecessor fails closed if Host revoke returns success but grant remains',async t=>{
51
+ if(process.platform!=='win32'){t.skip('Windows controller regression');return;}
52
+ const {base,marker}=await fixture(t,{revoke:'unchanged'});
53
+ await assert.rejects(runtimeUpdate.capturePredecessor(runtimeUpdate.legacyContext(base),true),/requires host plane DISARMED/);
54
+ assert.equal(await readFile(marker,'utf8'),'stale-host-grant\n');
55
+ });
56
+
57
+ test('R52 failed Host revoke leaves predecessor grant intact and denies capture',async t=>{
58
+ if(process.platform!=='win32'){t.skip('Windows controller regression');return;}
59
+ const {base,marker}=await fixture(t,{revoke:'failure'});
60
+ await assert.rejects(runtimeUpdate.capturePredecessor(runtimeUpdate.legacyContext(base),true));
61
+ assert.equal(await readFile(marker,'utf8'),'stale-host-grant\n');
62
+ });
63
+
64
+ test('R52 disarmed offline predecessor is accepted without invoking a failing revoke',async t=>{
65
+ if(process.platform!=='win32'){t.skip('Windows controller regression');return;}
66
+ const {base}=await fixture(t,{revoke:'failure',armed:false});
67
+ const result=await runtimeUpdate.capturePredecessor(runtimeUpdate.legacyContext(base),true);
68
+ assert.equal(result.mode,'offline-recovery');
69
+ assert.equal(result.status.host_armed,false);
70
+ });
71
+
72
+ test('R52 offline recovery requires explicit opt-in before any Host revocation',async t=>{
73
+ if(process.platform!=='win32'){t.skip('Windows controller regression');return;}
74
+ const {base,marker}=await fixture(t);
75
+ await assert.rejects(runtimeUpdate.capturePredecessor(runtimeUpdate.legacyContext(base),false),/requires ONLINE runtime/);
76
+ assert.equal(await readFile(marker,'utf8'),'stale-host-grant\n');
77
+ });
@@ -0,0 +1,58 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { readFile } from 'node:fs/promises';
4
+
5
+ const root=new URL('../',import.meta.url);
6
+ const read=rel=>readFile(new URL(rel,root),'utf8');
7
+ const json=rel=>read(rel).then(JSON.parse);
8
+
9
+ test('R52 current release identity is coherent across package lock source metadata SBOM and gate',async()=>{
10
+ const [pkg,lock,release,provenance,matrix,sbom,versionSource]=await Promise.all([
11
+ json('package.json'),json('package-lock.json'),json('RELEASE.json'),json('PROVENANCE.json'),json('TEST-MATRIX.json'),json('SBOM.cdx.json'),read('src/version.mjs')
12
+ ]);
13
+ assert.equal(pkg.version,'0.15.29');
14
+ assert.equal(lock.version,pkg.version);
15
+ assert.equal(lock.packages[''].version,pkg.version);
16
+ assert.match(versionSource,/DEADBYTE_VERSION = '0\.15\.29'/);
17
+ assert.equal(release.version,pkg.version);
18
+ assert.equal(release.train,'R52');
19
+ assert.equal(provenance.version,pkg.version);
20
+ assert.equal(matrix.version,pkg.version);
21
+ assert.equal(matrix.train,'R52');
22
+ assert.equal(sbom.metadata.component.version,pkg.version);
23
+ assert.equal(sbom.serialNumber,'urn:cdx:deadbyte-mcp:r52:0.15.29');
24
+ assert.match(pkg.scripts['gate:windows'],/gate-windows-r52\.ps1/);
25
+ assert.match(pkg.scripts['r52:focused:test'],/r52-offline-predecessor-recovery\.test\.mjs/);
26
+ });
27
+
28
+ test('R52 lineage binds published 0.15.28 parent separately from 0.15.27 runtime rollback target',async()=>{
29
+ const [release,provenance]=await Promise.all([json('RELEASE.json'),json('PROVENANCE.json')]);
30
+ const activeParent={
31
+ release_id:'v0.15.27-1b1dbe24b45ad06a',
32
+ manifest_sha256:'1b1dbe24b45ad06a62771d15f510662952f3d7c3e5aa08053c6c61511c523e47',
33
+ generation_manifest_sha256:'f87a1637b3c55ba050d079d8a5e0862a2244b306be7e985880708e0d8ded3db0'
34
+ };
35
+ assert.deepEqual(release.runtime_predecessor,activeParent);
36
+ assert.equal(release.parent.release_id,'v0.15.28-b879bdff63478f28');
37
+ assert.equal(release.parent.version,'0.15.28');
38
+ assert.equal(release.parent.manifest_sha256,release.source_predecessor.manifest_file_sha256);
39
+ assert.equal(release.parent.tarball_sha256,release.source_predecessor.tarball_sha256);
40
+ assert.equal(release.parent.generation_manifest_sha256,undefined);
41
+ assert.equal(release.source_predecessor.version,'0.15.28');
42
+ assert.equal(release.source_predecessor.tarball_sha256,'0a74332ba715d80a532dc871ece4826bdb917b8c7a442e71388a06d44698c411');
43
+ assert.equal(release.source_predecessor.manifest_file_sha256,'b879bdff63478f28d5dafadeedc8182a8fd090ab676dc0ff3566ea51ee07326f');
44
+ assert.equal(provenance.parent_release_id,release.parent.release_id);
45
+ assert.equal(provenance.parent_manifest_sha256,release.parent.manifest_sha256);
46
+ assert.equal(provenance.parent_tarball_sha256,release.parent.tarball_sha256);
47
+ assert.deepEqual(provenance.runtime_predecessor,activeParent);
48
+ assert.equal(provenance.parent_generation_manifest_sha256,undefined);
49
+ assert.equal(provenance.source_predecessor_version,'0.15.28');
50
+ assert.equal(provenance.source_predecessor_tarball_sha256,release.source_predecessor.tarball_sha256);
51
+ assert.equal(provenance.source_predecessor_manifest_file_sha256,release.source_predecessor.manifest_file_sha256);
52
+ assert.equal(provenance.source_predecessor_activation_failure_receipt_sha256,'090f165c96da0d5a8e9c0815fd389fe94e8fe36c23a6878ee825b8f46bfb0bc2');
53
+ });
54
+
55
+ test('R52 advertises updater-owned offline predecessor Host revocation only after implementation',async()=>{
56
+ const release=await json('RELEASE.json');
57
+ assert.equal(release.capabilities.offline_predecessor_host_revoke_before_capture,true);
58
+ });