deadbyte-mcp 0.15.32 → 0.15.34
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 +19 -11
- package/PROVENANCE.json +27 -22
- package/README.txt +22 -14
- package/RELEASE.json +29 -22
- package/SBOM.cdx.json +3 -3
- package/TEST-MATRIX.json +61 -33
- package/controller/deadbyte-process-policy.json +4 -4
- package/package.json +5 -3
- package/scripts/gate-windows-r56.ps1 +27 -0
- package/scripts/gate-windows-r57.ps1 +27 -0
- package/scripts/windows-gate-evidence-r56.mjs +81 -0
- package/scripts/windows-gate-evidence-r57.mjs +81 -0
- package/src/runtime-update.mjs +100 -9
- package/src/version.mjs +1 -1
- package/test/r49-windows-gate.test.mjs +19 -16
- package/test/r55-release-contract.test.mjs +5 -3
- package/test/r56-release-contract.test.mjs +65 -0
- package/test/r56-rollback-launcher-restore.test.mjs +103 -0
- package/test/r57-core-startup-race.test.mjs +85 -0
- package/test/r57-release-contract.test.mjs +65 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
Set-StrictMode -Version 3.0
|
|
3
|
+
|
|
4
|
+
function Invoke-GateStep {
|
|
5
|
+
param([Parameter(Mandatory)][string]$Name,[Parameter(Mandatory)][string[]]$NpmArgs)
|
|
6
|
+
Write-Host "==== $Name ===="
|
|
7
|
+
& npm.cmd @NpmArgs
|
|
8
|
+
if ($LASTEXITCODE -ne 0) { throw "DEADBYTE 0.15.34 / R57 gate failed at $Name with exit code $LASTEXITCODE" }
|
|
9
|
+
}
|
|
10
|
+
function Invoke-NodeTest {
|
|
11
|
+
param([Parameter(Mandatory)][string]$Name,[Parameter(Mandatory)][string[]]$TestFiles)
|
|
12
|
+
Write-Host "==== $Name ===="
|
|
13
|
+
& node.exe --test --test-concurrency=1 @TestFiles
|
|
14
|
+
if ($LASTEXITCODE -ne 0) { throw "DEADBYTE 0.15.34 / R57 gate failed at $Name with exit code $LASTEXITCODE" }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Invoke-NodeTest 'r57-package-boundary' @('test/package-boundary.test.mjs')
|
|
18
|
+
Invoke-GateStep 'r57-focused-contracts' @('run','r57:focused:test')
|
|
19
|
+
Write-Host '==== full-regression-evidence ===='
|
|
20
|
+
$matrix=Get-Content -LiteralPath (Join-Path (Split-Path $PSScriptRoot -Parent) 'TEST-MATRIX.json') -Raw | ConvertFrom-Json
|
|
21
|
+
if($matrix.observed_pre_release_evidence.full_regression.status -ne 'PASS'){ throw 'R57 full regression evidence is not PASS' }
|
|
22
|
+
Invoke-GateStep 'manifest-write' @('run','manifest:write')
|
|
23
|
+
Invoke-GateStep 'manifest-verify' @('run','manifest:verify')
|
|
24
|
+
Invoke-GateStep 'doctor' @('run','doctor')
|
|
25
|
+
& node.exe (Join-Path $PSScriptRoot 'windows-gate-evidence-r57.mjs')
|
|
26
|
+
if ($LASTEXITCODE -ne 0) { throw "R57 evidence writer failed with exit code $LASTEXITCODE" }
|
|
27
|
+
Write-Host '==== DEADBYTE 0.15.34 / R57 WINDOWS GATE: PASS ===='
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { doctor } from '../src/doctor-r45.mjs';
|
|
7
|
+
|
|
8
|
+
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
9
|
+
const stateRoot=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
|
|
10
|
+
const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
11
|
+
const insist=(ok,message)=>{if(!ok)throw new Error(message);};
|
|
12
|
+
const pkg=JSON.parse(await readFile(path.join(root,'package.json'),'utf8'));
|
|
13
|
+
const release=JSON.parse(await readFile(path.join(root,'RELEASE.json'),'utf8'));
|
|
14
|
+
const provenance=JSON.parse(await readFile(path.join(root,'PROVENANCE.json'),'utf8'));
|
|
15
|
+
const matrix=JSON.parse(await readFile(path.join(root,'TEST-MATRIX.json'),'utf8'));
|
|
16
|
+
const active=JSON.parse(await readFile(path.join(stateRoot,'active-release.json'),'utf8'));
|
|
17
|
+
const manifestSha256=sha256(await readFile(path.join(root,'MANIFEST.SHA256')));
|
|
18
|
+
|
|
19
|
+
insist(pkg.version==='0.15.33'&&release.version===pkg.version&&release.train==='R56','R56 candidate identity mismatch');
|
|
20
|
+
insist(release.parent.version==='0.15.32','R56 parent mismatch');
|
|
21
|
+
insist(release.parent.manifest_sha256==='e7223f467777f5dfeb20fd2cc7a6eca22619a44ed95e5bc0b32d9c7aabba361b','R56 parent manifest mismatch');
|
|
22
|
+
insist(release.parent.generation_manifest_sha256==='740443fd207c5e6ffd5952079214bf6e81e9281a6ec880de9bbe3b8ce1deb00e','R56 parent generation mismatch');
|
|
23
|
+
insist(release.parent.tarball_sha256==='c9e1f5875dd2177cee0089305b40321c128c54ce7f5522e34e507d4ecfb22638','R56 parent tarball mismatch');
|
|
24
|
+
insist(
|
|
25
|
+
active.release_id===release.runtime_predecessor.release_id&&
|
|
26
|
+
active.manifest_sha256===release.runtime_predecessor.manifest_sha256&&
|
|
27
|
+
active.generation_manifest_sha256===release.runtime_predecessor.generation_manifest_sha256,
|
|
28
|
+
'active runtime is not exact sealed 0.15.32 predecessor'
|
|
29
|
+
);
|
|
30
|
+
insist(release.hotfix?.signed_handoff_launcher_snapshot_required===true,'R56 signed rollback handoff launcher requirement missing');
|
|
31
|
+
insist(release.hotfix?.stored_predecessor_verified_without_live_launcher===true,'R56 stored predecessor verification split missing');
|
|
32
|
+
insist(release.hotfix?.predecessor_launcher_restored_before_pointer_commit===true,'R56 predecessor launcher ordering missing');
|
|
33
|
+
insist(release.hotfix?.successor_launcher_restored_before_recovery_pointer===true,'R56 successor recovery ordering missing');
|
|
34
|
+
insist(release.hotfix?.full_regression==='PASS','R56 full regression metadata is not PASS');
|
|
35
|
+
insist(matrix.observed_pre_release_evidence?.full_regression?.status==='PASS','R56 full regression evidence is not PASS');
|
|
36
|
+
insist(provenance.operator_override?.stable_final_claim===false,'R56 must not claim stable/final');
|
|
37
|
+
insist(matrix.operator_override?.four_hour_soak==='NOT_COMPLETED','R56 matrix must preserve soak bypass');
|
|
38
|
+
|
|
39
|
+
const health=await doctor();
|
|
40
|
+
insist(health.status==='ok'&&health.package_version===pkg.version&&health.manifest?.verified===true,'R56 doctor evidence invalid');
|
|
41
|
+
|
|
42
|
+
const evidence={
|
|
43
|
+
schema:'deadbyte.r56-windows-gate.v1',
|
|
44
|
+
status:'passed',
|
|
45
|
+
exit_code:0,
|
|
46
|
+
version:pkg.version,
|
|
47
|
+
train:'R56',
|
|
48
|
+
completed_at_utc:new Date().toISOString(),
|
|
49
|
+
manifest_sha256:manifestSha256,
|
|
50
|
+
parent:release.parent,
|
|
51
|
+
runtime_predecessor:release.runtime_predecessor,
|
|
52
|
+
hotfix:release.hotfix,
|
|
53
|
+
operator_override:provenance.operator_override,
|
|
54
|
+
active_predecessor:{
|
|
55
|
+
release_id:active.release_id,
|
|
56
|
+
version:active.version,
|
|
57
|
+
manifest_sha256:active.manifest_sha256,
|
|
58
|
+
generation_manifest_sha256:active.generation_manifest_sha256
|
|
59
|
+
},
|
|
60
|
+
doctor:{verified_entries:health.manifest.verified_entries},
|
|
61
|
+
gates:{
|
|
62
|
+
r56_focused:true,
|
|
63
|
+
package_boundary:true,
|
|
64
|
+
full_regression:true,
|
|
65
|
+
manifest_final_verify:true,
|
|
66
|
+
four_hour_soak_completed:false
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const bytes=Buffer.from(JSON.stringify(evidence,null,2)+'\n');
|
|
70
|
+
const dir=path.join(stateRoot,'evidence','release');
|
|
71
|
+
await mkdir(dir,{recursive:true});
|
|
72
|
+
const target=path.join(dir,`r56-windows-gate-${manifestSha256.slice(0,16)}.json`);
|
|
73
|
+
try{
|
|
74
|
+
const handle=await open(target,'wx',0o600);
|
|
75
|
+
try{await handle.writeFile(bytes);await handle.sync();}finally{await handle.close();}
|
|
76
|
+
}catch(error){
|
|
77
|
+
if(error?.code!=='EEXIST')throw error;
|
|
78
|
+
const prior=JSON.parse(await readFile(target,'utf8'));
|
|
79
|
+
insist(prior.schema===evidence.schema&&prior.status==='passed'&&prior.version===evidence.version&&prior.manifest_sha256===manifestSha256,'conflicting R56 gate evidence identity');
|
|
80
|
+
}
|
|
81
|
+
console.log(JSON.stringify({status:'passed',path:target,sha256:sha256(bytes),manifest_sha256:manifestSha256}));
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, open, readFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { doctor } from '../src/doctor-r45.mjs';
|
|
7
|
+
|
|
8
|
+
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
9
|
+
const stateRoot=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
|
|
10
|
+
const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
11
|
+
const insist=(ok,message)=>{if(!ok)throw new Error(message);};
|
|
12
|
+
const pkg=JSON.parse(await readFile(path.join(root,'package.json'),'utf8'));
|
|
13
|
+
const release=JSON.parse(await readFile(path.join(root,'RELEASE.json'),'utf8'));
|
|
14
|
+
const provenance=JSON.parse(await readFile(path.join(root,'PROVENANCE.json'),'utf8'));
|
|
15
|
+
const matrix=JSON.parse(await readFile(path.join(root,'TEST-MATRIX.json'),'utf8'));
|
|
16
|
+
const active=JSON.parse(await readFile(path.join(stateRoot,'active-release.json'),'utf8'));
|
|
17
|
+
const manifestSha256=sha256(await readFile(path.join(root,'MANIFEST.SHA256')));
|
|
18
|
+
|
|
19
|
+
insist(pkg.version==='0.15.34'&&release.version===pkg.version&&release.train==='R57','R57 candidate identity mismatch');
|
|
20
|
+
insist(release.parent.version==='0.15.33','R57 parent mismatch');
|
|
21
|
+
insist(release.parent.manifest_sha256==='776a51cb916e28ddc3cbb2ecd3857151a0e73e0aaf83d4e6308e6674125dd01c','R57 parent manifest mismatch');
|
|
22
|
+
insist(release.parent.generation_manifest_sha256==='20ccc5e4bbf8470b61e0a6d2ad181dad5e8533da980ceba7749e1d6f48d9dbd3','R57 parent generation mismatch');
|
|
23
|
+
insist(release.parent.tarball_sha256==='d84a6df0a972a43c50d5e31242d9217f59858b2d73c4284af296da76d2048e5f','R57 parent tarball mismatch');
|
|
24
|
+
insist(
|
|
25
|
+
active.release_id===release.runtime_predecessor.release_id&&
|
|
26
|
+
active.manifest_sha256===release.runtime_predecessor.manifest_sha256&&
|
|
27
|
+
active.generation_manifest_sha256===release.runtime_predecessor.generation_manifest_sha256,
|
|
28
|
+
'active runtime is not exact sealed 0.15.33 predecessor'
|
|
29
|
+
);
|
|
30
|
+
insist(release.hotfix?.bounded_core_discovery_after_online===true,'R57 bounded core discovery missing');
|
|
31
|
+
insist(release.hotfix?.retry_only_missing_core===true,'R57 zero-child-only retry contract missing');
|
|
32
|
+
insist(release.hotfix?.ambiguous_core_fail_closed===true,'R57 ambiguous-core fail-closed contract missing');
|
|
33
|
+
insist(release.hotfix?.timeout_fail_closed===true,'R57 timeout fail-closed contract missing');
|
|
34
|
+
insist(release.hotfix?.full_regression==='PASS','R57 full regression metadata is not PASS');
|
|
35
|
+
insist(matrix.observed_pre_release_evidence?.full_regression?.status==='PASS','R57 full regression evidence is not PASS');
|
|
36
|
+
insist(provenance.operator_override?.stable_final_claim===false,'R57 must not claim stable/final');
|
|
37
|
+
insist(matrix.operator_override?.four_hour_soak==='NOT_COMPLETED','R57 matrix must preserve soak bypass');
|
|
38
|
+
|
|
39
|
+
const health=await doctor();
|
|
40
|
+
insist(health.status==='ok'&&health.package_version===pkg.version&&health.manifest?.verified===true,'R57 doctor evidence invalid');
|
|
41
|
+
|
|
42
|
+
const evidenceBody={
|
|
43
|
+
schema:'deadbyte.r57-windows-gate.v1',
|
|
44
|
+
status:'passed',
|
|
45
|
+
exit_code:0,
|
|
46
|
+
version:pkg.version,
|
|
47
|
+
train:'R57',
|
|
48
|
+
completed_at_utc:new Date().toISOString(),
|
|
49
|
+
manifest_sha256:manifestSha256,
|
|
50
|
+
parent:release.parent,
|
|
51
|
+
runtime_predecessor:release.runtime_predecessor,
|
|
52
|
+
hotfix:release.hotfix,
|
|
53
|
+
operator_override:provenance.operator_override,
|
|
54
|
+
active_predecessor:{
|
|
55
|
+
release_id:active.release_id,
|
|
56
|
+
version:active.version,
|
|
57
|
+
manifest_sha256:active.manifest_sha256,
|
|
58
|
+
generation_manifest_sha256:active.generation_manifest_sha256
|
|
59
|
+
},
|
|
60
|
+
doctor:{verified_entries:health.manifest.verified_entries},
|
|
61
|
+
gates:{
|
|
62
|
+
r57_focused:true,
|
|
63
|
+
package_boundary:true,
|
|
64
|
+
full_regression:true,
|
|
65
|
+
manifest_final_verify:true,
|
|
66
|
+
four_hour_soak_completed:false
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const bytes=Buffer.from(JSON.stringify(evidenceBody,null,2)+'\n');
|
|
70
|
+
const dir=path.join(stateRoot,'evidence','release');
|
|
71
|
+
await mkdir(dir,{recursive:true});
|
|
72
|
+
const target=path.join(dir,`r57-windows-gate-${manifestSha256.slice(0,16)}.json`);
|
|
73
|
+
try{
|
|
74
|
+
const handle=await open(target,'wx',0o600);
|
|
75
|
+
try{await handle.writeFile(bytes);await handle.sync();}finally{await handle.close();}
|
|
76
|
+
}catch(error){
|
|
77
|
+
if(error?.code!=='EEXIST')throw error;
|
|
78
|
+
const prior=JSON.parse(await readFile(target,'utf8'));
|
|
79
|
+
insist(prior.schema===evidenceBody.schema&&prior.status==='passed'&&prior.version===evidenceBody.version&&prior.manifest_sha256===manifestSha256,'conflicting R57 gate evidence identity');
|
|
80
|
+
}
|
|
81
|
+
console.log(JSON.stringify({status:'passed',path:target,sha256:sha256(bytes),manifest_sha256:manifestSha256}));
|
package/src/runtime-update.mjs
CHANGED
|
@@ -743,6 +743,37 @@ export function selectCoreProcess(processes,coreRoot,bridgePid,verifiedBridge=nu
|
|
|
743
743
|
function coreProcess(coreRoot,bridge){
|
|
744
744
|
return selectCoreProcess(nodeProcesses(),coreRoot,bridge.pid,bridge);
|
|
745
745
|
}
|
|
746
|
+
export async function waitForCoreProcess(coreRoot,bridge,{
|
|
747
|
+
timeoutMs=10000,
|
|
748
|
+
pollMs=100,
|
|
749
|
+
processProvider=nodeProcesses,
|
|
750
|
+
sleepFn=sleep,
|
|
751
|
+
nowFn=Date.now
|
|
752
|
+
}={}){
|
|
753
|
+
const bridgePid=Number(bridge?.pid);
|
|
754
|
+
if(!Number.isInteger(bridgePid)||bridgePid<1) throw new Error('downstream core wait requires verified bridge PID');
|
|
755
|
+
if(!Number.isFinite(timeoutMs)||timeoutMs<0) throw new Error('downstream core wait timeout invalid');
|
|
756
|
+
if(!Number.isFinite(pollMs)||pollMs<1) throw new Error('downstream core wait poll invalid');
|
|
757
|
+
if(typeof processProvider!=='function'||typeof sleepFn!=='function'||typeof nowFn!=='function') throw new Error('downstream core wait hooks invalid');
|
|
758
|
+
const started=Number(nowFn());
|
|
759
|
+
let lastError=null;
|
|
760
|
+
while(true){
|
|
761
|
+
try {
|
|
762
|
+
const processes=await processProvider();
|
|
763
|
+
return selectCoreProcess(processes,coreRoot,bridgePid,bridge);
|
|
764
|
+
} catch(error){
|
|
765
|
+
const message=String(error?.message??error);
|
|
766
|
+
const transient=`expected one unambiguous downstream core for bridge ${bridgePid}, found children=0 visible_matches=0`;
|
|
767
|
+
if(message!==transient) throw error;
|
|
768
|
+
lastError=error;
|
|
769
|
+
}
|
|
770
|
+
const elapsed=Math.max(0,Number(nowFn())-started);
|
|
771
|
+
if(elapsed>=timeoutMs){
|
|
772
|
+
throw new Error(`downstream core did not appear for bridge ${bridgePid} within ${timeoutMs}ms: ${String(lastError?.message??lastError)}`);
|
|
773
|
+
}
|
|
774
|
+
await sleepFn(Math.min(pollMs,Math.max(1,timeoutMs-elapsed)));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
746
777
|
async function captureRuntime(ctx){
|
|
747
778
|
const statusText=controllerCaptured(ctx,'status',15000); const status=parseControllerStatus(statusText);
|
|
748
779
|
if(!status.online||!status.bridge_pid||!status.cloud_pid||!status.local_ready) throw new Error(`slot update requires ONLINE runtime: ${statusText}`);
|
|
@@ -826,7 +857,7 @@ async function startAndVerify(ctx,oldRuntime){
|
|
|
826
857
|
catch {} await sleep(500);
|
|
827
858
|
}
|
|
828
859
|
if(!status?.online||!status.bridge_pid||!status.cloud_pid||!status.local_ready) throw new Error(`new runtime failed ONLINE gate: ${statusText}`);
|
|
829
|
-
const bridge=await readLedger(ctx,'bridge'); const cloud=await readLedger(ctx,'cloud'); const core=
|
|
860
|
+
const bridge=await readLedger(ctx,'bridge'); const cloud=await readLedger(ctx,'cloud'); const core=await waitForCoreProcess(ctx.core_root,bridge);
|
|
830
861
|
if(oldRuntime?.bridge&&sameGeneration(oldRuntime.bridge,bridge)) throw new Error('fresh bridge generation gate failed');
|
|
831
862
|
if(oldRuntime?.cloud&&sameGeneration(oldRuntime.cloud,cloud)) throw new Error('fresh cloud generation gate failed');
|
|
832
863
|
if(oldRuntime?.core&&oldRuntime.core.pid===core.pid&&oldRuntime.core.creation_date===core.creation_date) throw new Error('fresh downstream core generation gate failed');
|
|
@@ -861,16 +892,21 @@ async function verifyPackageRoot(root,expectedVersion,expectedManifest,label){
|
|
|
861
892
|
if(pkg?.name!=='deadbyte-mcp'||pkg?.version!==expectedVersion) throw new Error(`${label} package identity mismatch`);
|
|
862
893
|
return {root:absolute,manifest_sha256:digest,entries:entries.size};
|
|
863
894
|
}
|
|
864
|
-
async function
|
|
895
|
+
async function verifyStoredInstallation(stateRoot,pointer){
|
|
865
896
|
const state=path.resolve(stateRoot); const expectedRelease=path.join(state,'releases',releaseId(pointer.version,pointer.manifest_sha256));
|
|
866
897
|
if(path.resolve(pointer.release_root)!==path.resolve(expectedRelease)) throw new Error('active pointer release root is not content-addressed store path');
|
|
867
898
|
const release=await verifyPackageRoot(pointer.release_root,pointer.version,pointer.manifest_sha256,'active release');
|
|
868
899
|
const generation=await verifyGenerationBundle(pointer.generation_root,pointer.generation_manifest_sha256);
|
|
869
900
|
const expectedConfig=path.join(path.resolve(pointer.generation_root),'controller-config.psd1');
|
|
870
901
|
if(path.resolve(pointer.config_path)!==path.resolve(expectedConfig)) throw new Error('active pointer config path mismatch');
|
|
902
|
+
return {release,generation};
|
|
903
|
+
}
|
|
904
|
+
async function verifyActiveInstallation(stateRoot,pointer){
|
|
905
|
+
const state=path.resolve(stateRoot);
|
|
906
|
+
const verified=await verifyStoredInstallation(state,pointer);
|
|
871
907
|
const currentLauncher=await sha256File(launcherPath(state));
|
|
872
908
|
if(currentLauncher!==pointer.launcher_sha256) throw new Error('active pointer launcher digest mismatch');
|
|
873
|
-
return
|
|
909
|
+
return verified;
|
|
874
910
|
}
|
|
875
911
|
async function loadPointerIfPresent(stateRoot){
|
|
876
912
|
const file=path.join(path.resolve(stateRoot),'active-release.json');
|
|
@@ -1084,6 +1120,41 @@ function decodeSnapshot(snapshot,label){
|
|
|
1084
1120
|
}
|
|
1085
1121
|
return {exists:false,bytes:null,sha256:null};
|
|
1086
1122
|
}
|
|
1123
|
+
export function rollbackLauncherPlanFromHandoff(body,priorPointer,activePointer,stateRoot=DEFAULT_STATE_ROOT){
|
|
1124
|
+
if(!body||typeof body!=='object') throw new Error('rollback handoff body required');
|
|
1125
|
+
const handoffPrior=body.prior_pointer?parseActivePointer(JSON.stringify(body.prior_pointer)):null;
|
|
1126
|
+
const handoffActive=body.new_pointer?parseActivePointer(JSON.stringify(body.new_pointer)):null;
|
|
1127
|
+
if(!samePublicPointer(handoffPrior,priorPointer)||!samePublicPointer(handoffActive,activePointer)) throw new Error('rollback handoff pointer binding mismatch');
|
|
1128
|
+
const state=path.resolve(stateRoot);
|
|
1129
|
+
const launchers={
|
|
1130
|
+
ps1:path.resolve(String(body.launchers?.ps1??'')),
|
|
1131
|
+
cmd:path.resolve(String(body.launchers?.cmd??'')),
|
|
1132
|
+
top:path.resolve(String(body.launchers?.top??TOP_LAUNCHER)),
|
|
1133
|
+
launcher_sha256:String(body.launchers?.launcher_sha256??''),
|
|
1134
|
+
ps1Bytes:Buffer.from(String(body.launchers?.ps1_bytes??''),'base64'),
|
|
1135
|
+
cmdBytes:Buffer.from(String(body.launchers?.cmd_bytes??''),'base64'),
|
|
1136
|
+
snapshots:{
|
|
1137
|
+
ps1:decodeSnapshot(body.launchers?.snapshots?.ps1,'rollback.launcher.ps1'),
|
|
1138
|
+
cmd:decodeSnapshot(body.launchers?.snapshots?.cmd,'rollback.launcher.cmd'),
|
|
1139
|
+
top:decodeSnapshot(body.launchers?.snapshots?.top,'rollback.launcher.top')
|
|
1140
|
+
},
|
|
1141
|
+
installed:true
|
|
1142
|
+
};
|
|
1143
|
+
if(launchers.ps1!==path.resolve(launcherPath(state))||
|
|
1144
|
+
launchers.cmd!==path.resolve(path.join(state,'bin','DEADBYTE-MCP.cmd'))||
|
|
1145
|
+
launchers.top!==path.resolve(TOP_LAUNCHER)) throw new Error('rollback launcher paths are not canonical');
|
|
1146
|
+
if(!HEX64.test(launchers.launcher_sha256)||sha256Bytes(launchers.ps1Bytes)!==launchers.launcher_sha256) throw new Error('rollback successor launcher bytes/hash mismatch');
|
|
1147
|
+
if(launchers.launcher_sha256!==activePointer.launcher_sha256) throw new Error('rollback successor launcher hash does not match active pointer');
|
|
1148
|
+
if(!launchers.snapshots.ps1.exists||launchers.snapshots.ps1.sha256!==priorPointer.launcher_sha256) throw new Error('rollback predecessor launcher snapshot does not match signed prior pointer');
|
|
1149
|
+
return launchers;
|
|
1150
|
+
}
|
|
1151
|
+
async function restoreRollbackLaunchers(plan){
|
|
1152
|
+
await restoreFile(plan.ps1,plan.snapshots.ps1);
|
|
1153
|
+
await restoreFile(plan.cmd,plan.snapshots.cmd);
|
|
1154
|
+
await restoreFile(plan.top,plan.snapshots.top);
|
|
1155
|
+
if(await sha256File(plan.ps1)!==plan.snapshots.ps1.sha256) throw new Error('rollback predecessor launcher restore hash mismatch');
|
|
1156
|
+
return plan;
|
|
1157
|
+
}
|
|
1087
1158
|
const UPDATE_PHASE_RANK=Object.freeze({
|
|
1088
1159
|
initial:0,candidate_prepared:10,old_runtime_verified:20,shutdown_handoff_armed:25,supervisor_spawned:30,
|
|
1089
1160
|
authority_revoked:40,old_runtime_stopped:50,workspace_ready:60,launcher_installed:70,pointer_committed:80,
|
|
@@ -1587,19 +1658,28 @@ export async function performSlotRollback({activationReceipt,stateRoot=DEFAULT_S
|
|
|
1587
1658
|
const activePointer=parseActivePointer(JSON.stringify(activationEnvelope.body.active_pointer));
|
|
1588
1659
|
const pointerInfo=await loadPointerIfPresent(state);
|
|
1589
1660
|
if(!pointerInfo.pointer||canonicalJson(publicPointer(pointerInfo.pointer))!==canonicalJson(publicPointer(activePointer))) throw new Error('rollback current pointer does not match signed activation pointer');
|
|
1590
|
-
|
|
1661
|
+
|
|
1662
|
+
const handoffPath=path.join(path.dirname(receiptFile),'handoff.json');
|
|
1663
|
+
const {body:handoffBody}=await readHandoffEnvelope(handoffPath,state);
|
|
1664
|
+
if(handoffBody.update_id!==activationEnvelope.body.update_id) throw new Error('rollback handoff update identity mismatch');
|
|
1665
|
+
const rollbackLaunchers=rollbackLauncherPlanFromHandoff(handoffBody,priorPointer,activePointer,state);
|
|
1666
|
+
await verifyStoredInstallation(state,priorPointer);
|
|
1667
|
+
|
|
1591
1668
|
const currentCtx=slotContext(state,pointerInfo.pointer),currentRuntime=await captureRuntime(currentCtx);
|
|
1592
1669
|
const revoked=await revokeAuthority(currentCtx,currentRuntime.status);
|
|
1593
1670
|
const rollbackId=`r31-rollback-${activationEnvelope.receipt_sha256.slice(0,16)}-${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`;
|
|
1594
1671
|
const evidenceDir=path.join(state,'evidence','updates',rollbackId),statePath=path.join(evidenceDir,'state.json'),receiptPath=path.join(evidenceDir,'receipt.json');
|
|
1595
1672
|
await mkdir(evidenceDir,{recursive:true});
|
|
1596
1673
|
const journal={schema:'deadbyte.r31-slot-rollback-state.v1',rollback_id:rollbackId,activation_receipt:path.resolve(receiptFile),
|
|
1597
|
-
activation_receipt_sha256:activationEnvelope.receipt_sha256,
|
|
1674
|
+
activation_receipt_sha256:activationEnvelope.receipt_sha256,handoff_path:handoffPath,
|
|
1675
|
+
from_pointer:publicPointer(activePointer),to_pointer:publicPointer(priorPointer),created_at_utc:new Date().toISOString()};
|
|
1598
1676
|
await writeState(statePath,journal,'verified');
|
|
1599
|
-
let stopped=false,pointerCommitted=false,priorCtx=null,priorRuntime=null;
|
|
1677
|
+
let stopped=false,launchersRestored=false,pointerCommitted=false,priorCtx=null,priorRuntime=null;
|
|
1600
1678
|
try {
|
|
1601
1679
|
await stopAndVerify(currentCtx,currentRuntime); stopped=true;
|
|
1602
1680
|
await writeState(statePath,journal,'successor_stopped');
|
|
1681
|
+
await restoreRollbackLaunchers(rollbackLaunchers); launchersRestored=true;
|
|
1682
|
+
await writeState(statePath,journal,'predecessor_launcher_restored',{launcher_sha256:priorPointer.launcher_sha256});
|
|
1603
1683
|
await atomicWriteActivePointer(pointerInfo.file,priorPointer); pointerCommitted=true;
|
|
1604
1684
|
await verifyActiveInstallation(state,priorPointer);
|
|
1605
1685
|
await writeState(statePath,journal,'prior_pointer_restored',{active_pointer:publicPointer(priorPointer)});
|
|
@@ -1610,16 +1690,27 @@ export async function performSlotRollback({activationReceipt,stateRoot=DEFAULT_S
|
|
|
1610
1690
|
if(finalStatus.process_armed||finalStatus.coding_armed||finalStatus.autonomous_armed||finalStatus.desktop_armed||finalStatus.host_armed) throw new Error('rollback predecessor authority must remain disarmed');
|
|
1611
1691
|
const body={schema:ROLLBACK_SCHEMA,rollback_id:rollbackId,activation_receipt_sha256:activationEnvelope.receipt_sha256,
|
|
1612
1692
|
completed_at_utc:new Date().toISOString(),from_pointer:publicPointer(activePointer),active_pointer:publicPointer(priorPointer),
|
|
1613
|
-
old_runtime:generationTuple(currentRuntime),new_runtime:generationTuple(priorRuntime),stabilization,
|
|
1693
|
+
launcher_restored:true,old_runtime:generationTuple(currentRuntime),new_runtime:generationTuple(priorRuntime),stabilization,
|
|
1694
|
+
authority_revoked:revoked.revoked,rollback_performed:true};
|
|
1614
1695
|
const receipt=await writeEnvelope(receiptPath,body,key);
|
|
1615
1696
|
await writeState(statePath,journal,'completed',{receipt_path:receiptPath,receipt_sha256:receipt.receipt_sha256});
|
|
1616
1697
|
return {ok:true,rollback_id:rollbackId,state_path:statePath,receipt_path:receiptPath,receipt_sha256:receipt.receipt_sha256,pointer:priorPointer,new_runtime:priorRuntime};
|
|
1617
1698
|
} catch(error){
|
|
1618
|
-
const recovery={attempted:false,prior_runtime_stopped:false,successor_pointer_restored:false,successor_runtime_restarted:false,errors:[]};
|
|
1699
|
+
const recovery={attempted:false,prior_runtime_stopped:false,successor_launcher_restored:false,successor_pointer_restored:false,successor_runtime_restarted:false,errors:[]};
|
|
1619
1700
|
if(stopped){
|
|
1620
1701
|
recovery.attempted=true;
|
|
1621
1702
|
if(pointerCommitted&&priorCtx){ try { controllerCaptured(priorCtx,'stop',30000); recovery.prior_runtime_stopped=true; } catch(recoveryError){ recovery.errors.push(`stop-prior:${recoveryError.message}`); } }
|
|
1622
|
-
if(
|
|
1703
|
+
if(launchersRestored){
|
|
1704
|
+
try { await installLaunchers(rollbackLaunchers); recovery.successor_launcher_restored=true; }
|
|
1705
|
+
catch(recoveryError){ recovery.errors.push(`restore-successor-launcher:${recoveryError.message}`); }
|
|
1706
|
+
}
|
|
1707
|
+
if(pointerCommitted){
|
|
1708
|
+
try { await atomicWriteActivePointer(pointerInfo.file,activePointer); await verifyActiveInstallation(state,activePointer); recovery.successor_pointer_restored=true; }
|
|
1709
|
+
catch(recoveryError){ recovery.errors.push(`restore-pointer:${recoveryError.message}`); }
|
|
1710
|
+
}else if(recovery.successor_launcher_restored){
|
|
1711
|
+
try { await verifyActiveInstallation(state,activePointer); recovery.successor_pointer_restored=true; }
|
|
1712
|
+
catch(recoveryError){ recovery.errors.push(`verify-successor-pointer:${recoveryError.message}`); }
|
|
1713
|
+
}
|
|
1623
1714
|
try { await startAndVerify(currentCtx,currentRuntime); recovery.successor_runtime_restarted=true; } catch(recoveryError){ recovery.errors.push(`restart-successor:${recoveryError.message}`); }
|
|
1624
1715
|
}
|
|
1625
1716
|
await writeState(statePath,journal,'failed',{error:{name:error?.name??'Error',message:String(error?.message??error)},recovery}).catch(()=>{});
|
package/src/version.mjs
CHANGED
|
@@ -4,9 +4,13 @@ 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,historicalR54,historicalEvidenceR54,historicalR53,historicalEvidenceR53,historicalR52,historicalEvidenceR52,historicalR51,historicalEvidenceR51,historicalR50,historicalR49,deferred]=await Promise.all([
|
|
7
|
+
test('R57 canonical Windows gate is successor-bound and preserves R56/R55/R54/R53/R52/R51/R50/R49 historical gates',async()=>{
|
|
8
|
+
const [pkgText,gate,evidence,historicalR56,historicalEvidenceR56,historicalR55,historicalEvidenceR55,historicalR54,historicalEvidenceR54,historicalR53,historicalEvidenceR53,historicalR52,historicalEvidenceR52,historicalR51,historicalEvidenceR51,historicalR50,historicalR49,deferred]=await Promise.all([
|
|
9
9
|
read('package.json'),
|
|
10
|
+
read('scripts/gate-windows-r57.ps1'),
|
|
11
|
+
read('scripts/windows-gate-evidence-r57.mjs'),
|
|
12
|
+
read('scripts/gate-windows-r56.ps1'),
|
|
13
|
+
read('scripts/windows-gate-evidence-r56.mjs'),
|
|
10
14
|
read('scripts/gate-windows-r55.ps1'),
|
|
11
15
|
read('scripts/windows-gate-evidence-r55.mjs'),
|
|
12
16
|
read('scripts/gate-windows-r54.ps1'),
|
|
@@ -22,18 +26,22 @@ test('R55 canonical Windows gate is successor-bound and preserves R54/R53/R52/R5
|
|
|
22
26
|
read('scripts/deferred-slot-operation-r46.mjs')
|
|
23
27
|
]);
|
|
24
28
|
const pkg=JSON.parse(pkgText);
|
|
25
|
-
assert.match(pkg.scripts['gate:windows'],/gate-windows-
|
|
26
|
-
assert.match(gate,/DEADBYTE 0\.15\.
|
|
27
|
-
assert.match(gate,/
|
|
28
|
-
assert.match(gate,/
|
|
29
|
-
assert.match(gate,/full-regression/);
|
|
29
|
+
assert.match(pkg.scripts['gate:windows'],/gate-windows-r57\.ps1$/);
|
|
30
|
+
assert.match(gate,/DEADBYTE 0\.15\.34 \/ R57 WINDOWS GATE: PASS/);
|
|
31
|
+
assert.match(gate,/r57-package-boundary/);
|
|
32
|
+
assert.match(gate,/r57-focused-contracts/);
|
|
33
|
+
assert.match(gate,/full-regression-evidence/);
|
|
30
34
|
assert.match(gate,/manifest-verify/);
|
|
31
35
|
assert.match(gate,/doctor/);
|
|
32
|
-
assert.match(evidence,/deadbyte\.
|
|
33
|
-
assert.match(evidence,/pkg\.version==='0\.15\.
|
|
34
|
-
assert.match(evidence,/release\.train==='
|
|
35
|
-
assert.match(evidence,/release\.parent\.version==='0\.15\.
|
|
36
|
+
assert.match(evidence,/deadbyte\.r57-windows-gate\.v1/);
|
|
37
|
+
assert.match(evidence,/pkg\.version==='0\.15\.34'/);
|
|
38
|
+
assert.match(evidence,/release\.train==='R57'/);
|
|
39
|
+
assert.match(evidence,/release\.parent\.version==='0\.15\.33'/);
|
|
36
40
|
assert.match(evidence,/four_hour_soak_completed:false/);
|
|
41
|
+
assert.match(historicalR56,/DEADBYTE 0\.15\.33 \/ R56 WINDOWS GATE: PASS/);
|
|
42
|
+
assert.match(historicalEvidenceR56,/deadbyte\.r56-windows-gate\.v1/);
|
|
43
|
+
assert.match(historicalR55,/DEADBYTE 0\.15\.32 \/ R55 WINDOWS GATE: PASS/);
|
|
44
|
+
assert.match(historicalEvidenceR55,/deadbyte\.r55-windows-gate\.v1/);
|
|
37
45
|
assert.match(historicalR54,/DEADBYTE 0\.15\.31 \/ R54 WINDOWS GATE: PASS/);
|
|
38
46
|
assert.match(historicalEvidenceR54,/deadbyte\.r54-windows-gate\.v1/);
|
|
39
47
|
assert.match(historicalR53,/DEADBYTE 0\.15\.30 \/ R53 WINDOWS GATE: PASS/);
|
|
@@ -46,10 +54,5 @@ test('R55 canonical Windows gate is successor-bound and preserves R54/R53/R52/R5
|
|
|
46
54
|
assert.match(historicalR49,/DEADBYTE 0\.15\.26 \/ R49 WINDOWS GATE: PASS/);
|
|
47
55
|
assert.match(deferred,/const expectedVersion=packageIdentity\.version/);
|
|
48
56
|
assert.match(deferred,/'--expected-version',expectedVersion/);
|
|
49
|
-
assert.match(deferred,/async function awaitHandoffCompletion/);
|
|
50
57
|
assert.match(deferred,/verifySlotUpdateEnvelope\(envelope,publicKey\)/);
|
|
51
|
-
assert.match(deferred,/active pointer does not match signed activation receipt/);
|
|
52
|
-
assert.match(deferred,/failure-receipt\.json/);
|
|
53
|
-
assert.match(deferred,/await awaitHandoffCompletion\(await runSlotUpdateCli/);
|
|
54
|
-
assert.doesNotMatch(deferred,/'--expected-version','0\.15\.5'/);
|
|
55
58
|
});
|
|
@@ -5,8 +5,10 @@ import { readFile } from 'node:fs/promises';
|
|
|
5
5
|
const root=new URL('../',import.meta.url);
|
|
6
6
|
const read=rel=>readFile(new URL(rel,root),'utf8');
|
|
7
7
|
const json=rel=>read(rel).then(JSON.parse);
|
|
8
|
+
const currentPackage=await json('package.json');
|
|
9
|
+
const r55test=currentPackage.version==='0.15.32'?test:test.skip;
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
r55test('R55 release identity is coherent across package lock source metadata SBOM and gate',async()=>{
|
|
10
12
|
const [pkg,lock,release,provenance,matrix,sbom,versionSource]=await Promise.all([
|
|
11
13
|
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
14
|
]);
|
|
@@ -25,7 +27,7 @@ test('R55 release identity is coherent across package lock source metadata SBOM
|
|
|
25
27
|
assert.match(pkg.scripts['r55:focused:test'],/r55-offline-predecessor-guardian-family\.test\.mjs/);
|
|
26
28
|
});
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
r55test('R55 source predecessor is exact immutable published 0.15.31 while runtime predecessor remains exact 0.15.29',async()=>{
|
|
29
31
|
const [release,provenance]=await Promise.all([json('RELEASE.json'),json('PROVENANCE.json')]);
|
|
30
32
|
assert.equal(release.parent.version,'0.15.31');
|
|
31
33
|
assert.equal(release.parent.release_id,'v0.15.31-16854fe5b3eb0fa1');
|
|
@@ -44,7 +46,7 @@ test('R55 source predecessor is exact immutable published 0.15.31 while runtime
|
|
|
44
46
|
assert.deepEqual(provenance.runtime_predecessor,release.runtime_predecessor);
|
|
45
47
|
});
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
r55test('R55 records the guardian sibling RED to GREEN and preserves fail-closed negative controls',async()=>{
|
|
48
50
|
const [release,provenance,matrix]=await Promise.all([json('RELEASE.json'),json('PROVENANCE.json'),json('TEST-MATRIX.json')]);
|
|
49
51
|
assert.equal(release.hotfix.activation_ancestry_excluded,true);
|
|
50
52
|
assert.equal(release.hotfix.foreground_guardian_owner_parent_binding,true);
|
|
@@ -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
|
+
|
|
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
|
+
const currentPackage=await json('package.json');
|
|
9
|
+
const r56test=currentPackage.version==='0.15.33'?test:test.skip;
|
|
10
|
+
|
|
11
|
+
r56test('R56 release identity is coherent across package lock source metadata SBOM and gate',async()=>{
|
|
12
|
+
const [pkg,lock,release,provenance,matrix,sbom,versionSource]=await Promise.all([
|
|
13
|
+
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')
|
|
14
|
+
]);
|
|
15
|
+
assert.equal(pkg.version,'0.15.33');
|
|
16
|
+
assert.equal(lock.version,pkg.version);
|
|
17
|
+
assert.equal(lock.packages[''].version,pkg.version);
|
|
18
|
+
assert.match(versionSource,/DEADBYTE_VERSION = '0\.15\.33'/);
|
|
19
|
+
assert.equal(release.version,pkg.version);
|
|
20
|
+
assert.equal(release.train,'R56');
|
|
21
|
+
assert.equal(provenance.version,pkg.version);
|
|
22
|
+
assert.equal(matrix.version,pkg.version);
|
|
23
|
+
assert.equal(matrix.train,'R56');
|
|
24
|
+
assert.equal(sbom.metadata.component.version,pkg.version);
|
|
25
|
+
assert.equal(sbom.serialNumber,'urn:cdx:deadbyte-mcp:r56:0.15.33');
|
|
26
|
+
assert.match(pkg.scripts['gate:windows'],/gate-windows-r56\.ps1/);
|
|
27
|
+
assert.match(pkg.scripts['r56:focused:test'],/r56-rollback-launcher-restore\.test\.mjs/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
r56test('R56 source and runtime predecessor are exact active published 0.15.32',async()=>{
|
|
31
|
+
const [release,provenance]=await Promise.all([json('RELEASE.json'),json('PROVENANCE.json')]);
|
|
32
|
+
assert.equal(release.parent.version,'0.15.32');
|
|
33
|
+
assert.equal(release.parent.release_id,'v0.15.32-e7223f467777f5df');
|
|
34
|
+
assert.equal(release.parent.manifest_sha256,'e7223f467777f5dfeb20fd2cc7a6eca22619a44ed95e5bc0b32d9c7aabba361b');
|
|
35
|
+
assert.equal(release.parent.generation_manifest_sha256,'740443fd207c5e6ffd5952079214bf6e81e9281a6ec880de9bbe3b8ce1deb00e');
|
|
36
|
+
assert.equal(release.parent.tarball_sha256,'c9e1f5875dd2177cee0089305b40321c128c54ce7f5522e34e507d4ecfb22638');
|
|
37
|
+
assert.equal(release.parent.activation_status,'ACTIVATED_ROLLBACK_DEFECT_OBSERVED');
|
|
38
|
+
assert.equal(release.source_predecessor.version,'0.15.32');
|
|
39
|
+
assert.deepEqual(release.runtime_predecessor,{
|
|
40
|
+
release_id:'v0.15.32-e7223f467777f5df',
|
|
41
|
+
manifest_sha256:'e7223f467777f5dfeb20fd2cc7a6eca22619a44ed95e5bc0b32d9c7aabba361b',
|
|
42
|
+
generation_manifest_sha256:'740443fd207c5e6ffd5952079214bf6e81e9281a6ec880de9bbe3b8ce1deb00e'
|
|
43
|
+
});
|
|
44
|
+
assert.equal(provenance.parent_manifest_sha256,release.parent.manifest_sha256);
|
|
45
|
+
assert.equal(provenance.parent_generation_manifest_sha256,release.parent.generation_manifest_sha256);
|
|
46
|
+
assert.deepEqual(provenance.runtime_predecessor,release.runtime_predecessor);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
r56test('R56 records signed rollback launcher ordering RED to GREEN and preserves non-final claim',async()=>{
|
|
50
|
+
const [release,provenance,matrix]=await Promise.all([json('RELEASE.json'),json('PROVENANCE.json'),json('TEST-MATRIX.json')]);
|
|
51
|
+
assert.equal(release.hotfix.signed_handoff_launcher_snapshot_required,true);
|
|
52
|
+
assert.equal(release.hotfix.stored_predecessor_verified_without_live_launcher,true);
|
|
53
|
+
assert.equal(release.hotfix.predecessor_launcher_restored_before_pointer_commit,true);
|
|
54
|
+
assert.equal(release.hotfix.successor_launcher_restored_before_recovery_pointer,true);
|
|
55
|
+
assert.equal(release.hotfix.exact_parent_activation_receipt_sha256,'6518a04154f485cfae42e582269634d69a8b05a0183149d5e10006fbdd9d933c');
|
|
56
|
+
assert.equal(release.hotfix.observed_parent_rollback_failure_receipt_sha256,'00cbd497e4902c67ab39313a378e2fcf32159113b46339ad1d54b5e5a7714205');
|
|
57
|
+
assert.equal(release.hotfix.red_parent_0_15_32,'FAIL_5_OF_5');
|
|
58
|
+
assert.equal(release.hotfix.green_r56_candidate,'PASS_5_OF_5');
|
|
59
|
+
assert.equal(matrix.observed_pre_release_evidence.red_parent_0_15_32.receipt_sha256,'f3b446306691d61e97896ab4d237abaa20d1243237299812e790233c46451dba');
|
|
60
|
+
assert.equal(matrix.observed_pre_release_evidence.green_r56_candidate.receipt_sha256,'4198930f38a3fc4b7978cfc94ca4509127561c170b05f7566bdf79513e449a8b');
|
|
61
|
+
assert.equal(release.hotfix.stable_final_claim,false);
|
|
62
|
+
assert.equal(provenance.operator_override.four_hour_soak,'NOT_COMPLETED');
|
|
63
|
+
assert.equal(provenance.operator_override.stable_final_claim,false);
|
|
64
|
+
assert.equal(matrix.operator_override.stable_final_claim,false);
|
|
65
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
const target=process.env.R56_RUNTIME_UPDATE_PATH
|
|
10
|
+
? path.resolve(process.env.R56_RUNTIME_UPDATE_PATH)
|
|
11
|
+
: path.resolve(new URL('../src/runtime-update.mjs',import.meta.url).pathname.replace(/^\/(?:[A-Za-z]:)/,m=>m.slice(1)));
|
|
12
|
+
const runtimeUpdate=await import(pathToFileURL(target).href+'?r56='+Date.now());
|
|
13
|
+
const sha256=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
14
|
+
const stateRoot=path.resolve(process.env.R56_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
|
|
15
|
+
const topLauncher=path.resolve(path.join(os.homedir(),'Downloads','DEADBYTE-MCP.cmd'));
|
|
16
|
+
|
|
17
|
+
function pointer(version,manifest,launcher){
|
|
18
|
+
const releaseId=`v${version}-${manifest.slice(0,16)}`;
|
|
19
|
+
const genId=`${releaseId}-g${'9'.repeat(16)}`;
|
|
20
|
+
return {
|
|
21
|
+
schema:'deadbyte.active-release.v1',
|
|
22
|
+
release_id:releaseId,
|
|
23
|
+
version,
|
|
24
|
+
manifest_sha256:manifest,
|
|
25
|
+
release_root:path.join(stateRoot,'releases',releaseId),
|
|
26
|
+
generation_root:path.join(stateRoot,'generations',genId),
|
|
27
|
+
generation_manifest_sha256:'8'.repeat(64),
|
|
28
|
+
launcher_sha256:launcher,
|
|
29
|
+
config_path:path.join(stateRoot,'generations',genId,'controller-config.psd1')
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function snapshot(bytes){
|
|
33
|
+
const b=Buffer.from(bytes);
|
|
34
|
+
return {exists:true,sha256:sha256(b),bytes_base64:b.toString('base64')};
|
|
35
|
+
}
|
|
36
|
+
function fixture(){
|
|
37
|
+
const priorBytes=Buffer.from('prior-launcher-r55\r\n');
|
|
38
|
+
const activeBytes=Buffer.from('active-launcher-r56\r\n');
|
|
39
|
+
const cmdBytes=Buffer.from('@echo off\r\n');
|
|
40
|
+
const prior=pointer('0.15.29','1'.repeat(64),sha256(priorBytes));
|
|
41
|
+
const active=pointer('0.15.32','2'.repeat(64),sha256(activeBytes));
|
|
42
|
+
const body={
|
|
43
|
+
prior_pointer:prior,
|
|
44
|
+
new_pointer:active,
|
|
45
|
+
launchers:{
|
|
46
|
+
ps1:path.join(stateRoot,'bin','DEADBYTE-MCP.ps1'),
|
|
47
|
+
cmd:path.join(stateRoot,'bin','DEADBYTE-MCP.cmd'),
|
|
48
|
+
top:topLauncher,
|
|
49
|
+
launcher_sha256:sha256(activeBytes),
|
|
50
|
+
ps1_bytes:activeBytes.toString('base64'),
|
|
51
|
+
cmd_bytes:cmdBytes.toString('base64'),
|
|
52
|
+
snapshots:{
|
|
53
|
+
ps1:snapshot(priorBytes),
|
|
54
|
+
cmd:snapshot(cmdBytes),
|
|
55
|
+
top:snapshot(cmdBytes)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
return {prior,active,body};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test('R56 exposes rollback launcher planning from the signed activation handoff',()=>{
|
|
63
|
+
assert.equal(typeof runtimeUpdate.rollbackLauncherPlanFromHandoff,'function');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('R56 accepts a prior launcher snapshot whose hash differs from the active launcher and binds both signed pointers',()=>{
|
|
67
|
+
const {prior,active,body}=fixture();
|
|
68
|
+
const plan=runtimeUpdate.rollbackLauncherPlanFromHandoff(body,prior,active,stateRoot);
|
|
69
|
+
assert.equal(plan.snapshots.ps1.sha256,prior.launcher_sha256);
|
|
70
|
+
assert.equal(plan.launcher_sha256,active.launcher_sha256);
|
|
71
|
+
assert.notEqual(plan.snapshots.ps1.sha256,plan.launcher_sha256);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('R56 rejects a predecessor launcher snapshot that does not match the signed prior pointer',()=>{
|
|
75
|
+
const {prior,active,body}=fixture();
|
|
76
|
+
body.launchers.snapshots.ps1=snapshot(Buffer.from('wrong-prior-launcher\r\n'));
|
|
77
|
+
assert.throws(()=>runtimeUpdate.rollbackLauncherPlanFromHandoff(body,prior,active,stateRoot),/predecessor launcher snapshot/i);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('R56 rejects handoff pointer drift instead of using unsigned rollback state',()=>{
|
|
81
|
+
const {prior,active,body}=fixture();
|
|
82
|
+
const drift={...prior,manifest_sha256:'3'.repeat(64),release_id:`v0.15.29-${'3'.repeat(16)}`,release_root:path.join(stateRoot,'releases',`v0.15.29-${'3'.repeat(16)}`)};
|
|
83
|
+
assert.throws(()=>runtimeUpdate.rollbackLauncherPlanFromHandoff(body,drift,active,stateRoot),/pointer binding mismatch/i);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('R56 rollback verifies stored predecessor bytes before stop, restores signed predecessor launcher before pointer commit, and reinstalls successor launcher before recovery pointer',async()=>{
|
|
87
|
+
const source=await readFile(target,'utf8');
|
|
88
|
+
const start=source.indexOf('export async function performSlotRollback');
|
|
89
|
+
const end=source.indexOf('export async function runSlotUpdateCli',start);
|
|
90
|
+
assert.ok(start>=0&&end>start);
|
|
91
|
+
const perform=source.slice(start,end);
|
|
92
|
+
const stored=perform.indexOf('verifyStoredInstallation(state,priorPointer)');
|
|
93
|
+
const stop=perform.indexOf('stopAndVerify(currentCtx,currentRuntime)');
|
|
94
|
+
const restoreLauncher=perform.indexOf('restoreRollbackLaunchers(rollbackLaunchers)');
|
|
95
|
+
const commit=perform.indexOf('atomicWriteActivePointer(pointerInfo.file,priorPointer)');
|
|
96
|
+
const verifyPrior=perform.indexOf('verifyActiveInstallation(state,priorPointer)');
|
|
97
|
+
const reinstall=perform.indexOf('installLaunchers(rollbackLaunchers)');
|
|
98
|
+
const restoreSuccessor=perform.indexOf('atomicWriteActivePointer(pointerInfo.file,activePointer)');
|
|
99
|
+
assert.ok(stored>=0&&stored<stop);
|
|
100
|
+
assert.ok(restoreLauncher>stop&&restoreLauncher<commit);
|
|
101
|
+
assert.ok(verifyPrior>commit);
|
|
102
|
+
assert.ok(reinstall>verifyPrior&&reinstall<restoreSuccessor);
|
|
103
|
+
});
|