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
@@ -5,7 +5,7 @@ import { writeFileAtomic } from '../../shared/io/index.js';
5
5
  import { safeRemove } from '../../shared/io/index.js';
6
6
  import { MARKER, forwardingCmd, forwardingSh } from './forwarders.js';
7
7
  import { commandsDir, cmdSafe, agentCommands } from './commands.js';
8
- import { editUserPath } from './user-path.js';
8
+ import { editUserPath, userPathHas } from './user-path.js';
9
9
  const ownFiles = (dir) => fs.existsSync(dir)
10
10
  ? fs
11
11
  .readdirSync(dir)
@@ -23,7 +23,7 @@ const ownFiles = (dir) => fs.existsSync(dir)
23
23
  * Makes this PC match agentCommands: on = a forwarding file per installed agent and the folder first on your PATH;
24
24
  * off = no files, no PATH entry. Only touches files it wrote and its own PATH entry; does nothing when already right.
25
25
  */
26
- export function reconcileAgentCommands(config, storageRoot, node, entry) {
26
+ export function reconcileAgentCommands(config, storageRoot, target) {
27
27
  const dir = commandsDir(storageRoot);
28
28
  if (!config.agentCommands.enabled) {
29
29
  if (!fs.existsSync(dir))
@@ -35,14 +35,14 @@ export function reconcileAgentCommands(config, storageRoot, node, entry) {
35
35
  safeRemove(dir, storageRoot);
36
36
  return { on: false, commands: [], changed: true };
37
37
  }
38
- if (!cmdSafe(node) || !cmdSafe(entry))
38
+ if (![target.node, target.entry, target.launcher ?? ''].every(cmdSafe))
39
39
  throw new Error('This tool is installed in a folder whose path has % " or ^, which cannot go into a command file safely.');
40
40
  const commands = agentCommands(config, storageRoot);
41
41
  ensureDir(dir);
42
42
  const wanted = new Map();
43
43
  for (const a of commands) {
44
- wanted.set(path.join(dir, a.command + '.cmd'), forwardingCmd(a, node, entry));
45
- wanted.set(path.join(dir, a.command), forwardingSh(a, node, entry));
44
+ wanted.set(path.join(dir, a.command + '.cmd'), forwardingCmd(a, target));
45
+ wanted.set(path.join(dir, a.command), forwardingSh(a, target));
46
46
  }
47
47
  let changed = false;
48
48
  for (const f of ownFiles(dir))
@@ -60,9 +60,8 @@ export function reconcileAgentCommands(config, storageRoot, node, entry) {
60
60
  changed = true;
61
61
  }
62
62
  }
63
- const onPath = (process.env.PATH ?? '')
64
- .split(path.delimiter)
65
- .some((d) => path.resolve(d).toLowerCase() === path.resolve(dir).toLowerCase());
63
+ // Your PATH as stored (not this process's copy of it, which an older terminal keeps): the registry says if it is on.
64
+ const onPath = !changed && userPathHas(dir, storageRoot);
66
65
  if (changed || !onPath)
67
66
  editUserPath(dir, true, storageRoot);
68
67
  return { on: true, commands, changed: changed || !onPath };
@@ -3,3 +3,8 @@
3
3
  * <storageRoot>\bin), else native\scripts\edit-user-path.ps1.
4
4
  */
5
5
  export declare function editUserPath(dir: string, add: boolean, storageRoot: string): void;
6
+ /**
7
+ * Whether dir is on YOUR PATH as stored in the registry (not this process's copy, which an older terminal keeps stale).
8
+ * False when it cannot be told: the caller then edits the PATH again, which is harmless.
9
+ */
10
+ export declare function userPathHas(dir: string, storageRoot: string): boolean;
@@ -23,3 +23,16 @@ export function editUserPath(dir, add, storageRoot) {
23
23
  ' your PATH: ' +
24
24
  (result.stderr || result.error?.message || 'exit ' + result.status).trim());
25
25
  }
26
+ /**
27
+ * Whether dir is on YOUR PATH as stored in the registry (not this process's copy, which an older terminal keeps stale).
28
+ * False when it cannot be told: the caller then edits the PATH again, which is harmless.
29
+ */
30
+ export function userPathHas(dir, storageRoot) {
31
+ refuseInSelfTest('read your PATH');
32
+ const tools = hostToolsExe(storageRoot);
33
+ const options = { encoding: 'utf8', windowsHide: true };
34
+ const result = tools
35
+ ? spawnSync(tools, ['user-path', '--has', dir], options)
36
+ : spawnSync(SYSTEM.powershell(), nativeScript('edit-user-path.ps1', ['-Dir', dir, '-Query']), options);
37
+ return result.status === 0;
38
+ }
@@ -4,3 +4,4 @@ export * from './acl-read.js';
4
4
  export * from './stale.js';
5
5
  export * from './config-backup.js';
6
6
  export * from './path-copies.js';
7
+ export * from './leftovers.js';
@@ -5,3 +5,4 @@ export * from './acl-read.js';
5
5
  export * from './stale.js';
6
6
  export * from './config-backup.js';
7
7
  export * from './path-copies.js';
8
+ export * from './leftovers.js';
@@ -0,0 +1,3 @@
1
+ import { type SandboxFacts } from './stale.js';
2
+ /** The facts for judgeSandbox, from the process list, our heartbeat and our life record. `ping`: ask the daemon too. */
3
+ export declare function readSandboxFacts(storageRoot: string, dataPath: string, ping: boolean): Promise<SandboxFacts>;
@@ -0,0 +1,40 @@
1
+ // Reads what is needed to judge "are the Windows Sandbox programs ours, alive, or left over?" (see judgeSandbox).
2
+ import { LifeRecord, readHeartbeat } from '../life/index.js';
3
+ import { SandboxChannel } from '../channel/index.js';
4
+ import { listProcesses, sessionOf } from '../sharing/index.js';
5
+ import { judgeSandbox } from './stale.js';
6
+ const PING_WAIT_MS = 5000;
7
+ /** The facts for judgeSandbox, from the process list, our heartbeat and our life record. `ping`: ask the daemon too. */
8
+ export async function readSandboxFacts(storageRoot, dataPath, ping) {
9
+ const rows = listProcesses();
10
+ const life = new LifeRecord(storageRoot, dataPath).read();
11
+ const heartbeat = readHeartbeat(dataPath);
12
+ const nowMs = Date.now();
13
+ // A heartbeat from before this launch belongs to an earlier sandbox.
14
+ const mine = heartbeat && (!life || Date.parse(heartbeat.at) >= Date.parse(life.started) - 60000) ? heartbeat : null;
15
+ const base = {
16
+ rows,
17
+ mySession: sessionOf(rows),
18
+ heartbeatAgeMs: mine ? Math.max(0, nowMs - Date.parse(mine.at)) : null,
19
+ lifeEnded: Boolean(life?.ended),
20
+ startedAgeMs: life ? Math.max(0, nowMs - Date.parse(life.started)) : null,
21
+ answers: null,
22
+ };
23
+ if (!ping || judgeSandbox(base).state !== 'leftover')
24
+ return base;
25
+ return { ...base, answers: await answers(dataPath) };
26
+ }
27
+ /** Does the sandbox's daemon answer a ping within a few seconds? */
28
+ async function answers(dataPath) {
29
+ try {
30
+ const reply = await Promise.race([
31
+ new SandboxChannel(dataPath).ping().then((r) => r.ok),
32
+ new Promise((resolve) => setTimeout(() => resolve(false), PING_WAIT_MS).unref()),
33
+ ]);
34
+ return reply;
35
+ }
36
+ catch {
37
+ // No answer, or a channel that cannot be used: it does not answer.
38
+ return false;
39
+ }
40
+ }
@@ -43,3 +43,51 @@ export declare function orphanWatchers(infos: readonly WatcherInfo[], o: {
43
43
  }): WatcherInfo[];
44
44
  /** null when a small file can be created in the folder, else the reason (the file is removed again). */
45
45
  export declare function writeProblem(dir: string): string | null;
46
+ /** The Windows Sandbox programs of the user that a dead sandbox leaves behind (never the VM's own vmwp.exe). */
47
+ export declare const LEFTOVER_IMAGES: readonly ["windowssandbox.exe", "windowssandboxclient.exe", "windowssandboxremotesession.exe", "windowssandboxserver.exe"];
48
+ /** A heartbeat this recent means the sandbox is alive (the sandbox writes one every 10 seconds). */
49
+ export declare const ALIVE_MS: number;
50
+ /** A heartbeat older than this, with the sandbox's programs still there, means the sandbox is gone. */
51
+ export declare const GONE_MS: number;
52
+ /** A sandbox started this recently may not have written a heartbeat yet (a first boot takes minutes). */
53
+ export declare const STARTING_MS: number;
54
+ /** What is known about the Windows Sandbox programs on this PC and our sandbox (read by the caller). */
55
+ export interface SandboxFacts {
56
+ /** Every process of every session (tasklist). */
57
+ readonly rows: readonly TaskRow[];
58
+ /** The Windows session this program runs in; null when unknown (then nothing is ever ended). */
59
+ readonly mySession: number | null;
60
+ /** Age of our sandbox's last heartbeat from this run; null when there is none. */
61
+ readonly heartbeatAgeMs: number | null;
62
+ /** Our life record says the last sandbox ended (the watcher saw it go). */
63
+ readonly lifeEnded: boolean;
64
+ /** Age of our last launch; null when unknown. */
65
+ readonly startedAgeMs: number | null;
66
+ /** Did the sandbox answer a ping just now? null: not asked. */
67
+ readonly answers: boolean | null;
68
+ }
69
+ export type SandboxState =
70
+ /** No Windows Sandbox program runs. */
71
+ 'none'
72
+ /** Ours, and alive (or not known to be gone). */
73
+ | 'running'
74
+ /** Ours, started a moment ago. */
75
+ | 'starting'
76
+ /** Programs of a sandbox that is gone: safe to end after asking. */
77
+ | 'leftover'
78
+ /** Another Windows session's sandbox: never touched. */
79
+ | 'foreign';
80
+ export interface SandboxJudgement {
81
+ readonly state: SandboxState;
82
+ /** The programs of this user's session that cleaning up would end. */
83
+ readonly pids: readonly number[];
84
+ readonly images: readonly string[];
85
+ /** Why, in plain words (for the question). */
86
+ readonly why: string;
87
+ }
88
+ /**
89
+ * Is the Windows Sandbox on this PC ours and alive, ours and gone, or someone else's? (pure) It is "leftover" only
90
+ * with positive evidence: our own programs in our own session, no answer, and either the watcher saw the sandbox end
91
+ * or its heartbeat stopped long ago. No heartbeat at all (an older sandbox) counts as running.
92
+ */
93
+ export declare function judgeSandbox(f: SandboxFacts): SandboxJudgement;
@@ -118,3 +118,43 @@ export function writeProblem(dir) {
118
118
  fs.rmSync(probe, { force: true });
119
119
  }
120
120
  }
121
+ /** The Windows Sandbox programs of the user that a dead sandbox leaves behind (never the VM's own vmwp.exe). */
122
+ export const LEFTOVER_IMAGES = [
123
+ 'windowssandbox.exe',
124
+ 'windowssandboxclient.exe',
125
+ 'windowssandboxremotesession.exe',
126
+ 'windowssandboxserver.exe',
127
+ ];
128
+ /** A heartbeat this recent means the sandbox is alive (the sandbox writes one every 10 seconds). */
129
+ export const ALIVE_MS = 45 * 1000;
130
+ /** A heartbeat older than this, with the sandbox's programs still there, means the sandbox is gone. */
131
+ export const GONE_MS = 2 * 60 * 1000;
132
+ /** A sandbox started this recently may not have written a heartbeat yet (a first boot takes minutes). */
133
+ export const STARTING_MS = 10 * 60 * 1000;
134
+ const isLeftoverImage = (image) => LEFTOVER_IMAGES.includes(image.toLowerCase());
135
+ /**
136
+ * Is the Windows Sandbox on this PC ours and alive, ours and gone, or someone else's? (pure) It is "leftover" only
137
+ * with positive evidence: our own programs in our own session, no answer, and either the watcher saw the sandbox end
138
+ * or its heartbeat stopped long ago. No heartbeat at all (an older sandbox) counts as running.
139
+ */
140
+ export function judgeSandbox(f) {
141
+ const sandbox = f.rows.filter((r) => isLeftoverImage(r.image));
142
+ const mine = f.mySession === null ? [] : sandbox.filter((r) => r.session === f.mySession);
143
+ const none = (state, why) => ({ state, pids: [], images: [], why });
144
+ if (sandbox.length === 0)
145
+ return none('none', 'No Windows Sandbox program is running.');
146
+ if (mine.length === 0)
147
+ return none('foreign', 'The Windows Sandbox that runs belongs to another Windows session.');
148
+ const pids = mine.map((r) => r.pid);
149
+ const images = [...new Set(mine.map((r) => r.image))];
150
+ const found = (state, why) => ({ state, pids, images, why });
151
+ if (f.answers === true || (f.heartbeatAgeMs !== null && f.heartbeatAgeMs <= ALIVE_MS))
152
+ return found('running', 'The sandbox is working.');
153
+ if (f.startedAgeMs !== null && f.startedAgeMs <= STARTING_MS && !f.lifeEnded)
154
+ return found('starting', 'The sandbox started a moment ago.');
155
+ if (f.lifeEnded)
156
+ return found('leftover', 'The last sandbox has ended, but its Windows programs are still there.');
157
+ if (f.heartbeatAgeMs !== null && f.heartbeatAgeMs > GONE_MS)
158
+ return found('leftover', 'The sandbox has not shown a sign of life for ' + Math.round(f.heartbeatAgeMs / 60000) + ' minutes.');
159
+ return found('running', 'A sandbox is running (there is no sign that it is gone).');
160
+ }
@@ -65,6 +65,7 @@ export function guestSession(o, parts) {
65
65
  loadProfile: loadProfileOf(config),
66
66
  pageFileMb: config.sandbox.pageFileMb,
67
67
  agentBackstop: config.sandbox.agentMemoryHeadroomMb,
68
+ codex: config.codex,
68
69
  pressureResponse: config.sandbox.pressureResponse,
69
70
  apps: config.apps,
70
71
  auditProcesses: config.audit.enabled && config.audit.processes,
@@ -35,4 +35,4 @@ export declare function newestCrashReport(storageRoot: string): {
35
35
  /**
36
36
  * When the sandbox ended in a crash (its own, this PC's, power, low memory): writes the report and returns its path.
37
37
  */
38
- export declare function saveCrashReport(storageRoot: string, dataPath: string, lastSeen: string): string | null;
38
+ export declare function saveCrashReport(storageRoot: string, dataPath: string, lastSeen: string, known?: StopCause | null): string | null;
@@ -12,6 +12,8 @@ const KEEP_REPORTS = 20;
12
12
  const MEMORY_TAIL_BYTES = 16 * 1024;
13
13
  /** A gap longer than this between "last seen running" and the restart means the PC stopped responding first. */
14
14
  const FREEZE_GAP_MS = 5 * 60 * 1000;
15
+ /** The ends that get a crash report (not updates, restarts and sleep: those are plain). */
16
+ const REPORTED = ['sandboxCrash', 'crash', 'memory', 'gpu', 'hyperv', 'vanished'];
15
17
  function crashDir(storageRoot) {
16
18
  return path.join(storageRoot, 'logs', 'crashes');
17
19
  }
@@ -230,10 +232,10 @@ function memoryLogTail(dataPath) {
230
232
  /**
231
233
  * When the sandbox ended in a crash (its own, this PC's, power, low memory): writes the report and returns its path.
232
234
  */
233
- export function saveCrashReport(storageRoot, dataPath, lastSeen) {
235
+ export function saveCrashReport(storageRoot, dataPath, lastSeen, known = null) {
234
236
  const events = readStopEvents(new Date(Date.parse(lastSeen) - 2 * 60 * 1000), storageRoot);
235
- const cause = explainStop(events);
236
- if (!cause || !['sandboxCrash', 'crash', 'memory'].includes(cause.kind))
237
+ const cause = explainStop(events) ?? known;
238
+ if (!cause || !REPORTED.includes(cause.kind))
237
239
  return null;
238
240
  const dir = crashDir(storageRoot);
239
241
  fs.mkdirSync(dir, { recursive: true });
@@ -1,5 +1,6 @@
1
1
  import { type Heartbeat } from '../../shared/channel/index.js';
2
2
  import type { Life } from './life-record.js';
3
+ import type { StopCause } from './stop-events.js';
3
4
  /** How the last sandbox ended, when nobody closed it. */
4
5
  export interface LastEnd {
5
6
  /** "ended unexpectedly 21:31: this PC's disks were overloaded (queue 40)". */
@@ -12,6 +13,12 @@ export declare function readHeartbeat(dataPath: string): Heartbeat | null;
12
13
  export declare function heartbeatLine(hb: Heartbeat): string;
13
14
  /** The likeliest reason, from the last heartbeat (pure). */
14
15
  export declare function guessReason(hb: Heartbeat | null): string;
16
+ /**
17
+ * The sandbox went and nothing was logged (pure): its last heartbeat is fresh, from this run, says it was working
18
+ * (a session running, or builds slowed down) and does not say it was shutting down in order, and nobody stopped it on
19
+ * purpose (`sandbox stop`, a restart). The guest cannot tell a closed window from a crash, so the wording says both.
20
+ */
21
+ export declare function vanishedCause(hb: Heartbeat | null, startedIso: string, closedOnPurpose: boolean, endedAtMs: number): StopCause | null;
15
22
  /**
16
23
  * How the last sandbox ended (pure): null when it was closed normally, or is still running. `heartbeat` only counts
17
24
  * when it was written during that run.
@@ -49,6 +49,27 @@ export function guessReason(hb) {
49
49
  return 'the sandbox was running out of memory';
50
50
  return 'Windows stopped it (no reason was logged)';
51
51
  }
52
+ /** A heartbeat older than this before the end is not "it was working when it went". */
53
+ const FRESH_MS = 2 * 60 * 1000;
54
+ /**
55
+ * The sandbox went and nothing was logged (pure): its last heartbeat is fresh, from this run, says it was working
56
+ * (a session running, or builds slowed down) and does not say it was shutting down in order, and nobody stopped it on
57
+ * purpose (`sandbox stop`, a restart). The guest cannot tell a closed window from a crash, so the wording says both.
58
+ */
59
+ export function vanishedCause(hb, startedIso, closedOnPurpose, endedAtMs) {
60
+ if (!hb || closedOnPurpose || hb.phase === 'ending')
61
+ return null;
62
+ const seen = Date.parse(hb.at);
63
+ if (!Number.isFinite(seen) || seen < Date.parse(startedIso) - 60000 || endedAtMs - seen > FRESH_MS)
64
+ return null;
65
+ if (hb.phase !== 'working' && hb.step === 0 && hb.hostStep === 0)
66
+ return null;
67
+ return {
68
+ kind: 'vanished',
69
+ at: new Date(endedAtMs).toISOString(),
70
+ text: 'the sandbox ended suddenly while it was working (its window was closed, or it crashed: Windows logged nothing)',
71
+ };
72
+ }
52
73
  /**
53
74
  * How the last sandbox ended (pure): null when it was closed normally, or is still running. `heartbeat` only counts
54
75
  * when it was written during that run.
@@ -14,6 +14,9 @@ declare const LifeSchema: z.ZodObject<{
14
14
  sleep: "sleep";
15
15
  memory: "memory";
16
16
  sandboxCrash: "sandboxCrash";
17
+ gpu: "gpu";
18
+ hyperv: "hyperv";
19
+ vanished: "vanished";
17
20
  }>;
18
21
  at: z.ZodString;
19
22
  text: z.ZodString;
@@ -5,7 +5,7 @@
5
5
  import { z } from 'zod';
6
6
  import { JsonFile } from '../../shared/io/index.js';
7
7
  import { hostStateFile } from '../storage/index.js';
8
- import { causeSince, IsoSchema } from './stop-events.js';
8
+ import { causeSince, IsoSchema, STOP_KINDS } from './stop-events.js';
9
9
  import { stopWarning } from './stop-warning.js';
10
10
  import { saveCrashReport } from './crash-report.js';
11
11
  const LifeSchema = z.object({
@@ -17,7 +17,7 @@ const LifeSchema = z.object({
17
17
  at: IsoSchema,
18
18
  cause: z
19
19
  .object({
20
- kind: z.enum(['updates', 'restart', 'crash', 'sleep', 'memory', 'sandboxCrash']),
20
+ kind: z.enum(STOP_KINDS),
21
21
  at: IsoSchema,
22
22
  text: z.string().max(400),
23
23
  })
@@ -6,7 +6,9 @@ export interface StopEvent {
6
6
  provider: string;
7
7
  process: string;
8
8
  }
9
- export type StopKind = 'updates' | 'restart' | 'crash' | 'sleep' | 'memory' | 'sandboxCrash';
9
+ /** The ways a sandbox can end that nobody asked for (the life record stores one of these). */
10
+ export declare const STOP_KINDS: readonly ["updates", "restart", "crash", "sleep", "memory", "sandboxCrash", "gpu", "hyperv", "vanished"];
11
+ export type StopKind = (typeof STOP_KINDS)[number];
10
12
  export interface StopCause {
11
13
  kind: StopKind;
12
14
  at: string;
@@ -17,6 +17,24 @@ const StopEventsSchema = z
17
17
  .transform((p) => p ?? ''),
18
18
  }))
19
19
  .max(100);
20
+ /** The ways a sandbox can end that nobody asked for (the life record stores one of these). */
21
+ export const STOP_KINDS = [
22
+ 'updates',
23
+ 'restart',
24
+ 'crash',
25
+ 'sleep',
26
+ 'memory',
27
+ 'sandboxCrash',
28
+ /** The graphics driver reset (display 4101, a GPU LiveKernelEvent): vGPU and the GPU's shared memory. */
29
+ 'gpu',
30
+ /** Hyper-V reported an error, or a program that holds the VM (vmwp.exe, vmmem) crashed. */
31
+ 'hyperv',
32
+ /** Nothing was logged, but the sandbox was working and left no word of leaving. */
33
+ 'vanished',
34
+ ];
35
+ /** Windows live kernel events that are a graphics driver (GPU) reset or timeout. */
36
+ const GPU_LIVE_KERNEL = ['117', '141', '1a8'];
37
+ const VM_PROGRAMS = /vmwp\.exe|vmmem\w*|vmcompute|windowssandbox\w*/i;
20
38
  const UPDATERS = /^(trustedinstaller|mousocoreworker|musnotification(ux)?|usoclient|wuauclt|tiworker|updateorchestrator)\.exe$/i;
21
39
  /** The stop code in Kernel-Power 41's detail ("BugcheckCode 209" -> "0xD1"); null for 0 (no blue screen). */
22
40
  export function bugcheckCode(detail) {
@@ -85,6 +103,47 @@ export function explainStop(events) {
85
103
  },
86
104
  ];
87
105
  }
106
+ // The graphics driver stopped responding and was reset: with vGPU the sandbox shares the GPU, so the VM goes too.
107
+ if (e.id === 4101 && p === 'display')
108
+ return [
109
+ {
110
+ kind: 'gpu',
111
+ at: e.at,
112
+ text: 'the graphics driver stopped responding and was reset while the sandbox used the GPU (vGPU)',
113
+ },
114
+ ];
115
+ const programs = VM_PROGRAMS.exec(e.process)?.[0];
116
+ if (e.id === 1001 && p.includes('windows error reporting') && /LiveKernelEvent/i.test(e.process)) {
117
+ const code = /\bP1:\s*([0-9a-f]{2,4})\b/i.exec(e.process)?.[1]?.toLowerCase();
118
+ if (code && GPU_LIVE_KERNEL.includes(code))
119
+ return [
120
+ {
121
+ kind: 'gpu',
122
+ at: e.at,
123
+ text: 'the graphics driver reset (Windows live kernel event ' +
124
+ code +
125
+ ') while the sandbox used the GPU (vGPU)',
126
+ },
127
+ ];
128
+ }
129
+ if ((e.id === 1000 || e.id === 1001) &&
130
+ programs &&
131
+ /^(application error|windows error reporting)$/i.test(e.provider))
132
+ return [
133
+ {
134
+ kind: 'hyperv',
135
+ at: e.at,
136
+ text: 'a program that runs the sandbox VM (' + programs.toLowerCase() + ') crashed',
137
+ },
138
+ ];
139
+ if (/^microsoft-windows-hyper-v-(worker|vmswitch|compute)/.test(p) && e.id !== 18590 && e.id !== 40001)
140
+ return [
141
+ {
142
+ kind: 'hyperv',
143
+ at: e.at,
144
+ text: 'Hyper-V, which runs the sandbox VM, reported an error (event ' + e.id + ')',
145
+ },
146
+ ];
88
147
  return [];
89
148
  });
90
149
  return causes.sort((a, b) => Date.parse(a.at) - Date.parse(b.at))[0] ?? null;
@@ -4,6 +4,9 @@ const ADVICE = {
4
4
  crash: 'sandbox logs --crash prints what Windows logged and how much memory this PC had; Event Viewer > Windows Logs > System has the rest. A laptop also needs to stay plugged in.',
5
5
  sleep: 'sandbox.keepAwake keeps this PC awake while a sandbox runs (closing a laptop lid still sleeps it). Check it is on, and that sleep is not forced by a policy.',
6
6
  sandboxCrash: 'C:\\AgentData\\logs\\memory-*.log shows what used memory just before. Fewer builds at once inside the sandbox (e.g. cargo build.jobs) makes it less likely.',
7
+ gpu: 'The graphics driver reset while the sandbox used the GPU (vGPU). Turn vGPU off (sandbox config set sandbox.vGpu false) or lower the GPU load, and keep heavy builds and a GPU game apart.',
8
+ hyperv: 'Hyper-V, which runs the sandbox, reported an error. Closing other virtual machines, updating the graphics driver, or turning vGPU off usually helps; sandbox logs --crash has the events.',
9
+ vanished: 'If you did not close its window, this is the known pattern of heavy builds plus a GPU game with vGPU on: turn vGPU off or run fewer builds at once. sandbox logs --crash says what it was doing in its last seconds.',
7
10
  memory: 'Give the sandbox less RAM (sandbox.maxMemoryMb, sandbox.reserveHostMb) or run fewer builds at once inside it.',
8
11
  };
9
12
  const when = (iso) => new Date(iso).toLocaleString(undefined, { weekday: 'short', hour: '2-digit', minute: '2-digit' });
@@ -9,6 +9,7 @@
9
9
  import fs from 'node:fs';
10
10
  import path from 'node:path';
11
11
  import { z } from 'zod';
12
+ import { ANTHROPIC_KEY_HINT } from '../../shared/accounts/index.js';
12
13
  import { isSafeFileName } from '../../shared/paths/index.js';
13
14
  import { JsonFile } from '../../shared/io/index.js';
14
15
  import { storageOf } from '../storage/index.js';
@@ -36,10 +37,7 @@ const SealedSchema = z.object({
36
37
  email: z.string().max(200).nullable(),
37
38
  /** Missing in accounts saved before keys could be: a subscription. */
38
39
  kind: z.enum(LOGIN_KINDS).optional(),
39
- keyHint: z
40
- .string()
41
- .regex(/^sk-ant-\.\.\.[A-Za-z0-9_-]{4}$/)
42
- .optional(),
40
+ keyHint: z.string().regex(ANTHROPIC_KEY_HINT).optional(),
43
41
  sealed: z.string().max(200000),
44
42
  });
45
43
  const StoredSchema = z.union([SealedSchema, AccountSchema]);
@@ -1,3 +1,4 @@
1
+ import { anthropicKeyProblem } from '../../shared/accounts/index.js';
1
2
  /** subscription: claude.ai OAuth; console: the key a Console login made; api-key: a key you gave. */
2
3
  export type LoginKind = 'subscription' | 'console' | 'api-key';
3
4
  export declare const LOGIN_KINDS: readonly ["subscription", "console", "api-key"];
@@ -16,8 +17,8 @@ export interface CaptureOptions {
16
17
  /** ANTHROPIC_API_KEY of this PC: the last place looked at (--from-host only). */
17
18
  readonly envKey?: string | null;
18
19
  }
19
- /** What is wrong with a typed or found API key, or null when it has the right shape. Never quotes the key. */
20
- export declare function apiKeyProblem(key: string): string | null;
20
+ /** What is wrong with a typed or found API key (sk-ant-api or sk-ant-usr), or null. Never quotes the key. */
21
+ export declare const apiKeyProblem: typeof anthropicKeyProblem;
21
22
  /** The most of a key that is ever shown: sk-ant-...abcd. */
22
23
  export declare function maskApiKey(key: string): string;
23
24
  export interface LoginFiles {
@@ -9,23 +9,13 @@
9
9
  // written through. A key read from the sandbox must have the shape of an API key.
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
+ import { anthropicKeyProblem } from '../../shared/accounts/index.js';
12
13
  import { assertTestWritable } from '../test-guard.js';
13
14
  import { hostDataFolder, hostDataRoot } from '../storage/index.js';
14
15
  import { assertNoLinks, fail, fileExists, isLink, removeLink, tryReadJson, writeJson } from '../../core/util/index.js';
15
16
  export const LOGIN_KINDS = ['subscription', 'console', 'api-key'];
16
- /** sk-ant-api03-...: a normal Anthropic API key (letters, digits, - and _, which is what Claude Code accepts). */
17
- const API_KEY = /^sk-ant-api\d{2}-[A-Za-z0-9_-]{20,400}$/;
18
- const API_USER_KEY = /^sk-ant-usr\d{2}-[A-Za-z0-9_-]{20,400}$/;
19
- /** What is wrong with a typed or found API key, or null when it has the right shape. Never quotes the key. */
20
- export function apiKeyProblem(key) {
21
- if (API_KEY.test(key) || API_USER_KEY.test(key))
22
- return null;
23
- if (key.startsWith('sk-ant-oat'))
24
- return 'That is a Claude subscription token (from claude setup-token), not an API key. Save the subscription login instead.';
25
- if (key.startsWith('sk-ant-admin'))
26
- return 'That is an Admin key: it cannot run Claude. Make an API key instead.';
27
- 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).';
28
- }
17
+ /** What is wrong with a typed or found API key (sk-ant-api or sk-ant-usr), or null. Never quotes the key. */
18
+ export const apiKeyProblem = anthropicKeyProblem;
29
19
  /** The most of a key that is ever shown: sk-ant-...abcd. */
30
20
  export function maskApiKey(key) {
31
21
  return 'sk-ant-...' + key.slice(-4);
@@ -0,0 +1,37 @@
1
+ import { LEFTOVER_IMAGES, type SandboxFacts, type SandboxJudgement } from '../health/stale.js';
2
+ /** taskkill arguments that end exactly these programs (pure). */
3
+ export declare function killArgs(pids: readonly number[]): string[];
4
+ /** Ends these Windows Sandbox programs (taskkill by absolute path). Returns false when taskkill failed. */
5
+ export declare function endSandboxPrograms(pids: readonly number[]): boolean;
6
+ /** Restarts the Windows Sandbox service (CmService) in an elevated PowerShell: Windows asks for permission. */
7
+ export declare function restartSandboxService(): boolean;
8
+ /** What the person is asked, and what is done about it: everything outside the decision is passed in. */
9
+ export interface CleanupDeps {
10
+ /** The facts now (a fresh process list each time). */
11
+ readonly facts: () => Promise<SandboxFacts>;
12
+ readonly end: (pids: readonly number[]) => boolean;
13
+ readonly restartService: () => boolean;
14
+ /** An interactive question: true for yes. */
15
+ readonly confirm: (q: {
16
+ title: string;
17
+ lines: string[];
18
+ yes: string;
19
+ }) => Promise<boolean>;
20
+ readonly say: (line: string) => void;
21
+ /** Also restart the service even when the programs ended (the caller said Windows still refuses). */
22
+ readonly alsoService?: boolean;
23
+ }
24
+ export type CleanupOutcome =
25
+ /** Nothing of the sandbox is on this PC. */
26
+ 'nothing'
27
+ /** A live sandbox (or another user's): nothing was ended. */
28
+ | 'alive' | 'declined' | 'cleaned' | 'service-restarted' | 'failed';
29
+ /** The question for a judgement of "leftover" (pure). */
30
+ export declare function cleanupQuestion(j: SandboxJudgement): {
31
+ title: string;
32
+ lines: string[];
33
+ yes: string;
34
+ };
35
+ /** The whole clean-up: judge, ask, end, check, and (when needed and allowed) restart the service. */
36
+ export declare function cleanUpLeftovers(deps: CleanupDeps): Promise<CleanupOutcome>;
37
+ export { LEFTOVER_IMAGES };
@@ -0,0 +1,82 @@
1
+ // Windows Sandbox leaves its programs behind when the VM dies (a crash, a kill from the host side): the next start then
2
+ // fails ("the sandbox was signed out", or "already running"). This ends those leftovers, after asking, and when Windows
3
+ // still refuses, restarts the Windows Sandbox service (CmService), which needs administrator permission. The decisions
4
+ // are pure and the actions are injected, so tests never end a process.
5
+ import { spawnSync } from 'node:child_process';
6
+ import { SYSTEM } from '../../shared/system.js';
7
+ import { refuseInSelfTest } from '../test-guard.js';
8
+ import { nativeScript } from '../native.js';
9
+ import { judgeSandbox, LEFTOVER_IMAGES } from '../health/stale.js';
10
+ const HELPER_TIMEOUT_MS = 15000;
11
+ const SERVICE_TIMEOUT_MS = 120000;
12
+ /** taskkill arguments that end exactly these programs (pure). */
13
+ export function killArgs(pids) {
14
+ const safe = pids.filter((pid) => Number.isInteger(pid) && pid > 4);
15
+ return ['/F', ...safe.flatMap((pid) => ['/PID', String(pid)])];
16
+ }
17
+ /** Ends these Windows Sandbox programs (taskkill by absolute path). Returns false when taskkill failed. */
18
+ export function endSandboxPrograms(pids) {
19
+ refuseInSelfTest('end Windows Sandbox programs');
20
+ if (killArgs(pids).length <= 1)
21
+ return true;
22
+ const result = spawnSync(SYSTEM.taskkill(), killArgs(pids), { windowsHide: true, timeout: HELPER_TIMEOUT_MS });
23
+ return result.status === 0;
24
+ }
25
+ /** Restarts the Windows Sandbox service (CmService) in an elevated PowerShell: Windows asks for permission. */
26
+ export function restartSandboxService() {
27
+ refuseInSelfTest('restart the Windows Sandbox service');
28
+ const result = spawnSync(SYSTEM.powershell(), nativeScript('run-elevated.ps1', ['-Command', 'Restart-Service -Name CmService -Force']), { windowsHide: true, timeout: SERVICE_TIMEOUT_MS });
29
+ return result.status === 0;
30
+ }
31
+ const NAMES = (j) => j.images.join(', ');
32
+ /** The question for a judgement of "leftover" (pure). */
33
+ export function cleanupQuestion(j) {
34
+ return {
35
+ title: 'Close the leftover Windows Sandbox programs?',
36
+ lines: [
37
+ j.why,
38
+ 'Windows Sandbox leaves its programs behind when the sandbox dies, and then refuses to start again.',
39
+ 'Only these programs of your Windows session are closed: ' + NAMES(j) + '. Nothing else.',
40
+ ],
41
+ yes: 'Close them',
42
+ };
43
+ }
44
+ /** The whole clean-up: judge, ask, end, check, and (when needed and allowed) restart the service. */
45
+ export async function cleanUpLeftovers(deps) {
46
+ const first = judgeSandbox(await deps.facts());
47
+ if (first.state === 'none')
48
+ return 'nothing';
49
+ if (first.state !== 'leftover') {
50
+ deps.say(first.why + ' Nothing was closed.');
51
+ return 'alive';
52
+ }
53
+ if (!(await deps.confirm(cleanupQuestion(first))))
54
+ return 'declined';
55
+ const ended = deps.end(first.pids);
56
+ const after = judgeSandbox(await deps.facts());
57
+ const gone = after.state === 'none' || after.state === 'foreign';
58
+ if (gone && !deps.alsoService) {
59
+ deps.say('Closed ' + first.pids.length + ' leftover program(s). The sandbox can start again now.');
60
+ return 'cleaned';
61
+ }
62
+ if (!gone)
63
+ deps.say(ended ? 'Some programs are still there.' : 'Windows would not let them be closed.');
64
+ const restart = deps.alsoService === true ||
65
+ (await deps.confirm({
66
+ title: 'Restart the Windows Sandbox service?',
67
+ lines: [
68
+ 'Windows still holds the old sandbox. Restarting its service (CmService) clears that.',
69
+ 'Windows asks for administrator permission once.',
70
+ ],
71
+ yes: 'Restart it',
72
+ }));
73
+ if (!restart)
74
+ return gone ? 'cleaned' : 'declined';
75
+ if (!deps.restartService()) {
76
+ deps.say('The service was not restarted (the permission was declined, or Windows refused).');
77
+ return 'failed';
78
+ }
79
+ deps.say('The Windows Sandbox service was restarted.');
80
+ return 'service-restarted';
81
+ }
82
+ export { LEFTOVER_IMAGES };
@@ -1,4 +1,5 @@
1
1
  export * from './platform.js';
2
+ export * from './cleanup.js';
2
3
  export * from './probe.js';
3
4
  export * from './tasklist.js';
4
5
  export * from './host-claude.js';