deadbyte-mcp 0.15.60 → 0.15.61

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.
@@ -15,6 +15,7 @@ import {proveLegacyOfflineScopeRepair} from '../src/legacy-offline-policy-proof-
15
15
  import {claimWithOneTimeMigration,verifyNarrowedPolicyGeneration} from '../src/clean-activation-recovery-r153.mjs';
16
16
  import {verifyUacApproval,uacPointerSha256,protectedUacReceiptPath,requireTrustedUacInstallation} from '../src/uac-approval-contract-r153.mjs';
17
17
  import {inspectTrustedInstallation} from '../src/privileged-install-trust-r89.mjs';
18
+ import {normalizeHostCommandPins,hostCommandPinArgs} from '../src/host-command-pins-r154.mjs';
18
19
 
19
20
  const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
20
21
  const state=path.resolve(process.env.DEADBYTE_STATE_ROOT||path.join(os.homedir(),'.deadbyte-mcp'));
@@ -24,16 +25,14 @@ const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
24
25
  function parseDeployArgs(argv){
25
26
  const out={preflight:false,approvalPath:null,approvalSha256:null,uacLegacyMigration:false,
26
27
  markerReceiptPath:null,markerReceiptSha256:null,migrationNonce:null,migrationIssuedAt:null,
27
- migrationExpiresAt:null,uacApprovalPath:null,uacApprovalSha256:null,hostCommandPin:null};
28
+ migrationExpiresAt:null,uacApprovalPath:null,uacApprovalSha256:null,hostCommandPins:[]};
28
29
  for(let index=0;index<argv.length;index++){
29
30
  const arg=argv[index];
30
31
  if(arg==='--preflight'){if(out.preflight)throw new Error('duplicate --preflight');out.preflight=true;continue;}
31
32
  if(arg==='--uac-legacy-migration'){if(out.uacLegacyMigration)throw new Error('duplicate --uac-legacy-migration');out.uacLegacyMigration=true;continue;}
32
33
  if(arg==='--host-command-pin'){
33
- if(out.hostCommandPin!==null)throw new Error('duplicate --host-command-pin');
34
34
  const value=String(argv[++index]??'');
35
- if(!/^powershell_script=[0-9a-f]{64}$/.test(value))throw new Error('--host-command-pin requires powershell_script=lowercase_sha256');
36
- out.hostCommandPin=value;continue;
35
+ out.hostCommandPins.push(value);continue;
37
36
  }
38
37
  if(arg==='--marker-reconciliation-receipt'){
39
38
  if(out.markerReceiptPath)throw new Error('duplicate --marker-reconciliation-receipt');
@@ -76,7 +75,8 @@ function parseDeployArgs(argv){
76
75
  const uacFields=[out.migrationNonce,out.migrationIssuedAt,out.migrationExpiresAt,out.uacApprovalPath,out.uacApprovalSha256];
77
76
  if(out.uacLegacyMigration&&uacFields.some(value=>!value))throw new Error('UAC migration requires nonce, lifetime, and approval receipt');
78
77
  if(!out.uacLegacyMigration&&uacFields.some(value=>value))throw new Error('UAC migration arguments require --uac-legacy-migration');
79
- if(out.hostCommandPin!==null&&(!out.uacLegacyMigration||out.preflight))throw new Error('explicit host repin requires the elevated UAC migration');
78
+ out.hostCommandPins=normalizeHostCommandPins(out.hostCommandPins);
79
+ if(out.hostCommandPins.length>0&&(!out.uacLegacyMigration||out.preflight))throw new Error('explicit host repin requires the elevated UAC migration');
80
80
  return out;
81
81
  }
82
82
  const deployArgs=parseDeployArgs(process.argv.slice(2));
@@ -301,6 +301,7 @@ if(deployArgs.uacLegacyMigration){
301
301
  predecessorPolicySha256:markerReceipt.predecessor_policy_sha256,
302
302
  preflightSubjectSha256:preflight.subject_sha256,nodeSha256:await sha(process.execPath),
303
303
  deployScriptSha256:await sha(fileURLToPath(import.meta.url)),
304
+ hostCommandPins:deployArgs.hostCommandPins,
304
305
  nonce:deployArgs.migrationNonce,issuedAt:deployArgs.migrationIssuedAt,
305
306
  expiresAt:deployArgs.migrationExpiresAt
306
307
  });
@@ -320,7 +321,7 @@ const args=[
320
321
  '--bridge-root',bridgeRoot,'--bridge-script-sha256',await sha(bridgeScript),
321
322
  '--enable-machine-fs','--enable-observation','--enable-desktop','--enable-autonomous-supervisor','--recover-offline-predecessor'
322
323
  ];
323
- if(deployArgs.hostCommandPin!==null)args.push('--host-command-pin',deployArgs.hostCommandPin);
324
+ args.push(...hostCommandPinArgs(deployArgs.hostCommandPins));
324
325
  let result=null,verification=null,activationOutcomePath=null;
325
326
  let continuityEngine=null,continuityUpdateId=null;
326
327
 
@@ -9,6 +9,7 @@ import {parseActivePointer} from '../src/release-slot.mjs';
9
9
  import {inspectTrustedInstallation,privilegedEpochInstallFiles} from '../src/privileged-install-trust-r89.mjs';
10
10
  import {verifyMarkerReconciliationReceipt} from '../src/clean-activation-recovery-r153.mjs';
11
11
  import {createUacApproval,uacPointerSha256,protectedUacReceiptPath} from '../src/uac-approval-contract-r153.mjs';
12
+ import {decodeHostCommandPins} from '../src/host-command-pins-r154.mjs';
12
13
 
13
14
  const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
14
15
  const sha=b=>createHash('sha256').update(b).digest('hex');
@@ -17,11 +18,15 @@ const hash=async f=>sha(await read(f));
17
18
  const fail=text=>{throw new Error('R153 UAC issuer rejected: '+text);};
18
19
  function args(argv){
19
20
  const expected=['--state-root','--marker-receipt','--marker-receipt-sha256','--preflight-subject-sha256'];
20
- if(argv.length!==8||expected.some((key,i)=>argv[2*i]!==key))fail('unexpected arguments');
21
+ if(![8,10].includes(argv.length)||expected.some((key,i)=>argv[2*i]!==key)||
22
+ (argv.length===10&&argv[8]!=='--host-command-pins-json'))fail('unexpected arguments');
21
23
  const [state,receipt,receiptHash,preflight]=[argv[1],argv[3],argv[5],argv[7]];
22
24
  if(!path.win32.isAbsolute(state)||!path.win32.isAbsolute(receipt)||
23
25
  !/^[0-9a-f]{64}$/.test(receiptHash)||!/^[0-9a-f]{64}$/.test(preflight))fail('subject arguments invalid');
24
- return {state:path.resolve(state),receipt:path.resolve(receipt),receiptHash,preflight};
26
+ let hostCommandPins=[];
27
+ try{hostCommandPins=decodeHostCommandPins(argv.length===10?argv[9]:'[]');}
28
+ catch(error){fail(error.message);}
29
+ return {state:path.resolve(state),receipt:path.resolve(receipt),receiptHash,preflight,hostCommandPins};
25
30
  }
26
31
  async function regular(file,label){const s=await lstat(file);if(!s.isFile()||s.isSymbolicLink())fail(label+' must be regular');return s;}
27
32
  async function absent(file,label){try{await lstat(file);fail(label+' present');}catch(e){if(e?.code!=='ENOENT')throw e;}}
@@ -33,7 +38,7 @@ async function main(){
33
38
  {encoding:'utf8',shell:false,windowsHide:true,timeout:12000});
34
39
  if(elevated.error||elevated.status!==0||String(elevated.stdout).trim().toLowerCase()!=='true')
35
40
  fail('explicit Windows UAC elevation required');
36
- const {state,receipt,receiptHash,preflight}=args(process.argv.slice(2));
41
+ const {state,receipt,receiptHash,preflight,hostCommandPins}=args(process.argv.slice(2));
37
42
  if(path.dirname(receipt).toLowerCase()!==path.join(state,'evidence','legacy-migration').toLowerCase())
38
43
  fail('marker receipt path not in state evidence root');
39
44
  const issueFile=fileURLToPath(import.meta.url);
@@ -41,11 +46,12 @@ async function main(){
41
46
  const helperFile=path.join(root,'scripts','request-uac-migration-r153.ps1');
42
47
  const trust=await inspectTrustedInstallation([...privilegedEpochInstallFiles(root,process.execPath),
43
48
  issueFile,deployFile,helperFile,path.join(root,'package.json'),path.join(root,'MANIFEST.SHA256'),path.join(root,'RELEASE.json'),path.join(root,'src','uac-approval-contract-r153.mjs'),
44
- path.join(root,'src','clean-activation-recovery-r153.mjs'),path.join(root,'src','legacy-offline-policy-proof-r146.mjs')]);
49
+ path.join(root,'src','clean-activation-recovery-r153.mjs'),path.join(root,'src','legacy-offline-policy-proof-r146.mjs'),
50
+ path.join(root,'src','host-command-pins-r154.mjs')]);
45
51
  if(trust.trusted!==true)fail('candidate not R89 protected');
46
52
  const pkg=JSON.parse((await read(path.join(root,'package.json'))).toString('utf8'));
47
53
  const rel=JSON.parse((await read(path.join(root,'RELEASE.json'))).toString('utf8'));
48
- if(pkg.version!=='0.15.60'||rel.version!==pkg.version)fail('candidate release identity mismatch');
54
+ if(pkg.version!=='0.15.61'||rel.version!==pkg.version)fail('candidate release identity mismatch');
49
55
  const pointer=parseActivePointer((await read(path.join(state,'active-release.json'))).toString('utf8'));
50
56
  if(pointer.version!=='0.15.45'||rel.parent?.release_id!==pointer.release_id||
51
57
  rel.parent?.manifest_sha256!==pointer.manifest_sha256||
@@ -85,6 +91,7 @@ async function main(){
85
91
  predecessorPointerSha256:uacPointerSha256(pointer),
86
92
  predecessorPolicySha256:policies,preflightSubjectSha256:preflight,
87
93
  nodeSha256:await hash(process.execPath),deployScriptSha256:await hash(deployFile),
94
+ hostCommandPins,
88
95
  nonce,issuedAt,expiresAt});
89
96
  // The strict installation manifest disallows unmanifested files. A receipt
90
97
  // under root would turn an otherwise valid candidate into an invalid tree.
@@ -6,9 +6,9 @@ import {fileURLToPath} from 'node:url';
6
6
  const defaultRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
7
7
  const hex64=/^[0-9a-f]{64}$/;
8
8
  const EXPECTED=Object.freeze({
9
- candidateVersion:'0.15.60', train:'R153',
10
- sourceVersion:'0.15.59',
11
- sourceSha256:'0c20152b8ee553f06f05dfe017f4bd823d30cd50710722e9e0874f0bd6c66c75',
9
+ candidateVersion:'0.15.61', train:'R154',
10
+ sourceVersion:'0.15.60',
11
+ sourceSha256:'b85f735f4c8be56f97ea5c78f0cc48583e8d5708a704c903596f5720e36ace83',
12
12
  runtimeVersion:'0.15.45',
13
13
  runtimeReleaseId:'v0.15.45-34aa8d1241cfb8bf',
14
14
  runtimeManifest:'34aa8d1241cfb8bf0b5ba303bf3c06e0e9e1bf01f64457bca659d6dd4d634fdc',
@@ -7,7 +7,7 @@ param(
7
7
  [Parameter(Mandatory=$true)][string]$MarkerReceiptPath,
8
8
  [Parameter(Mandatory=$true)][ValidatePattern('^[0-9a-f]{64}$')][string]$MarkerReceiptSha256,
9
9
  [ValidatePattern('^[0-9a-f]{64}$')][string]$PreflightSubjectSha256,
10
- [ValidatePattern('^powershell_script=[0-9a-f]{64}$')][string]$HostCommandPin,
10
+ [string]$HostCommandPinsJson = '[]',
11
11
  [switch]$Elevated
12
12
  )
13
13
  $ErrorActionPreference = 'Stop'
@@ -21,6 +21,19 @@ function Require-AbsoluteFile([string]$Value,[string]$Label) {
21
21
  return $item.FullName
22
22
  }
23
23
  function Full([string]$Value) { [IO.Path]::GetFullPath($Value).TrimEnd('\') }
24
+ function Validate-PinsJson([string]$Value) {
25
+ try { $pins = @($Value | ConvertFrom-Json) } catch { throw 'R154 host command pins JSON invalid' }
26
+ if ($pins.Count -gt 16) { throw 'R154 host command pin limit exceeded' }
27
+ $seen = @{}
28
+ foreach ($pin in $pins) {
29
+ if ([string]$pin -cnotmatch '^[A-Za-z0-9._-]{1,64}=[0-9a-f]{64}$') { throw 'R154 host command pin contract invalid' }
30
+ $id = ([string]$pin).Split('=')[0]
31
+ if ($seen.ContainsKey($id)) { throw 'R154 duplicate host command pin' }
32
+ $seen[$id] = $true
33
+ }
34
+ return $pins
35
+ }
36
+ $HostCommandPins = @(Validate-PinsJson $HostCommandPinsJson)
24
37
  $NodePath = Require-AbsoluteFile $NodePath 'Node executable'
25
38
  $DeployScript = Require-AbsoluteFile $DeployScript 'Deploy script'
26
39
  $MarkerReceiptPath = Require-AbsoluteFile $MarkerReceiptPath 'Marker reconciliation receipt'
@@ -100,7 +113,7 @@ if (-not $Elevated) {
100
113
  # -Verb RunAs is the UseShellExecute parameter set. PowerShell 5.1 rejects
101
114
  # -RedirectStandardOutput/-RedirectStandardError in that same Start-Process.
102
115
  # The elevated child writes bounded, no-clobber evidence instead.
103
- if ($HostCommandPin) { $arguments += @('-HostCommandPin',$HostCommandPin) }
116
+ if ($HostCommandPinsJson -cne '[]') { $arguments += @('-HostCommandPinsJson',('"' + $HostCommandPinsJson.Replace('"','\"') + '"')) }
104
117
  $child = Start-Process -FilePath $powershell -ArgumentList $arguments -Verb RunAs -Wait -PassThru -WindowStyle Hidden
105
118
  if (Test-Path -LiteralPath $StdoutPath) { [Console]::Out.Write([IO.File]::ReadAllText($StdoutPath)) }
106
119
  if (Test-Path -LiteralPath $StderrPath) { [Console]::Error.Write([IO.File]::ReadAllText($StderrPath)) }
@@ -123,7 +136,7 @@ try {
123
136
  try {
124
137
  $issuerOutput = @(& $NodePath $issuerScript '--state-root' $env:DEADBYTE_STATE_ROOT `
125
138
  '--marker-receipt' $MarkerReceiptPath '--marker-receipt-sha256' $MarkerReceiptSha256 `
126
- '--preflight-subject-sha256' $PreflightSubjectSha256 2>&1)
139
+ '--preflight-subject-sha256' $PreflightSubjectSha256 '--host-command-pins-json' $HostCommandPinsJson 2>&1)
127
140
  $issuerExit = $LASTEXITCODE
128
141
  } finally { $ErrorActionPreference = $priorErrorAction }
129
142
  $issuerText = (@($issuerOutput) | ForEach-Object { [string]$_ }) -join "`n"
@@ -145,7 +158,7 @@ try {
145
158
  '--migration-expires-at',$issued.expires_at_utc,
146
159
  '--uac-approval-receipt',$issued.receipt_path,
147
160
  '--uac-approval-receipt-sha256',$issued.receipt_sha256)
148
- if ($HostCommandPin) { $deployArgs += @('--host-command-pin',$HostCommandPin) }
161
+ foreach ($pin in $HostCommandPins) { $deployArgs += @('--host-command-pin',[string]$pin) }
149
162
  $deployOutput = @(& $NodePath $DeployScript @deployArgs 2>&1)
150
163
  $deployExit = $LASTEXITCODE
151
164
  } finally { $ErrorActionPreference = $priorErrorAction }
@@ -14,6 +14,7 @@ const files=[...privilegedEpochInstallFiles(root,process.execPath),
14
14
  path.join(root,'package.json'),path.join(root,'MANIFEST.SHA256'),path.join(root,'RELEASE.json'),
15
15
  path.join(root,'src','uac-approval-contract-r153.mjs'),
16
16
  path.join(root,'src','clean-activation-recovery-r153.mjs'),
17
+ path.join(root,'src','host-command-pins-r154.mjs'),
17
18
  path.join(root,'src','legacy-offline-policy-proof-r146.mjs')];
18
19
  const verdict=await inspectTrustedInstallation(files);
19
20
  if(verdict.trusted!==true)throw new Error('R153 protected stage trust rejected');
@@ -32,7 +32,7 @@ export function classifyLegacyMigration(input){
32
32
  if(!input.pointer||input.pointer.version!=='0.15.45'||
33
33
  !HEX64.test(String(input.pointer.manifest_sha256??''))||!HEX64.test(String(input.pointer.generation_manifest_sha256??'')))
34
34
  return reject('predecessor identity invalid',subject);
35
- if(!input.candidate||input.candidate.version!=='0.15.60'||
35
+ if(!input.candidate||input.candidate.version!=='0.15.61'||
36
36
  !HEX64.test(String(input.candidate.manifest_sha256??''))||!HEX64.test(String(input.candidate.tarball_sha256??'')))
37
37
  return reject('candidate identity invalid',subject);
38
38
  if(PLANES.some(plane=>!HEX64.test(String(input.policySha256?.[plane]??''))))
@@ -27,6 +27,7 @@ import {acquireForegroundSessionLock} from './foreground-session-lock.mjs';
27
27
  import { AUTHORITY_PLANES, authorityVector, authorityRestorePlan, parseAuthorityObservation, assertAuthorityVector, assertRuntimeReadyObservation, authorityBanner } from './remote-authority.mjs';
28
28
  import { decodeRemoteLogBytes, boundedRemoteDiagnostic, readRemoteLogAppend } from './remote-log-decode-r60.mjs';
29
29
  import {reconcileStaleMarker,resumeMarkerReconciliation,recoverPublishedMarkerTerminal} from './clean-activation-recovery-r153.mjs';
30
+ import {normalizeHostCommandPins,encodeHostCommandPins} from './host-command-pins-r154.mjs';
30
31
 
31
32
  const REMOTE_COMMANDS=new Set([
32
33
  'menu','start','resume','park','stop','restart','status','url','test','fulltest','toggle','clean','machinetest',
@@ -114,7 +115,7 @@ async function reportVersionCoherence(root,{displayMode='ascii'}={}){
114
115
  writeRemote(process.stderr,`${remoteTag('warn',displayMode)} VERSION SKEW: CLI package ${DEADBYTE_VERSION}; active runtime ${active.version} (${active.release_id})\n`,displayMode);
115
116
  return active;
116
117
  }
117
- async function synchronizeForegroundRuntime(ctx,{displayMode='unicode',migrationContext=null,hostCommandPin=null}={}){
118
+ async function synchronizeForegroundRuntime(ctx,{displayMode='unicode',migrationContext=null,hostCommandPins=[]}={}){
118
119
  const packageManifestSha256=createHash('sha256').update(await readFile(path.join(CURRENT_PACKAGE_ROOT,'MANIFEST.SHA256'))).digest('hex');
119
120
  return ensureRuntimeCoherence({
120
121
  cliVersion:DEADBYTE_VERSION,
@@ -182,7 +183,7 @@ async function synchronizeForegroundRuntime(ctx,{displayMode='unicode',migration
182
183
  '-ExpectedBootstrapSha256',bootstrapMatch[1],
183
184
  '-StdoutPath',stdoutPath,'-StderrPath',stderrPath,
184
185
  '-MarkerReceiptPath',migrationContext.receipt_path,'-MarkerReceiptSha256',migrationContext.receipt_sha256,
185
- ...(hostCommandPin?['-HostCommandPin',hostCommandPin]:[])],{
186
+ ...(hostCommandPins.length?['-HostCommandPinsJson',encodeHostCommandPins(hostCommandPins)]:[])],{
186
187
  cwd:CURRENT_PACKAGE_ROOT,env:windowsPowerShellEnvironment(process.env),encoding:'utf8',windowsHide:true,shell:false,
187
188
  timeout:REMOTE_UPDATE_TIMEOUT_MS,maxBuffer:16*1024*1024
188
189
  });
@@ -744,7 +745,7 @@ async function releaseRemoteSession(ctx,marker,{displayMode='unicode'}={}){
744
745
  if(deletePinnedRemoteMarker(remoteSessionMarker(ctx.root),snapshot.sha256)!=='REMOVED')
745
746
  throw new Error('remote session ownership marker replaced before native deletion; marker retained');
746
747
  }
747
- async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,filterExpression='',mode='default',hostCommandPin=null}={}){
748
+ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,filterExpression='',mode='default',hostCommandPins=[]}={}){
748
749
  const display=prepareRemoteConsole();
749
750
  const displayMode=display.mode;
750
751
  const ctx=await remoteContext();
@@ -756,7 +757,7 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
756
757
  startupLock.assertHeld();
757
758
  const migrationContext=await recoverRemoteSession(ctx,{displayMode});
758
759
  startupLock.assertHeld();
759
- await synchronizeForegroundRuntime(ctx,{displayMode,migrationContext,hostCommandPin});
760
+ await synchronizeForegroundRuntime(ctx,{displayMode,migrationContext,hostCommandPins});
760
761
  startupLock.assertHeld();
761
762
  marker=await claimRemoteSession(ctx);
762
763
  startupLock.assertHeld();
@@ -869,17 +870,16 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
869
870
  }
870
871
  }
871
872
  async function runRemote(args){
872
- let verbose=false,trace=false,filterExpression='',resumeCursor=null,mode='default',hostCommandPin=null;
873
+ let verbose=false,trace=false,filterExpression='',resumeCursor=null,mode='default';
874
+ const hostCommandPinValues=[];
873
875
  const positional=[];
874
876
  for(let index=0;index<args.length;index++){
875
877
  const arg=args[index];
876
878
  if(arg==='--verbose'||arg==='-v'){verbose=true;continue;}
877
879
  if(arg==='--trace'){trace=true;continue;}
878
880
  if(arg==='--host-command-pin'){
879
- if(hostCommandPin!==null)throw new Error('remote --host-command-pin may be specified once');
880
881
  const value=String(args[++index]??'');
881
- if(!/^powershell_script=[0-9a-f]{64}$/.test(value))throw new Error('remote --host-command-pin requires powershell_script=lowercase_sha256');
882
- hostCommandPin=value;continue;
882
+ hostCommandPinValues.push(value);continue;
883
883
  }
884
884
  if(['--operator','--desktop','--host','--full'].includes(arg)){
885
885
  if(mode!=='default') throw new Error('remote authority modes are mutually exclusive');
@@ -897,8 +897,11 @@ async function runRemote(args){
897
897
  }
898
898
  positional.push(arg);
899
899
  }
900
- const presentation=verbose||trace||Boolean(filterExpression)||resumeCursor!==null||mode!=='default'||hostCommandPin!==null;
901
- if(positional.length===0){await runRemoteSession({resumeCursor,verbose,trace,filterExpression,mode,hostCommandPin});return;}
900
+ let hostCommandPins;
901
+ try{hostCommandPins=normalizeHostCommandPins(hostCommandPinValues);}
902
+ catch(error){throw new Error(`remote --host-command-pin ${error.message}`);}
903
+ const presentation=verbose||trace||Boolean(filterExpression)||resumeCursor!==null||mode!=='default'||hostCommandPins.length>0;
904
+ if(positional.length===0){await runRemoteSession({resumeCursor,verbose,trace,filterExpression,mode,hostCommandPins});return;}
902
905
  if(presentation) throw new Error('remote presentation flags are valid only for foreground session');
903
906
  if(positional.length>1) throw new Error('remote accepts one controller command');
904
907
  const command=positional[0];if(!REMOTE_COMMANDS.has(command)) throw new Error(`unsupported remote command '${command}'`);
@@ -0,0 +1,34 @@
1
+ const PIN=/^([A-Za-z0-9._-]{1,64})=([0-9a-f]{64})$/;
2
+ const LIMIT=16;
3
+ const REPINNABLE=new Set(['powershell_script','powershell_inline','cmd_script','cmd_inline','node_script','node_inline']);
4
+
5
+ export function normalizeHostCommandPins(values=[]){
6
+ if(!Array.isArray(values)||values.length>LIMIT)throw new Error(`host command pin limit is ${LIMIT}`);
7
+ const seen=new Set(),pins=[];
8
+ for(const value of values){
9
+ const match=PIN.exec(String(value??''));
10
+ if(!match)throw new Error('host command pin contract requires command_id=lowercase_sha256');
11
+ const [,command_id,sha256]=match;
12
+ if(!REPINNABLE.has(command_id))throw new Error(`host command '${command_id}' is not repinnable`);
13
+ if(seen.has(command_id))throw new Error(`duplicate host command pin '${command_id}'`);
14
+ seen.add(command_id);pins.push(Object.freeze({command_id,sha256}));
15
+ }
16
+ pins.sort((a,b)=>a.command_id<b.command_id?-1:a.command_id>b.command_id?1:0);
17
+ return Object.freeze(pins);
18
+ }
19
+
20
+ export function encodeHostCommandPins(pins=[]){
21
+ return JSON.stringify(normalizeHostCommandPins(pins.map(pin=>`${pin.command_id}=${pin.sha256}`))
22
+ .map(pin=>`${pin.command_id}=${pin.sha256}`));
23
+ }
24
+
25
+ export function decodeHostCommandPins(encoded='[]'){
26
+ let values;
27
+ try{values=JSON.parse(String(encoded));}catch{throw new Error('host command pins JSON invalid');}
28
+ return normalizeHostCommandPins(values);
29
+ }
30
+
31
+ export function hostCommandPinArgs(pins=[]){
32
+ return normalizeHostCommandPins(pins.map(pin=>`${pin.command_id}=${pin.sha256}`))
33
+ .flatMap(pin=>['--host-command-pin',`${pin.command_id}=${pin.sha256}`]);
34
+ }
@@ -3,6 +3,7 @@
3
3
  // Verify it only when running elevated from an independently R89-verified install.
4
4
  import {createHash} from 'node:crypto';
5
5
  import path from 'node:path';
6
+ import {normalizeHostCommandPins} from './host-command-pins-r154.mjs';
6
7
 
7
8
  const HEX64=/^[0-9a-f]{64}$/;
8
9
  const PLANES=['process','coding','autonomous','desktop','host'];
@@ -10,7 +11,7 @@ const ALL_DISARMED=Object.freeze(Object.fromEntries(PLANES.map(p=>[p,false])));
10
11
  const TIME=/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{3}Z$/;
11
12
  const FIELDS=['schema','action','state_root','candidate_root','candidate_version','candidate_manifest_sha256',
12
13
  'tarball_sha256','marker_receipt_sha256','predecessor_pointer_sha256','predecessor_policy_sha256',
13
- 'preflight_subject_sha256','node_sha256','deploy_script_sha256','nonce','issued_at_utc',
14
+ 'preflight_subject_sha256','node_sha256','deploy_script_sha256','host_command_pins','nonce','issued_at_utc',
14
15
  'expires_at_utc','allowed_authority_after','subject_sha256'];
15
16
  function canonical(value){
16
17
  if(value===null)return 'null';
@@ -58,7 +59,7 @@ export function uacReceiptSubject(input){
58
59
  const candidate=windowsAbsolute(input.candidateRoot,'candidate root');
59
60
  if(!candidate.startsWith('c:\\program files\\')||candidate==='c:\\program files\\')
60
61
  throw new Error('UAC migration requires a protected candidate installation');
61
- if(input.candidateVersion!=='0.15.60')throw new Error('UAC migration candidate identity unsupported');
62
+ if(input.candidateVersion!=='0.15.61')throw new Error('UAC migration candidate identity unsupported');
62
63
  const hashes=['candidateManifestSha256','tarballSha256','markerReceiptSha256','predecessorPointerSha256',
63
64
  'preflightSubjectSha256','nodeSha256','deployScriptSha256','nonce'];
64
65
  for(const key of hashes)if(!HEX64.test(String(input[key]??'')))throw new Error('UAC approval hash/nonce invalid: '+key);
@@ -68,6 +69,8 @@ export function uacReceiptSubject(input){
68
69
  throw new Error('UAC predecessor policy identity incomplete');
69
70
  const issued=time(input.issuedAt,'issue time'),expires=time(input.expiresAt,'expiry time');
70
71
  if(expires<=issued||expires-issued>600_000)throw new Error('UAC consent lifetime invalid');
72
+ const hostCommandPins=normalizeHostCommandPins((input.hostCommandPins??[]).map(pin=>
73
+ typeof pin==='string'?pin:`${pin?.command_id??''}=${pin?.sha256??''}`));
71
74
  return Object.freeze({schema:'deadbyte.r153-uac-approval.v1',action:'narrow_legacy_policy_and_activate',
72
75
  state_root:root,candidate_root:candidate,candidate_version:input.candidateVersion,
73
76
  candidate_manifest_sha256:input.candidateManifestSha256,tarball_sha256:input.tarballSha256,
@@ -75,7 +78,7 @@ export function uacReceiptSubject(input){
75
78
  predecessor_pointer_sha256:input.predecessorPointerSha256,
76
79
  predecessor_policy_sha256:Object.freeze(Object.fromEntries(PLANES.map(p=>[p,policies[p]]))),
77
80
  preflight_subject_sha256:input.preflightSubjectSha256,node_sha256:input.nodeSha256,
78
- deploy_script_sha256:input.deployScriptSha256,nonce:input.nonce,
81
+ deploy_script_sha256:input.deployScriptSha256,host_command_pins:hostCommandPins,nonce:input.nonce,
79
82
  issued_at_utc:input.issuedAt,expires_at_utc:input.expiresAt,
80
83
  allowed_authority_after:Object.freeze({...ALL_DISARMED})});
81
84
  }
package/src/version.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.15.60';
1
+ export const DEADBYTE_VERSION = '0.15.61';
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';
@@ -13,15 +13,15 @@ test('R130 security patch has a new immutable package identity and truthful sour
13
13
  read('SBOM.cdx.json'),read('package-lock.json'),read('TEST-MATRIX.json')]);
14
14
  assert.equal(pkg.name,'deadbyte-mcp');
15
15
  for(const actual of [pkg.version,release.version,provenance.version,matrix.version,bom.metadata.component.version,lock.version,lock.packages[''].version,DEADBYTE_VERSION])
16
- assert.equal(actual,'0.15.60');
17
- assert.equal(release.train,'R153');
18
- assert.equal(matrix.train,'R153');
19
- assert.equal(release.source_predecessor.version,'0.15.59');
20
- assert.match(release.source_predecessor.source_path,/0\.15\.59/);
16
+ assert.equal(actual,'0.15.61');
17
+ assert.equal(release.train,'R154');
18
+ assert.equal(matrix.train,'R154');
19
+ assert.equal(release.source_predecessor.version,'0.15.60');
20
+ assert.match(release.source_predecessor.source_path,/0\.15\.60/);
21
21
  assert.equal(release.runtime_predecessor.version,'0.15.45');
22
- assert.equal(provenance.source_predecessor_tarball_sha256,'0c20152b8ee553f06f05dfe017f4bd823d30cd50710722e9e0874f0bd6c66c75');
23
- assert.equal(provenance.source_predecessor_version,'0.15.59');
24
- assert.equal(provenance.source_predecessor_manifest_file_sha256,'f727be4a63220f4215f18282a1fd89963924a64adcd07747ca3c12d8fb166d86');
22
+ assert.equal(provenance.source_predecessor_tarball_sha256,'b85f735f4c8be56f97ea5c78f0cc48583e8d5708a704c903596f5720e36ace83');
23
+ assert.equal(provenance.source_predecessor_version,'0.15.60');
24
+ assert.equal(provenance.source_predecessor_manifest_file_sha256,'2a19c9da7f11eb831dfb24ba6c6d9789e25a373cea2f3a25ff21d460e07fba36');
25
25
  assert.equal(pkg.dependencies['@modelcontextprotocol/client'],'2.3.1');
26
26
  assert.equal(pkg.dependencies['@modelcontextprotocol/server'],'2.3.1');
27
27
  for(const dep of ['@modelcontextprotocol/client','@modelcontextprotocol/server'])
@@ -15,13 +15,13 @@ test('R144 package identities, R132 historic evidence and published predecessor
15
15
  for(const value of [pkg.version,lock.version,lock.packages[''].version,release.version,prov.version,bom.metadata.component.version,matrix.version,DEADBYTE_VERSION]){
16
16
  assert.equal(value,pkg.version);
17
17
  }
18
- assert.equal(release.train,'R153');
19
- assert.equal(matrix.train,'R153');
20
- assert.equal(release.source_predecessor.version,'0.15.59');
21
- assert.match(release.source_predecessor.source_path,/0\.15\.59/);
22
- assert.equal(prov.source_predecessor_version,'0.15.59');
23
- assert.equal(prov.source_predecessor_manifest_file_sha256,'f727be4a63220f4215f18282a1fd89963924a64adcd07747ca3c12d8fb166d86');
24
- assert.equal(prov.parent_tarball_sha256,'0c20152b8ee553f06f05dfe017f4bd823d30cd50710722e9e0874f0bd6c66c75');
18
+ assert.equal(release.train,'R154');
19
+ assert.equal(matrix.train,'R154');
20
+ assert.equal(release.source_predecessor.version,'0.15.60');
21
+ assert.match(release.source_predecessor.source_path,/0\.15\.60/);
22
+ assert.equal(prov.source_predecessor_version,'0.15.60');
23
+ assert.equal(prov.source_predecessor_manifest_file_sha256,'2a19c9da7f11eb831dfb24ba6c6d9789e25a373cea2f3a25ff21d460e07fba36');
24
+ assert.equal(prov.parent_tarball_sha256,'b85f735f4c8be56f97ea5c78f0cc48583e8d5708a704c903596f5720e36ace83');
25
25
  assert.equal(prov.parent_registry_tarball_sha1,'04cc1a1e80bcb7062ac39eee6b7bab70edd5d8c3');
26
26
  assert.equal(release.runtime_predecessor.version,'0.15.45');
27
27
  assert.equal(prov.runtime_predecessor.release_id,release.runtime_predecessor.release_id);
@@ -22,7 +22,7 @@ const planes=['process','coding','autonomous','desktop','host'];
22
22
  const base=()=>({
23
23
  stateRoot:'C:\\Users\\owner\\.deadbyte-mcp',
24
24
  pointer:{release_id:'v0.15.45-parent',version:'0.15.45',manifest_sha256:'1'.repeat(64),generation_manifest_sha256:'2'.repeat(64)},
25
- candidate:{version:'0.15.60',manifest_sha256:'3'.repeat(64),tarball_sha256:'4'.repeat(64)},
25
+ candidate:{version:'0.15.61',manifest_sha256:'3'.repeat(64),tarball_sha256:'4'.repeat(64)},
26
26
  observation:{state:'OFFLINE',local_ready:false,bridge:{alive:false},core:{alive:false},cloud:{alive:false},supervisor:{running:false},
27
27
  authority:Object.fromEntries(planes.map(plane=>[plane,{configured:true,armed:false,reason:plane==='autonomous'?'unverified':'disarmed'}]))},
28
28
  policySha256:Object.fromEntries(planes.map((plane,index)=>[plane,String(index+5).repeat(64)])),
@@ -55,7 +55,7 @@ test('R153 RED: UAC subject binds candidate, predecessor, policy, marker, state
55
55
  issuedAt:'2026-10-23T16:00:00.000Z',expiresAt:'2026-10-23T16:10:00.000Z'});
56
56
  assert.equal(subject.action,'narrow_legacy_policy_and_activate');
57
57
  assert.equal(subject.marker_sha256,HEX);
58
- assert.equal(subject.candidate.version,'0.15.60');
58
+ assert.equal(subject.candidate.version,'0.15.61');
59
59
  assert.equal(subject.nonce,'b'.repeat(64));
60
60
  assert.deepEqual(subject.allowed_authority_after,Object.fromEntries(planes.map(plane=>[plane,false])));
61
61
  });
@@ -29,7 +29,7 @@ test('R153 rejects runtime predecessor or parent drift',()=>{
29
29
  test('R153 rejects version / train / bridge identity drift',()=>{
30
30
  const state=copy();state.pkg.version='0.15.56';
31
31
  assert.throws(()=>verifyR153Lineage(state),/package version/);
32
- const state2=copy();state2.release.train='R154';
32
+ const state2=copy();state2.release.train='R153';
33
33
  assert.throws(()=>verifyR153Lineage(state2),/train/);
34
34
  const state3=copy();state3.bridge.version='0.15.45';
35
35
  assert.throws(()=>verifyR153Lineage(state3),/bridge version/);
@@ -32,13 +32,13 @@ test('signed R27 initial precondition reject safely reconciles R65 unknown claim
32
32
  const dir=path.join(updates,'r27-'+'d'.repeat(16)+'-synthetic');
33
33
  await mkdir(dir);
34
34
  const failure=seal({schema:'deadbyte.r27-slot-update-failure.v1',update_id:path.basename(dir),
35
- release_version:'0.15.60',candidate_manifest_sha256:'d'.repeat(64),failed_at_utc:now(),
35
+ release_version:'0.15.61',candidate_manifest_sha256:'d'.repeat(64),failed_at_utc:now(),
36
36
  result:'PRECONDITION_REJECTED',phase:'initial',error:{name:'Error',message:"host command 'powershell_script' executable pin drift detected; explicit approval required"},
37
37
  prior_pointer:pointer,attempted_pointer:null,rollback:{attempted:false,errors:[]}},privateKey,publicKey);
38
38
  await writeFile(path.join(dir,'failure-receipt.json'),JSON.stringify(failure));
39
39
  await writeFile(path.join(updates,'r65-activation-outcome-test.json'),JSON.stringify({
40
40
  schema:'deadbyte.r65-activation-outcome.v1',claim_operation_id:claim.operation_id,
41
- preflight_subject_sha256:claim.subject_sha256,candidate_version:'0.15.60',
41
+ preflight_subject_sha256:claim.subject_sha256,candidate_version:'0.15.61',
42
42
  candidate_manifest_sha256:'d'.repeat(64),predecessor_release_id:pointer.release_id,
43
43
  outcome:{state:'ACTIVATION_OUTCOME_UNKNOWN'},update_id:null,continuity_update_id:null,state_path:null,observed_at_utc:now()
44
44
  }));
@@ -20,28 +20,39 @@ test('R153 Windows ACL reproduction rejects stripped leaf ACE and restores direc
20
20
  $root = Join-Path $env:TEMP ('r153-native-acl-test-' + [guid]::NewGuid().ToString('N'))
21
21
  $sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
22
22
  $leaf = Join-Path $root 'child\file.txt'
23
+ function Read-Digest([string]$Path) {
24
+ try {
25
+ $sha = [Security.Cryptography.SHA256]::Create()
26
+ try { return ([BitConverter]::ToString($sha.ComputeHash([IO.File]::ReadAllBytes($Path)))).Replace('-','') }
27
+ finally { $sha.Dispose() }
28
+ } catch [UnauthorizedAccessException] { return $null }
29
+ }
23
30
  try {
24
31
  New-Item -ItemType Directory -Path (Split-Path -Parent $leaf) -Force | Out-Null
25
32
  [IO.File]::WriteAllText($leaf,'read-only regression fixture')
26
33
  & icacls.exe $root '/inheritance:r' '/grant:r' ('*'+$sid+':(OI)(CI)(F)') '/T' '/C' | Out-Null
27
34
  if($LASTEXITCODE -ne 0){throw 'REPRO_SETUP_FAILED'}
28
- $blocked = Get-FileHash -LiteralPath $leaf -Algorithm SHA256 -ErrorAction SilentlyContinue
35
+ $blocked = Read-Digest $leaf
29
36
  if($null -ne $blocked){throw 'REPRO_DID_NOT_STRIP_LEAF_ACE'}
30
37
  & icacls.exe $root '/grant' ('*'+$sid+':(F)') '/T' '/C' | Out-Null
31
38
  if($LASTEXITCODE -ne 0){throw 'LEAF_REPAIR_FAILED'}
32
- $restored = Get-FileHash -LiteralPath $leaf -Algorithm SHA256
33
- if($null -eq $restored -or $restored.Hash.Length -ne 64){throw 'LEAF_NOT_READABLE_AFTER_REPAIR'}
34
- $acl = Get-Acl -LiteralPath $leaf
35
- $allow=@($acl.Access | Where-Object {$_.AccessControlType -eq 'Allow'})
39
+ $restored = Read-Digest $leaf
40
+ if($null -eq $restored -or $restored.Length -ne 64){throw 'LEAF_NOT_READABLE_AFTER_REPAIR'}
41
+ $acl = [IO.File]::GetAccessControl($leaf)
42
+ $allow=@($acl.GetAccessRules($true,$true,[Security.Principal.SecurityIdentifier]) | Where-Object {$_.AccessControlType -eq 'Allow'})
36
43
  if($allow.Count -ne 1){throw 'LEAF_HAS_UNEXPECTED_ALLOW_ACE_COUNT'}
37
- if($allow[0].IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value -ne $sid){throw 'LEAF_HAS_UNEXPECTED_TRUSTEE'}
44
+ if($allow[0].IdentityReference.Value -ne $sid){throw 'LEAF_HAS_UNEXPECTED_TRUSTEE'}
38
45
  Write-Output 'R153_WIN32_LEAF_ACL_REPAIR_PASS'
39
46
  } finally {
40
- if(Test-Path -LiteralPath $root){Remove-Item -LiteralPath $root -Force -Recurse}
47
+ if(Test-Path -LiteralPath $root){
48
+ & icacls.exe $root '/reset' '/T' '/C' | Out-Null
49
+ & icacls.exe $root '/inheritance:e' '/grant:r' ('*'+$sid+':(OI)(CI)(F)') '/T' '/C' | Out-Null
50
+ Remove-Item -LiteralPath $root -Force -Recurse
51
+ }
41
52
  }`;
42
53
  const encoded=Buffer.from(ps,'utf16le').toString('base64');
43
54
  const result=spawnSync('powershell.exe',['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',encoded],
44
55
  {encoding:'utf8',timeout:30000,maxBuffer:256*1024});
45
56
  assert.equal(result.status,0,result.stderr||result.stdout||String(result.error));
46
57
  assert.match(result.stdout,/R153_WIN32_LEAF_ACL_REPAIR_PASS/);
47
- });
58
+ });
@@ -4,12 +4,13 @@ import {createUacApproval,verifyUacApproval,validateApprovalWindow,requireTruste
4
4
  import {readFileSync} from 'node:fs';
5
5
  import {fileURLToPath} from 'node:url';
6
6
  const h=(n)=>n.repeat(64);
7
- const base=()=>({stateRoot:'C:\\Users\\DEADBYTE\\.deadbyte-mcp',candidateRoot:'C:\\Program Files\\DEADBYTE-R153-TEST',candidateVersion:'0.15.60',candidateManifestSha256:h('a'),tarballSha256:h('b'),markerReceiptSha256:h('c'),predecessorPointerSha256:h('d'),predecessorPolicySha256:{process:h('1'),coding:h('2'),autonomous:h('3'),desktop:h('4'),host:h('5')},preflightSubjectSha256:h('e'),nodeSha256:h('f'),deployScriptSha256:h('0'),nonce:h('6'),issuedAt:'2026-10-24T00:00:00.000Z',expiresAt:'2026-10-24T00:05:00.000Z'});
7
+ const base=()=>({stateRoot:'C:\\Users\\DEADBYTE\\.deadbyte-mcp',candidateRoot:'C:\\Program Files\\DEADBYTE-R153-TEST',candidateVersion:'0.15.61',candidateManifestSha256:h('a'),tarballSha256:h('b'),markerReceiptSha256:h('c'),predecessorPointerSha256:h('d'),predecessorPolicySha256:{process:h('1'),coding:h('2'),autonomous:h('3'),desktop:h('4'),host:h('5')},preflightSubjectSha256:h('e'),nodeSha256:h('f'),deployScriptSha256:h('0'),hostCommandPins:[],nonce:h('6'),issuedAt:'2026-10-24T00:00:00.000Z',expiresAt:'2026-10-24T00:05:00.000Z'});
8
8
  test('R153 UAC receipt carries exact no-grants narrow migration subject',()=>{
9
- const x=base(),receipt=createUacApproval(x);
9
+ const x={...base(),hostCommandPins:[{command_id:'powershell_script',sha256:h('a')},{command_id:'cmd_script',sha256:h('b')}]},receipt=createUacApproval(x);
10
10
  assert.equal(receipt.schema,'deadbyte.r153-uac-approval.v1');
11
11
  assert.equal(receipt.action,'narrow_legacy_policy_and_activate');
12
12
  assert.deepEqual(receipt.allowed_authority_after,{process:false,coding:false,autonomous:false,desktop:false,host:false});
13
+ assert.deepEqual(receipt.host_command_pins,[{command_id:'cmd_script',sha256:h('b')},{command_id:'powershell_script',sha256:h('a')}]);
13
14
  assert.match(receipt.subject_sha256,/^[0-9a-f]{64}$/);
14
15
  assert.deepEqual(verifyUacApproval(receipt,x,{now:'2026-10-24T00:02:00.000Z'}),receipt);
15
16
  });
@@ -19,6 +20,7 @@ test('R153 UAC receipt rejects drift across every authority/candidate/runtime in
19
20
  assert.throws(()=>verifyUacApproval(receipt,{...x,[key]:value},{now:'2026-10-24T00:02:00.000Z'}),undefined,key);
20
21
  }
21
22
  assert.throws(()=>verifyUacApproval(receipt,{...x,predecessorPolicySha256:{...x.predecessorPolicySha256,host:h('a')}},{now:'2026-10-24T00:02:00.000Z'}));
23
+ assert.throws(()=>verifyUacApproval(receipt,{...x,hostCommandPins:[{command_id:'cmd_script',sha256:h('a')}]},{now:'2026-10-24T00:02:00.000Z'}));
22
24
  });
23
25
  test('R153 UAC receipt fails closed on expiration, malformed, replay-like tamper and grant widening',()=>{
24
26
  const x=base(),receipt=createUacApproval(x);
@@ -31,7 +33,7 @@ test('R153 UAC receipt fails closed on expiration, malformed, replay-like tamper
31
33
  assert.throws(()=>createUacApproval({...x,issuedAt:'not-a-date'}));
32
34
  assert.throws(()=>createUacApproval({...x,nonce:'x'}));
33
35
  });
34
- test('R153 UAC scope only accepts protected root and exact v0.15.60',()=>{
36
+ test('R154 UAC scope only accepts protected root and exact v0.15.61',()=>{
35
37
  assert.throws(()=>createUacApproval({...base(),candidateRoot:'C:\\Users\\DEADBYTE\\npm-cache'}),/protected/i);
36
38
  assert.throws(()=>createUacApproval({...base(),candidateVersion:'0.15.55'}),/candidate/i);
37
39
  assert.throws(()=>validateApprovalWindow('2026-10-24T00:00:00.000Z','2026-10-24T00:00:00.000Z','2026-10-24T00:00:00.000Z'));
@@ -45,7 +45,7 @@ test('R153 RED: mutable npx entrypoint stages exact registry package before priv
45
45
  assert.doesNotMatch(script,/npm-cli\.js|npm install/i);
46
46
  assert.match(script,/Get-FileHash -LiteralPath \$file -Algorithm SHA256/);
47
47
  assert.match(script,/NODE_OPTIONS/);
48
- assert.match(script,/https:\/\/registry\.npmjs\.org\/deadbyte-mcp\/0\.15\.60/);
48
+ assert.match(script,/https:\/\/registry\.npmjs\.org\/deadbyte-mcp\/0\.15\.61/);
49
49
  assert.match(script,/live npm registry tarball SHA-256 mismatch/);
50
50
  assert.match(script,/live npm registry tarball integrity mismatch/);
51
51
  assert.match(script,/DEADBYTE-R153-Stage-/);
@@ -0,0 +1,45 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {spawnSync} from 'node:child_process';
4
+ import {fileURLToPath} from 'node:url';
5
+ import {
6
+ normalizeHostCommandPins,
7
+ encodeHostCommandPins,
8
+ decodeHostCommandPins
9
+ } from '../src/host-command-pins-r154.mjs';
10
+
11
+ const h=value=>value.repeat(64);
12
+
13
+ test('R154 accepts a bounded set of distinct existing-command repins',()=>{
14
+ const pins=normalizeHostCommandPins([
15
+ `powershell_script=${h('a')}`,
16
+ `cmd_script=${h('b')}`,
17
+ `cmd_inline=${h('b')}`
18
+ ]);
19
+ assert.deepEqual(pins,[
20
+ {command_id:'cmd_inline',sha256:h('b')},
21
+ {command_id:'cmd_script',sha256:h('b')},
22
+ {command_id:'powershell_script',sha256:h('a')}
23
+ ]);
24
+ assert.deepEqual(decodeHostCommandPins(encodeHostCommandPins(pins)),pins);
25
+ });
26
+
27
+ test('R154 rejects duplicate, malformed, oversized, and delimiter-injected repins',()=>{
28
+ assert.throws(()=>normalizeHostCommandPins([`cmd_script=${h('a')}`,`cmd_script=${h('a')}`]),/duplicate/i);
29
+ assert.throws(()=>normalizeHostCommandPins([`unrelated_command=${h('a')}`]),/not repinnable/i);
30
+ assert.throws(()=>normalizeHostCommandPins([`bad/name=${h('a')}`]),/contract/i);
31
+ assert.throws(()=>normalizeHostCommandPins([`cmd_script=${h('A')}`]),/contract/i);
32
+ assert.throws(()=>normalizeHostCommandPins(Array.from({length:17},(_,i)=>`cmd_${i}=${h('a')}`)),/limit/i);
33
+ assert.throws(()=>decodeHostCommandPins('["cmd_script='+h('a')+'","evil"]'),/contract/i);
34
+ });
35
+
36
+ test('R154 CLI no longer rejects the second explicit command repin in parsing',()=>{
37
+ const cli=fileURLToPath(new URL('../src/deadbyte-cli.mjs',import.meta.url));
38
+ const run=spawnSync(process.execPath,[cli,'remote',
39
+ '--host-command-pin',`powershell_script=${h('a')}`,
40
+ '--host-command-pin',`cmd_script=${h('b')}`,
41
+ 'status'],{encoding:'utf8',timeout:8000});
42
+ assert.equal(run.status,1);
43
+ assert.doesNotMatch(run.stderr,/may be specified once|requires powershell_script/);
44
+ assert.match(run.stderr,/presentation flags are valid only for foreground session/);
45
+ });
@@ -83,7 +83,7 @@ test('R72 compact baseline permits only R64 read offset and authorized coding-on
83
83
  assert.deepEqual(diff.added,[]);
84
84
  assert.deepEqual(diff.removed,[]);
85
85
  const packageVersion=JSON.parse(await readFile(path.join(repoRoot,'package.json'),'utf8')).version;
86
- if(['0.15.41','0.15.42','0.15.43','0.15.44','0.15.45','0.15.46','0.15.47','0.15.48','0.15.49','0.15.50','0.15.51','0.15.52','0.15.53','0.15.54','0.15.55','0.15.56','0.15.57','0.15.58','0.15.59','0.15.60'].includes(packageVersion)){
86
+ if(['0.15.41','0.15.42','0.15.43','0.15.44','0.15.45','0.15.46','0.15.47','0.15.48','0.15.49','0.15.50','0.15.51','0.15.52','0.15.53','0.15.54','0.15.55','0.15.56','0.15.57','0.15.58','0.15.59','0.15.60','0.15.61'].includes(packageVersion)){
87
87
  assert.deepEqual([...diff.schema_changed].sort(),['machine_fs','native_diff_preview']);
88
88
  assert.equal(diff.unchanged.length,41);
89
89
  const prior=baseline.tools.find(tool=>tool.name==='machine_fs');