windows-development-sandbox 1.18.4 → 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 (181) 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 +132 -1
  28. package/dist/app/features/defs.js +9 -0
  29. package/dist/app/features/settings/categories.def.js +5 -2
  30. package/dist/app/features/settings/settings-rows.js +5 -1
  31. package/dist/app/features/settings/settings.run.d.ts +8 -0
  32. package/dist/app/features/settings/settings.run.js +9 -0
  33. package/dist/app/features/updates/updates.run.js +12 -2
  34. package/dist/app/features/vpn/vpn.def.d.ts +33 -0
  35. package/dist/app/features/vpn/vpn.def.js +30 -6
  36. package/dist/app/features/vpn/vpn.run.d.ts +11 -0
  37. package/dist/app/features/vpn/vpn.run.js +4 -1
  38. package/dist/app/kernel/rpc.d.ts +1 -1
  39. package/dist/app/registry.js +7 -1
  40. package/dist/bin/agent-sandbox.js +2 -2
  41. package/dist/bin/{chunk-MY7NLQQY.js → chunk-2IMT7ZCT.js} +1 -1
  42. package/dist/bin/{chunk-N4GXYTLV.js → chunk-DK4NTBUF.js} +69 -26
  43. package/dist/bin/{chunk-FRPLQLSY.js → chunk-Z2P5OGKL.js} +4323 -3463
  44. package/dist/bin/claude-host.js +2 -2
  45. package/dist/bin/claude-sbx.js +2 -2
  46. package/dist/bin/codex-sbx.js +2 -2
  47. package/dist/bin/gemini-sbx.js +2 -2
  48. package/dist/bin/sandbox.js +2 -2
  49. package/dist/bin/sbx-hide.js +2 -2
  50. package/dist/bin/sbx-show.js +2 -2
  51. package/dist/cli/commands/agent-commands.d.ts +0 -1
  52. package/dist/cli/commands/agent-commands.js +2 -32
  53. package/dist/cli/commands/index.d.ts +1 -1
  54. package/dist/cli/commands/index.js +1 -1
  55. package/dist/cli/commands/launch/launch.js +2 -2
  56. package/dist/cli/commands/registry.js +3 -2
  57. package/dist/cli/manifest.d.ts +1 -1
  58. package/dist/cli/manifest.js +1 -1
  59. package/dist/cli/menu/menu.d.ts +1 -1
  60. package/dist/cli/menu/menu.js +3 -3
  61. package/dist/cli/menu/pages/agents-page.d.ts +7 -2
  62. package/dist/cli/menu/pages/agents-page.js +67 -11
  63. package/dist/cli/menu/pages/maintenance-page.js +1 -2
  64. package/dist/cli/menu/pages/settings-page.js +1 -0
  65. package/dist/cli/menu/views.js +2 -1
  66. package/dist/cli/services/agent-commands-service.d.ts +2 -0
  67. package/dist/cli/services/agent-commands-service.js +13 -7
  68. package/dist/cli/services/vpn-service.d.ts +5 -1
  69. package/dist/cli/services/vpn-service.js +6 -1
  70. package/dist/cli/tui/dialogs/text-screen.js +2 -1
  71. package/dist/cli/tui/terminal.js +3 -1
  72. package/dist/core/config/defaults.js +9 -0
  73. package/dist/core/detect/toolchains.js +9 -0
  74. package/dist/core/disk.d.ts +1 -5
  75. package/dist/core/disk.js +1 -16
  76. package/dist/core/schema/codex.d.ts +29 -0
  77. package/dist/core/schema/codex.js +24 -0
  78. package/dist/core/schema/config.d.ts +46 -14
  79. package/dist/core/schema/config.js +2 -0
  80. package/dist/core/schema/index.d.ts +1 -0
  81. package/dist/core/schema/index.js +1 -0
  82. package/dist/core/schema/toolchains.d.ts +4 -0
  83. package/dist/core/schema/toolchains.js +3 -0
  84. package/dist/core/types.d.ts +2 -0
  85. package/dist/guest/accounts/apply.d.ts +6 -6
  86. package/dist/guest/agents/agent-files.d.ts +26 -6
  87. package/dist/guest/agents/agent-files.js +84 -14
  88. package/dist/guest/agents/local-state.d.ts +5 -2
  89. package/dist/guest/agents/local-state.js +24 -10
  90. package/dist/guest/boot/boot.js +10 -5
  91. package/dist/guest/daemon/sessions.js +2 -1
  92. package/dist/guest/toolchains/installers.d.ts +6 -10
  93. package/dist/guest/toolchains/installers.js +4 -12
  94. package/dist/guest/toolchains/native/autosdk.d.ts +3 -0
  95. package/dist/guest/toolchains/native/autosdk.js +50 -0
  96. package/dist/guest/toolchains/native/unreal.js +3 -0
  97. package/dist/guest/toolchains/native/vcruntime.d.ts +0 -3
  98. package/dist/guest/toolchains/native/vcruntime.js +2 -5
  99. package/dist/guest/toolchains/native/visual-studio.d.ts +2 -2
  100. package/dist/guest/toolchains/native/visual-studio.js +100 -16
  101. package/dist/guest/toolchains/native/vs-request.d.ts +21 -0
  102. package/dist/guest/toolchains/native/vs-request.js +60 -1
  103. package/dist/guest/toolchains/not-yet.d.ts +1 -1
  104. package/dist/guest/toolchains/not-yet.js +3 -3
  105. package/dist/guest/toolchains/persisted.js +2 -7
  106. package/dist/guest/toolchains/plan.js +9 -4
  107. package/dist/guest/toolchains/signature.d.ts +5 -0
  108. package/dist/guest/toolchains/signature.js +22 -0
  109. package/dist/guest/vpn/tunnel.d.ts +3 -0
  110. package/dist/guest/vpn/tunnel.js +10 -12
  111. package/dist/guest/vpn/vpn.js +6 -1
  112. package/dist/host/accounts/leases.d.ts +3 -3
  113. package/dist/host/accounts/source.js +1 -1
  114. package/dist/host/agent-commands/commands.js +1 -1
  115. package/dist/host/agent-commands/forwarders.d.ts +14 -2
  116. package/dist/host/agent-commands/forwarders.js +31 -13
  117. package/dist/host/agent-commands/reconcile.d.ts +2 -1
  118. package/dist/host/agent-commands/reconcile.js +7 -8
  119. package/dist/host/agent-commands/user-path.d.ts +5 -0
  120. package/dist/host/agent-commands/user-path.js +13 -0
  121. package/dist/host/launch/session-file.js +1 -0
  122. package/dist/host/profile/accounts.js +2 -4
  123. package/dist/host/profile/claude-login.d.ts +3 -2
  124. package/dist/host/profile/claude-login.js +3 -13
  125. package/dist/host/vpn/index.d.ts +2 -1
  126. package/dist/host/vpn/index.js +2 -1
  127. package/dist/host/vpn/last.d.ts +28 -0
  128. package/dist/host/vpn/last.js +43 -0
  129. package/dist/host/vpn/login.d.ts +3 -1
  130. package/dist/host/vpn/login.js +17 -3
  131. package/dist/runtime/agent.js +28 -0
  132. package/dist/runtime/daemon.js +1117 -912
  133. package/dist/runtime/db.js +30 -0
  134. package/dist/runtime/main.js +1308 -1036
  135. package/dist/runtime/netproxy.js +22 -0
  136. package/dist/runtime/tool.js +238 -208
  137. package/dist/runtime/view.js +22 -0
  138. package/dist/selftest/agent-commands.test.js +45 -7
  139. package/dist/selftest/agent-paths.test.js +1 -12
  140. package/dist/selftest/agent-settings.test.d.ts +2 -0
  141. package/dist/selftest/agent-settings.test.js +94 -0
  142. package/dist/selftest/app.test.js +2 -1
  143. package/dist/selftest/cli.test.js +4 -0
  144. package/dist/selftest/index.js +2 -0
  145. package/dist/selftest/menu.test.js +5 -3
  146. package/dist/selftest/pc-installs.test.js +5 -0
  147. package/dist/selftest/persisted-installs.test.js +69 -0
  148. package/dist/selftest/service-accounts.test.js +1 -1
  149. package/dist/selftest/support/engine-golden.js +1 -1
  150. package/dist/selftest/support/fixtures.d.ts +9 -0
  151. package/dist/selftest/vpn.test.js +51 -3
  152. package/dist/shared/accounts/anthropic.d.ts +5 -0
  153. package/dist/shared/accounts/anthropic.js +15 -0
  154. package/dist/shared/accounts/index.d.ts +1 -0
  155. package/dist/shared/accounts/index.js +1 -0
  156. package/dist/shared/channel/schema.d.ts +19 -0
  157. package/dist/shared/channel/schema.js +2 -0
  158. package/dist/shared/io/disk.d.ts +9 -0
  159. package/dist/shared/io/disk.js +19 -0
  160. package/dist/shared/io/index.d.ts +1 -0
  161. package/dist/shared/io/index.js +1 -0
  162. package/dist/shared/readiness.d.ts +9 -0
  163. package/dist/shared/readiness.js +11 -0
  164. package/dist/shared/signer.d.ts +24 -0
  165. package/dist/shared/signer.js +15 -0
  166. package/dist/shared/vpn/failure.d.ts +10 -0
  167. package/dist/shared/vpn/failure.js +28 -0
  168. package/dist/sync/watcher.js +2 -0
  169. package/dist/web/index.html +1 -1
  170. package/dist/web/{main-GVZWWP4I.js → main-OUUQYNK4.js} +75 -74
  171. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  172. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  173. package/native/csharp/HostTools/Program.cs +14 -1
  174. package/native/csharp/HostTools/UserPath.cs +17 -0
  175. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  176. package/native/scripts/edit-user-path.ps1 +6 -2
  177. package/package.json +1 -1
  178. package/sandbox/scripts/get-signature.ps1 +13 -0
  179. package/schemas/config.schema.json +64 -0
  180. package/schemas/sandbox-config.schema.json +8 -0
  181. package/sandbox/scripts/get-signer.ps1 +0 -8
@@ -0,0 +1,22 @@
1
+ // A file's Authenticode signature (sandbox\scripts\get-signature.ps1) checked against the signer a download names.
2
+ import { z } from 'zod';
3
+ import { signerMatches } from '../../shared/signer.js';
4
+ import { scriptOutput } from '../runtime/process.js';
5
+ const SignatureSchema = z.object({ subject: z.string().max(4096), issuer: z.string().max(4096) });
6
+ /** The names on a file's valid signature, or null when it is unsigned, tampered with, or cannot be read. */
7
+ export function signatureOf(file) {
8
+ const text = scriptOutput('get-signature.ps1', ['-Path', file]).trim();
9
+ if (!text)
10
+ return null;
11
+ try {
12
+ const parsed = SignatureSchema.safeParse(JSON.parse(text));
13
+ return parsed.success ? parsed.data : null;
14
+ }
15
+ catch {
16
+ return null; // Not the script's JSON: treated as unsigned.
17
+ }
18
+ }
19
+ /** Whether a file's valid signature is the expected signer's. */
20
+ export function signedBy(file, signer) {
21
+ return signerMatches(signatureOf(file), signer);
22
+ }
@@ -1,6 +1,9 @@
1
+ import { type VpnFailureCode } from '../../shared/vpn/failure.js';
1
2
  export interface TunnelOutcome {
2
3
  readonly ok: boolean;
3
4
  readonly message: string;
5
+ /** Why it failed (when it did), for the host to act on. */
6
+ readonly code?: VpnFailureCode;
4
7
  /** The tunnel's own address (what the kill switch lets through). */
5
8
  readonly tunnelIp?: string;
6
9
  }
@@ -6,21 +6,14 @@ import { spawn } from 'node:child_process';
6
6
  import fs from 'node:fs';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
+ import { classifyFailure } from '../../shared/vpn/failure.js';
9
10
  import { tunnelAddressFromLog } from '../../shared/vpn/kill-switch.js';
10
11
  import { logsDir } from '../runtime/paths.js';
11
12
  import { log } from '../runtime/log.js';
12
13
  const DONE = 'Initialization Sequence Completed';
13
14
  /** Why the log says OpenVPN stopped, in a short plain sentence (or null when it does not say). */
14
15
  export function failureReason(text) {
15
- if (/AUTH_FAILED/i.test(text))
16
- return 'the VPN refused the login (for NordVPN use the service credentials, not your account password)';
17
- if (/TLS Error: TLS key negotiation failed|TLS handshake failed/i.test(text))
18
- return 'it could not reach the VPN server (the network blocks it, or the server is down)';
19
- if (/All wintun adapters on this system are in use|Cannot open TUN\/TAP|Could not create Wintun|ovpn-dco.*(error|failed)/i.test(text))
20
- return 'the network driver for the tunnel could not be used';
21
- if (/Exiting due to fatal error/i.test(text))
22
- return 'OpenVPN stopped with an error';
23
- return null;
16
+ return classifyFailure(text)?.reason ?? null;
24
17
  }
25
18
  const tail = (text, lines = 12) => text.trim().split(/\r?\n/).slice(-lines).join('\n');
26
19
  /** Connects. Never leaves the login on disk; kills OpenVPN when the tunnel does not come up in time. */
@@ -57,17 +50,22 @@ export async function startTunnel(o) {
57
50
  const tunnelIp = tunnelAddressFromLog(text) ?? undefined;
58
51
  return { ok: true, message: 'the tunnel is up', ...(tunnelIp ? { tunnelIp } : {}) };
59
52
  }
60
- const reason = failureReason(text);
53
+ const failure = classifyFailure(text);
54
+ const reason = failure?.reason ?? null;
61
55
  if (exited !== null || reason) {
62
56
  if (exited === null)
63
57
  child.kill();
64
58
  keepLog();
65
- return { ok: false, message: (reason ?? 'OpenVPN stopped') + '. ' + tail(text, 4) };
59
+ return {
60
+ ok: false,
61
+ code: failure?.code ?? 'other',
62
+ message: (reason ?? 'OpenVPN stopped') + '. ' + tail(text, 4),
63
+ };
66
64
  }
67
65
  if (Date.now() > deadline) {
68
66
  child.kill();
69
67
  keepLog();
70
- return { ok: false, message: 'the tunnel did not come up in time. ' + tail(text, 4) };
68
+ return { ok: false, code: 'timeout', message: 'the tunnel did not come up in time. ' + tail(text, 4) };
71
69
  }
72
70
  await new Promise((r) => setTimeout(r, 500));
73
71
  }
@@ -123,6 +123,7 @@ export async function setupVpn(secrets, ptyHostExe) {
123
123
  warn('VPN: the kill switch did not hold: a direct connection still got out. If the tunnel drops, traffic leaves through your normal connection.');
124
124
  };
125
125
  setVpnStatus('starting', 'Starting the VPN');
126
+ let failureCode;
126
127
  try {
127
128
  if (!session.networking)
128
129
  throw new Error('networking is off for this sandbox');
@@ -178,6 +179,7 @@ export async function setupVpn(secrets, ptyHostExe) {
178
179
  if (!result.ok) {
179
180
  if (wireguardExe)
180
181
  await removeWireguardTunnel(wireguardExe);
182
+ failureCode = result.code;
181
183
  throw new Error(result.message);
182
184
  }
183
185
  if (!result.tunnelIp || !hasAdapter(result.tunnelIp))
@@ -207,7 +209,10 @@ export async function setupVpn(secrets, ptyHostExe) {
207
209
  ? ' The kill switch is on, so this sandbox has NO internet access.'
208
210
  : ' The kill switch could NOT close the network, so this sandbox is ONLINE through your normal connection, without the VPN.';
209
211
  warn('VPN failed: ' + reason + after);
210
- setVpnStatus('failed', reason, killSwitch ? { killSwitchHeld: held } : {});
212
+ setVpnStatus('failed', reason, {
213
+ code: failureCode ?? 'other',
214
+ ...(killSwitch ? { killSwitchHeld: held } : {}),
215
+ });
211
216
  return false;
212
217
  }
213
218
  }
@@ -3,13 +3,13 @@ import { type ServiceKind } from '../../shared/accounts/index.js';
3
3
  declare const LeaseSchema: z.ZodObject<{
4
4
  kind: z.ZodEnum<{
5
5
  unity: "unity";
6
- steam: "steam";
7
- epic: "epic";
8
- cfx: "cfx";
9
6
  keystore: "keystore";
10
7
  play: "play";
11
8
  expo: "expo";
12
9
  appstore: "appstore";
10
+ cfx: "cfx";
11
+ steam: "steam";
12
+ epic: "epic";
13
13
  }>;
14
14
  dataSet: z.ZodString;
15
15
  launchId: z.ZodString;
@@ -11,5 +11,5 @@ export function pcEnv() {
11
11
  }
12
12
  /** What a kind whose host side is not written yet says. */
13
13
  export function notAvailableYet(kind) {
14
- return fail('Saving a ' + SERVICE_SPECS[kind].label + ' sign-in is not available yet.', 'It comes in a later version of the sandbox.');
14
+ return fail('Saving a ' + SERVICE_SPECS[kind].label + ' sign-in comes in a later version of the sandbox.', 'Nothing was changed.');
15
15
  }
@@ -1,4 +1,4 @@
1
- // (Beta) Agent commands: with agentCommands.enabled, typing an agent's own command on this PC (claude, codex, ...)
1
+ // Agent commands: with agentCommands.enabled, typing an agent's own command on this PC (claude, codex, ...)
2
2
  // starts it in a hidden sandbox for the folder you are in and attaches that terminal. Small forwarding files in
3
3
  // <storage>\agent-commands go first on YOUR PATH (so they win over npm's and the installers' copies); turning it off
4
4
  // removes the files and that one PATH entry. SANDBOX_AGENT_DIRECT=1 runs the real agent on this PC instead.
@@ -1,7 +1,19 @@
1
1
  import type { AgentCommand } from './commands.js';
2
2
  /** Marks files this tool wrote, so only those are ever replaced or removed. */
3
3
  export declare const MARKER = "windows-development-sandbox agent command";
4
+ /** What a forwarding file runs: the managed install's launcher when there is one, else node + the entry file. */
5
+ export interface ForwardTarget {
6
+ readonly node: string;
7
+ readonly entry: string;
8
+ /** The managed install's `bin\sandbox.cmd`: it survives updates, which remove the version folder `entry` is in. */
9
+ readonly launcher?: string;
10
+ }
11
+ /**
12
+ * Where the forwarding files send the typed command. Under a managed install (installRoot, the folder with `bin`,
13
+ * `versions` and `node`) that is its launcher; otherwise the entry file of this copy.
14
+ */
15
+ export declare function forwardTarget(packageRoot: string, installRoot: string | null, node: string): ForwardTarget;
4
16
  /** The .cmd file for one agent (cmd, PowerShell and Windows Terminal all run it). */
5
- export declare function forwardingCmd(a: AgentCommand, node: string, entry: string): string;
17
+ export declare function forwardingCmd(a: AgentCommand, target: ForwardTarget): string;
6
18
  /** The same for Git Bash, which looks for a file without an extension. */
7
- export declare function forwardingSh(a: AgentCommand, node: string, entry: string): string;
19
+ export declare function forwardingSh(a: AgentCommand, target: ForwardTarget): string;
@@ -1,12 +1,30 @@
1
+ import path from 'node:path';
1
2
  /** Marks files this tool wrote, so only those are ever replaced or removed. */
2
3
  export const MARKER = 'windows-development-sandbox agent command';
4
+ /**
5
+ * Where the forwarding files send the typed command. Under a managed install (installRoot, the folder with `bin`,
6
+ * `versions` and `node`) that is its launcher; otherwise the entry file of this copy.
7
+ */
8
+ export function forwardTarget(packageRoot, installRoot, node) {
9
+ const entry = path.join(packageRoot, 'dist', 'bin', 'sandbox.js');
10
+ return installRoot ? { node, entry, launcher: path.join(installRoot, 'bin', 'sandbox.cmd') } : { node, entry };
11
+ }
12
+ const posix = (p) => p.replace(/^([A-Za-z]):/, (_m, d) => '/' + d.toLowerCase()).replace(/\\/g, '/');
3
13
  /** The .cmd file for one agent (cmd, PowerShell and Windows Terminal all run it). */
4
- export function forwardingCmd(a, node, entry) {
14
+ export function forwardingCmd(a, target) {
15
+ const launch = ' launch --harness ' + a.harness + ' --headless --terminal -- %*';
5
16
  return [
6
17
  '@echo off',
7
- 'rem ' + MARKER + ' (Beta). Turn off: sandbox agent-commands off. Run the real one: set SANDBOX_AGENT_DIRECT=1',
18
+ 'rem ' + MARKER + '. Turn off: sandbox agent-commands off. Run the real one: set SANDBOX_AGENT_DIRECT=1',
8
19
  'if "%SANDBOX_AGENT_DIRECT%"=="1" goto direct',
9
- '"' + node + '" "' + entry + '" launch --harness ' + a.harness + ' --headless --terminal -- %*',
20
+ // A missing program (the tool was uninstalled or moved) runs the real agent instead of failing.
21
+ ...(target.launcher
22
+ ? ['if not exist "' + target.launcher + '" goto direct', 'call "' + target.launcher + '"' + launch]
23
+ : [
24
+ 'if not exist "' + target.node + '" goto direct',
25
+ 'if not exist "' + target.entry + '" goto direct',
26
+ '"' + target.node + '" "' + target.entry + '"' + launch,
27
+ ]),
10
28
  'exit /b %errorlevel%',
11
29
  ':direct',
12
30
  'call "' + a.real + '" %*',
@@ -14,20 +32,20 @@ export function forwardingCmd(a, node, entry) {
14
32
  ].join('\r\n');
15
33
  }
16
34
  /** The same for Git Bash, which looks for a file without an extension. */
17
- export function forwardingSh(a, node, entry) {
18
- const posix = (p) => p.replace(/^([A-Za-z]):/, (_m, d) => '/' + d.toLowerCase()).replace(/\\/g, '/');
35
+ export function forwardingSh(a, target) {
19
36
  const q = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
37
+ const launch = ' launch --harness ' + a.harness + ' --headless --terminal -- "$@"';
38
+ const shLauncher = target.launcher ? posix(target.launcher).replace(/\.cmd$/i, '') : null;
20
39
  return [
21
40
  '#!/bin/sh',
22
- '# ' + MARKER + ' (Beta). Turn off: sandbox agent-commands off. Run the real one: SANDBOX_AGENT_DIRECT=1',
41
+ '# ' + MARKER + '. Turn off: sandbox agent-commands off. Run the real one: SANDBOX_AGENT_DIRECT=1',
23
42
  'if [ "$SANDBOX_AGENT_DIRECT" = "1" ]; then exec ' + q(posix(a.real)) + ' "$@"; fi',
24
- 'exec ' +
25
- q(posix(node)) +
26
- ' ' +
27
- q(posix(entry)) +
28
- ' launch --harness ' +
29
- a.harness +
30
- ' --headless --terminal -- "$@"',
43
+ ...(shLauncher
44
+ ? ['[ -f ' + q(shLauncher) + ' ] || exec ' + q(posix(a.real)) + ' "$@"', 'exec ' + q(shLauncher) + launch]
45
+ : [
46
+ '[ -f ' + q(posix(target.entry)) + ' ] || exec ' + q(posix(a.real)) + ' "$@"',
47
+ 'exec ' + q(posix(target.node)) + ' ' + q(posix(target.entry)) + launch,
48
+ ]),
31
49
  '',
32
50
  ].join('\n');
33
51
  }
@@ -1,4 +1,5 @@
1
1
  import type { Config } from '../../core/types.js';
2
+ import { type ForwardTarget } from './forwarders.js';
2
3
  import { type AgentCommand } from './commands.js';
3
4
  export interface Reconciled {
4
5
  on: boolean;
@@ -9,4 +10,4 @@ export interface Reconciled {
9
10
  * Makes this PC match agentCommands: on = a forwarding file per installed agent and the folder first on your PATH;
10
11
  * off = no files, no PATH entry. Only touches files it wrote and its own PATH entry; does nothing when already right.
11
12
  */
12
- export declare function reconcileAgentCommands(config: Config, storageRoot: string, node: string, entry: string): Reconciled;
13
+ export declare function reconcileAgentCommands(config: Config, storageRoot: string, target: ForwardTarget): Reconciled;
@@ -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
+ }
@@ -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,
@@ -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);
@@ -1,4 +1,5 @@
1
- export { VpnLogin, loginProblem, type LoginState, type VpnCredentials } from './login.js';
1
+ export { VpnLogin, loginProblem, loginWarning, type LoginState, type VpnCredentials } from './login.js';
2
2
  export { fetchBounded, findCountry, nordProfile, recommendedServer, type Fetcher, type NordServer } from './nordvpn.js';
3
3
  export { prepareVpn, vpnNotes, wireguardFileProblem, type PrepareOptions } from './prepare.js';
4
4
  export { hostPublicIp, readVpnLine, vpnLine, type VpnLine } from './status.js';
5
+ export { readLastFailure, recordLastVpn, type LastVpn, type VpnFailureInfo } from './last.js';
@@ -1,4 +1,5 @@
1
- export { VpnLogin, loginProblem } from './login.js';
1
+ export { VpnLogin, loginProblem, loginWarning } from './login.js';
2
2
  export { fetchBounded, findCountry, nordProfile, recommendedServer } from './nordvpn.js';
3
3
  export { prepareVpn, vpnNotes, wireguardFileProblem } from './prepare.js';
4
4
  export { hostPublicIp, readVpnLine, vpnLine } from './status.js';
5
+ export { readLastFailure, recordLastVpn } from './last.js';
@@ -0,0 +1,28 @@
1
+ import { z } from 'zod';
2
+ declare const LastSchema: z.ZodObject<{
3
+ state: z.ZodEnum<{
4
+ failed: "failed";
5
+ up: "up";
6
+ }>;
7
+ code: z.ZodOptional<z.ZodEnum<{
8
+ auth: "auth";
9
+ unreachable: "unreachable";
10
+ driver: "driver";
11
+ timeout: "timeout";
12
+ other: "other";
13
+ }>>;
14
+ message: z.ZodString;
15
+ at: z.ZodString;
16
+ }, z.core.$strip>;
17
+ export type LastVpn = z.output<typeof LastSchema>;
18
+ /** What the page shows for a failure: the plain sentence, never the raw log. */
19
+ export interface VpnFailureInfo {
20
+ readonly code: NonNullable<LastVpn['code']>;
21
+ readonly text: string;
22
+ readonly at: string;
23
+ }
24
+ /** Copies the sandbox's final VPN state to host storage (nothing when the VPN was off). Never throws. */
25
+ export declare function recordLastVpn(storageRoot: string, dataPath: string): void;
26
+ /** The last attempt, when it failed (null when it worked or none is known). */
27
+ export declare function readLastFailure(storageRoot: string): VpnFailureInfo | null;
28
+ export {};
@@ -0,0 +1,43 @@
1
+ // The last VPN result of a sandbox, kept in host storage (<storage>\vpn\last.json) when the sandbox closes, so "the
2
+ // login was refused" can still be shown after the sandbox is gone. Read from the sandbox's run\vpn.json (validated),
3
+ // never from anything else the guest wrote; the text is made safe before it is kept.
4
+ import path from 'node:path';
5
+ import { z } from 'zod';
6
+ import { RunDir } from '../../shared/channel/index.js';
7
+ import { JsonFile } from '../../shared/io/index.js';
8
+ import { FAILURE_TEXT } from '../../shared/vpn/failure.js';
9
+ import { safeText } from '../../core/util/index.js';
10
+ const LastSchema = z.object({
11
+ state: z.enum(['up', 'failed']),
12
+ code: z.enum(['auth', 'unreachable', 'driver', 'timeout', 'other']).optional(),
13
+ message: z.string().max(400),
14
+ at: z.string().max(64),
15
+ });
16
+ function lastFile(storageRoot) {
17
+ return new JsonFile(LastSchema, path.join(storageRoot, 'vpn', 'last.json'));
18
+ }
19
+ /** Copies the sandbox's final VPN state to host storage (nothing when the VPN was off). Never throws. */
20
+ export function recordLastVpn(storageRoot, dataPath) {
21
+ try {
22
+ const status = new RunDir(dataPath).readVpnStatus();
23
+ if (!status || (status.state !== 'up' && status.state !== 'failed'))
24
+ return;
25
+ lastFile(storageRoot).write({
26
+ state: status.state,
27
+ ...(status.code ? { code: status.code } : {}),
28
+ message: safeText(status.message).slice(0, 400),
29
+ at: status.updatedAt,
30
+ });
31
+ }
32
+ catch {
33
+ // Only a convenience for the page: a failure here must not hold the final sync up.
34
+ }
35
+ }
36
+ /** The last attempt, when it failed (null when it worked or none is known). */
37
+ export function readLastFailure(storageRoot) {
38
+ const last = lastFile(storageRoot).read();
39
+ if (!last || last.state !== 'failed')
40
+ return null;
41
+ const code = last.code ?? 'other';
42
+ return { code, text: FAILURE_TEXT[code], at: last.at };
43
+ }
@@ -7,7 +7,9 @@ export interface VpnCredentials {
7
7
  /** What `sandbox vpn status` may say about the login: never the values. */
8
8
  export type LoginState = 'none' | 'saved' | 'unreadable';
9
9
  /** The login must go on two lines of a file for OpenVPN, so no line breaks or other control characters. */
10
- export declare function loginProblem(c: VpnCredentials): string | null;
10
+ export declare function loginProblem(c: VpnCredentials, provider?: VpnProvider): string | null;
11
+ /** A hint (not a refusal) when a NordVPN login does not look like service credentials (letters and digits, ~24). */
12
+ export declare function loginWarning(c: VpnCredentials, provider: VpnProvider): string | null;
11
13
  export declare class VpnLogin {
12
14
  private readonly storageRoot;
13
15
  private readonly secrets;
@@ -14,16 +14,30 @@ const SavedSchema = z.object({
14
14
  sealed: z.string().max(10000),
15
15
  });
16
16
  /** The login must go on two lines of a file for OpenVPN, so no line breaks or other control characters. */
17
- export function loginProblem(c) {
17
+ export function loginProblem(c, provider) {
18
18
  const clean = (v) => !/[\u0000-\u001f\u007f]/.test(v);
19
19
  if (!c.username.trim() || !c.password)
20
20
  return 'Both the user name and the password are needed.';
21
21
  if (!clean(c.username) || !clean(c.password))
22
22
  return 'The login may not contain line breaks or control characters.';
23
+ // The markers a terminal puts around pasted text ([200~ ... [201~) are not part of any login.
24
+ if (/\[20[01]~/.test(c.username + c.password))
25
+ return 'The login has hidden paste characters in it (the terminal added them when you pasted).';
23
26
  if (c.username.length > 256 || c.password.length > 1024)
24
27
  return 'The user name or password is too long.';
28
+ if (provider === 'nordvpn' && /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(c.username.trim()))
29
+ return 'That looks like your NordVPN account e-mail. Use the service credentials: Nord Account > NordVPN > Manual setup.';
25
30
  return null;
26
31
  }
32
+ /** A hint (not a refusal) when a NordVPN login does not look like service credentials (letters and digits, ~24). */
33
+ export function loginWarning(c, provider) {
34
+ if (provider !== 'nordvpn')
35
+ return null;
36
+ const ok = (v) => /^[A-Za-z0-9]{16,64}$/.test(v.trim());
37
+ return ok(c.username) && ok(c.password)
38
+ ? null
39
+ : 'NordVPN service credentials are letters and digits only (about 24 each). If the VPN says the login was refused, copy them again from Nord Account > NordVPN > Manual setup.';
40
+ }
27
41
  export class VpnLogin {
28
42
  storageRoot;
29
43
  secrets;
@@ -36,13 +50,13 @@ export class VpnLogin {
36
50
  }
37
51
  /** Encrypts and saves the login (replacing an earlier one). */
38
52
  save(provider, login) {
39
- const problem = loginProblem(login);
53
+ const problem = loginProblem({ username: login.username.trim(), password: login.password.trim() }, provider);
40
54
  if (problem)
41
55
  fail(problem, 'Type the service credentials again.');
42
56
  this.file(provider).write({
43
57
  provider,
44
58
  savedAt: new Date().toISOString(),
45
- sealed: this.secrets.sealJson({ username: login.username.trim(), password: login.password }),
59
+ sealed: this.secrets.sealJson({ username: login.username.trim(), password: login.password.trim() }),
46
60
  });
47
61
  }
48
62
  /** The login, decrypted. Only the launch calls this. */
@@ -25351,6 +25351,22 @@ var DatabasesSchema = external_exports.strictObject({
25351
25351
  })
25352
25352
  });
25353
25353
 
25354
+ // src/core/schema/codex.ts
25355
+ var CodexSchema = external_exports.strictObject({
25356
+ sandboxMode: external_exports.enum(["danger-full-access", "workspace-write", "read-only"]).describe(
25357
+ "Codex's own sandbox. Inside Windows Sandbox Codex's own sandbox only slows it down (it re-grants file permissions over your profile at every boot), so the default is danger-full-access: Windows Sandbox is the sandbox."
25358
+ ),
25359
+ approvalPolicy: external_exports.enum(["never", "on-request", "on-failure", "untrusted"]).describe('When Codex asks before it runs a command. "never" with danger-full-access starts it with no questions.'),
25360
+ model: external_exports.string().max(100).regex(/^[A-Za-z0-9._:/-]*$/, "letters, digits and . _ : / - only").describe("The model Codex uses. Empty = Codex's own default."),
25361
+ reasoningEffort: external_exports.enum(["default", "minimal", "low", "medium", "high", "xhigh"]).describe(`How hard Codex thinks. "default" = Codex's own default.`),
25362
+ windowsSandbox: external_exports.enum(["off", "unelevated", "elevated"]).describe(
25363
+ `Codex's Windows sandbox mode in its config.toml. "off" removes the setting so Codex does not set up its sandbox users and firewall rules at every boot.`
25364
+ ),
25365
+ mcpLoginsInFile: external_exports.boolean().describe(
25366
+ `Keep the sign-ins of Codex's MCP servers in a file in the saved Codex folder (mcp_oauth_credentials_store = "file"), so they survive between sandboxes. A value you set in config.toml is kept.`
25367
+ )
25368
+ });
25369
+
25354
25370
  // src/core/schema/harness.ts
25355
25371
  var SubfolderSchema = external_exports.string().regex(/^(?!.*(^|[\\/])\.\.([\\/]|$))[A-Za-z0-9._ -]+([\\/][A-Za-z0-9._ -]+)*$/, "a plain relative folder");
25356
25372
  var HarnessHomeSchema = external_exports.strictObject({
@@ -25663,6 +25679,9 @@ var ToolchainsSchema = external_exports.strictObject({
25663
25679
  keepInstallFiles: external_exports.boolean().describe(
25664
25680
  "Keep an offline copy of the Visual Studio install files in tools\\visualstudio (~10 GB, made once), so later boots install without downloading them again. Skipped while that disk has too little room."
25665
25681
  ),
25682
+ visualStudio: external_exports.enum(["full", "autosdk"]).describe(
25683
+ `"full": install Visual Studio Community (the engine finds it through the Setup API; ~10 GB, redone every boot). "autosdk" (experimental): skip Visual Studio and build an Unreal AutoSDK tree from the portable MSVC in tools\\unreal\\autosdk (kept between sandboxes); UnrealBuildTool finds MSVC through AutoSDK, but the editor's Visual Studio integration is not available. Needs toolchains.msvc (turned on for you).`
25684
+ ),
25666
25685
  persistEngines: external_exports.boolean().describe(
25667
25686
  "Engines the Epic Games Launcher installs go to tools\\unreal\\engines (kept between sandboxes, 40-60 GB each) and are registered with the launcher again every boot. Off: they go on the sandbox disk and are lost when it closes. Engines installed on this PC can be mapped instead (pcInstalls.unrealEngines)."
25668
25687
  ),
@@ -27119,6 +27138,7 @@ var ConfigSchema = external_exports.strictObject({
27119
27138
  apps: AppsSchema,
27120
27139
  claudeProfile: ClaudeProfileSchema,
27121
27140
  harnesses: external_exports.record(NameSchema, HarnessSchema).describe("Agents that can run in the sandbox."),
27141
+ codex: CodexSchema,
27122
27142
  hooks: external_exports.strictObject({ failFast: external_exports.boolean().describe("Stop when a hook script fails.") }),
27123
27143
  projects: ProjectsSchema.describe(
27124
27144
  "Several projects in one sandbox: folders of projects mapped at every start, and adding folders while it runs."
@@ -27362,6 +27382,8 @@ var ProjectStatusSchema = external_exports.object({
27362
27382
  var VpnStatusSchema = external_exports.object({
27363
27383
  state: external_exports.enum(["off", "starting", "up", "failed"]),
27364
27384
  message: Text(2e3),
27385
+ /** Why it failed, when it did: `auth` (login refused), `unreachable`, `driver`, `timeout` or `other`. */
27386
+ code: external_exports.enum(["auth", "unreachable", "driver", "timeout", "other"]).optional(),
27365
27387
  killSwitch: external_exports.boolean(),
27366
27388
  /** A direct connection was tried after the kill switch was applied: false = it still got out. */
27367
27389
  killSwitchHeld: external_exports.boolean().optional(),
@@ -28056,6 +28078,12 @@ function agentPaths(toolsDir2, name, harness) {
28056
28078
  function agentCommand(name, harness) {
28057
28079
  return harness.npmBin ?? harness.uvBin ?? (harness.hostExecutable ?? name).replace(/\.exe$/i, "");
28058
28080
  }
28081
+ var CODEX_MARKER = "# Set by the sandbox";
28082
+ var MAX_CODEX_CONFIG_BYTES = 1024 * 1024;
28083
+ var MARKED_LINES = new RegExp(
28084
+ "^" + CODEX_MARKER + "[^\r\n]*\r?\n(?:(?:sandbox_mode|approval_policy)[ ]*=[^\r\n]*\r?\n)*(?:[ ]*\r?\n)?",
28085
+ "m"
28086
+ );
28059
28087
 
28060
28088
  // src/guest/setup/path-plan.ts
28061
28089
  import path16 from "node:path";