windows-development-sandbox 1.23.2 → 1.25.1

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 (211) hide show
  1. package/config.example.json +5 -3
  2. package/dist/app/features/data/data.def.d.ts +4 -4
  3. package/dist/app/features/data/data.run.d.ts +1 -1
  4. package/dist/app/features/defs.d.ts +161 -6
  5. package/dist/app/features/defs.js +7 -0
  6. package/dist/app/features/projects/projects.def.d.ts +1 -1
  7. package/dist/app/features/projects/projects.run.d.ts +1 -1
  8. package/dist/app/features/unreal/index.d.ts +2 -0
  9. package/dist/app/features/unreal/index.js +3 -0
  10. package/dist/app/features/unreal/unreal.def.d.ts +236 -0
  11. package/dist/app/features/unreal/unreal.def.js +379 -0
  12. package/dist/app/features/unreal/unreal.run.d.ts +96 -0
  13. package/dist/app/features/unreal/unreal.run.js +217 -0
  14. package/dist/app/registry.js +5 -0
  15. package/dist/bin/agent-sandbox.js +2 -2
  16. package/dist/bin/{chunk-7YPEDAJD.js → chunk-3GAXLATK.js} +58 -5
  17. package/dist/bin/{chunk-USTRSHIE.js → chunk-F4PVBN23.js} +2615 -1515
  18. package/dist/bin/{chunk-QFSJX2KW.js → chunk-G3TG4ZT7.js} +1 -1
  19. package/dist/bin/claude-host.js +2 -2
  20. package/dist/bin/claude-sbx.js +2 -2
  21. package/dist/bin/codex-sbx.js +2 -2
  22. package/dist/bin/gemini-sbx.js +2 -2
  23. package/dist/bin/sandbox.js +2 -2
  24. package/dist/bin/sbx-hide.js +2 -2
  25. package/dist/bin/sbx-show.js +2 -2
  26. package/dist/cli/commands/launch/launch.js +5 -2
  27. package/dist/cli/commands/registry.js +3 -0
  28. package/dist/cli/manifest.d.ts +18 -0
  29. package/dist/cli/manifest.js +26 -0
  30. package/dist/cli/menu/coverage-exempt.js +1 -0
  31. package/dist/cli/menu/pages/sandbox-page.d.ts +3 -0
  32. package/dist/cli/menu/pages/sandbox-page.js +34 -4
  33. package/dist/cli/menu/views.js +2 -0
  34. package/dist/core/config/defaults.js +18 -5
  35. package/dist/core/detect/toolchains.js +0 -6
  36. package/dist/core/schema/config.d.ts +10 -9
  37. package/dist/core/schema/pc-installs.d.ts +3 -1
  38. package/dist/core/schema/pc-installs.js +10 -1
  39. package/dist/core/schema/toolchains.d.ts +1 -2
  40. package/dist/core/schema/toolchains.js +4 -7
  41. package/dist/guest/accounts/kinds/epic.js +18 -13
  42. package/dist/guest/agents/agent-files.d.ts +11 -7
  43. package/dist/guest/agents/agent-files.js +37 -11
  44. package/dist/guest/agents/local-state.d.ts +2 -1
  45. package/dist/guest/agents/local-state.js +2 -1
  46. package/dist/guest/apps/after-install.d.ts +1 -1
  47. package/dist/guest/apps/after-install.js +6 -1
  48. package/dist/guest/apps/app-installer.d.ts +1 -0
  49. package/dist/guest/apps/app-installer.js +13 -3
  50. package/dist/guest/apps/comet-unpack.js +3 -2
  51. package/dist/guest/apps/resolvers-gamedev.js +55 -1
  52. package/dist/guest/boot/boot-console.d.ts +28 -0
  53. package/dist/guest/boot/boot-console.js +98 -0
  54. package/dist/guest/boot/boot.d.ts +8 -1
  55. package/dist/guest/boot/boot.js +80 -43
  56. package/dist/guest/boot/graph.d.ts +9 -5
  57. package/dist/guest/boot/graph.js +8 -2
  58. package/dist/guest/boot/guide.js +24 -0
  59. package/dist/guest/boot/progress-view.d.ts +116 -0
  60. package/dist/guest/boot/progress-view.js +456 -0
  61. package/dist/guest/boot/runner.d.ts +20 -10
  62. package/dist/guest/boot/runner.js +39 -47
  63. package/dist/guest/daemon/daemon.js +3 -0
  64. package/dist/guest/runtime/log-line.d.ts +7 -0
  65. package/dist/guest/runtime/log-line.js +18 -0
  66. package/dist/guest/runtime/log.d.ts +17 -4
  67. package/dist/guest/runtime/log.js +40 -17
  68. package/dist/guest/runtime/process.d.ts +9 -0
  69. package/dist/guest/runtime/process.js +69 -8
  70. package/dist/guest/setup/addon-mcp.d.ts +7 -0
  71. package/dist/guest/setup/addon-mcp.js +19 -4
  72. package/dist/guest/setup/browser-gpu.js +14 -11
  73. package/dist/guest/setup/environment.js +9 -4
  74. package/dist/guest/setup/headroom.js +4 -0
  75. package/dist/guest/setup/hooks.d.ts +2 -0
  76. package/dist/guest/setup/hooks.js +4 -0
  77. package/dist/guest/setup/path-plan.d.ts +12 -0
  78. package/dist/guest/setup/path-plan.js +18 -0
  79. package/dist/guest/toolchains/engines/epic-launcher.d.ts +26 -0
  80. package/dist/guest/toolchains/engines/epic-launcher.js +127 -0
  81. package/dist/guest/toolchains/engines/unreal-engines.d.ts +21 -1
  82. package/dist/guest/toolchains/engines/unreal-engines.js +163 -3
  83. package/dist/guest/toolchains/engines/unreal-status.d.ts +13 -0
  84. package/dist/guest/toolchains/engines/unreal-status.js +41 -0
  85. package/dist/guest/toolchains/native/debuggers.d.ts +6 -0
  86. package/dist/guest/toolchains/native/debuggers.js +57 -0
  87. package/dist/guest/toolchains/native/mesa.js +15 -8
  88. package/dist/guest/toolchains/native/rust-extras.d.ts +7 -0
  89. package/dist/guest/toolchains/native/rust-extras.js +23 -1
  90. package/dist/guest/toolchains/native/unreal.d.ts +2 -2
  91. package/dist/guest/toolchains/native/unreal.js +96 -20
  92. package/dist/guest/toolchains/native/visual-studio.d.ts +14 -14
  93. package/dist/guest/toolchains/native/visual-studio.js +145 -174
  94. package/dist/guest/toolchains/native/vs-persist.d.ts +189 -0
  95. package/dist/guest/toolchains/native/vs-persist.js +256 -0
  96. package/dist/guest/toolchains/native/vs-request.d.ts +6 -26
  97. package/dist/guest/toolchains/native/vs-request.js +16 -50
  98. package/dist/guest/toolchains/plan.js +23 -15
  99. package/dist/guest/toolchains/tools/vscode-profile.js +2 -0
  100. package/dist/host/accounts/kinds/epic.js +11 -5
  101. package/dist/host/accounts/source.d.ts +2 -0
  102. package/dist/host/accounts/source.js +1 -0
  103. package/dist/host/migrate/migrations/drop-vs-install-files.d.ts +5 -0
  104. package/dist/host/migrate/migrations/drop-vs-install-files.js +34 -0
  105. package/dist/host/migrate/registry.js +7 -1
  106. package/dist/host/pc-installs/discover.d.ts +2 -0
  107. package/dist/host/pc-installs/discover.js +25 -5
  108. package/dist/host/pc-installs/epic.d.ts +3 -0
  109. package/dist/host/pc-installs/epic.js +33 -2
  110. package/dist/host/pc-installs/source.d.ts +1 -1
  111. package/dist/host/pc-installs/unreal-vault.d.ts +11 -0
  112. package/dist/host/pc-installs/unreal-vault.js +78 -0
  113. package/dist/host/pc-installs/unreal.d.ts +36 -0
  114. package/dist/host/pc-installs/unreal.js +145 -2
  115. package/dist/host/profile/claude-profile.d.ts +8 -0
  116. package/dist/host/profile/claude-profile.js +43 -4
  117. package/dist/host/profile/index.d.ts +1 -1
  118. package/dist/host/profile/index.js +1 -1
  119. package/dist/runtime/agent.js +148 -33
  120. package/dist/runtime/daemon.js +3146 -1546
  121. package/dist/runtime/db.js +148 -33
  122. package/dist/runtime/main.js +3982 -2049
  123. package/dist/runtime/netproxy.js +84 -13
  124. package/dist/runtime/tool.js +125 -31
  125. package/dist/runtime/view.js +85 -14
  126. package/dist/selftest/agent-paths.test.js +18 -1
  127. package/dist/selftest/agent-settings.test.js +36 -5
  128. package/dist/selftest/agents-web.test.js +12 -0
  129. package/dist/selftest/boot-console.test.d.ts +2 -0
  130. package/dist/selftest/boot-console.test.js +291 -0
  131. package/dist/selftest/boot-runner.test.js +10 -12
  132. package/dist/selftest/engines.test.js +602 -3
  133. package/dist/selftest/guest-boot.test.js +7 -1
  134. package/dist/selftest/heavy-load.test.js +3 -1
  135. package/dist/selftest/hostile-input.test.js +11 -1
  136. package/dist/selftest/index.js +4 -0
  137. package/dist/selftest/layout.test.js +4 -0
  138. package/dist/selftest/menu-screens.js +3 -1
  139. package/dist/selftest/migrate.test.js +21 -0
  140. package/dist/selftest/mixamo.test.d.ts +2 -0
  141. package/dist/selftest/mixamo.test.js +60 -0
  142. package/dist/selftest/persisted-installs.test.js +169 -33
  143. package/dist/selftest/secrets.test.js +36 -1
  144. package/dist/selftest/support/engine-golden.js +5 -0
  145. package/dist/selftest/support/fixtures.d.ts +4 -3
  146. package/dist/selftest/support/unreal-sample.d.ts +2 -0
  147. package/dist/selftest/support/unreal-sample.js +83 -0
  148. package/dist/selftest/toolchains.test.js +32 -2
  149. package/dist/shared/accounts/index.d.ts +1 -1
  150. package/dist/shared/accounts/index.js +1 -1
  151. package/dist/shared/accounts/kinds/epic.d.ts +6 -1
  152. package/dist/shared/accounts/kinds/epic.js +11 -1
  153. package/dist/shared/apps/associations.d.ts +7 -0
  154. package/dist/shared/apps/associations.js +26 -0
  155. package/dist/shared/apps/catalogue-agents-web.d.ts +4 -1
  156. package/dist/shared/apps/catalogue-agents-web.js +5 -2
  157. package/dist/shared/apps/catalogue-gamedev.js +71 -1
  158. package/dist/shared/apps/index.d.ts +2 -0
  159. package/dist/shared/apps/index.js +2 -0
  160. package/dist/shared/apps/preview.d.ts +2 -0
  161. package/dist/shared/apps/preview.js +10 -0
  162. package/dist/shared/browser-policy.d.ts +14 -0
  163. package/dist/shared/browser-policy.js +22 -0
  164. package/dist/shared/channel/schema.d.ts +3 -6
  165. package/dist/shared/io/index.d.ts +1 -0
  166. package/dist/shared/io/index.js +1 -0
  167. package/dist/shared/io/rotate-log.d.ts +2 -0
  168. package/dist/shared/io/rotate-log.js +16 -0
  169. package/dist/shared/memory/policy.d.ts +5 -0
  170. package/dist/shared/memory/policy.js +7 -0
  171. package/dist/shared/paths/pc-installs.d.ts +13 -2
  172. package/dist/shared/paths/pc-installs.js +1 -0
  173. package/dist/shared/pty/terminal.js +2 -1
  174. package/dist/shared/readiness.d.ts +0 -1
  175. package/dist/shared/readiness.js +0 -1
  176. package/dist/shared/signer.d.ts +4 -0
  177. package/dist/shared/signer.js +2 -0
  178. package/dist/shared/unreal/engines.d.ts +161 -0
  179. package/dist/shared/unreal/engines.js +308 -0
  180. package/dist/shared/unreal/guide.d.ts +19 -0
  181. package/dist/shared/unreal/guide.js +105 -0
  182. package/dist/shared/unreal/index.d.ts +7 -0
  183. package/dist/shared/unreal/index.js +8 -0
  184. package/dist/shared/unreal/launcher-files.d.ts +88 -0
  185. package/dist/shared/unreal/launcher-files.js +292 -0
  186. package/dist/shared/unreal/mixamo.d.ts +5 -0
  187. package/dist/shared/unreal/mixamo.js +16 -0
  188. package/dist/shared/unreal/room.d.ts +17 -0
  189. package/dist/shared/unreal/room.js +34 -0
  190. package/dist/shared/unreal/status.d.ts +54 -0
  191. package/dist/shared/unreal/status.js +57 -0
  192. package/dist/shared/unreal/vault.d.ts +21 -0
  193. package/dist/shared/unreal/vault.js +67 -0
  194. package/dist/shared/unreal/vs-check.d.ts +39 -0
  195. package/dist/shared/unreal/vs-check.js +79 -0
  196. package/dist/sync/watcher.js +5 -1
  197. package/dist/web/index.html +1 -1
  198. package/dist/web/{main-CCILLX2M.js → main-FNE6XEHA.js} +84 -84
  199. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  200. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  201. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  202. package/package.json +1 -1
  203. package/sandbox/csharp/VsSetup.cs +210 -0
  204. package/sandbox/scripts/get-vs-setup.ps1 +93 -0
  205. package/sandbox/scripts/restore-visual-studio.ps1 +78 -0
  206. package/sandbox/scripts/save-visual-studio.ps1 +106 -0
  207. package/sandbox/scripts/set-registry-values.ps1 +35 -0
  208. package/schemas/config.schema.json +38 -9
  209. package/schemas/sandbox-config.schema.json +11 -8
  210. package/dist/guest/toolchains/native/autosdk.d.ts +0 -3
  211. package/dist/guest/toolchains/native/autosdk.js +0 -50
@@ -27,9 +27,22 @@ export interface RunnerStatus {
27
27
  readonly running: readonly string[];
28
28
  readonly message: string;
29
29
  }
30
+ /** What the console's progress view hears (foreground tasks only: the background ones are file-only). */
31
+ export interface RunnerObserver {
32
+ started(task: BootTaskDef): void;
33
+ /** Also for a task that was skipped (it never started). */
34
+ finished(task: BootTaskDef, outcome: TaskOutcome): void;
35
+ }
36
+ /** Who logs while a task runs. */
37
+ export interface TaskContext {
38
+ readonly stage: number;
39
+ readonly title: string;
40
+ readonly quiet: boolean;
41
+ }
30
42
  export interface RunnerOptions {
43
+ /** A line for the log file only: when each task started and finished (the console shows the progress view). */
44
+ trace(line: string): void;
31
45
  /** `quiet`: file only (background tasks). */
32
- say(line: string, quiet?: boolean): void;
33
46
  warn(line: string, quiet?: boolean): void;
34
47
  /** Milliseconds. */
35
48
  now(): number;
@@ -39,13 +52,12 @@ export interface RunnerOptions {
39
52
  stages: number;
40
53
  stageTitle(stage: number): string;
41
54
  status?(status: RunnerStatus): void;
42
- /** Runs a task's work so that its log lines carry the task (`prefix()` is empty while it runs alone). */
43
- inContext?<T>(ctx: {
44
- prefix: () => string;
45
- quiet: boolean;
46
- }, work: () => T): T;
55
+ observer?: RunnerObserver;
56
+ /** Runs a task's work so that its log lines carry the task (its stage and title, and whether they stay quiet). */
57
+ inContext?<T>(ctx: TaskContext, work: () => T): T;
47
58
  }
48
- export declare const seconds: (ms: number) => string;
59
+ /** "4.6s" under a minute, "7m02s" from there. */
60
+ export declare function duration(ms: number): string;
49
61
  /** Dependencies must exist, ids be unique, the graph acyclic and no foreground task wait for a background one. */
50
62
  export declare function checkGraph(defs: readonly BootTaskDef[]): void;
51
63
  export declare class BootRunner<Id extends string> {
@@ -55,7 +67,6 @@ export declare class BootRunner<Id extends string> {
55
67
  private readonly byId;
56
68
  private readonly state;
57
69
  private readonly outcomes;
58
- private readonly announced;
59
70
  private readonly running;
60
71
  constructor(defs: readonly BootTaskDef<Id>[], impl: Readonly<Record<Id, TaskRun>>, o: RunnerOptions);
61
72
  /** Every outcome so far, in declaration order. */
@@ -68,10 +79,9 @@ export declare class BootRunner<Id extends string> {
68
79
  private isFinished;
69
80
  /** The task cannot have run properly: skipped, or failed with a critical failure. */
70
81
  private isBlocked;
82
+ private tag;
71
83
  private skip;
72
84
  private begin;
73
85
  private finish;
74
86
  private report;
75
87
  }
76
- /** "Boot ready in 31.8s. Slowest: Headroom 28.1s, Developer tools 7.0s. In the background: Visual Studio (...)". */
77
- export declare function bootSummary(totalMs: number, outcomes: readonly TaskOutcome[], background: readonly string[], logPath: string): string;
@@ -1,9 +1,15 @@
1
1
  // Runs the boot's tasks: each starts when the tasks it comes after are finished, a few at a time, in declaration order.
2
- // Pure (no runtime imports): the boot injects printing, the clock, the log context and the status.
2
+ // Pure (no runtime imports): the boot injects the log, the clock, the log context, the status and the progress view.
3
3
  const PARALLEL = 4;
4
- const CYAN = '\x1b[36m';
5
- const RESET = '\x1b[0m';
6
- export const seconds = (ms) => (ms / 1000).toFixed(1) + 's';
4
+ const MINUTE = 60000;
5
+ /** "4.6s" under a minute, "7m02s" from there. */
6
+ export function duration(ms) {
7
+ // 59.96 s would round to "60.0s": from there it is "1m00s".
8
+ if (ms < MINUTE - 50)
9
+ return (Math.max(0, ms) / 1000).toFixed(1) + 's';
10
+ const s = Math.round(ms / 1000);
11
+ return Math.floor(s / 60) + 'm' + String(s % 60).padStart(2, '0') + 's';
12
+ }
7
13
  /** Dependencies must exist, ids be unique, the graph acyclic and no foreground task wait for a background one. */
8
14
  export function checkGraph(defs) {
9
15
  const byId = new Map();
@@ -44,7 +50,6 @@ export class BootRunner {
44
50
  byId = new Map();
45
51
  state = new Map();
46
52
  outcomes = new Map();
47
- announced = new Set();
48
53
  running = new Set();
49
54
  constructor(defs, impl, o) {
50
55
  this.defs = defs;
@@ -84,13 +89,12 @@ export class BootRunner {
84
89
  this.state.set(d.id, 'running');
85
90
  this.running.add(d.id);
86
91
  const began = this.o.now();
87
- if (!quiet)
88
- this.begin(d);
89
- const prefix = () => (this.running.size > 1 && !d.exclusive ? d.title : '');
92
+ this.begin(d, quiet);
93
+ const ctx = { stage: d.stage, title: d.title, quiet };
90
94
  const work = async () => {
91
95
  await this.impl[d.id]();
92
96
  };
93
- const call = () => (this.o.inContext ? this.o.inContext({ prefix, quiet }, work) : work());
97
+ const call = () => (this.o.inContext ? this.o.inContext(ctx, work) : work());
94
98
  // A synchronous throw from a task is a failure too: the call starts inside a promise.
95
99
  void Promise.resolve()
96
100
  .then(call)
@@ -115,7 +119,7 @@ export class BootRunner {
115
119
  for (const d of pending()) {
116
120
  const bad = d.after.find((a) => this.isBlocked(a));
117
121
  if (bad) {
118
- this.skip(d, this.byId.get(bad).title + ' failed');
122
+ this.skip(d, this.byId.get(bad).title + ' failed', quiet);
119
123
  again = true;
120
124
  }
121
125
  }
@@ -147,43 +151,44 @@ export class BootRunner {
147
151
  const s = this.state.get(id);
148
152
  return s === 'skipped' || (s === 'failed' && this.byId.get(id).critical);
149
153
  }
150
- skip(d, why) {
154
+ tag(d) {
155
+ return (d.background ? '[background] ' : '[' + d.stage + '/' + this.o.stages + '] ') + d.title;
156
+ }
157
+ skip(d, why, quiet) {
151
158
  this.state.set(d.id, 'skipped');
152
- this.outcomes.set(d.id, { id: d.id, title: d.title, state: 'skipped', ms: 0, error: why });
153
- this.o.say('[' + d.stage + '/' + this.o.stages + '] ' + d.title + ': skipped (' + why + ')');
159
+ const outcome = { id: d.id, title: d.title, state: 'skipped', ms: 0, error: why };
160
+ this.outcomes.set(d.id, outcome);
161
+ this.o.trace(this.tag(d) + ': skipped (' + why + ')');
162
+ if (!quiet)
163
+ this.o.observer?.finished(d, outcome);
154
164
  }
155
- begin(d) {
156
- if (!this.announced.has(d.stage)) {
157
- this.announced.add(d.stage);
158
- this.o.say('\n' + CYAN + '[' + d.stage + '/' + this.o.stages + '] ' + this.o.stageTitle(d.stage) + RESET);
159
- }
160
- this.o.say('[' + d.stage + '/' + this.o.stages + '] ' + d.title + ': started');
165
+ begin(d, quiet) {
166
+ this.o.trace(this.tag(d) + ': started');
167
+ if (quiet)
168
+ return;
169
+ this.o.observer?.started(d);
161
170
  this.report(d.stage);
162
171
  }
163
172
  finish(d, began, quiet, error) {
164
173
  const ms = this.o.now() - began;
165
174
  const message = error === undefined ? undefined : error instanceof Error ? error.message : String(error);
166
- this.state.set(d.id, message === undefined ? 'done' : 'failed');
167
- this.outcomes.set(d.id, {
175
+ const outcome = {
168
176
  id: d.id,
169
177
  title: d.title,
170
178
  state: message === undefined ? 'done' : 'failed',
171
179
  ms,
172
180
  ...(message === undefined ? {} : { error: message }),
173
- });
174
- const tag = '[' + d.stage + '/' + this.o.stages + '] ' + d.title;
175
- if (message === undefined) {
176
- if (!quiet)
177
- this.o.say(tag + ': done in ' + seconds(ms));
178
- }
179
- else if (d.critical) {
180
- this.o.warn(tag + ': FAILED in ' + seconds(ms) + ': ' + message, quiet);
181
- }
182
- else {
181
+ };
182
+ this.state.set(d.id, outcome.state);
183
+ this.outcomes.set(d.id, outcome);
184
+ this.o.trace(this.tag(d) + (message === undefined ? ': done in ' : ': FAILED in ') + duration(ms));
185
+ // A soft failure is a warning and the boot goes on; a critical one stops the boot, which shows that error.
186
+ if (message !== undefined && !d.critical)
183
187
  this.o.warn(d.title + ' failed (continuing): ' + message, quiet);
184
- if (!quiet)
185
- this.o.say(tag + ': FAILED in ' + seconds(ms) + ': ' + message);
186
- }
188
+ if (message !== undefined && d.critical)
189
+ this.o.trace(this.tag(d) + ': ' + message);
190
+ if (!quiet)
191
+ this.o.observer?.finished(d, outcome);
187
192
  }
188
193
  report(stageHint) {
189
194
  if (!this.o.status)
@@ -200,16 +205,3 @@ export class BootRunner {
200
205
  });
201
206
  }
202
207
  }
203
- /** "Boot ready in 31.8s. Slowest: Headroom 28.1s, Developer tools 7.0s. In the background: Visual Studio (...)". */
204
- export function bootSummary(totalMs, outcomes, background, logPath) {
205
- const slowest = [...outcomes]
206
- .filter((o) => o.state !== 'skipped')
207
- .sort((a, b) => b.ms - a.ms)
208
- .slice(0, 3)
209
- .map((o) => o.title + ' ' + seconds(o.ms));
210
- return ('Boot ready in ' +
211
- seconds(totalMs) +
212
- '.' +
213
- (slowest.length ? ' Slowest: ' + slowest.join(', ') + '.' : '') +
214
- (background.length ? ' In the background: ' + background.join(', ') + ' (installing, see ' + logPath + ').' : ''));
215
- }
@@ -12,6 +12,7 @@ import { liveFolders } from './live.js';
12
12
  import { warn } from '../runtime/log.js';
13
13
  import { errMsg } from '../../core/util/errors.js';
14
14
  import { flushAccounts } from './accounts.js';
15
+ import { captureLauncherEngines } from '../toolchains/engines/unreal-engines.js';
15
16
  const POLL_MS = 400;
16
17
  const MEM_SAVE_MS = 120000;
17
18
  export class Daemon {
@@ -47,6 +48,8 @@ export class Daemon {
47
48
  safely('claude-mem save', saveClaudeMem);
48
49
  safely('agent databases save', saveLocalState);
49
50
  void flushAccounts();
51
+ // Engines the Epic Games Launcher installed on the data disk since: kept for the next sandbox.
52
+ void captureLauncherEngines().catch((e) => warn('Unreal launcher save: ' + errMsg(e)));
50
53
  }
51
54
  }
52
55
  handle(req) {
@@ -0,0 +1,7 @@
1
+ /** Text without its terminal colour and cursor sequences. */
2
+ export declare function plainText(text: string): string;
3
+ /**
4
+ * The file lines for one message: each line of it with an ISO timestamp (UTC, zero-padded:
5
+ * `2026-11-04T12:07:27.930Z`) and the task that logged it, if any; blank lines are dropped.
6
+ */
7
+ export declare function fileLines(message: string, at?: Date, task?: string): string[];
@@ -0,0 +1,18 @@
1
+ // The lines of the guest's log files: plain text (no colours), one timestamped line per line of the message. Pure.
2
+ /** Colour and cursor sequences (CSI ... letter), bounded so a hostile line costs linear time. */
3
+ const ESCAPES = /\x1b\[[0-9;?]{0,32}[A-Za-z]/g;
4
+ /** Text without its terminal colour and cursor sequences. */
5
+ export function plainText(text) {
6
+ return text.replace(ESCAPES, '');
7
+ }
8
+ /**
9
+ * The file lines for one message: each line of it with an ISO timestamp (UTC, zero-padded:
10
+ * `2026-11-04T12:07:27.930Z`) and the task that logged it, if any; blank lines are dropped.
11
+ */
12
+ export function fileLines(message, at = new Date(), task) {
13
+ const stamp = at.toISOString() + ' ' + (task ? '[' + task + '] ' : '');
14
+ return plainText(message)
15
+ .split(/\r?\n/)
16
+ .filter((line) => line.trim() !== '')
17
+ .map((line) => stamp + line.trimEnd());
18
+ }
@@ -1,15 +1,28 @@
1
- /** Who is logging: the task title (a prefix while several tasks run) and whether the console stays silent. */
1
+ /** Who is logging: the boot task (its stage and title) and whether the console stays silent. */
2
2
  export interface LogContext {
3
- readonly prefix?: () => string;
3
+ readonly stage?: number;
4
+ readonly task?: string;
4
5
  /** File only: nothing may print once the first session owns the console. */
5
6
  readonly quiet?: boolean;
6
7
  }
8
+ /** Takes the console's lines while the boot draws its progress (boot/boot-console.ts). */
9
+ export interface ConsoleSink {
10
+ /** An info line; `stage` is the stage of the task that logged it. */
11
+ line(message: string, stage: number | undefined): void;
12
+ warning(message: string): void;
13
+ /** A line the user should read once the boot is ready (shown with the summary). */
14
+ notice(message: string): void;
15
+ }
7
16
  /** Runs `work` so that everything it logs (also after awaits) carries `ctx`. */
8
17
  export declare function withLogContext<T>(ctx: LogContext, work: () => T): T;
9
18
  /** Runs `work` logging to the file only. */
10
19
  export declare function runQuiet<T>(work: () => T): T;
11
- /** One line of the file: an ISO timestamp first (the console shows no times). */
12
- export declare function fileLine(message: string, at?: Date): string;
20
+ /** Sends the console's lines to `next` (null: straight to the console again). */
21
+ export declare function routeConsole(next: ConsoleSink | null): void;
13
22
  export declare function log(message: string): void;
14
23
  export declare function warn(message: string): void;
24
+ /** A line for the user once the boot is ready (printed at once outside the boot). */
25
+ export declare function notice(message: string): void;
26
+ /** A line for the log file only. */
27
+ export declare function record(message: string): void;
15
28
  export declare const logPath: string;
@@ -1,10 +1,14 @@
1
+ // The guest's log: every line goes to the log file (timestamped, plain) and, unless it is quiet, to the console, or
2
+ // to the boot's progress view while that owns the console.
1
3
  import fs from 'node:fs';
2
4
  import path from 'node:path';
3
5
  import { AsyncLocalStorage } from 'node:async_hooks';
4
6
  import { logsDir } from './paths.js';
7
+ import { fileLines } from './log-line.js';
5
8
  fs.mkdirSync(logsDir, { recursive: true });
6
9
  const logFile = path.join(logsDir, (process.env.AGENT_LOG_PREFIX ?? 'guest') + '-' + new Date().toISOString().replace(/[:.]/g, '-') + '.log');
7
10
  const context = new AsyncLocalStorage();
11
+ let sink = null;
8
12
  /** Runs `work` so that everything it logs (also after awaits) carries `ctx`. */
9
13
  export function withLogContext(ctx, work) {
10
14
  return context.run(ctx, work);
@@ -13,34 +17,53 @@ export function withLogContext(ctx, work) {
13
17
  export function runQuiet(work) {
14
18
  return context.run({ ...context.getStore(), quiet: true }, work);
15
19
  }
16
- /** One line of the file: an ISO timestamp first (the console shows no times). */
17
- export function fileLine(message, at = new Date()) {
18
- return at.toISOString() + ' ' + message;
20
+ /** Sends the console's lines to `next` (null: straight to the console again). */
21
+ export function routeConsole(next) {
22
+ sink = next;
19
23
  }
20
- const append = (line) => {
24
+ const append = (message, ctx) => {
21
25
  try {
22
- fs.appendFileSync(logFile, fileLine(line) + '\n');
26
+ const lines = fileLines(message, new Date(), ctx?.task);
27
+ if (lines.length)
28
+ fs.appendFileSync(logFile, lines.join('\n') + '\n');
23
29
  }
24
30
  catch {
25
31
  /* logging must never break the run */
26
32
  }
27
33
  };
28
- const tagged = (message, ctx) => {
29
- const prefix = ctx?.prefix?.();
30
- return prefix ? prefix + ' | ' + message : message;
31
- };
32
34
  export function log(message) {
33
35
  const ctx = context.getStore();
34
- const line = tagged(message, ctx);
35
- if (!ctx?.quiet)
36
- console.log(line);
37
- append(line);
36
+ append(message, ctx);
37
+ if (ctx?.quiet)
38
+ return;
39
+ if (sink)
40
+ sink.line(message, ctx?.stage);
41
+ else
42
+ console.log(message);
38
43
  }
39
44
  export function warn(message) {
40
45
  const ctx = context.getStore();
41
- const line = tagged(message, ctx);
42
- if (!ctx?.quiet)
43
- console.warn('\x1b[33mWARNING: ' + line + '\x1b[0m');
44
- append('WARNING: ' + line);
46
+ append('WARNING: ' + message, ctx);
47
+ if (ctx?.quiet)
48
+ return;
49
+ if (sink)
50
+ sink.warning(message);
51
+ else
52
+ console.warn('\x1b[33mWARNING: ' + message + '\x1b[0m');
53
+ }
54
+ /** A line for the user once the boot is ready (printed at once outside the boot). */
55
+ export function notice(message) {
56
+ const ctx = context.getStore();
57
+ append(message, ctx);
58
+ if (ctx?.quiet)
59
+ return;
60
+ if (sink)
61
+ sink.notice(message);
62
+ else
63
+ console.log(message);
64
+ }
65
+ /** A line for the log file only. */
66
+ export function record(message) {
67
+ append(message, context.getStore());
45
68
  }
46
69
  export const logPath = logFile;
@@ -1,4 +1,13 @@
1
1
  import { type SpawnOptions, type SpawnSyncOptions } from 'node:child_process';
2
+ /** Takes the output of the programs started while the boot draws its progress (boot/boot-console.ts). */
3
+ export interface OutputSink {
4
+ /** Whether programs started now hand their output over instead of writing to the console. */
5
+ capturing(): boolean;
6
+ /** One line of a program's output (to the log). */
7
+ line(text: string): void;
8
+ }
9
+ /** Hands the output of programs started from now on to `sink` while it is capturing (null: the console again). */
10
+ export declare function routeOutput(sink: OutputSink | null): void;
2
11
  /** Runs a program, streaming output, and throws with a readable message when it fails. */
3
12
  export declare function exec(file: string, args: string[], options?: SpawnSyncOptions): void;
4
13
  /** exec without blocking: other toolchains keep downloading while this one runs an installer. */
@@ -8,9 +8,48 @@ import { robocopyErrors, robocopyFailed, robocopyMeaning } from '../../shared/io
8
8
  import { SYSTEM } from '../../shared/system.js';
9
9
  import { scriptArgs } from '../../shared/scripts.js';
10
10
  import { isWin } from './paths.js';
11
+ // Set by the boot only: this module stays free of the log (pure helpers such as tool-env import it).
12
+ let outputSink = null;
13
+ /** Hands the output of programs started from now on to `sink` while it is capturing (null: the console again). */
14
+ export function routeOutput(sink) {
15
+ outputSink = sink;
16
+ }
17
+ const captureOutput = () => outputSink?.capturing() ?? false;
18
+ /** A program's captured output, line by line (its blank lines dropped). */
19
+ function logOutput(text) {
20
+ for (const line of text.split(/\r?\n/))
21
+ if (line.trim())
22
+ outputSink?.line(line.trimEnd());
23
+ }
24
+ /** What a program may print while the boot holds its output for the log (a larger output stops it). */
25
+ const CAPTURE_BYTES = 64 * 1024 * 1024;
26
+ /** A captured line longer than this is cut (an installer's progress bar without line ends). */
27
+ const LINE_CHARS = 2000;
28
+ /** Feeds a stream to the log line by line as it arrives. */
29
+ function logLines(stream) {
30
+ if (!stream)
31
+ return;
32
+ let partial = '';
33
+ stream.setEncoding('utf8');
34
+ stream.on('data', (chunk) => {
35
+ const lines = (partial + chunk).split(/\r?\n/);
36
+ partial = lines.pop().slice(0, LINE_CHARS);
37
+ logOutput(lines.map((l) => l.slice(0, LINE_CHARS)).join('\n'));
38
+ });
39
+ stream.on('end', () => logOutput(partial));
40
+ }
11
41
  /** Runs a program, streaming output, and throws with a readable message when it fails. */
12
42
  export function exec(file, args, options = {}) {
13
- const result = spawnSync(file, args, { stdio: 'inherit', windowsHide: true, ...options });
43
+ // While the boot draws its progress, the program's output goes to the log (it would break the drawing).
44
+ const capture = captureOutput();
45
+ const result = spawnSync(file, args, {
46
+ stdio: capture ? ['inherit', 'pipe', 'pipe'] : 'inherit',
47
+ windowsHide: true,
48
+ ...(capture ? { maxBuffer: CAPTURE_BYTES } : {}),
49
+ ...options,
50
+ });
51
+ if (capture)
52
+ logOutput(String(result.stdout ?? '') + '\n' + String(result.stderr ?? ''));
14
53
  if (result.error)
15
54
  throw result.error;
16
55
  if (result.status !== 0)
@@ -18,36 +57,59 @@ export function exec(file, args, options = {}) {
18
57
  }
19
58
  /** exec without blocking: other toolchains keep downloading while this one runs an installer. */
20
59
  export async function execAsync(file, args, options = {}) {
21
- const code = await spawnAsync(file, args, { stdio: 'inherit', ...options });
60
+ const code = captureOutput()
61
+ ? await new Promise((resolve, reject) => {
62
+ const child = spawn(file, args, { stdio: ['inherit', 'pipe', 'pipe'], windowsHide: true, ...options });
63
+ logLines(child.stdout);
64
+ logLines(child.stderr);
65
+ child.once('error', reject);
66
+ child.once('close', (c) => resolve(c ?? -1));
67
+ })
68
+ : await spawnAsync(file, args, { stdio: 'inherit', ...options });
22
69
  if (code !== 0)
23
70
  throw new Error(path.basename(file) + ' ' + args.slice(0, 3).join(' ') + ' failed with exit code ' + code);
24
71
  }
72
+ /** A script's error output: to the log while the boot holds the console, else to the console's error stream. */
73
+ function scriptErrors(errors) {
74
+ if (!errors)
75
+ return;
76
+ if (captureOutput())
77
+ logOutput(errors);
78
+ else
79
+ process.stderr.write(errors + '\n');
80
+ }
25
81
  export function execOutput(file, args, options = {}) {
26
82
  const result = spawnSync(file, args, { encoding: 'utf8', windowsHide: true, ...options });
27
83
  return result.status === 0 ? String(result.stdout ?? '').trim() : '';
28
84
  }
29
85
  /** Runs sandbox\scripts\<name> (a .ps1 in the launch folder) with `args`; throws when it fails. */
30
86
  export function runScript(name, args = []) {
87
+ const capture = captureOutput();
31
88
  const result = spawnSync(SYSTEM.powershell(), scriptArgs(name, args), {
32
- stdio: ['inherit', 'inherit', 'pipe'],
89
+ stdio: ['inherit', capture ? 'pipe' : 'inherit', 'pipe'],
33
90
  encoding: 'utf8',
34
91
  windowsHide: true,
92
+ ...(capture ? { maxBuffer: CAPTURE_BYTES } : {}),
35
93
  });
94
+ if (capture)
95
+ logOutput(String(result.stdout ?? ''));
36
96
  if (result.error)
37
97
  throw result.error;
38
98
  const errors = String(result.stderr ?? '').trim();
39
- if (errors)
40
- process.stderr.write(errors + '\n');
99
+ scriptErrors(errors);
41
100
  if (result.status !== 0)
42
101
  throw new Error(name + ' failed with exit code ' + result.status + (errors ? ': ' + errors.slice(0, 400) : ''));
43
102
  }
44
103
  /** runScript without blocking the event loop (other boot tasks keep going while the script runs). */
45
104
  export function runScriptAsync(name, args = []) {
46
105
  return new Promise((resolve, reject) => {
106
+ const capture = captureOutput();
47
107
  const child = spawn(SYSTEM.powershell(), scriptArgs(name, args), {
48
- stdio: ['inherit', 'inherit', 'pipe'],
108
+ stdio: ['inherit', capture ? 'pipe' : 'inherit', 'pipe'],
49
109
  windowsHide: true,
50
110
  });
111
+ if (capture)
112
+ logLines(child.stdout);
51
113
  let errors = '';
52
114
  child.stderr?.setEncoding('utf8');
53
115
  child.stderr?.on('data', (chunk) => {
@@ -57,8 +119,7 @@ export function runScriptAsync(name, args = []) {
57
119
  child.once('error', reject);
58
120
  child.once('close', (code) => {
59
121
  errors = errors.trim();
60
- if (errors)
61
- process.stderr.write(errors + '\n');
122
+ scriptErrors(errors);
62
123
  if (code !== 0)
63
124
  reject(new Error(name + ' failed with exit code ' + code + (errors ? ': ' + errors.slice(0, 400) : '')));
64
125
  else
@@ -1,2 +1,9 @@
1
+ /** The Unreal editor's MCP server (Unreal 5.8's ModelContextProtocol plugin), set by the Unreal engines step. */
2
+ export declare const UNREAL_MCP_NAME = "sbx-unreal-editor";
3
+ /**
4
+ * Adds the Unreal editor's MCP server (its address) to the next writeAddonMcpServers, or takes it away (null). It only
5
+ * answers while the editor runs with the plugin on; until then Claude Code lists it as not connected.
6
+ */
7
+ export declare function setUnrealMcp(url: string | null): void;
1
8
  /** Sets the add-ons' MCP servers in the sandbox's .claude.json (and Codex's config.toml); the names that are on. */
2
9
  export declare function writeAddonMcpServers(): string[];
@@ -13,8 +13,18 @@ import { dryRun } from '../runtime/paths.js';
13
13
  import { claudeDir, session, stateDir } from '../runtime/session.js';
14
14
  import { log, warn } from '../runtime/log.js';
15
15
  import { installedMcpAddons } from './agent-addons.js';
16
- /** The names of every MCP server an add-on can add (the ones the sandbox may remove). */
17
- const ADDON_MCP_NAMES = APPS.flatMap((a) => (a.mcp ? [a.mcp.name] : []));
16
+ /** The Unreal editor's MCP server (Unreal 5.8's ModelContextProtocol plugin), set by the Unreal engines step. */
17
+ export const UNREAL_MCP_NAME = 'sbx-unreal-editor';
18
+ let unrealMcp = null;
19
+ /** The names of every MCP server the sandbox can add (the ones it may remove). */
20
+ const ADDON_MCP_NAMES = [...APPS.flatMap((a) => (a.mcp ? [a.mcp.name] : [])), UNREAL_MCP_NAME];
21
+ /**
22
+ * Adds the Unreal editor's MCP server (its address) to the next writeAddonMcpServers, or takes it away (null). It only
23
+ * answers while the editor runs with the plugin on; until then Claude Code lists it as not connected.
24
+ */
25
+ export function setUnrealMcp(url) {
26
+ unrealMcp = url ? { type: 'http', url } : null;
27
+ }
18
28
  /** Variables only this sandbox can tell an add-on's server (where Comet is). */
19
29
  function placeEnv(id) {
20
30
  if (id !== 'comet-mcp')
@@ -34,7 +44,10 @@ function addonServers() {
34
44
  export function writeAddonMcpServers() {
35
45
  const on = addonServers();
36
46
  writeCodexAddonMcpServers(on);
37
- const servers = Object.fromEntries(on.map(({ mcp, server }) => [mcp.name, server]));
47
+ const servers = {
48
+ ...Object.fromEntries(on.map(({ mcp, server }) => [mcp.name, server])),
49
+ ...(unrealMcp ? { [UNREAL_MCP_NAME]: unrealMcp } : {}),
50
+ };
38
51
  const file = path.join(claudeDir, '.claude.json');
39
52
  let state = {};
40
53
  if (fs.existsSync(file)) {
@@ -72,10 +85,12 @@ function writeCodexAddonMcpServers(on) {
72
85
  const servers = Object.fromEntries(on
73
86
  .filter(({ mcp }) => mcp.codex)
74
87
  .map(({ mcp, server }) => [mcp.name, { ...server, ...(mcp.envVars?.length ? { envVars: mcp.envVars } : {}) }]));
88
+ if (unrealMcp)
89
+ servers[UNREAL_MCP_NAME] = unrealMcp;
75
90
  const before = exists(file) ? fs.readFileSync(file, 'utf8') : '';
76
91
  if (!before && !Object.keys(servers).length)
77
92
  return;
78
- const known = APPS.flatMap((a) => (a.mcp?.codex ? [a.mcp.name] : []));
93
+ const known = [...APPS.flatMap((a) => (a.mcp?.codex ? [a.mcp.name] : [])), UNREAL_MCP_NAME];
79
94
  const result = codexWithAddonMcpServers(before, servers, known);
80
95
  for (const name of result.skipped)
81
96
  warn('Codex MCP servers from Apps: ' + name + ' is already in config.toml in another form, left as it is');
@@ -1,18 +1,21 @@
1
- // Browsers in the sandbox draw without the GPU (Edge and Chrome machine policy, written every boot). With sandbox.vGpu
2
- // the sandbox shares this PC's graphics card through one virtual GPU: a browser's GPU process starting next to a game
3
- // or other Direct3D program there has taken the whole sandbox down. Pages still render, WebGL included, in software;
4
- // programs that need the GPU keep it. Without vGPU nothing changes (there is no GPU to use).
1
+ // Browsers in the sandbox (Edge and Chrome machine policy, written every boot) draw without the GPU, and download to
2
+ // C:\AgentData\downloads, which is kept between sandboxes. With sandbox.vGpu the sandbox shares this PC's graphics card
3
+ // through one virtual GPU: a browser's GPU process starting next to a game or other Direct3D program there has taken
4
+ // the whole sandbox down. Pages still render, WebGL included, in software; programs that need the GPU keep it. Without
5
+ // vGPU there is no GPU to use, so only the download folder matters.
6
+ import path from 'node:path';
5
7
  import { SYSTEM } from '../../shared/system.js';
6
- import { dryRun, isWin } from '../runtime/paths.js';
8
+ import { browserPolicies } from '../../shared/browser-policy.js';
9
+ import { dataDir, dryRun, isWin } from '../runtime/paths.js';
7
10
  import { exec } from '../runtime/process.js';
8
11
  import { log } from '../runtime/log.js';
9
- const POLICY_KEYS = ['HKLM\\SOFTWARE\\Policies\\Microsoft\\Edge', 'HKLM\\SOFTWARE\\Policies\\Google\\Chrome'];
12
+ import { mkdirp } from '../runtime/fs.js';
10
13
  export function keepBrowsersOffGpu() {
11
14
  if (dryRun || !isWin)
12
15
  return;
13
- for (const key of POLICY_KEYS)
14
- exec(SYSTEM.reg(), ['add', key, '/v', 'HardwareAccelerationModeEnabled', '/t', 'REG_DWORD', '/d', '0', '/f'], {
15
- stdio: 'ignore',
16
- });
17
- log('Browsers draw without the GPU (Edge and Chrome policy).');
16
+ const downloads = path.join(dataDir, 'downloads');
17
+ mkdirp(downloads);
18
+ for (const p of browserPolicies({ downloads }))
19
+ exec(SYSTEM.reg(), ['add', p.key, '/v', p.name, '/t', p.type, '/d', p.data, '/f'], { stdio: 'ignore' });
20
+ log('Browsers draw without the GPU and download to ' + downloads + '.');
18
21
  }
@@ -1,7 +1,7 @@
1
1
  // The environment every agent session, hook and terminal tab in the sandbox gets.
2
2
  import path from 'node:path';
3
3
  import { claudeDir, session, stateDir, workspace } from '../runtime/session.js';
4
- import { dataDir, dryRun, isWin, testMode, toolsDir } from '../runtime/paths.js';
4
+ import { dataDir, dryRun, isWin, localDir, testMode, toolsDir } from '../runtime/paths.js';
5
5
  import { exists, mkdirp } from '../runtime/fs.js';
6
6
  import { execOutput, runScriptWithJson, runScriptWithJsonAsync } from '../runtime/process.js';
7
7
  import { SYSTEM } from '../../shared/system.js';
@@ -9,7 +9,7 @@ import { warn } from '../runtime/log.js';
9
9
  import { errMsg } from '../../core/util/errors.js';
10
10
  import { localStateVars } from '../agents/local-state.js';
11
11
  import { agentPaths } from '../agents/agent-files.js';
12
- import { appendMissing, expandVars, isPinnedNodeDir, msvcBeforeGitUsr, parseRegPath, pathKey, planPath, } from './path-plan.js';
12
+ import { appendMissing, expandVars, isPinnedNodeDir, parseRegPath, pathKey, planPath, sessionPath, } from './path-plan.js';
13
13
  /** Older launch folders have no agentHomes: these two were always set. */
14
14
  const LEGACY_HOMES = { CODEX_HOME: 'codex', GEMINI_CLI_HOME: 'gemini' };
15
15
  /** Every agent's home variable (CODEX_HOME ...) with the folder it points to in the persistent state. */
@@ -57,8 +57,13 @@ export function agentEnv(tools, target = { workspace, project: session.project,
57
57
  // The runner's own Node (bootstrap.ps1 puts it first on PATH) is not exposed: `node` is whatever nvm selected.
58
58
  const runners = [path.join(toolsDir, 'node'), path.dirname(process.execPath)].map((d) => d.toLowerCase());
59
59
  const base = (target.base.PATH ?? '').split(path.delimiter).filter((d) => d && !runners.includes(d.toLowerCase()));
60
- // MSVC's link.exe before Git's coreutils link.exe, wherever each came from.
61
- env.PATH = msvcBeforeGitUsr([...new Set([...tools.prepend, ...base, ...tools.append])]).join(path.delimiter);
60
+ // The sandbox's commands (agent shims, sbx-db) first; MSVC's link.exe before Git's coreutils link.exe.
61
+ env.PATH = sessionPath({
62
+ commands: path.join(localDir, 'bin'),
63
+ prepend: tools.prepend,
64
+ base,
65
+ append: tools.append,
66
+ }).join(path.delimiter);
62
67
  return env;
63
68
  }
64
69
  /** Makes the agent environment machine-wide, so new terminal tabs and anything the agent starts see the same tools. */