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
@@ -1,9 +1,11 @@
1
1
  // The web UI's HTTP server: the app's files, a small JSON API for sessions, the terminal WebSocket and the RPC
2
2
  // WebSocket of the shared core. Loopback only; nothing works without the token's cookie, and Host / Origin / CSRF
3
3
  // checks keep other sites out (docs/web-ui.md).
4
+ import crypto from 'node:crypto';
4
5
  import fs from 'node:fs';
5
6
  import http from 'node:http';
6
7
  import { WebSocketServer } from 'ws';
8
+ import { allows, callerAccess, callerLabel, OWNER } from '../../app/kernel/index.js';
7
9
  import { UserError, safeText } from '../../core/util/index.js';
8
10
  import { MAX_CLIENT_MESSAGE_BYTES, MAX_COLS, MAX_ROWS, MIN_COLS, MIN_ROWS } from '../../shared/web/index.js';
9
11
  import { cookieHeader, csrfOk, hostAllowed, originAllowed, WebAuth } from './auth.js';
@@ -28,6 +30,9 @@ export const IDLE_EXIT_MS = 20 * 60_000;
28
30
  export const IDLE_NO_SANDBOX_MS = 5 * 60_000;
29
31
  const SESSION_PATH = /^\/api\/sessions\/([A-Za-z0-9_-]{1,64})\/terminal$/;
30
32
  export const RPC_PATH = '/api/rpc';
33
+ /** What a service worker registered on this origin by someone else gets when the browser updates it: gone. */
34
+ const UNREGISTER_WORKER = "self.addEventListener('install',()=>self.skipWaiting());" +
35
+ "self.addEventListener('activate',(e)=>e.waitUntil(self.registration.unregister()));";
31
36
  class HttpFailure extends Error {
32
37
  status;
33
38
  constructor(status, message) {
@@ -80,7 +85,19 @@ export class WebServer {
80
85
  this.idleCheck = setInterval(() => void this.checkIdle(), Math.min(60_000, this.quietMs()));
81
86
  this.idleCheck.unref();
82
87
  }
83
- resolve();
88
+ const gate = this.options.gate;
89
+ if (!gate)
90
+ return resolve();
91
+ gate.start();
92
+ void gate
93
+ .startLan({
94
+ request: (req, res) => void this.handle(req, res),
95
+ upgrade: (req, socket, head) => {
96
+ this.lastUsed = Date.now();
97
+ this.upgrade(req, socket, head);
98
+ },
99
+ })
100
+ .finally(resolve);
84
101
  });
85
102
  });
86
103
  }
@@ -155,7 +172,8 @@ export class WebServer {
155
172
  for (const client of this.sockets.clients)
156
173
  client.close(1001, 'server stopping');
157
174
  this.server.closeAllConnections();
158
- return new Promise((resolve) => this.server.close(() => resolve()));
175
+ const gate = this.options.gate?.stop();
176
+ return new Promise((resolve) => this.server.close(() => void Promise.resolve(gate).then(() => resolve())));
159
177
  }
160
178
  headers(extra = {}) {
161
179
  return {
@@ -175,7 +193,7 @@ export class WebServer {
175
193
  async handle(req, res) {
176
194
  this.lastUsed = Date.now();
177
195
  try {
178
- if (!hostAllowed(req.headers.host, this.actualPort))
196
+ if (!this.hostOk(req))
179
197
  throw new HttpFailure(421, 'Wrong host name.');
180
198
  const url = new URL(req.url ?? '/', 'http://127.0.0.1');
181
199
  if (url.pathname.startsWith('/api/'))
@@ -192,9 +210,35 @@ export class WebServer {
192
210
  this.json(res, 500, { error: 'Something went wrong.' });
193
211
  }
194
212
  }
213
+ /** The LAN listener's own address:port, or loopback with our port. */
214
+ hostOk(req) {
215
+ const gate = this.options.gate;
216
+ if (gate?.isLan(req.socket))
217
+ return gate.lanHostAllowed(req.headers.host);
218
+ return hostAllowed(req.headers.host, this.actualPort);
219
+ }
220
+ originOk(req) {
221
+ const gate = this.options.gate;
222
+ if (gate?.isLan(req.socket))
223
+ return gate.lanOriginAllowed(req.headers.origin, req.headers.host);
224
+ return originAllowed(req.headers.origin, req.headers.host, this.actualPort);
225
+ }
226
+ /** Who sent the request: the owner's token cookie (loopback only), or whoever the gate lets in. Null: nobody. */
227
+ async callerOf(req) {
228
+ const gate = this.options.gate;
229
+ const ownerCookie = !gate?.isLan(req.socket) && this.auth.signedIn(req.headers.cookie);
230
+ if (!gate)
231
+ return { caller: ownerCookie ? OWNER : null, ownerCookie };
232
+ return { caller: await gate.caller(req, ownerCookie), ownerCookie };
233
+ }
195
234
  serveFile(req, res, pathname) {
196
235
  if (req.method !== 'GET' && req.method !== 'HEAD')
197
236
  throw new HttpFailure(405, 'Not allowed.');
237
+ if (req.headers['service-worker'] === 'script') {
238
+ res.writeHead(200, this.headers({ 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'no-store' }));
239
+ res.end(UNREGISTER_WORKER);
240
+ return;
241
+ }
198
242
  const file = this.files.find(pathname);
199
243
  if (!file)
200
244
  throw new HttpFailure(404, 'Not found.');
@@ -211,14 +255,21 @@ export class WebServer {
211
255
  }
212
256
  /** A call that changes something: the custom header, and an Origin that is this page. */
213
257
  requireSameSite(req) {
214
- if (!csrfOk(req.headers) || !originAllowed(req.headers.origin, req.headers.host, this.actualPort))
258
+ if (!csrfOk(req.headers) || !this.originOk(req))
215
259
  throw new HttpFailure(403, 'Refused.');
216
260
  }
217
261
  async api(req, res, pathname) {
218
262
  const method = req.method ?? 'GET';
219
263
  const { backend } = this.options;
220
264
  if (pathname === '/api/health' && method === 'GET')
221
- return this.json(res, 200, { ok: true });
265
+ return this.json(res, 200, this.health(req));
266
+ if (this.options.gate &&
267
+ (await this.options.gate.route(req, pathname, {
268
+ body: () => this.body(req),
269
+ json: (status, body, headers) => this.json(res, status, body, headers),
270
+ requireSameSite: () => this.requireSameSite(req),
271
+ })))
272
+ return;
222
273
  if (pathname === '/api/login' && method === 'POST') {
223
274
  this.requireSameSite(req);
224
275
  const body = await this.body(req);
@@ -227,14 +278,19 @@ export class WebServer {
227
278
  throw new HttpFailure(result.status, result.status === 429 ? 'Too many tries.' : 'Wrong token.');
228
279
  return this.json(res, 200, { ok: true }, { 'Set-Cookie': cookieHeader(result.cookie) });
229
280
  }
230
- if (!this.auth.signedIn(req.headers.cookie))
281
+ const { caller } = await this.callerOf(req);
282
+ if (!caller)
231
283
  throw new HttpFailure(401, 'Not signed in.');
284
+ if (pathname === '/api/me' && method === 'GET')
285
+ return this.json(res, 200, { kind: caller.kind, who: callerLabel(caller), access: callerAccess(caller) });
232
286
  if (pathname === '/api/info' && method === 'GET')
233
287
  return this.json(res, 200, await backend.info());
234
288
  if (pathname === '/api/sessions' && method === 'GET')
235
289
  return this.json(res, 200, await backend.sessions());
236
290
  if (pathname === '/api/stop' && method === 'POST') {
237
291
  this.requireSameSite(req);
292
+ if (caller.kind !== 'owner')
293
+ throw new HttpFailure(403, 'Only the owner of this PC can stop the web UI.');
238
294
  this.json(res, 200, { ok: true });
239
295
  setTimeout(() => this.options.onStop?.(), 100);
240
296
  return;
@@ -278,31 +334,48 @@ export class WebServer {
278
334
  if (!match && !rpc)
279
335
  return refuse('404 Not Found');
280
336
  // The Origin must be this page (a page of another site can open WebSockets too) and the cookie is the login.
281
- if (!originAllowed(req.headers.origin, req.headers.host, this.actualPort))
337
+ if (!this.hostOk(req) || !this.originOk(req))
282
338
  return refuse('403 Forbidden');
283
- if (!this.auth.signedIn(req.headers.cookie))
284
- return refuse('401 Unauthorized');
285
- if (rpc)
286
- return this.upgradeRpc(req, socket, head, rpc);
287
- if (!match)
288
- return refuse('404 Not Found');
289
- const endpoint = this.options.backend.endpoint(match[1]);
290
- if (!endpoint)
291
- return refuse('404 Not Found');
292
- const cols = bounded(url.searchParams.get('cols'), MIN_COLS, MAX_COLS, 120);
293
- const rows = bounded(url.searchParams.get('rows'), MIN_ROWS, MAX_ROWS, 30);
294
- this.sockets.handleUpgrade(req, socket, head, (ws) => {
295
- this.track(ws, 'terminal', MAX_TERMINALS);
296
- void new TerminalBridge(ws, endpoint, cols, rows, this.options.dial).start();
339
+ void this.callerOf(req).then(({ caller, ownerCookie }) => {
340
+ if (!caller)
341
+ return refuse('401 Unauthorized');
342
+ if (rpc)
343
+ return this.upgradeRpc(req, socket, head, rpc, caller, ownerCookie);
344
+ if (!match)
345
+ return refuse('404 Not Found');
346
+ const endpoint = this.options.backend.endpoint(match[1]);
347
+ if (!endpoint)
348
+ return refuse('404 Not Found');
349
+ const cols = bounded(url.searchParams.get('cols'), MIN_COLS, MAX_COLS, 120);
350
+ const rows = bounded(url.searchParams.get('rows'), MIN_ROWS, MAX_ROWS, 30);
351
+ // View only: the terminal shows, and the bridge drops every keystroke and resize.
352
+ const readOnly = !allows(caller, 'control');
353
+ this.sockets.handleUpgrade(req, socket, head, (ws) => {
354
+ this.track(ws, 'terminal', MAX_TERMINALS);
355
+ this.options.gate?.track(ws, req, ownerCookie, caller);
356
+ void new TerminalBridge(ws, endpoint, cols, rows, this.options.dial, readOnly).start();
357
+ });
297
358
  });
298
359
  }
299
- /** The RPC socket (after the same Origin and cookie checks as a terminal). */
300
- upgradeRpc(req, socket, head, rpc) {
360
+ /** The RPC socket (after the same Origin and caller checks as a terminal). */
361
+ upgradeRpc(req, socket, head, rpc, caller, ownerCookie) {
301
362
  this.sockets.handleUpgrade(req, socket, head, (ws) => {
302
363
  this.track(ws, 'rpc', MAX_RPC_SOCKETS);
303
- new RpcSession(ws, rpc, this.live).start();
364
+ const gate = this.options.gate;
365
+ const now = gate ? gate.track(ws, req, ownerCookie, caller) : () => caller;
366
+ new RpcSession(ws, rpc, this.live, now, gate ? (entry) => gate.recordCall(entry) : undefined).start();
304
367
  });
305
368
  }
369
+ /**
370
+ * GET /api/health: alive, and with ?challenge=<hex> on loopback the proof that this server holds the token (an
371
+ * HMAC of the challenge), so the CLI never hands the token link to another program on the port.
372
+ */
373
+ health(req) {
374
+ const challenge = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams.get('challenge') ?? '';
375
+ if (this.options.gate?.isLan(req.socket) || !/^[0-9a-f]{32,128}$/.test(challenge))
376
+ return { ok: true };
377
+ return { ok: true, proof: crypto.createHmac('sha256', this.options.token).update(challenge).digest('hex') };
378
+ }
306
379
  }
307
380
  function bounded(text, min, max, fallback) {
308
381
  const n = Number(text);
@@ -5,6 +5,11 @@ export declare const WebStateSchema: z.ZodObject<{
5
5
  token: z.ZodString;
6
6
  startedAt: z.ZodString;
7
7
  version: z.ZodOptional<z.ZodString>;
8
+ lan: z.ZodOptional<z.ZodObject<{
9
+ url: z.ZodNullable<z.ZodString>;
10
+ fingerprint: z.ZodNullable<z.ZodString>;
11
+ problem: z.ZodNullable<z.ZodString>;
12
+ }, z.core.$strict>>;
8
13
  }, z.core.$strict>;
9
14
  export type WebState = z.output<typeof WebStateSchema>;
10
15
  export declare class WebStore {
@@ -12,6 +12,14 @@ export const WebStateSchema = z.strictObject({
12
12
  startedAt: z.string().max(64),
13
13
  /** The version that started it (missing: an older one). Another version is replaced, never reused. */
14
14
  version: z.string().max(64).optional(),
15
+ /** LAN mode (docs/design/remote-access.md): its address for phones, the certificate, or why it is not running. */
16
+ lan: z
17
+ .strictObject({
18
+ url: z.string().max(64).nullable(),
19
+ fingerprint: z.string().max(100).nullable(),
20
+ problem: z.string().max(500).nullable(),
21
+ })
22
+ .optional(),
15
23
  });
16
24
  export class WebStore {
17
25
  storageRoot;
@@ -25541,14 +25541,19 @@ var NetworkSchema = external_exports.strictObject({
25541
25541
  });
25542
25542
  var AuditSchema = external_exports.strictObject({
25543
25543
  enabled: external_exports.boolean().describe(
25544
- "Keep an audit log on this PC of what happened in your sandboxes: programs started (with their command lines), files changed in the project, sessions, and what the repo guard caught. The sandbox cannot change it. Read it with sandbox audit."
25544
+ "Keep an audit log on this PC of what your agents did: programs started (with their command lines, secrets hidden), files changed in the projects, git commits, sessions, and what the repo guard caught, each linked to the agent session that did it where that can be known. The sandbox cannot change it. Read it with sandbox audit or on the Activity page."
25545
+ ),
25546
+ days: external_exports.number().int().min(1).max(365).describe(
25547
+ "Days the audit log is kept (1 to 30; a larger number keeps 30); older days are deleted when a sandbox starts and once a day."
25545
25548
  ),
25546
- days: external_exports.number().int().min(1).max(365).describe("Days the audit log is kept."),
25547
25549
  maxMb: external_exports.number().int().min(5).max(5e3).describe("Most space the audit log may take, in MB (older days go first). Days before today are compressed."),
25548
25550
  processes: external_exports.boolean().describe(
25549
25551
  "Record every program started in the sandbox with its command line (Windows process auditing; repeated runs are counted, not listed)."
25550
25552
  ),
25551
- files: external_exports.boolean().describe("Record the files changed in the project while the sandbox runs.")
25553
+ files: external_exports.boolean().describe("Record the files changed in the projects while the sandbox runs."),
25554
+ commits: external_exports.boolean().describe(
25555
+ "Record the git commits made in the projects while the sandbox runs (read from each project's .git folder on this PC)."
25556
+ )
25552
25557
  });
25553
25558
  var WorkspaceFilterSchema = external_exports.strictObject({
25554
25559
  enabled: external_exports.boolean().describe(
@@ -25751,7 +25756,100 @@ var ToolchainsSchema = external_exports.strictObject({
25751
25756
 
25752
25757
  // src/shared/apps/catalogue.ts
25753
25758
  var gh = (github, asset) => ({ github, asset });
25759
+ var RTK_VERSION = "1.0.1";
25760
+ var RTK_URL = "https://github.com/rtk-ai/rtk/releases/download/v" + RTK_VERSION + "/rtk-oss-x86_64-pc-windows-msvc.zip";
25761
+ var RTK_SHA256 = "0e53e2eca471e15fbd3636e64e6bc0b182cbc278ed0339c80450a2dd03f4cd6b";
25762
+ var HEADROOM_VERSION = "0.41.0";
25763
+ var CCUSAGE_VERSION = "20.0.36";
25764
+ var PLAYWRIGHT_MCP_VERSION = "0.0.83";
25765
+ var CONTEXT7_MCP_VERSION = "4.4.2";
25754
25766
  var APPS = [
25767
+ // ---- agent add-ons
25768
+ {
25769
+ id: "rtk",
25770
+ name: "rtk (Rust Token Killer)",
25771
+ category: "agents",
25772
+ about: "Compacts the output of shell commands (git, tests, builds, ls ...) before the agent reads it, so it uses fewer tokens. Claude Code's Bash commands go through it on their own.",
25773
+ size: "6 MB",
25774
+ source: { url: RTK_URL, sha256: RTK_SHA256 },
25775
+ exe: "rtk.exe",
25776
+ cli: true,
25777
+ plugsInto: "Claude Code through a hook in the sandbox's managed settings; other agents are told to use it",
25778
+ userOnly: true
25779
+ },
25780
+ {
25781
+ id: "headroom",
25782
+ name: "Headroom",
25783
+ category: "agents",
25784
+ about: "Compresses what Claude Code sends to the model (tool output, logs, files) so it uses fewer tokens: a local proxy on 127.0.0.1:8787 that sits in Claude Code's API path, offline apart from forwarding to api.anthropic.com.",
25785
+ size: "150 MB",
25786
+ source: { locked: "uv", package: "headroom-ai", version: HEADROOM_VERSION },
25787
+ exe: "headroom.exe",
25788
+ service: true,
25789
+ plugsInto: "Claude Code: the sandbox starts the proxy and points Claude Code at it (managed settings)",
25790
+ userOnly: true
25791
+ },
25792
+ {
25793
+ id: "ccusage",
25794
+ name: "ccusage",
25795
+ category: "agents",
25796
+ about: "Reports the agents' token use and cost by day, month or session from their logs in the sandbox (`ccusage daily`; add --offline to skip fetching prices).",
25797
+ size: "5 MB",
25798
+ source: { locked: "npm", package: "ccusage", version: CCUSAGE_VERSION },
25799
+ exe: "ccusage.cmd",
25800
+ cli: true,
25801
+ plugsInto: "Reads Claude Code's session logs in the sandbox; changes nothing",
25802
+ userOnly: true
25803
+ },
25804
+ {
25805
+ id: "playwright-mcp",
25806
+ name: "Playwright MCP",
25807
+ category: "agents",
25808
+ about: "Lets Claude Code drive a browser (Microsoft Edge, already in the sandbox) to open pages, click, fill forms and take screenshots: the MCP server of microsoft/playwright-mcp.",
25809
+ size: "20 MB",
25810
+ source: { locked: "npm", package: "@playwright/mcp", version: PLAYWRIGHT_MCP_VERSION },
25811
+ exe: "playwright-mcp.cmd",
25812
+ service: true,
25813
+ mcp: { name: "sbx-playwright", script: "node_modules/@playwright/mcp/cli.js", args: ["--browser", "msedge"] },
25814
+ plugsInto: "Claude Code as the MCP server sbx-playwright (the sandbox's Claude settings)",
25815
+ userOnly: true
25816
+ },
25817
+ {
25818
+ id: "context7",
25819
+ name: "Context7 MCP",
25820
+ category: "agents",
25821
+ about: "Up-to-date documentation and code examples for libraries, looked up by Claude Code while it works (upstash/context7). The library names and questions go to Upstash's context7.com.",
25822
+ size: "55 MB",
25823
+ source: { locked: "npm", package: "@upstash/context7-mcp", version: CONTEXT7_MCP_VERSION },
25824
+ exe: "context7-mcp.cmd",
25825
+ service: true,
25826
+ mcp: { name: "sbx-context7", script: "node_modules/@upstash/context7-mcp/dist/index.js" },
25827
+ plugsInto: "Claude Code as the MCP server sbx-context7 (the sandbox's Claude settings)",
25828
+ userOnly: true
25829
+ },
25830
+ {
25831
+ id: "claude-mem",
25832
+ name: "claude-mem",
25833
+ category: "agents",
25834
+ about: "Persistent memory for Claude Code (a plugin you install on this PC): its database is brought into the sandbox.",
25835
+ size: "",
25836
+ source: { resolve: "setting" },
25837
+ exe: "",
25838
+ setting: "claudeProfile.syncClaudeMem",
25839
+ plugsInto: "Claude Code, with your Claude settings copied from this PC",
25840
+ userOnly: true
25841
+ },
25842
+ {
25843
+ id: "rea",
25844
+ name: "REA (Reverse Engineer Anything)",
25845
+ category: "agents",
25846
+ about: "Agent-driven reverse engineering (MCP server and CLI, by morluto). Run `rea setup` once to add it to Claude Code and Codex. Native binaries need Ghidra; needs Node 22.19+.",
25847
+ size: "8 MB",
25848
+ source: { npm: "rea-agents" },
25849
+ exe: "rea.cmd",
25850
+ cli: true,
25851
+ plugsInto: "Claude Code and Codex as an MCP server, after `rea setup`"
25852
+ },
25755
25853
  // ---- editors
25756
25854
  {
25757
25855
  id: "vscode",
@@ -26213,16 +26311,6 @@ var APPS = [
26213
26311
  cli: true
26214
26312
  },
26215
26313
  // ---- reverse engineering
26216
- {
26217
- id: "rea",
26218
- name: "REA (Reverse Engineer Anything)",
26219
- category: "reversing",
26220
- about: "Agent-driven reverse engineering (MCP server and CLI, by morluto). Run `rea setup` once to add it to Claude Code and Codex. Native binaries need Ghidra; needs Node 22.19+.",
26221
- size: "8 MB",
26222
- source: { npm: "rea-agents" },
26223
- exe: "rea.cmd",
26224
- cli: true
26225
- },
26226
26314
  {
26227
26315
  id: "ghidra",
26228
26316
  name: "Ghidra",
@@ -26502,6 +26590,8 @@ var APPS = [
26502
26590
  // src/core/schema/apps.ts
26503
26591
  var CATALOGUE_IDS = APPS.filter((a) => !a.setting).map((a) => a.id);
26504
26592
  var AppIdSchema = external_exports.enum(CATALOGUE_IDS);
26593
+ var PROJECT_IDS = APPS.filter((a) => !a.setting && !a.userOnly).map((a) => a.id);
26594
+ var ProjectAppIdSchema = external_exports.enum(PROJECT_IDS);
26505
26595
  var CustomAppIdSchema = external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,40}$/, "lower-case letters, digits and - only");
26506
26596
  var compiles = (pattern) => {
26507
26597
  try {
@@ -26525,6 +26615,11 @@ var CustomAppSchema = external_exports.strictObject({
26525
26615
  }).refine((a) => a.github && a.asset ? !a.url : Boolean(a.url) && !a.github && !a.asset, {
26526
26616
  message: "give github + asset, or url (not both)"
26527
26617
  });
26618
+ var ProjectAppsSchema = external_exports.strictObject({
26619
+ install: external_exports.array(ProjectAppIdSchema).describe(
26620
+ "Apps from the catalogue this project adds to yours (agent add-ons such as rtk, Headroom and the MCP servers are set in your own settings only)."
26621
+ )
26622
+ });
26528
26623
  var AppsSchema = external_exports.strictObject({
26529
26624
  install: external_exports.array(AppIdSchema).describe(
26530
26625
  "Apps from the catalogue to install in every sandbox (the Apps screen in the menu lists them by category). Each is downloaded once and kept."
@@ -26565,12 +26660,39 @@ var ResumeSchema = external_exports.strictObject({
26565
26660
  });
26566
26661
 
26567
26662
  // src/core/schema/web.ts
26663
+ var REMOTE_ROLES = ["view", "control"];
26664
+ var WindowsUserNameSchema = external_exports.string().trim().min(1).max(120).regex(
26665
+ /^(?:[A-Za-z0-9][A-Za-z0-9._ -]{0,62}\\)?[^\\/"[\]:;|=,+*?<>@]{1,64}$/,
26666
+ "a Windows user name (name or DOMAIN\\name)"
26667
+ );
26668
+ var RemoteUserSchema = external_exports.strictObject({
26669
+ name: WindowsUserNameSchema.describe("The Windows user (name, or DOMAIN\\name)"),
26670
+ role: external_exports.enum(REMOTE_ROLES).describe("view: sees everything, terminals read-only. control: everything but remote access, logins and tokens.")
26671
+ });
26672
+ var WebLanSchema = external_exports.strictObject({
26673
+ enabled: external_exports.boolean().describe(
26674
+ "(Beta) Devices on your network (a phone): an HTTPS address on this PC for devices you pair with a one-time code (sandbox web pair). Needs a firewall rule (sandbox web lan on asks Windows once). Off: every paired device is signed out."
26675
+ ),
26676
+ port: external_exports.number().int().min(1024).max(65535).describe("HTTPS port for paired devices on your network."),
26677
+ address: external_exports.string().trim().max(15).regex(/^$|^\d{1,3}(\.\d{1,3}){3}$/, "an IPv4 address or empty").describe("This PC's private address to listen on. Empty: the one of its network adapter (never a public one).")
26678
+ });
26679
+ var WebRemoteSchema = external_exports.strictObject({
26680
+ windowsSignIn: external_exports.boolean().describe(
26681
+ "(Beta) Other Windows users of this PC named in web.remote.users sign in to your web UI (and sandbox remote) with their own Windows account: no password, no link."
26682
+ ),
26683
+ users: external_exports.array(RemoteUserSchema).max(32).describe("Who else may sign in with Windows, and their role (view or control)."),
26684
+ rememberDays: external_exports.number().int().min(1).max(90).describe(
26685
+ "How many days a paired device or a Windows sign-in stays signed in (from when it signed in, not renewed)."
26686
+ ),
26687
+ lan: WebLanSchema
26688
+ });
26568
26689
  var WebSchema = external_exports.strictObject({
26569
26690
  enabled: external_exports.boolean().describe(
26570
26691
  "(Beta) The web UI: your sandbox sessions in a browser on this PC (http://127.0.0.1:<port>, this PC only). On: every sandbox start also starts it in the background (it keeps running until sandbox web stop or a restart of the PC). Off: only sandbox web start runs it. Open it signed in with sandbox web open."
26571
26692
  ),
26572
26693
  port: external_exports.number().int().min(1024).max(65535).describe("Port of the web UI on 127.0.0.1 (this PC only). Pick another when it is taken."),
26573
- openOnLaunch: external_exports.boolean().describe("(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts.")
26694
+ openOnLaunch: external_exports.boolean().describe("(Beta) With the web UI on: open it in your browser, already signed in, every time a sandbox starts."),
26695
+ remote: WebRemoteSchema.describe("(Beta) Access for other Windows users of this PC and for paired devices.")
26574
26696
  });
26575
26697
 
26576
26698
  // src/core/schema/vpn.ts
@@ -26600,6 +26722,36 @@ var GithubSchema = external_exports.strictObject({
26600
26722
  });
26601
26723
 
26602
26724
  // src/core/schema/config.ts
26725
+ var RESERVED_ENV_PREFIX = /^(AGENT_|GIT_|GH_|GITHUB_|NODE_|NPM_CONFIG_(USERCONFIG|GLOBALCONFIG|PREFIX|SCRIPT_SHELL)|SSL_|CLAUDE|ANTHROPIC_|OPENAI_|CODEX_|GEMINI_|GOOGLE_|COPILOT_|AWS_|AZURE_)/i;
26726
+ var RESERVED_ENV_SUFFIX = /(_PROXY|_BASE_URL|_ENDPOINT|_CA_BUNDLE|_CA_CERTS)$/i;
26727
+ var RESERVED_ENV_NAMES = new Set(
26728
+ [
26729
+ "PATH",
26730
+ "PATHEXT",
26731
+ "BASH_ENV",
26732
+ "ENV",
26733
+ "COMSPEC",
26734
+ "PSMODULEPATH",
26735
+ "PYTHONPATH",
26736
+ "PYTHONSTARTUP",
26737
+ "PERL5OPT",
26738
+ "RUBYOPT",
26739
+ "JAVA_TOOL_OPTIONS",
26740
+ "LD_PRELOAD",
26741
+ "SYSTEMROOT",
26742
+ "WINDIR",
26743
+ "HOME",
26744
+ "USERPROFILE",
26745
+ "APPDATA",
26746
+ "LOCALAPPDATA",
26747
+ "PROGRAMDATA",
26748
+ "TEMP",
26749
+ "TMP"
26750
+ ].map((n) => n.toUpperCase())
26751
+ );
26752
+ function reservedEnvName(name) {
26753
+ return RESERVED_ENV_PREFIX.test(name) || RESERVED_ENV_SUFFIX.test(name) || RESERVED_ENV_NAMES.has(name.toUpperCase());
26754
+ }
26603
26755
  var ConfigSchema = external_exports.strictObject({
26604
26756
  $schema: SchemaRefSchema.optional(),
26605
26757
  defaultHarness: NameSchema.describe("Agent started by `sandbox launch` (sandbox claude always runs claude)."),
@@ -26666,10 +26818,14 @@ var LocalOverridesSchema = external_exports.strictObject({
26666
26818
  scripts: StringsSchema.describe(
26667
26819
  "Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent."
26668
26820
  ),
26669
- // This PC's resources are yours to share: CPU and paging file (this PC's disk) are not a project's to set.
26821
+ // This PC's resources are yours to share: CPU, paging file (this PC's disk) and the RAM kept for this PC are not a
26822
+ // project's to set.
26670
26823
  sandbox: SandboxSchema.omit({
26671
26824
  cpuLimit: true,
26672
26825
  pageFileMb: true,
26826
+ reserveHostMb: true,
26827
+ maxMemoryPercent: true,
26828
+ maxMemoryMb: true,
26673
26829
  engine: true,
26674
26830
  agentMemoryHeadroomMb: true,
26675
26831
  pressureResponse: true
@@ -26683,14 +26839,17 @@ var LocalOverridesSchema = external_exports.strictObject({
26683
26839
  // Every toolchain setting, each one optional (a project usually only switches languages on or pins a version).
26684
26840
  toolchains: external_exports.strictObject({
26685
26841
  ...import_lodash.default.mapValues(import_lodash.default.omit(ToolchainsSchema.shape, "environment"), (shape) => shape.partial()),
26686
- environment: ToolchainsSchema.shape.environment
26842
+ environment: ToolchainsSchema.shape.environment.refine(
26843
+ (env) => !Object.keys(env).some(reservedEnvName),
26844
+ "names the sandbox sets or that carry logins (AGENT_*, GIT_*, PATH, BASH_ENV, NODE_*, proxies, ANTHROPIC_*, CLAUDE_* ...) cannot be set by a project"
26845
+ )
26687
26846
  }).partial(),
26688
26847
  databases: external_exports.strictObject(import_lodash.default.mapValues(DatabasesSchema.shape, (shape) => shape.partial())).partial(),
26689
26848
  attach: AttachSchema.partial(),
26690
26849
  personalization: toggle("Copy your desktop look into the sandbox.").partial(),
26691
26850
  // Catalogue apps only (a vetted list): your own programs (apps.custom) download anything, so a repo cannot add
26692
- // them.
26693
- apps: AppsSchema.pick({ install: true }).partial(),
26851
+ // them, nor agent add-ons that are yours only (rtk hooks every agent command, Headroom sits in its API path).
26852
+ apps: ProjectAppsSchema.partial(),
26694
26853
  claudeProfile: ClaudeProfileSchema.pick({
26695
26854
  enabled: true,
26696
26855
  overwrite: true,
@@ -26753,6 +26912,7 @@ var ProjectSpecSchema = external_exports.strictObject({
26753
26912
  /** Its hooks were staged in C:\AgentLaunch\projects\<name>\hooks. */
26754
26913
  hooks: external_exports.boolean()
26755
26914
  });
26915
+ var ShellChoiceSchema = external_exports.enum(["auto", "pwsh", "powershell", "cmd"]);
26756
26916
  var GuestRequestSchema = external_exports.discriminatedUnion("kind", [
26757
26917
  external_exports.strictObject({
26758
26918
  kind: external_exports.literal("session"),
@@ -26762,8 +26922,12 @@ var GuestRequestSchema = external_exports.discriminatedUnion("kind", [
26762
26922
  cwd: Text(),
26763
26923
  project: ProjectSpecSchema.optional(),
26764
26924
  harnessSpec: HarnessSchema.optional(),
26925
+ /** A plain shell instead of an agent (harness is then "shell" and args are ignored). */
26926
+ shell: ShellChoiceSchema.optional(),
26765
26927
  createdAt: TimeSchema
26766
26928
  }),
26929
+ /** End a running session (an agent or a shell) and everything it started. */
26930
+ external_exports.strictObject({ kind: external_exports.literal("close"), id: IdSchema, session: IdSchema, createdAt: TimeSchema }),
26767
26931
  /** Provision a registered project without starting an agent (sandbox start / exec / db from its folder). */
26768
26932
  external_exports.strictObject({ kind: external_exports.literal("project"), id: IdSchema, project: ProjectSpecSchema, createdAt: TimeSchema }),
26769
26933
  external_exports.strictObject({
@@ -26829,6 +26993,8 @@ var SessionStatusSchema = external_exports.object({
26829
26993
  id: IdSchema,
26830
26994
  launchId: Text(64),
26831
26995
  harness: Text(64),
26996
+ /** A plain shell (harness is then the shell: pwsh, powershell or cmd); absent: an agent. */
26997
+ kind: external_exports.enum(["agent", "shell"]).optional(),
26832
26998
  cwd: Text(),
26833
26999
  /** The project it runs in: its hub name, or the launch folder's for the first project. */
26834
27000
  project: Text(64).optional(),