deadbyte-mcp 0.15.47 → 0.15.48

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.
Files changed (79) hide show
  1. package/MANIFEST.SHA256 +78 -41
  2. package/PROVENANCE.json +22 -11
  3. package/README.txt +11 -11
  4. package/RELEASE.json +378 -368
  5. package/TEST-MATRIX.json +1 -1
  6. package/bin/deadbyte-epoch-lock-r85.node +0 -0
  7. package/controller/deadbyte-process-policy.json +4 -4
  8. package/docs/R75-RECOVERY-HANDOFF.md +16 -0
  9. package/docs/R85-EPOCH-LOCK-BINARY.json +13 -0
  10. package/native/deadbyte-epoch-lock-r85.cpp +241 -0
  11. package/package.json +1 -1
  12. package/scripts/build-epoch-lock-r85.ps1 +34 -0
  13. package/scripts/deploy-r48.mjs +3 -1
  14. package/scripts/npm-release-r48.mjs +22 -4
  15. package/scripts/npm-release-readiness-r108.mjs +134 -0
  16. package/scripts/r75-npm-bin-integrity-gate.mjs +33 -0
  17. package/scripts/release-manifest.mjs +1 -1
  18. package/scripts/verify-r70-full-regression.ps1 +12 -2
  19. package/src/activation-authority-proof-r75.mjs +67 -0
  20. package/src/activation-continuity-engine-r65.mjs +17 -4
  21. package/src/activation-preflight-r65.mjs +13 -0
  22. package/src/authority-continuity-r65.mjs +265 -21
  23. package/src/deadbyte-cli.mjs +55 -18
  24. package/src/epoch-lock-r85.mjs +48 -0
  25. package/src/foreground-marker-cas-win32.mjs +284 -0
  26. package/src/foreground-process-identity.mjs +3 -3
  27. package/src/privileged-install-trust-r89.mjs +196 -0
  28. package/src/remote-session-guardian.mjs +9 -7
  29. package/src/runtime-update.mjs +5 -22
  30. package/src/version.mjs +1 -1
  31. package/src/windows-powershell-environment.mjs +24 -0
  32. package/test/cli-entrypoint.test.mjs +1 -1
  33. package/test/helpers/r75-authority-proof.mjs +9 -0
  34. package/test/r100-posix-owned-lock-race.test.mjs +72 -0
  35. package/test/r101-acl-probe-environment.test.mjs +51 -0
  36. package/test/r106-trusted-powershell-spawn.test.mjs +53 -0
  37. package/test/r107-updater-powershell-path.test.mjs +77 -0
  38. package/test/r33-closeout-regression.test.mjs +2 -1
  39. package/test/r54-offline-predecessor-self-exclusion.test.mjs +2 -2
  40. package/test/r55-offline-predecessor-guardian-family.test.mjs +2 -2
  41. package/test/r56-rollback-launcher-restore.test.mjs +2 -2
  42. package/test/r57-core-startup-race.test.mjs +2 -2
  43. package/test/r58-concurrent-recovery.test.mjs +10 -7
  44. package/test/r58-stale-owner.test.mjs +5 -6
  45. package/test/r59-tool-surface-compat.test.mjs +1 -1
  46. package/test/r65-activation-orchestration.test.mjs +2 -1
  47. package/test/r65-activation-preflight.test.mjs +7 -6
  48. package/test/r65-authority-epoch-recovery.test.mjs +3 -1
  49. package/test/r65-authority-restored-safe.test.mjs +2 -1
  50. package/test/r65-deploy-preflight.test.mjs +13 -14
  51. package/test/r65-epoch-native-activation-integration.test.mjs +5 -4
  52. package/test/r65-legacy-authority-migration.test.mjs +2 -1
  53. package/test/r67-npm-cross-volume.test.mjs +9 -2
  54. package/test/r71-full-verifier-artifact.test.mjs +21 -15
  55. package/test/r71-isolated-slot-rollback-restore.test.mjs +3 -1
  56. package/test/r71-release-identity.test.mjs +3 -1
  57. package/test/r74-kernel-lock.test.mjs +12 -6
  58. package/test/r74-prepublish-contract.test.mjs +1 -1
  59. package/test/r75-activation-authority-proof.test.mjs +46 -0
  60. package/test/r75-authority-evidence.test.mjs +77 -0
  61. package/test/r76-pinned-marker-cas.test.mjs +78 -0
  62. package/test/r77-epoch-grant-cas.test.mjs +51 -0
  63. package/test/r78-parent-junction-cas.test.mjs +62 -0
  64. package/test/r79-orphan-revocation-reconcile.test.mjs +88 -0
  65. package/test/r79-windows-fsync-regression.test.mjs +45 -0
  66. package/test/r80-durable-epoch-commit.test.mjs +76 -0
  67. package/test/r82-epoch-lock-owned-cas.test.mjs +93 -0
  68. package/test/r83-epoch-lock-fileid-cas.test.mjs +92 -0
  69. package/test/r84-epoch-lock-created-fileid.test.mjs +112 -0
  70. package/test/r85-epoch-lock-release-lifetime.test.mjs +25 -0
  71. package/test/r85-epoch-repeat-append.test.mjs +30 -0
  72. package/test/r86-epoch-cleanup-and-loader.test.mjs +126 -0
  73. package/test/r87-crashed-orphan-revoke-reconcile.test.mjs +110 -0
  74. package/test/r88-native-disposition-failure.test.mjs +35 -0
  75. package/test/r89-privileged-install-boundary.test.mjs +123 -0
  76. package/test/r92-acl-ace-applicability.test.mjs +45 -0
  77. package/test/r94-win32-propagation-enum.test.mjs +51 -0
  78. package/test/r99-privileged-import-closure.test.mjs +45 -0
  79. package/test/remote-session-cli.test.mjs +69 -9
@@ -0,0 +1,48 @@
1
+ import {createHash, randomBytes} from 'node:crypto';
2
+ import {lstat,readFile} from 'node:fs/promises';
3
+ import {createRequire} from 'node:module';
4
+ import path from 'node:path';
5
+ import {fileURLToPath} from 'node:url';
6
+ import {inspectTrustedInstallation,privilegedEpochInstallFiles} from './privileged-install-trust-r89.mjs';
7
+
8
+ const root=path.dirname(path.dirname(fileURLToPath(import.meta.url)));
9
+ const binary=path.join(root,'bin','deadbyte-epoch-lock-r85.node');
10
+ const metadata=path.join(root,'docs','R85-EPOCH-LOCK-BINARY.json');
11
+ const require=createRequire(import.meta.url);
12
+ const hex64=/^[0-9a-f]{64}$/;
13
+ let addon=null;
14
+
15
+ async function loadVerifiedAddon(){
16
+ if(process.platform!=='win32'||process.arch!=='x64')throw new Error('epoch held-handle lock requires Windows x64');
17
+ await inspectTrustedInstallation(privilegedEpochInstallFiles(root,process.execPath));
18
+ const meta=JSON.parse(await readFile(metadata,'utf8'));
19
+ if(meta.schema!=='deadbyte.r85-epoch-lock-binary.v1'||meta.platform!=='win32-x64'||
20
+ meta.addon!=='bin/deadbyte-epoch-lock-r85.node'||meta.source!=='native/deadbyte-epoch-lock-r85.cpp'||
21
+ meta.node_api_version>Number(process.versions.napi)||!hex64.test(meta.addon_sha256)||!hex64.test(meta.source_sha256))
22
+ throw new Error('epoch held-handle binary metadata invalid');
23
+ const [st,bytes,sourceBytes]=await Promise.all([lstat(binary),readFile(binary),readFile(path.join(root,meta.source))]);
24
+ if(!st.isFile()||st.isSymbolicLink()||
25
+ createHash('sha256').update(bytes).digest('hex')!==meta.addon_sha256||
26
+ createHash('sha256').update(sourceBytes).digest('hex')!==meta.source_sha256)
27
+ throw new Error('epoch held-handle binary/source hash mismatch');
28
+ addon??=require(binary);
29
+ if(typeof addon.acquire!=='function'||typeof addon.release!=='function')
30
+ throw new Error('epoch held-handle exports invalid');
31
+ return addon;
32
+ }
33
+
34
+ export async function acquireHeldEpochLock(file,bytes){
35
+ if(!Buffer.isBuffer(bytes)||bytes.length<2||bytes.length>16384||typeof file!=='string'||!path.isAbsolute(file))
36
+ throw new Error('epoch held-handle lock arguments invalid');
37
+ const native=await loadVerifiedAddon();
38
+ const handle=native.acquire(file,bytes,randomBytes(16).toString('hex'));
39
+ if(handle===null)return null;
40
+ let released=false;
41
+ return Object.freeze({
42
+ async release(){
43
+ if(released)throw new Error('epoch held-handle lock already released');
44
+ native.release(handle);
45
+ released=true;
46
+ }
47
+ });
48
+ }
@@ -0,0 +1,284 @@
1
+ // Windows-only, handle-bound conditional deletion. Never use pathname rm after a read.
2
+ import {createHash} from 'node:crypto';
3
+ import {lstat,readFile} from 'node:fs/promises';
4
+ import {spawnSync} from 'node:child_process';
5
+ import path from 'node:path';
6
+ import {windowsPowerShellEnvironment} from './windows-powershell-environment.mjs';
7
+
8
+ const HEX64=/^[0-9a-f]{64}$/;
9
+ const digest=bytes=>createHash('sha256').update(bytes).digest('hex');
10
+ const sameFile=(a,b)=>a&&b&&a.isFile()&&b.isFile()&&!a.isSymbolicLink()&&!b.isSymbolicLink()&&
11
+ a.dev===b.dev&&a.ino===b.ino&&a.size===b.size&&a.mtimeMs===b.mtimeMs&&a.ctimeMs===b.ctimeMs;
12
+
13
+ export async function readRemoteMarkerSnapshot(file){
14
+ let before;
15
+ try{before=await lstat(file);}catch(error){if(error?.code==='ENOENT')return null;throw error;}
16
+ if(!before.isFile()||before.isSymbolicLink()||before.size>16384||before.size<2)
17
+ throw new Error('remote marker not a bounded regular file');
18
+ const bytes=await readFile(file);
19
+ const after=await lstat(file);
20
+ if(!sameFile(before,after)||bytes.length!==before.size)
21
+ throw new Error('remote marker identity changed during snapshot');
22
+ let marker;
23
+ try{marker=JSON.parse(bytes.toString('utf8').replace(/^\uFEFF/,''));}
24
+ catch{throw new Error('remote marker JSON invalid');}
25
+ return Object.freeze({marker,sha256:digest(bytes)});
26
+ }
27
+
28
+ // Native ancestor handles pin the non-reparse path chain, then a single leaf handle owns read/hash/deletion.
29
+ // Open share=READ only: concurrent replace, rename, delete and writes must fail.
30
+ // Do not replace this with a Node readFile()+rm(), rename(), or link()+unlink().
31
+ const nativeCommand=String.raw`
32
+ $ErrorActionPreference='Stop'
33
+ $source=@'
34
+ using System;
35
+ using System.IO;
36
+ using System.ComponentModel;
37
+ using System.Runtime.InteropServices;
38
+ using System.Security.Cryptography;
39
+ using Microsoft.Win32.SafeHandles;
40
+ using System.Collections.Generic;
41
+ public static class DeadbytePinnedDeleteV2 {
42
+ const uint FILE_READ_DATA = 0x0001;
43
+ const uint FILE_WRITE_DATA = 0x0002;
44
+ const uint FILE_READ_ATTRIBUTES = 0x0080;
45
+ const uint DELETE = 0x00010000;
46
+ const uint FILE_SHARE_READ = 0x00000001;
47
+ const uint OPEN_EXISTING = 3;
48
+ const uint CREATE_NEW = 1;
49
+ const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
50
+ const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
51
+ const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
52
+ const uint FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400;
53
+ [StructLayout(LayoutKind.Sequential)] struct FILE_ATTRIBUTE_TAG_INFO {
54
+ public uint FileAttributes;
55
+ public uint ReparseTag;
56
+ }
57
+ // FileIdInfo = 18, FILE_ID_INFO = 8-byte volume serial + 16-byte file ID.
58
+ // No fallback to filename, hashes, inode heuristics, or legacy 64-bit file index.
59
+ [StructLayout(LayoutKind.Sequential)] struct FILE_ID_INFO {
60
+ public ulong VolumeSerialNumber;
61
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst=16)] public byte[] FileId;
62
+ }
63
+ [DllImport("kernel32.dll",EntryPoint="CreateFileW",CharSet=CharSet.Unicode,SetLastError=true)]
64
+ static extern SafeFileHandle CreateFile(string file,uint access,uint share,IntPtr security,uint creation,uint flags,IntPtr templateFile);
65
+ [DllImport("kernel32.dll",SetLastError=true)]
66
+ static extern bool GetFileInformationByHandleEx(SafeFileHandle file,int cls,out FILE_ATTRIBUTE_TAG_INFO info,uint size);
67
+ [DllImport("kernel32.dll",EntryPoint="GetFileInformationByHandleEx",SetLastError=true)]
68
+ static extern bool GetFileIdInformationByHandleEx(SafeFileHandle file,int cls,out FILE_ID_INFO info,uint size);
69
+ [DllImport("kernel32.dll",SetLastError=true)]
70
+ static extern bool SetFileInformationByHandle(SafeFileHandle file,int cls,IntPtr info,uint size);
71
+ // Pin the ENTIRE local-drive ancestor chain from the drive root to the leaf's parent.
72
+ // Share=READ only denies concurrent write/rename/delete of each pinned component.
73
+ // Checking names separately and closing each handle would reintroduce the race.
74
+ static void PinAncestor(string directory,List<SafeFileHandle> pins) {
75
+ var handle=CreateFile(directory,FILE_READ_ATTRIBUTES,FILE_SHARE_READ,
76
+ IntPtr.Zero,OPEN_EXISTING,FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OPEN_REPARSE_POINT,IntPtr.Zero);
77
+ if(handle.IsInvalid){
78
+ int error=Marshal.GetLastWin32Error();handle.Dispose();
79
+ throw new Win32Exception(error,"cannot pin path ancestor (fail closed)");
80
+ }
81
+ try {
82
+ FILE_ATTRIBUTE_TAG_INFO attr;
83
+ if(!GetFileInformationByHandleEx(handle,9,out attr,(uint)Marshal.SizeOf(typeof(FILE_ATTRIBUTE_TAG_INFO))))
84
+ throw new Win32Exception(Marshal.GetLastWin32Error(),"ancestor attributes unavailable");
85
+ if((attr.FileAttributes&FILE_ATTRIBUTE_DIRECTORY)==0 ||
86
+ (attr.FileAttributes&FILE_ATTRIBUTE_REPARSE_POINT)!=0)
87
+ throw new InvalidOperationException("ancestor is a junction, symlink, reparse point or non-directory");
88
+ pins.Add(handle);
89
+ } catch {handle.Dispose();throw;}
90
+ }
91
+ static string FileIdentity(SafeFileHandle handle) {
92
+ FILE_ID_INFO info;
93
+ if(!GetFileIdInformationByHandleEx(handle,18,out info,(uint)Marshal.SizeOf(typeof(FILE_ID_INFO))))
94
+ throw new Win32Exception(Marshal.GetLastWin32Error(),"128-bit native File ID unavailable; fail closed");
95
+ if(info.FileId==null||info.FileId.Length!=16)
96
+ throw new InvalidOperationException("native file identity unavailable");
97
+ bool allZero=true;
98
+ foreach(byte b in info.FileId)if(b!=0){allZero=false;break;}
99
+ if(allZero)throw new InvalidOperationException("zero native File ID; fail closed");
100
+ return info.VolumeSerialNumber.ToString("x16")+":"+BitConverter.ToString(info.FileId).Replace("-","").ToLowerInvariant();
101
+ }
102
+ public static string DeleteIfHash(string file,string expectedSha,string expectedId) {
103
+ return WithPinnedAncestors(file,"delete",expectedSha,expectedId,null);
104
+ }
105
+ public static string Snapshot(string file) {
106
+ return WithPinnedAncestors(file,"snapshot",null,null,null);
107
+ }
108
+ public static string CreateLock(string file,string encodedBytes) {
109
+ return WithPinnedAncestors(file,"create",null,null,encodedBytes);
110
+ }
111
+ static string WithPinnedAncestors(string file,string operation,string expectedSha,string expectedId,string encodedBytes) {
112
+ string full=Path.GetFullPath(file);
113
+ // No UNC paths, device paths, ADS or extended-path aliases in this safety boundary.
114
+ if(full.Length<4 || !Char.IsLetter(full[0]) || full[1]!=':' || full[2]!='\\' ||
115
+ full.IndexOf(':',2)>=0 || String.IsNullOrEmpty(Path.GetFileName(full)))
116
+ throw new InvalidOperationException("pinned deletion requires an ordinary local drive file path");
117
+ string root=Path.GetPathRoot(full);
118
+ string parent=Path.GetDirectoryName(full);
119
+ var pins=new List<SafeFileHandle>();
120
+ try {
121
+ string current=root;
122
+ PinAncestor(current,pins);
123
+ string tail=parent.Substring(root.Length);
124
+ foreach(string component in tail.Split(new[]{'\\'},StringSplitOptions.RemoveEmptyEntries)){
125
+ current=Path.Combine(current,component);
126
+ PinAncestor(current,pins);
127
+ }
128
+ if(operation=="snapshot")return SnapshotPinnedLeaf(full);
129
+ if(operation=="create")return CreatePinnedLeaf(full,encodedBytes);
130
+ return DeletePinnedLeaf(full,expectedSha,expectedId);
131
+ } finally {
132
+ for(int i=pins.Count-1;i>=0;i--)pins[i].Dispose();
133
+ }
134
+ }
135
+ static byte[] ReadBoundedLeaf(SafeFileHandle handle){
136
+ using(var stream=new FileStream(handle,FileAccess.Read)){
137
+ if(stream.Length<2||stream.Length>16384)throw new InvalidOperationException("pinned file size invalid");
138
+ byte[] content=new byte[(int)stream.Length];int index=0,read;
139
+ while(index<content.Length&&(read=stream.Read(content,index,content.Length-index))>0)index+=read;
140
+ if(index!=content.Length)throw new IOException("pinned file truncated");
141
+ return content;
142
+ }
143
+ }
144
+ static void ValidateLeaf(SafeFileHandle handle){
145
+ FILE_ATTRIBUTE_TAG_INFO attr;
146
+ if(!GetFileInformationByHandleEx(handle,9,out attr,(uint)Marshal.SizeOf(typeof(FILE_ATTRIBUTE_TAG_INFO))))
147
+ throw new Win32Exception(Marshal.GetLastWin32Error(),"pinned leaf attributes unavailable");
148
+ if((attr.FileAttributes&(FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_REPARSE_POINT))!=0)
149
+ throw new InvalidOperationException("pinned leaf is a reparse point or directory");
150
+ }
151
+ static string SnapshotPinnedLeaf(string file){
152
+ using(var handle=CreateFile(file,FILE_READ_DATA|FILE_READ_ATTRIBUTES,FILE_SHARE_READ,IntPtr.Zero,OPEN_EXISTING,FILE_FLAG_OPEN_REPARSE_POINT,IntPtr.Zero)){
153
+ if(handle.IsInvalid){
154
+ int error=Marshal.GetLastWin32Error();
155
+ if(error==2||error==3)return "MISSING";
156
+ throw new Win32Exception(error,"epoch lock could not be pinned for identity snapshot");
157
+ }
158
+ ValidateLeaf(handle);
159
+ string id=FileIdentity(handle);
160
+ byte[] bytes=ReadBoundedLeaf(handle);
161
+ string hash=BitConverter.ToString(SHA256.Create().ComputeHash(bytes)).Replace("-","").ToLowerInvariant();
162
+ return "SNAPSHOT|"+id+"|"+hash+"|"+Convert.ToBase64String(bytes);
163
+ }
164
+ }
165
+ static string CreatePinnedLeaf(string file,string encodedBytes){
166
+ byte[] bytes=Convert.FromBase64String(encodedBytes);
167
+ if(bytes.Length<2||bytes.Length>16384)throw new InvalidOperationException("epoch lock bytes invalid");
168
+ using(var handle=CreateFile(file,FILE_READ_DATA|FILE_WRITE_DATA|FILE_READ_ATTRIBUTES,
169
+ FILE_SHARE_READ,IntPtr.Zero,CREATE_NEW,FILE_FLAG_OPEN_REPARSE_POINT,IntPtr.Zero)){
170
+ if(handle.IsInvalid){
171
+ int error=Marshal.GetLastWin32Error();
172
+ if(error==80||error==183)return "EXISTS";
173
+ throw new Win32Exception(error,"epoch lock creation failed closed");
174
+ }
175
+ ValidateLeaf(handle);
176
+ string id=FileIdentity(handle);
177
+ using(var stream=new FileStream(handle,FileAccess.ReadWrite)){
178
+ stream.Write(bytes,0,bytes.Length);
179
+ stream.Flush(true);
180
+ }
181
+ string hash=BitConverter.ToString(SHA256.Create().ComputeHash(bytes)).Replace("-","").ToLowerInvariant();
182
+ return "CREATED|"+id+"|"+hash;
183
+ }
184
+ }
185
+ static string DeletePinnedLeaf(string file,string expectedSha,string expectedId) {
186
+ using(var handle=CreateFile(file,FILE_READ_DATA|FILE_READ_ATTRIBUTES|DELETE,FILE_SHARE_READ,IntPtr.Zero,OPEN_EXISTING,FILE_FLAG_OPEN_REPARSE_POINT,IntPtr.Zero)) {
187
+ if(handle.IsInvalid){
188
+ int error=Marshal.GetLastWin32Error();
189
+ if(error==2||error==3)return "MISSING";
190
+ throw new Win32Exception(error,"remote marker cannot be exclusively pinned");
191
+ }
192
+ ValidateLeaf(handle);
193
+ // PowerShell can marshal $null to an empty string for a C# string parameter.
194
+ // Only epoch-lock callers with an explicit File ID require this comparison.
195
+ string actualId=String.IsNullOrEmpty(expectedId)?null:FileIdentity(handle);
196
+ // Identity must be checked on the SAME handle used for hash and disposition.
197
+ if(!String.IsNullOrEmpty(expectedId)&&!String.Equals(actualId,expectedId,StringComparison.Ordinal))return "CHANGED";
198
+ using(var stream=new FileStream(handle,FileAccess.Read)){
199
+ if(stream.Length<2||stream.Length>16384)throw new InvalidOperationException("pinned file size invalid");
200
+ byte[] content=new byte[(int)stream.Length];int index=0,read;
201
+ while(index<content.Length&&(read=stream.Read(content,index,content.Length-index))>0)index+=read;
202
+ if(index!=content.Length)throw new IOException("pinned file truncated");
203
+ string actual=BitConverter.ToString(SHA256.Create().ComputeHash(content)).Replace("-","").ToLowerInvariant();
204
+ if(!String.Equals(actual,expectedSha,StringComparison.Ordinal))return "CHANGED";
205
+ IntPtr flag=Marshal.AllocHGlobal(4);
206
+ try {
207
+ Marshal.WriteInt32(flag,1);
208
+ if(!SetFileInformationByHandle(handle,4,flag,4))
209
+ throw new Win32Exception(Marshal.GetLastWin32Error(),"pinned file delete disposition denied");
210
+ } finally {Marshal.FreeHGlobal(flag);}
211
+ return "REMOVED";
212
+ }
213
+ }
214
+ }
215
+ }
216
+ '@
217
+ Add-Type -TypeDefinition $source -Language CSharp -ErrorAction Stop | Out-Null
218
+ $file=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:DEADBYTE_PINNED_MARKER_B64))
219
+ $sha=$env:DEADBYTE_PINNED_MARKER_SHA
220
+ $id=$env:DEADBYTE_PINNED_MARKER_FILE_ID
221
+ if($env:DEADBYTE_PINNED_MARKER_OP -eq 'snapshot') {
222
+ [Console]::Out.WriteLine([DeadbytePinnedDeleteV2]::Snapshot($file))
223
+ } elseif($env:DEADBYTE_PINNED_MARKER_OP -eq 'create') {
224
+ [Console]::Out.WriteLine([DeadbytePinnedDeleteV2]::CreateLock($file,$env:DEADBYTE_PINNED_MARKER_BYTES_B64))
225
+ } else {
226
+ if([String]::IsNullOrEmpty($id)){$id=$null}
227
+ [Console]::Out.WriteLine([DeadbytePinnedDeleteV2]::DeleteIfHash($file,$sha,$id))
228
+ }
229
+ `;
230
+
231
+ const FILE_ID_PATTERN=/^[0-9a-f]{16}:[0-9a-f]{32}$/;
232
+ function invokePinnedNative(file,{op,sha=null,fileId=null,bytes=null}={}){
233
+ if(process.platform!=='win32')throw new Error('native pinned file I/O is Windows-only');
234
+ if(typeof file!=='string'||!path.isAbsolute(file)||!['snapshot','delete','create'].includes(op)||
235
+ (op==='delete'&&!HEX64.test(sha??''))||
236
+ (op==='create'&&(!Buffer.isBuffer(bytes)||bytes.length<2||bytes.length>16384))||
237
+ (fileId!==null&&!FILE_ID_PATTERN.test(fileId)))
238
+ throw new Error('native pinned file arguments invalid');
239
+ const powershell='C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
240
+ const result=spawnSync(powershell,['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',
241
+ Buffer.from(nativeCommand,'utf16le').toString('base64')],{
242
+ encoding:'utf8',windowsHide:true,shell:false,timeout:25000,maxBuffer:262144,
243
+ env:windowsPowerShellEnvironment({...process.env,SystemRoot:'C:\\Windows',WINDIR:'C:\\Windows',
244
+ DEADBYTE_PINNED_MARKER_B64:Buffer.from(file,'utf8').toString('base64'),
245
+ DEADBYTE_PINNED_MARKER_SHA:sha??'',
246
+ DEADBYTE_PINNED_MARKER_FILE_ID:fileId??'',
247
+ DEADBYTE_PINNED_MARKER_BYTES_B64:bytes?.toString('base64')??'',
248
+ DEADBYTE_PINNED_MARKER_OP:op})
249
+ });
250
+ if(result.error||result.status!==0)throw new Error('native pinned file I/O failed closed');
251
+ return result.stdout.trim();
252
+ }
253
+
254
+ // Snapshot locks using one native handle for owner bytes, SHA-256, and FileIdInfo.
255
+ // Node lstat dev/ino is insufficient as the sole cross-API identity authority.
256
+ export function readPinnedEpochLockSnapshot(file){
257
+ const result=invokePinnedNative(file,{op:'snapshot'});
258
+ if(result==='MISSING')return null;
259
+ const match=/^SNAPSHOT\|([0-9a-f]{16}:[0-9a-f]{32})\|([0-9a-f]{64})\|([A-Za-z0-9+/]+={0,2})$/.exec(result);
260
+ if(!match)throw new Error('native pinned epoch lock snapshot malformed');
261
+ const bytes=Buffer.from(match[3],'base64');
262
+ if(bytes.length<2||bytes.length>16384||bytes.toString('base64')!==match[3]||digest(bytes)!==match[2])
263
+ throw new Error('native pinned epoch lock snapshot content mismatch');
264
+ return Object.freeze({bytes,sha256:match[2],fileId:match[1]});
265
+ }
266
+
267
+ // Creation is pinned under the same non-reparse ancestor chain. The returned
268
+ // File ID belongs to the handle that wrote and flushed the original lock bytes.
269
+ export function createPinnedEpochLock(file,bytes){
270
+ const result=invokePinnedNative(file,{op:'create',bytes});
271
+ if(result==='EXISTS')return null;
272
+ const match=/^CREATED\|([0-9a-f]{16}:[0-9a-f]{32})\|([0-9a-f]{64})$/.exec(result);
273
+ if(!match||digest(bytes)!==match[2])throw new Error('native epoch lock creation identity invalid');
274
+ return Object.freeze({fileId:match[1],sha256:match[2]});
275
+ }
276
+
277
+ // Existing remote-marker API remains content-CAS for backward compatibility.
278
+ // Epoch locks MUST pass an expected FileIdInfo captured from a native snapshot.
279
+ export function deletePinnedRemoteMarker(file,expectedSha256,options={}){
280
+ const expectedFileId=options?.expectedFileId??null;
281
+ const answer=invokePinnedNative(file,{op:'delete',sha:expectedSha256,fileId:expectedFileId});
282
+ if(!['REMOVED','MISSING','CHANGED'].includes(answer))throw new Error('handle-bound deletion result invalid');
283
+ return answer;
284
+ }
@@ -1,7 +1,7 @@
1
1
  import {spawnSync} from 'node:child_process';
2
2
  import {createHash} from 'node:crypto';
3
3
  import path from 'node:path';
4
- import {windowsPowerShellEnvironment} from './runtime-update.mjs';
4
+ import {windowsPowerShellEnvironment} from './windows-powershell-environment.mjs';
5
5
 
6
6
  export function validForegroundIdentity(value){
7
7
  return value?.schema==='deadbyte.foreground-process.v1'&&Number.isSafeInteger(value.pid)&&value.pid>0&&Number.isSafeInteger(value.parent_pid)&&value.parent_pid>=0&&typeof value.creation_utc==='string'&&/^\d{4}-\d\d-\d\dT.*Z$/.test(value.creation_utc)&&Number.isFinite(Date.parse(value.creation_utc))&&typeof value.executable_path==='string'&&path.isAbsolute(value.executable_path)&&/^[0-9a-f]{64}$/.test(value.command_line_sha256??'');
@@ -14,9 +14,9 @@ export function foregroundIdentityMatches(expected,observed){
14
14
  }
15
15
  export function queryForegroundIdentity(pid){
16
16
  if(process.platform!=='win32'||!Number.isSafeInteger(pid)||pid<1)throw new Error('foreground identity requires a positive Windows PID');
17
- const root=process.env.SystemRoot||process.env.WINDIR;if(!root)throw new Error('Windows system root missing');
17
+ const powershell='C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
18
18
  const command=`$p=Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}'; if ($null -eq $p) {Write-Output 'null';exit 0}; [pscustomobject]@{pid=$p.ProcessId;parent_pid=$p.ParentProcessId;creation_utc=$p.CreationDate.ToUniversalTime().ToString('o');executable_path=$p.ExecutablePath;command_line=$p.CommandLine} | ConvertTo-Json -Compress`;
19
- const result=spawnSync(path.join(root,'System32/WindowsPowerShell/v1.0/powershell.exe'),['-NoLogo','-NoProfile','-Command',command],{encoding:'utf8',windowsHide:true,env:windowsPowerShellEnvironment(process.env),timeout:15000,maxBuffer:1024*1024});
19
+ const result=spawnSync(powershell,['-NoLogo','-NoProfile','-Command',command],{encoding:'utf8',windowsHide:true,env:windowsPowerShellEnvironment({...process.env,SystemRoot:'C:\\Windows',WINDIR:'C:\\Windows'}),timeout:15000,maxBuffer:1024*1024});
20
20
  if(result.error||result.status!==0)throw new Error('foreground process identity query failed');
21
21
  let raw;try{raw=JSON.parse(result.stdout);}catch{throw new Error('foreground process identity query malformed');}
22
22
  if(raw===null)return null;
@@ -0,0 +1,196 @@
1
+ // R89 development: fail closed before requiring a privileged Windows native addon.
2
+ // Trust is an OS-owned installation property, not a manifest hash or npm version.
3
+ // This checks an honest process against lower-trust filesystem principals; it is
4
+ // NOT an attestation against a malicious actor already executing with equivalent
5
+ // write/administrative rights, or against a compromised Node/PowerShell runtime.
6
+ import {execFile} from 'node:child_process';
7
+ import {promisify} from 'node:util';
8
+ import path from 'node:path';
9
+
10
+ const execFileAsync=promisify(execFile);
11
+ function windowsPowerShellEnvironment(base=process.env){
12
+ const env={...base};
13
+ // The ACL probe must not autoload commands from caller-controlled module
14
+ // directories. Its executable is pinned to C:\Windows below, so use only
15
+ // that installation's built-in modules and matching OS-root variables.
16
+ for(const key of Object.keys(env)){
17
+ if(['psmodulepath','systemroot','windir'].includes(key.toLowerCase()))delete env[key];
18
+ }
19
+ env.SystemRoot='C:\\Windows';
20
+ env.WINDIR='C:\\Windows';
21
+ env.PSModulePath='C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\Modules';
22
+ return env;
23
+ }
24
+ const DRIVE=/^[A-Za-z]:\\/;
25
+ const SYSTEM='S-1-5-18';
26
+ const ADMINISTRATORS='S-1-5-32-544';
27
+ const TRUSTED_INSTALLER='S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464';
28
+ const TRUSTED=new Set([SYSTEM,ADMINISTRATORS,TRUSTED_INSTALLER]);
29
+ // Block untrusted changes to an existing directory or file, including security
30
+ // descriptor, content, attributes, or deletion. Create-child alone is not enough
31
+ // to replace an existing child, but full Modify contains DELETE and is denied.
32
+ const COMMON_MUTATION=0x000d0150; // WRITE_EA, DELETE_CHILD, WRITE_ATTRIBUTES, DELETE, WRITE_DAC, WRITE_OWNER
33
+ const FILE_MUTATION=COMMON_MUTATION|0x00000002|0x00000004; // WRITE_DATA / APPEND_DATA
34
+ const DIR_MUTATION=COMMON_MUTATION;
35
+ // Win32 ACEs may retain generic/indeterminate masks rather than expanded
36
+ // FILE_GENERIC_WRITE/FILE_ALL_ACCESS. Do not treat those as harmless.
37
+ const GENERIC_WRITE_OR_ALL=0x40000000|0x10000000;
38
+ const INDETERMINATE_ACCESS=0x02000000; // MAXIMUM_ALLOWED is not a finite read-only grant
39
+ const ANY_MUTATION_OR_UNDECIDABLE=(GENERIC_WRITE_OR_ALL|INDETERMINATE_ACCESS);
40
+ // Windows ACE scope is not the same as ACE inheritance origin. INHERIT_ONLY
41
+ // means the ACE does NOT apply to the currently inspected file/directory;
42
+ // it may apply to children, which are checked separately in the path chain.
43
+ // Missing / malformed scope must never silently become an allow.
44
+ const INHERIT_ONLY=2; // .NET PropagationFlags.InheritOnly=2; NoPropagateInherit=1
45
+ const KNOWN_INHERITANCE_FLAGS=3; // ContainerInherit | ObjectInherit
46
+ const KNOWN_PROPAGATION_FLAGS=3; // InheritOnly | NoPropagateInherit
47
+
48
+ function normalizeWindows(file){
49
+ if(typeof file!=='string'||!DRIVE.test(file)||file.includes('\0')||file.startsWith('\\\\'))
50
+ throw new Error('R89 privileged path must be a Windows drive-absolute path');
51
+ const normalized=path.win32.normalize(file);
52
+ if(!DRIVE.test(normalized))throw new Error('R89 privileged path normalization invalid');
53
+ return normalized.toLowerCase();
54
+ }
55
+ export function privilegedEpochInstallFiles(root,nodeExecutable){
56
+ const join=(...segments)=>path.win32.join(root,...segments);
57
+ return [nodeExecutable,
58
+ join('bin','deadbyte-epoch-lock-r85.node'),
59
+ join('docs','R85-EPOCH-LOCK-BINARY.json'),
60
+ join('native','deadbyte-epoch-lock-r85.cpp'),
61
+ join('src','epoch-lock-r85.mjs'),
62
+ join('src','privileged-install-trust-r89.mjs'),
63
+ join('src','authority-continuity-r65.mjs'),
64
+ join('src','canonical-json.mjs'),
65
+ join('src','text-policy.mjs'),
66
+ join('src','foreground-marker-cas-win32.mjs'),
67
+ join('src','foreground-process-identity.mjs'),
68
+ join('src','windows-powershell-environment.mjs')];
69
+ }
70
+ export function requiredAclPathChain(files){
71
+ if(!Array.isArray(files)||files.length<1||files.length>32)
72
+ throw new Error('R89 privileged path inventory invalid');
73
+ const seen=new Set();
74
+ for(const file of files){
75
+ let current=normalizeWindows(file);
76
+ for(let depth=0;depth<128;depth++){
77
+ seen.add(current);
78
+ const parent=path.win32.dirname(current);
79
+ if(parent===current)break;
80
+ if(depth===127)throw new Error('R89 privileged ancestor depth exceeded');
81
+ current=parent;
82
+ }
83
+ }
84
+ return [...seen].sort();
85
+ }
86
+
87
+ // Pure conservative evaluator. Records must come from Windows Get-Acl, never
88
+ // from a writable package file, environment variable or release metadata.
89
+ export function evaluatePrivilegedAclSnapshot(records,files){
90
+ const required=requiredAclPathChain(files);
91
+ if(!Array.isArray(records)||records.length!==required.length)
92
+ return Object.freeze({trusted:false,reason:'INCOMPLETE_ACL_SNAPSHOT'});
93
+ const byPath=new Map();
94
+ for(const rec of records){
95
+ let key;
96
+ try{key=normalizeWindows(rec?.path);}catch{return Object.freeze({trusted:false,reason:'INVALID_ACL_PATH'});}
97
+ if(byPath.has(key))return Object.freeze({trusted:false,reason:'DUPLICATE_ACL_PATH',path:key});
98
+ byPath.set(key,rec);
99
+ }
100
+ const leaves=new Set(files.map(normalizeWindows));
101
+ for(const file of required){
102
+ const rec=byPath.get(file);
103
+ if(!rec)return Object.freeze({trusted:false,reason:'MISSING_ACL_NODE',path:file});
104
+ if(rec.reparse!==false || typeof rec.directory!=='boolean' ||
105
+ !TRUSTED.has(rec.owner) || !Array.isArray(rec.aces))
106
+ return Object.freeze({trusted:false,reason:'UNTRUSTED_OWNER_OR_REPARSE',path:file});
107
+ if(leaves.has(file)&&rec.directory===true)
108
+ return Object.freeze({trusted:false,reason:'EXPECTED_FILE_NOT_FILE',path:file});
109
+ if(!leaves.has(file)&&rec.directory!==true)
110
+ return Object.freeze({trusted:false,reason:'EXPECTED_PARENT_NOT_DIRECTORY',path:file});
111
+ for(const ace of rec.aces){
112
+ if(!ace||typeof ace.sid!=='string'||!['Allow','Deny'].includes(ace.type)||
113
+ !Number.isSafeInteger(ace.mask)||ace.mask<0||ace.mask>0xffffffff)
114
+ return Object.freeze({trusted:false,reason:'UNKNOWN_ACL_RULE',path:file});
115
+ // These values are obtained directly from OS AccessRule fields below.
116
+ // An inheritance-only CREATOR OWNER placeholder is not an effective
117
+ // mutation grant on THIS object. An effective CREATOR OWNER ACE (or an
118
+ // untrusted effective GENERIC_ALL) is still rejected, not whitelisted.
119
+ if(!Number.isInteger(ace.inheritance)||ace.inheritance<0||
120
+ (ace.inheritance&~KNOWN_INHERITANCE_FLAGS)!==0||
121
+ !Number.isInteger(ace.propagation)||ace.propagation<0||
122
+ (ace.propagation&~KNOWN_PROPAGATION_FLAGS)!==0||
123
+ (ace.propagation!==0&&ace.inheritance===0)||
124
+ ((ace.propagation&INHERIT_ONLY)!==0&&(!rec.directory||ace.inheritance===0)))
125
+ return Object.freeze({trusted:false,reason:'UNKNOWN_ACL_SCOPE',path:file});
126
+ if((ace.propagation&INHERIT_ONLY)!==0)continue;
127
+ if(ace.type==='Allow'&&!TRUSTED.has(ace.sid)&&
128
+ (ace.mask&((rec.directory?DIR_MUTATION:FILE_MUTATION)|ANY_MUTATION_OR_UNDECIDABLE))!==0)
129
+ return Object.freeze({trusted:false,reason:'UNTRUSTED_WRITE_ACE',path:file,sid:ace.sid});
130
+ }
131
+ }
132
+ return Object.freeze({trusted:true,reason:'PROTECTED_INSTALLATION_ACL_SNAPSHOT'});
133
+ }
134
+
135
+ // This command is compiled into the module; it does NOT run a repo-local .ps1.
136
+ // Errors, unknown ACEs/SIDs and inaccessible paths are fatal.
137
+ const aclProbe=String.raw`
138
+ $ErrorActionPreference='Stop'; Set-StrictMode -Version Latest
139
+ try {
140
+ $utf8=[Text.Encoding]::UTF8
141
+ $files=ConvertFrom-Json -InputObject ($utf8.GetString([Convert]::FromBase64String('__PATHS_BASE64__')))
142
+ $seen=[System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
143
+ $items=New-Object System.Collections.ArrayList
144
+ foreach($p in @($files)) {
145
+ $node=[IO.Path]::GetFullPath([string]$p)
146
+ while($true) {
147
+ if($seen.Add($node)) {
148
+ $item=Get-Item -LiteralPath $node -Force -ErrorAction Stop
149
+ $acl=Get-Acl -LiteralPath $node -ErrorAction Stop
150
+ $owner=$acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value
151
+ $aces=New-Object System.Collections.ArrayList
152
+ foreach($entry in $acl.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier])) {
153
+ [void]$aces.Add(@{sid=$entry.IdentityReference.Value;type=$entry.AccessControlType.ToString();mask=([int64]$entry.FileSystemRights -band [int64]4294967295);inheritance=[int]$entry.InheritanceFlags;propagation=[int]$entry.PropagationFlags})
154
+ }
155
+ [void]$items.Add(@{path=$node;owner=$owner;reparse=[bool]($item.Attributes -band [IO.FileAttributes]::ReparsePoint);directory=[bool]$item.PSIsContainer;aces=@($aces.ToArray())})
156
+ }
157
+ $parent=[IO.Directory]::GetParent($node)
158
+ if($null -eq $parent){break}
159
+ $node=$parent.FullName
160
+ }
161
+ }
162
+ ConvertTo-Json -InputObject @($items.ToArray()) -Depth 9 -Compress
163
+ } catch { [Console]::Error.WriteLine('R89_ACL_PROBE_FAILED: '+$_.Exception.GetType().Name); exit 12 }
164
+ `;
165
+
166
+ export async function inspectTrustedInstallation(files){
167
+ if(process.platform!=='win32'||process.arch!=='x64')
168
+ throw new Error('R89 privileged native loading requires Windows x64');
169
+ const required=requiredAclPathChain(files);
170
+ // Absolute OS executable: do not resolve a user-controlled PATH, run a
171
+ // repo-owned helper, or honor a caller-provided tool override.
172
+ const ps='C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
173
+ const json=Buffer.from(JSON.stringify(files),'utf8').toString('base64');
174
+ const script=aclProbe.replace('__PATHS_BASE64__',json);
175
+ const encoded=Buffer.from(script,'utf16le').toString('base64');
176
+ let stdout;
177
+ try{
178
+ ({stdout}=await execFileAsync(ps,['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',encoded],
179
+ {windowsHide:true,timeout:12000,maxBuffer:512*1024,encoding:'utf8',env:windowsPowerShellEnvironment()}));
180
+ }catch(error){
181
+ throw Object.assign(new Error('R89 privileged installation ACL inspection failed closed',{cause:error}),
182
+ {code:'EPOCH_NATIVE_SLOT_UNTRUSTED'});
183
+ }
184
+ let records;
185
+ try{records=JSON.parse(stdout);}catch{
186
+ throw Object.assign(new Error('R89 privileged installation ACL response invalid'),
187
+ {code:'EPOCH_NATIVE_SLOT_UNTRUSTED'});
188
+ }
189
+ const decision=evaluatePrivilegedAclSnapshot(records,files);
190
+ if(!decision.trusted){
191
+ throw Object.assign(new Error(`R89 privileged installation not protected: ${decision.reason}`),
192
+ {code:'EPOCH_NATIVE_SLOT_UNTRUSTED',reason:decision.reason});
193
+ }
194
+ if(required.length!==records.length)throw new Error('R89 ACL result cardinality changed');
195
+ return decision;
196
+ }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { lstat, readFile, rm } from 'node:fs/promises';
2
+ import { lstat } from 'node:fs/promises';
3
3
  import { spawnSync } from 'node:child_process';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
@@ -7,6 +7,7 @@ import { setTimeout as sleep } from 'node:timers/promises';
7
7
  import { windowsPowerShellEnvironment } from './runtime-update.mjs';
8
8
  import { resolveStateRoot } from './state-root.mjs';
9
9
  import { AUTHORITY_PLANES, parseAuthorityObservation, assertAuthorityVector } from './remote-authority.mjs';
10
+ import { readRemoteMarkerSnapshot, deletePinnedRemoteMarker } from './foreground-marker-cas-win32.mjs';
10
11
 
11
12
  const CLEANUP=['disarmautonomous','disarmcoding','disarmprocess','disarmdesktop','disarmhost'];
12
13
  // This inherited IPC channel is an OS-owned parent lifetime handle, not a PID
@@ -33,10 +34,8 @@ function pidAlive(pid){
33
34
  catch(error){return error?.code!=='ESRCH';}
34
35
  }
35
36
  async function readMarker(file){
36
- let text;
37
- try{text=await readFile(file,'utf8');}
38
- catch(error){if(error?.code==='ENOENT') return null;throw error;}
39
- try{return JSON.parse(text);}catch{return null;}
37
+ const snapshot=await readRemoteMarkerSnapshot(file);
38
+ return snapshot?.marker??null;
40
39
  }
41
40
  function markerMatches(marker,{ownerPid,nonce}){
42
41
  return marker?.schema==='deadbyte.remote-session.v2'&&
@@ -99,8 +98,11 @@ async function main(){
99
98
  if(observed.state==='ONLINE'&&!invokeCleanup(powershell,launcher,'machinetest',root,'revoked'))
100
99
  throw new Error('guardian runtime enforcement cleanup unverified; ownership marker retained');
101
100
  if(!invokeCleanup(powershell,launcher,'park',root)) throw new Error('guardian park failed; ownership marker retained');
102
- const current=await readMarker(markerPath);
103
- if(current&&markerMatches(current,{ownerPid,nonce})) await rm(markerPath,{force:true});
101
+ const current=await readRemoteMarkerSnapshot(markerPath);
102
+ if(!current)return;
103
+ if(!markerMatches(current.marker,{ownerPid,nonce}))return;
104
+ if(deletePinnedRemoteMarker(markerPath,current.sha256)!=='REMOVED')
105
+ throw new Error('guardian marker changed before native deletion; marker retained');
104
106
  return;
105
107
  }
106
108
  }
@@ -21,6 +21,9 @@ import { resolveStateRoot } from './state-root.mjs';
21
21
  import {canonicalLauncherPaths,assertCanonicalLaunchers,renderStateBoundLauncherCmd} from './launcher-isolation.mjs';
22
22
  import {assertStageOwnedPath,assertStagingBootstrapInputs,assertStagingControlConfig} from './staging-input-isolation.mjs';
23
23
  import { verifyProjectOwnerApproval } from './r70-owner-approval.mjs';
24
+ import {windowsPowerShellEnvironment} from './windows-powershell-environment.mjs';
25
+
26
+ export {windowsPowerShellEnvironment};
24
27
 
25
28
  const HEX64=/^[0-9a-f]{64}$/;
26
29
  const SEMVER=/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
@@ -216,31 +219,11 @@ export function parseRollbackArgs(argv){
216
219
  }
217
220
 
218
221
  function powershellPath(){
219
- return path.join(process.env.SystemRoot||'C:\\Windows','System32','WindowsPowerShell','v1.0','powershell.exe');
220
- }
221
- export function windowsPowerShellEnvironment(base=process.env){
222
- const systemRoot=base.SystemRoot||base.SYSTEMROOT||'C:\\Windows';
223
- const userProfile=base.USERPROFILE||os.homedir();
224
- const programFiles=base.ProgramFiles||base.PROGRAMFILES||'C:\\Program Files';
225
- const environment={...base};
226
- const inherited=Object.entries(base).filter(([key])=>key.toLowerCase()==='psmodulepath').flatMap(([,value])=>String(value??'').split(';')).filter(Boolean);
227
- for(const key of Object.keys(environment)) if(key.toLowerCase()==='psmodulepath') delete environment[key];
228
- const modules=[
229
- path.join(systemRoot,'System32','WindowsPowerShell','v1.0','Modules'),
230
- path.join(userProfile,'Documents','WindowsPowerShell','Modules'),
231
- path.join(programFiles,'WindowsPowerShell','Modules'),
232
- ...inherited
233
- ];
234
- const seen=new Set();
235
- return {...environment,PSModulePath:modules.filter(modulePath=>{
236
- const key=modulePath.toLowerCase();
237
- if(seen.has(key)) return false;
238
- seen.add(key);return true;
239
- }).join(';')};
222
+ return 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
240
223
  }
241
224
  function commandEnvironment(exe,base=process.env){
242
225
  return path.resolve(exe).toLowerCase()===path.resolve(powershellPath()).toLowerCase()
243
- ?windowsPowerShellEnvironment(base):base;
226
+ ?windowsPowerShellEnvironment({...base,SystemRoot:'C:\\Windows',WINDIR:'C:\\Windows'}):base;
244
227
  }
245
228
  function runCaptured(exe,args,{cwd,env,timeoutMs=45000}={}){
246
229
  const baseEnv=env??process.env;
package/src/version.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export const DEADBYTE_VERSION = '0.15.47';
1
+ export const DEADBYTE_VERSION = '0.15.48';
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';