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.
- package/MANIFEST.SHA256 +28 -18
- package/PROVENANCE.json +16 -10
- package/README.txt +16 -14
- package/RELEASE.json +14 -7
- package/SBOM.cdx.json +3 -3
- package/TEST-MATRIX.json +22 -9
- package/bin/deadbyte-process-host.exe +0 -0
- package/bin/deadbyte-process-host.obj +0 -0
- package/controller/deadbyte-controller.ps1 +4 -1
- package/controller/deadbyte-process-policy.json +1 -1
- package/docs/R51-PROVENANCE.json +18 -0
- package/docs/R51-RELEASE.json +115 -0
- package/docs/R51-TEST-MATRIX.json +166 -0
- package/docs/R52-ACTIVATION-FORENSICS.md +85 -0
- package/native/deadbyte-process-host.cpp +5 -3
- package/package.json +95 -94
- package/scripts/gate-windows-r52.ps1 +58 -0
- package/scripts/windows-gate-evidence-r52.mjs +94 -0
- package/src/coding-plane.mjs +43 -12
- package/src/coding-task-runtime.mjs +10 -4
- package/src/runtime-update.mjs +8 -2
- package/src/version.mjs +1 -1
- package/test/coding-runtime.test.mjs +70 -1
- package/test/helpers/coding-disconnect-server.mjs +11 -0
- package/test/r49-windows-gate.test.mjs +14 -9
- package/test/r51-release-contract.test.mjs +22 -20
- package/test/r52-host-revocation.test.mjs +65 -0
- package/test/r52-offline-predecessor-recovery.test.mjs +77 -0
- package/test/r52-release-contract.test.mjs +58 -0
package/src/coding-plane.mjs
CHANGED
|
@@ -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(
|
|
651
|
+
const child = spawn(executable, launchArgs, {
|
|
638
652
|
cwd: cwdResolved, env: codingEnvironment(), shell: false, windowsHide: true,
|
|
639
|
-
detached:
|
|
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',
|
|
644
|
-
collectBounded(child.stderr, command.max_output_bytes, stderrState, 'stderr',
|
|
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;
|
|
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 };
|
package/src/runtime-update.mjs
CHANGED
|
@@ -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)
|
|
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
|
@@ -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('
|
|
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-
|
|
18
|
-
assert.match(gate, /DEADBYTE 0\.15\.
|
|
19
|
-
assert.match(gate, /
|
|
20
|
-
assert.match(gate, /
|
|
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\.
|
|
25
|
-
assert.match(evidence, /pkg\.version === '0\.15\.
|
|
26
|
-
assert.match(evidence, /release\.train === '
|
|
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)
|
|
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
|
|
10
|
-
const [
|
|
11
|
-
|
|
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(
|
|
14
|
-
assert.equal(
|
|
15
|
-
assert.equal(
|
|
16
|
-
|
|
17
|
-
|
|
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,
|
|
20
|
-
assert.equal(matrix.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
|
|
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
|
|
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
|
+
});
|