windows-development-sandbox 1.13.2 → 1.15.0

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 (153) hide show
  1. package/config.example.json +2 -1
  2. package/dist/app/features/defs.d.ts +1 -1
  3. package/dist/app/features/projects/projects.def.d.ts +1 -1
  4. package/dist/app/features/projects/projects.run.d.ts +1 -1
  5. package/dist/app/features/projects/projects.run.js +10 -2
  6. package/dist/app/features/sandbox/sandbox.def.js +3 -1
  7. package/dist/app/features/settings/categories.def.js +1 -0
  8. package/dist/app/features/storage/storage.run.js +1 -1
  9. package/dist/bin/agent-sandbox.js +2 -2
  10. package/dist/bin/{chunk-VENWSGX2.js → chunk-D7CM63BS.js} +2303 -1462
  11. package/dist/bin/{chunk-K4SU3MHJ.js → chunk-HTQT5SBY.js} +1 -1
  12. package/dist/bin/{chunk-IL7BXJF2.js → chunk-RQ4BWBYX.js} +1 -1
  13. package/dist/bin/claude-host.js +2 -2
  14. package/dist/bin/claude-sbx.js +2 -2
  15. package/dist/bin/codex-sbx.js +2 -2
  16. package/dist/bin/gemini-sbx.js +2 -2
  17. package/dist/bin/sandbox.js +2 -2
  18. package/dist/bin/sbx-hide.js +2 -2
  19. package/dist/bin/sbx-show.js +2 -2
  20. package/dist/cli/commands/launch/launch.js +1 -1
  21. package/dist/cli/commands/projects/here.js +2 -0
  22. package/dist/cli/commands/repair/checks/contained.js +1 -1
  23. package/dist/cli/services/health-service.js +15 -3
  24. package/dist/cli/services/projects-service.d.ts +8 -3
  25. package/dist/cli/services/projects-service.js +21 -4
  26. package/dist/core/config/defaults.js +1 -0
  27. package/dist/core/schema/config.d.ts +4 -0
  28. package/dist/core/schema/projects.d.ts +4 -0
  29. package/dist/core/schema/projects.js +3 -0
  30. package/dist/guest/boot/boot.js +8 -2
  31. package/dist/guest/daemon/daemon.js +3 -0
  32. package/dist/guest/daemon/live-folders.d.ts +48 -0
  33. package/dist/guest/daemon/live-folders.js +245 -0
  34. package/dist/guest/daemon/live.d.ts +4 -0
  35. package/dist/guest/daemon/live.js +57 -0
  36. package/dist/guest/daemon/sessions.d.ts +7 -0
  37. package/dist/guest/daemon/sessions.js +23 -2
  38. package/dist/guest/entries/daemon.js +5 -2
  39. package/dist/guest/runtime/csc.d.ts +1 -0
  40. package/dist/guest/runtime/csc.js +1 -0
  41. package/dist/guest/runtime/secrets.d.ts +5 -2
  42. package/dist/guest/runtime/secrets.js +12 -5
  43. package/dist/guest/sessions/pty-host.d.ts +10 -0
  44. package/dist/guest/sessions/pty-host.js +48 -3
  45. package/dist/host/channel/channel.js +1 -1
  46. package/dist/host/engines/hcs/live-folders.d.ts +117 -0
  47. package/dist/host/engines/hcs/live-folders.js +347 -0
  48. package/dist/host/engines/index.d.ts +1 -0
  49. package/dist/host/engines/index.js +1 -0
  50. package/dist/host/install/install-record.d.ts +1 -0
  51. package/dist/host/install/install-record.js +2 -0
  52. package/dist/host/launch/generate.js +8 -1
  53. package/dist/host/projects/join.d.ts +4 -0
  54. package/dist/host/projects/join.js +11 -3
  55. package/dist/host/projects/mapper.d.ts +34 -5
  56. package/dist/host/projects/mapper.js +88 -20
  57. package/dist/host/projects/registry.d.ts +2 -0
  58. package/dist/host/projects/registry.js +1 -1
  59. package/dist/host/sandbox/contained-deps.d.ts +2 -1
  60. package/dist/host/sandbox/contained-deps.js +6 -4
  61. package/dist/host/secrets/launch-secrets.js +2 -1
  62. package/dist/host/sharing/lifecycle.js +9 -1
  63. package/dist/host/storage/contained/contained-storage.js +13 -0
  64. package/dist/host/storage/contained/disk-port.d.ts +5 -0
  65. package/dist/host/storage/contained/helper-port.d.ts +72 -0
  66. package/dist/host/storage/contained/helper-port.js +162 -0
  67. package/dist/host/storage/contained/index.d.ts +1 -0
  68. package/dist/host/storage/contained/index.js +1 -0
  69. package/dist/host/web/bridge.d.ts +5 -0
  70. package/dist/host/web/bridge.js +11 -1
  71. package/dist/host/web/server.d.ts +2 -0
  72. package/dist/host/web/server.js +11 -1
  73. package/dist/runtime/agent.js +23 -0
  74. package/dist/runtime/daemon.js +523 -135
  75. package/dist/runtime/db.js +23 -0
  76. package/dist/runtime/main.js +221 -15
  77. package/dist/runtime/netproxy.js +23 -0
  78. package/dist/runtime/view.js +209 -12
  79. package/dist/selftest/channel.test.js +72 -1
  80. package/dist/selftest/contained.test.js +1 -37
  81. package/dist/selftest/hcs.test.js +5 -2
  82. package/dist/selftest/index.js +4 -0
  83. package/dist/selftest/install.test.js +11 -0
  84. package/dist/selftest/layout.test.js +1 -0
  85. package/dist/selftest/live-daemon.test.js +51 -1
  86. package/dist/selftest/live-folders.test.d.ts +2 -0
  87. package/dist/selftest/live-folders.test.js +588 -0
  88. package/dist/selftest/live-terminal.test.js +112 -4
  89. package/dist/selftest/projects.test.js +7 -7
  90. package/dist/selftest/storage-helper.test.d.ts +2 -0
  91. package/dist/selftest/storage-helper.test.js +493 -0
  92. package/dist/selftest/support/engine-golden.js +1 -0
  93. package/dist/selftest/support/fixtures.d.ts +1 -0
  94. package/dist/selftest/support/live.js +2 -0
  95. package/dist/selftest/support/vhdx.d.ts +14 -0
  96. package/dist/selftest/support/vhdx.js +72 -0
  97. package/dist/selftest/web.test.js +14 -2
  98. package/dist/shared/channel/index.d.ts +1 -0
  99. package/dist/shared/channel/index.js +1 -0
  100. package/dist/shared/channel/live.d.ts +53 -0
  101. package/dist/shared/channel/live.js +92 -0
  102. package/dist/shared/channel/run-dir.d.ts +11 -0
  103. package/dist/shared/channel/run-dir.js +20 -0
  104. package/dist/shared/channel/secrets.d.ts +1 -0
  105. package/dist/shared/channel/secrets.js +5 -0
  106. package/dist/shared/paths/projects.d.ts +6 -1
  107. package/dist/shared/paths/projects.js +21 -1
  108. package/dist/shared/pty/index.d.ts +3 -1
  109. package/dist/shared/pty/index.js +3 -1
  110. package/dist/shared/pty/key-gate.d.ts +24 -0
  111. package/dist/shared/pty/key-gate.js +45 -0
  112. package/dist/shared/pty/mouse-modes.d.ts +27 -0
  113. package/dist/shared/pty/mouse-modes.js +155 -0
  114. package/dist/shared/pty/terminal.d.ts +0 -5
  115. package/dist/shared/pty/terminal.js +51 -20
  116. package/dist/sync/watcher.js +10 -1
  117. package/dist/web/index.html +1 -1
  118. package/dist/web/{main-NBQ3LF4E.js → main-7DJZ2M27.js} +34 -34
  119. package/native/csharp/HcsHost/AssemblyInfo.cs +4 -4
  120. package/native/csharp/HcsHost/FolderAccess.cs +361 -0
  121. package/native/csharp/HcsHost/Hcs.cs +31 -3
  122. package/native/csharp/HcsHost/Program.cs +54 -6
  123. package/native/csharp/HcsHost/Share.cs +64 -11
  124. package/native/csharp/HcsHost/WsbGuard.cs +130 -0
  125. package/native/csharp/HcsHost/WsbShare.cs +295 -0
  126. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  127. package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
  128. package/native/csharp/StorageHelper/Client.cs +297 -0
  129. package/native/csharp/StorageHelper/Log.cs +177 -0
  130. package/native/csharp/StorageHelper/MountRecords.cs +160 -0
  131. package/native/csharp/StorageHelper/Operations.cs +596 -0
  132. package/native/csharp/StorageHelper/Pins.cs +270 -0
  133. package/native/csharp/StorageHelper/Privileges.cs +78 -0
  134. package/native/csharp/StorageHelper/Program.cs +349 -0
  135. package/native/csharp/StorageHelper/Protocol.cs +107 -0
  136. package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
  137. package/native/csharp/StorageHelper/Rules.cs +156 -0
  138. package/native/csharp/StorageHelper/Server.cs +656 -0
  139. package/native/csharp/StorageHelper/Settings.cs +297 -0
  140. package/native/csharp/StorageHelper/StorageService.cs +53 -0
  141. package/native/csharp/StorageHelper/UserSettings.cs +206 -0
  142. package/native/csharp/StorageHelper/Vhdx.cs +230 -0
  143. package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
  144. package/native/csharp/StorageHelper/Volumes.cs +379 -0
  145. package/native/csharp/StorageHelper/Watchdog.cs +128 -0
  146. package/native/csharp/StorageHelper/Win32.cs +250 -0
  147. package/native/scripts/contained-common.ps1 +4 -0
  148. package/native/scripts/hcs-live-share.ps1 +191 -0
  149. package/native/scripts/set-storage-helper.ps1 +203 -0
  150. package/package.json +1 -1
  151. package/sandbox/csharp/BindMount.cs +120 -0
  152. package/sandbox/csharp/PtyHost.cs +236 -1
  153. package/schemas/config.schema.json +8 -0
@@ -0,0 +1,250 @@
1
+ // The Windows calls the storage helper makes that are shared by several files: opening files and folders as handles
2
+ // (never following a link), the facts a handle can tell (attributes, identity, final path, owner and permissions,
3
+ // volume), and device I/O. Signatures from the Microsoft Win32 reference.
4
+ using System;
5
+ using System.ComponentModel;
6
+ using System.Runtime.InteropServices;
7
+ using System.Text;
8
+ using Microsoft.Win32.SafeHandles;
9
+
10
+ namespace AgentSandbox.StorageHelper
11
+ {
12
+ public static class Win32
13
+ {
14
+ public const uint GenericRead = 0x80000000;
15
+ public const uint GenericWrite = 0x40000000;
16
+ public const uint ReadControl = 0x00020000;
17
+ public const uint Synchronize = 0x00100000;
18
+ public const uint FileReadAttributes = 0x0080;
19
+ public const uint FileWriteAttributes = 0x0100;
20
+ public const uint FileReadData = 0x0001;
21
+ public const uint FileWriteData = 0x0002;
22
+ public const uint Delete = 0x00010000;
23
+ public const uint FileShareRead = 0x1;
24
+ public const uint FileShareWrite = 0x2;
25
+ public const uint FileShareDelete = 0x4;
26
+ public const uint OpenExisting = 3;
27
+ public const uint FlagBackupSemantics = 0x02000000;
28
+ public const uint FlagOpenReparsePoint = 0x00200000;
29
+ public const uint AttributeDirectory = 0x10;
30
+ public const uint AttributeReparsePoint = 0x400;
31
+ public const int ErrorFileNotFound = 2;
32
+ public const int ErrorPathNotFound = 3;
33
+ public const int ErrorSharingViolation = 32;
34
+ public const int ErrorMoreData = 234;
35
+ public const int ErrorNoMoreFiles = 18;
36
+ public const uint SeFileObject = 1;
37
+ public const uint OwnerSecurityInformation = 0x1;
38
+ public const uint GroupSecurityInformation = 0x2;
39
+ public const uint DaclSecurityInformation = 0x4;
40
+ public const uint DriveFixed = 3;
41
+
42
+ [StructLayout(LayoutKind.Sequential)]
43
+ public struct FileTime
44
+ {
45
+ public uint Low;
46
+ public uint High;
47
+ }
48
+
49
+ [StructLayout(LayoutKind.Sequential)]
50
+ public struct ByHandleFileInformation
51
+ {
52
+ public uint FileAttributes;
53
+ public FileTime CreationTime;
54
+ public FileTime LastAccessTime;
55
+ public FileTime LastWriteTime;
56
+ public uint VolumeSerialNumber;
57
+ public uint FileSizeHigh;
58
+ public uint FileSizeLow;
59
+ public uint NumberOfLinks;
60
+ public uint FileIndexHigh;
61
+ public uint FileIndexLow;
62
+ }
63
+
64
+ public static class NativeMethods
65
+ {
66
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
67
+ public static extern SafeFileHandle CreateFileW(string fileName, uint access, uint share, IntPtr security, uint creation, uint flags, IntPtr template);
68
+
69
+ [DllImport("kernel32.dll", SetLastError = true)]
70
+ [return: MarshalAs(UnmanagedType.Bool)]
71
+ public static extern bool GetFileInformationByHandle(SafeFileHandle file, out ByHandleFileInformation information);
72
+
73
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
74
+ public static extern uint GetFinalPathNameByHandleW(SafeFileHandle file, StringBuilder path, uint size, uint flags);
75
+
76
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
77
+ [return: MarshalAs(UnmanagedType.Bool)]
78
+ public static extern bool GetVolumeInformationByHandleW(SafeFileHandle file, StringBuilder volumeName, uint volumeNameSize, out uint serial, out uint maxComponent, out uint flags, StringBuilder fileSystem, uint fileSystemSize);
79
+
80
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
81
+ public static extern uint GetDriveTypeW(string root);
82
+
83
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
84
+ public static extern uint GetLongPathNameW(string shortPath, StringBuilder longPath, uint size);
85
+
86
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
87
+ [return: MarshalAs(UnmanagedType.Bool)]
88
+ public static extern bool GetVolumePathNameW(string fileName, StringBuilder volumePath, uint size);
89
+
90
+ [DllImport("advapi32.dll", SetLastError = false)]
91
+ public static extern uint GetSecurityInfo(SafeFileHandle handle, uint objectType, uint securityInfo, out IntPtr owner, out IntPtr group, out IntPtr dacl, out IntPtr sacl, out IntPtr securityDescriptor);
92
+
93
+ [DllImport("advapi32.dll")]
94
+ public static extern uint GetSecurityDescriptorLength(IntPtr securityDescriptor);
95
+
96
+ [DllImport("kernel32.dll")]
97
+ public static extern IntPtr LocalFree(IntPtr memory);
98
+
99
+ [DllImport("kernel32.dll", SetLastError = true)]
100
+ [return: MarshalAs(UnmanagedType.Bool)]
101
+ public static extern bool DeviceIoControl(SafeFileHandle device, uint code, byte[] input, int inputSize, byte[] output, int outputSize, out int returned, IntPtr overlapped);
102
+ }
103
+
104
+ public static Win32Exception LastError(string what)
105
+ {
106
+ int code = Marshal.GetLastWin32Error();
107
+ return new Win32Exception(code, what + ": " + new Win32Exception(code).Message);
108
+ }
109
+
110
+ /// <summary>A handle to a file or folder that is never followed through a link. Null when it does not exist.</summary>
111
+ public static SafeFileHandle OpenNoFollow(string path, uint access, uint share)
112
+ {
113
+ SafeFileHandle handle = NativeMethods.CreateFileW(path, access, share, IntPtr.Zero, OpenExisting, FlagBackupSemantics | FlagOpenReparsePoint, IntPtr.Zero);
114
+ if (handle.IsInvalid)
115
+ {
116
+ int code = Marshal.GetLastWin32Error();
117
+ handle.Dispose();
118
+ if (code == ErrorFileNotFound || code == ErrorPathNotFound)
119
+ {
120
+ return null;
121
+ }
122
+ if (code == ErrorSharingViolation)
123
+ {
124
+ throw new RefusedException("Something else has " + path + " open (it may be in the middle of a move); try again.");
125
+ }
126
+ throw new Win32Exception(code, "Could not open " + path + ": " + new Win32Exception(code).Message);
127
+ }
128
+ return handle;
129
+ }
130
+
131
+ public static ByHandleFileInformation InfoOf(SafeFileHandle handle, string what)
132
+ {
133
+ ByHandleFileInformation info;
134
+ if (!NativeMethods.GetFileInformationByHandle(handle, out info))
135
+ {
136
+ throw LastError("Could not read " + what);
137
+ }
138
+ return info;
139
+ }
140
+
141
+ /// <summary>The path Windows says the handle has (with its drive letter, without the \\?\ prefix).</summary>
142
+ public static string FinalPath(SafeFileHandle handle)
143
+ {
144
+ StringBuilder sb = new StringBuilder(1024);
145
+ uint n = NativeMethods.GetFinalPathNameByHandleW(handle, sb, (uint)sb.Capacity, 0);
146
+ if (n == 0 || n >= sb.Capacity)
147
+ {
148
+ throw LastError("Could not read where a folder is");
149
+ }
150
+ string path = sb.ToString();
151
+ if (path.StartsWith("\\\\?\\UNC\\", StringComparison.Ordinal))
152
+ {
153
+ throw new RefusedException("A network folder cannot hold contained storage.");
154
+ }
155
+ return path.StartsWith("\\\\?\\", StringComparison.Ordinal) ? path.Substring(4) : path;
156
+ }
157
+
158
+ /// <summary>The owner and the permissions of what the handle points to (it needs READ_CONTROL).</summary>
159
+ public static System.Security.AccessControl.RawSecurityDescriptor SecurityOf(SafeFileHandle handle)
160
+ {
161
+ IntPtr owner;
162
+ IntPtr group;
163
+ IntPtr dacl;
164
+ IntPtr sacl;
165
+ IntPtr descriptor;
166
+ uint error = NativeMethods.GetSecurityInfo(handle, SeFileObject, OwnerSecurityInformation | GroupSecurityInformation | DaclSecurityInformation, out owner, out group, out dacl, out sacl, out descriptor);
167
+ if (error != 0)
168
+ {
169
+ throw new Win32Exception((int)error, "Could not read the permissions: " + new Win32Exception((int)error).Message);
170
+ }
171
+ try
172
+ {
173
+ int length = (int)NativeMethods.GetSecurityDescriptorLength(descriptor);
174
+ byte[] bytes = new byte[length];
175
+ Marshal.Copy(descriptor, bytes, 0, length);
176
+ return new System.Security.AccessControl.RawSecurityDescriptor(bytes, 0);
177
+ }
178
+ finally
179
+ {
180
+ NativeMethods.LocalFree(descriptor);
181
+ }
182
+ }
183
+
184
+ public static string FileSystemOf(SafeFileHandle handle)
185
+ {
186
+ StringBuilder name = new StringBuilder(261);
187
+ StringBuilder fs = new StringBuilder(261);
188
+ uint serial;
189
+ uint max;
190
+ uint flags;
191
+ if (!NativeMethods.GetVolumeInformationByHandleW(handle, name, (uint)name.Capacity, out serial, out max, out flags, fs, (uint)fs.Capacity))
192
+ {
193
+ throw LastError("Could not read the drive's file system");
194
+ }
195
+ return fs.ToString();
196
+ }
197
+
198
+ /// <summary>The root of the volume a path is on (C:\, or a folder when a volume is mounted there).</summary>
199
+ public static string VolumePathOf(string path)
200
+ {
201
+ StringBuilder sb = new StringBuilder(1024);
202
+ if (!NativeMethods.GetVolumePathNameW(path, sb, (uint)sb.Capacity))
203
+ {
204
+ throw LastError("Could not tell which drive " + path + " is on");
205
+ }
206
+ return sb.ToString();
207
+ }
208
+
209
+ /// <summary>True when the handle is at exactly this path (8.3 short names in it are fine).</summary>
210
+ public static bool IsAt(SafeFileHandle handle, string expected)
211
+ {
212
+ string actual = FinalPath(handle);
213
+ return Rules.SamePath(actual, expected) || Rules.SamePath(actual, LongPath(expected));
214
+ }
215
+
216
+ /// <summary>The path with 8.3 short names spelled out (the path itself when Windows cannot say).</summary>
217
+ public static string LongPath(string path)
218
+ {
219
+ StringBuilder sb = new StringBuilder(1024);
220
+ uint n = NativeMethods.GetLongPathNameW(path, sb, (uint)sb.Capacity);
221
+ return n == 0 || n >= sb.Capacity ? path : sb.ToString();
222
+ }
223
+
224
+ public static uint DriveTypeOf(string root)
225
+ {
226
+ return NativeMethods.GetDriveTypeW(root);
227
+ }
228
+
229
+ /// <summary>DeviceIoControl with byte buffers; throws on failure.</summary>
230
+ public static int Ioctl(SafeFileHandle device, uint code, byte[] input, byte[] output, string what)
231
+ {
232
+ int returned;
233
+ if (!NativeMethods.DeviceIoControl(device, code, input, input == null ? 0 : input.Length, output, output == null ? 0 : output.Length, out returned, IntPtr.Zero))
234
+ {
235
+ throw LastError(what);
236
+ }
237
+ return returned;
238
+ }
239
+
240
+ /// <summary>DeviceIoControl that reports the Windows error code instead of throwing (0 = done).</summary>
241
+ public static int TryIoctl(SafeFileHandle device, uint code, byte[] input, byte[] output, out int returned)
242
+ {
243
+ if (NativeMethods.DeviceIoControl(device, code, input, input == null ? 0 : input.Length, output, output == null ? 0 : output.Length, out returned, IntPtr.Zero))
244
+ {
245
+ return 0;
246
+ }
247
+ return Marshal.GetLastWin32Error();
248
+ }
249
+ }
250
+ }
@@ -15,6 +15,10 @@ function Add-Note([string]$Text) {
15
15
  $script:Lines.Add($Text)
16
16
  }
17
17
 
18
+ function Get-Notes {
19
+ return @($script:Lines)
20
+ }
21
+
18
22
  function Test-PlainAbsolute([string]$Path) {
19
23
  if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
20
24
  if ($Path -notmatch $script:PathRule) { return $false }
@@ -0,0 +1,191 @@
1
+ #Requires -Version 5.1
2
+ # Live folders (opt-in alpha, docs/design/hcs-live-folders.md): adds or removes one folder share on the running Windows
3
+ # Sandbox through HcsHost.exe wsb-share. Run elevated (host/engines/hcs/live-folders.ts starts it through
4
+ # run-elevated.ps1: one Windows permission prompt per add or remove). The request is a JSON file in a private temp
5
+ # folder: { action, id, name, path, readOnly, storageRoot, dataPath, userSid }; its SHA-256 is on the command line, and
6
+ # HcsHost hashes the exact bytes it parses again.
7
+ # Safety, all checked here again: the temp folder is the user's and not a link; HcsHost.exe is compiled here, from the
8
+ # sources next to this script (copied first), into a fresh folder under %ProgramData%\AgentSandboxLiveFolders that only
9
+ # administrators and SYSTEM may use (checked, and repaired when someone else made it, on every run), never taken from
10
+ # the caller; one add or remove at a time (a machine-wide lock). HcsHost does the rest (the caller must be the account
11
+ # that answered the prompt, which compute system, which folder, the canary). Work folders left by an earlier run (a
12
+ # canary the sandbox kept busy) are swept. -DryRun checks the request and prints what it would do, and does nothing
13
+ # else. The result goes to result.json next to the request file.
14
+ param(
15
+ [Parameter(Mandatory = $true)][string]$Json,
16
+ [string]$Sha256 = '',
17
+ [switch]$DryRun
18
+ )
19
+ $ErrorActionPreference = 'Stop'
20
+ Set-StrictMode -Version 2.0
21
+ . (Join-Path $PSScriptRoot 'contained-common.ps1')
22
+
23
+ $result = @{ ok = $false; message = ''; id = ''; canary = '' }
24
+ $AdminSids = @('S-1-5-18', 'S-1-5-32-544')
25
+
26
+ function Assert-LiveRequest($Req) {
27
+ foreach ($name in @('action', 'name', 'userSid')) {
28
+ if (-not ($Req.PSObject.Properties.Name -contains $name) -or -not $Req.$name) { throw "The request has no $name." }
29
+ }
30
+ if (@('add', 'remove') -notcontains [string]$Req.action) { throw 'The action must be add or remove.' }
31
+ if ([string]$Req.name -cnotmatch '^agentsbx-[0-9a-f]{8,32}$') { throw "Not one of this tool's share names: $($Req.name)" }
32
+ if ([string]$Req.userSid -notmatch '^S-1-[0-9-]{3,120}$') { throw 'The request has no valid user.' }
33
+ if ($Req.PSObject.Properties.Name -contains 'id' -and $Req.id -and ([string]$Req.id -notmatch '^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$')) {
34
+ throw 'The sandbox id must be a GUID.'
35
+ }
36
+ if ($Req.action -eq 'remove' -and -not $Req.id) { throw 'A remove needs the id of the sandbox the folder was added to.' }
37
+ if ($Req.action -eq 'add') {
38
+ if (-not ($Req.PSObject.Properties.Name -contains 'path') -or -not (Test-PlainAbsolute $Req.path)) { throw 'The request has no plain full path to share.' }
39
+ if (-not ($Req.PSObject.Properties.Name -contains 'readOnly') -or -not ($Req.readOnly -is [bool])) { throw 'readOnly must be true or false.' }
40
+ foreach ($name in @('storageRoot', 'dataPath')) {
41
+ if (-not ($Req.PSObject.Properties.Name -contains $name) -or -not (Test-PlainAbsolute $Req.$name)) { throw "The request has no plain full $name." }
42
+ }
43
+ }
44
+ }
45
+
46
+ # The request's own folder: a private temp folder of the user who asked, not a link.
47
+ function Assert-PrivateTemp($Req, [string]$Dir) {
48
+ if (Test-ReparsePoint $Dir) { throw "Refusing a link: $Dir" }
49
+ $acl = Get-Acl -LiteralPath $Dir
50
+ $owner = $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value
51
+ if (@($Req.userSid, 'S-1-5-32-544') -notcontains $owner) { throw "The request folder is not yours (owner $owner)." }
52
+ foreach ($rule in $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])) {
53
+ if ($rule.AccessControlType -ne 'Allow') { continue }
54
+ if (@('S-1-1-0', 'S-1-5-11', 'S-1-5-32-545') -contains $rule.IdentityReference.Value) { throw 'The request folder is open to other users.' }
55
+ }
56
+ }
57
+
58
+ function New-AdminSecurity {
59
+ $acl = New-Object Security.AccessControl.DirectorySecurity
60
+ $acl.SetOwner((New-Object Security.Principal.SecurityIdentifier('S-1-5-32-544')))
61
+ $acl.SetAccessRuleProtection($true, $false)
62
+ $inherit = [Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'
63
+ foreach ($sid in $AdminSids) {
64
+ $who = New-Object Security.Principal.SecurityIdentifier($sid)
65
+ $acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule($who, 'FullControl', $inherit, 'None', 'Allow')))
66
+ }
67
+ return $acl
68
+ }
69
+
70
+ function Test-AdminOnly([string]$Path) {
71
+ $acl = Get-Acl -LiteralPath $Path
72
+ if ($AdminSids -notcontains $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value) { return $false }
73
+ if (-not $acl.AreAccessRulesProtected) { return $false }
74
+ foreach ($rule in $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])) {
75
+ if ($rule.AccessControlType -eq 'Allow' -and $AdminSids -notcontains $rule.IdentityReference.Value) { return $false }
76
+ }
77
+ return $true
78
+ }
79
+
80
+ # %ProgramData%\AgentSandboxLiveFolders: made with an administrator-only DACL. One that someone else made first (any
81
+ # user may create folders in ProgramData) is taken over: owner Administrators and the DACL written whole; its old
82
+ # content stays, unused (each run works in a new random folder made with its own DACL).
83
+ function Get-AdminParent {
84
+ $base = [Environment]::GetFolderPath('CommonApplicationData')
85
+ if (-not $base -or (Test-ReparsePoint $base)) { throw "ProgramData is missing or a link: $base" }
86
+ $parent = Join-Path $base 'AgentSandboxLiveFolders'
87
+ if (-not (Test-Path -LiteralPath $parent)) {
88
+ [void][IO.Directory]::CreateDirectory($parent, (New-AdminSecurity))
89
+ }
90
+ if (Test-ReparsePoint $parent) { throw "Refusing a link: $parent" }
91
+ if (-not (Test-AdminOnly $parent)) {
92
+ Add-Note "taking over $parent (someone else made it or may use it)"
93
+ Set-Acl -LiteralPath $parent -AclObject (New-AdminSecurity)
94
+ if ((Test-ReparsePoint $parent) -or -not (Test-AdminOnly $parent)) { throw "$parent could not be made administrator-only." }
95
+ }
96
+ # Work folders an earlier run could not delete (the sandbox kept its canary busy): gone after an hour.
97
+ foreach ($old in @(Get-ChildItem -LiteralPath $parent -Force -Directory -ErrorAction SilentlyContinue)) {
98
+ if ($old.Name -match '^[0-9a-f]{32}$' -and -not ($old.Attributes -band [IO.FileAttributes]::ReparsePoint) -and $old.CreationTimeUtc -lt [DateTime]::UtcNow.AddHours(-1)) {
99
+ Remove-Item -LiteralPath $old.FullName -Recurse -Force -ErrorAction SilentlyContinue
100
+ }
101
+ }
102
+ return $parent
103
+ }
104
+
105
+ # HcsHost.exe built here from this package's sources (copied into the admin-only work folder first), with the
106
+ # compiler that ships with Windows: nothing the caller names is ever run elevated.
107
+ function Build-HcsHost([string]$Work) {
108
+ $sources = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\csharp\HcsHost'))
109
+ if (-not (Test-Path -LiteralPath $sources -PathType Container) -or (Test-ReparsePoint $sources)) { throw "The HcsHost sources are missing or a link: $sources" }
110
+ $copy = Join-Path $Work 'src'
111
+ [void][IO.Directory]::CreateDirectory($copy)
112
+ foreach ($file in @(Get-ChildItem -LiteralPath $sources -Filter '*.cs' -File)) {
113
+ if ($file.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw "Refusing a link: $($file.FullName)" }
114
+ [IO.File]::Copy($file.FullName, (Join-Path $copy $file.Name), $false)
115
+ }
116
+ $csc = Join-Path ([Environment]::GetFolderPath('Windows')) 'Microsoft.NET\Framework64\v4.0.30319\csc.exe'
117
+ $exe = Join-Path $Work 'HcsHost.exe'
118
+ $files = @(Get-ChildItem -LiteralPath $copy -Filter '*.cs' -File | ForEach-Object { $_.FullName })
119
+ if ($files.Count -eq 0) { throw 'No HcsHost sources were found.' }
120
+ & $csc '/nologo' '/optimize' '/target:exe' '/r:System.Web.Extensions.dll' ('/out:' + $exe) @files | Out-Null
121
+ if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) { throw 'HcsHost.exe could not be built (the .NET Framework compiler that ships with Windows is needed).' }
122
+ return $exe
123
+ }
124
+
125
+ function Invoke-Live($Req, [string]$JsonPath, [string]$Checksum) {
126
+ $parent = Get-AdminParent
127
+ $work = Join-Path $parent ([Guid]::NewGuid().ToString('N'))
128
+ if (Test-Path -LiteralPath $work) { throw "The work folder exists already: $work" }
129
+ [void][IO.Directory]::CreateDirectory($work, (New-AdminSecurity))
130
+ try {
131
+ $exe = Build-HcsHost $work
132
+ $out = Join-Path $work 'reply.json'
133
+ & $exe 'wsb-share' $JsonPath '--sha256' $Checksum '--out' $out | Out-Null
134
+ if (-not (Test-Path -LiteralPath $out)) { throw 'HcsHost.exe gave no answer.' }
135
+ $bytes = [IO.File]::ReadAllBytes($out)
136
+ if ($bytes.Length -gt 65536) { throw 'HcsHost.exe answered with too much text.' }
137
+ $reply = [Text.Encoding]::UTF8.GetString($bytes) | ConvertFrom-Json
138
+ $script:result.ok = [bool]$reply.ok
139
+ $script:result.message = [string]$reply.message
140
+ $names = @($reply.values.PSObject.Properties | ForEach-Object { $_.Name })
141
+ if ($names -contains 'id') { $script:result.id = [string]$reply.values.id }
142
+ if ($names -contains 'canary') { $script:result.canary = [string]$reply.values.canary }
143
+ } finally {
144
+ if (-not (Test-ReparsePoint $work)) { Remove-Item -LiteralPath $work -Recurse -Force -ErrorAction SilentlyContinue }
145
+ }
146
+ }
147
+
148
+ # result.json in the caller's folder: checked for a link right before, and only made as a new file.
149
+ function Write-LiveResult([hashtable]$Result, [string]$JsonPath) {
150
+ $Result.notes = @(Get-Notes)
151
+ $dir = Split-Path -Parent $JsonPath
152
+ if (Test-ReparsePoint $dir) { return }
153
+ $bytes = (New-Object Text.UTF8Encoding($false)).GetBytes(($Result | ConvertTo-Json -Compress))
154
+ $stream = New-Object IO.FileStream((Join-Path $dir 'result.json'), [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
155
+ try { $stream.Write($bytes, 0, $bytes.Length) } finally { $stream.Dispose() }
156
+ }
157
+
158
+ $mutex = $null
159
+ $owned = $false
160
+ try {
161
+ $request = Read-ContainedRequest -JsonPath $Json -Sha256 $Sha256 -Dry $DryRun.IsPresent
162
+ Assert-LiveRequest $request
163
+ if ($DryRun) {
164
+ Add-Note ("would {0} the share {1}{2} on the running Windows Sandbox{3}" -f $request.action, $request.name, $(if ($request.action -eq 'add') { ' (' + $request.path + ', ' + $(if ($request.readOnly) { 'read-only' } else { 'writable' }) + ')' } else { '' }), $(if ($request.id) { ' ' + $request.id } else { '' }))
165
+ $result.ok = $true
166
+ $result.message = 'checked'
167
+ } else {
168
+ if (-not (Test-IsAdministrator)) { throw 'Adding a folder to the running sandbox needs administrator rights.' }
169
+ Assert-PrivateTemp $request (Split-Path -Parent $Json)
170
+ $mutex = New-Object System.Threading.Mutex($false, 'Global\AgentSandboxLiveFolders')
171
+ try { $owned = $mutex.WaitOne(120000) } catch [System.Threading.AbandonedMutexException] { $owned = $true }
172
+ if (-not $owned) { throw 'Another live folder is being added or removed (maybe by another user of this PC). Try again in a few minutes.' }
173
+ Invoke-Live $request $Json $Sha256
174
+ }
175
+ } catch {
176
+ $result.ok = $false
177
+ $result.message = $_.Exception.Message
178
+ } finally {
179
+ if ($mutex) {
180
+ if ($owned) { $mutex.ReleaseMutex() }
181
+ $mutex.Dispose()
182
+ }
183
+ }
184
+ if ($DryRun) {
185
+ Write-ContainedResult $result $Json $true
186
+ } else {
187
+ try { Write-LiveResult $result $Json } catch { [Console]::Error.WriteLine('The result could not be written: ' + $_.Exception.Message) }
188
+ }
189
+ if ($result.ok) { exit 0 }
190
+ if ($DryRun) { [Console]::Error.WriteLine($result.message) }
191
+ exit 1
@@ -0,0 +1,203 @@
1
+ #Requires -Version 5.1
2
+ # Installs (or, with -Remove, removes) the storage helper: the Windows service AgentSandboxStorage, which mounts and
3
+ # dismounts a user's contained-storage disk image without a Windows permission prompt (native\csharp\StorageHelper).
4
+ # Run by scripts\install.ps1 for an install for everyone only, in its admin PowerShell.
5
+ # -Install -GroupSid <sid> -InstallRoot <Program Files install folder> -Sources <...\native\csharp\StorageHelper>
6
+ # builds AgentSandboxStorage.exe with the csc that ships with Windows, from sources inside the install folder, into
7
+ # <InstallRoot>\helper (nothing a normal user can write is used), makes the machine folders logs\ and storage-helper\
8
+ # (administrators only), and creates or updates the service: LocalSystem, automatic start (it must own its pipe before
9
+ # anyone signs in), restart on failure, only the privileges it needs. The service is stopped while its file changes.
10
+ # -Remove [-InstallRoot <...>]
11
+ # stops the service, dismounts every image it mounted (AgentSandboxStorage.exe release-all), deletes the service and
12
+ # <InstallRoot>\helper. The log stays (it goes with the machine folder when saved data is removed).
13
+ # -DryRun prints what it would do (STEP: lines) after checking the arguments, and changes nothing (no admin needed).
14
+ # Exit 0 when done, 2 when not admin, 1 with the reason on stderr otherwise.
15
+ [CmdletBinding(DefaultParameterSetName = 'Install')]
16
+ param(
17
+ [Parameter(Mandatory = $true, ParameterSetName = 'Install')][switch]$Install,
18
+ [Parameter(Mandatory = $true, ParameterSetName = 'Remove')][switch]$Remove,
19
+ [Parameter(ParameterSetName = 'Install')][string]$GroupSid,
20
+ [Parameter(ParameterSetName = 'Install')][string]$Sources,
21
+ [string]$InstallRoot,
22
+ [ValidatePattern('^[A-Za-z][A-Za-z0-9-]{2,63}$')][string]$ServiceName = 'AgentSandboxStorage',
23
+ [string]$MachineData,
24
+ [switch]$DryRun
25
+ )
26
+ $ErrorActionPreference = 'Stop'
27
+ $exeName = 'AgentSandboxStorage.exe'
28
+ $privileges = 'SeChangeNotifyPrivilege/SeManageVolumePrivilege/SeBackupPrivilege/SeRestorePrivilege/SeImpersonatePrivilege'
29
+ $sc = Join-Path $env:SystemRoot 'System32\sc.exe'
30
+ $csc = Join-Path $env:SystemRoot 'Microsoft.NET\Framework64\v4.0.30319\csc.exe'
31
+
32
+ function Write-Step([string]$Text) {
33
+ if ($DryRun) { Write-Output ('STEP: ' + $Text) }
34
+ }
35
+
36
+ function Test-Inside([string]$Path, [string]$Root) {
37
+ $full = [IO.Path]::GetFullPath($Path).TrimEnd('\')
38
+ $base = [IO.Path]::GetFullPath($Root).TrimEnd('\')
39
+ return $full.StartsWith($base + '\', [StringComparison]::OrdinalIgnoreCase)
40
+ }
41
+
42
+ function Assert-NotLink([string]$Path) {
43
+ if ((Test-Path -LiteralPath $Path) -and ((Get-Item -LiteralPath $Path -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) {
44
+ throw "Refusing a link: $Path"
45
+ }
46
+ }
47
+
48
+ # A machine folder only administrators may change: made when missing, its owner and permissions written whole.
49
+ function Set-AdminFolder([string]$Path) {
50
+ Write-Step "admin-only folder $Path"
51
+ if ($DryRun) { return }
52
+ Assert-NotLink (Split-Path -Parent $Path)
53
+ Assert-NotLink $Path
54
+ New-Item -ItemType Directory -Force $Path | Out-Null
55
+ Assert-NotLink $Path
56
+ $acl = New-Object Security.AccessControl.DirectorySecurity
57
+ $acl.SetOwner((New-Object Security.Principal.SecurityIdentifier('S-1-5-32-544')))
58
+ $acl.SetAccessRuleProtection($true, $false)
59
+ $inherit = [Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit'
60
+ foreach ($sid in @('S-1-5-18', 'S-1-5-32-544')) {
61
+ $who = New-Object Security.Principal.SecurityIdentifier($sid)
62
+ $acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule($who, 'FullControl', $inherit, 'None', 'Allow')))
63
+ }
64
+ Set-Acl -LiteralPath $Path -AclObject $acl
65
+ }
66
+
67
+ # The machine folder itself must be one that only administrators may change (install.ps1 sets that up first): else a
68
+ # user could swap logs\ or storage-helper\ between the checks above and the permissions being written.
69
+ function Assert-AdminOnlyMachineFolder([string]$Path) {
70
+ Write-Step "check that only administrators may change $Path"
71
+ if ($DryRun) { return }
72
+ if (-not (Test-Path -LiteralPath $Path -PathType Container)) { throw "The machine folder is missing: $Path (run the installer for everyone first)." }
73
+ Assert-NotLink $Path
74
+ $acl = Get-Acl -LiteralPath $Path
75
+ $admins = @('S-1-5-18', 'S-1-5-32-544', 'S-1-3-0', 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464')
76
+ $owner = $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value
77
+ if ($admins -notcontains $owner) { throw "$Path belongs to $owner, not to administrators; run the installer for everyone again." }
78
+ $writeBits = [int]([Security.AccessControl.FileSystemRights]'WriteData, AppendData, WriteExtendedAttributes, DeleteSubdirectoriesAndFiles, WriteAttributes, Delete, ChangePermissions, TakeOwnership')
79
+ foreach ($rule in $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])) {
80
+ if ($rule.AccessControlType -ne 'Allow') { continue }
81
+ $rights = [int]$rule.FileSystemRights
82
+ if (($rights -band $writeBits) -eq 0 -and ($rights -band 0x50000000) -eq 0) { continue }
83
+ if ($admins -notcontains $rule.IdentityReference.Value) { throw "$($rule.IdentityReference.Value) may change $Path; run the installer for everyone again." }
84
+ }
85
+ }
86
+
87
+ function Wait-Stopped([string]$Name) {
88
+ $running = Get-Service -Name $Name -ErrorAction SilentlyContinue
89
+ if (-not $running -or $running.Status -eq 'Stopped') { return }
90
+ Stop-Service -Name $Name -Force -ErrorAction SilentlyContinue
91
+ $running.WaitForStatus('Stopped', (New-TimeSpan -Seconds 60))
92
+ }
93
+
94
+ function Invoke-Sc([string[]]$Arguments) {
95
+ $output = & $sc @Arguments 2>&1 | Out-String
96
+ if ($LASTEXITCODE -ne 0) { throw "sc.exe $($Arguments[0]) failed (exit $LASTEXITCODE): $($output.Trim())" }
97
+ }
98
+
99
+ try {
100
+ if (-not $DryRun) {
101
+ $principalCheck = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
102
+ if (-not $principalCheck.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
103
+ [Console]::Error.WriteLine('This needs admin rights. Run it in an admin PowerShell (right-click Start > Terminal (Admin)).')
104
+ exit 2
105
+ }
106
+ }
107
+ if (-not $MachineData) { $MachineData = Join-Path $(if ($env:ProgramData) { $env:ProgramData } else { 'C:\ProgramData' }) 'AgentSandbox' }
108
+ if (-not $InstallRoot) { $InstallRoot = Join-Path $env:ProgramFiles 'WindowsDevelopmentSandbox' }
109
+ # A dry run (the self-test) may name a temp folder; a real run only the Program Files install folder.
110
+ if (-not [IO.Path]::IsPathRooted($InstallRoot) -or (-not $DryRun -and -not (Test-Inside $InstallRoot $env:ProgramFiles))) {
111
+ throw "-InstallRoot must be a folder inside $env:ProgramFiles (normal users cannot write there): $InstallRoot"
112
+ }
113
+ $helperDir = Join-Path $InstallRoot 'helper'
114
+ $exe = Join-Path $helperDir $exeName
115
+
116
+ if ($Remove) {
117
+ $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
118
+ if ($service) {
119
+ Write-Step "stop the service $ServiceName"
120
+ if (-not $DryRun) { Wait-Stopped $ServiceName }
121
+ if (Test-Path -LiteralPath $exe) {
122
+ Write-Step 'dismount every image the helper mounted (release-all)'
123
+ if (-not $DryRun) {
124
+ $released = & $exe release-all 2>&1 | Out-String
125
+ if ($released.Trim()) { Write-Output $released.Trim() }
126
+ if ($LASTEXITCODE -ne 0) {
127
+ Write-Output 'WARNING: some images are still mounted (a sandbox may be running). Close it and run sandbox repair, which dismounts them (Windows asks for permission).'
128
+ }
129
+ }
130
+ }
131
+ Write-Step "delete the service $ServiceName"
132
+ if (-not $DryRun) { Invoke-Sc @('delete', $ServiceName) }
133
+ }
134
+ Write-Step "remove $helperDir"
135
+ if (-not $DryRun -and (Test-Path -LiteralPath $helperDir)) {
136
+ Assert-NotLink $helperDir
137
+ Remove-Item -LiteralPath $helperDir -Recurse -Force -ErrorAction SilentlyContinue
138
+ if (Test-Path -LiteralPath $helperDir) { Write-Output "WARNING: $helperDir is still there (the service may still be ending); delete it later." }
139
+ }
140
+ exit 0
141
+ }
142
+
143
+ if ($GroupSid -notmatch '^S-1-5-21(-\d{1,10}){4}$') { throw "-GroupSid must be the SID of the local group Agent Sandbox Users (got '$GroupSid')." }
144
+ if (-not $Sources -or -not (Test-Path -LiteralPath $Sources -PathType Container) -or -not (Test-Inside $Sources $InstallRoot)) {
145
+ throw "-Sources must be the StorageHelper source folder inside $InstallRoot (got '$Sources')."
146
+ }
147
+ $files = @(Get-ChildItem -LiteralPath $Sources -Filter '*.cs' -File | Sort-Object Name | ForEach-Object { $_.FullName })
148
+ if ($files.Count -eq 0) { throw "No sources in $Sources." }
149
+ if (-not (Test-Path -LiteralPath $csc)) { throw "The C# compiler that ships with Windows is missing: $csc" }
150
+ foreach ($p in @($InstallRoot, $helperDir, $Sources)) { Assert-NotLink $p }
151
+ Assert-AdminOnlyMachineFolder $MachineData
152
+
153
+ $build = Join-Path $InstallRoot ('helper-build-' + [guid]::NewGuid().ToString('N'))
154
+ Write-Step "build $exeName from $($files.Count) sources in $build"
155
+ if (-not $DryRun) {
156
+ New-Item -ItemType Directory -Force $build | Out-Null
157
+ try {
158
+ $built = Join-Path $build $exeName
159
+ $compiled = & $csc /nologo /optimize /target:exe /r:System.Web.Extensions.dll /r:System.ServiceProcess.dll "/out:$built" @files 2>&1 | Out-String
160
+ if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $built)) { throw "The storage helper did not build: $($compiled.Trim())" }
161
+ Write-Step "stop the service $ServiceName while its file is replaced"
162
+ Wait-Stopped $ServiceName
163
+ New-Item -ItemType Directory -Force $helperDir | Out-Null
164
+ Assert-NotLink $helperDir
165
+ Move-Item -LiteralPath $built -Destination $exe -Force
166
+ } finally {
167
+ if (Test-Inside $build $InstallRoot) { Remove-Item -LiteralPath $build -Recurse -Force -ErrorAction SilentlyContinue }
168
+ }
169
+ }
170
+
171
+ Set-AdminFolder (Join-Path $MachineData 'logs')
172
+ Set-AdminFolder (Join-Path $MachineData 'storage-helper')
173
+
174
+ $binaryPath = '"' + $exe + '" service --group ' + $GroupSid
175
+ $existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
176
+ if ($existing) {
177
+ Write-Step "update the service $ServiceName to run $binaryPath"
178
+ if (-not $DryRun) {
179
+ Set-ItemProperty -LiteralPath "HKLM:\SYSTEM\CurrentControlSet\Services\$ServiceName" -Name ImagePath -Value $binaryPath
180
+ Invoke-Sc @('config', $ServiceName, 'start=', 'auto', 'obj=', 'LocalSystem')
181
+ }
182
+ } else {
183
+ Write-Step "create the service $ServiceName (LocalSystem, automatic start) running $binaryPath"
184
+ if (-not $DryRun) {
185
+ New-Service -Name $ServiceName -BinaryPathName $binaryPath -DisplayName 'Agent Sandbox storage helper' -Description 'Mounts the contained-storage disk image of Windows Development Sandbox users without a permission prompt. Removed by uninstalling Windows Development Sandbox.' -StartupType Automatic | Out-Null
186
+ }
187
+ }
188
+ Write-Step "restart on failure, privileges $privileges, unrestricted service SID"
189
+ if (-not $DryRun) {
190
+ Invoke-Sc @('failure', $ServiceName, 'reset=', '86400', 'actions=', 'restart/5000/restart/60000//')
191
+ Invoke-Sc @('privs', $ServiceName, $privileges)
192
+ Invoke-Sc @('sidtype', $ServiceName, 'unrestricted')
193
+ }
194
+ Write-Step "start the service $ServiceName"
195
+ if (-not $DryRun) {
196
+ Start-Service -Name $ServiceName
197
+ (Get-Service -Name $ServiceName).WaitForStatus('Running', (New-TimeSpan -Seconds 30))
198
+ }
199
+ exit 0
200
+ } catch {
201
+ [Console]::Error.WriteLine($_.Exception.Message)
202
+ exit 1
203
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windows-development-sandbox",
3
- "version": "1.13.2",
3
+ "version": "1.15.0",
4
4
  "description": "A disposable Windows Sandbox for development, with or without an AI agent (Claude Code, Codex, Gemini, opencode, ...): the folder you run it from is the workspace, toolchains and databases for your project install themselves, and nothing else on your PC is reachable.",
5
5
  "type": "module",
6
6
  "license": "MIT",