windows-development-sandbox 1.14.0 → 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 (90) 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/bin/agent-sandbox.js +2 -2
  9. package/dist/bin/{chunk-BQ23GBLC.js → chunk-D7CM63BS.js} +1893 -1405
  10. package/dist/bin/{chunk-F7IMME5G.js → chunk-HTQT5SBY.js} +1 -1
  11. package/dist/bin/{chunk-LQHEUN2A.js → chunk-RQ4BWBYX.js} +1 -1
  12. package/dist/bin/claude-host.js +2 -2
  13. package/dist/bin/claude-sbx.js +2 -2
  14. package/dist/bin/codex-sbx.js +2 -2
  15. package/dist/bin/gemini-sbx.js +2 -2
  16. package/dist/bin/sandbox.js +2 -2
  17. package/dist/bin/sbx-hide.js +2 -2
  18. package/dist/bin/sbx-show.js +2 -2
  19. package/dist/cli/commands/projects/here.js +2 -0
  20. package/dist/cli/services/health-service.js +15 -3
  21. package/dist/cli/services/projects-service.d.ts +8 -3
  22. package/dist/cli/services/projects-service.js +21 -4
  23. package/dist/core/config/defaults.js +1 -0
  24. package/dist/core/schema/config.d.ts +4 -0
  25. package/dist/core/schema/projects.d.ts +4 -0
  26. package/dist/core/schema/projects.js +3 -0
  27. package/dist/guest/boot/boot.js +8 -2
  28. package/dist/guest/daemon/daemon.js +3 -0
  29. package/dist/guest/daemon/live-folders.d.ts +48 -0
  30. package/dist/guest/daemon/live-folders.js +245 -0
  31. package/dist/guest/daemon/live.d.ts +4 -0
  32. package/dist/guest/daemon/live.js +57 -0
  33. package/dist/guest/entries/daemon.js +4 -2
  34. package/dist/guest/runtime/csc.d.ts +1 -0
  35. package/dist/guest/runtime/csc.js +1 -0
  36. package/dist/guest/runtime/secrets.d.ts +5 -2
  37. package/dist/guest/runtime/secrets.js +12 -5
  38. package/dist/host/engines/hcs/live-folders.d.ts +117 -0
  39. package/dist/host/engines/hcs/live-folders.js +347 -0
  40. package/dist/host/engines/index.d.ts +1 -0
  41. package/dist/host/engines/index.js +1 -0
  42. package/dist/host/launch/generate.js +8 -1
  43. package/dist/host/projects/join.d.ts +4 -0
  44. package/dist/host/projects/join.js +11 -3
  45. package/dist/host/projects/mapper.d.ts +34 -5
  46. package/dist/host/projects/mapper.js +88 -20
  47. package/dist/host/projects/registry.d.ts +2 -0
  48. package/dist/host/projects/registry.js +1 -1
  49. package/dist/host/secrets/launch-secrets.js +2 -1
  50. package/dist/runtime/agent.js +14 -0
  51. package/dist/runtime/daemon.js +466 -132
  52. package/dist/runtime/db.js +14 -0
  53. package/dist/runtime/main.js +26 -3
  54. package/dist/runtime/netproxy.js +14 -0
  55. package/dist/runtime/view.js +14 -0
  56. package/dist/selftest/index.js +2 -0
  57. package/dist/selftest/layout.test.js +1 -0
  58. package/dist/selftest/live-folders.test.d.ts +2 -0
  59. package/dist/selftest/live-folders.test.js +588 -0
  60. package/dist/selftest/projects.test.js +7 -7
  61. package/dist/selftest/support/engine-golden.js +1 -0
  62. package/dist/selftest/support/fixtures.d.ts +1 -0
  63. package/dist/selftest/support/live.js +2 -0
  64. package/dist/shared/channel/index.d.ts +1 -0
  65. package/dist/shared/channel/index.js +1 -0
  66. package/dist/shared/channel/live.d.ts +53 -0
  67. package/dist/shared/channel/live.js +92 -0
  68. package/dist/shared/channel/run-dir.d.ts +5 -0
  69. package/dist/shared/channel/run-dir.js +11 -0
  70. package/dist/shared/channel/secrets.d.ts +1 -0
  71. package/dist/shared/channel/secrets.js +5 -0
  72. package/dist/shared/paths/projects.d.ts +6 -1
  73. package/dist/shared/paths/projects.js +21 -1
  74. package/dist/sync/watcher.js +9 -0
  75. package/dist/web/index.html +1 -1
  76. package/dist/web/{main-6XNBTJLT.js → main-7DJZ2M27.js} +1 -1
  77. package/native/csharp/HcsHost/AssemblyInfo.cs +4 -4
  78. package/native/csharp/HcsHost/FolderAccess.cs +361 -0
  79. package/native/csharp/HcsHost/Hcs.cs +31 -3
  80. package/native/csharp/HcsHost/Program.cs +54 -6
  81. package/native/csharp/HcsHost/Share.cs +64 -11
  82. package/native/csharp/HcsHost/WsbGuard.cs +130 -0
  83. package/native/csharp/HcsHost/WsbShare.cs +295 -0
  84. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  85. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  86. package/native/scripts/contained-common.ps1 +4 -0
  87. package/native/scripts/hcs-live-share.ps1 +191 -0
  88. package/package.json +1 -1
  89. package/sandbox/csharp/BindMount.cs +120 -0
  90. package/schemas/config.schema.json +8 -0
@@ -0,0 +1,361 @@
1
+ // What wsb-share may expose, checked here in the elevated helper without relying on the unelevated caller: the plain
2
+ // long path of an existing folder on a fixed local drive (ShareRequest.FolderProblem), not on a subst drive, opened
3
+ // without following links and shared in the form Windows itself reports for that handle; never Windows, Program Files,
4
+ // ProgramData, the profile root or another user's profile, nor this tool's storage or data folder (or a folder that
5
+ // holds them); and only a folder the unelevated user can read (and write, for a writable share) themselves.
6
+ using System;
7
+ using System.ComponentModel;
8
+ using System.IO;
9
+ using System.Runtime.InteropServices;
10
+ using System.Text;
11
+
12
+ namespace AgentSandbox.HcsHost
13
+ {
14
+ public static class FolderAccess
15
+ {
16
+ private const uint FileReadAttributes = 0x80;
17
+ private const uint FileShareAll = 0x7;
18
+ /// <summary>Read and write, never delete: while the handle is held the folder cannot be renamed or replaced.</summary>
19
+ private const uint FileShareReadWrite = 0x3;
20
+ private const uint OpenExisting = 3;
21
+ private const uint FlagBackupSemantics = 0x02000000;
22
+ private const uint FlagOpenReparsePoint = 0x00200000;
23
+ private const uint AttributeDirectory = 0x10;
24
+ private const uint AttributeReparsePoint = 0x400;
25
+ private const uint TokenQuery = 0x0008;
26
+ private const uint TokenDuplicate = 0x0002;
27
+ private const int TokenLinkedTokenClass = 19;
28
+ private const int SecurityIdentification = 1;
29
+ private const uint OwnerGroupDacl = 0x1 | 0x2 | 0x4;
30
+ private const uint FileGenericRead = 0x120089;
31
+ private const uint FileGenericWrite = 0x120116;
32
+ private const uint FileGenericExecute = 0x1200A0;
33
+ private const uint FileAllAccess = 0x1F01FF;
34
+ private const uint FileTraverse = 0x20;
35
+ private static readonly IntPtr InvalidHandle = new IntPtr(-1);
36
+
37
+ [StructLayout(LayoutKind.Sequential)]
38
+ private struct GenericMapping
39
+ {
40
+ public uint GenericRead;
41
+ public uint GenericWrite;
42
+ public uint GenericExecute;
43
+ public uint GenericAll;
44
+ }
45
+
46
+ [StructLayout(LayoutKind.Sequential)]
47
+ private struct ByHandleFileInformation
48
+ {
49
+ public uint FileAttributes;
50
+ public long CreationTime;
51
+ public long LastAccessTime;
52
+ public long LastWriteTime;
53
+ public uint VolumeSerialNumber;
54
+ public uint FileSizeHigh;
55
+ public uint FileSizeLow;
56
+ public uint NumberOfLinks;
57
+ public uint FileIndexHigh;
58
+ public uint FileIndexLow;
59
+ }
60
+
61
+ private static class NativeMethods
62
+ {
63
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
64
+ public static extern IntPtr CreateFileW(string name, uint access, uint share, IntPtr security, uint disposition, uint flags, IntPtr template);
65
+
66
+ [DllImport("kernel32.dll", SetLastError = true)]
67
+ public static extern bool GetFileInformationByHandle(IntPtr file, out ByHandleFileInformation info);
68
+
69
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
70
+ public static extern uint GetFinalPathNameByHandleW(IntPtr file, StringBuilder path, uint length, uint flags);
71
+
72
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
73
+ public static extern uint QueryDosDeviceW(string device, StringBuilder target, uint length);
74
+
75
+ [DllImport("kernel32.dll", SetLastError = true)]
76
+ public static extern bool CloseHandle(IntPtr handle);
77
+
78
+ [DllImport("kernel32.dll")]
79
+ public static extern IntPtr GetCurrentProcess();
80
+
81
+ [DllImport("advapi32.dll", SetLastError = true)]
82
+ public static extern bool OpenProcessToken(IntPtr process, uint access, out IntPtr token);
83
+
84
+ [DllImport("advapi32.dll", SetLastError = true)]
85
+ public static extern bool GetTokenInformation(IntPtr token, int infoClass, out IntPtr linked, int length, out int returned);
86
+
87
+ [DllImport("advapi32.dll", SetLastError = true)]
88
+ public static extern bool DuplicateToken(IntPtr token, int level, out IntPtr duplicate);
89
+
90
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
91
+ public static extern bool GetFileSecurityW(string name, uint info, byte[] descriptor, uint length, out uint needed);
92
+
93
+ [DllImport("advapi32.dll", SetLastError = true)]
94
+ public static extern bool AccessCheck(byte[] descriptor, IntPtr token, uint desired, ref GenericMapping mapping, byte[] privileges, ref uint privilegesLength, out uint granted, out bool status);
95
+ }
96
+
97
+ /// <summary>
98
+ /// Null (with the path to share in `finalPath` and the folder still open in `held`, without delete sharing, so it
99
+ /// cannot be renamed or swapped for a link until the caller closes it after the share) when the folder may be
100
+ /// shared with Windows Sandbox; otherwise why not.
101
+ /// </summary>
102
+ public static string Problem(string path, bool readOnly, string storageRoot, string dataPath, out string finalPath, out IntPtr held)
103
+ {
104
+ finalPath = null;
105
+ held = IntPtr.Zero;
106
+ if (string.IsNullOrEmpty(storageRoot) || string.IsNullOrEmpty(dataPath))
107
+ {
108
+ return "the request must name this tool's storage and data folders (they are never shared)";
109
+ }
110
+ string problem = ShareRequest.FolderProblem(path);
111
+ if (problem != null)
112
+ {
113
+ return problem;
114
+ }
115
+ string folder = path.TrimEnd('\\');
116
+ if (IsSubst(folder.Substring(0, 2)))
117
+ {
118
+ return "the folder is on a subst drive: give its real path";
119
+ }
120
+ string real;
121
+ problem = Open(folder, out real, out held);
122
+ if (problem != null)
123
+ {
124
+ return problem;
125
+ }
126
+ problem = CheckOpened(folder, real, readOnly, storageRoot, dataPath);
127
+ if (problem != null)
128
+ {
129
+ Close(held);
130
+ held = IntPtr.Zero;
131
+ return problem;
132
+ }
133
+ finalPath = real;
134
+ return null;
135
+ }
136
+
137
+ private static string CheckOpened(string folder, string real, bool readOnly, string storageRoot, string dataPath)
138
+ {
139
+ string problem;
140
+ if (!string.Equals(real, folder, StringComparison.OrdinalIgnoreCase))
141
+ {
142
+ return "the folder's real path is " + real + ": give that one";
143
+ }
144
+ problem = DeniedProblem(real, storageRoot, dataPath);
145
+ if (problem != null)
146
+ {
147
+ return problem;
148
+ }
149
+ return UserAccessProblem(real, readOnly);
150
+ }
151
+
152
+ public static void Close(IntPtr held)
153
+ {
154
+ if (held != IntPtr.Zero && held != InvalidHandle)
155
+ {
156
+ NativeMethods.CloseHandle(held);
157
+ }
158
+ }
159
+
160
+ /// <summary>After the share: the held folder is still at `path` (null), or why not.</summary>
161
+ public static string MovedProblem(IntPtr held, string path)
162
+ {
163
+ string now;
164
+ string problem = FinalPath(held, out now);
165
+ if (problem != null)
166
+ {
167
+ return problem;
168
+ }
169
+ return string.Equals(now, path, StringComparison.OrdinalIgnoreCase) ? null : "the folder moved while it was being shared";
170
+ }
171
+
172
+ private static bool IsSubst(string drive)
173
+ {
174
+ StringBuilder target = new StringBuilder(1024);
175
+ if (NativeMethods.QueryDosDeviceW(drive, target, (uint)target.Capacity) == 0)
176
+ {
177
+ return true; // unknown: treated like a subst (fail closed)
178
+ }
179
+ return target.ToString().StartsWith("\\??\\", StringComparison.Ordinal);
180
+ }
181
+
182
+ /// <summary>
183
+ /// Opens the folder itself without following a link and without delete sharing (kept open in `handle`), and
184
+ /// says the path Windows reports for it.
185
+ /// </summary>
186
+ private static string Open(string folder, out string real, out IntPtr handle)
187
+ {
188
+ real = null;
189
+ handle = NativeMethods.CreateFileW(folder, FileReadAttributes, FileShareReadWrite, IntPtr.Zero, OpenExisting, FlagBackupSemantics | FlagOpenReparsePoint, IntPtr.Zero);
190
+ if (handle == InvalidHandle)
191
+ {
192
+ handle = IntPtr.Zero;
193
+ return "the folder cannot be opened (or something is renaming it): " + new Win32Exception(Marshal.GetLastWin32Error()).Message;
194
+ }
195
+ bool keep = false;
196
+ try
197
+ {
198
+ ByHandleFileInformation info;
199
+ if (!NativeMethods.GetFileInformationByHandle(handle, out info))
200
+ {
201
+ return "the folder cannot be read: " + new Win32Exception(Marshal.GetLastWin32Error()).Message;
202
+ }
203
+ if ((info.FileAttributes & AttributeDirectory) == 0 || (info.FileAttributes & AttributeReparsePoint) != 0)
204
+ {
205
+ return "only a plain folder (not a file or a link) can be shared";
206
+ }
207
+ string problem = FinalPath(handle, out real);
208
+ keep = problem == null;
209
+ return problem;
210
+ }
211
+ finally
212
+ {
213
+ if (!keep)
214
+ {
215
+ NativeMethods.CloseHandle(handle);
216
+ handle = IntPtr.Zero;
217
+ }
218
+ }
219
+ }
220
+
221
+ private static string FinalPath(IntPtr handle, out string real)
222
+ {
223
+ real = null;
224
+ StringBuilder sb = new StringBuilder(1024);
225
+ uint length = NativeMethods.GetFinalPathNameByHandleW(handle, sb, (uint)sb.Capacity, 0);
226
+ if (length == 0 || length >= sb.Capacity)
227
+ {
228
+ return "the folder's real path cannot be read";
229
+ }
230
+ string text = sb.ToString();
231
+ {
232
+ if (!text.StartsWith("\\\\?\\", StringComparison.Ordinal) || text.StartsWith("\\\\?\\UNC\\", StringComparison.OrdinalIgnoreCase))
233
+ {
234
+ return "only folders on a local drive can be shared";
235
+ }
236
+ real = text.Substring(4).TrimEnd('\\');
237
+ return null;
238
+ }
239
+ }
240
+
241
+ private static bool Within(string parent, string child)
242
+ {
243
+ if (string.IsNullOrEmpty(parent))
244
+ {
245
+ return false;
246
+ }
247
+ string p = parent.TrimEnd('\\');
248
+ return string.Equals(p, child, StringComparison.OrdinalIgnoreCase) ||
249
+ child.StartsWith(p + "\\", StringComparison.OrdinalIgnoreCase);
250
+ }
251
+
252
+ /// <summary>Pure apart from the special folders: the folders that are never shared.</summary>
253
+ public static string DeniedProblem(string real, string storageRoot, string dataPath)
254
+ {
255
+ string[] system = new string[]
256
+ {
257
+ Environment.GetFolderPath(Environment.SpecialFolder.Windows),
258
+ Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
259
+ Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
260
+ Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
261
+ };
262
+ foreach (string s in system)
263
+ {
264
+ if (Within(s, real) || Within(real, s))
265
+ {
266
+ return "system folders cannot be shared: " + real;
267
+ }
268
+ }
269
+ string profile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile).TrimEnd('\\');
270
+ string users = Path.GetDirectoryName(profile);
271
+ if (Within(users, real) && !(Within(profile, real) && !string.Equals(profile, real, StringComparison.OrdinalIgnoreCase)))
272
+ {
273
+ return "only a folder inside your own profile can be shared from the users folder (never the profile itself or another user's)";
274
+ }
275
+ foreach (string own in new string[] { storageRoot, dataPath })
276
+ {
277
+ if (!string.IsNullOrEmpty(own) && (Within(own, real) || Within(real, own)))
278
+ {
279
+ return "this tool's own storage or data folder cannot be shared: " + real;
280
+ }
281
+ }
282
+ return null;
283
+ }
284
+
285
+ /// <summary>The token of the user who asked: the unelevated half of an elevated token, else this one.</summary>
286
+ private static IntPtr UserToken()
287
+ {
288
+ IntPtr process;
289
+ if (!NativeMethods.OpenProcessToken(NativeMethods.GetCurrentProcess(), TokenQuery | TokenDuplicate, out process))
290
+ {
291
+ throw new Win32Exception(Marshal.GetLastWin32Error());
292
+ }
293
+ try
294
+ {
295
+ IntPtr linked;
296
+ int returned;
297
+ if (NativeMethods.GetTokenInformation(process, TokenLinkedTokenClass, out linked, IntPtr.Size, out returned) && linked != IntPtr.Zero)
298
+ {
299
+ return linked;
300
+ }
301
+ IntPtr duplicate;
302
+ if (!NativeMethods.DuplicateToken(process, SecurityIdentification, out duplicate))
303
+ {
304
+ throw new Win32Exception(Marshal.GetLastWin32Error());
305
+ }
306
+ return duplicate;
307
+ }
308
+ finally
309
+ {
310
+ NativeMethods.CloseHandle(process);
311
+ }
312
+ }
313
+
314
+ /// <summary>Null when the unelevated user may list (and, for a writable share, change) the folder themselves.</summary>
315
+ public static string UserAccessProblem(string folder, bool readOnly)
316
+ {
317
+ uint needed;
318
+ NativeMethods.GetFileSecurityW(folder, OwnerGroupDacl, null, 0, out needed);
319
+ if (needed == 0 || needed > 1024 * 1024)
320
+ {
321
+ return "the folder's permissions cannot be read";
322
+ }
323
+ byte[] descriptor = new byte[needed];
324
+ if (!NativeMethods.GetFileSecurityW(folder, OwnerGroupDacl, descriptor, needed, out needed))
325
+ {
326
+ return "the folder's permissions cannot be read: " + new Win32Exception(Marshal.GetLastWin32Error()).Message;
327
+ }
328
+ IntPtr token = IntPtr.Zero;
329
+ try
330
+ {
331
+ token = UserToken();
332
+ GenericMapping mapping = new GenericMapping();
333
+ mapping.GenericRead = FileGenericRead;
334
+ mapping.GenericWrite = FileGenericWrite;
335
+ mapping.GenericExecute = FileGenericExecute;
336
+ mapping.GenericAll = FileAllAccess;
337
+ uint desired = FileGenericRead | FileTraverse | (readOnly ? 0u : FileGenericWrite);
338
+ byte[] privileges = new byte[256];
339
+ uint privilegesLength = (uint)privileges.Length;
340
+ uint granted;
341
+ bool allowed;
342
+ if (!NativeMethods.AccessCheck(descriptor, token, desired, ref mapping, privileges, ref privilegesLength, out granted, out allowed))
343
+ {
344
+ return "the folder's permissions cannot be checked: " + new Win32Exception(Marshal.GetLastWin32Error()).Message;
345
+ }
346
+ if (!allowed)
347
+ {
348
+ return readOnly ? "you cannot read this folder yourself, so it cannot be shared" : "you cannot change this folder yourself, so it cannot be shared writable";
349
+ }
350
+ return null;
351
+ }
352
+ finally
353
+ {
354
+ if (token != IntPtr.Zero)
355
+ {
356
+ NativeMethods.CloseHandle(token);
357
+ }
358
+ }
359
+ }
360
+ }
361
+ }
@@ -63,7 +63,9 @@ namespace AgentSandbox.HcsHost
63
63
 
64
64
  public static class Hcs
65
65
  {
66
+ /// <summary>HcsOpenComputeSystem documents no narrower right than GENERIC_ALL for modify.</summary>
66
67
  private const uint GenericAll = 0x10000000;
68
+ private const uint LoadLibrarySearchSystem32 = 0x00000800;
67
69
  private const uint QueryMs = 15000;
68
70
  private const uint ChangeMs = 180000;
69
71
 
@@ -116,7 +118,7 @@ namespace AgentSandbox.HcsHost
116
118
  public static extern IntPtr LocalFree(IntPtr mem);
117
119
 
118
120
  [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
119
- public static extern IntPtr LoadLibrary(string fileName);
121
+ public static extern IntPtr LoadLibraryEx(string fileName, IntPtr file, uint flags);
120
122
 
121
123
  [DllImport("kernel32.dll", SetLastError = true)]
122
124
  public static extern bool FreeLibrary(IntPtr module);
@@ -139,10 +141,10 @@ namespace AgentSandbox.HcsHost
139
141
  }
140
142
  }
141
143
 
142
- /// <summary>Whether a DLL (computecore.dll) can be loaded at all on this PC.</summary>
144
+ /// <summary>Whether a DLL (computecore.dll) can be loaded at all on this PC (from System32 only).</summary>
143
145
  public static bool CanLoad(string dll)
144
146
  {
145
- IntPtr module = NativeMethods.LoadLibrary(dll);
147
+ IntPtr module = NativeMethods.LoadLibraryEx(dll, IntPtr.Zero, LoadLibrarySearchSystem32);
146
148
  if (module == IntPtr.Zero)
147
149
  {
148
150
  return false;
@@ -254,6 +256,32 @@ namespace AgentSandbox.HcsHost
254
256
  return WithSystem(id, ChangeMs, delegate(IntPtr system, IntPtr op) { return NativeMethods.HcsModifyComputeSystem(system, op, modifySettingRequest, IntPtr.Zero); });
255
257
  }
256
258
 
259
+ /// <summary>Opens a compute system and keeps the handle (Close it): checks and changes then use the same system.</summary>
260
+ public static int Open(string id, out IntPtr system)
261
+ {
262
+ return NativeMethods.HcsOpenComputeSystem(id, GenericAll, out system);
263
+ }
264
+
265
+ public static void Close(IntPtr system)
266
+ {
267
+ if (system != IntPtr.Zero)
268
+ {
269
+ NativeMethods.HcsCloseComputeSystem(system);
270
+ }
271
+ }
272
+
273
+ /// <summary>The properties of an opened system (Id, SystemType, Owner, State, ...).</summary>
274
+ public static HcsReply PropertiesOf(IntPtr system)
275
+ {
276
+ return Run(delegate(IntPtr op) { return NativeMethods.HcsGetComputeSystemProperties(system, op, "{\"PropertyTypes\":[]}"); }, QueryMs);
277
+ }
278
+
279
+ /// <summary>One ModifySettingRequest on an opened system, waiting at most `waitMs`.</summary>
280
+ public static HcsReply ModifyOn(IntPtr system, string modifySettingRequest, uint waitMs)
281
+ {
282
+ return Run(delegate(IntPtr op) { return NativeMethods.HcsModifyComputeSystem(system, op, modifySettingRequest, IntPtr.Zero); }, waitMs);
283
+ }
284
+
257
285
  /// <summary>Lets the virtual machine `vmId` open the file (its disk). The file must exist.</summary>
258
286
  public static HcsReply GrantVmAccess(string vmId, string filePath)
259
287
  {
@@ -1,6 +1,8 @@
1
1
  // HcsHost.exe <verb> [arguments] --out <response.json>
2
2
  // Verbs: check, enumerate, properties <id>, create <request.json>, start <id>, terminate <id>,
3
- // grant-vm-access <vmId> <file>, create-vhdx <path> <maxGb>, share <request.json> (prototype).
3
+ // grant-vm-access <vmId> <file>, create-vhdx <path> <maxGb>, share <request.json> (our own systems),
4
+ // wsb-share <request.json> --sha256 <hex> (live folders on the running Windows Sandbox, WsbShare.cs),
5
+ // wsb-rules <enumerate.json> [id] (pure: applies WsbGuard's rules to a recorded list; asks HCS nothing).
4
6
  // The answer is one JSON file (see Json.Response); exit code 0 = ok, 1 = failed, 2 = wrong use. A create request is
5
7
  // {"id": "...", "document": "<the HCS document as a JSON string>"}. Never attaches a disk to this PC.
6
8
  using System;
@@ -23,6 +25,7 @@ namespace AgentSandbox.HcsHost
23
25
  public static int Main(string[] args)
24
26
  {
25
27
  string outFile = null;
28
+ string sha256 = null;
26
29
  List<string> rest = new List<string>();
27
30
  for (int i = 0; i < args.Length; i++)
28
31
  {
@@ -31,6 +34,11 @@ namespace AgentSandbox.HcsHost
31
34
  outFile = args[i + 1];
32
35
  i++;
33
36
  }
37
+ else if (args[i] == "--sha256" && i + 1 < args.Length)
38
+ {
39
+ sha256 = args[i + 1];
40
+ i++;
41
+ }
34
42
  else
35
43
  {
36
44
  rest.Add(args[i]);
@@ -40,7 +48,7 @@ namespace AgentSandbox.HcsHost
40
48
  int code;
41
49
  try
42
50
  {
43
- code = Dispatch(rest, out response);
51
+ code = Dispatch(rest, sha256, out response);
44
52
  }
45
53
  catch (Exception ex)
46
54
  {
@@ -73,11 +81,11 @@ namespace AgentSandbox.HcsHost
73
81
  return reply.Ok ? ExitOk : ExitFailed;
74
82
  }
75
83
 
76
- private static int Dispatch(List<string> args, out string response)
84
+ private static int Dispatch(List<string> args, string sha256, out string response)
77
85
  {
78
86
  if (args.Count == 0)
79
87
  {
80
- return Usage(out response, "HcsHost <check|enumerate|properties|create|start|terminate|grant-vm-access|create-vhdx> ... --out file");
88
+ return Usage(out response, "HcsHost <check|enumerate|properties|create|start|terminate|grant-vm-access|create-vhdx|share|wsb-share|wsb-rules> ... --out file");
81
89
  }
82
90
  string verb = args[0];
83
91
  if (verb == "check")
@@ -113,6 +121,22 @@ namespace AgentSandbox.HcsHost
113
121
  {
114
122
  return Share(args[1], out response);
115
123
  }
124
+ if (verb == "wsb-share" && args.Count == 2)
125
+ {
126
+ if (sha256 == null || !System.Text.RegularExpressions.Regex.IsMatch(sha256, "^[0-9A-Fa-f]{64}$"))
127
+ {
128
+ return Usage(out response, "wsb-share <request.json> --sha256 <its SHA-256>");
129
+ }
130
+ Dictionary<string, string> values = new Dictionary<string, string>();
131
+ string message;
132
+ bool ok = WsbShare.Run(args[1], sha256, values, out message);
133
+ response = Json.Response(ok, 0, message, "", values);
134
+ return ok ? ExitOk : (message.StartsWith("Refused: ") ? ExitUsage : ExitFailed);
135
+ }
136
+ if (verb == "wsb-rules" && (args.Count == 2 || args.Count == 3))
137
+ {
138
+ return WsbRules(args[1], args.Count == 3 ? args[2] : null, out response);
139
+ }
116
140
  if (verb == "grant-vm-access" && args.Count == 3)
117
141
  {
118
142
  if (Refused(Guard.IdProblem(args[1]), out response) || Refused(Guard.DiskProblem(args[2]), out response))
@@ -175,8 +199,8 @@ namespace AgentSandbox.HcsHost
175
199
  }
176
200
 
177
201
  /// <summary>
178
- /// PROTOTYPE (docs/design/hcs-live-folders.md): adds or removes a vSMB share on a running system of OURS (id
179
- /// prefix and Owner, as for start/terminate). Windows Sandbox's own system is refused like any other.
202
+ /// Adds or removes a vSMB share on a running system of OURS (id prefix and Owner, as for start/terminate).
203
+ /// Windows Sandbox's own system is refused like any other: that is wsb-share's, with its own rules.
180
204
  /// </summary>
181
205
  private static int Share(string requestFile, out string response)
182
206
  {
@@ -195,6 +219,30 @@ namespace AgentSandbox.HcsHost
195
219
  return FromReply(Hcs.Modify(request.Id, values["request"]), values, out response);
196
220
  }
197
221
 
222
+ /// <summary>The test hook for WsbGuard: its rules on a recorded enumerate answer. Never calls HCS.</summary>
223
+ private static int WsbRules(string file, string id, out string response)
224
+ {
225
+ FileInfo info = new FileInfo(file);
226
+ if (!info.Exists || info.Length > MaxRequestBytes)
227
+ {
228
+ return Usage(out response, "wsb-rules <enumerate.json> [id]");
229
+ }
230
+ string found;
231
+ string problem = WsbGuard.Candidate(File.ReadAllText(file, Encoding.UTF8), out found);
232
+ if (problem == null && id != null)
233
+ {
234
+ problem = WsbGuard.IdProblem(id) ?? WsbGuard.MatchProblem(found, id);
235
+ }
236
+ Dictionary<string, string> values = new Dictionary<string, string>();
237
+ values["id"] = found ?? "";
238
+ if (Refused(problem, out response))
239
+ {
240
+ return ExitUsage;
241
+ }
242
+ response = Json.Response(true, 0, "ok", "", values);
243
+ return ExitOk;
244
+ }
245
+
198
246
  /// <summary>True (with the refusal as the response) when `problem` says why the request is not allowed.</summary>
199
247
  private static bool Refused(string problem, out string response)
200
248
  {
@@ -1,8 +1,9 @@
1
- // Live folders (PROTOTYPE, not wired into the CLI; docs/design/hcs-live-folders.md): a vSMB share added to or removed
2
- // from a RUNNING compute system with HcsModifyComputeSystem. The request is built here from checked fields, never
3
- // passed through: only ResourcePath "VirtualMachine/Devices/VirtualSmb/Shares", only Add or Remove, a share name with
4
- // this tool's prefix, and fixed options (NoDirectmap always; read-only unless asked; never TakeBackupPrivilege,
5
- // UseShareRootIdentity, AllowedFiles or any other option that changes whose rights the guest uses).
1
+ // Live folders (docs/design/hcs-live-folders.md): a vSMB share added to or removed from a RUNNING compute system with
2
+ // HcsModifyComputeSystem. The request is built here from checked fields, never passed through: only ResourcePath
3
+ // "VirtualMachine/Devices/VirtualSmb/Shares", only Add or Remove, a share name with this tool's prefix, and fixed
4
+ // options (NoDirectmap always; read-only unless asked; never TakeBackupPrivilege, UseShareRootIdentity, AllowedFiles or
5
+ // any other option that changes whose rights the guest uses). `share` uses it on our own systems, `wsb-share` on the
6
+ // running Windows Sandbox (WsbShare.cs).
6
7
  using System;
7
8
  using System.Collections.Generic;
8
9
  using System.IO;
@@ -25,9 +26,23 @@ namespace AgentSandbox.HcsHost
25
26
  public string Name;
26
27
  public string Path;
27
28
  public bool ReadOnly = true;
29
+ /// <summary>wsb-share: this tool's storage and data folders, never shared (the data folder holds secrets\).</summary>
30
+ public string StorageRoot;
31
+ public string DataPath;
32
+ /// <summary>wsb-share: the Windows user who asked (must be the one running HcsHost: no other account's prompt).</summary>
33
+ public string UserSid;
28
34
 
29
35
  /// <summary>Reads and checks a request file. Null with `problem` set when it is not allowed.</summary>
30
36
  public static ShareRequest Read(string file, out string problem)
37
+ {
38
+ return Read(file, null, out problem);
39
+ }
40
+
41
+ /// <summary>
42
+ /// The same, and when `sha256` is given the exact bytes parsed must have that SHA-256 (a file swapped after the
43
+ /// elevated script checked it is caught here).
44
+ /// </summary>
45
+ public static ShareRequest Read(string file, string sha256, out string problem)
31
46
  {
32
47
  FileInfo info = new FileInfo(file);
33
48
  if (!info.Exists || info.Length > MaxRequestBytes)
@@ -35,10 +50,27 @@ namespace AgentSandbox.HcsHost
35
50
  problem = "the request must be a file of at most 64 KB";
36
51
  return null;
37
52
  }
53
+ byte[] bytes = File.ReadAllBytes(file);
54
+ if (bytes.Length > MaxRequestBytes)
55
+ {
56
+ problem = "the request must be a file of at most 64 KB";
57
+ return null;
58
+ }
59
+ if (sha256 != null && !string.Equals(Sha256Hex(bytes), sha256, StringComparison.OrdinalIgnoreCase))
60
+ {
61
+ problem = "the request file changed after it was checked";
62
+ return null;
63
+ }
38
64
  Dictionary<string, object> raw;
39
65
  try
40
66
  {
41
- raw = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(File.ReadAllText(file, Encoding.UTF8));
67
+ string text = new UTF8Encoding(false, true).GetString(bytes).TrimStart('\uFEFF');
68
+ raw = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(text);
69
+ }
70
+ catch (DecoderFallbackException)
71
+ {
72
+ problem = "the request is not UTF-8";
73
+ return null;
42
74
  }
43
75
  catch (ArgumentException)
44
76
  {
@@ -55,6 +87,9 @@ namespace AgentSandbox.HcsHost
55
87
  string action = Text(raw, "action");
56
88
  r.Name = Text(raw, "name");
57
89
  r.Path = Text(raw, "path");
90
+ r.StorageRoot = Text(raw, "storageRoot");
91
+ r.DataPath = Text(raw, "dataPath");
92
+ r.UserSid = Text(raw, "userSid");
58
93
  object ro;
59
94
  if (raw != null && raw.TryGetValue("readOnly", out ro))
60
95
  {
@@ -80,6 +115,14 @@ namespace AgentSandbox.HcsHost
80
115
  return problem == null ? r : null;
81
116
  }
82
117
 
118
+ public static string Sha256Hex(byte[] bytes)
119
+ {
120
+ using (System.Security.Cryptography.SHA256 sha = System.Security.Cryptography.SHA256.Create())
121
+ {
122
+ return BitConverter.ToString(sha.ComputeHash(bytes)).Replace("-", "").ToLowerInvariant();
123
+ }
124
+ }
125
+
83
126
  private static string Text(Dictionary<string, object> raw, string key)
84
127
  {
85
128
  object value;
@@ -150,17 +193,27 @@ namespace AgentSandbox.HcsHost
150
193
  /// <summary>The ModifySettingRequest (HCS schema; learn.microsoft.com .../hcsmodifycomputesystem).</summary>
151
194
  public string ToModifySettingRequest()
152
195
  {
196
+ return Build(Add, Name, Add ? System.IO.Path.GetFullPath(Path.TrimEnd('\\')) : null, ReadOnly);
197
+ }
198
+
199
+ /// <summary>The one place a share request is written: the fixed resource path and options.</summary>
200
+ public static string Build(bool add, string name, string folder, bool readOnly)
201
+ {
202
+ if (name == null || !SafeName.IsMatch(name))
203
+ {
204
+ throw new ArgumentException("not one of this tool's share names");
205
+ }
153
206
  StringBuilder sb = new StringBuilder();
154
207
  sb.Append("{\"ResourcePath\":").Append(Json.Quote(ResourcePath));
155
- sb.Append(",\"RequestType\":").Append(Json.Quote(Add ? "Add" : "Remove"));
156
- sb.Append(",\"Settings\":{\"Name\":").Append(Json.Quote(Name));
157
- if (Add)
208
+ sb.Append(",\"RequestType\":").Append(Json.Quote(add ? "Add" : "Remove"));
209
+ sb.Append(",\"Settings\":{\"Name\":").Append(Json.Quote(name));
210
+ if (add)
158
211
  {
159
- sb.Append(",\"Path\":").Append(Json.Quote(System.IO.Path.GetFullPath(Path.TrimEnd('\\'))));
212
+ sb.Append(",\"Path\":").Append(Json.Quote(folder));
160
213
  // hcsshim's read-only defaults (internal/uvm/vsmb.go DefaultVSMBOptions) plus NoDirectmap: a share that
161
214
  // was never direct-mapped can be removed again while the system runs.
162
215
  sb.Append(",\"Options\":{\"NoDirectmap\":true");
163
- if (ReadOnly)
216
+ if (readOnly)
164
217
  {
165
218
  sb.Append(",\"ReadOnly\":true,\"ShareRead\":true,\"CacheIo\":true,\"PseudoOplocks\":true");
166
219
  }