windows-development-sandbox 1.1.0 → 1.4.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 (222) hide show
  1. package/.sandbox-config.example.json +2 -1
  2. package/README.md +3 -2
  3. package/config.example.json +39 -5
  4. package/dist/cli/args.d.ts +3 -0
  5. package/dist/cli/args.js +3 -1
  6. package/dist/cli/boot-progress.d.ts +22 -0
  7. package/dist/cli/boot-progress.js +59 -0
  8. package/dist/cli/commands/accounts.js +2 -2
  9. package/dist/cli/commands/agent-commands.js +1 -1
  10. package/dist/cli/commands/context.js +2 -1
  11. package/dist/cli/commands/data/clean.js +6 -1
  12. package/dist/cli/commands/data/collect.js +3 -3
  13. package/dist/cli/commands/index.d.ts +1 -1
  14. package/dist/cli/commands/index.js +1 -1
  15. package/dist/cli/commands/launch.d.ts +5 -2
  16. package/dist/cli/commands/launch.js +113 -25
  17. package/dist/cli/commands/maintenance.d.ts +2 -0
  18. package/dist/cli/commands/maintenance.js +70 -4
  19. package/dist/cli/commands/project.d.ts +10 -6
  20. package/dist/cli/commands/project.js +42 -3
  21. package/dist/cli/commands/restart.js +17 -7
  22. package/dist/cli/commands/sessions.js +5 -4
  23. package/dist/cli/commands/user-config.js +1 -1
  24. package/dist/cli/index.js +6 -4
  25. package/dist/cli/manifest.js +6 -1
  26. package/dist/cli/menu/apps.d.ts +21 -0
  27. package/dist/cli/menu/apps.js +122 -0
  28. package/dist/cli/menu/command-form.d.ts +28 -0
  29. package/dist/cli/menu/command-form.js +219 -0
  30. package/dist/cli/menu/index.d.ts +26 -0
  31. package/dist/cli/menu/index.js +180 -0
  32. package/dist/cli/menu/settings/categories.d.ts +9 -0
  33. package/dist/cli/menu/settings/categories.js +28 -0
  34. package/dist/cli/menu/settings/config-file.d.ts +40 -0
  35. package/dist/cli/menu/settings/config-file.js +71 -0
  36. package/dist/cli/menu/settings/editors.d.ts +30 -0
  37. package/dist/cli/menu/settings/editors.js +305 -0
  38. package/dist/cli/menu/settings/index.d.ts +32 -0
  39. package/dist/cli/menu/settings/index.js +136 -0
  40. package/dist/cli/menu/settings/schema-node.d.ts +62 -0
  41. package/dist/cli/menu/settings/schema-node.js +104 -0
  42. package/dist/cli/{menu-multi.d.ts → menu/several.d.ts} +10 -2
  43. package/dist/cli/menu/several.js +102 -0
  44. package/dist/cli/progress.d.ts +15 -0
  45. package/dist/cli/progress.js +52 -0
  46. package/dist/cli/tui/app.d.ts +39 -0
  47. package/dist/cli/tui/app.js +52 -0
  48. package/dist/cli/tui/dialogs.d.ts +57 -0
  49. package/dist/cli/tui/dialogs.js +162 -0
  50. package/dist/cli/tui/frame.d.ts +18 -0
  51. package/dist/cli/tui/frame.js +27 -0
  52. package/dist/cli/tui/index.d.ts +7 -0
  53. package/dist/cli/tui/index.js +8 -0
  54. package/dist/cli/tui/list-screen.d.ts +101 -0
  55. package/dist/cli/tui/list-screen.js +299 -0
  56. package/dist/cli/tui/terminal.d.ts +37 -0
  57. package/dist/cli/tui/terminal.js +89 -0
  58. package/dist/cli/tui/text.d.ts +20 -0
  59. package/dist/cli/tui/text.js +105 -0
  60. package/dist/cli/tui/theme.d.ts +45 -0
  61. package/dist/cli/tui/theme.js +30 -0
  62. package/dist/core/config.js +10 -7
  63. package/dist/core/local-config.d.ts +31 -1
  64. package/dist/core/local-config.js +72 -7
  65. package/dist/core/project-policy.d.ts +50 -3
  66. package/dist/core/project-policy.js +90 -4
  67. package/dist/core/schema/apps.d.ts +40 -0
  68. package/dist/core/schema/apps.js +27 -0
  69. package/dist/core/schema/config.d.ts +69 -3
  70. package/dist/core/schema/config.js +18 -6
  71. package/dist/core/schema/profile.d.ts +5 -0
  72. package/dist/core/schema/profile.js +4 -2
  73. package/dist/core/schema/projects.d.ts +1 -0
  74. package/dist/core/schema/projects.js +1 -0
  75. package/dist/core/schema/sandbox.d.ts +29 -1
  76. package/dist/core/schema/sandbox.js +29 -1
  77. package/dist/core/schema/toolchains.d.ts +3 -0
  78. package/dist/core/schema/toolchains.js +6 -1
  79. package/dist/core/types.d.ts +24 -1
  80. package/dist/core/util.d.ts +2 -0
  81. package/dist/core/util.js +2 -0
  82. package/dist/core/workspace.d.ts +21 -4
  83. package/dist/core/workspace.js +99 -21
  84. package/dist/guest/apps/index.d.ts +10 -0
  85. package/dist/guest/apps/index.js +206 -0
  86. package/dist/guest/apps/resolvers.d.ts +8 -0
  87. package/dist/guest/apps/resolvers.js +90 -0
  88. package/dist/guest/audit-parse.d.ts +7 -0
  89. package/dist/guest/audit-parse.js +24 -0
  90. package/dist/guest/audit.d.ts +4 -0
  91. package/dist/guest/audit.js +47 -0
  92. package/dist/guest/daemon/relays.d.ts +1 -0
  93. package/dist/guest/daemon/relays.js +18 -0
  94. package/dist/guest/daemon.js +4 -0
  95. package/dist/guest/env.d.ts +7 -2
  96. package/dist/guest/env.js +17 -3
  97. package/dist/guest/helpers.d.ts +1 -1
  98. package/dist/guest/main.js +20 -3
  99. package/dist/guest/netproxy.d.ts +1 -0
  100. package/dist/guest/netproxy.js +100 -0
  101. package/dist/guest/ptyhost.js +14 -4
  102. package/dist/guest/setup/claude.d.ts +0 -4
  103. package/dist/guest/setup/claude.js +48 -2
  104. package/dist/guest/setup/network.d.ts +9 -0
  105. package/dist/guest/setup/network.js +97 -4
  106. package/dist/guest/setup/user.js +4 -0
  107. package/dist/guest/toolchains/index.js +29 -3
  108. package/dist/guest/toolchains/native/mesa.js +13 -3
  109. package/dist/guest/toolchains/plan.d.ts +3 -1
  110. package/dist/guest/toolchains/plan.js +53 -9
  111. package/dist/guest/toolchains/python.js +26 -2
  112. package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
  113. package/dist/guest/toolchains/vscode-profile.js +69 -0
  114. package/dist/guest/toolchains/vscode.d.ts +17 -0
  115. package/dist/guest/toolchains/vscode.js +87 -1
  116. package/dist/host/attach.js +2 -1
  117. package/dist/host/audit/collect.d.ts +34 -0
  118. package/dist/host/audit/collect.js +126 -0
  119. package/dist/host/audit/log.d.ts +46 -0
  120. package/dist/host/audit/log.js +143 -0
  121. package/dist/host/channel.d.ts +2 -2
  122. package/dist/host/channel.js +3 -2
  123. package/dist/host/generate.d.ts +7 -1
  124. package/dist/host/generate.js +23 -6
  125. package/dist/host/join.d.ts +7 -0
  126. package/dist/host/join.js +23 -9
  127. package/dist/host/launch/mappings.d.ts +7 -2
  128. package/dist/host/launch/mappings.js +68 -18
  129. package/dist/host/launch/projects.d.ts +2 -0
  130. package/dist/host/launch/projects.js +11 -5
  131. package/dist/host/launch/stage.js +1 -1
  132. package/dist/host/motw.d.ts +17 -0
  133. package/dist/host/motw.js +74 -0
  134. package/dist/host/platform.js +3 -0
  135. package/dist/host/port-forward.d.ts +8 -0
  136. package/dist/host/port-forward.js +37 -0
  137. package/dist/host/projects.d.ts +25 -2
  138. package/dist/host/projects.js +54 -5
  139. package/dist/host/repo-guard.d.ts +10 -3
  140. package/dist/host/repo-guard.js +142 -14
  141. package/dist/host/stops.d.ts +67 -0
  142. package/dist/host/stops.js +126 -0
  143. package/dist/host/terminal.d.ts +8 -0
  144. package/dist/host/terminal.js +45 -1
  145. package/dist/host/workspace-view/index.d.ts +20 -0
  146. package/dist/host/workspace-view/index.js +53 -0
  147. package/dist/host/workspace-view/rules.d.ts +26 -0
  148. package/dist/host/workspace-view/rules.js +93 -0
  149. package/dist/host/workspace-view/view.d.ts +50 -0
  150. package/dist/host/workspace-view/view.js +281 -0
  151. package/dist/runtime/daemon.js +1387 -558
  152. package/dist/runtime/db.js +199 -14
  153. package/dist/runtime/main.js +3082 -631
  154. package/dist/runtime/netproxy.js +25948 -0
  155. package/dist/runtime/view.js +531 -148
  156. package/dist/selftest/audit.test.d.ts +2 -0
  157. package/dist/selftest/audit.test.js +57 -0
  158. package/dist/selftest/channel.test.js +45 -2
  159. package/dist/selftest/cli.test.js +47 -17
  160. package/dist/selftest/fixtures.d.ts +47 -1
  161. package/dist/selftest/fixtures.js +3 -1
  162. package/dist/selftest/generate.test.js +7 -2
  163. package/dist/selftest/index.js +6 -2
  164. package/dist/selftest/live-network.test.d.ts +2 -0
  165. package/dist/selftest/live-network.test.js +63 -0
  166. package/dist/selftest/live-terminal.test.d.ts +2 -0
  167. package/dist/selftest/live-terminal.test.js +169 -0
  168. package/dist/selftest/local-config.test.js +42 -4
  169. package/dist/selftest/mirror.test.js +36 -7
  170. package/dist/selftest/network.test.js +12 -0
  171. package/dist/selftest/projects.test.js +77 -8
  172. package/dist/selftest/repo-guard.test.js +59 -1
  173. package/dist/selftest/sync.test.js +50 -10
  174. package/dist/selftest/toolchains.test.js +30 -0
  175. package/dist/selftest/workspace-view.test.d.ts +2 -0
  176. package/dist/selftest/workspace-view.test.js +89 -0
  177. package/dist/shared/apps.d.ts +49 -0
  178. package/dist/shared/apps.js +114 -0
  179. package/dist/shared/console-input.d.ts +11 -0
  180. package/dist/shared/console-input.js +61 -0
  181. package/dist/shared/download.d.ts +3 -0
  182. package/dist/shared/download.js +25 -2
  183. package/dist/shared/guest-paths.d.ts +27 -0
  184. package/dist/shared/guest-paths.js +68 -0
  185. package/dist/shared/netpolicy.d.ts +27 -0
  186. package/dist/shared/netpolicy.js +51 -0
  187. package/dist/shared/projects.d.ts +27 -4
  188. package/dist/shared/projects.js +72 -13
  189. package/dist/shared/pty-client.d.ts +26 -4
  190. package/dist/shared/pty-client.js +204 -25
  191. package/dist/shared/rundir.d.ts +2 -0
  192. package/dist/shared/rundir.js +2 -0
  193. package/dist/shared/schema.d.ts +34 -0
  194. package/dist/shared/schema.js +14 -0
  195. package/dist/shared/system.d.ts +1 -0
  196. package/dist/shared/system.js +1 -0
  197. package/dist/sync/claude-mem.js +3 -2
  198. package/dist/sync/history.d.ts +6 -0
  199. package/dist/sync/history.js +26 -11
  200. package/dist/sync/watcher.js +78 -6
  201. package/package.json +2 -1
  202. package/sandbox/cargoshim.cs +56 -0
  203. package/sandbox/cpucap.cs +33 -0
  204. package/sandbox/cpucap.ps1 +4 -35
  205. package/sandbox/logon.cmd +6 -0
  206. package/sandbox/memlog.ps1 +34 -0
  207. package/sandbox/pagefile.cs +31 -0
  208. package/sandbox/pagefile.ps1 +21 -0
  209. package/sandbox/ptyhost.cs +162 -7
  210. package/schemas/config.schema.json +318 -3
  211. package/schemas/sandbox-config.schema.json +129 -14
  212. package/dist/cli/menu-multi.js +0 -82
  213. package/dist/cli/menu.d.ts +0 -14
  214. package/dist/cli/menu.js +0 -290
  215. package/dist/cli/settings/edit.d.ts +0 -13
  216. package/dist/cli/settings/edit.js +0 -19
  217. package/dist/cli/settings/index.d.ts +0 -8
  218. package/dist/cli/settings/index.js +0 -131
  219. package/dist/cli/settings/prompts.d.ts +0 -7
  220. package/dist/cli/settings/prompts.js +0 -48
  221. package/dist/cli/settings/schema.d.ts +0 -33
  222. package/dist/cli/settings/schema.js +0 -29
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import crypto from 'node:crypto';
6
6
  import { decideToolchains, resolveToolchains } from '../core/detect-toolchains.js';
7
7
  import { readPwshScripts, usesPwsh } from '../core/detect-pwsh.js';
8
+ import { widenForProjects } from '../core/project-policy.js';
8
9
  import { dirExists, ensureDir, fail, writeJson } from '../core/util.js';
9
10
  import { substDrives } from '../shared/guest-paths.js';
10
11
  import { PROJECTS_GUEST, hubName } from '../shared/projects.js';
@@ -37,7 +38,18 @@ const hostToolsOf = (probe) => ({
37
38
  volta: Boolean(probe.volta),
38
39
  pyenv: Boolean(probe.pyenv),
39
40
  });
40
- export const generateSandbox = (o) => {
41
+ /**
42
+ * env.cmd, which logon.cmd calls before it opens Windows Terminal: Terminal expands "%AGENT_WORKSPACE%" (the starting
43
+ * folder of new tabs) from its own environment, and the runtime only sets the variable machine-wide after Terminal
44
+ * started. `%` is doubled for cmd; a Windows path holds no quotes.
45
+ */
46
+ export const logonEnv = (workspace) => '@set "AGENT_WORKSPACE=' + workspace.replace(/[\r\n"]/g, '').replace(/%/g, '%%') + '"\r\n';
47
+ export const generateSandbox = (given) => {
48
+ // Several projects at boot: the sandbox also gets what the others want and you allowed (core/project-policy.ts).
49
+ const others = given.bootWants?.projects ?? [];
50
+ const boot = planBootProjects({ run: given.run, packageRoot: given.packageRoot, extraProjects: given.extraProjects, knownProjects: given.knownProjects });
51
+ const wide = widenForProjects(given.run.config, others, { keepOff: given.bootWants?.keepOff, exists: dirExists });
52
+ const o = { ...given, run: { ...given.run, config: wide.config } };
41
53
  const { config, dataPath, workspaceHost, harnessName, storageRoot } = o.run;
42
54
  const harness = harnessName === 'none' ? null : config.harnesses[harnessName] ?? fail('Unknown agent: ' + harnessName, 'Configured agents: ' + Object.keys(config.harnesses).join(', '));
43
55
  if (!dirExists(workspaceHost))
@@ -45,13 +57,13 @@ export const generateSandbox = (o) => {
45
57
  assertTestWritable(storageRoot);
46
58
  assertTestWritable(dataPath);
47
59
  // Checked before anything is written, so a refusal leaves nothing behind.
48
- const memoryMb = resolveMemoryMb(config.sandbox, o.freeMemoryBytes);
60
+ const memoryMb = Math.max(resolveMemoryMb(config.sandbox, o.freeMemoryBytes), ...others.map((p) => typeof p.config.sandbox.memoryMb === 'number' ? p.config.sandbox.memoryMb : 0));
49
61
  const launchId = Date.now() + '-' + crypto.randomBytes(3).toString('hex');
50
62
  const launchPath = path.join(storageRoot, 'launches', launchId);
51
63
  ensureDir(launchPath);
52
64
  ensureDir(dataPath);
53
- const boot = planBootProjects({ run: o.run, packageRoot: o.packageRoot, extraProjects: o.extraProjects, knownProjects: o.knownProjects });
54
- const mappings = collectMappings(o, launchPath, boot);
65
+ const mappingNotes = [];
66
+ const mappings = collectMappings(o, launchPath, boot, mappingNotes);
55
67
  const profile = harnessName === 'claude' ? stageClaudeProfile(config.claudeProfile, launchPath, workspaceHost, config.secrets) : NO_PROFILE;
56
68
  stageGitIdentity(launchPath);
57
69
  const personalization = config.personalization.enabled ? capturePersonalization(launchPath) : 'disabled';
@@ -64,7 +76,8 @@ export const generateSandbox = (o) => {
64
76
  workspace: config.guestWorkspace,
65
77
  workspaceHost,
66
78
  projectsDir: PROJECTS_GUEST,
67
- roots: boot.roots.map((r) => r.guest),
79
+ // Every folder mapped for other projects (roots and projects mapped at boot): the guest accepts projects in them.
80
+ roots: [...boot.roots, ...boot.projects].map((r) => r.guest),
68
81
  ...(o.autoSession === false ? { autoSession: false } : {}),
69
82
  harnessName,
70
83
  harness,
@@ -87,12 +100,16 @@ export const generateSandbox = (o) => {
87
100
  personalization: fs.existsSync(path.join(launchPath, 'personalize', 'personalize.reg')),
88
101
  substDrives: substDrives(mappings.map((m) => m.guest)),
89
102
  cpuLimitPercent: config.sandbox.cpuLimit === 'none' ? null : config.sandbox.cpuLimit,
103
+ pageFileMb: config.sandbox.pageFileMb,
104
+ apps: config.apps,
105
+ auditProcesses: config.audit.enabled && config.audit.processes,
90
106
  };
91
107
  const sessionPath = path.join(launchPath, 'session.json');
92
108
  writeJson(sessionPath, session);
109
+ fs.writeFileSync(path.join(launchPath, 'env.cmd'), logonEnv(session.workspace), 'utf8');
93
110
  copyRuntime(o.packageRoot, launchPath);
94
111
  stageHooks(o.packageRoot, launchPath, o.run.scripts);
95
112
  const wsbPath = path.join(launchPath, 'agent.wsb');
96
113
  fs.writeFileSync(wsbPath, buildWsb(config, memoryMb, mappings), 'utf8');
97
- return { wsbPath, launchPath, launchId, sessionPath, memoryMb, mappings, profile, personalization, databases, toolchains: toolchainDecisions, projects: boot };
114
+ return { wsbPath, launchPath, launchId, sessionPath, memoryMb, bootConfig: config, widened: wide.widened, mappings, profile, personalization, databases, toolchains: toolchainDecisions, projects: { ...boot, notes: [...boot.notes, ...wide.notes] }, mappingNotes };
98
115
  };
@@ -1,4 +1,5 @@
1
1
  import type { ResolvedRun } from '../core/types.js';
2
+ import { type Missing } from '../core/project-policy.js';
2
3
  import { type ProjectSpecT } from '../shared/schema.js';
3
4
  import { type ProjectMapper, type RegistryEntry } from './projects.js';
4
5
  /** The launch folder of the running sandbox (host-only storage; its id comes from the guest, so it is checked). */
@@ -10,7 +11,11 @@ export interface Joined {
10
11
  spec: ProjectSpecT | null;
11
12
  run: ResolvedRun | null;
12
13
  notes: string[];
14
+ /** What it wants at boot that the running sandbox started without (it joined without them). */
15
+ missing: Missing | null;
13
16
  }
17
+ /** What to do when a joining project wants something the running sandbox started without. */
18
+ export type MissingChoice = 'continue' | 'restart' | 'cancel';
14
19
  /** The project spec the guest provisions from (scans the project folder like a launch does). */
15
20
  export declare const projectSpec: (run: ResolvedRun, entry: RegistryEntry, hooks: boolean) => ProjectSpecT;
16
21
  /**
@@ -25,4 +30,6 @@ export declare const joinRunning: (o: {
25
30
  wsbShare: boolean;
26
31
  resolve: (projectRoot: string) => Promise<ResolvedRun>;
27
32
  mapper?: ProjectMapper;
33
+ /** Asked before anything is registered when something is missing (default: continue without it). */
34
+ decide?: (name: string, missing: Missing) => Promise<MissingChoice>;
28
35
  }) => Promise<Joined>;
package/dist/host/join.js CHANGED
@@ -2,18 +2,20 @@
2
2
  // resolve it (its approved .sandbox-config.json, versions, toolchain detection, database plan), checked against the
3
3
  // running sandbox's machine-wide protections, made visible inside (host/projects.ts: a project root, a project
4
4
  // mapped at boot, or wsb share) and described to the guest, which provisions what is missing before its session.
5
- // When none of those can make it visible, NeedsRestart reaches the caller, which offers a restart.
5
+ // When none of those can make it visible, NeedsRestart reaches the caller, which offers a restart. So does a project
6
+ // that wants what the sandbox only gets at its start (a .wsb device, more memory, its mappings) when you choose to
7
+ // restart for it; otherwise it joins without them and they are named.
6
8
  import fs from 'node:fs';
7
9
  import path from 'node:path';
8
- import { checkJoin, protectionsOf } from '../core/project-policy.js';
10
+ import { checkJoin, missingAtBoot, nothingMissing, protectionsOf } from '../core/project-policy.js';
9
11
  import { resolveToolchains } from '../core/detect-toolchains.js';
10
- import { assertNoLinks, fail } from '../core/util.js';
12
+ import { assertNoLinks, dirExists, fail } from '../core/util.js';
11
13
  import { Id } from '../shared/schema.js';
12
14
  import { readSandboxStatus } from '../shared/rundir.js';
13
15
  import { syncProjectIn } from '../sync/watcher.js';
14
16
  import { planDatabases } from './database-plan.js';
15
17
  import { decideProjectToolchains } from './generate.js';
16
- import { projectHere, projectMapper, readRegistry, rememberProject, rootHere } from './projects.js';
18
+ import { bootRecordOf, NeedsRestart, projectHere, projectMapper, readRegistry, rememberProject, rootHere } from './projects.js';
17
19
  import { stageScripts } from './launch/stage.js';
18
20
  import { addRepoSnapshot } from './repo-guard.js';
19
21
  /** The launch folder of the running sandbox (host-only storage; its id comes from the guest, so it is checked). */
@@ -44,15 +46,27 @@ export const projectSpec = (run, entry, hooks) => {
44
46
  export const joinRunning = async (o) => {
45
47
  const here = projectHere(o.storageRoot, o.hostDir);
46
48
  if (here?.project.primary)
47
- return { ...here, spec: null, run: null, notes: [] };
49
+ return { ...here, spec: null, run: null, notes: [], missing: null };
48
50
  const registry = readRegistry(o.storageRoot) ?? fail('The running sandbox was started by an older version, so it cannot take another folder.', 'Stop it (sandbox stop) and start it again.');
49
51
  // A folder below a project already in the sandbox uses that project; anything else becomes a project of its own.
50
52
  const root = here?.project.host ?? path.resolve(o.hostDir);
51
53
  const run = await o.resolve(root);
52
- const check = checkJoin(registry.protections, protectionsOf(run.config));
54
+ const check = checkJoin(registry.protections, protectionsOf(run.config), Object.keys(registry.atBoot?.widened ?? {}));
53
55
  if (check.stricter.length) {
54
56
  fail('This project needs stricter protection than the running sandbox has, and those settings are machine-wide:\n ' + check.stricter.join('\n '), 'Its sessions cannot get them without changing them for the sessions already running. Stop the sandbox (sandbox stop) and start it from ' + root + '.');
55
57
  }
58
+ // Boot-only wishes (a .wsb device you allowed for it, more memory, its mappings) the running sandbox lacks.
59
+ const launchPath = runningLaunchPath(o.storageRoot, o.dataPath);
60
+ const boot = bootRecordOf(o.storageRoot, launchPath);
61
+ const found = boot ? missingAtBoot(registry.protections, boot, run.config, dirExists) : null;
62
+ const missing = found && !nothingMissing(found) ? found : null;
63
+ if (missing) {
64
+ const choice = o.decide ? await o.decide(here?.project.name ?? path.basename(root), missing) : 'continue';
65
+ if (choice === 'cancel')
66
+ fail('Cancelled: nothing changed.');
67
+ if (choice === 'restart')
68
+ throw new NeedsRestart(root, 'it wants what the sandbox only gets at its start', missing);
69
+ }
56
70
  const mapper = o.mapper ?? projectMapper({ storageRoot: o.storageRoot, packageRoot: o.packageRoot, wsbShare: o.wsbShare });
57
71
  // Guarded from now on. A folder the sandbox could already write (inside a root, or mapped at boot) and that has no
58
72
  // snapshot from the launch gets an empty one: what is there now may have been planted, so it counts as new.
@@ -60,7 +74,6 @@ export const joinRunning = async (o) => {
60
74
  addRepoSnapshot(o.dataPath, root, { empty: Boolean(here) || Boolean(rootHere(o.storageRoot, root)) });
61
75
  const entry = mapper.register(root);
62
76
  rememberProject(o.storageRoot, entry.host);
63
- const launchPath = runningLaunchPath(o.storageRoot, o.dataPath);
64
77
  // Your hooks in ~/.agent-sandbox/hooks ran at boot (once per sandbox); the project's own scripts ("scripts" in its
65
78
  // .sandbox-config.json, --script) run before its first agent. The launch folder is read-only for the sandbox, but
66
79
  // this side can still add them to it.
@@ -71,6 +84,7 @@ export const joinRunning = async (o) => {
71
84
  syncProjectIn(o.dataPath, { hostWorkspace: entry.host, guestWorkspace: entry.guest });
72
85
  }
73
86
  catch { /* history is a convenience; never block the session */ }
74
- const notes = [...run.notes, ...check.looser.map((l) => 'The running sandbox is stricter: ' + l + '.')];
75
- return { project: entry, guestDir: here ? here.guestDir : entry.guest, spec: projectSpec(run, entry, hooks), run, notes };
87
+ const notes = [...run.notes, ...check.looser.map((l) => 'The running sandbox is stricter: ' + l + '.'),
88
+ ...check.shared.map((s) => s + ' is on in this sandbox for another of its projects (you allowed it there); this project shares it.')];
89
+ return { project: entry, guestDir: here ? here.guestDir : entry.guest, spec: projectSpec(run, entry, hooks), run, notes, missing };
76
90
  };
@@ -1,3 +1,8 @@
1
1
  import type { GenerateOptions, ResolvedMapping } from '../../core/types.js';
2
- import type { BootProjects } from './projects.js';
3
- export declare const collectMappings: (o: GenerateOptions, launchPath: string, boot?: BootProjects) => ResolvedMapping[];
2
+ import { type BootProjects } from './projects.js';
3
+ /**
4
+ * Every mapping, placed. Your extra folders never stop a launch because of their guest path: one the sandbox cannot
5
+ * use (reserved, invalid, or already in use) goes to C:\Mapped\<drive>\<path>, and `notes` says where and why. What
6
+ * does stop it (a folder that is not there, a whole drive, this tool's own folders) is listed in one message.
7
+ */
8
+ export declare const collectMappings: (o: GenerateOptions, launchPath: string, boot?: BootProjects, notes?: string[]) => ResolvedMapping[];
@@ -1,34 +1,84 @@
1
+ // Which folders are mapped into the sandbox: the launch folder, the data folder, the workspace, your extra folders and
2
+ // the tools copied from this PC. Everything is checked before it can reach the .wsb.
3
+ import path from 'node:path';
1
4
  import { assertNoGuestOverlap } from '../../core/config.js';
2
5
  import { assertSafeMapping } from '../../core/safe-paths.js';
3
- import { dirExists, fail } from '../../core/util.js';
4
- import { isValidGuestPath, sandboxFolder } from '../../shared/guest-paths.js';
6
+ import { dirExists, errMsg, fail, isInsideOrEqual, UserError } from '../../core/util.js';
7
+ import { shownByRoot } from './projects.js';
8
+ import { deriveGuest, isValidGuestPath, placeGuest, sandboxFolder, tidyGuestPath } from '../../shared/guest-paths.js';
9
+ import { PROJECTS_GUEST } from '../../shared/projects.js';
5
10
  import { GUEST } from './wsb.js';
6
- export const collectMappings = (o, launchPath, boot = { roots: [], projects: [], notes: [] }) => {
11
+ import { guestGitDir, planView } from '../workspace-view/index.js';
12
+ /**
13
+ * Every mapping, placed. Your extra folders never stop a launch because of their guest path: one the sandbox cannot
14
+ * use (reserved, invalid, or already in use) goes to C:\Mapped\<drive>\<path>, and `notes` says where and why. What
15
+ * does stop it (a folder that is not there, a whole drive, this tool's own folders) is listed in one message.
16
+ */
17
+ export const collectMappings = (o, launchPath, boot = { roots: [], projects: [], notes: [] }, notes = []) => {
7
18
  const { config, dataPath, workspaceHost, harnessName } = o.run;
8
19
  assertGuestPath(config.guestWorkspace, 'guestWorkspace');
20
+ // workspaceFilter: the sandbox gets the project's filtered view (no git-ignored files) and its real .git beside it.
21
+ const view = planView(config, o.run.storageRoot, workspaceHost);
22
+ // A launch folder inside a project root is shown by the root, at the place it has whichever project starts the sandbox.
23
+ const inRoot = !view && shownByRoot(boot.roots, workspaceHost, config.guestWorkspace);
9
24
  const mappings = [
10
25
  { host: launchPath, guest: GUEST.launch, readOnly: true },
11
26
  { host: dataPath, guest: GUEST.data, readOnly: false },
12
- { host: workspaceHost, guest: config.guestWorkspace, readOnly: config.readOnly },
27
+ ...(inRoot ? [] : [{ host: view ? view.options.view : workspaceHost, guest: config.guestWorkspace, readOnly: config.readOnly }]),
28
+ ...(view?.hostGit ? [{ host: view.hostGit, guest: guestGitDir(config.guestWorkspace), readOnly: config.readOnly }] : []),
13
29
  ];
30
+ // Project roots and other projects, so they can join the running sandbox without a restart (launch/projects.ts).
31
+ const projects = [];
32
+ for (const m of [...boot.roots, ...boot.projects]) {
33
+ assertGuestPath(m.guest, 'project folder for ' + m.host);
34
+ projects.push({ host: m.host, guest: m.guest, readOnly: false });
35
+ }
36
+ const taken = [
37
+ { guest: config.guestWorkspace, what: 'the project' },
38
+ ...(view?.hostGit ? [{ guest: guestGitDir(config.guestWorkspace), what: 'the project\'s .git' }] : []),
39
+ { guest: PROJECTS_GUEST, what: 'the other projects' },
40
+ ...projects.map((p) => ({ guest: p.guest, what: p.host })),
41
+ ];
42
+ const outside = new Set((o.run.outsideMappings ?? []).map((h) => path.resolve(h).toLowerCase()));
43
+ const problems = [];
44
+ const missing = [];
45
+ const extra = [];
14
46
  for (const m of config.mappings) {
15
47
  const host = m.host;
16
- assertGuestPath(m.guest, 'mappings[].guest for ' + host);
17
48
  if (!dirExists(host)) {
18
- if (m.optional) {
19
- console.warn('Skipping optional mapping (folder not found): ' + host);
20
- continue;
21
- }
22
- fail('A mapped folder does not exist: ' + host, 'Create it, fix the path in your config, or mark the mapping "optional": true.');
49
+ if (m.optional)
50
+ missing.push(host);
51
+ else
52
+ problems.push(host + ': the folder does not exist. Fix: create it, correct the path, or mark the mapping "optional": true.');
53
+ continue;
23
54
  }
24
- assertSafeMapping(host, o.packageRoot, o.run.storageRoot);
25
- mappings.push({ host, guest: m.guest, readOnly: m.readOnly });
26
- }
27
- // Project roots and other projects, so they can join the running sandbox without a restart (launch/projects.ts).
28
- for (const m of [...boot.roots, ...boot.projects]) {
29
- assertGuestPath(m.guest, 'project folder for ' + m.host);
30
- mappings.push({ host: m.host, guest: m.guest, readOnly: false });
55
+ try {
56
+ assertSafeMapping(host, o.packageRoot, o.run.storageRoot);
57
+ }
58
+ catch (e) {
59
+ problems.push(errMsg(e) + (e instanceof UserError && e.fix ? ' Fix: ' + e.fix : ''));
60
+ continue;
61
+ }
62
+ // A folder inside the project with no guest path of its own is already there (unless the filtered view hides it).
63
+ if (!m.guest && !view && isInsideOrEqual(workspaceHost, host)) {
64
+ notes.push(host + ' is inside the project, so it is already in the sandbox at ' + deriveGuest(host, workspaceHost, config.guestWorkspace) + '.');
65
+ continue;
66
+ }
67
+ const wanted = tidyGuestPath(m.guest ?? deriveGuest(host, workspaceHost, config.guestWorkspace));
68
+ // The same folder at the same place twice (your settings and the project file): once is enough.
69
+ if (extra.some((x) => x.host.toLowerCase() === host.toLowerCase() && x.guest.toLowerCase() === wanted.toLowerCase()))
70
+ continue;
71
+ const placed = placeGuest(host, wanted, [...taken, ...extra.map((x) => ({ guest: x.guest, what: x.host }))]);
72
+ if (placed.note)
73
+ notes.push(placed.note);
74
+ const label = outside.has(path.resolve(host).toLowerCase()) ? 'outside the project, allowed' : undefined;
75
+ extra.push({ host, guest: placed.guest, readOnly: m.readOnly, ...(label ? { label } : {}) });
31
76
  }
77
+ if (problems.length)
78
+ fail((problems.length === 1 ? 'A mapped folder cannot be used:\n ' : problems.length + ' mapped folders cannot be used:\n ') + problems.join('\n '));
79
+ if (missing.length)
80
+ notes.push('Skipped ' + (missing.length === 1 ? 'an optional folder' : missing.length + ' optional folders') + ' not on this PC: ' + missing.join(', '));
81
+ mappings.push(...extra, ...projects);
32
82
  const { probe } = o;
33
83
  if (probe.nodeDir)
34
84
  mappings.push({ host: probe.nodeDir, guest: GUEST.hostTools + '\\node', readOnly: true });
@@ -38,8 +88,8 @@ export const collectMappings = (o, launchPath, boot = { roots: [], projects: [],
38
88
  mappings.push({ host: probe.gitDir, guest: GUEST.hostTools + '\\git', readOnly: true });
39
89
  if (probe.nvmDir)
40
90
  mappings.push({ host: probe.nvmDir, guest: GUEST.hostTools + '\\nvm', readOnly: true });
91
+ // Placement keeps extra folders apart; this guards the rest (and where the folders really are: F:\x is C:\Mirror\F\x).
41
92
  assertNoGuestOverlap(mappings);
42
- // Same check on where the folders really are (F:\x and C:\Mirror\F\x are one place).
43
93
  assertNoGuestOverlap(mappings.map((m) => ({ guest: sandboxFolder(m.guest) })));
44
94
  return mappings;
45
95
  };
@@ -13,3 +13,5 @@ export declare const planBootProjects: (o: {
13
13
  extraProjects?: string[];
14
14
  knownProjects?: string[];
15
15
  }) => BootProjects;
16
+ /** The launch folder is inside a mapped root and placed under it (core/workspace.ts): the root shows it, no own mapping. */
17
+ export declare const shownByRoot: (roots: RootMapping[], workspaceHost: string, guestWorkspace: string) => boolean;
@@ -4,7 +4,7 @@
4
4
  import path from 'node:path';
5
5
  import { assertSafeMapping, assertSafeWorkspace } from '../../core/safe-paths.js';
6
6
  import { dirExists, errMsg, fail } from '../../core/util.js';
7
- import { isWithin, planProjectMappings } from '../../shared/projects.js';
7
+ import { guestDirFor, isWithin, planProjectMappings, projectForHostDir, usableRoots } from '../../shared/projects.js';
8
8
  import { allProjectsToMap, readKnown } from '../projects.js';
9
9
  /** More folders than this would make the .wsb (and every boot) unwieldy; the rest join with a restart later. */
10
10
  export const MAX_BOOT_PROJECTS = 48;
@@ -21,7 +21,6 @@ export const planBootProjects = (o) => {
21
21
  return errMsg(e);
22
22
  }
23
23
  };
24
- const roots = [];
25
24
  for (const root of config.projects.roots.map((r) => path.resolve(r))) {
26
25
  if (!dirExists(root)) {
27
26
  notes.push('Project root not found, skipped: ' + root);
@@ -30,9 +29,9 @@ export const planBootProjects = (o) => {
30
29
  const refusal = safe(root);
31
30
  if (refusal)
32
31
  fail('projects.roots: ' + refusal, 'Use a folder that holds your projects, e.g. F:\\Code (not a drive, your profile or a system folder).');
33
- if (!roots.some((r) => isWithin(r, root)))
34
- roots.push(root);
35
32
  }
33
+ // The same list resolveRun used to place a launch folder that is inside a root (core/workspace.ts).
34
+ const roots = usableRoots(config.projects.roots, dirExists);
36
35
  const known = o.knownProjects ?? readKnown(storageRoot).map((k) => k.host);
37
36
  const wanted = [...(o.extraProjects ?? []), ...(config.projects.all ? allProjectsToMap(known, { primary: workspaceHost, roots, exclude: config.projects.exclude }) : [])];
38
37
  const projects = [];
@@ -52,5 +51,12 @@ export const planBootProjects = (o) => {
52
51
  }
53
52
  projects.push(folder);
54
53
  }
55
- return { ...planProjectMappings(roots, projects, Boolean(o.run.mirror)), notes };
54
+ // Projects mapped on their own sit where they would as the launch folder (C:\Workspace\<name>), never on top of it.
55
+ const taken = [config.guestWorkspace, ...config.mappings.flatMap((m) => (m.guest ? [m.guest] : []))];
56
+ return { ...planProjectMappings(roots, projects, Boolean(o.run.mirror), taken), notes };
57
+ };
58
+ /** The launch folder is inside a mapped root and placed under it (core/workspace.ts): the root shows it, no own mapping. */
59
+ export const shownByRoot = (roots, workspaceHost, guestWorkspace) => {
60
+ const root = projectForHostDir(roots, workspaceHost);
61
+ return root !== null && guestDirFor(root, workspaceHost).toLowerCase() === guestWorkspace.replace(/[\\/]+$/, '').toLowerCase();
56
62
  };
@@ -25,7 +25,7 @@ export const stageScripts = (target, scripts) => {
25
25
  fs.copyFileSync(script, path.join(target, '90-extra-' + index + '-' + path.basename(script)));
26
26
  });
27
27
  };
28
- export const RUNTIME_FILES = ['main.js', 'daemon.js', 'view.js', 'gitguard.js', 'db.js'];
28
+ export const RUNTIME_FILES = ['main.js', 'daemon.js', 'view.js', 'gitguard.js', 'db.js', 'netproxy.js'];
29
29
  /** The bundled sandbox runtime (dist/runtime: one self-contained file per entry point) and the sandbox/ scripts. */
30
30
  export const copyRuntime = (packageRoot, launchPath) => {
31
31
  const runtime = path.join(packageRoot, 'dist', 'runtime');
@@ -0,0 +1,17 @@
1
+ /** What Windows runs when you open it (not source files such as .js or .py: those need a program you choose). */
2
+ export declare const RISKY: RegExp;
3
+ /** Marks one file if it is a program or script (and a plain file, not a link). Returns true when it marked it. */
4
+ export declare const markFromSandbox: (file: string) => boolean;
5
+ /** Has this file the mark? */
6
+ export declare const isMarked: (file: string) => boolean;
7
+ /** Watches the projects while the sandbox runs and marks what appears or changes; returns a function that stops it. */
8
+ export declare const watchAndMark: (projects: string[], note?: (message: string) => void) => (() => void);
9
+ /** The projects to watch and mark (empty: storage.markSandboxFiles is off). */
10
+ export declare const saveMarkedProjects: (storageRoot: string, dataPath: string, projects: string[]) => void;
11
+ export declare const readMarkedProjects: (storageRoot: string, dataPath: string) => string[];
12
+ /** The data folder's size, measured by the watcher when a sandbox closes (measuring at launch would be slow). */
13
+ export declare const saveDataSize: (storageRoot: string, dataPath: string, bytes: number) => void;
14
+ export declare const readDataSize: (storageRoot: string, dataPath: string) => {
15
+ bytes: number;
16
+ at: string;
17
+ } | null;
@@ -0,0 +1,74 @@
1
+ // storage.markSandboxFiles: programs and scripts that appear or change in a project while a sandbox runs get Windows'
2
+ // "downloaded from the internet" mark (the Zone.Identifier stream, zone 3). Running one on this PC then goes through
3
+ // SmartScreen, and PowerShell's RemoteSigned policy refuses an unsigned marked script. So something an agent planted is
4
+ // not run by a double-click or a habit. Runs in the background watcher; only the mark is written, never the file.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { z } from 'zod';
8
+ import { readValid, writeJsonAtomic } from '../shared/rundir.js';
9
+ /** What Windows runs when you open it (not source files such as .js or .py: those need a program you choose). */
10
+ export const RISKY = /\.(exe|dll|com|scr|pif|msi|msix|msp|appx|appxbundle|bat|cmd|ps1|psm1|psd1|vbs|vbe|wsf|wsh|hta|lnk|url|jar|reg|cpl|application|gadget|chm)$/i;
11
+ const MARK = '[ZoneTransfer]\r\nZoneId=3\r\nHostUrl=about:windows-development-sandbox\r\n';
12
+ /** Folders not worth watching closely (git's own files; dependency and build folders are still marked). */
13
+ const SKIP = /(^|[\\/])\.git([\\/]|$)/i;
14
+ /** Marks one file if it is a program or script (and a plain file, not a link). Returns true when it marked it. */
15
+ export const markFromSandbox = (file) => {
16
+ if (!RISKY.test(file) || SKIP.test(file))
17
+ return false;
18
+ try {
19
+ const stat = fs.lstatSync(file);
20
+ if (!stat.isFile())
21
+ return false;
22
+ fs.writeFileSync(file + ':Zone.Identifier', MARK);
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ };
29
+ /** Has this file the mark? */
30
+ export const isMarked = (file) => {
31
+ try {
32
+ return fs.readFileSync(file + ':Zone.Identifier', 'utf8').includes('ZoneId=3');
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ };
38
+ /** Watches the projects while the sandbox runs and marks what appears or changes; returns a function that stops it. */
39
+ export const watchAndMark = (projects, note = () => undefined) => {
40
+ const pending = new Set();
41
+ let timer;
42
+ const flush = () => {
43
+ timer = undefined;
44
+ const marked = [...pending].filter(markFromSandbox);
45
+ pending.clear();
46
+ if (marked.length)
47
+ note('marked as from the sandbox (SmartScreen asks before they run here): ' + marked.slice(0, 10).map((f) => path.basename(f)).join(', ') + (marked.length > 10 ? ' and ' + (marked.length - 10) + ' more' : ''));
48
+ };
49
+ const watchers = projects.flatMap((project) => {
50
+ try {
51
+ return [fs.watch(project, { recursive: true }, (_event, file) => {
52
+ const rel = file ? String(file) : '';
53
+ if (!rel || !RISKY.test(rel) || SKIP.test(rel))
54
+ return;
55
+ pending.add(path.join(project, rel));
56
+ timer ??= setTimeout(flush, 1000);
57
+ })];
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ });
63
+ return () => { watchers.forEach((w) => w.close()); clearTimeout(timer); if (pending.size)
64
+ flush(); };
65
+ };
66
+ // ---- what the watcher marks, and the size of the persisted data (host-only files next to the sync state) ----------
67
+ const marksFile = (storageRoot, dataPath) => path.join(storageRoot, 'sync', path.basename(dataPath) + '-mark.json');
68
+ const sizeFile = (storageRoot, dataPath) => path.join(storageRoot, 'sync', path.basename(dataPath) + '-size.json');
69
+ /** The projects to watch and mark (empty: storage.markSandboxFiles is off). */
70
+ export const saveMarkedProjects = (storageRoot, dataPath, projects) => writeJsonAtomic(marksFile(storageRoot, dataPath), projects);
71
+ export const readMarkedProjects = (storageRoot, dataPath) => readValid(z.array(z.string().max(1024)).max(64), marksFile(storageRoot, dataPath)) ?? [];
72
+ /** The data folder's size, measured by the watcher when a sandbox closes (measuring at launch would be slow). */
73
+ export const saveDataSize = (storageRoot, dataPath, bytes) => writeJsonAtomic(sizeFile(storageRoot, dataPath), { bytes, at: new Date().toISOString() });
74
+ export const readDataSize = (storageRoot, dataPath) => readValid(z.object({ bytes: z.number().min(0), at: z.string().max(40) }), sizeFile(storageRoot, dataPath));
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { spawn, spawnSync } from 'node:child_process';
5
5
  import { SYSTEM } from '../shared/system.js';
6
6
  import { refuseInSelfTest } from './test-guard.js';
7
+ import { updateRestartPending } from './stops.js';
7
8
  import { commandsDir, realAgent } from './agent-commands.js';
8
9
  import { dirExists, ensureDir, fail, fileExists, findOnPath, homeDir, isWin, realPath } from '../core/util.js';
9
10
  const findHarnessExe = (config, harnessName) => {
@@ -116,6 +117,8 @@ export const doctorRows = (config, harnessName, storageRoot, configPath) => {
116
117
  ['Windows Sandbox feature', sandboxFeatureInstalled() ? 'enabled' : 'NOT enabled'],
117
118
  ['Sandbox running', isSandboxRunning() ? 'YES' : 'no'],
118
119
  ['Free host RAM (MB)', String(Math.floor(os.freemem() / 1048576))],
120
+ ['Update restart pending', updateRestartPending() ? 'YES (Windows may restart on its own and close the sandbox; restart first or pause updates)' : 'no'],
121
+ ['Keep this PC awake', config.sandbox.keepAwake ? 'yes, while a sandbox runs' : 'no (sandbox.keepAwake is off: the PC may sleep under running agents)'],
119
122
  ['Provisioning', config.provisioning.copyFromHost ? 'copy from this PC' + (config.provisioning.fallbackToInstall ? ', fall back to download' : ', no fallback') : 'download inside sandbox'],
120
123
  ['Host Node (runner)', probe.nvmNode ? 'nvm ' + probe.hostNodeVersion : probe.nodeDir ?? '(none; will download)'],
121
124
  ['Host nvm versions', probe.nvmDir ?? '(no NVM_HOME; versions will be downloaded)'],
@@ -0,0 +1,8 @@
1
+ import { type PortForward } from '../shared/netpolicy.js';
2
+ /** Host-only: the forwards of the running sandbox (written at launch, read by the watcher). */
3
+ export declare const saveForwards: (storageRoot: string, dataPath: string, forwards: PortForward[]) => void;
4
+ export declare const readForwards: (storageRoot: string, dataPath: string) => PortForward[];
5
+ /** The sandbox's address on this PC's Hyper-V network, from its status (null until it has one). */
6
+ export declare const sandboxAddress: (dataPath: string) => string | null;
7
+ /** Starts one listener per forward; returns a function that stops them. */
8
+ export declare const startPortForwards: (forwards: PortForward[], address: () => string | null, note?: (message: string) => void) => (() => void);
@@ -0,0 +1,37 @@
1
+ // network.forwardPorts, PC side: http://localhost:<pc> on this PC reaches <sandbox> inside the sandbox while it runs.
2
+ // Runs in the background watcher. Listens on 127.0.0.1 only (nothing else on your network can use it), and connects
3
+ // only to an address the sandbox published that is on this PC's Hyper-V network (isSandboxAddress). Bytes are passed
4
+ // through, never looked at. No admin rights needed (no netsh portproxy).
5
+ import net from 'node:net';
6
+ import path from 'node:path';
7
+ import { z } from 'zod';
8
+ import { relayPort } from '../shared/netpolicy.js';
9
+ import { readSandboxStatus, readValid, writeJsonAtomic } from '../shared/rundir.js';
10
+ import { isSandboxAddress } from './attach.js';
11
+ const Forwards = z.array(z.object({ pc: z.number().int().min(1).max(65535), sandbox: z.number().int().min(1).max(65535) })).max(64);
12
+ const forwardsFile = (storageRoot, dataPath) => path.join(storageRoot, 'sync', path.basename(dataPath) + '-forwards.json');
13
+ /** Host-only: the forwards of the running sandbox (written at launch, read by the watcher). */
14
+ export const saveForwards = (storageRoot, dataPath, forwards) => writeJsonAtomic(forwardsFile(storageRoot, dataPath), forwards);
15
+ export const readForwards = (storageRoot, dataPath) => readValid(Forwards, forwardsFile(storageRoot, dataPath)) ?? [];
16
+ /** The sandbox's address on this PC's Hyper-V network, from its status (null until it has one). */
17
+ export const sandboxAddress = (dataPath) => readSandboxStatus(dataPath)?.addresses?.find((a) => isSandboxAddress(a)) ?? null;
18
+ /** Starts one listener per forward; returns a function that stops them. */
19
+ export const startPortForwards = (forwards, address, note = () => undefined) => {
20
+ const servers = forwards.map((f, i) => {
21
+ const server = net.createServer((client) => {
22
+ const ip = address();
23
+ if (!ip) {
24
+ client.destroy();
25
+ return;
26
+ }
27
+ const target = net.connect(relayPort(i), ip);
28
+ client.pipe(target).pipe(client);
29
+ target.on('error', () => client.destroy());
30
+ client.on('error', () => target.destroy());
31
+ });
32
+ server.on('error', (e) => note('localhost:' + f.pc + ' could not be forwarded' + (e.code === 'EADDRINUSE' ? ': something on this PC already uses that port' : ': ' + e.message)));
33
+ server.listen(f.pc, '127.0.0.1', () => note('localhost:' + f.pc + ' -> sandbox port ' + f.sandbox));
34
+ return server;
35
+ });
36
+ return () => servers.forEach((s) => s.close());
37
+ };
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import type { Missing } from '../core/project-policy.js';
2
3
  import { PROJECTS_GUEST, type ProjectEntry, type RootMapping } from '../shared/projects.js';
3
4
  import { type Wsb } from './wsb.js';
4
5
  export { PROJECTS_GUEST };
@@ -36,7 +37,17 @@ declare const Registry: z.ZodObject<{
36
37
  }>;
37
38
  registeredAt: z.ZodString;
38
39
  }, z.core.$strict>>;
40
+ atBoot: z.ZodOptional<z.ZodObject<{
41
+ memoryMb: z.ZodNullable<z.ZodNumber>;
42
+ mappings: z.ZodArray<z.ZodObject<{
43
+ host: z.ZodString;
44
+ guest: z.ZodString;
45
+ readOnly: z.ZodBoolean;
46
+ }, z.core.$strict>>;
47
+ widened: z.ZodRecord<z.ZodString, z.ZodString>;
48
+ }, z.core.$strict>>;
39
49
  }, z.core.$strict>;
50
+ export type AtBoot = NonNullable<z.output<typeof Registry>['atBoot']>;
40
51
  export type RegistryEntry = z.output<typeof Entry>;
41
52
  export type ProjectRegistry = z.output<typeof Registry>;
42
53
  export declare const readRegistry: (storageRoot: string) => ProjectRegistry | null;
@@ -47,12 +58,24 @@ export declare const startRegistry: (storageRoot: string, o: {
47
58
  primary: RootMapping;
48
59
  roots: RootMapping[];
49
60
  boot: RootMapping[];
61
+ atBoot?: AtBoot;
50
62
  }) => RegistryEntry;
51
- /** Why a folder cannot join the running sandbox without a restart (the caller offers one). */
63
+ /** Reads the memory and mapped folders back from a .wsb this tool wrote (host-only launch folder; bounded). */
64
+ export declare const parseWsbBoot: (xml: string) => AtBoot;
65
+ /**
66
+ * What the running sandbox booted with. A sandbox started by 1.4.0 or older has no record: it is read from the .wsb in
67
+ * its launch folder (host-only storage) and recorded. null when neither is there.
68
+ */
69
+ export declare const bootRecordOf: (storageRoot: string, launchPath: string | null) => AtBoot | null;
70
+ /**
71
+ * Why a folder cannot join the running sandbox without a restart (the caller offers one). With `missing`: it could
72
+ * join, but you chose to restart for what it wants at boot (core/project-policy.ts).
73
+ */
52
74
  export declare class NeedsRestart extends Error {
53
75
  readonly hostDir: string;
54
76
  readonly reason: string;
55
- constructor(hostDir: string, reason: string);
77
+ readonly missing?: Missing | undefined;
78
+ constructor(hostDir: string, reason: string, missing?: Missing | undefined);
56
79
  }
57
80
  export interface ProjectMapper {
58
81
  /** The projects of the running sandbox (the launch folder first). */