windows-development-sandbox 1.18.3 → 1.20.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 (216) hide show
  1. package/README.md +1 -1
  2. package/config.example.json +9 -0
  3. package/dist/app/features/accounts/accounts.def.d.ts +4 -0
  4. package/dist/app/features/accounts/accounts.run.d.ts +1 -0
  5. package/dist/app/features/accounts/services.def.d.ts +3 -0
  6. package/dist/app/features/accounts/services.def.js +4 -2
  7. package/dist/app/features/accounts/services.run.d.ts +1 -0
  8. package/dist/app/features/accounts/services.run.js +3 -1
  9. package/dist/app/features/agent-commands/agent-commands.def.d.ts +37 -0
  10. package/dist/app/features/agent-commands/agent-commands.def.js +91 -0
  11. package/dist/app/features/agent-commands/agent-commands.run.d.ts +28 -0
  12. package/dist/app/features/agent-commands/agent-commands.run.js +35 -0
  13. package/dist/app/features/agent-commands/index.d.ts +2 -0
  14. package/dist/app/features/agent-commands/index.js +3 -0
  15. package/dist/app/features/agents/agents.def.d.ts +14 -1
  16. package/dist/app/features/agents/agents.def.js +61 -6
  17. package/dist/app/features/agents/agents.run.d.ts +1 -0
  18. package/dist/app/features/agents/agents.run.js +8 -1
  19. package/dist/app/features/agents/claude-env.def.d.ts +36 -0
  20. package/dist/app/features/agents/claude-env.def.js +305 -0
  21. package/dist/app/features/agents/env.def.d.ts +163 -0
  22. package/dist/app/features/agents/env.def.js +184 -0
  23. package/dist/app/features/agents/env.run.d.ts +43 -0
  24. package/dist/app/features/agents/env.run.js +86 -0
  25. package/dist/app/features/agents/index.d.ts +2 -0
  26. package/dist/app/features/agents/index.js +2 -0
  27. package/dist/app/features/defs.d.ts +150 -1
  28. package/dist/app/features/defs.js +11 -1
  29. package/dist/app/features/sandbox/sandbox.def.d.ts +22 -0
  30. package/dist/app/features/sandbox/sandbox.def.js +41 -14
  31. package/dist/app/features/sandbox/sandbox.run.d.ts +20 -0
  32. package/dist/app/features/sandbox/sandbox.run.js +41 -1
  33. package/dist/app/features/settings/categories.def.js +5 -2
  34. package/dist/app/features/settings/settings-rows.js +5 -1
  35. package/dist/app/features/settings/settings.run.d.ts +8 -0
  36. package/dist/app/features/settings/settings.run.js +9 -0
  37. package/dist/app/features/updates/updates.run.js +12 -2
  38. package/dist/app/features/vpn/vpn.def.d.ts +33 -0
  39. package/dist/app/features/vpn/vpn.def.js +30 -6
  40. package/dist/app/features/vpn/vpn.run.d.ts +11 -0
  41. package/dist/app/features/vpn/vpn.run.js +4 -1
  42. package/dist/app/kernel/rpc.d.ts +1 -1
  43. package/dist/app/registry.js +9 -2
  44. package/dist/bin/agent-sandbox.js +2 -2
  45. package/dist/bin/{chunk-M6XUQK53.js → chunk-2IMT7ZCT.js} +1 -1
  46. package/dist/bin/{chunk-3G53NNZP.js → chunk-DK4NTBUF.js} +69 -26
  47. package/dist/bin/{chunk-FQWYCF6L.js → chunk-Z2P5OGKL.js} +6427 -5259
  48. package/dist/bin/claude-host.js +2 -2
  49. package/dist/bin/claude-sbx.js +2 -2
  50. package/dist/bin/codex-sbx.js +2 -2
  51. package/dist/bin/gemini-sbx.js +2 -2
  52. package/dist/bin/sandbox.js +2 -2
  53. package/dist/bin/sbx-hide.js +2 -2
  54. package/dist/bin/sbx-show.js +2 -2
  55. package/dist/cli/commands/agent-commands.d.ts +0 -1
  56. package/dist/cli/commands/agent-commands.js +2 -32
  57. package/dist/cli/commands/index.d.ts +1 -1
  58. package/dist/cli/commands/index.js +1 -1
  59. package/dist/cli/commands/launch/launch.js +6 -3
  60. package/dist/cli/commands/launch/leftover-offer.d.ts +3 -0
  61. package/dist/cli/commands/launch/leftover-offer.js +26 -0
  62. package/dist/cli/commands/registry.js +3 -2
  63. package/dist/cli/commands/repair/checks/sandbox.js +29 -2
  64. package/dist/cli/manifest.d.ts +1 -1
  65. package/dist/cli/manifest.js +1 -1
  66. package/dist/cli/menu/menu.d.ts +1 -1
  67. package/dist/cli/menu/menu.js +3 -3
  68. package/dist/cli/menu/pages/agents-page.d.ts +7 -2
  69. package/dist/cli/menu/pages/agents-page.js +67 -11
  70. package/dist/cli/menu/pages/maintenance-page.js +1 -2
  71. package/dist/cli/menu/pages/settings-page.js +1 -0
  72. package/dist/cli/menu/views.js +2 -1
  73. package/dist/cli/services/agent-commands-service.d.ts +2 -0
  74. package/dist/cli/services/agent-commands-service.js +13 -7
  75. package/dist/cli/services/vpn-service.d.ts +5 -1
  76. package/dist/cli/services/vpn-service.js +6 -1
  77. package/dist/cli/tui/dialogs/text-screen.js +2 -1
  78. package/dist/cli/tui/terminal.js +3 -1
  79. package/dist/core/config/defaults.js +9 -0
  80. package/dist/core/detect/toolchains.js +9 -0
  81. package/dist/core/disk.d.ts +1 -5
  82. package/dist/core/disk.js +1 -16
  83. package/dist/core/schema/codex.d.ts +29 -0
  84. package/dist/core/schema/codex.js +24 -0
  85. package/dist/core/schema/config.d.ts +46 -14
  86. package/dist/core/schema/config.js +2 -0
  87. package/dist/core/schema/index.d.ts +1 -0
  88. package/dist/core/schema/index.js +1 -0
  89. package/dist/core/schema/toolchains.d.ts +4 -0
  90. package/dist/core/schema/toolchains.js +3 -0
  91. package/dist/core/types.d.ts +2 -0
  92. package/dist/guest/accounts/apply.d.ts +6 -6
  93. package/dist/guest/agents/agent-files.d.ts +26 -6
  94. package/dist/guest/agents/agent-files.js +84 -14
  95. package/dist/guest/agents/local-state.d.ts +5 -2
  96. package/dist/guest/agents/local-state.js +24 -10
  97. package/dist/guest/boot/boot.js +10 -5
  98. package/dist/guest/daemon/heartbeat.d.ts +7 -2
  99. package/dist/guest/daemon/heartbeat.js +34 -15
  100. package/dist/guest/daemon/idle-shutdown.js +2 -0
  101. package/dist/guest/daemon/sessions.js +2 -1
  102. package/dist/guest/entries/daemon.js +1 -1
  103. package/dist/guest/toolchains/installers.d.ts +6 -10
  104. package/dist/guest/toolchains/installers.js +4 -12
  105. package/dist/guest/toolchains/native/autosdk.d.ts +3 -0
  106. package/dist/guest/toolchains/native/autosdk.js +50 -0
  107. package/dist/guest/toolchains/native/unreal.js +3 -0
  108. package/dist/guest/toolchains/native/vcruntime.d.ts +0 -3
  109. package/dist/guest/toolchains/native/vcruntime.js +2 -5
  110. package/dist/guest/toolchains/native/visual-studio.d.ts +2 -2
  111. package/dist/guest/toolchains/native/visual-studio.js +100 -16
  112. package/dist/guest/toolchains/native/vs-request.d.ts +21 -0
  113. package/dist/guest/toolchains/native/vs-request.js +60 -1
  114. package/dist/guest/toolchains/not-yet.d.ts +1 -1
  115. package/dist/guest/toolchains/not-yet.js +3 -3
  116. package/dist/guest/toolchains/persisted.js +2 -7
  117. package/dist/guest/toolchains/plan.js +9 -4
  118. package/dist/guest/toolchains/signature.d.ts +5 -0
  119. package/dist/guest/toolchains/signature.js +22 -0
  120. package/dist/guest/vpn/tunnel.d.ts +3 -0
  121. package/dist/guest/vpn/tunnel.js +10 -12
  122. package/dist/guest/vpn/vpn.js +6 -1
  123. package/dist/host/accounts/leases.d.ts +3 -3
  124. package/dist/host/accounts/source.js +1 -1
  125. package/dist/host/agent-commands/commands.js +1 -1
  126. package/dist/host/agent-commands/forwarders.d.ts +14 -2
  127. package/dist/host/agent-commands/forwarders.js +31 -13
  128. package/dist/host/agent-commands/reconcile.d.ts +2 -1
  129. package/dist/host/agent-commands/reconcile.js +7 -8
  130. package/dist/host/agent-commands/user-path.d.ts +5 -0
  131. package/dist/host/agent-commands/user-path.js +13 -0
  132. package/dist/host/health/index.d.ts +1 -0
  133. package/dist/host/health/index.js +1 -0
  134. package/dist/host/health/leftovers.d.ts +3 -0
  135. package/dist/host/health/leftovers.js +40 -0
  136. package/dist/host/health/stale.d.ts +48 -0
  137. package/dist/host/health/stale.js +40 -0
  138. package/dist/host/launch/session-file.js +1 -0
  139. package/dist/host/life/crash-report.d.ts +1 -1
  140. package/dist/host/life/crash-report.js +5 -3
  141. package/dist/host/life/last-end.d.ts +7 -0
  142. package/dist/host/life/last-end.js +21 -0
  143. package/dist/host/life/life-record.d.ts +3 -0
  144. package/dist/host/life/life-record.js +2 -2
  145. package/dist/host/life/stop-events.d.ts +3 -1
  146. package/dist/host/life/stop-events.js +59 -0
  147. package/dist/host/life/stop-warning.js +3 -0
  148. package/dist/host/profile/accounts.js +2 -4
  149. package/dist/host/profile/claude-login.d.ts +3 -2
  150. package/dist/host/profile/claude-login.js +3 -13
  151. package/dist/host/sandbox/cleanup.d.ts +37 -0
  152. package/dist/host/sandbox/cleanup.js +82 -0
  153. package/dist/host/sandbox/index.d.ts +1 -0
  154. package/dist/host/sandbox/index.js +1 -0
  155. package/dist/host/sandbox/platform.js +1 -1
  156. package/dist/host/vpn/index.d.ts +2 -1
  157. package/dist/host/vpn/index.js +2 -1
  158. package/dist/host/vpn/last.d.ts +28 -0
  159. package/dist/host/vpn/last.js +43 -0
  160. package/dist/host/vpn/login.d.ts +3 -1
  161. package/dist/host/vpn/login.js +17 -3
  162. package/dist/runtime/agent.js +31 -1
  163. package/dist/runtime/daemon.js +1419 -1197
  164. package/dist/runtime/db.js +33 -1
  165. package/dist/runtime/main.js +1311 -1037
  166. package/dist/runtime/netproxy.js +25 -1
  167. package/dist/runtime/tool.js +241 -209
  168. package/dist/runtime/view.js +25 -1
  169. package/dist/selftest/agent-commands.test.js +45 -7
  170. package/dist/selftest/agent-paths.test.js +1 -12
  171. package/dist/selftest/agent-settings.test.d.ts +2 -0
  172. package/dist/selftest/agent-settings.test.js +94 -0
  173. package/dist/selftest/app.test.js +2 -1
  174. package/dist/selftest/cli.test.js +4 -0
  175. package/dist/selftest/heavy-load.test.js +119 -2
  176. package/dist/selftest/index.js +2 -0
  177. package/dist/selftest/menu.test.js +5 -3
  178. package/dist/selftest/pc-installs.test.js +5 -0
  179. package/dist/selftest/persisted-installs.test.js +69 -0
  180. package/dist/selftest/service-accounts.test.js +1 -1
  181. package/dist/selftest/support/engine-golden.js +1 -1
  182. package/dist/selftest/support/fixtures.d.ts +9 -0
  183. package/dist/selftest/vpn.test.js +51 -3
  184. package/dist/shared/accounts/anthropic.d.ts +5 -0
  185. package/dist/shared/accounts/anthropic.js +15 -0
  186. package/dist/shared/accounts/index.d.ts +1 -0
  187. package/dist/shared/accounts/index.js +1 -0
  188. package/dist/shared/channel/schema.d.ts +20 -0
  189. package/dist/shared/channel/schema.js +4 -1
  190. package/dist/shared/io/disk.d.ts +9 -0
  191. package/dist/shared/io/disk.js +19 -0
  192. package/dist/shared/io/index.d.ts +1 -0
  193. package/dist/shared/io/index.js +1 -0
  194. package/dist/shared/readiness.d.ts +9 -0
  195. package/dist/shared/readiness.js +11 -0
  196. package/dist/shared/signer.d.ts +24 -0
  197. package/dist/shared/signer.js +15 -0
  198. package/dist/shared/system.d.ts +1 -0
  199. package/dist/shared/system.js +1 -0
  200. package/dist/shared/vpn/failure.d.ts +10 -0
  201. package/dist/shared/vpn/failure.js +28 -0
  202. package/dist/sync/watcher.js +11 -4
  203. package/dist/web/index.html +1 -1
  204. package/dist/web/{main-6XYO5FZC.js → main-OUUQYNK4.js} +75 -74
  205. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  206. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  207. package/native/csharp/HostTools/Program.cs +19 -2
  208. package/native/csharp/HostTools/UserPath.cs +17 -0
  209. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  210. package/native/scripts/edit-user-path.ps1 +6 -2
  211. package/native/scripts/stop-events.ps1 +7 -3
  212. package/package.json +1 -1
  213. package/sandbox/scripts/get-signature.ps1 +13 -0
  214. package/schemas/config.schema.json +64 -0
  215. package/schemas/sandbox-config.schema.json +8 -0
  216. package/sandbox/scripts/get-signer.ps1 +0 -8
@@ -306,7 +306,7 @@ export const serviceAccountsSuite = {
306
306
  });
307
307
  t.check(empty.message.includes('No saved'), 'no launcher sign-in on this PC: it says so');
308
308
  await t.throws(() => loginServiceFlow(fakeIO([]).io, 'epic', { store: flowStore, share: true }), 'nothing to type', 'Epic has nothing to type');
309
- await t.throws(() => loginServiceFlow(fakeIO([CFX, '']).io, 'cfx', { store: flowStore, share: true }), 'not available yet', 'a kind whose host side is not written yet says so');
309
+ await t.throws(() => loginServiceFlow(fakeIO([CFX, '']).io, 'cfx', { store: flowStore, share: true }), 'comes in a later version', 'a kind whose host side is not written yet says so');
310
310
  t.check(Boolean(HOST_SOURCES.epic.fromPc) && !HOST_SOURCES.epic.fromAnswers, 'Epic comes from this PC (or a sandbox), never typed');
311
311
  const gone = await logoutServiceFlow(fakeIO([true]).io, 'epic', t.dir('svc-flow-root'), flowStore, new AccountLeases(t.dir('svc-flow-root')));
312
312
  t.check(gone.deleted && flowStore.state('epic') === 'none', 'forgetting deletes it after asking');
@@ -67,7 +67,7 @@ export const LAUNCH_FILES_GOLDEN = [
67
67
  'sandbox/scripts/enable-vt-input.ps1',
68
68
  'sandbox/scripts/expand-archive.ps1',
69
69
  'sandbox/scripts/get-gpu-names.ps1',
70
- 'sandbox/scripts/get-signer.ps1',
70
+ 'sandbox/scripts/get-signature.ps1',
71
71
  'sandbox/scripts/install-shell-path.ps1',
72
72
  'sandbox/scripts/link-profile-dirs.ps1',
73
73
  'sandbox/scripts/new-shortcuts.ps1',
@@ -194,6 +194,7 @@ export declare function createFixtures(t: Harness): {
194
194
  acceptLicense: boolean;
195
195
  components: string[];
196
196
  keepInstallFiles: boolean;
197
+ visualStudio: "full" | "autosdk";
197
198
  persistEngines: boolean;
198
199
  derivedDataCache: "persisted" | "local";
199
200
  };
@@ -429,6 +430,14 @@ export declare function createFixtures(t: Harness): {
429
430
  resumeArguments?: string[] | undefined;
430
431
  resumeOneArguments?: string[] | undefined;
431
432
  }>;
433
+ codex: {
434
+ sandboxMode: "danger-full-access" | "workspace-write" | "read-only";
435
+ approvalPolicy: "never" | "on-request" | "on-failure" | "untrusted";
436
+ model: string;
437
+ reasoningEffort: "default" | "minimal" | "low" | "medium" | "high" | "xhigh";
438
+ windowsSandbox: "off" | "unelevated" | "elevated";
439
+ mcpLoginsInFile: boolean;
440
+ };
432
441
  hooks: {
433
442
  failFast: boolean;
434
443
  };
@@ -14,7 +14,8 @@ import { ConfigSchema, LocalOverridesSchema } from '../core/schema/index.js';
14
14
  import { SecretStore } from '../host/secrets/index.js';
15
15
  import { clearSecrets } from '../host/secrets/index.js';
16
16
  import { generateSandbox } from '../host/launch/index.js';
17
- import { findCountry, nordProfile, prepareVpn, wireguardFileProblem, recommendedServer, VpnLogin, vpnLine, vpnNotes, } from '../host/vpn/index.js';
17
+ import { classifyFailure, FAILURE_TEXT } from '../shared/vpn/failure.js';
18
+ import { findCountry, loginProblem, loginWarning, nordProfile, prepareVpn, wireguardFileProblem, recommendedServer, VpnLogin, vpnLine, vpnNotes, } from '../host/vpn/index.js';
18
19
  import { RunDir } from '../shared/channel/index.js';
19
20
  import { OPENVPN_MSI, WIREGUARD_MSI } from '../shared/vpn/download.js';
20
21
  import { cleanServers, ipv4Except, killSwitchCommands, removeKillSwitchCommands, tunnelAddressFromLog, tunnelCommands, } from '../shared/vpn/kill-switch.js';
@@ -221,6 +222,52 @@ export const vpnSuite = {
221
222
  t.check(login.state('nordvpn') === 'unreadable', 'a login this Windows user cannot open is reported as unreadable');
222
223
  login.remove('nordvpn');
223
224
  t.check(login.state('nordvpn') === 'none', 'logout deletes it');
225
+ t.section('NordVPN login checks and why a tunnel failed');
226
+ const sampleStatus = () => ({
227
+ enabled: true,
228
+ provider: 'nordvpn',
229
+ protocol: 'udp',
230
+ country: '',
231
+ killSwitch: true,
232
+ login: 'saved',
233
+ loginSavedAt: '2026-11-02T19:00:00.000Z',
234
+ running: null,
235
+ lastFailure: null,
236
+ });
237
+ t.check((loginProblem({ username: 'me@example.com', password: 'x' }, 'nordvpn') ?? '').includes('service credentials') &&
238
+ loginProblem({ username: 'me@example.com', password: 'x' }, 'openvpn-file') === null &&
239
+ (loginProblem({ username: 'abc', password: 'p[200~w' }, 'nordvpn') ?? '').includes('paste'), 'an e-mail is refused for NordVPN only (it needs the service credentials); hidden paste markers are refused');
240
+ login.save('nordvpn', { username: ' ' + USERNAME + ' ', password: ' ' + PASSWORD + ' ' });
241
+ t.equal(login.read('nordvpn'), { username: USERNAME, password: PASSWORD }, 'spaces around the user name and the password are not saved (a pasted login often has them)');
242
+ t.check(loginWarning({ username: 'x', password: 'y' }, 'openvpn-file') === null &&
243
+ loginWarning({ username: 'a'.repeat(24), password: 'B'.repeat(24) }, 'nordvpn') === null &&
244
+ loginWarning({ username: 'a b', password: 'c' }, 'nordvpn') !== null, 'a NordVPN login that does not look like service credentials gets a hint, not a refusal');
245
+ login.remove('nordvpn');
246
+ const failed = (log) => classifyFailure(log)?.code;
247
+ t.equal([
248
+ failed('AUTH: Received control message: AUTH_FAILED'),
249
+ failed('TLS Error: TLS key negotiation failed to occur within 60 seconds'),
250
+ failed('Could not create Wintun interface'),
251
+ failed('Exiting due to fatal error'),
252
+ failed('all fine'),
253
+ ], ['auth', 'unreachable', 'driver', 'other', undefined], 'the OpenVPN log is turned into a stable code the host acts on');
254
+ const refusedPage = vpnPage({
255
+ ...sampleStatus(),
256
+ lastFailure: { code: 'auth', text: FAILURE_TEXT.auth, at: '2026-11-02T19:15:21.000Z' },
257
+ });
258
+ const refusedText = pageLines(refusedPage).join('\n');
259
+ const refusedOps = pageActions(refusedPage).flatMap((a) => (a.kind === 'operation' ? [[a.op, a.label]] : []));
260
+ t.check(refusedText.includes('Last attempt') &&
261
+ refusedText.includes('login refused') &&
262
+ refusedText.includes('service credentials') &&
263
+ refusedOps.some(([op, label]) => op === 'vpn.login' && label === 'Save the login again…') &&
264
+ !/sandbox (vpn|config|agent)/.test(refusedText), 'after a refused login the VPN page says so and offers to save the login again, without naming a command');
265
+ const unreachablePage = vpnPage({
266
+ ...sampleStatus(),
267
+ lastFailure: { code: 'unreachable', text: FAILURE_TEXT.unreachable, at: '2026-11-02T19:15:21.000Z' },
268
+ });
269
+ t.check(pageActions(unreachablePage).some((a) => a.kind === 'operation' && a.input.key === 'vpn.protocol') &&
270
+ !pageActions(vpnPage({ ...sampleStatus(), protocol: 'tcp', lastFailure: { code: 'unreachable', text: 'x', at: 'y' } })).some((a) => a.kind === 'operation' && a.input.key === 'vpn.protocol'), 'a server that could not be reached offers TCP instead of UDP (once)');
224
271
  t.section('settings: what a project may and may not do');
225
272
  t.check(DEFAULTS.vpn.enabled === false && DEFAULTS.vpn.provider === 'nordvpn' && DEFAULTS.vpn.killSwitch === true, 'the VPN is off by default, NordVPN, with the kill switch on');
226
273
  t.check(ConfigSchema.safeParse(DEFAULTS).success, 'the defaults are valid');
@@ -458,11 +505,12 @@ export const vpnSuite = {
458
505
  login: 'none',
459
506
  loginSavedAt: null,
460
507
  running: null,
508
+ lastFailure: null,
461
509
  };
462
510
  const lines = pageLines(vpnPage(sample)).join('\n');
463
- t.check(lines.includes('Next: sandbox vpn login') &&
511
+ t.check(!/sandbox (vpn|config|agent)/.test(lines) &&
464
512
  VPN_WARNINGS.every((w) => lines.includes(w)) &&
465
- !lines.includes('Running now'), 'vpn status page: the facts, the next step on the command line only, the warnings', lines);
513
+ !lines.includes('Running now'), 'vpn status page: the facts and the warnings, no command to type', lines);
466
514
  t.equal(pageActions(vpnPage({ ...sample, provider: 'wireguard-file' })).map((a) => a.kind === 'operation' ? a.op : a.kind), ['settings.set'], 'vpn: wireguard needs no login and offers only turning the VPN on or off');
467
515
  t.equal(pageActions(vpnPage({ ...sample, login: 'saved', loginSavedAt: '2026-10-14T09:30:00.000Z' })).map((a) => a.kind === 'operation' ? a.op : a.kind), ['settings.set', 'vpn.login', 'vpn.logout'], 'a saved login offers turning the VPN on or off, saving the login again and deleting it');
468
516
  t.equal(vpnLoginRowOf(sample).hint, 'not saved', 'the Settings row says not saved when no login is saved');
@@ -0,0 +1,5 @@
1
+ export declare const ANTHROPIC_KEY_PATTERN: RegExp;
2
+ /** The only part of a key ever shown: sk-ant-...abcd. */
3
+ export declare const ANTHROPIC_KEY_HINT: RegExp;
4
+ /** What is wrong with a typed or found API key, or null when it has the right shape. Never quotes the key. */
5
+ export declare function anthropicKeyProblem(key: string): string | null;
@@ -0,0 +1,15 @@
1
+ // The one shape of an Anthropic API key, for the host and the web (no node imports). A key from the Claude Console
2
+ // starts with sk-ant-api, a key from a user workspace with sk-ant-usr.
3
+ export const ANTHROPIC_KEY_PATTERN = /^sk-ant-(api|usr)\d{2}-[A-Za-z0-9_-]{20,400}$/;
4
+ /** The only part of a key ever shown: sk-ant-...abcd. */
5
+ export const ANTHROPIC_KEY_HINT = /^sk-ant-\.\.\.[A-Za-z0-9_-]{4}$/;
6
+ /** What is wrong with a typed or found API key, or null when it has the right shape. Never quotes the key. */
7
+ export function anthropicKeyProblem(key) {
8
+ if (ANTHROPIC_KEY_PATTERN.test(key))
9
+ return null;
10
+ if (key.startsWith('sk-ant-oat'))
11
+ return 'That is a Claude subscription token (from claude setup-token), not an API key. Save the subscription login instead.';
12
+ if (key.startsWith('sk-ant-admin'))
13
+ return 'That is an Admin key: it cannot run Claude. Make an API key instead.';
14
+ return 'An Anthropic API key starts with sk-ant-api or sk-ant-usr and has only letters, digits, - and _ (copy it from the Claude Console).';
15
+ }
@@ -3,3 +3,4 @@ export * from './payloads.js';
3
3
  export { fingerprint } from './fingerprint.js';
4
4
  export { epicSettingsFile, MAX_EPIC_INI_BYTES, readEpicLogin, withEpicLogin, type EpicPayload } from './kinds/epic.js';
5
5
  export * from './kinds/steam.js';
6
+ export * from './anthropic.js';
@@ -5,3 +5,4 @@ export * from './payloads.js';
5
5
  export { fingerprint } from './fingerprint.js';
6
6
  export { epicSettingsFile, MAX_EPIC_INI_BYTES, readEpicLogin, withEpicLogin } from './kinds/epic.js';
7
7
  export * from './kinds/steam.js';
8
+ export * from './anthropic.js';
@@ -93,6 +93,10 @@ export declare const ProjectSpecSchema: z.ZodObject<{
93
93
  acceptLicense: z.ZodBoolean;
94
94
  components: z.ZodArray<z.ZodString>;
95
95
  keepInstallFiles: z.ZodBoolean;
96
+ visualStudio: z.ZodEnum<{
97
+ full: "full";
98
+ autosdk: "autosdk";
99
+ }>;
96
100
  persistEngines: z.ZodBoolean;
97
101
  derivedDataCache: z.ZodEnum<{
98
102
  persisted: "persisted";
@@ -326,6 +330,10 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
326
330
  acceptLicense: z.ZodBoolean;
327
331
  components: z.ZodArray<z.ZodString>;
328
332
  keepInstallFiles: z.ZodBoolean;
333
+ visualStudio: z.ZodEnum<{
334
+ full: "full";
335
+ autosdk: "autosdk";
336
+ }>;
329
337
  persistEngines: z.ZodBoolean;
330
338
  derivedDataCache: z.ZodEnum<{
331
339
  persisted: "persisted";
@@ -579,6 +587,10 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
579
587
  acceptLicense: z.ZodBoolean;
580
588
  components: z.ZodArray<z.ZodString>;
581
589
  keepInstallFiles: z.ZodBoolean;
590
+ visualStudio: z.ZodEnum<{
591
+ full: "full";
592
+ autosdk: "autosdk";
593
+ }>;
582
594
  persistEngines: z.ZodBoolean;
583
595
  derivedDataCache: z.ZodEnum<{
584
596
  persisted: "persisted";
@@ -828,6 +840,7 @@ export declare const HeartbeatSchema: z.ZodObject<{
828
840
  phase: z.ZodEnum<{
829
841
  ready: "ready";
830
842
  working: "working";
843
+ ending: "ending";
831
844
  }>;
832
845
  }, z.core.$strip>;
833
846
  export type Heartbeat = z.output<typeof HeartbeatSchema>;
@@ -897,6 +910,13 @@ export declare const VpnStatusSchema: z.ZodObject<{
897
910
  up: "up";
898
911
  }>;
899
912
  message: z.ZodString;
913
+ code: z.ZodOptional<z.ZodEnum<{
914
+ auth: "auth";
915
+ unreachable: "unreachable";
916
+ driver: "driver";
917
+ timeout: "timeout";
918
+ other: "other";
919
+ }>>;
900
920
  killSwitch: z.ZodBoolean;
901
921
  killSwitchHeld: z.ZodOptional<z.ZodBoolean>;
902
922
  server: z.ZodOptional<z.ZodString>;
@@ -150,7 +150,8 @@ export const HeartbeatSchema = z.object({
150
150
  topProcesses: z.array(z.object({ name: Text(64), privateMb: z.number().int().min(0).max(1e7) })).max(5),
151
151
  /** The disk queue this PC last reported while asking the sandbox to slow down. */
152
152
  diskQueueReported: z.number().min(0).max(100000).optional(),
153
- phase: z.enum(['ready', 'working']),
153
+ /** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
154
+ phase: z.enum(['ready', 'working', 'ending']),
154
155
  });
155
156
  /** C:\AgentData\run\responses\<id>.json: the guest's reply. */
156
157
  export const GuestResponseSchema = z.object({
@@ -197,6 +198,8 @@ export const ProjectStatusSchema = z.object({
197
198
  export const VpnStatusSchema = z.object({
198
199
  state: z.enum(['off', 'starting', 'up', 'failed']),
199
200
  message: Text(2000),
201
+ /** Why it failed, when it did: `auth` (login refused), `unreachable`, `driver`, `timeout` or `other`. */
202
+ code: z.enum(['auth', 'unreachable', 'driver', 'timeout', 'other']).optional(),
200
203
  killSwitch: z.boolean(),
201
204
  /** A direct connection was tried after the kill switch was applied: false = it still got out. */
202
205
  killSwitchHeld: z.boolean().optional(),
@@ -0,0 +1,9 @@
1
+ export type StatFs = (dir: string) => {
2
+ bavail: number;
3
+ bsize: number;
4
+ };
5
+ /**
6
+ * Free bytes for this user on the drive that holds `dir` (which may not exist yet: the nearest existing parent is
7
+ * asked); null when it cannot be told. `statfs` is injectable for tests.
8
+ */
9
+ export declare function freeBytesAt(dir: string, statfs?: StatFs): number | null;
@@ -0,0 +1,19 @@
1
+ // Free room on a disk: one implementation for the PC (core/disk.ts) and the guest (toolchains/persisted.ts).
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ /**
5
+ * Free bytes for this user on the drive that holds `dir` (which may not exist yet: the nearest existing parent is
6
+ * asked); null when it cannot be told. `statfs` is injectable for tests.
7
+ */
8
+ export function freeBytesAt(dir, statfs = fs.statfsSync) {
9
+ let at = path.resolve(dir);
10
+ while (!fs.existsSync(at) && path.dirname(at) !== at)
11
+ at = path.dirname(at);
12
+ try {
13
+ const s = statfs(at);
14
+ return s.bavail * s.bsize;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
@@ -5,3 +5,4 @@ export * from './robocopy-result.js';
5
5
  export * from './copy-tree.js';
6
6
  export * from './bounded.js';
7
7
  export * from './text.js';
8
+ export * from './disk.js';
@@ -5,3 +5,4 @@ export * from './robocopy-result.js';
5
5
  export * from './copy-tree.js';
6
6
  export * from './bounded.js';
7
7
  export * from './text.js';
8
+ export * from './disk.js';
@@ -0,0 +1,9 @@
1
+ export declare const NOT_READY: {
2
+ /** Toolchains whose step is a stub: their settings rows are hidden and "auto" never turns them on. */
3
+ readonly toolchains: readonly ["unity", "godot", "android", "flutter", "dart", "reactNative", "fivem"];
4
+ /** Service sign-ins whose host or guest side is a stub: the menu and the web do not list them. */
5
+ readonly kinds: readonly ["unity", "keystore", "play", "expo", "appstore", "cfx"];
6
+ readonly unreal: readonly ["engines"];
7
+ };
8
+ export declare const isToolchainReady: (name: string) => boolean;
9
+ export declare const isKindReady: (kind: string) => boolean;
@@ -0,0 +1,11 @@
1
+ // What is not written yet, so no menu, web page or "auto" decision offers it. Each package that lands deletes its own
2
+ // entries here (the end of round 2 leaves it empty). Pure.
3
+ export const NOT_READY = {
4
+ /** Toolchains whose step is a stub: their settings rows are hidden and "auto" never turns them on. */
5
+ toolchains: ['unity', 'godot', 'android', 'flutter', 'dart', 'reactNative', 'fivem'],
6
+ /** Service sign-ins whose host or guest side is a stub: the menu and the web do not list them. */
7
+ kinds: ['unity', 'keystore', 'play', 'expo', 'appstore', 'cfx'],
8
+ unreal: ['engines'],
9
+ };
10
+ export const isToolchainReady = (name) => !NOT_READY.toolchains.includes(name);
11
+ export const isKindReady = (kind) => !NOT_READY.kinds.includes(kind);
@@ -0,0 +1,24 @@
1
+ /** Patterns (regex sources, case-insensitive) the signature's subject and issuer must match; every one of them. */
2
+ export interface AppSigner {
3
+ readonly subject: readonly string[];
4
+ readonly issuer?: readonly string[];
5
+ }
6
+ /** A valid Authenticode signature's certificate names (an invalid or missing one is reported as null). */
7
+ export interface Signature {
8
+ readonly subject: string;
9
+ readonly issuer: string;
10
+ }
11
+ /** Whether a signature is the expected signer's: every subject and issuer pattern matches. */
12
+ export declare function signerMatches(signature: Signature | null, signer: AppSigner): boolean;
13
+ /** Who signs the installers the toolchains run (the patterns the signature's subject must match). */
14
+ export declare const SIGNERS: {
15
+ readonly microsoft: {
16
+ readonly subject: readonly ["^CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US$"];
17
+ };
18
+ readonly valve: {
19
+ readonly subject: readonly ["^CN=\"?Valve(,| Corp)"];
20
+ };
21
+ readonly unity: {
22
+ readonly subject: readonly ["^CN=\"?Unity Technologies"];
23
+ };
24
+ };
@@ -0,0 +1,15 @@
1
+ // Who must sign an app's download (AppEntry.signer), for vendors that publish no checksum: the Authenticode subject
2
+ // and issuer as patterns, checked against what sandbox\scripts\get-signature.ps1 reports for a valid signature. Pure.
3
+ /** Whether a signature is the expected signer's: every subject and issuer pattern matches. */
4
+ export function signerMatches(signature, signer) {
5
+ if (!signature || !signature.subject)
6
+ return false;
7
+ const all = (text, patterns) => patterns.every((p) => new RegExp(p, 'i').test(text));
8
+ return all(signature.subject, signer.subject) && all(signature.issuer, signer.issuer ?? []);
9
+ }
10
+ /** Who signs the installers the toolchains run (the patterns the signature's subject must match). */
11
+ export const SIGNERS = {
12
+ microsoft: { subject: ['^CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US$'] },
13
+ valve: { subject: ['^CN="?Valve(,| Corp)'] },
14
+ unity: { subject: ['^CN="?Unity Technologies'] },
15
+ };
@@ -3,6 +3,7 @@ export declare const SYSTEM: {
3
3
  readonly cmd: () => string;
4
4
  readonly powershell: () => string;
5
5
  readonly tasklist: () => string;
6
+ readonly taskkill: () => string;
6
7
  readonly whoami: () => string;
7
8
  readonly icacls: () => string;
8
9
  readonly tar: () => string;
@@ -9,6 +9,7 @@ export const SYSTEM = {
9
9
  cmd: () => system32('cmd.exe'),
10
10
  powershell: () => system32('WindowsPowerShell\\v1.0\\powershell.exe'),
11
11
  tasklist: () => system32('tasklist.exe'),
12
+ taskkill: () => system32('taskkill.exe'),
12
13
  whoami: () => system32('whoami.exe'),
13
14
  icacls: () => system32('icacls.exe'),
14
15
  tar: () => system32('tar.exe'),
@@ -0,0 +1,10 @@
1
+ export declare const VPN_FAILURE_CODES: readonly ["auth", "unreachable", "driver", "timeout", "other"];
2
+ export type VpnFailureCode = (typeof VPN_FAILURE_CODES)[number];
3
+ export interface VpnFailure {
4
+ readonly code: VpnFailureCode;
5
+ readonly reason: string;
6
+ }
7
+ /** What the log says went wrong, or null when it says nothing (yet). */
8
+ export declare function classifyFailure(text: string): VpnFailure | null;
9
+ /** The plain words for a code, for the page ("login refused by NordVPN"). */
10
+ export declare const FAILURE_TEXT: Readonly<Record<VpnFailureCode, string>>;
@@ -0,0 +1,28 @@
1
+ // Why an OpenVPN run failed, from its log: a stable code (what the host acts on) and a plain sentence. Pure.
2
+ export const VPN_FAILURE_CODES = ['auth', 'unreachable', 'driver', 'timeout', 'other'];
3
+ /** What the log says went wrong, or null when it says nothing (yet). */
4
+ export function classifyFailure(text) {
5
+ if (/AUTH_FAILED/i.test(text))
6
+ return {
7
+ code: 'auth',
8
+ reason: 'the VPN refused the login (for NordVPN use the service credentials, not your account password)',
9
+ };
10
+ if (/TLS Error: TLS key negotiation failed|TLS handshake failed/i.test(text))
11
+ return {
12
+ code: 'unreachable',
13
+ reason: 'it could not reach the VPN server (the network blocks it, or the server is down)',
14
+ };
15
+ if (/All wintun adapters on this system are in use|Cannot open TUN\/TAP|Could not create Wintun|ovpn-dco.*(error|failed)/i.test(text))
16
+ return { code: 'driver', reason: 'the network driver for the tunnel could not be used' };
17
+ if (/Exiting due to fatal error/i.test(text))
18
+ return { code: 'other', reason: 'OpenVPN stopped with an error' };
19
+ return null;
20
+ }
21
+ /** The plain words for a code, for the page ("login refused by NordVPN"). */
22
+ export const FAILURE_TEXT = {
23
+ auth: 'login refused by the VPN',
24
+ unreachable: 'the VPN server could not be reached',
25
+ driver: 'the tunnel driver could not be used',
26
+ timeout: 'the tunnel did not come up in time',
27
+ other: 'the VPN failed',
28
+ };
@@ -13,7 +13,8 @@ import { readForwards, sandboxAddress, startPortForwards } from '../host/network
13
13
  import { AuditCollector, AuditLog, readAuditSettings } from '../host/audit/index.js';
14
14
  import { readMarkedProjects, saveDataSize, watchAndMark } from '../host/guard/index.js';
15
15
  import { sizeWithin } from '../core/disk.js';
16
- import { autoRestartAllowed, causeSince, appendHostMemory, cpuPercentBetween, KeepAwake, LifeRecord, HostMemoryStream, readHostMemoryAsync, readLoadSettings, REQUEST_SECONDS, WATCHDOG_START, watchdogStep, saveCrashReport, SessionLedger, } from '../host/life/index.js';
16
+ import { autoRestartAllowed, causeSince, readHeartbeat, vanishedCause, appendHostMemory, cpuPercentBetween, KeepAwake, LifeRecord, HostMemoryStream, readHostMemoryAsync, readLoadSettings, REQUEST_SECONDS, WATCHDOG_START, watchdogStep, saveCrashReport, SessionLedger, } from '../host/life/index.js';
17
+ import { recordLastVpn } from '../host/vpn/index.js';
17
18
  import { SandboxChannel } from '../host/channel/index.js';
18
19
  import { ProjectRegistry } from '../host/projects/registry.js';
19
20
  import { ContainedStorage, setNameOf, storageOf } from '../host/storage/index.js';
@@ -67,7 +68,7 @@ export function watcherRunning(dataPath) {
67
68
  }
68
69
  }
69
70
  /** The ways the sandbox itself ends badly (the PC going down takes the watcher with it, so it never gets here). */
70
- const RESTARTABLE = ['sandboxCrash', 'memory'];
71
+ const RESTARTABLE = ['sandboxCrash', 'memory', 'gpu', 'hyperv', 'vanished'];
71
72
  /**
72
73
  * The background watcher of one data folder: while the sandbox runs it syncs history back every minute, guards the
73
74
  * projects' git folders, keeps filtered workspaces in step, forwards ports, marks downloaded files, records the audit
@@ -276,6 +277,7 @@ export class Watcher {
276
277
  catch (e) {
277
278
  this.note('repo guard failed: ' + errMsg(e));
278
279
  }
280
+ recordLastVpn(this.storageRoot, this.dataPath);
279
281
  try {
280
282
  const s = syncBack(this.dataPath, final);
281
283
  if (s.history || final)
@@ -318,8 +320,13 @@ export class Watcher {
318
320
  recordEnd() {
319
321
  try {
320
322
  const last = this.life.read();
321
- const cause = last ? causeSince(last.lastSeen, this.storageRoot) : null;
322
- const report = last && cause ? saveCrashReport(this.storageRoot, this.dataPath, last.lastSeen) : null;
323
+ // Windows' own log first; when it says nothing, a sandbox that was busy and left no word of leaving "vanished".
324
+ const logged = last ? causeSince(last.lastSeen, this.storageRoot) : null;
325
+ const cause = logged ??
326
+ (last
327
+ ? vanishedCause(readHeartbeat(this.dataPath), last.started, this.ledger.read()?.closedOnPurpose === true, Date.now())
328
+ : null);
329
+ const report = last && cause ? saveCrashReport(this.storageRoot, this.dataPath, last.lastSeen, cause) : null;
323
330
  this.life.end(cause, new Date(), report);
324
331
  this.cause = cause;
325
332
  // Ended with nothing wrong (its window closed, or it shut itself down idle): on purpose.
@@ -6,7 +6,7 @@
6
6
  <title>Sandbox</title>
7
7
  <link rel="icon" href="data:," />
8
8
  <link rel="stylesheet" href="/main-XT7T6ODM.css" />
9
- <script type="module" src="/main-6XYO5FZC.js"></script>
9
+ <script type="module" src="/main-OUUQYNK4.js"></script>
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>