windows-development-sandbox 0.0.0-stage → 1.0.5

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 (231) hide show
  1. package/.sandbox-config.example.json +14 -0
  2. package/README.md +78 -2
  3. package/config.example.json +332 -0
  4. package/dist/api.d.ts +31 -0
  5. package/dist/api.js +30 -0
  6. package/dist/bin/agent-sandbox.d.ts +2 -0
  7. package/dist/bin/agent-sandbox.js +3 -0
  8. package/dist/bin/claude-host.d.ts +2 -0
  9. package/dist/bin/claude-host.js +5 -0
  10. package/dist/bin/claude-sbx.d.ts +2 -0
  11. package/dist/bin/claude-sbx.js +3 -0
  12. package/dist/bin/codex-sbx.d.ts +2 -0
  13. package/dist/bin/codex-sbx.js +3 -0
  14. package/dist/bin/gemini-sbx.d.ts +2 -0
  15. package/dist/bin/gemini-sbx.js +3 -0
  16. package/dist/bin/sandbox.d.ts +2 -0
  17. package/dist/bin/sandbox.js +4 -0
  18. package/dist/bin/sbx-hide.d.ts +2 -0
  19. package/dist/bin/sbx-hide.js +3 -0
  20. package/dist/bin/sbx-show.d.ts +2 -0
  21. package/dist/bin/sbx-show.js +3 -0
  22. package/dist/cli/args.d.ts +32 -0
  23. package/dist/cli/args.js +111 -0
  24. package/dist/cli/commands/accounts.d.ts +2 -0
  25. package/dist/cli/commands/accounts.js +52 -0
  26. package/dist/cli/commands/context.d.ts +21 -0
  27. package/dist/cli/commands/context.js +54 -0
  28. package/dist/cli/commands/data.d.ts +46 -0
  29. package/dist/cli/commands/data.js +249 -0
  30. package/dist/cli/commands/index.d.ts +8 -0
  31. package/dist/cli/commands/index.js +8 -0
  32. package/dist/cli/commands/launch.d.ts +5 -0
  33. package/dist/cli/commands/launch.js +152 -0
  34. package/dist/cli/commands/maintenance.d.ts +8 -0
  35. package/dist/cli/commands/maintenance.js +125 -0
  36. package/dist/cli/commands/repair.d.ts +59 -0
  37. package/dist/cli/commands/repair.js +332 -0
  38. package/dist/cli/commands/sessions.d.ts +11 -0
  39. package/dist/cli/commands/sessions.js +113 -0
  40. package/dist/cli/commands/setup.d.ts +45 -0
  41. package/dist/cli/commands/setup.js +152 -0
  42. package/dist/cli/index.d.ts +14 -0
  43. package/dist/cli/index.js +96 -0
  44. package/dist/cli/manifest.d.ts +95 -0
  45. package/dist/cli/manifest.js +87 -0
  46. package/dist/cli/menu.d.ts +22 -0
  47. package/dist/cli/menu.js +187 -0
  48. package/dist/cli/settings.d.ts +11 -0
  49. package/dist/cli/settings.js +156 -0
  50. package/dist/core/config.d.ts +17 -0
  51. package/dist/core/config.js +103 -0
  52. package/dist/core/detect-databases.d.ts +15 -0
  53. package/dist/core/detect-databases.js +147 -0
  54. package/dist/core/detect-pwsh.d.ts +18 -0
  55. package/dist/core/detect-pwsh.js +111 -0
  56. package/dist/core/detect-toolchains.d.ts +30 -0
  57. package/dist/core/detect-toolchains.js +58 -0
  58. package/dist/core/local-config.d.ts +12 -0
  59. package/dist/core/local-config.js +64 -0
  60. package/dist/core/schema.d.ts +604 -0
  61. package/dist/core/schema.js +264 -0
  62. package/dist/core/types.d.ts +162 -0
  63. package/dist/core/types.js +1 -0
  64. package/dist/core/util.d.ts +53 -0
  65. package/dist/core/util.js +145 -0
  66. package/dist/core/workspace.d.ts +23 -0
  67. package/dist/core/workspace.js +162 -0
  68. package/dist/guest/daemon.d.ts +1 -0
  69. package/dist/guest/daemon.js +186 -0
  70. package/dist/guest/db.d.ts +1 -0
  71. package/dist/guest/db.js +60 -0
  72. package/dist/guest/env.d.ts +49 -0
  73. package/dist/guest/env.js +99 -0
  74. package/dist/guest/gitguard.d.ts +7 -0
  75. package/dist/guest/gitguard.js +60 -0
  76. package/dist/guest/gitrules.d.ts +2 -0
  77. package/dist/guest/gitrules.js +11 -0
  78. package/dist/guest/guide.d.ts +12 -0
  79. package/dist/guest/guide.js +68 -0
  80. package/dist/guest/main.d.ts +1 -0
  81. package/dist/guest/main.js +120 -0
  82. package/dist/guest/ptyhost.d.ts +40 -0
  83. package/dist/guest/ptyhost.js +94 -0
  84. package/dist/guest/setup/agents.d.ts +8 -0
  85. package/dist/guest/setup/agents.js +73 -0
  86. package/dist/guest/setup/claude.d.ts +21 -0
  87. package/dist/guest/setup/claude.js +138 -0
  88. package/dist/guest/setup/environment.d.ts +6 -0
  89. package/dist/guest/setup/environment.js +65 -0
  90. package/dist/guest/setup/hooks.d.ts +1 -0
  91. package/dist/guest/setup/hooks.js +36 -0
  92. package/dist/guest/setup/network.d.ts +10 -0
  93. package/dist/guest/setup/network.js +99 -0
  94. package/dist/guest/setup/policy.d.ts +3 -0
  95. package/dist/guest/setup/policy.js +18 -0
  96. package/dist/guest/setup/user.d.ts +7 -0
  97. package/dist/guest/setup/user.js +47 -0
  98. package/dist/guest/toolchains/binstall.d.ts +7 -0
  99. package/dist/guest/toolchains/binstall.js +55 -0
  100. package/dist/guest/toolchains/catalogue.d.ts +51 -0
  101. package/dist/guest/toolchains/catalogue.js +31 -0
  102. package/dist/guest/toolchains/cli.d.ts +10 -0
  103. package/dist/guest/toolchains/cli.js +64 -0
  104. package/dist/guest/toolchains/common.d.ts +37 -0
  105. package/dist/guest/toolchains/common.js +124 -0
  106. package/dist/guest/toolchains/databases.d.ts +7 -0
  107. package/dist/guest/toolchains/databases.js +226 -0
  108. package/dist/guest/toolchains/git.d.ts +3 -0
  109. package/dist/guest/toolchains/git.js +112 -0
  110. package/dist/guest/toolchains/go.d.ts +3 -0
  111. package/dist/guest/toolchains/go.js +33 -0
  112. package/dist/guest/toolchains/index.d.ts +13 -0
  113. package/dist/guest/toolchains/index.js +122 -0
  114. package/dist/guest/toolchains/java.d.ts +8 -0
  115. package/dist/guest/toolchains/java.js +96 -0
  116. package/dist/guest/toolchains/managers.d.ts +16 -0
  117. package/dist/guest/toolchains/managers.js +82 -0
  118. package/dist/guest/toolchains/msvc.d.ts +10 -0
  119. package/dist/guest/toolchains/msvc.js +216 -0
  120. package/dist/guest/toolchains/native.d.ts +12 -0
  121. package/dist/guest/toolchains/native.js +220 -0
  122. package/dist/guest/toolchains/node.d.ts +3 -0
  123. package/dist/guest/toolchains/node.js +132 -0
  124. package/dist/guest/toolchains/pwsh.d.ts +4 -0
  125. package/dist/guest/toolchains/pwsh.js +14 -0
  126. package/dist/guest/toolchains/python.d.ts +3 -0
  127. package/dist/guest/toolchains/python.js +47 -0
  128. package/dist/guest/toolchains/runtimes.d.ts +17 -0
  129. package/dist/guest/toolchains/runtimes.js +40 -0
  130. package/dist/guest/toolchains/scripting.d.ts +10 -0
  131. package/dist/guest/toolchains/scripting.js +137 -0
  132. package/dist/guest/toolchains/versions.d.ts +47 -0
  133. package/dist/guest/toolchains/versions.js +77 -0
  134. package/dist/guest/toolchains/vscode.d.ts +2 -0
  135. package/dist/guest/toolchains/vscode.js +53 -0
  136. package/dist/guest/view.d.ts +1 -0
  137. package/dist/guest/view.js +22 -0
  138. package/dist/host/accounts.d.ts +32 -0
  139. package/dist/host/accounts.js +119 -0
  140. package/dist/host/channel.d.ts +37 -0
  141. package/dist/host/channel.js +180 -0
  142. package/dist/host/claude-profile.d.ts +15 -0
  143. package/dist/host/claude-profile.js +103 -0
  144. package/dist/host/cleanup.d.ts +23 -0
  145. package/dist/host/cleanup.js +66 -0
  146. package/dist/host/generate.d.ts +15 -0
  147. package/dist/host/generate.js +185 -0
  148. package/dist/host/personalize.d.ts +10 -0
  149. package/dist/host/personalize.js +97 -0
  150. package/dist/host/platform.d.ts +17 -0
  151. package/dist/host/platform.js +114 -0
  152. package/dist/host/project-scan.d.ts +23 -0
  153. package/dist/host/project-scan.js +173 -0
  154. package/dist/host/repo-guard.d.ts +23 -0
  155. package/dist/host/repo-guard.js +128 -0
  156. package/dist/host/secrets.d.ts +14 -0
  157. package/dist/host/secrets.js +133 -0
  158. package/dist/host/terminal.d.ts +32 -0
  159. package/dist/host/terminal.js +127 -0
  160. package/dist/host/window.d.ts +4 -0
  161. package/dist/host/window.js +68 -0
  162. package/dist/runtime/daemon.js +20291 -0
  163. package/dist/runtime/db.js +20423 -0
  164. package/dist/runtime/gitguard.js +84 -0
  165. package/dist/runtime/main.js +22398 -0
  166. package/dist/runtime/view.js +19966 -0
  167. package/dist/selftest/cli.test.d.ts +2 -0
  168. package/dist/selftest/cli.test.js +30 -0
  169. package/dist/selftest/config.test.d.ts +3 -0
  170. package/dist/selftest/config.test.js +74 -0
  171. package/dist/selftest/data.test.d.ts +2 -0
  172. package/dist/selftest/data.test.js +73 -0
  173. package/dist/selftest/detect.test.d.ts +2 -0
  174. package/dist/selftest/detect.test.js +42 -0
  175. package/dist/selftest/fixtures.d.ts +232 -0
  176. package/dist/selftest/fixtures.js +63 -0
  177. package/dist/selftest/guest.test.d.ts +3 -0
  178. package/dist/selftest/guest.test.js +61 -0
  179. package/dist/selftest/harness.d.ts +14 -0
  180. package/dist/selftest/harness.js +36 -0
  181. package/dist/selftest/helpers.test.d.ts +2 -0
  182. package/dist/selftest/helpers.test.js +125 -0
  183. package/dist/selftest/host.test.d.ts +2 -0
  184. package/dist/selftest/host.test.js +71 -0
  185. package/dist/selftest/index.d.ts +1 -0
  186. package/dist/selftest/index.js +56 -0
  187. package/dist/selftest/live.test.d.ts +7 -0
  188. package/dist/selftest/live.test.js +127 -0
  189. package/dist/selftest/mirror.test.d.ts +3 -0
  190. package/dist/selftest/mirror.test.js +74 -0
  191. package/dist/selftest/security.test.d.ts +3 -0
  192. package/dist/selftest/security.test.js +143 -0
  193. package/dist/selftest/setup.test.d.ts +2 -0
  194. package/dist/selftest/setup.test.js +23 -0
  195. package/dist/selftest/sync.test.d.ts +3 -0
  196. package/dist/selftest/sync.test.js +82 -0
  197. package/dist/shared/download.d.ts +58 -0
  198. package/dist/shared/download.js +200 -0
  199. package/dist/shared/guest-paths.d.ts +30 -0
  200. package/dist/shared/guest-paths.js +61 -0
  201. package/dist/shared/netrules.d.ts +18 -0
  202. package/dist/shared/netrules.js +98 -0
  203. package/dist/shared/paths.d.ts +9 -0
  204. package/dist/shared/paths.js +22 -0
  205. package/dist/shared/pty-client.d.ts +86 -0
  206. package/dist/shared/pty-client.js +227 -0
  207. package/dist/shared/rundir.d.ts +30 -0
  208. package/dist/shared/rundir.js +86 -0
  209. package/dist/shared/safe-remove.d.ts +10 -0
  210. package/dist/shared/safe-remove.js +55 -0
  211. package/dist/shared/schema.d.ts +84 -0
  212. package/dist/shared/schema.js +51 -0
  213. package/dist/shared/system.d.ts +15 -0
  214. package/dist/shared/system.js +19 -0
  215. package/dist/sync/claude-mem.d.ts +8 -0
  216. package/dist/sync/claude-mem.js +77 -0
  217. package/dist/sync/history.d.ts +30 -0
  218. package/dist/sync/history.js +129 -0
  219. package/dist/sync/watcher.d.ts +13 -0
  220. package/dist/sync/watcher.js +116 -0
  221. package/hooks/post-agent/README.txt +1 -0
  222. package/hooks/pre-provision/README.txt +1 -0
  223. package/package.json +91 -4
  224. package/sandbox/bootstrap.ps1 +258 -0
  225. package/sandbox/gitshim.cs +35 -0
  226. package/sandbox/logon.cmd +16 -0
  227. package/sandbox/ptyhost.cs +603 -0
  228. package/sandbox/trust.cmd +35 -0
  229. package/sandbox/vswhere.cs +83 -0
  230. package/schemas/config.schema.json +1062 -0
  231. package/schemas/sandbox-config.schema.json +952 -0
@@ -0,0 +1,603 @@
1
+ // ptyhost: runs one console program inside a Windows pseudo console (ConPTY) and shares it with any number of TCP
2
+ // clients (the terminal tab inside the sandbox and `--terminal` on the host). Compiled inside the sandbox with the
3
+ // .NET Framework csc.exe, so this is C# 5 (no string interpolation, no ?., no expression-bodied members).
4
+ //
5
+ // Usage: ptyhost.exe <port> <token> <cols> <rows> <logFile> <commandLine> [<restartCommandLine>]
6
+ //
7
+ // Wire format, both directions: [type:1][length:4, little endian][payload]
8
+ // client -> host 1 HELLO [cols:2][rows:2][flags:1][token utf8] flags bit 0 = control-only (no output, no sizing),
9
+ // bit 1 = status query (answered with STATUS, then closed)
10
+ // 2 INPUT bytes for the program
11
+ // 3 RESIZE [cols:2][rows:2]
12
+ // 4 RESTART restart the program with <restartCommandLine> (e.g. after switching accounts)
13
+ // 5 PING "still here": viewers send it every few seconds, so a closed terminal stops counting
14
+ // host -> client 16 OUTPUT VT output
15
+ // 17 EXIT [exitCode:4]
16
+ // 18 ERROR utf8 message
17
+ // 19 RESTARTING
18
+ // 20 STATUS [viewers:2][remoteViewers:2] viewers heard from recently (remote = not loopback, i.e. the PC)
19
+ using System;
20
+ using System.Collections.Concurrent;
21
+ using System.Collections.Generic;
22
+ using System.IO;
23
+ using System.Net;
24
+ using System.Net.Sockets;
25
+ using System.Runtime.InteropServices;
26
+ using System.Text;
27
+ using System.Text.RegularExpressions;
28
+ using System.Threading;
29
+ using Microsoft.Win32.SafeHandles;
30
+
31
+ static class Program
32
+ {
33
+ static int Main(string[] args)
34
+ {
35
+ if (args.Length < 6)
36
+ {
37
+ Console.Error.WriteLine("usage: ptyhost <port> <token> <cols> <rows> <logFile> <commandLine> [<restartCommandLine>]");
38
+ return 2;
39
+ }
40
+ var log = new Log(args[4]);
41
+ try
42
+ {
43
+ var hub = new ClientHub(Encoding.UTF8.GetBytes(args[1]), short.Parse(args[2]), short.Parse(args[3]));
44
+ var host = new SessionHost(hub, log, args[5], args.Length > 6 && args[6].Length > 0 ? args[6] : args[5]);
45
+ new Listener(int.Parse(args[0]), hub, host, log).Start();
46
+ return host.Run();
47
+ }
48
+ catch (Exception e)
49
+ {
50
+ log.Write("FATAL " + e);
51
+ return 1;
52
+ }
53
+ }
54
+ }
55
+
56
+ /** Runs the program (and restarts it on request) until it exits on its own. */
57
+ sealed class SessionHost
58
+ {
59
+ readonly ClientHub hub;
60
+ readonly Log log;
61
+ readonly string restartCommand;
62
+ string command;
63
+ volatile bool restartRequested;
64
+
65
+ public SessionHost(ClientHub hub, Log log, string command, string restartCommand)
66
+ {
67
+ this.hub = hub; this.log = log; this.command = command; this.restartCommand = restartCommand;
68
+ }
69
+
70
+ public int Run()
71
+ {
72
+ log.Write("starting: " + command);
73
+ while (true)
74
+ {
75
+ using (var console = PseudoConsole.Start(command, hub.Cols, hub.Rows, hub.Broadcast))
76
+ {
77
+ hub.Attach(console);
78
+ int exitCode = console.WaitForExit();
79
+ if (!restartRequested)
80
+ {
81
+ log.Write("program exited with code " + exitCode + ". Last output:" + Environment.NewLine + hub.TailText());
82
+ hub.Exit(exitCode);
83
+ return exitCode;
84
+ }
85
+ }
86
+ restartRequested = false;
87
+ command = restartCommand;
88
+ log.Write("restarting: " + command);
89
+ }
90
+ }
91
+
92
+ public void Restart()
93
+ {
94
+ restartRequested = true;
95
+ hub.Notify(Frame.Restarting);
96
+ hub.KillProgram();
97
+ }
98
+ }
99
+
100
+ /** Accepts TCP clients and turns their frames into actions. */
101
+ sealed class Listener
102
+ {
103
+ readonly int port;
104
+ readonly ClientHub hub;
105
+ readonly SessionHost host;
106
+ readonly Log log;
107
+ const int MaxConnections = 32;
108
+ int connections;
109
+
110
+ public Listener(int port, ClientHub hub, SessionHost host, Log log)
111
+ {
112
+ this.port = port; this.hub = hub; this.host = host; this.log = log;
113
+ }
114
+
115
+ public void Start()
116
+ {
117
+ // Reachable from the host only when attach is enabled; otherwise just from inside the sandbox.
118
+ bool remote = Environment.GetEnvironmentVariable("PTYHOST_BIND") == "any";
119
+ var listener = new TcpListener(remote ? IPAddress.Any : IPAddress.Loopback, port);
120
+ listener.Start();
121
+ log.Write("listening on " + (remote ? "all addresses" : "loopback") + ", port " + port);
122
+ Background(delegate
123
+ {
124
+ while (true)
125
+ {
126
+ TcpClient tcp = listener.AcceptTcpClient();
127
+ if (Interlocked.Increment(ref connections) > MaxConnections) { Interlocked.Decrement(ref connections); tcp.Close(); continue; }
128
+ tcp.NoDelay = true;
129
+ KeepAlive(tcp.Client);
130
+ Background(delegate { try { Serve(tcp); } finally { Interlocked.Decrement(ref connections); } });
131
+ }
132
+ });
133
+ }
134
+
135
+ void Serve(TcpClient tcp)
136
+ {
137
+ var client = new Client(tcp);
138
+ try
139
+ {
140
+ byte type; byte[] payload;
141
+ // The handshake must arrive quickly; idle unauthenticated connections are not kept.
142
+ tcp.ReceiveTimeout = 10000;
143
+ if (!Frame.Read(client.Stream, out type, out payload) || type != Frame.Hello || payload.Length < 5) return;
144
+ if (!hub.Admit(client, payload)) return;
145
+ if (client.Query) { hub.SendStatus(client); return; }
146
+ tcp.ReceiveTimeout = 0;
147
+ while (Frame.Read(client.Stream, out type, out payload))
148
+ {
149
+ client.Seen();
150
+ if (type == Frame.Input) hub.Input(client, payload);
151
+ else if (type == Frame.Resize && payload.Length >= 4) hub.Resize(client, BitConverter.ToInt16(payload, 0), BitConverter.ToInt16(payload, 2));
152
+ else if (type == Frame.Restart) host.Restart();
153
+ }
154
+ }
155
+ catch (IOException) { }
156
+ catch (ObjectDisposedException) { }
157
+ finally { hub.Kill(client); }
158
+ }
159
+
160
+ static void Background(ThreadStart work) { new Thread(work) { IsBackground = true }.Start(); }
161
+
162
+ /** TCP keepalive after 10 s idle, probing every 3 s: a peer that vanished (PC asleep, network gone) is dropped. */
163
+ static void KeepAlive(Socket socket)
164
+ {
165
+ try
166
+ {
167
+ var values = new byte[12];
168
+ BitConverter.GetBytes(1).CopyTo(values, 0);
169
+ BitConverter.GetBytes(10000).CopyTo(values, 4);
170
+ BitConverter.GetBytes(3000).CopyTo(values, 8);
171
+ socket.IOControl(IOControlCode.KeepAliveValues, values, null);
172
+ }
173
+ catch (SocketException) { }
174
+ }
175
+ }
176
+
177
+ /** One connection. Frames go through a bounded outbox written by its own thread, so a slow viewer never blocks the agent. */
178
+ sealed class Client
179
+ {
180
+ public readonly TcpClient Tcp;
181
+ public readonly NetworkStream Stream;
182
+ public readonly BlockingCollection<byte[]> Outbox = new BlockingCollection<byte[]>(4096);
183
+ public readonly bool Remote;
184
+ public bool ControlOnly, Query;
185
+ public short Cols, Rows;
186
+ long lastSeen = DateTime.UtcNow.Ticks;
187
+
188
+ public Client(TcpClient tcp)
189
+ {
190
+ Tcp = tcp;
191
+ Stream = tcp.GetStream();
192
+ var endpoint = tcp.Client.RemoteEndPoint as IPEndPoint;
193
+ Remote = endpoint != null && !IPAddress.IsLoopback(endpoint.Address);
194
+ new Thread(Write) { IsBackground = true }.Start();
195
+ }
196
+
197
+ public void Seen() { Interlocked.Exchange(ref lastSeen, DateTime.UtcNow.Ticks); }
198
+
199
+ /** Heard from in the last 15 s. Viewers ping every 5 s, so anything quieter is a terminal that went away. */
200
+ public bool Active { get { return DateTime.UtcNow.Ticks - Interlocked.Read(ref lastSeen) < TimeSpan.FromSeconds(15).Ticks; } }
201
+
202
+ void Write()
203
+ {
204
+ try { foreach (byte[] frame in Outbox.GetConsumingEnumerable()) Stream.Write(frame, 0, frame.Length); }
205
+ catch (Exception) { }
206
+ finally { try { Tcp.Close(); } catch (Exception) { } }
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Everyone connected to the session. Output goes to all of them; the most recent client to connect, type or resize
212
+ * decides the size (like tmux "latest"), and every size change forces a full repaint so no viewer is left with a
213
+ * half-drawn screen. When the owner leaves, the most recently active viewer left takes over at its own size.
214
+ */
215
+ sealed class ClientHub
216
+ {
217
+ readonly object gate = new object();
218
+ readonly List<Client> clients = new List<Client>();
219
+ readonly byte[] token;
220
+ readonly byte[] tail = new byte[8192];
221
+ int tailLength;
222
+ PseudoConsole console;
223
+ Client sizeOwner;
224
+ short cols, rows;
225
+
226
+ public ClientHub(byte[] token, short cols, short rows) { this.token = token; this.cols = cols; this.rows = rows; }
227
+
228
+ public short Cols { get { lock (gate) return cols; } }
229
+ public short Rows { get { lock (gate) return rows; } }
230
+
231
+ public void Attach(PseudoConsole next) { lock (gate) console = next; }
232
+
233
+ public bool Admit(Client client, byte[] hello)
234
+ {
235
+ var offered = new byte[hello.Length - 5];
236
+ Buffer.BlockCopy(hello, 5, offered, 0, offered.Length);
237
+ if (!TokenMatches(offered))
238
+ {
239
+ Send(client, Frame.Error, Encoding.UTF8.GetBytes("access denied"));
240
+ Retire(client);
241
+ return false;
242
+ }
243
+ client.Cols = BitConverter.ToInt16(hello, 0);
244
+ client.Rows = BitConverter.ToInt16(hello, 2);
245
+ client.ControlOnly = (hello[4] & 3) != 0;
246
+ client.Query = (hello[4] & 2) != 0;
247
+ if (client.Query) return true;
248
+ lock (gate) clients.Add(client);
249
+ if (!client.ControlOnly) TakeOwnership(client);
250
+ return true;
251
+ }
252
+
253
+ public void Input(Client client, byte[] data)
254
+ {
255
+ if (client.ControlOnly) return;
256
+ if (sizeOwner != client) TakeOwnership(client);
257
+ PseudoConsole current;
258
+ lock (gate) current = console;
259
+ if (current != null) current.Write(data);
260
+ }
261
+
262
+ /** A viewer being resized is the one being looked at, so it takes over the size. */
263
+ public void Resize(Client client, short width, short height)
264
+ {
265
+ if (client.ControlOnly) return;
266
+ client.Cols = width; client.Rows = height;
267
+ TakeOwnership(client);
268
+ }
269
+
270
+ public void SendStatus(Client client)
271
+ {
272
+ int viewers = 0, remote = 0;
273
+ foreach (Client c in Snapshot()) if (!c.ControlOnly && c.Active) { viewers++; if (c.Remote) remote++; }
274
+ var payload = new byte[4];
275
+ BitConverter.GetBytes((short)viewers).CopyTo(payload, 0);
276
+ BitConverter.GetBytes((short)remote).CopyTo(payload, 2);
277
+ Send(client, Frame.Status, payload);
278
+ Retire(client);
279
+ }
280
+
281
+ public void Broadcast(byte[] output)
282
+ {
283
+ Remember(output);
284
+ foreach (Client c in Snapshot()) if (!c.ControlOnly) Send(c, Frame.Output, output);
285
+ }
286
+
287
+ /** The program's last output as plain text (escape sequences removed), for the log when it exits. */
288
+ public string TailText()
289
+ {
290
+ string text;
291
+ lock (tail) text = Encoding.UTF8.GetString(tail, 0, tailLength);
292
+ text = Regex.Replace(text, "\x1b(\\[[0-9;?]*[ -/]*[@-~]|\\][^\x07\x1b]*(\x07|\x1b\\\\)|[@-Z\\\\-_])", "");
293
+ return Regex.Replace(text, "[\\x00-\\x08\\x0b-\\x1f]", "").Trim();
294
+ }
295
+
296
+ void Remember(byte[] output)
297
+ {
298
+ lock (tail)
299
+ {
300
+ int keep = Math.Min(tailLength, tail.Length - Math.Min(output.Length, tail.Length));
301
+ Buffer.BlockCopy(tail, tailLength - keep, tail, 0, keep);
302
+ int add = Math.Min(output.Length, tail.Length);
303
+ Buffer.BlockCopy(output, output.Length - add, tail, keep, add);
304
+ tailLength = keep + add;
305
+ }
306
+ }
307
+
308
+ public void Notify(byte type)
309
+ {
310
+ foreach (Client c in Snapshot()) if (!c.ControlOnly) Send(c, type, new byte[0]);
311
+ }
312
+
313
+ public void KillProgram()
314
+ {
315
+ PseudoConsole current;
316
+ lock (gate) current = console;
317
+ if (current != null) current.Kill();
318
+ }
319
+
320
+ public void Exit(int exitCode)
321
+ {
322
+ byte[] code = BitConverter.GetBytes(exitCode);
323
+ var everyone = Snapshot();
324
+ foreach (Client c in everyone) { Send(c, Frame.Exit, code); Retire(c); }
325
+ // Give the writer threads a moment to deliver the EXIT frame before the process ends.
326
+ foreach (Client c in everyone) for (int i = 0; i < 20 && c.Outbox.Count > 0; i++) Thread.Sleep(50);
327
+ }
328
+
329
+ /** Stops sending to a client after what is already queued (its writer closes the connection when done). */
330
+ public void Retire(Client client)
331
+ {
332
+ Client next = null;
333
+ lock (gate)
334
+ {
335
+ clients.Remove(client);
336
+ if (sizeOwner == client)
337
+ {
338
+ sizeOwner = null;
339
+ for (int i = clients.Count - 1; i >= 0 && next == null; i--) if (!clients[i].ControlOnly) next = clients[i];
340
+ }
341
+ }
342
+ try { client.Outbox.CompleteAdding(); } catch (ObjectDisposedException) { }
343
+ if (next != null) TakeOwnership(next);
344
+ }
345
+
346
+ /** Disconnects a client (after at most half a second for frames already queued, e.g. "access denied"). */
347
+ public void Kill(Client client)
348
+ {
349
+ Retire(client);
350
+ for (int i = 0; i < 20 && client.Outbox.Count > 0; i++) Thread.Sleep(25);
351
+ try { client.Tcp.Close(); } catch (Exception) { }
352
+ }
353
+
354
+ void TakeOwnership(Client client)
355
+ {
356
+ lock (gate)
357
+ {
358
+ // Kept in order of activity, so a departing owner hands over to whoever was active most recently.
359
+ if (clients.Remove(client)) clients.Add(client);
360
+ sizeOwner = client;
361
+ }
362
+ Apply(client.Cols, client.Rows);
363
+ }
364
+
365
+ void Apply(short width, short height)
366
+ {
367
+ if (width < 2 || height < 2) return;
368
+ PseudoConsole current;
369
+ lock (gate) { current = console; cols = width; rows = height; }
370
+ if (current == null) return;
371
+ // ConPTY repaints everything on a size change; resizing by one row first forces that even for the same size.
372
+ current.Resize(width, (short)(height - 1));
373
+ current.Resize(width, height);
374
+ }
375
+
376
+ bool TokenMatches(byte[] offered)
377
+ {
378
+ int diff = offered.Length ^ token.Length;
379
+ for (int i = 0; i < offered.Length && i < token.Length; i++) diff |= offered[i] ^ token[i];
380
+ return diff == 0;
381
+ }
382
+
383
+ List<Client> Snapshot() { lock (gate) return new List<Client>(clients); }
384
+
385
+ /** Queues a frame; a client whose outbox is full has stopped reading and is disconnected. */
386
+ void Send(Client client, byte type, byte[] payload)
387
+ {
388
+ bool queued;
389
+ try { queued = client.Outbox.TryAdd(Frame.Encode(type, payload)); }
390
+ catch (InvalidOperationException) { return; }
391
+ if (!queued) Kill(client);
392
+ }
393
+ }
394
+
395
+ /** One program running inside a pseudo console, with a thread pumping its output to `onOutput`. */
396
+ sealed class PseudoConsole : IDisposable
397
+ {
398
+ readonly IntPtr handle;
399
+ readonly IntPtr process;
400
+ readonly FileStream input;
401
+ readonly Thread pump;
402
+ readonly object gate = new object();
403
+ bool closed, disposed;
404
+
405
+ PseudoConsole(IntPtr handle, IntPtr process, FileStream input, Thread pump)
406
+ {
407
+ this.handle = handle; this.process = process; this.input = input; this.pump = pump;
408
+ }
409
+
410
+ public static PseudoConsole Start(string commandLine, short cols, short rows, Action<byte[]> onOutput)
411
+ {
412
+ SafeFileHandle inRead, inWrite, outRead, outWrite;
413
+ Native.Check(Native.CreatePipe(out inRead, out inWrite, IntPtr.Zero, 0), "CreatePipe");
414
+ Native.Check(Native.CreatePipe(out outRead, out outWrite, IntPtr.Zero, 0), "CreatePipe");
415
+ IntPtr handle;
416
+ int hr = Native.CreatePseudoConsole(new Native.Coord { X = cols, Y = rows }, inRead, outWrite, 0, out handle);
417
+ if (hr != 0) throw new IOException("CreatePseudoConsole failed: 0x" + hr.ToString("X8"));
418
+ inRead.Dispose();
419
+ outWrite.Dispose();
420
+ IntPtr process = Native.Spawn(commandLine, handle);
421
+ var output = new FileStream(outRead, FileAccess.Read);
422
+ var pump = new Thread(() => Pump(output, onOutput)) { IsBackground = true };
423
+ pump.Start();
424
+ return new PseudoConsole(handle, process, new FileStream(inWrite, FileAccess.Write), pump);
425
+ }
426
+
427
+ static void Pump(FileStream output, Action<byte[]> onOutput)
428
+ {
429
+ var buffer = new byte[16384];
430
+ try
431
+ {
432
+ int n;
433
+ while ((n = output.Read(buffer, 0, buffer.Length)) > 0)
434
+ {
435
+ var chunk = new byte[n];
436
+ Buffer.BlockCopy(buffer, 0, chunk, 0, n);
437
+ onOutput(chunk);
438
+ }
439
+ }
440
+ catch (IOException) { }
441
+ finally { output.Dispose(); }
442
+ }
443
+
444
+ public int WaitForExit()
445
+ {
446
+ Native.WaitForSingleObject(process, 0xFFFFFFFF);
447
+ uint code;
448
+ Native.GetExitCodeProcess(process, out code);
449
+ Close();
450
+ pump.Join(5000);
451
+ return (int)code;
452
+ }
453
+
454
+ public void Write(byte[] data)
455
+ {
456
+ try { lock (input) { input.Write(data, 0, data.Length); input.Flush(); } }
457
+ catch (IOException) { }
458
+ catch (ObjectDisposedException) { }
459
+ }
460
+
461
+ public void Resize(short cols, short rows)
462
+ {
463
+ lock (gate) if (!closed) Native.ResizePseudoConsole(handle, new Native.Coord { X = cols, Y = rows });
464
+ }
465
+
466
+ /** Closing the pseudo console ends every process attached to it (the agent and anything it started). */
467
+ public void Kill()
468
+ {
469
+ lock (gate) { if (disposed) return; Native.TerminateProcess(process, 1); }
470
+ Close();
471
+ }
472
+
473
+ public void Dispose()
474
+ {
475
+ Close();
476
+ lock (gate) { if (disposed) return; disposed = true; Native.CloseHandle(process); }
477
+ }
478
+
479
+ void Close()
480
+ {
481
+ lock (gate)
482
+ {
483
+ if (closed) return;
484
+ closed = true;
485
+ try { input.Dispose(); } catch (IOException) { }
486
+ Native.ClosePseudoConsole(handle);
487
+ }
488
+ }
489
+ }
490
+
491
+ /** The wire format (see the top of this file). */
492
+ static class Frame
493
+ {
494
+ public const byte Hello = 1, Input = 2, Resize = 3, Restart = 4, Ping = 5;
495
+ public const byte Output = 16, Exit = 17, Error = 18, Restarting = 19, Status = 20;
496
+ const int MaxPayload = 1 << 20;
497
+
498
+ public static bool Read(Stream stream, out byte type, out byte[] payload)
499
+ {
500
+ type = 0; payload = null;
501
+ var header = new byte[5];
502
+ if (!ReadExactly(stream, header)) return false;
503
+ int length = BitConverter.ToInt32(header, 1);
504
+ if (length < 0 || length > MaxPayload) return false;
505
+ type = header[0];
506
+ payload = new byte[length];
507
+ return ReadExactly(stream, payload);
508
+ }
509
+
510
+ public static byte[] Encode(byte type, byte[] payload)
511
+ {
512
+ var frame = new byte[5 + payload.Length];
513
+ frame[0] = type;
514
+ BitConverter.GetBytes(payload.Length).CopyTo(frame, 1);
515
+ Buffer.BlockCopy(payload, 0, frame, 5, payload.Length);
516
+ return frame;
517
+ }
518
+
519
+ static bool ReadExactly(Stream stream, byte[] buffer)
520
+ {
521
+ int offset = 0;
522
+ while (offset < buffer.Length)
523
+ {
524
+ int n = stream.Read(buffer, offset, buffer.Length - offset);
525
+ if (n <= 0) return false;
526
+ offset += n;
527
+ }
528
+ return true;
529
+ }
530
+ }
531
+
532
+ sealed class Log
533
+ {
534
+ readonly string file;
535
+ public Log(string file) { this.file = file; }
536
+ public void Write(string message)
537
+ {
538
+ try { File.AppendAllText(file, DateTime.Now.ToString("s") + " " + message + Environment.NewLine); } catch (Exception) { }
539
+ }
540
+ }
541
+
542
+ static class Native
543
+ {
544
+ [StructLayout(LayoutKind.Sequential)] public struct Coord { public short X; public short Y; }
545
+
546
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
547
+ struct StartupInfo
548
+ {
549
+ public int cb; public string lpReserved; public string lpDesktop; public string lpTitle;
550
+ public int dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars, dwFillAttribute, dwFlags;
551
+ public short wShowWindow, cbReserved2; public IntPtr lpReserved2, hStdInput, hStdOutput, hStdError;
552
+ }
553
+
554
+ [StructLayout(LayoutKind.Sequential)] struct StartupInfoEx { public StartupInfo StartupInfo; public IntPtr lpAttributeList; }
555
+ [StructLayout(LayoutKind.Sequential)] struct ProcessInformation { public IntPtr hProcess, hThread; public int dwProcessId, dwThreadId; }
556
+
557
+ const uint ExtendedStartupInfoPresent = 0x00080000;
558
+ const int UseStdHandles = 0x00000100;
559
+ static readonly IntPtr PseudoConsoleAttribute = (IntPtr)0x00020016;
560
+
561
+ public static void Check(bool ok, string what) { if (!ok) throw new IOException(what + " failed: " + Marshal.GetLastWin32Error()); }
562
+
563
+ /** Starts `commandLine` attached to the pseudo console. Null std handles: never leak our own stdio into the child. */
564
+ public static IntPtr Spawn(string commandLine, IntPtr pseudoConsole)
565
+ {
566
+ IntPtr size = IntPtr.Zero;
567
+ InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref size);
568
+ IntPtr attributes = Marshal.AllocHGlobal(size);
569
+ try
570
+ {
571
+ Check(InitializeProcThreadAttributeList(attributes, 1, 0, ref size), "InitializeProcThreadAttributeList");
572
+ Check(UpdateProcThreadAttribute(attributes, 0, PseudoConsoleAttribute, pseudoConsole, (IntPtr)IntPtr.Size, IntPtr.Zero, IntPtr.Zero), "UpdateProcThreadAttribute");
573
+ var info = new StartupInfoEx();
574
+ info.StartupInfo.cb = Marshal.SizeOf(typeof(StartupInfoEx));
575
+ info.StartupInfo.dwFlags = UseStdHandles;
576
+ info.lpAttributeList = attributes;
577
+ ProcessInformation pi;
578
+ if (!CreateProcessW(null, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, false, ExtendedStartupInfoPresent, IntPtr.Zero, null, ref info, out pi))
579
+ throw new IOException("CreateProcess failed (" + Marshal.GetLastWin32Error() + "): " + commandLine);
580
+ CloseHandle(pi.hThread);
581
+ return pi.hProcess;
582
+ }
583
+ finally
584
+ {
585
+ DeleteProcThreadAttributeList(attributes);
586
+ Marshal.FreeHGlobal(attributes);
587
+ }
588
+ }
589
+
590
+ [DllImport("kernel32.dll")] public static extern int CreatePseudoConsole(Coord size, SafeFileHandle hInput, SafeFileHandle hOutput, uint dwFlags, out IntPtr phPC);
591
+ [DllImport("kernel32.dll")] public static extern int ResizePseudoConsole(IntPtr hPC, Coord size);
592
+ [DllImport("kernel32.dll")] public static extern void ClosePseudoConsole(IntPtr hPC);
593
+ [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, IntPtr lpPipeAttributes, int nSize);
594
+ [DllImport("kernel32.dll", SetLastError = true)] static extern bool InitializeProcThreadAttributeList(IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref IntPtr lpSize);
595
+ [DllImport("kernel32.dll", SetLastError = true)] static extern bool UpdateProcThreadAttribute(IntPtr lpAttributeList, uint dwFlags, IntPtr attribute, IntPtr lpValue, IntPtr cbSize, IntPtr lpPreviousValue, IntPtr lpReturnSize);
596
+ [DllImport("kernel32.dll")] static extern void DeleteProcThreadAttributeList(IntPtr lpAttributeList);
597
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
598
+ static extern bool CreateProcessW(string lpApplicationName, StringBuilder lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref StartupInfoEx lpStartupInfo, out ProcessInformation lpProcessInformation);
599
+ [DllImport("kernel32.dll")] public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
600
+ [DllImport("kernel32.dll")] public static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
601
+ [DllImport("kernel32.dll")] public static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode);
602
+ [DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr hObject);
603
+ }
@@ -0,0 +1,35 @@
1
+ @echo off
2
+ rem Runs first at logon, INSIDE the disposable sandbox only (the host PC is never touched).
3
+ rem Lets the sandbox's own tools run without "Open File - Security Warning", SmartScreen, script-policy or UAC prompts.
4
+ rem The security boundary is Windows Sandbox itself plus workspace-only write access and the LAN block; these prompts add
5
+ rem nothing inside a throwaway VM except interruptions.
6
+ set "POL=HKCU\Software\Microsoft\Windows\CurrentVersion\Policies"
7
+ reg add "%POL%\Associations" /v LowRiskFileTypes /t REG_SZ /d ".exe;.com;.cmd;.bat;.ps1;.psm1;.msi;.vbs;.js;.zip;.7z" /f >nul
8
+ reg add "%POL%\Attachments" /v SaveZoneInformation /t REG_DWORD /d 1 /f >nul
9
+ rem Mapped folders and UNC paths count as Local Intranet, where launching programs is allowed without asking.
10
+ reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap" /v UNCAsIntranet /t REG_DWORD /d 1 /f >nul
11
+ reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap" /v IntranetName /t REG_DWORD /d 1 /f >nul
12
+ rem Files copied from this PC keep their "downloaded from the internet" mark, so launching is allowed in every zone
13
+ rem (1806 = 0), for this user and machine-wide, and ShellExecute skips the zone check altogether.
14
+ for %%Z in (0 1 2 3 4) do (
15
+ reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\%%Z" /v 1806 /t REG_DWORD /d 0 /f >nul
16
+ reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\%%Z" /v 1806 /t REG_DWORD /d 0 /f >nul 2>&1
17
+ )
18
+ reg add "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v SEE_MASK_NOZONECHECKS /t REG_SZ /d 1 /f >nul 2>&1
19
+ set "SEE_MASK_NOZONECHECKS=1"
20
+ rem Paths over 260 characters (deep node_modules, Gradle and NuGet caches) work for every program.
21
+ reg add "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f >nul 2>&1
22
+ rem "Windows Defender Firewall has blocked some features of this app" pops up whenever a program (node, a dev server,
23
+ rem the session host) listens on the network. Inbound stays blocked, just silently; the attach rule lets this PC in.
24
+ netsh advfirewall set allprofiles settings inboundusernotification disable >nul 2>&1
25
+ for %%P in (DomainProfile PrivateProfile PublicProfile) do reg add "HKLM\SOFTWARE\Policies\Microsoft\WindowsFirewall\%%P" /v DisableNotifications /t REG_DWORD /d 1 /f >nul 2>&1
26
+ rem SmartScreen ("Windows protected your PC") for downloaded, unsigned tools.
27
+ reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\System" /v EnableSmartScreen /t REG_DWORD /d 0 /f >nul 2>&1
28
+ reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer" /v SmartScreenEnabled /t REG_SZ /d Off /f >nul 2>&1
29
+ rem PowerShell scripts (hooks, installers) run without the "Do you want to run ..." question.
30
+ reg add "HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" /v ExecutionPolicy /t REG_SZ /d Bypass /f >nul 2>&1
31
+ reg add "HKCU\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" /v ExecutionPolicy /t REG_SZ /d Bypass /f >nul
32
+ rem Elevation requests from the (already admin) sandbox user are granted without a dialog.
33
+ reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v ConsentPromptBehaviorAdmin /t REG_DWORD /d 0 /f >nul 2>&1
34
+ reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v PromptOnSecureDesktop /t REG_DWORD /d 0 /f >nul 2>&1
35
+ exit /b 0