windows-development-sandbox 1.12.0 → 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 (160) hide show
  1. package/README.md +1 -0
  2. package/config.example.json +11 -1
  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.def.js +1 -0
  12. package/dist/app/features/audit/audit.def.d.ts +11 -2
  13. package/dist/app/features/audit/audit.def.js +7 -4
  14. package/dist/app/features/audit/audit.run.d.ts +3 -0
  15. package/dist/app/features/defs.d.ts +155 -3
  16. package/dist/app/features/defs.js +12 -1
  17. package/dist/app/features/files/files.def.js +1 -0
  18. package/dist/app/features/health/health.def.js +1 -0
  19. package/dist/app/features/sandbox/sandbox.def.js +3 -0
  20. package/dist/app/features/sessions/sessions.def.d.ts +36 -0
  21. package/dist/app/features/sessions/sessions.def.js +77 -9
  22. package/dist/app/features/sessions/sessions.run.d.ts +25 -2
  23. package/dist/app/features/sessions/sessions.run.js +71 -3
  24. package/dist/app/features/settings/settings.def.d.ts +8 -0
  25. package/dist/app/features/settings/settings.def.js +17 -0
  26. package/dist/app/features/web/web.def.d.ts +9 -0
  27. package/dist/app/features/web/web.def.js +26 -1
  28. package/dist/app/features/web/web.run.d.ts +3 -0
  29. package/dist/app/features/web/web.run.js +11 -3
  30. package/dist/app/kernel/access.d.ts +33 -0
  31. package/dist/app/kernel/access.js +44 -0
  32. package/dist/app/kernel/define.d.ts +14 -1
  33. package/dist/app/kernel/index.d.ts +1 -0
  34. package/dist/app/kernel/index.js +1 -0
  35. package/dist/app/kernel/rpc.d.ts +5 -2
  36. package/dist/app/registry.d.ts +4 -1
  37. package/dist/app/registry.js +24 -4
  38. package/dist/cli/commands/registry.js +2 -0
  39. package/dist/cli/commands/remote.d.ts +2 -0
  40. package/dist/cli/commands/remote.js +49 -0
  41. package/dist/cli/commands/sessions/reopen.js +3 -0
  42. package/dist/cli/commands/web.js +66 -0
  43. package/dist/cli/manifest.d.ts +63 -4
  44. package/dist/cli/manifest.js +33 -4
  45. package/dist/cli/menu/io-menu.d.ts +5 -3
  46. package/dist/cli/menu/io-menu.js +12 -5
  47. package/dist/cli/menu/remote-menu.d.ts +19 -0
  48. package/dist/cli/menu/remote-menu.js +108 -0
  49. package/dist/cli/menu/views.js +2 -0
  50. package/dist/cli/operations/handler.js +15 -1
  51. package/dist/cli/services/remote-client.d.ts +16 -0
  52. package/dist/cli/services/remote-client.js +141 -0
  53. package/dist/cli/services/sessions-service.d.ts +5 -0
  54. package/dist/cli/services/sessions-service.js +17 -1
  55. package/dist/core/config/defaults.js +11 -1
  56. package/dist/core/local-config/file.js +16 -2
  57. package/dist/core/local-config/overrides.js +7 -0
  58. package/dist/core/run/versions.js +9 -8
  59. package/dist/core/schema/config.d.ts +22 -3
  60. package/dist/core/schema/config.js +39 -2
  61. package/dist/core/schema/web.d.ts +51 -0
  62. package/dist/core/schema/web.js +46 -1
  63. package/dist/core/util/fs.d.ts +5 -0
  64. package/dist/core/util/fs.js +23 -0
  65. package/dist/core/util/index.d.ts +1 -1
  66. package/dist/core/util/index.js +1 -1
  67. package/dist/guest/daemon/daemon.js +2 -0
  68. package/dist/guest/daemon/idle-shutdown.js +2 -1
  69. package/dist/guest/daemon/sessions.d.ts +16 -2
  70. package/dist/guest/daemon/sessions.js +126 -20
  71. package/dist/guest/git/decision.js +7 -0
  72. package/dist/guest/sessions/cmdline.d.ts +14 -0
  73. package/dist/guest/sessions/cmdline.js +10 -0
  74. package/dist/guest/toolchains/setup.js +3 -0
  75. package/dist/host/audit/log.d.ts +3 -2
  76. package/dist/host/audit/log.js +3 -3
  77. package/dist/host/channel/channel.d.ts +5 -1
  78. package/dist/host/channel/channel.js +12 -0
  79. package/dist/host/guard/agent-files.js +7 -1
  80. package/dist/host/guard/commands.js +13 -1
  81. package/dist/host/guard/git-config.js +27 -7
  82. package/dist/host/guard/git-dirs.js +78 -3
  83. package/dist/host/guard/mark-of-the-web.js +3 -1
  84. package/dist/host/guard/nested-repos.js +7 -2
  85. package/dist/host/guard/store.js +1 -0
  86. package/dist/host/guard/types.d.ts +2 -0
  87. package/dist/host/launch/mappings.js +11 -1
  88. package/dist/host/launch/project-scan.js +2 -21
  89. package/dist/host/launch/stage.js +4 -0
  90. package/dist/host/life/session-ledger.js +2 -1
  91. package/dist/host/remote/access-store.d.ts +156 -0
  92. package/dist/host/remote/access-store.js +308 -0
  93. package/dist/host/remote/cert.d.ts +20 -0
  94. package/dist/host/remote/cert.js +103 -0
  95. package/dist/host/remote/door-client.d.ts +13 -0
  96. package/dist/host/remote/door-client.js +54 -0
  97. package/dist/host/remote/firewall.d.ts +44 -0
  98. package/dist/host/remote/firewall.js +118 -0
  99. package/dist/host/remote/gate.d.ts +125 -0
  100. package/dist/host/remote/gate.js +391 -0
  101. package/dist/host/remote/index.d.ts +8 -0
  102. package/dist/host/remote/index.js +10 -0
  103. package/dist/host/remote/lan.d.ts +24 -0
  104. package/dist/host/remote/lan.js +88 -0
  105. package/dist/host/remote/tls-store.d.ts +22 -0
  106. package/dist/host/remote/tls-store.js +63 -0
  107. package/dist/host/remote/windows-helper.d.ts +67 -0
  108. package/dist/host/remote/windows-helper.js +129 -0
  109. package/dist/host/web/bridge.d.ts +5 -1
  110. package/dist/host/web/bridge.js +6 -2
  111. package/dist/host/web/browser.js +1 -1
  112. package/dist/host/web/control.d.ts +4 -1
  113. package/dist/host/web/control.js +28 -4
  114. package/dist/host/web/rpc.d.ts +16 -2
  115. package/dist/host/web/rpc.js +37 -4
  116. package/dist/host/web/runner.d.ts +4 -0
  117. package/dist/host/web/runner.js +12 -0
  118. package/dist/host/web/server.d.ts +18 -2
  119. package/dist/host/web/server.js +97 -24
  120. package/dist/host/web/state.d.ts +5 -0
  121. package/dist/host/web/state.js +8 -0
  122. package/dist/runtime/agent.js +74 -3
  123. package/dist/runtime/daemon.js +205 -24
  124. package/dist/runtime/db.js +74 -3
  125. package/dist/runtime/gitguard.js +6 -0
  126. package/dist/runtime/main.js +77 -3
  127. package/dist/runtime/netproxy.js +74 -3
  128. package/dist/runtime/view.js +76 -3
  129. package/dist/selftest/app.test.js +41 -6
  130. package/dist/selftest/channel.test.js +16 -0
  131. package/dist/selftest/cmdline.test.js +16 -0
  132. package/dist/selftest/git-access.test.js +8 -0
  133. package/dist/selftest/hostile-input.test.js +12 -1
  134. package/dist/selftest/index.js +2 -0
  135. package/dist/selftest/live-daemon.test.js +25 -0
  136. package/dist/selftest/local-config.test.js +30 -0
  137. package/dist/selftest/remote.test.d.ts +2 -0
  138. package/dist/selftest/remote.test.js +536 -0
  139. package/dist/selftest/repo-guard.test.js +56 -2
  140. package/dist/selftest/resume.test.js +2 -1
  141. package/dist/selftest/sharing.test.js +9 -2
  142. package/dist/selftest/support/fixtures.d.ts +13 -0
  143. package/dist/selftest/support/web-preview.js +63 -0
  144. package/dist/selftest/web.test.js +2 -0
  145. package/dist/shared/channel/schema.d.ts +23 -0
  146. package/dist/shared/channel/schema.js +8 -0
  147. package/dist/shared/pty/protocol.d.ts +2 -0
  148. package/dist/shared/pty/protocol.js +2 -0
  149. package/dist/web/index.html +1 -1
  150. package/dist/web/main-Z2WYAE2Z.js +221 -0
  151. package/native/csharp/HostTools/Program.cs +17 -1
  152. package/native/csharp/HostTools/WebHelper.cs +499 -0
  153. package/native/scripts/get-network-profile.ps1 +19 -0
  154. package/native/scripts/set-folder-access.ps1 +11 -0
  155. package/native/scripts/set-web-firewall.ps1 +103 -0
  156. package/package.json +1 -1
  157. package/sandbox/csharp/PtyHost.cs +20 -0
  158. package/schemas/config.schema.json +65 -0
  159. package/schemas/sandbox-config.schema.json +0 -18
  160. package/dist/web/main-UD4QCXZR.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;
@@ -26660,12 +26660,39 @@ var ResumeSchema = external_exports.strictObject({
26660
26660
  });
26661
26661
 
26662
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
+ });
26663
26689
  var WebSchema = external_exports.strictObject({
26664
26690
  enabled: external_exports.boolean().describe(
26665
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."
26666
26692
  ),
26667
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."),
26668
- 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.")
26669
26696
  });
26670
26697
 
26671
26698
  // src/core/schema/vpn.ts
@@ -26695,6 +26722,36 @@ var GithubSchema = external_exports.strictObject({
26695
26722
  });
26696
26723
 
26697
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
+ }
26698
26755
  var ConfigSchema = external_exports.strictObject({
26699
26756
  $schema: SchemaRefSchema.optional(),
26700
26757
  defaultHarness: NameSchema.describe("Agent started by `sandbox launch` (sandbox claude always runs claude)."),
@@ -26761,10 +26818,14 @@ var LocalOverridesSchema = external_exports.strictObject({
26761
26818
  scripts: StringsSchema.describe(
26762
26819
  "Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent."
26763
26820
  ),
26764
- // 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.
26765
26823
  sandbox: SandboxSchema.omit({
26766
26824
  cpuLimit: true,
26767
26825
  pageFileMb: true,
26826
+ reserveHostMb: true,
26827
+ maxMemoryPercent: true,
26828
+ maxMemoryMb: true,
26768
26829
  engine: true,
26769
26830
  agentMemoryHeadroomMb: true,
26770
26831
  pressureResponse: true
@@ -26778,7 +26839,10 @@ var LocalOverridesSchema = external_exports.strictObject({
26778
26839
  // Every toolchain setting, each one optional (a project usually only switches languages on or pins a version).
26779
26840
  toolchains: external_exports.strictObject({
26780
26841
  ...import_lodash.default.mapValues(import_lodash.default.omit(ToolchainsSchema.shape, "environment"), (shape) => shape.partial()),
26781
- 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
+ )
26782
26846
  }).partial(),
26783
26847
  databases: external_exports.strictObject(import_lodash.default.mapValues(DatabasesSchema.shape, (shape) => shape.partial())).partial(),
26784
26848
  attach: AttachSchema.partial(),
@@ -26848,6 +26912,7 @@ var ProjectSpecSchema = external_exports.strictObject({
26848
26912
  /** Its hooks were staged in C:\AgentLaunch\projects\<name>\hooks. */
26849
26913
  hooks: external_exports.boolean()
26850
26914
  });
26915
+ var ShellChoiceSchema = external_exports.enum(["auto", "pwsh", "powershell", "cmd"]);
26851
26916
  var GuestRequestSchema = external_exports.discriminatedUnion("kind", [
26852
26917
  external_exports.strictObject({
26853
26918
  kind: external_exports.literal("session"),
@@ -26857,8 +26922,12 @@ var GuestRequestSchema = external_exports.discriminatedUnion("kind", [
26857
26922
  cwd: Text(),
26858
26923
  project: ProjectSpecSchema.optional(),
26859
26924
  harnessSpec: HarnessSchema.optional(),
26925
+ /** A plain shell instead of an agent (harness is then "shell" and args are ignored). */
26926
+ shell: ShellChoiceSchema.optional(),
26860
26927
  createdAt: TimeSchema
26861
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 }),
26862
26931
  /** Provision a registered project without starting an agent (sandbox start / exec / db from its folder). */
26863
26932
  external_exports.strictObject({ kind: external_exports.literal("project"), id: IdSchema, project: ProjectSpecSchema, createdAt: TimeSchema }),
26864
26933
  external_exports.strictObject({
@@ -26924,6 +26993,8 @@ var SessionStatusSchema = external_exports.object({
26924
26993
  id: IdSchema,
26925
26994
  launchId: Text(64),
26926
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(),
26927
26998
  cwd: Text(),
26928
26999
  /** The project it runs in: its hub name, or the launch folder's for the first project. */
26929
27000
  project: Text(64).optional(),