deadbyte-mcp 0.15.59 → 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.
@@ -1,5 +1,5 @@
1
1
  param(
2
- [Parameter(Mandatory=$true)][ValidateSet('0.15.59')][string]$ExpectedVersion,
2
+ [Parameter(Mandatory=$true)][ValidateSet('0.15.61')][string]$ExpectedVersion,
3
3
  [Parameter(Mandatory=$true)][string]$StateRoot,
4
4
  [Parameter(Mandatory=$true)][string]$TarballPath,
5
5
  [Parameter(Mandatory=$true)][ValidatePattern('^[0-9a-f]{64}$')][string]$ExpectedTarballSha256,
@@ -9,6 +9,7 @@ param(
9
9
  [Parameter(Mandatory=$true)][string]$StderrPath,
10
10
  [Parameter(Mandatory=$true)][string]$MarkerReceiptPath,
11
11
  [Parameter(Mandatory=$true)][ValidatePattern('^[0-9a-f]{64}$')][string]$MarkerReceiptSha256,
12
+ [string]$HostCommandPinsJson = '[]',
12
13
  [switch]$Bootstrap
13
14
  )
14
15
  $ErrorActionPreference = 'Stop'
@@ -20,6 +21,18 @@ function Require-Absolute([string]$Value,[string]$Label) {
20
21
  return [IO.Path]::GetFullPath($Value)
21
22
  }
22
23
  function Quote([string]$Value) { '"' + $Value.Replace('"','\"') + '"' }
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
+ }
35
+ Validate-PinsJson $HostCommandPinsJson
23
36
 
24
37
  $StateRoot = Require-Absolute $StateRoot 'State root'
25
38
  $TarballPath = Require-Absolute $TarballPath 'Exact tarball'
@@ -65,6 +78,7 @@ if (-not $Bootstrap) {
65
78
  '-ExpectedManifestSha256',$ExpectedManifestSha256,'-ExpectedBootstrapSha256',$ExpectedBootstrapSha256,
66
79
  '-StdoutPath',(Quote $StdoutPath),'-StderrPath',(Quote $StderrPath),
67
80
  '-MarkerReceiptPath',(Quote $MarkerReceiptPath),'-MarkerReceiptSha256',$MarkerReceiptSha256)
81
+ if ($HostCommandPinsJson -cne '[]') { $arguments += @('-HostCommandPinsJson',(Quote $HostCommandPinsJson)) }
68
82
  $child = Start-Process -FilePath $powershell -ArgumentList $arguments -Verb RunAs -Wait -PassThru -WindowStyle Hidden
69
83
  if (Test-Path -LiteralPath $StdoutPath) { [Console]::Out.Write([IO.File]::ReadAllText($StdoutPath)) }
70
84
  if (Test-Path -LiteralPath $StderrPath) { [Console]::Error.Write([IO.File]::ReadAllText($StderrPath)) }
@@ -99,7 +113,7 @@ if ($LASTEXITCODE -ne 0) { throw 'R153 protected staging initial ACL normalizati
99
113
  & $icacls $prefix '/setowner' '*S-1-5-32-544' '/T' '/C' | Out-Null
100
114
  if ($LASTEXITCODE -ne 0) { throw 'R153 protected staging initial owner normalization failed' }
101
115
 
102
- $metadataUri = 'https://registry.npmjs.org/deadbyte-mcp/0.15.59'
116
+ $metadataUri = 'https://registry.npmjs.org/deadbyte-mcp/0.15.61'
103
117
  $tagsUri = 'https://registry.npmjs.org/-/package/deadbyte-mcp/dist-tags'
104
118
  [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
105
119
  $metadata = Invoke-RestMethod -UseBasicParsing -Uri $metadataUri -Method Get -TimeoutSec 30
@@ -223,9 +237,11 @@ $preflight = ($preflightOutput -join "`n") | ConvertFrom-Json
223
237
  if ($preflight.state -ne 'AUTHORITY_UNVERIFIED' -or $preflight.subject_sha256 -notmatch '^[0-9a-f]{64}$') {
224
238
  throw "R153 protected migration not applicable: $($preflight.state)"
225
239
  }
226
- & $helper -Elevated -NodePath $node -DeployScript $deploy -StateRoot $StateRoot `
227
- -StdoutPath $StdoutPath -StderrPath $StderrPath -MarkerReceiptPath $MarkerReceiptPath `
228
- -MarkerReceiptSha256 $MarkerReceiptSha256 -PreflightSubjectSha256 $preflight.subject_sha256
240
+ $helperArgs = @('-Elevated','-NodePath',$node,'-DeployScript',$deploy,'-StateRoot',$StateRoot,
241
+ '-StdoutPath',$StdoutPath,'-StderrPath',$StderrPath,'-MarkerReceiptPath',$MarkerReceiptPath,
242
+ '-MarkerReceiptSha256',$MarkerReceiptSha256,'-PreflightSubjectSha256',$preflight.subject_sha256)
243
+ if ($HostCommandPinsJson -cne '[]') { $helperArgs += @('-HostCommandPinsJson',$HostCommandPinsJson) }
244
+ & $helper @helperArgs
229
245
  $helperExit = $LASTEXITCODE
230
246
  try { Remove-Item -LiteralPath $prefix -Recurse -Force -ErrorAction Stop }
231
247
  catch { [Console]::Error.WriteLine('R153_STAGE_CLEANUP_WARNING: protected stage retained for operator reconciliation') }
@@ -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,11 +25,15 @@ 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};
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;}
33
+ if(arg==='--host-command-pin'){
34
+ const value=String(argv[++index]??'');
35
+ out.hostCommandPins.push(value);continue;
36
+ }
32
37
  if(arg==='--marker-reconciliation-receipt'){
33
38
  if(out.markerReceiptPath)throw new Error('duplicate --marker-reconciliation-receipt');
34
39
  const value=argv[++index];if(!value)throw new Error('--marker-reconciliation-receipt requires a path');
@@ -70,6 +75,8 @@ function parseDeployArgs(argv){
70
75
  const uacFields=[out.migrationNonce,out.migrationIssuedAt,out.migrationExpiresAt,out.uacApprovalPath,out.uacApprovalSha256];
71
76
  if(out.uacLegacyMigration&&uacFields.some(value=>!value))throw new Error('UAC migration requires nonce, lifetime, and approval receipt');
72
77
  if(!out.uacLegacyMigration&&uacFields.some(value=>value))throw new Error('UAC migration arguments require --uac-legacy-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');
73
80
  return out;
74
81
  }
75
82
  const deployArgs=parseDeployArgs(process.argv.slice(2));
@@ -294,6 +301,7 @@ if(deployArgs.uacLegacyMigration){
294
301
  predecessorPolicySha256:markerReceipt.predecessor_policy_sha256,
295
302
  preflightSubjectSha256:preflight.subject_sha256,nodeSha256:await sha(process.execPath),
296
303
  deployScriptSha256:await sha(fileURLToPath(import.meta.url)),
304
+ hostCommandPins:deployArgs.hostCommandPins,
297
305
  nonce:deployArgs.migrationNonce,issuedAt:deployArgs.migrationIssuedAt,
298
306
  expiresAt:deployArgs.migrationExpiresAt
299
307
  });
@@ -313,6 +321,7 @@ const args=[
313
321
  '--bridge-root',bridgeRoot,'--bridge-script-sha256',await sha(bridgeScript),
314
322
  '--enable-machine-fs','--enable-observation','--enable-desktop','--enable-autonomous-supervisor','--recover-offline-predecessor'
315
323
  ];
324
+ args.push(...hostCommandPinArgs(deployArgs.hostCommandPins));
316
325
  let result=null,verification=null,activationOutcomePath=null;
317
326
  let continuityEngine=null,continuityUpdateId=null;
318
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.59'||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.59', train:'R153',
10
- sourceVersion:'0.15.58',
11
- sourceSha256:'4bc53cfa6b3890b5749066cb507e8773b36d6c255192a8973dadb8546a39822e',
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,6 +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
+ [string]$HostCommandPinsJson = '[]',
10
11
  [switch]$Elevated
11
12
  )
12
13
  $ErrorActionPreference = 'Stop'
@@ -20,6 +21,19 @@ function Require-AbsoluteFile([string]$Value,[string]$Label) {
20
21
  return $item.FullName
21
22
  }
22
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)
23
37
  $NodePath = Require-AbsoluteFile $NodePath 'Node executable'
24
38
  $DeployScript = Require-AbsoluteFile $DeployScript 'Deploy script'
25
39
  $MarkerReceiptPath = Require-AbsoluteFile $MarkerReceiptPath 'Marker reconciliation receipt'
@@ -99,6 +113,7 @@ if (-not $Elevated) {
99
113
  # -Verb RunAs is the UseShellExecute parameter set. PowerShell 5.1 rejects
100
114
  # -RedirectStandardOutput/-RedirectStandardError in that same Start-Process.
101
115
  # The elevated child writes bounded, no-clobber evidence instead.
116
+ if ($HostCommandPinsJson -cne '[]') { $arguments += @('-HostCommandPinsJson',('"' + $HostCommandPinsJson.Replace('"','\"') + '"')) }
102
117
  $child = Start-Process -FilePath $powershell -ArgumentList $arguments -Verb RunAs -Wait -PassThru -WindowStyle Hidden
103
118
  if (Test-Path -LiteralPath $StdoutPath) { [Console]::Out.Write([IO.File]::ReadAllText($StdoutPath)) }
104
119
  if (Test-Path -LiteralPath $StderrPath) { [Console]::Error.Write([IO.File]::ReadAllText($StderrPath)) }
@@ -121,7 +136,7 @@ try {
121
136
  try {
122
137
  $issuerOutput = @(& $NodePath $issuerScript '--state-root' $env:DEADBYTE_STATE_ROOT `
123
138
  '--marker-receipt' $MarkerReceiptPath '--marker-receipt-sha256' $MarkerReceiptSha256 `
124
- '--preflight-subject-sha256' $PreflightSubjectSha256 2>&1)
139
+ '--preflight-subject-sha256' $PreflightSubjectSha256 '--host-command-pins-json' $HostCommandPinsJson 2>&1)
125
140
  $issuerExit = $LASTEXITCODE
126
141
  } finally { $ErrorActionPreference = $priorErrorAction }
127
142
  $issuerText = (@($issuerOutput) | ForEach-Object { [string]$_ }) -join "`n"
@@ -135,14 +150,16 @@ try {
135
150
  $priorErrorAction = $ErrorActionPreference
136
151
  $ErrorActionPreference = 'Continue'
137
152
  try {
138
- $deployOutput = @(& $NodePath $DeployScript '--uac-legacy-migration' `
139
- '--marker-reconciliation-receipt' $MarkerReceiptPath `
140
- '--marker-reconciliation-receipt-sha256' $MarkerReceiptSha256 `
141
- '--migration-nonce' $issued.nonce `
142
- '--migration-issued-at' $issued.issued_at_utc `
143
- '--migration-expires-at' $issued.expires_at_utc `
144
- '--uac-approval-receipt' $issued.receipt_path `
145
- '--uac-approval-receipt-sha256' $issued.receipt_sha256 2>&1)
153
+ $deployArgs = @('--uac-legacy-migration',
154
+ '--marker-reconciliation-receipt',$MarkerReceiptPath,
155
+ '--marker-reconciliation-receipt-sha256',$MarkerReceiptSha256,
156
+ '--migration-nonce',$issued.nonce,
157
+ '--migration-issued-at',$issued.issued_at_utc,
158
+ '--migration-expires-at',$issued.expires_at_utc,
159
+ '--uac-approval-receipt',$issued.receipt_path,
160
+ '--uac-approval-receipt-sha256',$issued.receipt_sha256)
161
+ foreach ($pin in $HostCommandPins) { $deployArgs += @('--host-command-pin',[string]$pin) }
162
+ $deployOutput = @(& $NodePath $DeployScript @deployArgs 2>&1)
146
163
  $deployExit = $LASTEXITCODE
147
164
  } finally { $ErrorActionPreference = $priorErrorAction }
148
165
  $deployText = (@($deployOutput) | ForEach-Object { [string]$_ }) -join "`n"
@@ -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');
@@ -85,6 +85,41 @@ async function isSafeTerminalActivation(outcome,lock,stateRoot){
85
85
  return true;
86
86
  }
87
87
 
88
+ async function isSafeSignedPreconditionReject(outcome,lock,stateRoot){
89
+ if(!outcomeMatchesLock(outcome,lock)||outcome?.outcome?.state!=='ACTIVATION_OUTCOME_UNKNOWN'||
90
+ outcome.update_id!==null||outcome.state_path!==null||outcome.continuity_update_id!==null||
91
+ typeof outcome.candidate_version!=='string'||!HEX64.test(String(outcome.candidate_manifest_sha256??''))||
92
+ typeof outcome.predecessor_release_id!=='string')return false;
93
+ const lockTime=Date.parse(lock.created_at_utc),observedTime=Date.parse(outcome.observed_at_utc);
94
+ if(!Number.isFinite(observedTime)||observedTime<lockTime||observedTime-lockTime>120000)return false;
95
+ const updatesDir=path.join(path.resolve(stateRoot),'evidence','updates');
96
+ let current,publicKey,files;
97
+ try{
98
+ current=JSON.parse(await readFile(path.join(stateRoot,'active-release.json'),'utf8'));
99
+ publicKey=createPublicKey(await readFile(path.join(stateRoot,'trust','ed25519-public.pem')));
100
+ files=await readdir(updatesDir,{withFileTypes:true});
101
+ }catch{return false;}
102
+ if(publicKey.asymmetricKeyType!=='ed25519'||current?.release_id!==outcome.predecessor_release_id)return false;
103
+ const prefix='r27-'+outcome.candidate_manifest_sha256.slice(0,16)+'-';
104
+ const candidates=files.filter(f=>f.name.startsWith(prefix)&&/^r27-[a-f0-9]{16}-[a-z0-9-]+$/.test(f.name));
105
+ if(candidates.length!==1||!candidates[0].isDirectory()||candidates[0].isSymbolicLink())return false;
106
+ let signed;
107
+ try{signed=JSON.parse(await readFile(path.join(updatesDir,candidates[0].name,'failure-receipt.json'),'utf8'));}
108
+ catch{return false;}
109
+ if(!verifySlotUpdateEnvelope(signed,publicKey))return false;
110
+ const body=signed.body,failedTime=Date.parse(body?.failed_at_utc??'');
111
+ if(body?.schema!=='deadbyte.r27-slot-update-failure.v1'||
112
+ body.update_id!==candidates[0].name||body.release_version!==outcome.candidate_version||
113
+ body.candidate_manifest_sha256!==outcome.candidate_manifest_sha256||
114
+ body.result!=='PRECONDITION_REJECTED'||body.phase!=='initial'||
115
+ body.attempted_pointer!==null||body.rollback?.attempted!==false||
116
+ !Array.isArray(body.rollback?.errors)||body.rollback.errors.length!==0||
117
+ !/^host command 'powershell_script' executable pin drift detected;/.test(String(body.error?.message??''))||
118
+ !Number.isFinite(failedTime)||failedTime<lockTime||failedTime>observedTime||
119
+ !samePointerIdentity(body.prior_pointer,current))return false;
120
+ return true;
121
+ }
122
+
88
123
  async function findTerminalOutcome(updatesDir,lock,stateRoot){
89
124
  const names=(await readdir(updatesDir)).filter(name=>/^r65-activation-outcome-.*\.json$/.test(name)).sort();
90
125
  if(names.length>MAX_OUTCOME_SCAN)throw new Error('activation outcome inventory exceeds safe scan budget');
@@ -95,7 +130,8 @@ async function findTerminalOutcome(updatesDir,lock,stateRoot){
95
130
  catch{continue;}
96
131
  if(value?.claim_operation_id!==lock.operation_id)continue;
97
132
  if(value?.preflight_subject_sha256!==lock.subject_sha256){foreign=true;continue;}
98
- if(isSafePreMutationReject(value,lock)||await isSafeTerminalActivation(value,lock,stateRoot))matched=true;
133
+ if(isSafePreMutationReject(value,lock)||await isSafeTerminalActivation(value,lock,stateRoot)||
134
+ await isSafeSignedPreconditionReject(value,lock,stateRoot))matched=true;
99
135
  else unknown=true;
100
136
  }
101
137
  return {matched,foreign,unknown};
@@ -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.59'||
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}={}){
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,
@@ -181,7 +182,8 @@ async function synchronizeForegroundRuntime(ctx,{displayMode='unicode',migration
181
182
  '-ExpectedTarballSha256',artifactSha256,'-ExpectedManifestSha256',packagedManifestSha256,
182
183
  '-ExpectedBootstrapSha256',bootstrapMatch[1],
183
184
  '-StdoutPath',stdoutPath,'-StderrPath',stderrPath,
184
- '-MarkerReceiptPath',migrationContext.receipt_path,'-MarkerReceiptSha256',migrationContext.receipt_sha256],{
185
+ '-MarkerReceiptPath',migrationContext.receipt_path,'-MarkerReceiptSha256',migrationContext.receipt_sha256,
186
+ ...(hostCommandPins.length?['-HostCommandPinsJson',encodeHostCommandPins(hostCommandPins)]:[])],{
185
187
  cwd:CURRENT_PACKAGE_ROOT,env:windowsPowerShellEnvironment(process.env),encoding:'utf8',windowsHide:true,shell:false,
186
188
  timeout:REMOTE_UPDATE_TIMEOUT_MS,maxBuffer:16*1024*1024
187
189
  });
@@ -743,7 +745,7 @@ async function releaseRemoteSession(ctx,marker,{displayMode='unicode'}={}){
743
745
  if(deletePinnedRemoteMarker(remoteSessionMarker(ctx.root),snapshot.sha256)!=='REMOVED')
744
746
  throw new Error('remote session ownership marker replaced before native deletion; marker retained');
745
747
  }
746
- async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,filterExpression='',mode='default'}={}){
748
+ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,filterExpression='',mode='default',hostCommandPins=[]}={}){
747
749
  const display=prepareRemoteConsole();
748
750
  const displayMode=display.mode;
749
751
  const ctx=await remoteContext();
@@ -755,7 +757,7 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
755
757
  startupLock.assertHeld();
756
758
  const migrationContext=await recoverRemoteSession(ctx,{displayMode});
757
759
  startupLock.assertHeld();
758
- await synchronizeForegroundRuntime(ctx,{displayMode,migrationContext});
760
+ await synchronizeForegroundRuntime(ctx,{displayMode,migrationContext,hostCommandPins});
759
761
  startupLock.assertHeld();
760
762
  marker=await claimRemoteSession(ctx);
761
763
  startupLock.assertHeld();
@@ -869,11 +871,16 @@ async function runRemoteSession({resumeCursor=null,verbose=false,trace=false,fil
869
871
  }
870
872
  async function runRemote(args){
871
873
  let verbose=false,trace=false,filterExpression='',resumeCursor=null,mode='default';
874
+ const hostCommandPinValues=[];
872
875
  const positional=[];
873
876
  for(let index=0;index<args.length;index++){
874
877
  const arg=args[index];
875
878
  if(arg==='--verbose'||arg==='-v'){verbose=true;continue;}
876
879
  if(arg==='--trace'){trace=true;continue;}
880
+ if(arg==='--host-command-pin'){
881
+ const value=String(args[++index]??'');
882
+ hostCommandPinValues.push(value);continue;
883
+ }
877
884
  if(['--operator','--desktop','--host','--full'].includes(arg)){
878
885
  if(mode!=='default') throw new Error('remote authority modes are mutually exclusive');
879
886
  mode=arg.slice(2);continue;
@@ -890,8 +897,11 @@ async function runRemote(args){
890
897
  }
891
898
  positional.push(arg);
892
899
  }
893
- const presentation=verbose||trace||Boolean(filterExpression)||resumeCursor!==null||mode!=='default';
894
- if(positional.length===0){await runRemoteSession({resumeCursor,verbose,trace,filterExpression,mode});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;}
895
905
  if(presentation) throw new Error('remote presentation flags are valid only for foreground session');
896
906
  if(positional.length>1) throw new Error('remote accepts one controller command');
897
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.59')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.59';
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.59');
17
- assert.equal(release.train,'R153');
18
- assert.equal(matrix.train,'R153');
19
- assert.equal(release.source_predecessor.version,'0.15.58');
20
- assert.match(release.source_predecessor.source_path,/0\.15\.58/);
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,'4bc53cfa6b3890b5749066cb507e8773b36d6c255192a8973dadb8546a39822e');
23
- assert.equal(provenance.source_predecessor_version,'0.15.58');
24
- assert.equal(provenance.source_predecessor_manifest_file_sha256,'ec621a8a16f264f8824aa4644c914aca8f6ce57cb3578f5bce8acf2ac77fcc18');
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.58');
21
- assert.match(release.source_predecessor.source_path,/0\.15\.58/);
22
- assert.equal(prov.source_predecessor_version,'0.15.58');
23
- assert.equal(prov.source_predecessor_manifest_file_sha256,'ec621a8a16f264f8824aa4644c914aca8f6ce57cb3578f5bce8acf2ac77fcc18');
24
- assert.equal(prov.parent_tarball_sha256,'4bc53cfa6b3890b5749066cb507e8773b36d6c255192a8973dadb8546a39822e');
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.59',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.59');
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/);
@@ -0,0 +1,30 @@
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
+ import {fileURLToPath} from 'node:url';
6
+ const root=new URL('../',import.meta.url);
7
+ const source=async path=>readFile(new URL(path,root),'utf8');
8
+
9
+ test('R60 explicit host-command pin is owner supplied and forwarded end to end',async()=>{
10
+ const [cli,bootstrap,helper,deploy]=await Promise.all([
11
+ source('src/deadbyte-cli.mjs'),source('scripts/bootstrap-uac-migration-r153.ps1'),
12
+ source('scripts/request-uac-migration-r153.ps1'),source('scripts/deploy-r48.mjs')]);
13
+ assert.match(cli,/arg===['"]--host-command-pin['"]/, 'remote CLI must parse a bounded owner-supplied pin');
14
+ assert.match(cli,/hostCommandPin/, 'remote must preserve the parsed pin');
15
+ assert.match(bootstrap,/HostCommandPin/, 'protected UAC bootstrap must receive the owner pin');
16
+ assert.match(helper,/HostCommandPin/, 'protected helper must carry the pin without shell injection');
17
+ assert.match(deploy,/hostCommandPin/, 'deploy must pass pin to R27 before activation');
18
+ assert.match(deploy,/explicit host repin requires the elevated UAC migration/);
19
+ });
20
+
21
+ test('R60 malformed, unrelated, and duplicate pins fail before remote context or activation',()=>{
22
+ const cli=fileURLToPath(new URL('../src/deadbyte-cli.mjs',import.meta.url));
23
+ const cases=[['powershell_script=notahash'],['other_command='+'a'.repeat(64)],
24
+ ['powershell_script='+'a'.repeat(64),'--host-command-pin','powershell_script='+'b'.repeat(64)]];
25
+ for(const args of cases){
26
+ const p=spawnSync(process.execPath,[cli,'remote','--host-command-pin',...args],{encoding:'utf8',timeout:8000});
27
+ assert.equal(p.status,1,JSON.stringify(args));
28
+ assert.match(p.stderr,/remote --host-command-pin/);
29
+ }
30
+ });