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
@@ -6,22 +6,27 @@ import { Name, SchemaRef, Strings, toggle } from './common.js';
6
6
  import { Databases } from './databases.js';
7
7
  import { Harness } from './harness.js';
8
8
  import { AgentCommands, Attach, ClaudeProfile, Secrets } from './profile.js';
9
- import { Downloads, Git, GuestWorkspace, Mapping, Provisioning, Sandbox } from './sandbox.js';
9
+ import { Audit, Downloads, Git, GuestWorkspace, Mapping, Network, Provisioning, Sandbox, WorkspaceFilter } from './sandbox.js';
10
10
  import { Toolchains } from './toolchains.js';
11
+ import { Apps } from './apps.js';
11
12
  import { Projects } from './projects.js';
12
13
  export const Config = z.strictObject({
13
14
  $schema: SchemaRef.optional(),
14
15
  defaultHarness: Name.describe('Agent started by `sandbox launch` (sandbox claude always runs claude).'),
15
16
  guestWorkspace: GuestWorkspace,
16
17
  readOnly: z.boolean().describe('Map the workspace read-only.'),
18
+ workspaceFilter: WorkspaceFilter,
19
+ audit: Audit,
17
20
  mappings: z.array(Mapping).describe('Extra folders from this PC.'),
18
21
  sandbox: Sandbox,
19
22
  storage: z.strictObject({
20
23
  root: z.string().min(1).describe('Where everything is kept on this PC (tools, databases, logins, launches).'),
21
24
  persistAgentState: z.boolean().describe('Keep agent logins and history between sandboxes.'),
22
25
  dataName: Name.describe('Name of the persistent data set (C:\\AgentData) shared by all workspaces.'),
26
+ markSandboxFiles: z.boolean().describe('Programs and scripts (.exe, .ps1, .bat, .cmd, .msi, .lnk ...) that appear or change in your project while a sandbox runs get Windows\' "downloaded from the internet" mark, so SmartScreen and PowerShell ask before they run on this PC.'),
27
+ warnDataGb: z.number().min(0).describe('Say at launch when the persisted data (tools, databases, logins) is bigger than this many GB; 0 = never. sandbox data shows what takes the space.'),
23
28
  }),
24
- network: z.strictObject({ blockPrivateRanges: z.boolean().describe('Block your LAN (10/8, 172.16/12, 192.168/16, SMB) from inside the sandbox.') }),
29
+ network: Network,
25
30
  provisioning: Provisioning,
26
31
  downloads: Downloads,
27
32
  toolchains: Toolchains,
@@ -30,6 +35,7 @@ export const Config = z.strictObject({
30
35
  agentCommands: AgentCommands,
31
36
  secrets: Secrets,
32
37
  personalization: toggle('Copy your desktop look (theme, accent, taskbar, wallpaper, ...) into the sandbox.'),
38
+ apps: Apps,
33
39
  claudeProfile: ClaudeProfile,
34
40
  harnesses: z.record(Name, Harness).describe('Agents that can run in the sandbox.'),
35
41
  hooks: z.strictObject({ failFast: z.boolean().describe('Stop when a hook script fails.') }),
@@ -44,10 +50,14 @@ export const LocalOverrides = z.strictObject({
44
50
  harness: Name.describe('Agent for this project.'),
45
51
  guestWorkspace: GuestWorkspace,
46
52
  readOnly: z.boolean().describe('Map this project read-only.'),
47
- mappings: z.array(Mapping).describe('Extra folders (relative to this file).'),
53
+ // A project may turn the filter on and keep more out, never let ignored files in.
54
+ workspaceFilter: WorkspaceFilter.pick({ enabled: true, exclude: true }).partial(),
55
+ mappings: z.array(Mapping).describe('Extra folders (relative paths are relative to this file), e.g. a read-only data folder at another place in the sandbox. Folders outside the project are used only as your projects.allowOutsideMappings setting allows (by default you are asked once).'),
48
56
  scripts: Strings.describe('Scripts (.ps1 / .mjs / .cmd, relative to this file) run inside the sandbox before the agent.'),
49
- sandbox: Sandbox.omit({ cpuLimit: true }).partial(),
50
- network: z.strictObject({ blockPrivateRanges: z.boolean() }).partial(),
57
+ // This PC's resources are yours to share: CPU and paging file (this PC's disk) are not a project's to set.
58
+ sandbox: Sandbox.omit({ cpuLimit: true, pageFileMb: true }).partial(),
59
+ // A project may block more (the LAN, hosts); it never opens hosts, ports, DNS or a proxy.
60
+ network: Network.pick({ blockPrivateRanges: true, blockHosts: true }).partial(),
51
61
  provisioning: Provisioning.pick({ copyFromHost: true, fallbackToInstall: true, refresh: true }).extend({ git: Git.partial(), terminal: toggle('Windows Terminal as the sandbox window.').partial() }).partial(),
52
62
  downloads: Downloads.partial(),
53
63
  // Every toolchain setting, each one optional (a project usually only switches languages on or pins a version).
@@ -58,6 +68,8 @@ export const LocalOverrides = z.strictObject({
58
68
  databases: z.strictObject(_.mapValues(Databases.shape, (shape) => shape.partial())).partial(),
59
69
  attach: Attach.partial(),
60
70
  personalization: toggle('Copy your desktop look into the sandbox.').partial(),
61
- claudeProfile: ClaudeProfile.pick({ enabled: true, include: true, overwrite: true, copyCredentials: true, syncHistory: true, syncClaudeMem: true }).partial(),
71
+ // Catalogue apps only (a vetted list): your own programs (apps.custom) download anything, so a repo cannot add them.
72
+ apps: Apps.pick({ install: true }).partial(),
73
+ claudeProfile: ClaudeProfile.pick({ enabled: true, overwrite: true, copyCredentials: true, syncHistory: true, syncClaudeMem: true }).partial(),
62
74
  hooks: z.strictObject({ failFast: z.boolean() }).partial(),
63
75
  }).partial();
@@ -23,5 +23,10 @@ export declare const ClaudeProfile: z.ZodObject<{
23
23
  copyCredentials: z.ZodBoolean;
24
24
  overwrite: z.ZodBoolean;
25
25
  syncHistory: z.ZodBoolean;
26
+ historyScope: z.ZodEnum<{
27
+ project: "project";
28
+ all: "all";
29
+ }>;
26
30
  syncClaudeMem: z.ZodBoolean;
31
+ syncClaudeMemBack: z.ZodBoolean;
27
32
  }, z.core.$strict>;
@@ -24,6 +24,8 @@ export const ClaudeProfile = z.strictObject({
24
24
  globalStateFile: z.string().describe('Your .claude.json, or "auto". Empty string = do not copy.'),
25
25
  copyCredentials: z.boolean().describe('Copy your Claude LOGIN into the sandbox. Prefer sandbox account instead.'),
26
26
  overwrite: z.boolean().describe('Refresh these settings from this PC on every launch.'),
27
- syncHistory: z.boolean().describe('Bring all conversation history in, and sync new history back (path-translated).'),
28
- syncClaudeMem: z.boolean().describe('Bring claude-mem data in, and its database back when the sandbox closes.'),
27
+ syncHistory: z.boolean().describe('Bring conversation history in, and sync new history back (path-translated).'),
28
+ historyScope: z.enum(['project', 'all']).describe('Which history comes in: "project" = only the projects in this sandbox (other projects\' conversations, file backups and prompts stay on this PC), "all" = everything. Only this sandbox\'s projects ever sync back.'),
29
+ syncClaudeMem: z.boolean().describe('Bring claude-mem data into the sandbox (its settings and backups stay on this PC).'),
30
+ syncClaudeMemBack: z.boolean().describe('Also copy claude-mem\'s database back to this PC when the sandbox closes. Off by default: what is in it is loaded into every Claude session on this PC, so a compromised agent could plant instructions there.'),
29
31
  });
@@ -5,5 +5,6 @@ export declare const Projects: z.ZodObject<{
5
5
  roots: z.ZodArray<z.ZodString>;
6
6
  askToAddRoot: z.ZodBoolean;
7
7
  declinedRoots: z.ZodArray<z.ZodString>;
8
+ allowOutsideMappings: z.ZodUnion<readonly [z.ZodLiteral<"ask">, z.ZodBoolean]>;
8
9
  wsbShare: z.ZodBoolean;
9
10
  }, z.core.$strict>;
@@ -7,5 +7,6 @@ export const Projects = z.strictObject({
7
7
  roots: z.array(z.string().min(1)).describe('Folders whose subfolders are your projects (e.g. "F:\\\\Code"), mapped read/write at every start. Any project inside one, including ones you create later, opens in the running sandbox at once, without a restart. Every agent in the sandbox can see all of them.'),
8
8
  askToAddRoot: z.boolean().describe('When you start a project whose parent folder is not a root yet, ask once whether to add it.'),
9
9
  declinedRoots: z.array(z.string().min(1)).describe('Parent folders you said no to (not asked again).'),
10
+ allowOutsideMappings: z.union([z.literal('ask'), z.boolean()]).describe('A project\'s .sandbox-config.json may map folders from outside the project (e.g. a game folder on another drive). "ask" = an interactive launch asks once per project and set of folders, and remembers your answer; true = always allowed (still listed when you approve the file); false = never (the launch stops and says which folders). Only your own settings decide this, never a project file.'),
10
11
  wsbShare: z.boolean().describe('On Windows 11 24H2+ with the Windows Sandbox command line (wsb), add other folders to the running sandbox with `wsb share` (untested). Otherwise starting a project outside every root offers a restart.'),
11
12
  });
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
  export declare const GuestWorkspace: z.ZodUnion<readonly [z.ZodLiteral<"auto">, z.ZodLiteral<"mirror">, z.ZodString]>;
3
3
  export declare const Mapping: z.ZodObject<{
4
4
  host: z.ZodString;
5
- guest: z.ZodString;
5
+ guest: z.ZodOptional<z.ZodString>;
6
6
  readOnly: z.ZodBoolean;
7
7
  optional: z.ZodOptional<z.ZodBoolean>;
8
8
  }, z.core.$strict>;
@@ -11,6 +11,7 @@ export declare const Sandbox: z.ZodObject<{
11
11
  maxMemoryMb: z.ZodNumber;
12
12
  maxMemoryPercent: z.ZodNumber;
13
13
  cpuLimit: z.ZodUnion<readonly [z.ZodLiteral<"none">, z.ZodNumber]>;
14
+ pageFileMb: z.ZodNumber;
14
15
  reserveHostMb: z.ZodNumber;
15
16
  vGpu: z.ZodBoolean;
16
17
  networking: z.ZodBoolean;
@@ -20,6 +21,30 @@ export declare const Sandbox: z.ZodObject<{
20
21
  printerRedirection: z.ZodBoolean;
21
22
  protectedClient: z.ZodBoolean;
22
23
  headless: z.ZodBoolean;
24
+ keepAwake: z.ZodBoolean;
25
+ }, z.core.$strict>;
26
+ export declare const Network: z.ZodObject<{
27
+ blockPrivateRanges: z.ZodBoolean;
28
+ allowOnly: z.ZodArray<z.ZodString>;
29
+ blockHosts: z.ZodArray<z.ZodString>;
30
+ forwardPorts: z.ZodArray<z.ZodObject<{
31
+ pc: z.ZodNumber;
32
+ sandbox: z.ZodNumber;
33
+ }, z.core.$strict>>;
34
+ dns: z.ZodArray<z.ZodString>;
35
+ proxy: z.ZodString;
36
+ }, z.core.$strict>;
37
+ export declare const Audit: z.ZodObject<{
38
+ enabled: z.ZodBoolean;
39
+ days: z.ZodNumber;
40
+ maxMb: z.ZodNumber;
41
+ processes: z.ZodBoolean;
42
+ files: z.ZodBoolean;
43
+ }, z.core.$strict>;
44
+ export declare const WorkspaceFilter: z.ZodObject<{
45
+ enabled: z.ZodBoolean;
46
+ exclude: z.ZodArray<z.ZodString>;
47
+ include: z.ZodArray<z.ZodString>;
23
48
  }, z.core.$strict>;
24
49
  export declare const Git: z.ZodObject<{
25
50
  enabled: z.ZodBoolean;
@@ -44,6 +69,9 @@ export declare const Provisioning: z.ZodObject<{
44
69
  terminal: z.ZodObject<{
45
70
  enabled: z.ZodBoolean;
46
71
  }, z.core.$strict>;
72
+ conpty: z.ZodObject<{
73
+ enabled: z.ZodBoolean;
74
+ }, z.core.$strict>;
47
75
  }, z.core.$strict>;
48
76
  export declare const Downloads: z.ZodObject<{
49
77
  requireChecksum: z.ZodBoolean;
@@ -4,7 +4,7 @@ import { GitAccess, GuestPath, toggle } from './common.js';
4
4
  export const GuestWorkspace = z.union([z.literal('auto'), z.literal('mirror'), GuestPath]).describe('Where the workspace appears inside the sandbox. "auto" = C:\\Workspace\\<folder name>. "mirror" = the same path as on this PC, so absolute paths in configs and build caches match (another drive letter such as F: is a subst drive onto C:\\Mirror\\F); paths the sandbox reserves (C:\\Users\\..., C:\\Program Files\\...) fall back to "auto".');
5
5
  export const Mapping = z.strictObject({
6
6
  host: z.string().min(1).describe('Folder on this PC (relative paths are relative to the config file).'),
7
- guest: GuestPath.describe('Where it appears inside the sandbox, e.g. C:\\Assets, or on another drive letter (D: to Z:) such as F:\\SteamLibrary\\steamapps\\common\\Game: mapped to C:\\Mirror\\F\\... and shown as F: (subst) at boot.'),
7
+ guest: z.string().min(1).max(1024).optional().describe('Where it appears inside the sandbox, e.g. C:\\Assets, or on another drive letter (D: to Z:) such as F:\\SteamLibrary\\steamapps\\common\\Game (shown as F: at boot). Leave it out to use the same path as on this PC (a folder inside the project stays where it is in the project). A path the sandbox cannot use (C:\\Windows\\..., C:\\Users\\..., one already in use) goes to C:\\Mapped\\<drive>\\<path> instead, and the launch says so.'),
8
8
  readOnly: z.boolean().describe('true = the agent can only read it.'),
9
9
  optional: z.boolean().optional().describe('true = skip it quietly when the host folder does not exist.'),
10
10
  });
@@ -13,6 +13,7 @@ export const Sandbox = z.strictObject({
13
13
  maxMemoryMb: z.number().int().positive().describe('Upper limit for memoryMb "auto".'),
14
14
  maxMemoryPercent: z.number().int().min(10).max(100).describe('memoryMb "auto" never takes more than this share of this PC\'s total RAM (percent).'),
15
15
  cpuLimit: z.union([z.literal('none'), z.number().int().min(5).max(100)]).describe('Most of this PC\'s CPU the sandbox may use, in percent (5-100), e.g. 50 keeps half free for you; "none" = no cap (Windows Sandbox would take every processor). Covers everything this tool starts in the sandbox (agents, installs, builds, its terminal).'),
16
+ pageFileMb: z.number().int().min(0).max(262144).describe('Size of the sandbox\'s paging file in MB, created at full size when it starts so it never grows while builds run (8 GB of its disk always stays free). Windows Sandbox starts with about 1 GB: parallel builds then fail with "The paging file is too small", and Windows grows the file under load. 0 = leave Windows\' own.'),
16
17
  reserveHostMb: z.number().int().nonnegative().describe('RAM always left for this PC when memoryMb is "auto".'),
17
18
  vGpu: z.boolean().describe('Virtual GPU. Off by default: Microsoft notes it widens the sandbox attack surface.'),
18
19
  networking: z.boolean().describe('Internet access. false is the only hard network isolation Windows Sandbox offers.'),
@@ -22,6 +23,32 @@ export const Sandbox = z.strictObject({
22
23
  printerRedirection: z.boolean().describe('Printers from this PC.'),
23
24
  protectedClient: z.boolean().describe('Run the sandbox window in Protected Client mode (recommended).'),
24
25
  headless: z.boolean().describe('Hide the sandbox window (sbx-show brings it back). A headless sandbox shuts down when its last session ends.'),
26
+ keepAwake: z.boolean().describe('Keep this PC from sleeping while a sandbox runs, so agents left working overnight keep going (the screen may still turn off; closing a laptop lid still sleeps it).'),
27
+ });
28
+ const Port = z.number().int().min(1).max(65535);
29
+ const HostEntry = z.string().max(253).regex(/^(@developer|(\*\.)?([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+)$/, 'a host name like example.com, *.example.com, or @developer');
30
+ export const Network = z.strictObject({
31
+ blockPrivateRanges: z.boolean().describe('Block your LAN (10/8, 172.16/12, 192.168/16, SMB) from inside the sandbox.'),
32
+ allowOnly: z.array(HostEntry).describe('Only these hosts can be reached once the sandbox is set up (tools still download first). *.example.com covers its subdomains; @developer adds package registries, GitHub, toolchain downloads and the AI providers. Programs go through a proxy in the sandbox (HTTP_PROXY); anything else is blocked by its firewall. Empty = no limit.'),
33
+ blockHosts: z.array(z.string().max(253).regex(/^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$/, 'a host name like telemetry.example.com')).describe('Never reachable from the sandbox, e.g. production servers or telemetry (each exact name; the sandbox\'s hosts file sends them nowhere).'),
34
+ forwardPorts: z.array(z.strictObject({
35
+ pc: Port.describe('The port on this PC (localhost only).'),
36
+ sandbox: Port.describe('The port inside the sandbox (a server there may listen on localhost only).'),
37
+ })).describe('Reach servers in the sandbox from this PC: http://localhost:<pc> here goes to <sandbox> inside, while it runs. Nothing else on your network can use them.'),
38
+ dns: z.array(z.string().regex(/^\d{1,3}(\.\d{1,3}){3}$/, 'an IPv4 address like 1.1.1.1')).describe('DNS servers for the sandbox instead of your network\'s, e.g. 1.1.1.1 and 9.9.9.9.'),
39
+ proxy: z.string().regex(/^(https?:\/\/[^\s]+)?$/, 'http://host:port').describe('An HTTP proxy the sandbox uses to reach the internet (HTTP_PROXY / HTTPS_PROXY for everything in it), e.g. http://proxy.corp:8080. Empty = none.'),
40
+ });
41
+ export const Audit = z.strictObject({
42
+ enabled: z.boolean().describe('Keep an audit log on this PC of what happened in your sandboxes: programs started (with their command lines), files changed in the project, sessions, and what the repo guard caught. The sandbox cannot change it. Read it with sandbox audit.'),
43
+ days: z.number().int().min(1).max(365).describe('Days the audit log is kept.'),
44
+ maxMb: z.number().int().min(5).max(5000).describe('Most space the audit log may take, in MB (older days go first). Days before today are compressed.'),
45
+ processes: z.boolean().describe('Record every program started in the sandbox with its command line (Windows process auditing; repeated runs are counted, not listed).'),
46
+ files: z.boolean().describe('Record the files changed in the project while the sandbox runs.'),
47
+ });
48
+ export const WorkspaceFilter = z.strictObject({
49
+ enabled: z.boolean().describe('Keep what your .gitignore lists (.env files, node_modules, build output ...) out of the sandbox. The sandbox works on a copy of the project without those files, kept in step with it both ways within about a second; what it creates that is ignored (its own node_modules, builds) stays in the sandbox. Git works as usual. Off = the project folder itself is mapped.'),
50
+ exclude: z.array(z.string().max(500)).describe('Also keep these out, as .gitignore patterns, e.g. secrets/, *.pem, data/private/.'),
51
+ include: z.array(z.string().max(500)).describe('Let these in although .gitignore lists them, e.g. .env.example, data/fixtures/.'),
25
52
  });
26
53
  export const Git = z.strictObject({ enabled: z.boolean().describe('Provide Git for Windows (and Git Bash, which Claude Code needs).'), access: GitAccess });
27
54
  export const Provisioning = z.strictObject({
@@ -30,6 +57,7 @@ export const Provisioning = z.strictObject({
30
57
  refresh: z.boolean().describe('Re-copy / re-download everything on the next launch.'),
31
58
  git: Git,
32
59
  terminal: toggle('Windows Terminal as the sandbox window (downloaded once).'),
60
+ conpty: toggle('Agent sessions use Microsoft\'s newer pseudo console (conpty.dll, downloaded once) instead of the one built into Windows: no garbled screen after resizing, real scrollback for the mouse wheel. Off = the one built into Windows.'),
33
61
  });
34
62
  export const Downloads = z.strictObject({
35
63
  requireChecksum: z.boolean().describe('Refuse any toolchain, database or tool download that has no published checksum (SHA-256/SHA-512) to verify it against. false = allow those, as before. Downloads that do have a published checksum are always verified. A project\'s .sandbox-config.json can only turn this on.'),
@@ -88,6 +88,9 @@ export declare const Toolchains: z.ZodObject<{
88
88
  }, z.core.$strict>;
89
89
  vscode: z.ZodObject<{
90
90
  enabled: z.ZodBoolean;
91
+ setup: z.ZodBoolean;
92
+ recommended: z.ZodBoolean;
93
+ extensions: z.ZodArray<z.ZodString>;
91
94
  }, z.core.$strict>;
92
95
  environment: z.ZodRecord<z.ZodString, z.ZodString>;
93
96
  }, z.core.$strict>;
@@ -78,6 +78,11 @@ export const Toolchains = z.strictObject({
78
78
  enabled: z.boolean().describe('Mesa 3D (llvmpipe): OpenGL 4.5 in software for games and apps when there is no GPU (sandbox.vGpu off). Copy opengl32.dll and libgallium_wgl.dll from %MESA_DIR% next to the program\'s .exe.'),
79
79
  vulkan: z.boolean().describe('Also software Vulkan 1.4 (Mesa lavapipe) with the Khronos loader, registered for every program; for wgpu set WGPU_BACKEND=vulkan.'),
80
80
  }),
81
- vscode: toggle('Portable VS Code inside the sandbox (code on PATH, desktop shortcut); its settings and extensions persist.'),
81
+ vscode: z.strictObject({
82
+ enabled: z.boolean().describe('Portable VS Code inside the sandbox (code on PATH, desktop shortcut); its settings and extensions persist.'),
83
+ setup: z.boolean().describe('Point VS Code at the sandbox\'s own tools (JDK, Python, Go, compilers, .NET, Git, PowerShell 7 and Git Bash terminals) and install the language extensions for the toolchains in use.'),
84
+ recommended: z.boolean().describe('Also install the extensions a project recommends in its .vscode/extensions.json.'),
85
+ extensions: z.array(z.string().regex(/^[\w-]+\.[\w-]+$/, 'publisher.name, e.g. eamodio.gitlens')).describe('More extensions to install in every sandbox, e.g. eamodio.gitlens, ms-azuretools.vscode-docker.'),
86
+ }),
82
87
  environment: z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, 'letters, digits and _ only'), z.string()).describe('Extra environment variables for the agent and every terminal, e.g. registry mirrors: {"NPM_CONFIG_REGISTRY": "https://...", "PIP_INDEX_URL": "https://..."}.'),
83
88
  });
@@ -8,10 +8,12 @@ export type Harness = z.output<typeof HarnessSchema>;
8
8
  export type Mapping = z.output<typeof MappingSchema>;
9
9
  export type Toolchains = Config['toolchains'];
10
10
  export type ClaudeProfileConfig = Config['claudeProfile'];
11
+ /** label: shown in the launch table, e.g. "outside the project, allowed". */
11
12
  export interface ResolvedMapping {
12
13
  host: string;
13
14
  guest: string;
14
15
  readOnly: boolean;
16
+ label?: string;
15
17
  }
16
18
  export interface LocalConfigFile {
17
19
  path: string;
@@ -39,6 +41,8 @@ export interface RunRequest {
39
41
  nodeVersion?: string;
40
42
  pythonVersion?: string;
41
43
  ask?: (question: string) => Promise<boolean>;
44
+ /** `ask` can reach you (default: stdin is a terminal). Tests set it. */
45
+ interactive?: boolean;
42
46
  }
43
47
  export interface ResolvedRun {
44
48
  config: Config;
@@ -52,6 +56,8 @@ export interface ResolvedRun {
52
56
  versions: ToolVersions;
53
57
  /** Your config asks for guestWorkspace "mirror" (project roots then appear at their PC paths too). */
54
58
  mirror?: boolean;
59
+ /** The project file's folders outside the project that you allowed (host paths; the launch table marks them). */
60
+ outsideMappings?: string[];
55
61
  }
56
62
  /** A requested version plus where the request came from (shown to the user). */
57
63
  export interface VersionSpec {
@@ -100,6 +106,11 @@ export interface GenerateOptions {
100
106
  knownProjects?: string[];
101
107
  /** false: no agent session at boot. */
102
108
  autoSession?: boolean;
109
+ /** The other projects mapped at boot with their effective configs: the sandbox boots with what they want too. */
110
+ bootWants?: {
111
+ projects: import('./project-policy.js').BootWant[];
112
+ keepOff: string[];
113
+ };
103
114
  }
104
115
  export interface Generated {
105
116
  wsbPath: string;
@@ -107,6 +118,10 @@ export interface Generated {
107
118
  launchId: string;
108
119
  sessionPath: string;
109
120
  memoryMb: number;
121
+ /** The config the sandbox boots with: the launch folder's plus what the other projects mapped at boot want. */
122
+ bootConfig: Config;
123
+ /** Boot settings turned on for another project (setting -> project). */
124
+ widened: Record<string, string>;
110
125
  mappings: ResolvedMapping[];
111
126
  profile: ProfileResult;
112
127
  databases: DatabasePlan[];
@@ -123,6 +138,8 @@ export interface Generated {
123
138
  notes: string[];
124
139
  };
125
140
  toolchains: Record<import('./detect-toolchains.js').AutoToolchain, import('./detect-toolchains.js').ToolchainDecision>;
141
+ /** Where extra folders went when their own path could not be used, and the optional ones skipped. */
142
+ mappingNotes: string[];
126
143
  }
127
144
  /** secretsRemoved: what secrets.strip kept on this PC, e.g. "settings.json env.AWS_SECRET_ACCESS_KEY". */
128
145
  export interface ProfileResult {
@@ -140,7 +157,7 @@ export interface GuestSession {
140
157
  workspaceHost: string;
141
158
  /** The launch folder's project id (file-name safe; status files and sessions use it). Older launch folders lack it. */
142
159
  projectId?: string;
143
- /** Where other projects appear (C:\Projects), and the project roots' guest folders. Older launch folders lack them. */
160
+ /** Where project roots appear (C:\Projects), and the guest folders of the roots and the projects mapped at boot. Older launch folders lack them. */
144
161
  projectsDir?: string;
145
162
  roots?: string[];
146
163
  /** false: start no agent session at boot (a restart that adds a project opens its sessions itself). */
@@ -187,4 +204,10 @@ export interface GuestSession {
187
204
  }[];
188
205
  /** sandbox.cpuLimit as a number (null = no cap); sandbox\cpucap.ps1 applies it before anything starts. */
189
206
  cpuLimitPercent?: number | null;
207
+ /** sandbox.pageFileMb (0 = Windows' own); sandbox\pagefile.ps1 applies it at logon. */
208
+ pageFileMb?: number;
209
+ /** Apps to install (catalogue ids and your own programs); missing in launch folders of older versions. */
210
+ apps?: Config['apps'];
211
+ /** audit.enabled and audit.processes: the sandbox reports the programs it starts (host keeps the log). */
212
+ auditProcesses?: boolean;
190
213
  }
@@ -34,6 +34,8 @@ export declare const isInsideOrEqual: (parent: string, child: string) => boolean
34
34
  * plain colours (SGR), so nothing written by the sandbox can drive your terminal (titles, clipboard, cursor tricks).
35
35
  */
36
36
  export declare const safeText: (text: string) => string;
37
+ /** safeText without colours either: for text you are asked to approve, which must not be able to hide parts of itself. */
38
+ export declare const plainText: (text: string) => string;
37
39
  export declare const findOnPath: (names: string[]) => string | null;
38
40
  /** True if `p` itself is a symlink or junction (lstat; never follows it). */
39
41
  export declare const isLink: (p: string) => boolean;
package/dist/core/util.js CHANGED
@@ -81,6 +81,8 @@ export const isInsideOrEqual = (parent, child) => {
81
81
  * plain colours (SGR), so nothing written by the sandbox can drive your terminal (titles, clipboard, cursor tricks).
82
82
  */
83
83
  export const safeText = (text) => text.replace(/\x1b\[[0-9;]*m|\x1b(\[[0-9;?]*[ -\/]*[@-~]|\][^\x07\x1b]*(\x07|\x1b\\)|[@-Z\\-_])|[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, (m) => (/^\x1b\[[0-9;]*m$/.test(m) ? m : ''));
84
+ /** safeText without colours either: for text you are asked to approve, which must not be able to hide parts of itself. */
85
+ export const plainText = (text) => safeText(text).replace(/\x1b\[[0-9;]*m/g, '');
84
86
  export const findOnPath = (names) => {
85
87
  const dirs = (process.env.PATH ?? process.env.Path ?? '').split(path.delimiter).filter((d) => d !== '');
86
88
  for (const name of names)
@@ -1,9 +1,13 @@
1
- import type { Config, Overrides, ResolvedRun, RunRequest, ToolVersions } from './types.js';
1
+ import type { Config, Mapping, Overrides, ResolvedRun, RunRequest, ToolVersions } from './types.js';
2
+ /** 'auto' guest workspace: C:\Workspace\<folder name> (shared/projects.ts, where every project's place is decided). */
3
+ export declare const autoGuestWorkspace: (hostDir: string) => string;
2
4
  /**
3
- * 'auto' guest workspace: C:\Workspace\<folder name>. Keeping the real folder name means Claude's per-project history
4
- * and claude-mem's project name match this PC instead of every project being "Workspace".
5
+ * A launch folder inside a project root appears under the root's folder (C:\Projects\<root>\<name>, or its PC path
6
+ * with "mirror"), where it also is when another project started the sandbox, so git worktrees and Claude's project
7
+ * folders keep working whichever project starts it. Only for "auto" / "mirror"; a filtered or read-only workspace
8
+ * keeps its own mapping (the root would show the folder unfiltered and writable at that place).
5
9
  */
6
- export declare const autoGuestWorkspace: (workspaceHost: string) => string;
10
+ export declare const placeInRoot: (config: Config, workspaceHost: string, mirror: boolean, notes: string[]) => Config;
7
11
  /** The Node version a Volta project pins ("volta": { "node": "20.11.1" } in package.json), so nvm defaults to it too. */
8
12
  export declare const voltaNode: (workspace: string) => string | null;
9
13
  /**
@@ -16,3 +20,16 @@ export declare const resolveVersions: (config: Config, workspace: string, local:
16
20
  }, hostNodeVersion: string) => ToolVersions;
17
21
  /** Precedence (lowest to highest): built-in defaults, user config, .sandbox-config.json in the workspace, command-line flags. */
18
22
  export declare const resolveRun: (req: RunRequest, hostNodeVersion?: string) => Promise<ResolvedRun>;
23
+ /** Where the workspace appears: guestWorkspace "auto", "mirror" (with a note when it falls back) or a path. */
24
+ export declare const guestWorkspaceOf: (config: Pick<Config, "guestWorkspace">, workspaceHost: string) => {
25
+ guest: string;
26
+ note: string | null;
27
+ };
28
+ /**
29
+ * Where a mapping will most likely appear, before the launch knows every other folder (the prompt and config check
30
+ * show it; the launch places it for real in host/launch/mappings.ts with the same rules).
31
+ */
32
+ export declare const previewGuest: (m: Pick<Mapping, "host" | "guest">, workspaceHost: string, workspaceGuest: string) => {
33
+ guest: string;
34
+ note: string | null;
35
+ };
@@ -1,18 +1,29 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { z } from 'zod';
4
- import { planMirrorWorkspace } from '../shared/guest-paths.js';
4
+ import { deriveGuest, placeGuest, planMirrorWorkspace } from '../shared/guest-paths.js';
5
+ import { autoGuestFolder, PROJECTS_GUEST, rootGuestDir, usableRoots } from '../shared/projects.js';
5
6
  import { DATABASES } from './detect-databases.js';
6
- import { applyOverrides, describeOverrides, isTrusted, normalizeOverrides, readLocalConfig, saveTrust, trustStorePath } from './local-config.js';
7
+ import { allowedAnswer, allowRequests, allowStorePath, applyOverrides, describeOverrides, isTrusted, LOCAL_CONFIG_NAME, loosenedRequests, normalizeOverrides, OUTSIDE_FOLDER, outsideKey, outsideMappings, readLocalConfig, saveAllowed, saveTrust, trustStorePath } from './local-config.js';
7
8
  import { assertSafeWorkspace } from './safe-paths.js';
8
- import { dirExists, expandPath, fail, fileExists } from './util.js';
9
+ import { dirExists, expandPath, fail, fileExists, isInsideOrEqual, plainText } from './util.js';
10
+ /** 'auto' guest workspace: C:\Workspace\<folder name> (shared/projects.ts, where every project's place is decided). */
11
+ export const autoGuestWorkspace = autoGuestFolder;
9
12
  /**
10
- * 'auto' guest workspace: C:\Workspace\<folder name>. Keeping the real folder name means Claude's per-project history
11
- * and claude-mem's project name match this PC instead of every project being "Workspace".
13
+ * A launch folder inside a project root appears under the root's folder (C:\Projects\<root>\<name>, or its PC path
14
+ * with "mirror"), where it also is when another project started the sandbox, so git worktrees and Claude's project
15
+ * folders keep working whichever project starts it. Only for "auto" / "mirror"; a filtered or read-only workspace
16
+ * keeps its own mapping (the root would show the folder unfiltered and writable at that place).
12
17
  */
13
- export const autoGuestWorkspace = (workspaceHost) => {
14
- const name = path.basename(workspaceHost).replace(/[^A-Za-z0-9._ -]/g, '_').replace(/^[. ]+|[. ]+$/g, '');
15
- return 'C:\\Workspace\\' + (name || 'project');
18
+ export const placeInRoot = (config, workspaceHost, mirror, notes) => {
19
+ const guest = rootGuestDir(usableRoots(config.projects.roots, dirExists), mirror, workspaceHost);
20
+ if (!guest)
21
+ return config;
22
+ if (config.workspaceFilter.enabled || config.readOnly) {
23
+ notes.push(workspaceHost + ' is inside a project root, but ' + (config.readOnly ? 'readOnly' : 'workspaceFilter') + ' is on, so it is mapped on its own at ' + config.guestWorkspace + ' (when another project starts the sandbox it is at ' + guest + ').');
24
+ return config;
25
+ }
26
+ return { ...config, guestWorkspace: guest };
16
27
  };
17
28
  const firstLine = (file) => {
18
29
  if (!fileExists(file))
@@ -79,24 +90,77 @@ export const resolveRun = async (req, hostNodeVersion = process.version) => {
79
90
  let localConfigPath = null;
80
91
  let overrides = {};
81
92
  const local = req.noLocalConfig ? null : readLocalConfig(workspaceHost);
93
+ let outsideAllowed = [];
82
94
  if (local) {
83
95
  overrides = normalizeOverrides(local.data, workspaceHost, local.path);
84
96
  const store = trustStorePath(storageRoot);
85
97
  if (!isTrusted(store, local.path, local.hash)) {
86
- const summary = describeOverrides(overrides);
98
+ const summary = describeOverrides(overrides, workspaceHost);
87
99
  let trusted = req.trustConfig;
88
100
  if (!trusted && req.ask) {
89
101
  console.log('\nFound ' + local.path + ' which is not trusted yet. It would apply:');
90
102
  for (const line of summary)
91
- console.log(' ' + line);
103
+ console.log(' ' + plainText(line));
92
104
  trusted = await req.ask('Trust and use this file?');
93
105
  }
94
106
  if (!trusted)
95
- fail(local.path + ' has not been approved yet (a repo can ship this file, so it needs your OK). It would apply:\n ' + (summary.join('\n ') || '(no changes)'), 'Review it, then run again in an interactive terminal and answer y, or add --trust-config. To ignore the file: --no-local-config');
107
+ fail(local.path + ' has not been approved yet (a repo can ship this file, so it needs your OK). It would apply:\n ' + (summary.map(plainText).join('\n ') || '(no changes)'), 'Review it, then run again in an interactive terminal and answer y, or add --trust-config. To ignore the file: --no-local-config');
96
108
  saveTrust(store, local.path, local.hash);
97
109
  notes.push('Trusted ' + local.path + ' (you will only be asked again if the file changes).');
98
110
  }
99
- config = applyOverrides(config, overrides);
111
+ // A project may only tighten. What it asks for beyond that (a looser setting, a folder outside the project) is used
112
+ // only if you allowed it for this project: asked once, in one question, at an interactive launch, never by
113
+ // --trust-config. Folders outside the project follow projects.allowOutsideMappings (your settings only). A looser
114
+ // setting you did not allow is named in the notes instead of dropped silently.
115
+ const allowStore = allowStorePath(storageRoot);
116
+ const policy = config.projects.allowOutsideMappings;
117
+ // An optional folder that is not on this PC is skipped at launch, so it needs no answer (yet).
118
+ const outside = outsideMappings(workspaceHost, overrides.mappings).filter((m) => !m.optional || dirExists(m.host));
119
+ const name = path.basename(workspaceHost) + '\'s ' + LOCAL_CONFIG_NAME;
120
+ const preview = guestWorkspaceOf(applyOverrides(config, overrides), workspaceHost).guest;
121
+ const describe = (m) => (m.readOnly ? 'read-only ' : 'READ/WRITE ') + m.host + ' -> ' + previewGuest(m, workspaceHost, preview).guest;
122
+ if (outside.length && policy === false) {
123
+ fail(name + ' maps ' + outside.length + ' folder' + (outside.length === 1 ? '' : 's') + ' outside the project, and your settings do not allow that:\n ' + outside.map(describe).join('\n '), 'Allow it: set projects.allowOutsideMappings to "ask" or true in your settings (sandbox > Settings, or the file sandbox config path shows). Or remove those mappings from the file.');
124
+ }
125
+ const looser = loosenedRequests(config, overrides);
126
+ const interactive = Boolean(req.ask) && (req.interactive ?? Boolean(process.stdin.isTTY));
127
+ const openOutside = policy === 'ask' ? outside.filter((m) => allowedAnswer(allowStore, local.path, OUTSIDE_FOLDER, outsideKey(m)) !== true) : [];
128
+ const openLooser = looser.filter((a) => allowedAnswer(allowStore, local.path, a.key, a.wanted) === null);
129
+ let declinedOutside = false;
130
+ if (interactive && req.ask && (openOutside.length || openLooser.length)) {
131
+ console.log('\n' + name + ' asks for more than your settings allow:');
132
+ if (openOutside.length)
133
+ console.log(' Folders outside the project:\n' + openOutside.map((m) => ' ' + plainText(describe(m))).join('\n'));
134
+ for (const a of openLooser)
135
+ console.log(' ' + a.key + ' = ' + JSON.stringify(a.wanted) + ' (your settings: ' + JSON.stringify(a.yours) + ')');
136
+ const ok = await req.ask('Allow ' + (openOutside.length + openLooser.length === 1 ? 'it' : 'these') + ' for this project?');
137
+ for (const a of openLooser)
138
+ saveAllowed(allowStore, local.path, a.key, a.wanted, ok);
139
+ // A "no" to folders is not kept: the next interactive launch asks again.
140
+ if (ok)
141
+ for (const m of openOutside)
142
+ saveAllowed(allowStore, local.path, OUTSIDE_FOLDER, outsideKey(m), true);
143
+ declinedOutside = !ok && openOutside.length > 0;
144
+ }
145
+ const notAllowed = policy === 'ask' ? outside.filter((m) => allowedAnswer(allowStore, local.path, OUTSIDE_FOLDER, outsideKey(m)) !== true) : [];
146
+ if (notAllowed.length) {
147
+ const what = name + ' maps ' + notAllowed.length + ' folder' + (notAllowed.length === 1 ? '' : 's') + ' outside the project:\n ' + notAllowed.map(describe).join('\n ');
148
+ fail(what + (declinedOutside ? '\nYou did not allow ' + (notAllowed.length === 1 ? 'it' : 'them') + ', so the sandbox was not started.' : '\nThat needs your OK once (a repo ships this file), and this launch cannot ask (no interactive terminal).'), 'Allow ' + (notAllowed.length === 1 ? 'it' : 'them') + ': start it from an interactive terminal and answer y, or set projects.allowOutsideMappings to true in your settings (sandbox > Settings, or the file sandbox config path shows).');
149
+ }
150
+ outsideAllowed = [...new Set(outside.map((m) => m.host))];
151
+ if (outside.length)
152
+ notes.push('Folders outside the project allowed for ' + name + ' (' + (policy === true ? 'projects.allowOutsideMappings is true' : 'your answer is kept in ' + allowStore) + '): ' + outsideAllowed.join(', '));
153
+ const allowed = [];
154
+ for (const ask of looser) {
155
+ const ok = allowedAnswer(allowStore, local.path, ask.key, ask.wanted);
156
+ if (ok) {
157
+ allowed.push(ask);
158
+ notes.push('Allowed for this project: ' + ask.key + ' = ' + JSON.stringify(ask.wanted) + ' (your answer is kept in ' + allowStore + ').');
159
+ }
160
+ else
161
+ notes.push('The project asks for ' + ask.key + ' = ' + JSON.stringify(ask.wanted) + ', but your settings say ' + JSON.stringify(ask.yours) + ', so it is not used. ' + (ok === false ? 'You said no (kept in ' + allowStore + ').' : 'Start it from an interactive terminal to allow it for this project.') + ' Or set ' + ask.key + ' in your own settings (sandbox, then Settings > All projects).');
162
+ }
163
+ config = applyOverrides(allowRequests(config, allowed), overrides);
100
164
  scripts.push(...(overrides.scripts ?? []));
101
165
  localConfigPath = local.path;
102
166
  notes.push('Using ' + local.path);
@@ -109,14 +173,12 @@ export const resolveRun = async (req, hostNodeVersion = process.version) => {
109
173
  fail('Unknown database "' + name + '".', 'Choose from: ' + DATABASES.join(', '));
110
174
  config = { ...config, databases: { ...config.databases, [db]: { ...config.databases[db], enabled: true } } };
111
175
  }
112
- if (config.guestWorkspace === 'auto')
113
- config = { ...config, guestWorkspace: autoGuestWorkspace(workspaceHost) };
114
- else if (config.guestWorkspace === 'mirror') {
115
- const plan = planMirrorWorkspace(workspaceHost, autoGuestWorkspace(workspaceHost));
116
- if (plan.note)
117
- notes.push(plan.note);
118
- config = { ...config, guestWorkspace: plan.guest };
119
- }
176
+ // auto / mirror: a project inside a root is where the root shows it, whichever project starts the sandbox.
177
+ const guest = guestWorkspaceOf(config, workspaceHost);
178
+ const placed = config.guestWorkspace === 'auto' || mirror ? placeInRoot({ ...config, guestWorkspace: guest.guest }, workspaceHost, mirror, notes) : { ...config, guestWorkspace: guest.guest };
179
+ if (guest.note && placed.guestWorkspace === guest.guest)
180
+ notes.push(guest.note);
181
+ config = placed;
120
182
  scripts.push(...req.scripts.map((s) => path.resolve(s)));
121
183
  const harnessName = req.noAgent ? 'none' : (req.harness ?? config.defaultHarness);
122
184
  if (harnessName !== 'none' && !config.harnesses[harnessName])
@@ -124,5 +186,21 @@ export const resolveRun = async (req, hostNodeVersion = process.version) => {
124
186
  const versions = resolveVersions(config, workspaceHost, overrides, req, hostNodeVersion);
125
187
  const dataPath = path.join(storageRoot, 'data', config.storage.dataName);
126
188
  // A project file may ask for "mirror" too (it decides only its own path); project roots follow the user's choice.
127
- return { config, storageRoot, dataPath, workspaceHost, harnessName, scripts, localConfigPath, notes, versions, mirror };
189
+ return { config, storageRoot, dataPath, workspaceHost, harnessName, scripts, localConfigPath, notes, versions, mirror, outsideMappings: outsideAllowed };
190
+ };
191
+ /** Where the workspace appears: guestWorkspace "auto", "mirror" (with a note when it falls back) or a path. */
192
+ export const guestWorkspaceOf = (config, workspaceHost) => {
193
+ if (config.guestWorkspace === 'auto')
194
+ return { guest: autoGuestWorkspace(workspaceHost), note: null };
195
+ if (config.guestWorkspace === 'mirror')
196
+ return planMirrorWorkspace(workspaceHost, autoGuestWorkspace(workspaceHost));
197
+ return { guest: config.guestWorkspace, note: null };
128
198
  };
199
+ /**
200
+ * Where a mapping will most likely appear, before the launch knows every other folder (the prompt and config check
201
+ * show it; the launch places it for real in host/launch/mappings.ts with the same rules).
202
+ */
203
+ export const previewGuest = (m, workspaceHost, workspaceGuest) => placeGuest(m.host, m.guest ?? deriveGuest(m.host, workspaceHost, workspaceGuest), [
204
+ ...(m.guest || !isInsideOrEqual(workspaceHost, m.host) ? [{ guest: workspaceGuest, what: 'the project' }] : []),
205
+ { guest: PROJECTS_GUEST, what: 'other projects' },
206
+ ]);
@@ -0,0 +1,10 @@
1
+ import type { Config, GitAccess } from '../../core/types.js';
2
+ import { type ToolEnv } from '../toolchains/common.js';
3
+ export { appsNeedMesa } from '../../shared/apps.js';
4
+ export interface AppsOptions {
5
+ gitAccess: GitAccess;
6
+ refresh: boolean;
7
+ }
8
+ /** The program in an installed folder (breadth first, bounded: big apps have tens of thousands of files). */
9
+ export declare const findExe: (root: string, exe: string) => string | null;
10
+ export declare const setupApps: (env: ToolEnv, apps: Config["apps"], o: AppsOptions) => Promise<void>;