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.
- package/config.example.json +2 -1
- package/dist/app/features/defs.d.ts +1 -1
- package/dist/app/features/projects/projects.def.d.ts +1 -1
- package/dist/app/features/projects/projects.run.d.ts +1 -1
- package/dist/app/features/projects/projects.run.js +10 -2
- package/dist/app/features/sandbox/sandbox.def.js +3 -1
- package/dist/app/features/settings/categories.def.js +1 -0
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-BQ23GBLC.js → chunk-D7CM63BS.js} +1893 -1405
- package/dist/bin/{chunk-F7IMME5G.js → chunk-HTQT5SBY.js} +1 -1
- package/dist/bin/{chunk-LQHEUN2A.js → chunk-RQ4BWBYX.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/projects/here.js +2 -0
- package/dist/cli/services/health-service.js +15 -3
- package/dist/cli/services/projects-service.d.ts +8 -3
- package/dist/cli/services/projects-service.js +21 -4
- package/dist/core/config/defaults.js +1 -0
- package/dist/core/schema/config.d.ts +4 -0
- package/dist/core/schema/projects.d.ts +4 -0
- package/dist/core/schema/projects.js +3 -0
- package/dist/guest/boot/boot.js +8 -2
- package/dist/guest/daemon/daemon.js +3 -0
- package/dist/guest/daemon/live-folders.d.ts +48 -0
- package/dist/guest/daemon/live-folders.js +245 -0
- package/dist/guest/daemon/live.d.ts +4 -0
- package/dist/guest/daemon/live.js +57 -0
- package/dist/guest/entries/daemon.js +4 -2
- package/dist/guest/runtime/csc.d.ts +1 -0
- package/dist/guest/runtime/csc.js +1 -0
- package/dist/guest/runtime/secrets.d.ts +5 -2
- package/dist/guest/runtime/secrets.js +12 -5
- package/dist/host/engines/hcs/live-folders.d.ts +117 -0
- package/dist/host/engines/hcs/live-folders.js +347 -0
- package/dist/host/engines/index.d.ts +1 -0
- package/dist/host/engines/index.js +1 -0
- package/dist/host/launch/generate.js +8 -1
- package/dist/host/projects/join.d.ts +4 -0
- package/dist/host/projects/join.js +11 -3
- package/dist/host/projects/mapper.d.ts +34 -5
- package/dist/host/projects/mapper.js +88 -20
- package/dist/host/projects/registry.d.ts +2 -0
- package/dist/host/projects/registry.js +1 -1
- package/dist/host/secrets/launch-secrets.js +2 -1
- package/dist/runtime/agent.js +14 -0
- package/dist/runtime/daemon.js +466 -132
- package/dist/runtime/db.js +14 -0
- package/dist/runtime/main.js +26 -3
- package/dist/runtime/netproxy.js +14 -0
- package/dist/runtime/view.js +14 -0
- package/dist/selftest/index.js +2 -0
- package/dist/selftest/layout.test.js +1 -0
- package/dist/selftest/live-folders.test.d.ts +2 -0
- package/dist/selftest/live-folders.test.js +588 -0
- package/dist/selftest/projects.test.js +7 -7
- package/dist/selftest/support/engine-golden.js +1 -0
- package/dist/selftest/support/fixtures.d.ts +1 -0
- package/dist/selftest/support/live.js +2 -0
- package/dist/shared/channel/index.d.ts +1 -0
- package/dist/shared/channel/index.js +1 -0
- package/dist/shared/channel/live.d.ts +53 -0
- package/dist/shared/channel/live.js +92 -0
- package/dist/shared/channel/run-dir.d.ts +5 -0
- package/dist/shared/channel/run-dir.js +11 -0
- package/dist/shared/channel/secrets.d.ts +1 -0
- package/dist/shared/channel/secrets.js +5 -0
- package/dist/shared/paths/projects.d.ts +6 -1
- package/dist/shared/paths/projects.js +21 -1
- package/dist/sync/watcher.js +9 -0
- package/dist/web/index.html +1 -1
- package/dist/web/{main-6XNBTJLT.js → main-7DJZ2M27.js} +1 -1
- package/native/csharp/HcsHost/AssemblyInfo.cs +4 -4
- package/native/csharp/HcsHost/FolderAccess.cs +361 -0
- package/native/csharp/HcsHost/Hcs.cs +31 -3
- package/native/csharp/HcsHost/Program.cs +54 -6
- package/native/csharp/HcsHost/Share.cs +64 -11
- package/native/csharp/HcsHost/WsbGuard.cs +130 -0
- package/native/csharp/HcsHost/WsbShare.cs +295 -0
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/native/scripts/contained-common.ps1 +4 -0
- package/native/scripts/hcs-live-share.ps1 +191 -0
- package/package.json +1 -1
- package/sandbox/csharp/BindMount.cs +120 -0
- package/schemas/config.schema.json +8 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// The one exception to "only systems this tool created" (docs/design/hcs-live-folders.md, section 4): the wsb-share
|
|
2
|
+
// verb may add a vSMB share to the running Windows Sandbox. It acts only when exactly one compute system is a running
|
|
3
|
+
// VirtualMachine with a GUID id and an Owner from a fixed list, and that id is the one asked for. An unknown Owner,
|
|
4
|
+
// none or two candidates (another user's sandbox, Application Guard) all fail closed. Ids with this tool's own prefix
|
|
5
|
+
// are refused here, and `share` refuses GUIDs, so neither verb reaches the other's systems.
|
|
6
|
+
using System;
|
|
7
|
+
using System.Collections;
|
|
8
|
+
using System.Collections.Generic;
|
|
9
|
+
using System.Text.RegularExpressions;
|
|
10
|
+
using System.Web.Script.Serialization;
|
|
11
|
+
|
|
12
|
+
namespace AgentSandbox.HcsHost
|
|
13
|
+
{
|
|
14
|
+
public static class WsbGuard
|
|
15
|
+
{
|
|
16
|
+
/// <summary>The Owner Windows Sandbox's compute system is reported to have (unconfirmed: the PC test records it).</summary>
|
|
17
|
+
private static readonly string[] Owners = new string[] { "Madrid" };
|
|
18
|
+
private static readonly Regex GuidId = new Regex("^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$");
|
|
19
|
+
|
|
20
|
+
/// <summary>Null when `id` has the shape of Windows Sandbox's id (a GUID, never one of ours).</summary>
|
|
21
|
+
public static string IdProblem(string id)
|
|
22
|
+
{
|
|
23
|
+
if (id == null || id.StartsWith(Guard.IdPrefix, StringComparison.OrdinalIgnoreCase) || !GuidId.IsMatch(id))
|
|
24
|
+
{
|
|
25
|
+
return "only the running Windows Sandbox (a GUID id) can be given a live folder";
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// <summary>One system from HCS JSON: its Id, SystemType, Owner and State ("" when missing).</summary>
|
|
31
|
+
private static string Field(Dictionary<string, object> s, string key)
|
|
32
|
+
{
|
|
33
|
+
object value;
|
|
34
|
+
return s != null && s.TryGetValue(key, out value) && value is string ? (string)value : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private static bool IsCandidate(Dictionary<string, object> s)
|
|
38
|
+
{
|
|
39
|
+
return Field(s, "SystemType") == "VirtualMachine" && Field(s, "State") == "Running" &&
|
|
40
|
+
Array.IndexOf(Owners, Field(s, "Owner")) >= 0 && GuidId.IsMatch(Field(s, "Id"));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// <summary>
|
|
44
|
+
/// Pure: the Windows Sandbox candidate in HcsEnumerateComputeSystems' answer. Null (with `id`) when there is
|
|
45
|
+
/// exactly one; otherwise why not.
|
|
46
|
+
/// </summary>
|
|
47
|
+
public static string Candidate(string enumerateJson, out string id)
|
|
48
|
+
{
|
|
49
|
+
id = null;
|
|
50
|
+
object parsed;
|
|
51
|
+
try
|
|
52
|
+
{
|
|
53
|
+
parsed = new JavaScriptSerializer().DeserializeObject(enumerateJson ?? "");
|
|
54
|
+
}
|
|
55
|
+
catch (ArgumentException)
|
|
56
|
+
{
|
|
57
|
+
return "the list of compute systems is not JSON";
|
|
58
|
+
}
|
|
59
|
+
catch (InvalidOperationException)
|
|
60
|
+
{
|
|
61
|
+
return "the list of compute systems is not JSON";
|
|
62
|
+
}
|
|
63
|
+
IEnumerable systems = parsed as object[];
|
|
64
|
+
if (systems == null)
|
|
65
|
+
{
|
|
66
|
+
return "the list of compute systems is not a list";
|
|
67
|
+
}
|
|
68
|
+
List<string> found = new List<string>();
|
|
69
|
+
foreach (object o in systems)
|
|
70
|
+
{
|
|
71
|
+
Dictionary<string, object> s = o as Dictionary<string, object>;
|
|
72
|
+
if (IsCandidate(s))
|
|
73
|
+
{
|
|
74
|
+
found.Add(Field(s, "Id"));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (found.Count != 1)
|
|
78
|
+
{
|
|
79
|
+
return "exactly one running Windows Sandbox is needed, and " + found.Count + " were found (another user's sandbox or Application Guard counts too; a Windows Sandbox whose Owner is not " + string.Join(", ", Owners) + " is not recognised)";
|
|
80
|
+
}
|
|
81
|
+
id = found[0];
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// <summary>Pure: null when the candidate is the system asked for (case-insensitive GUIDs).</summary>
|
|
86
|
+
public static string MatchProblem(string candidate, string wanted)
|
|
87
|
+
{
|
|
88
|
+
if (wanted != null && wanted.Length > 0 && !string.Equals(candidate, wanted, StringComparison.OrdinalIgnoreCase))
|
|
89
|
+
{
|
|
90
|
+
return "the running Windows Sandbox is not the one this live folder was recorded for (it was restarted)";
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// <summary>Pure: null when an opened system's own properties still say it is the running Windows Sandbox `id`.</summary>
|
|
96
|
+
public static string PropertiesProblem(string propertiesJson, string id)
|
|
97
|
+
{
|
|
98
|
+
Dictionary<string, object> s;
|
|
99
|
+
try
|
|
100
|
+
{
|
|
101
|
+
s = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(propertiesJson ?? "");
|
|
102
|
+
}
|
|
103
|
+
catch (ArgumentException)
|
|
104
|
+
{
|
|
105
|
+
return "its properties are not JSON";
|
|
106
|
+
}
|
|
107
|
+
catch (InvalidOperationException)
|
|
108
|
+
{
|
|
109
|
+
return "its properties are not JSON";
|
|
110
|
+
}
|
|
111
|
+
if (!IsCandidate(s) || !string.Equals(Field(s, "Id"), id, StringComparison.OrdinalIgnoreCase))
|
|
112
|
+
{
|
|
113
|
+
return "the opened system is no longer the running Windows Sandbox";
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// <summary>Live: asks HCS for every system and finds Windows Sandbox (null with `id`, or why not).</summary>
|
|
119
|
+
public static string Find(out string id)
|
|
120
|
+
{
|
|
121
|
+
id = null;
|
|
122
|
+
HcsReply reply = Hcs.Enumerate(null);
|
|
123
|
+
if (!reply.Ok)
|
|
124
|
+
{
|
|
125
|
+
return "the compute systems could not be listed: " + reply.Message();
|
|
126
|
+
}
|
|
127
|
+
return Candidate(reply.Json, out id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// wsb-share (docs/design/hcs-live-folders.md): adds or removes one live folder on the running Windows Sandbox, run by
|
|
2
|
+
// native\scripts\hcs-live-share.ps1 from an administrator-only copy of this exe. One process holds one handle to the
|
|
3
|
+
// system for the whole add:
|
|
4
|
+
// 1. the request (its exact bytes hashed and compared), the folder (FolderAccess) and the system (WsbGuard: exactly
|
|
5
|
+
// one candidate, the id asked for if one was recorded, its own properties re-read through the handle);
|
|
6
|
+
// 2. a canary: a fresh folder next to this exe (Administrators, SYSTEM and the Virtual Machines group only) holding a
|
|
7
|
+
// random 128-bit token, shared read-only under a random name; canary.json in the request's folder tells the
|
|
8
|
+
// unelevated caller its name; the token our sandbox's daemon read from that share must come back in answer.txt
|
|
9
|
+
// within the time limit;
|
|
10
|
+
// 3. only then the real folder is shared. The canary share and folder are always removed again.
|
|
11
|
+
// It never terminates, pauses or saves the system, and every HCS operation waits at most 60 seconds.
|
|
12
|
+
using System;
|
|
13
|
+
using System.Collections.Generic;
|
|
14
|
+
using System.IO;
|
|
15
|
+
using System.Security.AccessControl;
|
|
16
|
+
using System.Security.Cryptography;
|
|
17
|
+
using System.Security.Principal;
|
|
18
|
+
using System.Text;
|
|
19
|
+
using System.Text.RegularExpressions;
|
|
20
|
+
using System.Threading;
|
|
21
|
+
|
|
22
|
+
namespace AgentSandbox.HcsHost
|
|
23
|
+
{
|
|
24
|
+
public static class WsbShare
|
|
25
|
+
{
|
|
26
|
+
private const uint OperationMs = 60000;
|
|
27
|
+
private const int CanaryWaitMs = 45000;
|
|
28
|
+
private const int PollMs = 250;
|
|
29
|
+
private const string CanaryFile = "canary.json";
|
|
30
|
+
private const string AnswerFile = "answer.txt";
|
|
31
|
+
private const string TokenFile = "token.txt";
|
|
32
|
+
private static readonly Regex Token = new Regex("^[0-9a-f]{32}$");
|
|
33
|
+
private static readonly SecurityIdentifier VirtualMachines = new SecurityIdentifier("S-1-5-83-0");
|
|
34
|
+
|
|
35
|
+
/// <summary>Adds or removes per the request; `values` say what happened (id, canary, the share request).</summary>
|
|
36
|
+
public static bool Run(string requestFile, string sha256, IDictionary<string, string> values, out string message)
|
|
37
|
+
{
|
|
38
|
+
string problem;
|
|
39
|
+
ShareRequest request = ShareRequest.Read(requestFile, sha256, out problem);
|
|
40
|
+
if (problem != null)
|
|
41
|
+
{
|
|
42
|
+
message = "Refused: " + problem;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
// Elevated with another account's password (a standard user's prompt): the profile, access and ownership
|
|
46
|
+
// checks would be that account's, so it is refused. Live folders need your own administrator account.
|
|
47
|
+
string me = WindowsIdentity.GetCurrent().User.Value;
|
|
48
|
+
if (string.IsNullOrEmpty(request.UserSid) || !string.Equals(request.UserSid, me, StringComparison.OrdinalIgnoreCase))
|
|
49
|
+
{
|
|
50
|
+
message = "Refused: the permission prompt was answered with another account; live folders need your own administrator account";
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
string wanted = string.IsNullOrEmpty(request.Id) ? null : request.Id;
|
|
54
|
+
if (wanted != null && (problem = WsbGuard.IdProblem(wanted)) != null)
|
|
55
|
+
{
|
|
56
|
+
message = "Refused: " + problem;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (!request.Add && wanted == null)
|
|
60
|
+
{
|
|
61
|
+
message = "Refused: a live folder is only removed from the sandbox it was added to (give its id)";
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
string folder = null;
|
|
65
|
+
IntPtr held = IntPtr.Zero;
|
|
66
|
+
if (request.Add && (problem = FolderAccess.Problem(request.Path, request.ReadOnly, request.StorageRoot, request.DataPath, out folder, out held)) != null)
|
|
67
|
+
{
|
|
68
|
+
message = "Refused: " + problem;
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
try
|
|
72
|
+
{
|
|
73
|
+
return WithFolder(request, requestFile, folder, held, wanted, values, out message);
|
|
74
|
+
}
|
|
75
|
+
finally
|
|
76
|
+
{
|
|
77
|
+
FolderAccess.Close(held);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// <summary>The rest of Run, while the folder to share is held open (it cannot be renamed or replaced).</summary>
|
|
82
|
+
private static bool WithFolder(ShareRequest request, string requestFile, string folder, IntPtr held, string wanted, IDictionary<string, string> values, out string message)
|
|
83
|
+
{
|
|
84
|
+
string problem;
|
|
85
|
+
string id;
|
|
86
|
+
if ((problem = WsbGuard.Find(out id)) != null || (problem = WsbGuard.MatchProblem(id, wanted)) != null)
|
|
87
|
+
{
|
|
88
|
+
message = "Refused: " + problem;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
values["id"] = id;
|
|
92
|
+
IntPtr system;
|
|
93
|
+
int hr = Hcs.Open(id, out system);
|
|
94
|
+
if (hr != 0)
|
|
95
|
+
{
|
|
96
|
+
HcsReply failed = new HcsReply();
|
|
97
|
+
failed.HResult = hr;
|
|
98
|
+
message = "Windows Sandbox could not be opened: " + failed.Message();
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
try
|
|
102
|
+
{
|
|
103
|
+
HcsReply props = Hcs.PropertiesOf(system);
|
|
104
|
+
if (!props.Ok || (problem = WsbGuard.PropertiesProblem(props.Json, id)) != null)
|
|
105
|
+
{
|
|
106
|
+
message = "Refused: " + (problem ?? "its properties could not be read: " + props.Message());
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
if (!request.Add)
|
|
110
|
+
{
|
|
111
|
+
string remove = ShareRequest.Build(false, request.Name, null, true);
|
|
112
|
+
values["request"] = remove;
|
|
113
|
+
return Reply(Hcs.ModifyOn(system, remove, OperationMs), "removed", out message);
|
|
114
|
+
}
|
|
115
|
+
return AddWithCanary(system, request, folder, held, Path.GetDirectoryName(Path.GetFullPath(requestFile)), values, out message);
|
|
116
|
+
}
|
|
117
|
+
finally
|
|
118
|
+
{
|
|
119
|
+
Hcs.Close(system);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private static bool Reply(HcsReply reply, string done, out string message)
|
|
124
|
+
{
|
|
125
|
+
message = reply.Ok ? done : "The Host Compute Service said: " + reply.Message() + " (0x" + reply.HResult.ToString("X8") + ")";
|
|
126
|
+
return reply.Ok;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private static string RandomHex(int bytes)
|
|
130
|
+
{
|
|
131
|
+
byte[] b = new byte[bytes];
|
|
132
|
+
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
|
|
133
|
+
{
|
|
134
|
+
rng.GetBytes(b);
|
|
135
|
+
}
|
|
136
|
+
return BitConverter.ToString(b).Replace("-", "").ToLowerInvariant();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// <summary>A new folder only Administrators, SYSTEM and the VM worker processes can use (made with that DACL).</summary>
|
|
140
|
+
private static string MakeCanaryFolder(string token)
|
|
141
|
+
{
|
|
142
|
+
string here = Path.GetDirectoryName(typeof(WsbShare).Assembly.Location);
|
|
143
|
+
string dir = Path.Combine(here, "canary-" + RandomHex(8));
|
|
144
|
+
if (Directory.Exists(dir) || File.Exists(dir))
|
|
145
|
+
{
|
|
146
|
+
throw new IOException("the canary folder exists already");
|
|
147
|
+
}
|
|
148
|
+
DirectorySecurity security = new DirectorySecurity();
|
|
149
|
+
security.SetAccessRuleProtection(true, false);
|
|
150
|
+
security.SetOwner(new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null));
|
|
151
|
+
InheritanceFlags inherit = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit;
|
|
152
|
+
security.AddAccessRule(new FileSystemAccessRule(new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), FileSystemRights.FullControl, inherit, PropagationFlags.None, AccessControlType.Allow));
|
|
153
|
+
security.AddAccessRule(new FileSystemAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), FileSystemRights.FullControl, inherit, PropagationFlags.None, AccessControlType.Allow));
|
|
154
|
+
security.AddAccessRule(new FileSystemAccessRule(VirtualMachines, FileSystemRights.ReadAndExecute, inherit, PropagationFlags.None, AccessControlType.Allow));
|
|
155
|
+
Directory.CreateDirectory(dir, security);
|
|
156
|
+
File.WriteAllText(Path.Combine(dir, TokenFile), token, new UTF8Encoding(false));
|
|
157
|
+
return dir;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// <summary>Writes a small file whole: a temp name first, then moved into place.</summary>
|
|
161
|
+
private static void WriteWhole(string file, string text)
|
|
162
|
+
{
|
|
163
|
+
// The caller's own temp folder: checked for a link right before writing, and only a new file is made there
|
|
164
|
+
// (never over an existing file or link).
|
|
165
|
+
string dir = Path.GetDirectoryName(file);
|
|
166
|
+
if ((File.GetAttributes(dir) & FileAttributes.ReparsePoint) != 0)
|
|
167
|
+
{
|
|
168
|
+
throw new IOException("the request folder became a link");
|
|
169
|
+
}
|
|
170
|
+
string temp = file + "." + RandomHex(4) + ".tmp";
|
|
171
|
+
byte[] bytes = new UTF8Encoding(false).GetBytes(text);
|
|
172
|
+
using (FileStream stream = new FileStream(temp, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
|
173
|
+
{
|
|
174
|
+
stream.Write(bytes, 0, bytes.Length);
|
|
175
|
+
}
|
|
176
|
+
File.Move(temp, file);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// <summary>The token the caller relays from our sandbox's daemon (32 hex digits), or null until it is there.</summary>
|
|
180
|
+
private static string ReadAnswer(string file)
|
|
181
|
+
{
|
|
182
|
+
try
|
|
183
|
+
{
|
|
184
|
+
FileInfo info = new FileInfo(file);
|
|
185
|
+
if (!info.Exists || info.Length > 64)
|
|
186
|
+
{
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
string text = File.ReadAllText(file, Encoding.ASCII).Trim();
|
|
190
|
+
return text.Length > 0 ? text : null;
|
|
191
|
+
}
|
|
192
|
+
catch (IOException)
|
|
193
|
+
{
|
|
194
|
+
return null; // still being written
|
|
195
|
+
}
|
|
196
|
+
catch (UnauthorizedAccessException)
|
|
197
|
+
{
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private static bool FixedTimeEquals(string a, string b)
|
|
203
|
+
{
|
|
204
|
+
if (a == null || b == null || a.Length != b.Length)
|
|
205
|
+
{
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
int diff = 0;
|
|
209
|
+
for (int i = 0; i < a.Length; i++)
|
|
210
|
+
{
|
|
211
|
+
diff |= a[i] ^ b[i];
|
|
212
|
+
}
|
|
213
|
+
return diff == 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private static bool AddWithCanary(IntPtr system, ShareRequest request, string folder, IntPtr held, string exchange, IDictionary<string, string> values, out string message)
|
|
217
|
+
{
|
|
218
|
+
string token = RandomHex(16);
|
|
219
|
+
string canaryName = "agentsbx-" + RandomHex(8);
|
|
220
|
+
string canaryDir = null;
|
|
221
|
+
bool canaryAdded = false;
|
|
222
|
+
try
|
|
223
|
+
{
|
|
224
|
+
canaryDir = MakeCanaryFolder(token);
|
|
225
|
+
// Removed in finally even when the add fails: an add that timed out may still complete.
|
|
226
|
+
canaryAdded = true;
|
|
227
|
+
HcsReply added = Hcs.ModifyOn(system, ShareRequest.Build(true, canaryName, canaryDir, true), OperationMs);
|
|
228
|
+
if (!added.Ok)
|
|
229
|
+
{
|
|
230
|
+
values["canary"] = "not added";
|
|
231
|
+
return Reply(added, "", out message);
|
|
232
|
+
}
|
|
233
|
+
WriteWhole(Path.Combine(exchange, CanaryFile), "{\"share\":" + Json.Quote(canaryName) + "}");
|
|
234
|
+
string answer = null;
|
|
235
|
+
for (int waited = 0; waited < CanaryWaitMs && answer == null; waited += PollMs)
|
|
236
|
+
{
|
|
237
|
+
Thread.Sleep(PollMs);
|
|
238
|
+
answer = ReadAnswer(Path.Combine(exchange, AnswerFile));
|
|
239
|
+
}
|
|
240
|
+
if (answer == null || answer == "none")
|
|
241
|
+
{
|
|
242
|
+
values["canary"] = answer == null ? "no answer" : "not read";
|
|
243
|
+
message = "The sandbox did not read the test share" + (answer == null ? " in time" : "") + ", so this may not be your sandbox; nothing was shared";
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
if (!Token.IsMatch(answer) || !FixedTimeEquals(answer, token))
|
|
247
|
+
{
|
|
248
|
+
values["canary"] = "wrong token";
|
|
249
|
+
message = "The test share came back with the wrong token, so this is not your sandbox; nothing was shared";
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
values["canary"] = "matched";
|
|
253
|
+
string add = ShareRequest.Build(true, request.Name, folder, request.ReadOnly);
|
|
254
|
+
values["request"] = add;
|
|
255
|
+
HcsReply real = Hcs.ModifyOn(system, add, OperationMs);
|
|
256
|
+
string moved = real.Ok ? FolderAccess.MovedProblem(held, folder) : null;
|
|
257
|
+
if (!real.Ok || moved != null)
|
|
258
|
+
{
|
|
259
|
+
// Failed, timed out (it may still complete) or the folder moved: the share is taken away again.
|
|
260
|
+
HcsReply undone = Hcs.ModifyOn(system, ShareRequest.Build(false, request.Name, null, true), OperationMs);
|
|
261
|
+
values["undone"] = undone.Ok ? "yes" : undone.Message();
|
|
262
|
+
if (moved != null)
|
|
263
|
+
{
|
|
264
|
+
message = "Refused: " + moved;
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return Reply(real, "added", out message);
|
|
269
|
+
}
|
|
270
|
+
finally
|
|
271
|
+
{
|
|
272
|
+
if (canaryAdded)
|
|
273
|
+
{
|
|
274
|
+
HcsReply removed = Hcs.ModifyOn(system, ShareRequest.Build(false, canaryName, null, true), OperationMs);
|
|
275
|
+
values["canaryRemoved"] = removed.Ok ? "yes" : removed.Message();
|
|
276
|
+
}
|
|
277
|
+
if (canaryDir != null)
|
|
278
|
+
{
|
|
279
|
+
try
|
|
280
|
+
{
|
|
281
|
+
Directory.Delete(canaryDir, true);
|
|
282
|
+
}
|
|
283
|
+
catch (IOException)
|
|
284
|
+
{
|
|
285
|
+
// Still open by the VM: the elevated script deletes the whole work folder afterwards.
|
|
286
|
+
}
|
|
287
|
+
catch (UnauthorizedAccessException)
|
|
288
|
+
{
|
|
289
|
+
// The same.
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
|
|
|
8
8
|
[assembly: AssemblyCompany("windows-development-sandbox (open source)")]
|
|
9
9
|
[assembly: AssemblyProduct("Windows Development Sandbox")]
|
|
10
10
|
[assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
|
|
11
|
-
[assembly: AssemblyVersion("1.
|
|
12
|
-
[assembly: AssemblyFileVersion("1.
|
|
13
|
-
[assembly: AssemblyInformationalVersion("1.
|
|
11
|
+
[assembly: AssemblyVersion("1.15.0.0")]
|
|
12
|
+
[assembly: AssemblyFileVersion("1.15.0.0")]
|
|
13
|
+
[assembly: AssemblyInformationalVersion("1.15.0")]
|
|
14
14
|
[assembly: ComVisible(false)]
|
|
@@ -9,7 +9,7 @@ using System.Runtime.InteropServices;
|
|
|
9
9
|
[assembly: AssemblyCompany("windows-development-sandbox (open source)")]
|
|
10
10
|
[assembly: AssemblyProduct("Windows Development Sandbox")]
|
|
11
11
|
[assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
|
|
12
|
-
[assembly: AssemblyVersion("1.
|
|
13
|
-
[assembly: AssemblyFileVersion("1.
|
|
14
|
-
[assembly: AssemblyInformationalVersion("1.
|
|
12
|
+
[assembly: AssemblyVersion("1.15.0.0")]
|
|
13
|
+
[assembly: AssemblyFileVersion("1.15.0.0")]
|
|
14
|
+
[assembly: AssemblyInformationalVersion("1.15.0")]
|
|
15
15
|
[assembly: ComVisible(false)]
|
|
@@ -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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "windows-development-sandbox",
|
|
3
|
-
"version": "1.
|
|
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",
|