windows-development-sandbox 1.13.2 → 1.14.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.
- package/dist/app/features/storage/storage.run.js +1 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-VENWSGX2.js → chunk-BQ23GBLC.js} +411 -58
- package/dist/bin/{chunk-K4SU3MHJ.js → chunk-F7IMME5G.js} +1 -1
- package/dist/bin/{chunk-IL7BXJF2.js → chunk-LQHEUN2A.js} +1 -1
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/cli/commands/launch/launch.js +1 -1
- package/dist/cli/commands/repair/checks/contained.js +1 -1
- package/dist/guest/daemon/sessions.d.ts +7 -0
- package/dist/guest/daemon/sessions.js +23 -2
- package/dist/guest/entries/daemon.js +1 -0
- package/dist/guest/sessions/pty-host.d.ts +10 -0
- package/dist/guest/sessions/pty-host.js +48 -3
- package/dist/host/channel/channel.js +1 -1
- package/dist/host/install/install-record.d.ts +1 -0
- package/dist/host/install/install-record.js +2 -0
- package/dist/host/sandbox/contained-deps.d.ts +2 -1
- package/dist/host/sandbox/contained-deps.js +6 -4
- package/dist/host/sharing/lifecycle.js +9 -1
- package/dist/host/storage/contained/contained-storage.js +13 -0
- package/dist/host/storage/contained/disk-port.d.ts +5 -0
- package/dist/host/storage/contained/helper-port.d.ts +72 -0
- package/dist/host/storage/contained/helper-port.js +162 -0
- package/dist/host/storage/contained/index.d.ts +1 -0
- package/dist/host/storage/contained/index.js +1 -0
- package/dist/host/web/bridge.d.ts +5 -0
- package/dist/host/web/bridge.js +11 -1
- package/dist/host/web/server.d.ts +2 -0
- package/dist/host/web/server.js +11 -1
- package/dist/runtime/agent.js +9 -0
- package/dist/runtime/daemon.js +57 -3
- package/dist/runtime/db.js +9 -0
- package/dist/runtime/main.js +195 -12
- package/dist/runtime/netproxy.js +9 -0
- package/dist/runtime/view.js +195 -12
- package/dist/selftest/channel.test.js +72 -1
- package/dist/selftest/contained.test.js +1 -37
- package/dist/selftest/hcs.test.js +5 -2
- package/dist/selftest/index.js +2 -0
- package/dist/selftest/install.test.js +11 -0
- package/dist/selftest/live-daemon.test.js +51 -1
- package/dist/selftest/live-terminal.test.js +112 -4
- package/dist/selftest/storage-helper.test.d.ts +2 -0
- package/dist/selftest/storage-helper.test.js +493 -0
- package/dist/selftest/support/vhdx.d.ts +14 -0
- package/dist/selftest/support/vhdx.js +72 -0
- package/dist/selftest/web.test.js +14 -2
- package/dist/shared/channel/run-dir.d.ts +6 -0
- package/dist/shared/channel/run-dir.js +9 -0
- package/dist/shared/pty/index.d.ts +3 -1
- package/dist/shared/pty/index.js +3 -1
- package/dist/shared/pty/key-gate.d.ts +24 -0
- package/dist/shared/pty/key-gate.js +45 -0
- package/dist/shared/pty/mouse-modes.d.ts +27 -0
- package/dist/shared/pty/mouse-modes.js +155 -0
- package/dist/shared/pty/terminal.d.ts +0 -5
- package/dist/shared/pty/terminal.js +51 -20
- package/dist/sync/watcher.js +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/{main-NBQ3LF4E.js → main-6XNBTJLT.js} +33 -33
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
- package/native/csharp/StorageHelper/Client.cs +297 -0
- package/native/csharp/StorageHelper/Log.cs +177 -0
- package/native/csharp/StorageHelper/MountRecords.cs +160 -0
- package/native/csharp/StorageHelper/Operations.cs +596 -0
- package/native/csharp/StorageHelper/Pins.cs +270 -0
- package/native/csharp/StorageHelper/Privileges.cs +78 -0
- package/native/csharp/StorageHelper/Program.cs +349 -0
- package/native/csharp/StorageHelper/Protocol.cs +107 -0
- package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
- package/native/csharp/StorageHelper/Rules.cs +156 -0
- package/native/csharp/StorageHelper/Server.cs +656 -0
- package/native/csharp/StorageHelper/Settings.cs +297 -0
- package/native/csharp/StorageHelper/StorageService.cs +53 -0
- package/native/csharp/StorageHelper/UserSettings.cs +206 -0
- package/native/csharp/StorageHelper/Vhdx.cs +230 -0
- package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
- package/native/csharp/StorageHelper/Volumes.cs +379 -0
- package/native/csharp/StorageHelper/Watchdog.cs +128 -0
- package/native/csharp/StorageHelper/Win32.cs +250 -0
- package/native/scripts/set-storage-helper.ps1 +203 -0
- package/package.json +1 -1
- package/sandbox/csharp/PtyHost.cs +236 -1
|
@@ -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
|
+
}
|
|
@@ -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.
|
|
3
|
+
"version": "1.14.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",
|