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
@@ -15,19 +15,19 @@ export function agentCommand(name, harness) {
15
15
  return harness.npmBin ?? harness.uvBin ?? (harness.hostExecutable ?? name).replace(/\.exe$/i, '');
16
16
  }
17
17
  /** One shim per configured agent that can be installed here (a name that is not a plain command word is skipped). */
18
- export function agentShims(toolsDir, agents) {
18
+ export function agentShims(toolsDir, agents, flags = {}) {
19
19
  return Object.entries(agents).flatMap(([name, harness]) => {
20
20
  const p = agentPaths(toolsDir, name, harness);
21
21
  const target = p.exe ?? p.cmd;
22
22
  const command = agentCommand(name, harness);
23
23
  return target && /^[A-Za-z0-9_-]+$/.test(command) && /^[A-Za-z0-9_-]+$/.test(name)
24
- ? [{ name, command, target }]
24
+ ? [{ name, command, target, ...(flags[name]?.length ? { arguments: flags[name] } : {}) }]
25
25
  : [];
26
26
  });
27
27
  }
28
28
  /**
29
29
  * The text of `<command>.cmd`: runs the installed agent, or installs it first (`install` = node + installer script).
30
- * The agent's own arguments pass through %* untouched.
30
+ * The agent's own arguments pass through %* untouched, after the shim's flags if it has any.
31
31
  */
32
32
  export function shimText(shim, install) {
33
33
  return [
@@ -39,7 +39,7 @@ export function shimText(shim, install) {
39
39
  '"' + install.node + '" "' + install.script + '" install ' + shim.name,
40
40
  'if errorlevel 1 exit /b 1',
41
41
  ':run',
42
- 'endlocal & "%SBX_REAL%" %*',
42
+ 'endlocal & "%SBX_REAL%" ' + (shim.arguments?.length ? shim.arguments.join(' ') + ' ' : '') + '%*',
43
43
  '',
44
44
  ].join('\r\n');
45
45
  }
@@ -61,19 +61,89 @@ export function localStateEntries(agents, stateDir, localRoot) {
61
61
  export function bootResetPaths(agents, stateDir) {
62
62
  return Object.entries(agents).flatMap(([agent, harness]) => (harness.bootReset ?? []).map((rel) => ({ agent, path: path.join(stateDir, agent, rel) })));
63
63
  }
64
+ /** The marker the sandbox puts above the lines it writes into config.toml. */
65
+ const CODEX_MARKER = '# Set by the sandbox';
64
66
  /**
65
- * Codex's config.toml with its own command sandbox off, or null when it already says which one to use. Inside Windows
66
- * Sandbox that inner sandbox only costs: its setup re-grants ACLs over the whole profile at every boot (minutes), and
67
- * every command hangs until it is done. A `sandbox_mode` the user set (before the first [table]) is kept.
67
+ * The flags every Codex start gets. Flags beat config.toml and a resumed thread's saved permissions, which is why the
68
+ * sandbox does not rely on the file. Full access with no questions is the one flag that skips Codex's Windows sandbox
69
+ * setup for certain. Pure.
70
+ */
71
+ export function codexArguments(c) {
72
+ const out = c.sandboxMode === 'danger-full-access' && c.approvalPolicy === 'never'
73
+ ? ['--dangerously-bypass-approvals-and-sandbox']
74
+ : ['--sandbox', c.sandboxMode, '--ask-for-approval', c.approvalPolicy];
75
+ if (c.model)
76
+ out.push('-c', 'model="' + c.model + '"');
77
+ if (c.reasoningEffort !== 'default')
78
+ out.push('-c', 'model_reasoning_effort="' + c.reasoningEffort + '"');
79
+ return out;
80
+ }
81
+ /** The arguments an agent always starts with: its own, and for codex the sandbox's settings. */
82
+ export function agentArguments(name, harness, codex) {
83
+ return name === 'codex' && codex ? [...harness.arguments, ...codexArguments(codex)] : [...harness.arguments];
84
+ }
85
+ /** The largest config.toml the sandbox edits. */
86
+ export const MAX_CODEX_CONFIG_BYTES = 1024 * 1024;
87
+ /**
88
+ * Codex's config.toml as the sandbox wants it, every other line byte for byte: the lines an older version wrote above
89
+ * the first table (sandbox_mode, approval_policy, under the marker comment) are gone, `[windows] sandbox` follows
90
+ * windowsSandbox ("off" removes the line, so Codex does not set up its own sandbox users), and
91
+ * mcp_oauth_credentials_store = "file" is added when it is not set. Running it again changes nothing. Pure.
92
+ */
93
+ export function codexConfigOwned(toml, c) {
94
+ const eol = toml.includes('\r\n') ? '\r\n' : '\n';
95
+ const firstTable = toml.search(/^[ \t]*\[/m);
96
+ let top = firstTable < 0 ? toml : toml.slice(0, firstTable);
97
+ const tables = firstTable < 0 ? '' : toml.slice(firstTable);
98
+ top = top.replace(MARKED_LINES, '');
99
+ if (c.mcpLoginsInFile && !/^[ \t]*mcp_oauth_credentials_store[ \t]*=/m.test(top)) {
100
+ if (top && !top.endsWith('\n'))
101
+ top += eol;
102
+ top += 'mcp_oauth_credentials_store = "file"' + eol + (tables ? eol : '');
103
+ }
104
+ return top + codexWindowsSandbox(tables, c.windowsSandbox, eol);
105
+ }
106
+ /** The lines an older version wrote above the first table: its marker comment, then sandbox_mode / approval_policy. */
107
+ const MARKED_LINES = new RegExp('^' + CODEX_MARKER + '[^\r\n]*\r?\n(?:(?:sandbox_mode|approval_policy)[ \t]*=[^\r\n]*\r?\n)*(?:[ \t]*\r?\n)?', 'm');
108
+ /** The tables of config.toml (from the first one on) with `[windows] sandbox` set to the mode, or removed for "off". */
109
+ function codexWindowsSandbox(tables, mode, eol) {
110
+ const setting = 'sandbox = "' + mode + '"';
111
+ const header = /^[ \t]*\[windows\][ \t]*(#[^\r\n]*)?(\r?\n|$)/m.exec(tables);
112
+ if (!header) {
113
+ if (mode === 'off')
114
+ return tables;
115
+ const gap = tables && !tables.endsWith('\n') ? eol : '';
116
+ return tables + gap + (tables ? eol : '') + '[windows]' + eol + setting + eol;
117
+ }
118
+ const bodyStart = header.index + header[0].length;
119
+ const next = tables.slice(bodyStart).search(/^[ \t]*\[/m);
120
+ const bodyEnd = next < 0 ? tables.length : bodyStart + next;
121
+ const body = tables.slice(bodyStart, bodyEnd);
122
+ const line = /^[ \t]*sandbox[ \t]*=[^\r\n]*(\r?\n|$)/m.exec(body);
123
+ let updated;
124
+ if (!line)
125
+ updated = mode === 'off' ? body : setting + eol + body;
126
+ else if (mode === 'off')
127
+ updated = body.slice(0, line.index) + body.slice(line.index + line[0].length);
128
+ else
129
+ updated = body.slice(0, line.index) + setting + line[1] + body.slice(line.index + line[0].length);
130
+ const head = tables.slice(0, bodyStart);
131
+ return (head.endsWith('\n') || !updated ? head : head + eol) + updated + tables.slice(bodyEnd);
132
+ }
133
+ /**
134
+ * Codex's config.toml with its own command sandbox off, or null when that changes nothing. Kept for callers that
135
+ * predate the codex settings; use codexConfigOwned.
68
136
  */
69
137
  export function codexConfigWithoutSandbox(toml) {
70
- const firstTable = toml.search(/^\s*\[/m);
71
- const topLevel = firstTable < 0 ? toml : toml.slice(0, firstTable);
72
- if (/^\s*sandbox_mode\s*=/m.test(topLevel))
73
- return null;
74
- return ('# Set by the sandbox: Windows Sandbox already contains Codex, its own command sandbox would only slow it down.\n' +
75
- 'sandbox_mode = "danger-full-access"\n' +
76
- (toml ? '\n' + toml : ''));
138
+ const out = codexConfigOwned(toml, {
139
+ sandboxMode: 'danger-full-access',
140
+ approvalPolicy: 'never',
141
+ model: '',
142
+ reasoningEffort: 'default',
143
+ windowsSandbox: 'off',
144
+ mcpLoginsInFile: true,
145
+ });
146
+ return out === toml ? null : out;
77
147
  }
78
148
  /** A SQLite database file or one of its -wal / -shm siblings. */
79
149
  export const isDatabaseFile = (name) => /\.(sqlite3?|db)(-wal|-shm)?$/i.test(name);
@@ -5,8 +5,11 @@ export declare function agentLocalStates(): LocalState[];
5
5
  export declare function localStateVars(): Record<string, string>;
6
6
  /** Boot: what an agent sealed to the last sandbox (bootReset) is deleted, so it sets it up again. Links are left. */
7
7
  export declare function resetMachineBound(): void;
8
- /** Boot: Codex runs its commands without its own sandbox (codexConfigWithoutSandbox), unless the user picked one. */
9
- export declare function codexWithoutInnerSandbox(): void;
8
+ /**
9
+ * Boot: Codex's config.toml follows the codex settings (codexConfigOwned): no Windows sandbox setup of its own, its MCP
10
+ * logins in a file. The sandbox mode itself travels as launch flags (codexArguments), which beat this file.
11
+ */
12
+ export declare function codexBootConfig(): void;
10
13
  /** Boot: the saved databases of every configured agent are copied to the sandbox's disk, where the agent runs them. */
11
14
  export declare function restoreLocalState(): void;
12
15
  /**
@@ -10,7 +10,7 @@ import { execOutput } from '../runtime/process.js';
10
10
  import { log, warn } from '../runtime/log.js';
11
11
  import { session, stateDir } from '../runtime/session.js';
12
12
  import { errMsg } from '../../core/util/errors.js';
13
- import { bootResetPaths, codexConfigWithoutSandbox, isCopyableDatabaseFile, isDatabaseFile, localStateEntries, } from './agent-files.js';
13
+ import { bootResetPaths, codexConfigOwned, MAX_CODEX_CONFIG_BYTES, isCopyableDatabaseFile, isDatabaseFile, localStateEntries, } from './agent-files.js';
14
14
  const SQLITE_HEADER = 'SQLite format 3\0';
15
15
  /** The sandbox-disk folder that holds the working copies. */
16
16
  const localRoot = () => path.join(localDir, 'agent-state');
@@ -102,17 +102,31 @@ export function resetMachineBound() {
102
102
  }
103
103
  }
104
104
  }
105
- /** Boot: Codex runs its commands without its own sandbox (codexConfigWithoutSandbox), unless the user picked one. */
106
- export function codexWithoutInnerSandbox() {
107
- if (dryRun || !session.agents?.codex)
105
+ /**
106
+ * Boot: Codex's config.toml follows the codex settings (codexConfigOwned): no Windows sandbox setup of its own, its MCP
107
+ * logins in a file. The sandbox mode itself travels as launch flags (codexArguments), which beat this file.
108
+ */
109
+ export function codexBootConfig() {
110
+ if (dryRun || !session.agents?.codex || !session.codex)
108
111
  return;
109
112
  const file = path.join(stateDir, 'codex', 'config.toml');
110
- const updated = codexConfigWithoutSandbox(exists(file) ? fs.readFileSync(file, 'utf8') : '');
111
- if (updated === null)
112
- return;
113
- mkdirp(path.dirname(file));
114
- fs.writeFileSync(file, updated, 'utf8');
115
- log('codex: its own command sandbox is off (sandbox_mode = "danger-full-access" in config.toml).');
113
+ try {
114
+ const size = exists(file) ? fs.statSync(file).size : 0;
115
+ if (size > MAX_CODEX_CONFIG_BYTES) {
116
+ warn('codex: config.toml is over 1 MB; left as it is.');
117
+ return;
118
+ }
119
+ const before = size ? fs.readFileSync(file, 'utf8') : '';
120
+ const after = codexConfigOwned(before, session.codex);
121
+ if (after === before)
122
+ return;
123
+ mkdirp(path.dirname(file));
124
+ fs.writeFileSync(file, after, 'utf8');
125
+ log('codex: config.toml updated (Windows sandbox "' + session.codex.windowsSandbox + '").');
126
+ }
127
+ catch (e) {
128
+ warn('codex: could not update config.toml (' + errMsg(e) + ').');
129
+ }
116
130
  }
117
131
  /** Boot: the saved databases of every configured agent are copied to the sandbox's disk, where the agent runs them. */
118
132
  export function restoreLocalState() {
@@ -5,8 +5,8 @@ import { spawn } from 'node:child_process';
5
5
  import { writeGuides } from './guide.js';
6
6
  import { setupToolchains } from '../toolchains/index.js';
7
7
  import { agents } from '../agents/agent-installer.js';
8
- import { agentShims, sbxAgentText, shimText } from '../agents/agent-files.js';
9
- import { codexWithoutInnerSandbox, resetMachineBound, restoreLocalState } from '../agents/local-state.js';
8
+ import { agentArguments, agentShims, codexArguments, sbxAgentText, shimText } from '../agents/agent-files.js';
9
+ import { codexBootConfig, resetMachineBound, restoreLocalState } from '../agents/local-state.js';
10
10
  import { installShellPath } from '../setup/shell-path.js';
11
11
  import { linkProfileDirs } from '../agents/profile-links.js';
12
12
  import { restoreClaudeMem } from '../setup/claude-mem.js';
@@ -120,7 +120,7 @@ function writeDevToolCommands(dir) {
120
120
  */
121
121
  function writeAgentCommands(dir) {
122
122
  const install = { node: process.execPath, script: path.join(path.dirname(process.argv[1]), 'agent.js') };
123
- for (const shim of agentShims(toolsDir, session.agents ?? {}))
123
+ for (const shim of agentShims(toolsDir, session.agents ?? {}, session.codex ? { codex: codexArguments(session.codex) } : {}))
124
124
  fs.writeFileSync(path.join(dir, shim.command + '.cmd'), shimText(shim, install));
125
125
  fs.writeFileSync(path.join(dir, 'sbx-agent.cmd'), sbxAgentText(install));
126
126
  }
@@ -280,7 +280,7 @@ export class Boot {
280
280
  await soft('Claude settings', () => syncClaudeProfile(this.secrets.claudeCredentials));
281
281
  await soft('claude-mem restore', restoreClaudeMem);
282
282
  await soft('Agent files of the last sandbox', resetMachineBound);
283
- await soft('Codex without its own sandbox', codexWithoutInnerSandbox);
283
+ await soft('Codex settings', codexBootConfig);
284
284
  await soft('Agent databases restore', restoreLocalState);
285
285
  // Agent add-ons: MCP servers in the sandbox's Claude Code, and the Headroom proxy before the managed settings point
286
286
  // Claude Code at it (the guide reads them back, so it comes last).
@@ -316,7 +316,12 @@ export class Boot {
316
316
  if (dryRun) {
317
317
  const launch = agents.resolveLaunch(session.harnessName, session.harness);
318
318
  log('(dry run) would run: ' +
319
- [launch.file, ...launch.args, ...session.harness.arguments, ...session.harnessArguments].join(' ') +
319
+ [
320
+ launch.file,
321
+ ...launch.args,
322
+ ...agentArguments(session.harnessName, session.harness, session.codex),
323
+ ...session.harnessArguments,
324
+ ].join(' ') +
320
325
  ' | cwd=' +
321
326
  workspace);
322
327
  runHooks('post-agent', env);
@@ -1,6 +1,11 @@
1
1
  import type { Heartbeat } from '../../shared/channel/schema.js';
2
2
  import { type GovernorSnapshot } from '../governor/index.js';
3
3
  /** The heartbeat for these numbers (the governor's snapshot when it has one, else Windows' own free memory). */
4
- export declare function buildHeartbeat(snapshot: GovernorSnapshot | null, sessionCount: number, uptimeSeconds: number, freeBytes: number, totalBytes: number, at: string): Heartbeat;
4
+ export declare function buildHeartbeat(snapshot: GovernorSnapshot | null, sessionCount: number, uptimeSeconds: number, freeBytes: number, totalBytes: number, at: string, ending?: boolean): Heartbeat;
5
+ /**
6
+ * The last heartbeat of an orderly end (idle shutdown, `sandbox stop`, Windows shutting down): the host then knows the
7
+ * sandbox was not lost. A sandbox that just vanishes leaves a busy heartbeat instead.
8
+ */
9
+ export declare function markEnding(): void;
5
10
  /** Writes the heartbeat now and every 10 seconds; a failed write is only a warning. */
6
- export declare function startHeartbeat(): void;
11
+ export declare function startHeartbeat(sessionCount: () => number): void;
@@ -7,11 +7,10 @@ import { governor } from '../governor/index.js';
7
7
  import { run } from '../runtime/paths.js';
8
8
  import { warn } from '../runtime/log.js';
9
9
  import { errMsg } from '../../core/util/errors.js';
10
- import { sessions } from './sessions.js';
11
10
  const BEAT_MS = 10000;
12
11
  const clamp = (n, low, high) => Math.max(low, Math.min(high, n));
13
12
  /** The heartbeat for these numbers (the governor's snapshot when it has one, else Windows' own free memory). */
14
- export function buildHeartbeat(snapshot, sessionCount, uptimeSeconds, freeBytes, totalBytes, at) {
13
+ export function buildHeartbeat(snapshot, sessionCount, uptimeSeconds, freeBytes, totalBytes, at, ending = false) {
15
14
  const ramFree = snapshot?.ramFreePercent ?? Math.round((freeBytes / Math.max(1, totalBytes)) * 100);
16
15
  return {
17
16
  at,
@@ -21,20 +20,40 @@ export function buildHeartbeat(snapshot, sessionCount, uptimeSeconds, freeBytes,
21
20
  step: clamp(snapshot?.step ?? 0, 0, 4),
22
21
  hostStep: clamp(snapshot?.hostStep ?? 0, 0, 4),
23
22
  topProcesses: (snapshot?.top ?? []).slice(0, 5).map((p) => ({ name: p.name.slice(0, 64), privateMb: p.mb })),
24
- ...(snapshot?.diskQueue != null ? { diskQueueReported: snapshot.diskQueue } : {}),
25
- phase: sessionCount > 0 ? 'working' : 'ready',
23
+ ...(snapshot?.diskQueue !== null && snapshot?.diskQueue !== undefined
24
+ ? { diskQueueReported: snapshot.diskQueue }
25
+ : {}),
26
+ phase: ending ? 'ending' : sessionCount > 0 ? 'working' : 'ready',
26
27
  };
27
28
  }
29
+ /** How many sessions run (set by startHeartbeat; this file must not import the sessions, which end through it). */
30
+ let countSessions = () => 0;
31
+ function writeBeat(ending) {
32
+ try {
33
+ writeJsonAtomic(run.heartbeatFile, buildHeartbeat(governor.snapshot(), countSessions(), os.uptime(), os.freemem(), os.totalmem(), now(), ending));
34
+ }
35
+ catch (e) {
36
+ warn('heartbeat: ' + errMsg(e));
37
+ }
38
+ }
39
+ /**
40
+ * The last heartbeat of an orderly end (idle shutdown, `sandbox stop`, Windows shutting down): the host then knows the
41
+ * sandbox was not lost. A sandbox that just vanishes leaves a busy heartbeat instead.
42
+ */
43
+ export function markEnding() {
44
+ writeBeat(true);
45
+ }
28
46
  /** Writes the heartbeat now and every 10 seconds; a failed write is only a warning. */
29
- export function startHeartbeat() {
30
- const beat = () => {
31
- try {
32
- writeJsonAtomic(run.heartbeatFile, buildHeartbeat(governor.snapshot(), sessions.count, os.uptime(), os.freemem(), os.totalmem(), now()));
33
- }
34
- catch (e) {
35
- warn('heartbeat: ' + errMsg(e));
36
- }
37
- };
38
- beat();
39
- setInterval(beat, BEAT_MS).unref();
47
+ export function startHeartbeat(sessionCount) {
48
+ countSessions = sessionCount;
49
+ writeBeat(false);
50
+ const timer = setInterval(() => writeBeat(false), BEAT_MS);
51
+ timer.unref();
52
+ // Windows shutting down or the console closing: say so, then end (the default for these signals).
53
+ for (const signal of ['SIGHUP', 'SIGBREAK', 'SIGTERM'])
54
+ process.on(signal, () => {
55
+ clearInterval(timer);
56
+ markEnding();
57
+ process.exit(0);
58
+ });
40
59
  }
@@ -9,6 +9,7 @@ import { log } from '../runtime/log.js';
9
9
  import { session } from '../runtime/session.js';
10
10
  import { safely } from './state.js';
11
11
  import { flushAccounts } from './accounts.js';
12
+ import { markEnding } from './heartbeat.js';
12
13
  /** How long the sandbox must stay idle before it shuts down. */
13
14
  const GRACE_MS = 15000;
14
15
  export class IdleShutdown {
@@ -27,6 +28,7 @@ export class IdleShutdown {
27
28
  if (!this.idle())
28
29
  return;
29
30
  log('Headless sandbox has no sessions left; shutting down.');
31
+ safely('heartbeat', markEnding);
30
32
  safely('claude-mem save', saveClaudeMem);
31
33
  safely('agent databases save', saveLocalState);
32
34
  // Renewed sign-ins go to the PC, and a held one (a Unity seat) is let go, before Windows stops.
@@ -18,6 +18,7 @@ import { freePort } from '../runtime/ports.js';
18
18
  import { primaryProject } from '../boot/project-status.js';
19
19
  import { endLeftoverTree, ensurePtyHost, ptyHostLogTail, recordedTrees, startPtyHost, waitForPtyHost, } from '../sessions/pty-host.js';
20
20
  import { agents } from '../agents/agent-installer.js';
21
+ import { agentArguments } from '../agents/agent-files.js';
21
22
  import { saveClaudeMem } from '../setup/claude-mem.js';
22
23
  import { saveLocalState } from '../agents/local-state.js';
23
24
  import { projectHooks, runHooks, runHooksAsync } from '../setup/hooks.js';
@@ -108,7 +109,7 @@ const agentProgram = async (req, env, say) => {
108
109
  say('Preparing ' + req.harness);
109
110
  await agents.provision(req.harness, harness, env, false);
110
111
  const launch = agents.resolveLaunch(req.harness, harness);
111
- const base = [...launch.args, ...harness.arguments];
112
+ const base = [...launch.args, ...agentArguments(req.harness, harness, session.codex)];
112
113
  return {
113
114
  command: commandLine(launch.file, [...base, ...req.args]),
114
115
  restartCommand: commandLine(launch.file, [...base, ...(harness.resumeArguments ?? [])]),
@@ -25,5 +25,5 @@ safely('leftover processes', () => sessions.sweepLeftovers());
25
25
  safely('memory governor', () => governor.start());
26
26
  log('Memory governor: ' + governor.describe());
27
27
  safely('service sign-ins', startAccountWatch);
28
- safely('heartbeat', startHeartbeat);
28
+ safely('heartbeat', () => startHeartbeat(() => sessions.count));
29
29
  new Daemon().start();
@@ -1,12 +1,8 @@
1
- /** Who signs the installers the toolchains run (the subject of the signature). */
2
- export declare const SIGNERS: {
3
- readonly microsoft: "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US";
4
- readonly valve: RegExp;
5
- readonly unity: RegExp;
6
- };
7
- /** Whether a file's signature is the expected signer's. */
8
- export declare function signedBy(file: string, signer: string | RegExp): boolean;
1
+ import { SIGNERS, type AppSigner } from '../../shared/signer.js';
2
+ import { signedBy } from './signature.js';
3
+ export { SIGNERS };
4
+ export { signedBy };
9
5
  /** A downloader `verify` hook: refuses a file not signed by `signer` (it counts as integrity for requireChecksum). */
10
- export declare function signatureCheck(signer: string | RegExp, what: string): (file: string) => void;
6
+ export declare function signatureCheck(signer: AppSigner, what: string): (file: string) => void;
11
7
  /** The installer at `file` (downloaded once from `url`), checked against its signer and, when given, its SHA-256. */
12
- export declare function signedInstaller(url: string, file: string, what: string, signer: string | RegExp, sha256?: string | null): Promise<string>;
8
+ export declare function signedInstaller(url: string, file: string, what: string, signer: AppSigner, sha256?: string | null): Promise<string>;
@@ -5,18 +5,10 @@ import path from 'node:path';
5
5
  import { downloader } from '../../shared/download/index.js';
6
6
  import { exists, mkdirp } from '../runtime/fs.js';
7
7
  import { log } from '../runtime/log.js';
8
- import { MICROSOFT_SIGNER, signerOf } from './native/vcruntime.js';
9
- /** Who signs the installers the toolchains run (the subject of the signature). */
10
- export const SIGNERS = {
11
- microsoft: MICROSOFT_SIGNER,
12
- valve: /^CN="?Valve(,| Corp)/,
13
- unity: /^CN="?Unity Technologies/,
14
- };
15
- /** Whether a file's signature is the expected signer's. */
16
- export function signedBy(file, signer) {
17
- const subject = signerOf(file);
18
- return typeof signer === 'string' ? subject === signer : signer.test(subject);
19
- }
8
+ import { SIGNERS } from '../../shared/signer.js';
9
+ import { signedBy } from './signature.js';
10
+ export { SIGNERS };
11
+ export { signedBy };
20
12
  /** A downloader `verify` hook: refuses a file not signed by `signer` (it counts as integrity for requireChecksum). */
21
13
  export function signatureCheck(signer, what) {
22
14
  return (file) => {
@@ -0,0 +1,3 @@
1
+ import { type ToolEnv } from '../tool-env.js';
2
+ /** Makes the AutoSDK tree from the portable MSVC if it is missing for this MSVC and SDK, and points Unreal at it. */
3
+ export declare function setupAutosdk(env: ToolEnv): Promise<void>;
@@ -0,0 +1,50 @@
1
+ // Unreal without Visual Studio (toolchains.unreal.visualStudio = "autosdk", experimental): an AutoSDK tree made from
2
+ // the portable MSVC (tools\msvc), at tools\unreal\autosdk, which UnrealBuildTool finds through UE_SDKS_ROOT. Made of
3
+ // copies (mapped folders refuse links), once; kept between sandboxes. The editor's Visual Studio integration is not
4
+ // available.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { z } from 'zod';
8
+ import { writeJsonAtomic } from '../../../shared/io/json.js';
9
+ import { robocopyFailed } from '../../../shared/io/robocopy-result.js';
10
+ import { spawnAsync } from '../../../shared/download/index.js';
11
+ import { SYSTEM } from '../../../shared/system.js';
12
+ import { exists, mkdirp } from '../../runtime/fs.js';
13
+ import { log } from '../../runtime/log.js';
14
+ import { tool } from '../tool-env.js';
15
+ import { hasCompiler, readInstalled, ROOT as MSVC_ROOT } from './msvc/tree.js';
16
+ import { autosdkLayout } from './vs-request.js';
17
+ const AUTOSDK_DIR = tool('unreal', 'autosdk');
18
+ const MARKER = path.join(AUTOSDK_DIR, '.complete.json');
19
+ const MarkerSchema = z.object({ msvc: z.string(), sdk: z.string() });
20
+ function builtFor(msvc, sdk) {
21
+ try {
22
+ const done = MarkerSchema.parse(JSON.parse(fs.readFileSync(MARKER, 'utf8')));
23
+ return done.msvc === msvc && done.sdk === sdk;
24
+ }
25
+ catch {
26
+ return false; // No marker, or a half-written one: build again.
27
+ }
28
+ }
29
+ /** Makes the AutoSDK tree from the portable MSVC if it is missing for this MSVC and SDK, and points Unreal at it. */
30
+ export async function setupAutosdk(env) {
31
+ const msvc = readInstalled();
32
+ if (!msvc || !hasCompiler(msvc.msvc))
33
+ throw new Error('toolchains.unreal.visualStudio = "autosdk" builds from the portable MSVC, which is not installed ' +
34
+ '(is toolchains.msvc off?)');
35
+ if (!builtFor(msvc.msvc, msvc.sdk)) {
36
+ log('Making the Unreal AutoSDK from MSVC ' + msvc.msvc + ' (~2 GB, once) ...');
37
+ fs.rmSync(MARKER, { force: true });
38
+ for (const pair of autosdkLayout(MSVC_ROOT, msvc.msvc, msvc.sdk, AUTOSDK_DIR)) {
39
+ mkdirp(pair.to);
40
+ const code = await spawnAsync(SYSTEM.robocopy(), [pair.from, pair.to, '/E', '/COPY:DAT', '/R:1', '/W:1', '/NP']);
41
+ if (robocopyFailed(code))
42
+ throw new Error('could not copy ' + pair.from + ' into the AutoSDK (robocopy ' + code + ')');
43
+ }
44
+ writeJsonAtomic(MARKER, { msvc: msvc.msvc, sdk: msvc.sdk });
45
+ }
46
+ if (!exists(MARKER))
47
+ throw new Error('the Unreal AutoSDK was not completed');
48
+ env.vars['UE_SDKS_ROOT'] = AUTOSDK_DIR;
49
+ env.summary.push('unreal autosdk: MSVC ' + msvc.msvc + ', Windows SDK ' + msvc.sdk + ' (UE_SDKS_ROOT=' + AUTOSDK_DIR + ')');
50
+ }
@@ -12,6 +12,7 @@ import { log } from '../../runtime/log.js';
12
12
  import { setupUnrealEngines } from '../engines/unreal-engines.js';
13
13
  import { SIGNERS, signedInstaller } from '../installers.js';
14
14
  import { tool } from '../tool-env.js';
15
+ import { setupAutosdk } from './autosdk.js';
15
16
  import { installedVs } from './visual-studio.js';
16
17
  export { UNREAL_COMPONENTS } from './vs-request.js';
17
18
  const DIRECTX_URL = 'https://download.microsoft.com/download/8/4/A/84A35BF1-DAFE-4AE8-82AF-AD2AE20B6B14/directx_Jun2010_redist.exe';
@@ -39,6 +40,8 @@ export async function setupUnreal(env, config, ctx) {
39
40
  return;
40
41
  }
41
42
  await installDirectX();
43
+ if (config.visualStudio === 'autosdk')
44
+ await setupAutosdk(env);
42
45
  await setupUnrealEngines(env, config, ctx);
43
46
  const vs = installedVs();
44
47
  env.summary.push('unreal: ' +
@@ -1,7 +1,4 @@
1
1
  import { type ToolEnv } from '../tool-env.js';
2
- export declare const MICROSOFT_SIGNER = "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US";
3
- /** The subject of a valid Authenticode signature, or '' when the file is unsigned or tampered with. */
4
- export declare const signerOf: (file: string) => string;
5
2
  /**
6
3
  * A real installer (it writes to System32), so it runs on every boot; the installer itself is kept in persistent
7
4
  * storage and only run when Microsoft's signature on it checks out. Resolves true when this call ran the installer.
@@ -3,17 +3,14 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { spawnAsync } from '../../../shared/download/index.js';
5
5
  import { dryRun } from '../../runtime/paths.js';
6
- import { scriptOutput } from '../../runtime/process.js';
7
6
  import { exists } from '../../runtime/fs.js';
8
7
  import { log } from '../../runtime/log.js';
9
8
  import { downloadFile } from '../install.js';
9
+ import { SIGNERS, signedBy } from '../installers.js';
10
10
  import { tool } from '../tool-env.js';
11
11
  const VC_REDIST = 'https://aka.ms/vs/17/release/vc_redist.x64.exe';
12
- export const MICROSOFT_SIGNER = 'CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US';
13
12
  /** 1638: a newer version is already installed; 3010: installed, a restart would finish it (not needed for the DLLs). */
14
13
  const OK_EXIT_CODES = [0, 1638, 3010];
15
- /** The subject of a valid Authenticode signature, or '' when the file is unsigned or tampered with. */
16
- export const signerOf = (file) => scriptOutput('get-signer.ps1', ['-Path', file]).trim();
17
14
  /**
18
15
  * A real installer (it writes to System32), so it runs on every boot; the installer itself is kept in persistent
19
16
  * storage and only run when Microsoft's signature on it checks out. Resolves true when this call ran the installer.
@@ -32,7 +29,7 @@ export async function setupVcRuntime(env, summary = true) {
32
29
  log('Downloading the Visual C++ runtime (~25 MB once) ...');
33
30
  await downloadFile(VC_REDIST, installer);
34
31
  }
35
- if (signerOf(installer) !== MICROSOFT_SIGNER) {
32
+ if (!signedBy(installer, SIGNERS.microsoft)) {
36
33
  fs.rmSync(installer, { force: true });
37
34
  throw new Error('vc_redist.x64.exe is not signed by Microsoft; deleted it (it is downloaded again next boot)');
38
35
  }
@@ -1,6 +1,6 @@
1
1
  import { type ToolEnv } from '../tool-env.js';
2
- import type { VsRequest } from './vs-request.js';
3
- export { BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, visualStudioWanted, type VsRequest } from './vs-request.js';
2
+ import { type VsRequest } from './vs-request.js';
3
+ export { BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, unrealUsesAutosdk, visualStudioWanted, type VsRequest, } from './vs-request.js';
4
4
  /** The newest Visual Studio (or Build Tools) with the C++ compiler: its folder and version, or null. */
5
5
  export declare function installedVs(): {
6
6
  path: string;