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.
Files changed (91) hide show
  1. package/dist/app/features/storage/storage.run.js +1 -1
  2. package/dist/bin/agent-sandbox.js +2 -2
  3. package/dist/bin/{chunk-VENWSGX2.js → chunk-BQ23GBLC.js} +411 -58
  4. package/dist/bin/{chunk-K4SU3MHJ.js → chunk-F7IMME5G.js} +1 -1
  5. package/dist/bin/{chunk-IL7BXJF2.js → chunk-LQHEUN2A.js} +1 -1
  6. package/dist/bin/claude-host.js +2 -2
  7. package/dist/bin/claude-sbx.js +2 -2
  8. package/dist/bin/codex-sbx.js +2 -2
  9. package/dist/bin/gemini-sbx.js +2 -2
  10. package/dist/bin/sandbox.js +2 -2
  11. package/dist/bin/sbx-hide.js +2 -2
  12. package/dist/bin/sbx-show.js +2 -2
  13. package/dist/cli/commands/launch/launch.js +1 -1
  14. package/dist/cli/commands/repair/checks/contained.js +1 -1
  15. package/dist/guest/daemon/sessions.d.ts +7 -0
  16. package/dist/guest/daemon/sessions.js +23 -2
  17. package/dist/guest/entries/daemon.js +1 -0
  18. package/dist/guest/sessions/pty-host.d.ts +10 -0
  19. package/dist/guest/sessions/pty-host.js +48 -3
  20. package/dist/host/channel/channel.js +1 -1
  21. package/dist/host/install/install-record.d.ts +1 -0
  22. package/dist/host/install/install-record.js +2 -0
  23. package/dist/host/sandbox/contained-deps.d.ts +2 -1
  24. package/dist/host/sandbox/contained-deps.js +6 -4
  25. package/dist/host/sharing/lifecycle.js +9 -1
  26. package/dist/host/storage/contained/contained-storage.js +13 -0
  27. package/dist/host/storage/contained/disk-port.d.ts +5 -0
  28. package/dist/host/storage/contained/helper-port.d.ts +72 -0
  29. package/dist/host/storage/contained/helper-port.js +162 -0
  30. package/dist/host/storage/contained/index.d.ts +1 -0
  31. package/dist/host/storage/contained/index.js +1 -0
  32. package/dist/host/web/bridge.d.ts +5 -0
  33. package/dist/host/web/bridge.js +11 -1
  34. package/dist/host/web/server.d.ts +2 -0
  35. package/dist/host/web/server.js +11 -1
  36. package/dist/runtime/agent.js +9 -0
  37. package/dist/runtime/daemon.js +57 -3
  38. package/dist/runtime/db.js +9 -0
  39. package/dist/runtime/main.js +195 -12
  40. package/dist/runtime/netproxy.js +9 -0
  41. package/dist/runtime/view.js +195 -12
  42. package/dist/selftest/channel.test.js +72 -1
  43. package/dist/selftest/contained.test.js +1 -37
  44. package/dist/selftest/hcs.test.js +5 -2
  45. package/dist/selftest/index.js +2 -0
  46. package/dist/selftest/install.test.js +11 -0
  47. package/dist/selftest/live-daemon.test.js +51 -1
  48. package/dist/selftest/live-terminal.test.js +112 -4
  49. package/dist/selftest/storage-helper.test.d.ts +2 -0
  50. package/dist/selftest/storage-helper.test.js +493 -0
  51. package/dist/selftest/support/vhdx.d.ts +14 -0
  52. package/dist/selftest/support/vhdx.js +72 -0
  53. package/dist/selftest/web.test.js +14 -2
  54. package/dist/shared/channel/run-dir.d.ts +6 -0
  55. package/dist/shared/channel/run-dir.js +9 -0
  56. package/dist/shared/pty/index.d.ts +3 -1
  57. package/dist/shared/pty/index.js +3 -1
  58. package/dist/shared/pty/key-gate.d.ts +24 -0
  59. package/dist/shared/pty/key-gate.js +45 -0
  60. package/dist/shared/pty/mouse-modes.d.ts +27 -0
  61. package/dist/shared/pty/mouse-modes.js +155 -0
  62. package/dist/shared/pty/terminal.d.ts +0 -5
  63. package/dist/shared/pty/terminal.js +51 -20
  64. package/dist/sync/watcher.js +1 -1
  65. package/dist/web/index.html +1 -1
  66. package/dist/web/{main-NBQ3LF4E.js → main-6XNBTJLT.js} +33 -33
  67. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  68. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  69. package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
  70. package/native/csharp/StorageHelper/Client.cs +297 -0
  71. package/native/csharp/StorageHelper/Log.cs +177 -0
  72. package/native/csharp/StorageHelper/MountRecords.cs +160 -0
  73. package/native/csharp/StorageHelper/Operations.cs +596 -0
  74. package/native/csharp/StorageHelper/Pins.cs +270 -0
  75. package/native/csharp/StorageHelper/Privileges.cs +78 -0
  76. package/native/csharp/StorageHelper/Program.cs +349 -0
  77. package/native/csharp/StorageHelper/Protocol.cs +107 -0
  78. package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
  79. package/native/csharp/StorageHelper/Rules.cs +156 -0
  80. package/native/csharp/StorageHelper/Server.cs +656 -0
  81. package/native/csharp/StorageHelper/Settings.cs +297 -0
  82. package/native/csharp/StorageHelper/StorageService.cs +53 -0
  83. package/native/csharp/StorageHelper/UserSettings.cs +206 -0
  84. package/native/csharp/StorageHelper/Vhdx.cs +230 -0
  85. package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
  86. package/native/csharp/StorageHelper/Volumes.cs +379 -0
  87. package/native/csharp/StorageHelper/Watchdog.cs +128 -0
  88. package/native/csharp/StorageHelper/Win32.cs +250 -0
  89. package/native/scripts/set-storage-helper.ps1 +203 -0
  90. package/package.json +1 -1
  91. package/sandbox/csharp/PtyHost.cs +236 -1
@@ -0,0 +1,107 @@
1
+ // What goes over the pipe. A request is one line of JSON, at most 512 bytes, with exactly two string fields:
2
+ // {"verb":"mount"|"dismount","set":"<data set name>"}; anything else is refused. The client is never asked who it is
3
+ // or where its files are: that comes from Windows and from the service's own reading of the settings. The answer is
4
+ // one JSON object: {"protocol":1,"ok":..,"message":..,"created":..,"alreadyMounted":..,"fallback":..,"image":..,
5
+ // "mount":..,"notes":[..]}. The same shape is checked on the CLI side (src/host/storage/contained/helper-port.ts).
6
+ using System;
7
+ using System.Collections.Generic;
8
+ using System.Text;
9
+ using System.Web.Script.Serialization;
10
+
11
+ namespace AgentSandbox.StorageHelper
12
+ {
13
+ public static class Protocol
14
+ {
15
+ public const int Version = 1;
16
+ public const int MaxRequestBytes = 512;
17
+
18
+ /// <summary>The verb and set of a request, or a RefusedException that says what is wrong with it.</summary>
19
+ public static void Parse(string text, out string verb, out string set)
20
+ {
21
+ verb = null;
22
+ set = null;
23
+ if (text == null || Encoding.UTF8.GetByteCount(text) > MaxRequestBytes)
24
+ {
25
+ throw new RefusedException("The request is too long.");
26
+ }
27
+ object parsed;
28
+ try
29
+ {
30
+ JavaScriptSerializer serializer = new JavaScriptSerializer();
31
+ serializer.MaxJsonLength = MaxRequestBytes;
32
+ serializer.RecursionLimit = 4;
33
+ parsed = serializer.DeserializeObject(text);
34
+ }
35
+ catch (Exception)
36
+ {
37
+ throw new RefusedException("The request is not valid JSON.");
38
+ }
39
+ Dictionary<string, object> fields = parsed as Dictionary<string, object>;
40
+ if (fields == null || fields.Count != 2 || !fields.ContainsKey("verb") || !fields.ContainsKey("set"))
41
+ {
42
+ throw new RefusedException("The request must have exactly a verb and a set.");
43
+ }
44
+ verb = fields["verb"] as string;
45
+ set = fields["set"] as string;
46
+ if (verb != "mount" && verb != "dismount")
47
+ {
48
+ throw new RefusedException("Unknown verb: only mount and dismount.");
49
+ }
50
+ if (!Rules.IsSetName(set))
51
+ {
52
+ throw new RefusedException("The data set name is not allowed.");
53
+ }
54
+ }
55
+
56
+ public static string Answer(Outcome outcome)
57
+ {
58
+ StringBuilder sb = new StringBuilder();
59
+ sb.Append("{\"protocol\":").Append(Version);
60
+ sb.Append(",\"ok\":").Append(outcome.Ok ? "true" : "false");
61
+ sb.Append(",\"message\":").Append(Quote(Safe.Text(outcome.Message, 1500)));
62
+ sb.Append(",\"created\":").Append(outcome.Created ? "true" : "false");
63
+ sb.Append(",\"alreadyMounted\":").Append(outcome.AlreadyMounted ? "true" : "false");
64
+ sb.Append(",\"fallback\":").Append(outcome.Fallback ? "true" : "false");
65
+ sb.Append(",\"image\":").Append(Quote(outcome.Image ?? ""));
66
+ sb.Append(",\"mount\":").Append(Quote(outcome.Mount ?? ""));
67
+ sb.Append(",\"notes\":[");
68
+ for (int i = 0; i < outcome.Notes.Count && i < 20; i++)
69
+ {
70
+ if (i > 0)
71
+ {
72
+ sb.Append(',');
73
+ }
74
+ sb.Append(Quote(Safe.Text(outcome.Notes[i], 300)));
75
+ }
76
+ sb.Append("]}");
77
+ return sb.ToString();
78
+ }
79
+
80
+ public static string Quote(string text)
81
+ {
82
+ StringBuilder sb = new StringBuilder();
83
+ sb.Append('"');
84
+ foreach (char ch in text ?? "")
85
+ {
86
+ if (ch == '"')
87
+ {
88
+ sb.Append("\\\"");
89
+ }
90
+ else if (ch == '\\')
91
+ {
92
+ sb.Append("\\\\");
93
+ }
94
+ else if (ch < ' ' || ch > (char)126)
95
+ {
96
+ sb.Append("\\u").Append(((int)ch).ToString("x4"));
97
+ }
98
+ else
99
+ {
100
+ sb.Append(ch);
101
+ }
102
+ }
103
+ sb.Append('"');
104
+ return sb.ToString();
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,97 @@
1
+ // How often the service answers: a few calls a minute per user and a cap for everyone together, so no client can keep
2
+ // it (and the disks) busy. A refused call is logged only once per window for each user, so the log cannot be flooded.
3
+ using System;
4
+ using System.Collections.Generic;
5
+
6
+ namespace AgentSandbox.StorageHelper
7
+ {
8
+ public sealed class RateLimiter
9
+ {
10
+ private readonly object gate = new object();
11
+ private readonly int perKey;
12
+ private readonly int total;
13
+ private readonly TimeSpan window;
14
+ private readonly Dictionary<string, Queue<DateTime>> calls = new Dictionary<string, Queue<DateTime>>(StringComparer.OrdinalIgnoreCase);
15
+ private readonly Queue<DateTime> all = new Queue<DateTime>();
16
+ private readonly Dictionary<string, DateTime> lastRefusalLogged = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
17
+
18
+ public RateLimiter(int perKey, int total, TimeSpan window)
19
+ {
20
+ this.perKey = perKey;
21
+ this.total = total;
22
+ this.window = window;
23
+ }
24
+
25
+ /// <summary>True when this call may run (and counts it).</summary>
26
+ public bool TryTake(string key, DateTime now)
27
+ {
28
+ lock (gate)
29
+ {
30
+ Trim(all, now);
31
+ Queue<DateTime> mine;
32
+ if (!calls.TryGetValue(key, out mine))
33
+ {
34
+ mine = new Queue<DateTime>();
35
+ calls[key] = mine;
36
+ }
37
+ Trim(mine, now);
38
+ DropIdleKeys(now);
39
+ if (mine.Count >= perKey || all.Count >= total)
40
+ {
41
+ return false;
42
+ }
43
+ mine.Enqueue(now);
44
+ all.Enqueue(now);
45
+ return true;
46
+ }
47
+ }
48
+
49
+ /// <summary>True for the first refusal of this key in a window (that one is logged, the rest are not).</summary>
50
+ public bool ShouldLogRefusal(string key, DateTime now)
51
+ {
52
+ lock (gate)
53
+ {
54
+ DateTime last;
55
+ if (lastRefusalLogged.TryGetValue(key, out last) && now - last < window)
56
+ {
57
+ return false;
58
+ }
59
+ if (lastRefusalLogged.Count > 1000)
60
+ {
61
+ lastRefusalLogged.Clear();
62
+ }
63
+ lastRefusalLogged[key] = now;
64
+ return true;
65
+ }
66
+ }
67
+
68
+ private void Trim(Queue<DateTime> queue, DateTime now)
69
+ {
70
+ while (queue.Count > 0 && now - queue.Peek() >= window)
71
+ {
72
+ queue.Dequeue();
73
+ }
74
+ }
75
+
76
+ private void DropIdleKeys(DateTime now)
77
+ {
78
+ if (calls.Count < 1000)
79
+ {
80
+ return;
81
+ }
82
+ List<string> idle = new List<string>();
83
+ foreach (KeyValuePair<string, Queue<DateTime>> pair in calls)
84
+ {
85
+ Trim(pair.Value, now);
86
+ if (pair.Value.Count == 0)
87
+ {
88
+ idle.Add(pair.Key);
89
+ }
90
+ }
91
+ foreach (string key in idle)
92
+ {
93
+ calls.Remove(key);
94
+ }
95
+ }
96
+ }
97
+ }
@@ -0,0 +1,156 @@
1
+ // The pure rules of contained storage, the same as native\scripts\contained-common.ps1 (Test-PlainAbsolute and
2
+ // Assert-ContainedRequest): a data set name, a plain full path, and the two paths a data set may use, which the service
3
+ // builds itself from the storage folder: <storage>\images\<set>.vhdx and <storage>\mounts\<set>.
4
+ using System;
5
+ using System.Text.RegularExpressions;
6
+
7
+ namespace AgentSandbox.StorageHelper
8
+ {
9
+ public sealed class ContainedPaths
10
+ {
11
+ public ContainedPaths(string storageRoot, string set)
12
+ {
13
+ StorageRoot = storageRoot;
14
+ Set = set;
15
+ ImagesDir = storageRoot + "\\images";
16
+ MountsDir = storageRoot + "\\mounts";
17
+ Image = ImagesDir + "\\" + set + ".vhdx";
18
+ Mount = MountsDir + "\\" + set;
19
+ }
20
+
21
+ public string StorageRoot { get; private set; }
22
+ public string Set { get; private set; }
23
+ public string ImagesDir { get; private set; }
24
+ public string MountsDir { get; private set; }
25
+ public string Image { get; private set; }
26
+ public string Mount { get; private set; }
27
+ }
28
+
29
+ public static class Rules
30
+ {
31
+ public const int MinGb = 8;
32
+ public const int MaxGb = 4096;
33
+ private static readonly Regex NameRule = new Regex("^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$", RegexOptions.CultureInvariant);
34
+ private static readonly Regex PathRule = new Regex("^[^\"<>|*?\\x00-\\x1f]+$", RegexOptions.CultureInvariant);
35
+ private static readonly Regex DriveRule = new Regex("^[A-Za-z]:\\\\", RegexOptions.CultureInvariant);
36
+ private const int MaxPathLength = 240;
37
+
38
+ public static bool IsSetName(string name)
39
+ {
40
+ return name != null && NameRule.IsMatch(name);
41
+ }
42
+
43
+ /// <summary>A drive letter and plain folder names: no UNC or \\?\ form, no . or .. part, no stream, no slash.</summary>
44
+ public static bool IsPlainAbsolute(string path)
45
+ {
46
+ if (string.IsNullOrEmpty(path) || path.Trim().Length == 0 || path.Length > MaxPathLength)
47
+ {
48
+ return false;
49
+ }
50
+ if (!PathRule.IsMatch(path) || !DriveRule.IsMatch(path))
51
+ {
52
+ return false;
53
+ }
54
+ if (path.IndexOf(':', 2) >= 0 || path.IndexOf('/') >= 0)
55
+ {
56
+ return false;
57
+ }
58
+ string rest = path.Substring(3).TrimEnd('\\');
59
+ if (rest.Length == 0)
60
+ {
61
+ return true;
62
+ }
63
+ foreach (string part in rest.Split('\\'))
64
+ {
65
+ if (part.Length == 0 || part == "." || part == ".." || part.EndsWith(" ") || part.EndsWith("."))
66
+ {
67
+ return false;
68
+ }
69
+ }
70
+ return true;
71
+ }
72
+
73
+ /// <summary>The paths of one data set under a storage folder, or an exception that says why not.</summary>
74
+ public static ContainedPaths PathsFor(string storageRoot, string set)
75
+ {
76
+ if (!IsSetName(set))
77
+ {
78
+ throw new RefusedException("The data set name is not allowed: " + Safe.Text(set, 80));
79
+ }
80
+ if (!IsPlainAbsolute(storageRoot))
81
+ {
82
+ throw new RefusedException("The storage folder is not a plain full path (no .., no network path): " + Safe.Text(storageRoot, 300));
83
+ }
84
+ string root = storageRoot.TrimEnd('\\');
85
+ if (root.Length <= 3)
86
+ {
87
+ throw new RefusedException("The storage folder cannot be a drive root.");
88
+ }
89
+ ContainedPaths paths = new ContainedPaths(root, set);
90
+ if (!IsPlainAbsolute(paths.Image) || !IsPlainAbsolute(paths.Mount))
91
+ {
92
+ throw new RefusedException("The storage folder path is too long.");
93
+ }
94
+ return paths;
95
+ }
96
+
97
+ public static void CheckMaxGb(long maxGb)
98
+ {
99
+ if (maxGb < MinGb || maxGb > MaxGb)
100
+ {
101
+ throw new RefusedException("The size limit must be between 8 and 4096 GB.");
102
+ }
103
+ }
104
+
105
+ /// <summary>The folders from the drive root down to (and with) the storage folder, the drive root first.</summary>
106
+ public static string[] FoldersDownTo(string root)
107
+ {
108
+ string[] parts = root.Substring(3).Split('\\');
109
+ string[] folders = new string[parts.Length + 1];
110
+ folders[0] = root.Substring(0, 3);
111
+ string at = root.Substring(0, 2);
112
+ for (int i = 0; i < parts.Length; i++)
113
+ {
114
+ at = at + "\\" + parts[i];
115
+ folders[i + 1] = at;
116
+ }
117
+ return folders;
118
+ }
119
+
120
+ public static bool SamePath(string a, string b)
121
+ {
122
+ return string.Equals(a.TrimEnd('\\'), b.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase);
123
+ }
124
+ }
125
+
126
+ /// <summary>A request the rules refuse: the message is for the user (no stack, no internals).</summary>
127
+ public sealed class RefusedException : Exception
128
+ {
129
+ public RefusedException(string message) : base(message)
130
+ {
131
+ }
132
+ }
133
+
134
+ public static class Safe
135
+ {
136
+ /// <summary>Printable ASCII only, at most max characters: what goes into the log and the answer from outside text.</summary>
137
+ public static string Text(string text, int max)
138
+ {
139
+ if (text == null)
140
+ {
141
+ return "";
142
+ }
143
+ System.Text.StringBuilder sb = new System.Text.StringBuilder();
144
+ foreach (char ch in text)
145
+ {
146
+ if (sb.Length >= max)
147
+ {
148
+ sb.Append("...");
149
+ break;
150
+ }
151
+ sb.Append(ch >= ' ' && ch < (char)127 ? ch : '?');
152
+ }
153
+ return sb.ToString();
154
+ }
155
+ }
156
+ }