windows-development-sandbox 1.11.7 → 1.13.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 (246) hide show
  1. package/README.md +13 -6
  2. package/config.example.json +15 -4
  3. package/dist/app/features/access/access.def.d.ts +190 -0
  4. package/dist/app/features/access/access.def.js +273 -0
  5. package/dist/app/features/access/access.run.d.ts +79 -0
  6. package/dist/app/features/access/access.run.js +129 -0
  7. package/dist/app/features/access/index.d.ts +2 -0
  8. package/dist/app/features/access/index.js +3 -0
  9. package/dist/app/features/agents/agent-entry.js +5 -0
  10. package/dist/app/features/agents/agents.def.js +1 -0
  11. package/dist/app/features/apps/apps-rows.d.ts +4 -1
  12. package/dist/app/features/apps/apps-rows.js +7 -2
  13. package/dist/app/features/apps/apps.def.d.ts +19 -0
  14. package/dist/app/features/apps/apps.def.js +50 -12
  15. package/dist/app/features/apps/apps.run.d.ts +2 -0
  16. package/dist/app/features/apps/apps.run.js +1 -1
  17. package/dist/app/features/audit/audit.def.d.ts +350 -0
  18. package/dist/app/features/audit/audit.def.js +457 -0
  19. package/dist/app/features/audit/audit.run.d.ts +110 -0
  20. package/dist/app/features/audit/audit.run.js +133 -0
  21. package/dist/app/features/audit/index.d.ts +2 -0
  22. package/dist/app/features/audit/index.js +3 -0
  23. package/dist/app/features/defs.d.ts +349 -4
  24. package/dist/app/features/defs.js +16 -1
  25. package/dist/app/features/files/files.def.js +1 -0
  26. package/dist/app/features/health/health.def.js +1 -0
  27. package/dist/app/features/sandbox/sandbox.def.js +3 -0
  28. package/dist/app/features/sessions/sessions.def.d.ts +36 -0
  29. package/dist/app/features/sessions/sessions.def.js +77 -9
  30. package/dist/app/features/sessions/sessions.run.d.ts +25 -2
  31. package/dist/app/features/sessions/sessions.run.js +71 -3
  32. package/dist/app/features/settings/settings.def.d.ts +8 -0
  33. package/dist/app/features/settings/settings.def.js +17 -0
  34. package/dist/app/features/settings/settings.run.d.ts +3 -0
  35. package/dist/app/features/settings/settings.run.js +16 -2
  36. package/dist/app/features/web/web.def.d.ts +9 -0
  37. package/dist/app/features/web/web.def.js +26 -1
  38. package/dist/app/features/web/web.run.d.ts +3 -0
  39. package/dist/app/features/web/web.run.js +11 -3
  40. package/dist/app/kernel/access.d.ts +33 -0
  41. package/dist/app/kernel/access.js +44 -0
  42. package/dist/app/kernel/define.d.ts +26 -2
  43. package/dist/app/kernel/index.d.ts +1 -0
  44. package/dist/app/kernel/index.js +1 -0
  45. package/dist/app/kernel/rpc.d.ts +5 -2
  46. package/dist/app/registry.d.ts +4 -1
  47. package/dist/app/registry.js +26 -4
  48. package/dist/cli/args.d.ts +0 -3
  49. package/dist/cli/args.js +3 -4
  50. package/dist/cli/commands/index.d.ts +1 -1
  51. package/dist/cli/commands/index.js +1 -1
  52. package/dist/cli/commands/launch/prepare.js +1 -1
  53. package/dist/cli/commands/maintenance/index.d.ts +0 -1
  54. package/dist/cli/commands/maintenance/index.js +0 -1
  55. package/dist/cli/commands/registry.js +3 -2
  56. package/dist/cli/commands/remote.d.ts +2 -0
  57. package/dist/cli/commands/remote.js +49 -0
  58. package/dist/cli/commands/sessions/reopen.js +3 -0
  59. package/dist/cli/commands/web.js +66 -0
  60. package/dist/cli/manifest.d.ts +63 -45
  61. package/dist/cli/manifest.js +33 -18
  62. package/dist/cli/menu/io-menu.d.ts +5 -3
  63. package/dist/cli/menu/io-menu.js +12 -5
  64. package/dist/cli/menu/pages/apps-page.js +2 -15
  65. package/dist/cli/menu/pages/maintenance-page.d.ts +4 -0
  66. package/dist/cli/menu/pages/maintenance-page.js +51 -8
  67. package/dist/cli/menu/remote-menu.d.ts +19 -0
  68. package/dist/cli/menu/remote-menu.js +108 -0
  69. package/dist/cli/menu/views.js +4 -0
  70. package/dist/cli/operations/handler.js +15 -1
  71. package/dist/cli/services/remote-client.d.ts +16 -0
  72. package/dist/cli/services/remote-client.js +141 -0
  73. package/dist/cli/services/sessions-service.d.ts +5 -0
  74. package/dist/cli/services/sessions-service.js +17 -1
  75. package/dist/core/config/defaults.js +12 -2
  76. package/dist/core/local-config/file.js +16 -2
  77. package/dist/core/local-config/overrides.js +7 -0
  78. package/dist/core/run/versions.js +9 -8
  79. package/dist/core/schema/apps.d.ts +9 -0
  80. package/dist/core/schema/apps.js +9 -0
  81. package/dist/core/schema/config.d.ts +23 -3
  82. package/dist/core/schema/config.js +42 -5
  83. package/dist/core/schema/sandbox.d.ts +1 -0
  84. package/dist/core/schema/sandbox.js +11 -3
  85. package/dist/core/schema/web.d.ts +51 -0
  86. package/dist/core/schema/web.js +46 -1
  87. package/dist/core/util/fs.d.ts +5 -0
  88. package/dist/core/util/fs.js +23 -0
  89. package/dist/core/util/index.d.ts +1 -1
  90. package/dist/core/util/index.js +1 -1
  91. package/dist/guest/apps/app-installer.js +50 -4
  92. package/dist/guest/apps/index.d.ts +1 -1
  93. package/dist/guest/apps/index.js +1 -1
  94. package/dist/guest/apps/sources.js +1 -1
  95. package/dist/guest/audit/parse.d.ts +32 -2
  96. package/dist/guest/audit/parse.js +43 -5
  97. package/dist/guest/audit/process-audit.d.ts +3 -1
  98. package/dist/guest/audit/process-audit.js +9 -6
  99. package/dist/guest/boot/boot.js +12 -3
  100. package/dist/guest/boot/guide.d.ts +1 -1
  101. package/dist/guest/boot/guide.js +27 -3
  102. package/dist/guest/daemon/daemon.js +2 -0
  103. package/dist/guest/daemon/idle-shutdown.js +2 -1
  104. package/dist/guest/daemon/sessions.d.ts +23 -2
  105. package/dist/guest/daemon/sessions.js +151 -20
  106. package/dist/guest/entries/daemon.js +2 -1
  107. package/dist/guest/git/decision.js +7 -0
  108. package/dist/guest/sessions/cmdline.d.ts +14 -0
  109. package/dist/guest/sessions/cmdline.js +10 -0
  110. package/dist/guest/setup/addon-mcp.d.ts +2 -0
  111. package/dist/guest/setup/addon-mcp.js +49 -0
  112. package/dist/guest/setup/agent-addons.d.ts +9 -0
  113. package/dist/guest/setup/agent-addons.js +22 -0
  114. package/dist/guest/setup/headroom.d.ts +7 -0
  115. package/dist/guest/setup/headroom.js +60 -0
  116. package/dist/guest/setup/managed-policy.d.ts +2 -1
  117. package/dist/guest/setup/managed-policy.js +15 -6
  118. package/dist/guest/toolchains/plan.js +8 -3
  119. package/dist/guest/toolchains/setup.js +3 -0
  120. package/dist/host/audit/attribute.d.ts +46 -0
  121. package/dist/host/audit/attribute.js +76 -0
  122. package/dist/host/audit/collect.d.ts +29 -3
  123. package/dist/host/audit/collect.js +256 -61
  124. package/dist/host/audit/git-commits.d.ts +29 -0
  125. package/dist/host/audit/git-commits.js +157 -0
  126. package/dist/host/audit/index.d.ts +5 -0
  127. package/dist/host/audit/index.js +6 -0
  128. package/dist/host/audit/log.d.ts +45 -7
  129. package/dist/host/audit/log.js +148 -45
  130. package/dist/host/audit/redact.d.ts +2 -0
  131. package/dist/host/audit/redact.js +67 -0
  132. package/dist/host/channel/channel.d.ts +5 -1
  133. package/dist/host/channel/channel.js +12 -0
  134. package/dist/host/guard/agent-files.js +7 -1
  135. package/dist/host/guard/commands.js +13 -1
  136. package/dist/host/guard/git-config.js +27 -7
  137. package/dist/host/guard/git-dirs.js +78 -3
  138. package/dist/host/guard/mark-of-the-web.js +3 -1
  139. package/dist/host/guard/nested-repos.js +7 -2
  140. package/dist/host/guard/store.js +1 -0
  141. package/dist/host/guard/types.d.ts +2 -0
  142. package/dist/host/launch/mappings.js +11 -1
  143. package/dist/host/launch/project-scan.js +2 -21
  144. package/dist/host/launch/stage.js +4 -0
  145. package/dist/host/life/session-ledger.js +2 -1
  146. package/dist/host/remote/access-store.d.ts +156 -0
  147. package/dist/host/remote/access-store.js +308 -0
  148. package/dist/host/remote/cert.d.ts +20 -0
  149. package/dist/host/remote/cert.js +103 -0
  150. package/dist/host/remote/door-client.d.ts +13 -0
  151. package/dist/host/remote/door-client.js +54 -0
  152. package/dist/host/remote/firewall.d.ts +44 -0
  153. package/dist/host/remote/firewall.js +118 -0
  154. package/dist/host/remote/gate.d.ts +125 -0
  155. package/dist/host/remote/gate.js +391 -0
  156. package/dist/host/remote/index.d.ts +8 -0
  157. package/dist/host/remote/index.js +10 -0
  158. package/dist/host/remote/lan.d.ts +24 -0
  159. package/dist/host/remote/lan.js +88 -0
  160. package/dist/host/remote/tls-store.d.ts +22 -0
  161. package/dist/host/remote/tls-store.js +63 -0
  162. package/dist/host/remote/windows-helper.d.ts +67 -0
  163. package/dist/host/remote/windows-helper.js +129 -0
  164. package/dist/host/web/bridge.d.ts +5 -1
  165. package/dist/host/web/bridge.js +6 -2
  166. package/dist/host/web/browser.js +1 -1
  167. package/dist/host/web/control.d.ts +4 -1
  168. package/dist/host/web/control.js +28 -4
  169. package/dist/host/web/rpc.d.ts +16 -2
  170. package/dist/host/web/rpc.js +37 -4
  171. package/dist/host/web/runner.d.ts +4 -0
  172. package/dist/host/web/runner.js +12 -0
  173. package/dist/host/web/server.d.ts +18 -2
  174. package/dist/host/web/server.js +97 -24
  175. package/dist/host/web/state.d.ts +5 -0
  176. package/dist/host/web/state.js +8 -0
  177. package/dist/runtime/agent.js +184 -18
  178. package/dist/runtime/daemon.js +772 -338
  179. package/dist/runtime/db.js +184 -18
  180. package/dist/runtime/gitguard.js +6 -0
  181. package/dist/runtime/main.js +966 -553
  182. package/dist/runtime/netproxy.js +184 -18
  183. package/dist/runtime/view.js +186 -18
  184. package/dist/selftest/app.test.js +66 -7
  185. package/dist/selftest/audit.test.js +354 -47
  186. package/dist/selftest/channel.test.js +16 -0
  187. package/dist/selftest/cmdline.test.js +16 -0
  188. package/dist/selftest/git-access.test.js +8 -0
  189. package/dist/selftest/hcs.test.js +35 -0
  190. package/dist/selftest/hostile-input.test.js +12 -1
  191. package/dist/selftest/index.js +2 -0
  192. package/dist/selftest/live-daemon.test.js +25 -0
  193. package/dist/selftest/local-config.test.js +30 -0
  194. package/dist/selftest/menu-screens.js +1 -1
  195. package/dist/selftest/menu.test.js +3 -3
  196. package/dist/selftest/remote.test.d.ts +2 -0
  197. package/dist/selftest/remote.test.js +536 -0
  198. package/dist/selftest/repo-guard.test.js +56 -2
  199. package/dist/selftest/resume.test.js +2 -1
  200. package/dist/selftest/sharing.test.js +9 -2
  201. package/dist/selftest/support/engine-golden.js +9 -0
  202. package/dist/selftest/support/fixtures.d.ts +14 -0
  203. package/dist/selftest/support/web-preview.js +157 -0
  204. package/dist/selftest/toolchains.test.js +147 -2
  205. package/dist/selftest/web.test.js +2 -0
  206. package/dist/shared/apps/agent-addons.d.ts +64 -0
  207. package/dist/shared/apps/agent-addons.js +95 -0
  208. package/dist/shared/apps/catalogue.d.ts +33 -2
  209. package/dist/shared/apps/catalogue.js +102 -11
  210. package/dist/shared/apps/index.d.ts +3 -2
  211. package/dist/shared/apps/index.js +3 -2
  212. package/dist/shared/apps/selection.d.ts +2 -0
  213. package/dist/shared/apps/selection.js +7 -0
  214. package/dist/shared/channel/schema.d.ts +23 -0
  215. package/dist/shared/channel/schema.js +8 -0
  216. package/dist/shared/pty/protocol.d.ts +2 -0
  217. package/dist/shared/pty/protocol.js +2 -0
  218. package/dist/sync/watcher.js +9 -1
  219. package/dist/web/index.html +2 -2
  220. package/dist/web/{main-RQFN4WBY.css → main-UAQPI5XD.css} +1 -1
  221. package/dist/web/main-Z2WYAE2Z.js +221 -0
  222. package/native/csharp/HcsHost/AssemblyInfo.cs +5 -3
  223. package/native/csharp/HcsHost/Hcs.cs +9 -0
  224. package/native/csharp/HcsHost/Program.cs +26 -1
  225. package/native/csharp/HcsHost/Share.cs +173 -0
  226. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  227. package/native/csharp/HostTools/Program.cs +17 -1
  228. package/native/csharp/HostTools/WebHelper.cs +499 -0
  229. package/native/scripts/get-network-profile.ps1 +19 -0
  230. package/native/scripts/set-folder-access.ps1 +11 -0
  231. package/native/scripts/set-web-firewall.ps1 +103 -0
  232. package/package.json +1 -1
  233. package/sandbox/addons/ccusage/package-lock.json +111 -0
  234. package/sandbox/addons/ccusage/package.json +7 -0
  235. package/sandbox/addons/context7/package-lock.json +1313 -0
  236. package/sandbox/addons/context7/package.json +7 -0
  237. package/sandbox/addons/headroom/requirements.in +1 -0
  238. package/sandbox/addons/headroom/requirements.txt +2922 -0
  239. package/sandbox/addons/playwright-mcp/package-lock.json +56 -0
  240. package/sandbox/addons/playwright-mcp/package.json +7 -0
  241. package/sandbox/csharp/PtyHost.cs +20 -0
  242. package/schemas/config.schema.json +78 -4
  243. package/schemas/sandbox-config.schema.json +2 -20
  244. package/dist/cli/commands/maintenance/audit.d.ts +0 -3
  245. package/dist/cli/commands/maintenance/audit.js +0 -58
  246. package/dist/web/main-K2JMCAZI.js +0 -219
@@ -0,0 +1,499 @@
1
+ // Windows sign-in for the web UI (docs/design/remote-access.md). C# 5.
2
+ // web-helper --pipe NAME --server-pid N long-running, for the owner's web server: serves the pipe NAME (one client
3
+ // at a time) and asks Windows who connected (the client's token: user SID, integrity, AppContainer), writing
4
+ // "door\t<n>\t<sid>\t<integrity>\t<appcontainer 0|1>\t<DOMAIN\user>" to stdout and writing the server's answer
5
+ // ("answer\t<n>\t<text>" on stdin) back down the pipe. Requests on stdin, one per line, tab-separated:
6
+ // peer <id> <local address> <local port> <remote address> <remote port>
7
+ // the Windows user of the process at the other end of that loopback TCP connection (our server's end is
8
+ // local): "peer\t<id>\tok\t<sid>\t<DOMAIN\user>" or "peer\t<id>\tnone\t<reason>"
9
+ // lookup <id> <name> "lookup\t<id>\tok\t<sid>\t<DOMAIN\user>" or "lookup\t<id>\tnone"
10
+ // Ends when stdin closes.
11
+ // door-client --owner USER for another user's sandbox remote: USER (a name or a SID) is the owner; connects to
12
+ // agent-sandbox-web-<their SID>, checks that the pipe's server runs in a session of that SID (so nobody else can
13
+ // squat the name), sends "hello", prints the answer line. Exit 3: the pipe is not the owner's; 4: no such pipe;
14
+ // 5: no such user.
15
+ using System;
16
+ using System.ComponentModel;
17
+ using System.Diagnostics;
18
+ using System.IO;
19
+ using System.IO.Pipes;
20
+ using System.Net;
21
+ using System.Runtime.InteropServices;
22
+ using System.Security.AccessControl;
23
+ using System.Security.Principal;
24
+ using System.Text;
25
+ using System.Threading;
26
+
27
+ namespace AgentSandbox
28
+ {
29
+ public static class WebHelper
30
+ {
31
+ private const int AfInet = 2;
32
+ private const int TcpTableOwnerPidConnections = 4;
33
+ private const int ErrorInsufficientBuffer = 122;
34
+ private const int StateEstablished = 5;
35
+ private const int WtsUserName = 5;
36
+ private const int WtsDomainName = 7;
37
+ private const int TokenIntegrityLevel = 25;
38
+ private const int TokenIsAppContainer = 29;
39
+ private const int AnswerTimeoutMs = 10000;
40
+ private const int MaxLine = 512;
41
+ public const int NotOwner = 3;
42
+ public const int NoPipe = 4;
43
+ public const int NoUser = 5;
44
+ public const string PipePrefix = "agent-sandbox-web-";
45
+
46
+ private static readonly object StdoutLock = new object();
47
+ private static readonly object AnswerLock = new object();
48
+ private static string pendingAnswer;
49
+ private static int pendingNumber;
50
+
51
+ private static class NativeMethods
52
+ {
53
+ [DllImport("iphlpapi.dll", SetLastError = true)]
54
+ public static extern int GetExtendedTcpTable(IntPtr table, ref int size, bool order, int family, int tableClass, int reserved);
55
+
56
+ [DllImport("kernel32.dll", SetLastError = true)]
57
+ public static extern bool ProcessIdToSessionId(uint processId, out uint sessionId);
58
+
59
+ [DllImport("wtsapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
60
+ public static extern bool WTSQuerySessionInformation(IntPtr server, uint sessionId, int infoClass, out IntPtr buffer, out int bytes);
61
+
62
+ [DllImport("wtsapi32.dll")]
63
+ public static extern void WTSFreeMemory(IntPtr memory);
64
+
65
+ [DllImport("advapi32.dll", SetLastError = true)]
66
+ public static extern bool GetTokenInformation(IntPtr token, int infoClass, IntPtr info, int length, out int returned);
67
+
68
+ [DllImport("advapi32.dll", SetLastError = true)]
69
+ public static extern IntPtr GetSidSubAuthorityCount(IntPtr sid);
70
+
71
+ [DllImport("advapi32.dll", SetLastError = true)]
72
+ public static extern IntPtr GetSidSubAuthority(IntPtr sid, uint index);
73
+
74
+ [DllImport("kernel32.dll", SetLastError = true)]
75
+ public static extern bool GetNamedPipeServerProcessId(IntPtr pipe, out uint serverProcessId);
76
+ }
77
+
78
+ private static void Emit(string line)
79
+ {
80
+ lock (StdoutLock)
81
+ {
82
+ Console.Out.WriteLine(line);
83
+ Console.Out.Flush();
84
+ }
85
+ }
86
+
87
+ private static string Clean(string text)
88
+ {
89
+ StringBuilder b = new StringBuilder();
90
+ foreach (char c in text ?? "")
91
+ {
92
+ if (c >= ' ' && c != '\t' && c != '\u007f')
93
+ {
94
+ b.Append(c);
95
+ }
96
+ }
97
+ return b.ToString();
98
+ }
99
+
100
+ /// <summary>Serves the pipe and answers requests until stdin closes.</summary>
101
+ public static int Serve(string pipeName, int serverPid)
102
+ {
103
+ // An empty name: no Windows sign-in, only the peer checks and lookups.
104
+ if (pipeName.Length > 0)
105
+ {
106
+ Thread door = new Thread(delegate () { DoorLoop(pipeName); });
107
+ door.IsBackground = true;
108
+ door.Start();
109
+ }
110
+ string line;
111
+ while ((line = Console.In.ReadLine()) != null)
112
+ {
113
+ if (line.Length > MaxLine)
114
+ {
115
+ continue;
116
+ }
117
+ string[] f = line.Split('\t');
118
+ try
119
+ {
120
+ if (f[0] == "peer" && f.Length == 6)
121
+ {
122
+ Emit(Peer(f[1], f[2], f[3], f[4], f[5], serverPid));
123
+ }
124
+ else if (f[0] == "lookup" && f.Length == 3)
125
+ {
126
+ Emit(Lookup(f[1], f[2]));
127
+ }
128
+ else if (f[0] == "answer" && f.Length == 3)
129
+ {
130
+ int n;
131
+ if (int.TryParse(f[1], out n))
132
+ {
133
+ lock (AnswerLock)
134
+ {
135
+ if (n == pendingNumber)
136
+ {
137
+ pendingAnswer = f[2];
138
+ Monitor.PulseAll(AnswerLock);
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ catch (Exception e)
145
+ {
146
+ Emit("error\t" + Clean(f.Length > 1 ? f[1] : "") + "\t" + Clean(e.Message));
147
+ }
148
+ }
149
+ return 0;
150
+ }
151
+
152
+ private static PipeSecurity Security()
153
+ {
154
+ PipeSecurity security = new PipeSecurity();
155
+ SecurityIdentifier me = WindowsIdentity.GetCurrent().User;
156
+ security.AddAccessRule(new PipeAccessRule(me, PipeAccessRights.FullControl, AccessControlType.Allow));
157
+ security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), PipeAccessRights.FullControl, AccessControlType.Allow));
158
+ security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null), PipeAccessRights.ReadWrite, AccessControlType.Allow));
159
+ return security;
160
+ }
161
+
162
+ private static void DoorLoop(string pipeName)
163
+ {
164
+ int number = 0;
165
+ while (true)
166
+ {
167
+ NamedPipeServerStream pipe;
168
+ try
169
+ {
170
+ // One instance: if someone else made this name first, this fails and sign-in stays off.
171
+ pipe = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.None, 1024, 1024, Security());
172
+ }
173
+ catch (Exception e)
174
+ {
175
+ Emit("door-failed\t" + Clean(e.Message));
176
+ return;
177
+ }
178
+ try
179
+ {
180
+ pipe.WaitForConnection();
181
+ number++;
182
+ string sid = "";
183
+ string user = "";
184
+ int integrity = 0;
185
+ bool container = true;
186
+ pipe.RunAsClient(delegate ()
187
+ {
188
+ using (WindowsIdentity client = WindowsIdentity.GetCurrent(TokenAccessLevels.Query))
189
+ {
190
+ sid = client.User == null ? "" : client.User.Value;
191
+ user = client.Name;
192
+ integrity = Integrity(client.Token);
193
+ container = IsAppContainer(client.Token);
194
+ }
195
+ });
196
+ StreamReader reader = new StreamReader(pipe, Encoding.UTF8, false, 1024, true);
197
+ string hello = ReadBounded(reader);
198
+ string answer = "refused\tNo answer.";
199
+ if (hello == "hello")
200
+ {
201
+ lock (AnswerLock)
202
+ {
203
+ pendingNumber = number;
204
+ pendingAnswer = null;
205
+ Emit("door\t" + number + "\t" + sid + "\t" + integrity + "\t" + (container ? "1" : "0") + "\t" + Clean(user));
206
+ Monitor.Wait(AnswerLock, AnswerTimeoutMs);
207
+ if (pendingAnswer != null)
208
+ {
209
+ answer = pendingAnswer;
210
+ }
211
+ pendingNumber = 0;
212
+ }
213
+ }
214
+ byte[] bytes = Encoding.UTF8.GetBytes(Clean(answer.Replace('\t', ' ')) + "\n");
215
+ pipe.Write(bytes, 0, bytes.Length);
216
+ pipe.Flush();
217
+ pipe.WaitForPipeDrain();
218
+ }
219
+ catch (Exception)
220
+ {
221
+ // A client that went away or sent nonsense: next one.
222
+ }
223
+ finally
224
+ {
225
+ pipe.Dispose();
226
+ }
227
+ }
228
+ }
229
+
230
+ private static string ReadBounded(StreamReader reader)
231
+ {
232
+ StringBuilder b = new StringBuilder();
233
+ int c;
234
+ while (b.Length < MaxLine && (c = reader.Read()) >= 0 && c != '\n')
235
+ {
236
+ if (c != '\r')
237
+ {
238
+ b.Append((char)c);
239
+ }
240
+ }
241
+ return b.ToString();
242
+ }
243
+
244
+ private static int Integrity(IntPtr token)
245
+ {
246
+ int size;
247
+ NativeMethods.GetTokenInformation(token, TokenIntegrityLevel, IntPtr.Zero, 0, out size);
248
+ if (size <= 0)
249
+ {
250
+ return 0;
251
+ }
252
+ IntPtr info = Marshal.AllocHGlobal(size);
253
+ try
254
+ {
255
+ if (!NativeMethods.GetTokenInformation(token, TokenIntegrityLevel, info, size, out size))
256
+ {
257
+ return 0;
258
+ }
259
+ IntPtr sid = Marshal.ReadIntPtr(info);
260
+ int count = Marshal.ReadByte(NativeMethods.GetSidSubAuthorityCount(sid));
261
+ return count == 0 ? 0 : Marshal.ReadInt32(NativeMethods.GetSidSubAuthority(sid, (uint)(count - 1)));
262
+ }
263
+ finally
264
+ {
265
+ Marshal.FreeHGlobal(info);
266
+ }
267
+ }
268
+
269
+ private static bool IsAppContainer(IntPtr token)
270
+ {
271
+ IntPtr info = Marshal.AllocHGlobal(4);
272
+ try
273
+ {
274
+ int returned;
275
+ // A token that cannot be asked counts as an AppContainer: refused.
276
+ return !NativeMethods.GetTokenInformation(token, TokenIsAppContainer, info, 4, out returned) || Marshal.ReadInt32(info) != 0;
277
+ }
278
+ finally
279
+ {
280
+ Marshal.FreeHGlobal(info);
281
+ }
282
+ }
283
+
284
+ private struct Row
285
+ {
286
+ public int State;
287
+ public uint LocalAddr;
288
+ public int LocalPort;
289
+ public uint RemoteAddr;
290
+ public int RemotePort;
291
+ public uint Pid;
292
+ }
293
+
294
+ private static Row[] TcpTable()
295
+ {
296
+ int size = 0;
297
+ NativeMethods.GetExtendedTcpTable(IntPtr.Zero, ref size, false, AfInet, TcpTableOwnerPidConnections, 0);
298
+ for (int attempt = 0; attempt < 5; attempt++)
299
+ {
300
+ IntPtr buffer = Marshal.AllocHGlobal(size);
301
+ try
302
+ {
303
+ int result = NativeMethods.GetExtendedTcpTable(buffer, ref size, false, AfInet, TcpTableOwnerPidConnections, 0);
304
+ if (result == ErrorInsufficientBuffer)
305
+ {
306
+ continue;
307
+ }
308
+ if (result != 0)
309
+ {
310
+ throw new Win32Exception(result);
311
+ }
312
+ int count = Marshal.ReadInt32(buffer);
313
+ Row[] rows = new Row[count];
314
+ for (int i = 0; i < count; i++)
315
+ {
316
+ IntPtr at = buffer + 4 + i * 24;
317
+ rows[i].State = Marshal.ReadInt32(at);
318
+ rows[i].LocalAddr = (uint)Marshal.ReadInt32(at + 4);
319
+ rows[i].LocalPort = Port(Marshal.ReadInt32(at + 8));
320
+ rows[i].RemoteAddr = (uint)Marshal.ReadInt32(at + 12);
321
+ rows[i].RemotePort = Port(Marshal.ReadInt32(at + 16));
322
+ rows[i].Pid = (uint)Marshal.ReadInt32(at + 20);
323
+ }
324
+ return rows;
325
+ }
326
+ finally
327
+ {
328
+ Marshal.FreeHGlobal(buffer);
329
+ }
330
+ }
331
+ throw new InvalidOperationException("The TCP table kept growing.");
332
+ }
333
+
334
+ private static int Port(int raw)
335
+ {
336
+ return ((raw & 0xff) << 8) | ((raw >> 8) & 0xff);
337
+ }
338
+
339
+ private static uint Address(string text)
340
+ {
341
+ IPAddress ip = IPAddress.Parse(text);
342
+ byte[] b = ip.GetAddressBytes();
343
+ if (b.Length != 4)
344
+ {
345
+ throw new ArgumentException("IPv4 only.");
346
+ }
347
+ return BitConverter.ToUInt32(b, 0);
348
+ }
349
+
350
+ /// <summary>The process at the client end, read twice around the session lookup (same pid, still established).</summary>
351
+ private static string Peer(string id, string localAddr, string localPort, string remoteAddr, string remotePort, int serverPid)
352
+ {
353
+ uint la = Address(localAddr);
354
+ uint ra = Address(remoteAddr);
355
+ int lp = int.Parse(localPort);
356
+ int rp = int.Parse(remotePort);
357
+ uint pid;
358
+ string why = ClientPid(la, lp, ra, rp, serverPid, out pid);
359
+ if (why != null)
360
+ {
361
+ return "peer\t" + id + "\tnone\t" + why;
362
+ }
363
+ uint session;
364
+ if (!NativeMethods.ProcessIdToSessionId(pid, out session))
365
+ {
366
+ return "peer\t" + id + "\tnone\tno session";
367
+ }
368
+ if (session == 0)
369
+ {
370
+ return "peer\t" + id + "\tnone\tservice session";
371
+ }
372
+ string account = SessionUser(session);
373
+ uint again;
374
+ if (account == null || ClientPid(la, lp, ra, rp, serverPid, out again) != null || again != pid)
375
+ {
376
+ return "peer\t" + id + "\tnone\tchanged while checking";
377
+ }
378
+ string sid = SidOf(account);
379
+ return sid == null ? "peer\t" + id + "\tnone\tunknown user" : "peer\t" + id + "\tok\t" + sid + "\t" + Clean(account);
380
+ }
381
+
382
+ private static string ClientPid(uint la, int lp, uint ra, int rp, int serverPid, out uint pid)
383
+ {
384
+ pid = 0;
385
+ bool ours = false;
386
+ foreach (Row r in TcpTable())
387
+ {
388
+ if (r.LocalAddr == la && r.LocalPort == lp && r.RemoteAddr == ra && r.RemotePort == rp)
389
+ {
390
+ ours = r.Pid == (uint)serverPid && r.State == StateEstablished;
391
+ }
392
+ if (r.LocalAddr == ra && r.LocalPort == rp && r.RemoteAddr == la && r.RemotePort == lp && r.State == StateEstablished)
393
+ {
394
+ pid = r.Pid;
395
+ }
396
+ }
397
+ if (!ours)
398
+ {
399
+ return "not our connection";
400
+ }
401
+ return pid == 0 ? "no such connection" : null;
402
+ }
403
+
404
+ private static string SessionUser(uint session)
405
+ {
406
+ string user = SessionText(session, WtsUserName);
407
+ string domain = SessionText(session, WtsDomainName);
408
+ if (string.IsNullOrEmpty(user))
409
+ {
410
+ return null;
411
+ }
412
+ return string.IsNullOrEmpty(domain) ? user : domain + "\\" + user;
413
+ }
414
+
415
+ private static string SessionText(uint session, int infoClass)
416
+ {
417
+ IntPtr buffer;
418
+ int bytes;
419
+ if (!NativeMethods.WTSQuerySessionInformation(IntPtr.Zero, session, infoClass, out buffer, out bytes))
420
+ {
421
+ return null;
422
+ }
423
+ try
424
+ {
425
+ return Marshal.PtrToStringUni(buffer);
426
+ }
427
+ finally
428
+ {
429
+ NativeMethods.WTSFreeMemory(buffer);
430
+ }
431
+ }
432
+
433
+ private static string SidOf(string account)
434
+ {
435
+ try
436
+ {
437
+ return ((SecurityIdentifier)new NTAccount(account).Translate(typeof(SecurityIdentifier))).Value;
438
+ }
439
+ catch (IdentityNotMappedException)
440
+ {
441
+ return null;
442
+ }
443
+ }
444
+
445
+ private static string Lookup(string id, string name)
446
+ {
447
+ string sid = SidOf(name);
448
+ if (sid == null)
449
+ {
450
+ return "lookup\t" + id + "\tnone";
451
+ }
452
+ string full = ((NTAccount)new SecurityIdentifier(sid).Translate(typeof(NTAccount))).Value;
453
+ return "lookup\t" + id + "\tok\t" + sid + "\t" + Clean(full);
454
+ }
455
+
456
+ /// <summary>The other user's side: the pipe must be served from a session of the owner; prints the answer.</summary>
457
+ public static int Client(string owner)
458
+ {
459
+ string ownerSid = owner.StartsWith("S-1-", StringComparison.OrdinalIgnoreCase) ? owner : SidOf(owner);
460
+ if (ownerSid == null)
461
+ {
462
+ return NoUser;
463
+ }
464
+ string pipeName = PipePrefix + ownerSid;
465
+ using (NamedPipeClientStream pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.None, TokenImpersonationLevel.Identification))
466
+ {
467
+ try
468
+ {
469
+ pipe.Connect(3000);
470
+ }
471
+ catch (TimeoutException)
472
+ {
473
+ return NoPipe;
474
+ }
475
+ catch (IOException)
476
+ {
477
+ return NoPipe;
478
+ }
479
+ uint serverPid;
480
+ uint session;
481
+ if (!NativeMethods.GetNamedPipeServerProcessId(pipe.SafePipeHandle.DangerousGetHandle(), out serverPid) || !NativeMethods.ProcessIdToSessionId(serverPid, out session))
482
+ {
483
+ return NotOwner;
484
+ }
485
+ string account = SessionUser(session);
486
+ if (account == null || !string.Equals(SidOf(account), ownerSid, StringComparison.OrdinalIgnoreCase))
487
+ {
488
+ return NotOwner;
489
+ }
490
+ byte[] hello = Encoding.UTF8.GetBytes("hello\n");
491
+ pipe.Write(hello, 0, hello.Length);
492
+ pipe.Flush();
493
+ StreamReader reader = new StreamReader(pipe, Encoding.UTF8);
494
+ Console.Out.Write(Clean(ReadBounded(reader)));
495
+ return 0;
496
+ }
497
+ }
498
+ }
499
+ }
@@ -0,0 +1,19 @@
1
+ #Requires -Version 5.1
2
+ # The network an address of this PC is on, for the web UI's LAN mode: its category (Private, Public, DomainAuthenticated)
3
+ # and name, as one line of JSON. LAN mode starts only on a Private network, and only the one it was turned on for.
4
+ param(
5
+ [Parameter(Mandatory = $true)][string]$Address
6
+ )
7
+ $ErrorActionPreference = 'Stop'
8
+ if ($Address -notmatch '^\d{1,3}(\.\d{1,3}){3}$') { throw 'An IPv4 address, please.' }
9
+ $ip = Get-NetIPAddress -IPAddress $Address -AddressFamily IPv4 -ErrorAction SilentlyContinue | Select-Object -First 1
10
+ if (-not $ip) {
11
+ @{ found = $false; category = ''; name = '' } | ConvertTo-Json -Compress
12
+ exit 0
13
+ }
14
+ $conn = Get-NetConnectionProfile -InterfaceIndex $ip.InterfaceIndex -ErrorAction SilentlyContinue | Select-Object -First 1
15
+ if (-not $conn) {
16
+ @{ found = $true; category = ''; name = '' } | ConvertTo-Json -Compress
17
+ exit 0
18
+ }
19
+ @{ found = $true; category = [string]$conn.NetworkCategory; name = [string]$conn.Name } | ConvertTo-Json -Compress
@@ -50,6 +50,17 @@ try {
50
50
  $who, 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow')))
51
51
  }
52
52
  $have = Get-Acl -LiteralPath $e.path
53
+ # A folder someone else owns is never made "private" for you: under an all-users install another member of
54
+ # the group could have made %ProgramData%\AgentSandbox\users\<you> first, and as its owner can give
55
+ # themselves access to every file you write later. Administrators own what an elevated you creates.
56
+ $owner = $have.GetOwner([System.Security.Principal.SecurityIdentifier])
57
+ $adminsSid = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544')
58
+ $elevated = (New-Object System.Security.Principal.WindowsPrincipal([System.Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole($adminsSid)
59
+ if (-not ($owner -eq $me -or ($owner -eq $adminsSid -and $elevated))) {
60
+ $ownerName = $owner.Value
61
+ try { $ownerName = $owner.Translate([System.Security.Principal.NTAccount]).Value } catch { $ownerName = $owner.Value }
62
+ throw "$($e.path) belongs to $ownerName, not to you (someone else may have created it). An administrator must delete it before you use it."
63
+ }
53
64
  if ($have.GetSecurityDescriptorSddlForm('Access') -eq $want.GetSecurityDescriptorSddlForm('Access')) { continue }
54
65
  $children = @(Get-ChildItem -LiteralPath $e.path -Force | Select-Object -First 1)
55
66
  if ($children.Count -eq 0) { Set-OwnDacl $e.path $want } else { Set-Acl -LiteralPath $e.path -AclObject $want }
@@ -0,0 +1,103 @@
1
+ #Requires -Version 5.1
2
+ # Adds or removes the one Windows Firewall rule of the web UI's LAN mode (docs/design/remote-access.md). Run elevated
3
+ # through run-elevated.ps1 (one Windows permission prompt). The request is a JSON file:
4
+ # { action: "add" | "remove", ruleName, port, localAddress, program }
5
+ # Everything is checked again here, from the bytes whose SHA-256 was given: the rule name is exactly ours, the port is
6
+ # 1024-65535, the local address is a private IPv4 address (10/8, 172.16/12, 192.168/16), the program is an existing
7
+ # node.exe not in a temp folder. The rule only allows inbound TCP to that address and port, from the local subnet, on
8
+ # Private networks: never the internet. -DryRun checks and prints what it would run (as JSON) and changes nothing; a
9
+ # real run writes result.json next to the request file.
10
+ param(
11
+ [Parameter(Mandatory = $true)][string]$Json,
12
+ [string]$Sha256 = '',
13
+ [switch]$DryRun
14
+ )
15
+ $ErrorActionPreference = 'Stop'
16
+ $RuleName = 'Agent Sandbox web UI (LAN)'
17
+ $result = @{ ok = $false; message = ''; commands = @() }
18
+
19
+ function Test-PrivateIPv4([string]$Ip) {
20
+ if ($Ip -notmatch '^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$') { return $false }
21
+ $p = @([int]$Matches[1], [int]$Matches[2], [int]$Matches[3], [int]$Matches[4])
22
+ foreach ($n in $p) { if ($n -gt 255) { return $false } }
23
+ return ($p[0] -eq 10) -or ($p[0] -eq 172 -and $p[1] -ge 16 -and $p[1] -le 31) -or ($p[0] -eq 192 -and $p[1] -eq 168)
24
+ }
25
+
26
+ function Read-Request([string]$Path, [string]$Expected) {
27
+ $bytes = [IO.File]::ReadAllBytes($Path)
28
+ if ($bytes.Length -gt 4096) { throw 'The request file is too big.' }
29
+ if ($Expected) {
30
+ $sha = [Security.Cryptography.SHA256]::Create()
31
+ $hash = -join ($sha.ComputeHash($bytes) | ForEach-Object { $_.ToString('x2') })
32
+ if ($hash -ine $Expected) { throw 'The request file changed after it was written; nothing was done.' }
33
+ }
34
+ return ([Text.Encoding]::UTF8.GetString($bytes) | ConvertFrom-Json)
35
+ }
36
+
37
+ function Assert-Request($Req) {
38
+ foreach ($name in @('action', 'ruleName')) {
39
+ if (-not ($Req.PSObject.Properties.Name -contains $name)) { throw "The request has no $name." }
40
+ }
41
+ if ($Req.ruleName -cne $RuleName) { throw "Only the rule '$RuleName' is managed here." }
42
+ if ($Req.action -ne 'add' -and $Req.action -ne 'remove') { throw 'The action must be add or remove.' }
43
+ if ($Req.action -eq 'remove') { return }
44
+ foreach ($name in @('port', 'localAddress', 'program')) {
45
+ if (-not ($Req.PSObject.Properties.Name -contains $name)) { throw "The request has no $name." }
46
+ }
47
+ if (-not ($Req.port -is [int] -or $Req.port -is [long]) -or $Req.port -lt 1024 -or $Req.port -gt 65535) {
48
+ throw 'The port must be a whole number from 1024 to 65535.'
49
+ }
50
+ if (-not (Test-PrivateIPv4 ([string]$Req.localAddress))) {
51
+ throw 'The address must be a private IPv4 address (10.x, 172.16-31.x, 192.168.x): LAN mode is for your own network only.'
52
+ }
53
+ $program = [string]$Req.program
54
+ if (-not [IO.Path]::IsPathRooted($program) -or $program.Contains('..')) { throw 'The program must be a full path.' }
55
+ if ([IO.Path]::GetFileName($program) -ine 'node.exe') { throw 'The program must be node.exe.' }
56
+ if (-not (Test-Path -LiteralPath $program -PathType Leaf)) { throw "The program does not exist: $program" }
57
+ $full = [IO.Path]::GetFullPath($program)
58
+ foreach ($temp in @($env:TEMP, $env:TMP, (Join-Path $env:SystemRoot 'Temp'))) {
59
+ if ($temp -and $full.StartsWith([IO.Path]::GetFullPath($temp).TrimEnd('\') + '\', [StringComparison]::OrdinalIgnoreCase)) {
60
+ throw 'The program must not be in a temp folder.'
61
+ }
62
+ }
63
+ }
64
+
65
+ function Get-Commands($Req) {
66
+ $remove = "Remove-NetFirewallRule -DisplayName '$RuleName'"
67
+ if ($Req.action -eq 'remove') { return @($remove) }
68
+ $add = "New-NetFirewallRule -DisplayName '$RuleName' -Direction Inbound -Action Allow -Protocol TCP " +
69
+ "-LocalPort $($Req.port) -LocalAddress $($Req.localAddress) -RemoteAddress LocalSubnet -Profile Private " +
70
+ "-Program '$([IO.Path]::GetFullPath([string]$Req.program))'"
71
+ return @($remove, $add)
72
+ }
73
+
74
+ try {
75
+ $request = Read-Request $Json $Sha256
76
+ Assert-Request $request
77
+ $result.commands = @(Get-Commands $request)
78
+ if ($DryRun) {
79
+ $result.ok = $true
80
+ $result.message = 'Checked; nothing was changed.'
81
+ $result | ConvertTo-Json -Compress
82
+ exit 0
83
+ }
84
+ Get-NetFirewallRule -DisplayName $RuleName -ErrorAction SilentlyContinue | Remove-NetFirewallRule
85
+ if ($request.action -eq 'add') {
86
+ New-NetFirewallRule -DisplayName $RuleName -Direction Inbound -Action Allow -Protocol TCP `
87
+ -LocalPort ([int]$request.port) -LocalAddress ([string]$request.localAddress) -RemoteAddress LocalSubnet `
88
+ -Profile Private -Program ([IO.Path]::GetFullPath([string]$request.program)) | Out-Null
89
+ $result.message = 'The firewall rule is in place.'
90
+ } else {
91
+ $result.message = 'The firewall rule is removed.'
92
+ }
93
+ $result.ok = $true
94
+ } catch {
95
+ $result.ok = $false
96
+ $result.message = $_.Exception.Message
97
+ if ($DryRun) {
98
+ $result | ConvertTo-Json -Compress
99
+ exit 2
100
+ }
101
+ }
102
+ $result | ConvertTo-Json -Compress | Set-Content -LiteralPath (Join-Path (Split-Path -Parent $Json) 'result.json') -Encoding UTF8
103
+ if ($result.ok) { exit 0 } else { exit 2 }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "windows-development-sandbox",
3
- "version": "1.11.7",
3
+ "version": "1.13.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",