windows-development-sandbox 1.1.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +3 -2
  2. package/config.example.json +38 -5
  3. package/dist/cli/args.d.ts +3 -0
  4. package/dist/cli/args.js +3 -1
  5. package/dist/cli/commands/accounts.js +2 -2
  6. package/dist/cli/commands/agent-commands.js +1 -1
  7. package/dist/cli/commands/context.js +2 -1
  8. package/dist/cli/commands/data/clean.js +6 -1
  9. package/dist/cli/commands/data/collect.js +3 -3
  10. package/dist/cli/commands/index.d.ts +1 -1
  11. package/dist/cli/commands/index.js +1 -1
  12. package/dist/cli/commands/launch.js +36 -3
  13. package/dist/cli/commands/maintenance.d.ts +2 -0
  14. package/dist/cli/commands/maintenance.js +35 -2
  15. package/dist/cli/commands/sessions.js +3 -3
  16. package/dist/cli/commands/user-config.js +1 -1
  17. package/dist/cli/index.js +6 -4
  18. package/dist/cli/manifest.js +5 -0
  19. package/dist/cli/menu/apps.d.ts +21 -0
  20. package/dist/cli/menu/apps.js +122 -0
  21. package/dist/cli/menu/command-form.d.ts +28 -0
  22. package/dist/cli/menu/command-form.js +219 -0
  23. package/dist/cli/menu/index.d.ts +26 -0
  24. package/dist/cli/menu/index.js +180 -0
  25. package/dist/cli/menu/settings/categories.d.ts +9 -0
  26. package/dist/cli/menu/settings/categories.js +28 -0
  27. package/dist/cli/menu/settings/config-file.d.ts +40 -0
  28. package/dist/cli/menu/settings/config-file.js +71 -0
  29. package/dist/cli/menu/settings/editors.d.ts +30 -0
  30. package/dist/cli/menu/settings/editors.js +305 -0
  31. package/dist/cli/menu/settings/index.d.ts +32 -0
  32. package/dist/cli/menu/settings/index.js +136 -0
  33. package/dist/cli/menu/settings/schema-node.d.ts +62 -0
  34. package/dist/cli/menu/settings/schema-node.js +104 -0
  35. package/dist/cli/{menu-multi.d.ts → menu/several.d.ts} +10 -2
  36. package/dist/cli/menu/several.js +102 -0
  37. package/dist/cli/tui/app.d.ts +39 -0
  38. package/dist/cli/tui/app.js +52 -0
  39. package/dist/cli/tui/dialogs.d.ts +57 -0
  40. package/dist/cli/tui/dialogs.js +162 -0
  41. package/dist/cli/tui/frame.d.ts +18 -0
  42. package/dist/cli/tui/frame.js +27 -0
  43. package/dist/cli/tui/index.d.ts +7 -0
  44. package/dist/cli/tui/index.js +8 -0
  45. package/dist/cli/tui/list-screen.d.ts +101 -0
  46. package/dist/cli/tui/list-screen.js +299 -0
  47. package/dist/cli/tui/terminal.d.ts +37 -0
  48. package/dist/cli/tui/terminal.js +89 -0
  49. package/dist/cli/tui/text.d.ts +20 -0
  50. package/dist/cli/tui/text.js +105 -0
  51. package/dist/cli/tui/theme.d.ts +45 -0
  52. package/dist/cli/tui/theme.js +30 -0
  53. package/dist/core/config.js +9 -6
  54. package/dist/core/local-config.d.ts +15 -0
  55. package/dist/core/local-config.js +63 -4
  56. package/dist/core/schema/apps.d.ts +40 -0
  57. package/dist/core/schema/apps.js +27 -0
  58. package/dist/core/schema/config.d.ts +66 -1
  59. package/dist/core/schema/config.js +18 -6
  60. package/dist/core/schema/profile.d.ts +5 -0
  61. package/dist/core/schema/profile.js +4 -2
  62. package/dist/core/schema/sandbox.d.ts +28 -0
  63. package/dist/core/schema/sandbox.js +28 -0
  64. package/dist/core/schema/toolchains.d.ts +3 -0
  65. package/dist/core/schema/toolchains.js +6 -1
  66. package/dist/core/types.d.ts +6 -0
  67. package/dist/core/util.d.ts +2 -0
  68. package/dist/core/util.js +2 -0
  69. package/dist/core/workspace.js +22 -5
  70. package/dist/guest/apps/index.d.ts +10 -0
  71. package/dist/guest/apps/index.js +206 -0
  72. package/dist/guest/apps/resolvers.d.ts +8 -0
  73. package/dist/guest/apps/resolvers.js +90 -0
  74. package/dist/guest/audit-parse.d.ts +7 -0
  75. package/dist/guest/audit-parse.js +24 -0
  76. package/dist/guest/audit.d.ts +4 -0
  77. package/dist/guest/audit.js +47 -0
  78. package/dist/guest/daemon/relays.d.ts +1 -0
  79. package/dist/guest/daemon/relays.js +18 -0
  80. package/dist/guest/daemon.js +4 -0
  81. package/dist/guest/env.d.ts +1 -0
  82. package/dist/guest/env.js +3 -1
  83. package/dist/guest/helpers.d.ts +1 -1
  84. package/dist/guest/main.js +15 -2
  85. package/dist/guest/netproxy.d.ts +1 -0
  86. package/dist/guest/netproxy.js +100 -0
  87. package/dist/guest/ptyhost.js +14 -4
  88. package/dist/guest/setup/claude.d.ts +0 -4
  89. package/dist/guest/setup/claude.js +48 -2
  90. package/dist/guest/setup/network.d.ts +9 -0
  91. package/dist/guest/setup/network.js +97 -4
  92. package/dist/guest/setup/user.js +4 -0
  93. package/dist/guest/toolchains/native/mesa.js +13 -3
  94. package/dist/guest/toolchains/plan.d.ts +3 -1
  95. package/dist/guest/toolchains/plan.js +53 -9
  96. package/dist/guest/toolchains/python.js +26 -2
  97. package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
  98. package/dist/guest/toolchains/vscode-profile.js +69 -0
  99. package/dist/guest/toolchains/vscode.d.ts +17 -0
  100. package/dist/guest/toolchains/vscode.js +87 -1
  101. package/dist/host/attach.js +2 -1
  102. package/dist/host/audit/collect.d.ts +34 -0
  103. package/dist/host/audit/collect.js +126 -0
  104. package/dist/host/audit/log.d.ts +46 -0
  105. package/dist/host/audit/log.js +143 -0
  106. package/dist/host/generate.d.ts +6 -0
  107. package/dist/host/generate.js +10 -0
  108. package/dist/host/launch/mappings.js +5 -1
  109. package/dist/host/launch/stage.js +1 -1
  110. package/dist/host/motw.d.ts +17 -0
  111. package/dist/host/motw.js +74 -0
  112. package/dist/host/platform.js +3 -0
  113. package/dist/host/port-forward.d.ts +8 -0
  114. package/dist/host/port-forward.js +37 -0
  115. package/dist/host/repo-guard.d.ts +10 -3
  116. package/dist/host/repo-guard.js +142 -14
  117. package/dist/host/stops.d.ts +67 -0
  118. package/dist/host/stops.js +126 -0
  119. package/dist/host/terminal.d.ts +8 -0
  120. package/dist/host/terminal.js +45 -1
  121. package/dist/host/workspace-view/index.d.ts +20 -0
  122. package/dist/host/workspace-view/index.js +53 -0
  123. package/dist/host/workspace-view/rules.d.ts +26 -0
  124. package/dist/host/workspace-view/rules.js +93 -0
  125. package/dist/host/workspace-view/view.d.ts +50 -0
  126. package/dist/host/workspace-view/view.js +281 -0
  127. package/dist/runtime/daemon.js +1319 -552
  128. package/dist/runtime/db.js +168 -13
  129. package/dist/runtime/main.js +2898 -608
  130. package/dist/runtime/netproxy.js +25936 -0
  131. package/dist/runtime/view.js +272 -23
  132. package/dist/selftest/audit.test.d.ts +2 -0
  133. package/dist/selftest/audit.test.js +57 -0
  134. package/dist/selftest/channel.test.js +7 -0
  135. package/dist/selftest/cli.test.js +47 -17
  136. package/dist/selftest/fixtures.d.ts +45 -0
  137. package/dist/selftest/fixtures.js +3 -1
  138. package/dist/selftest/generate.test.js +7 -2
  139. package/dist/selftest/index.js +5 -2
  140. package/dist/selftest/live-network.test.d.ts +2 -0
  141. package/dist/selftest/live-network.test.js +63 -0
  142. package/dist/selftest/local-config.test.js +16 -3
  143. package/dist/selftest/network.test.js +12 -0
  144. package/dist/selftest/projects.test.js +1 -1
  145. package/dist/selftest/repo-guard.test.js +59 -1
  146. package/dist/selftest/sync.test.js +50 -10
  147. package/dist/selftest/toolchains.test.js +30 -0
  148. package/dist/selftest/workspace-view.test.d.ts +2 -0
  149. package/dist/selftest/workspace-view.test.js +89 -0
  150. package/dist/shared/apps.d.ts +49 -0
  151. package/dist/shared/apps.js +114 -0
  152. package/dist/shared/netpolicy.d.ts +27 -0
  153. package/dist/shared/netpolicy.js +51 -0
  154. package/dist/shared/pty-client.d.ts +9 -4
  155. package/dist/shared/pty-client.js +139 -17
  156. package/dist/shared/rundir.d.ts +2 -0
  157. package/dist/shared/rundir.js +2 -0
  158. package/dist/shared/schema.d.ts +10 -0
  159. package/dist/shared/schema.js +2 -0
  160. package/dist/shared/system.d.ts +1 -0
  161. package/dist/shared/system.js +1 -0
  162. package/dist/sync/claude-mem.js +3 -2
  163. package/dist/sync/history.d.ts +6 -0
  164. package/dist/sync/history.js +26 -11
  165. package/dist/sync/watcher.js +78 -6
  166. package/package.json +2 -1
  167. package/sandbox/cargoshim.cs +56 -0
  168. package/sandbox/cpucap.cs +33 -0
  169. package/sandbox/cpucap.ps1 +4 -35
  170. package/sandbox/logon.cmd +6 -0
  171. package/sandbox/memlog.ps1 +34 -0
  172. package/sandbox/pagefile.cs +31 -0
  173. package/sandbox/pagefile.ps1 +21 -0
  174. package/sandbox/ptyhost.cs +105 -7
  175. package/schemas/config.schema.json +303 -2
  176. package/schemas/sandbox-config.schema.json +126 -12
  177. package/dist/cli/menu-multi.js +0 -82
  178. package/dist/cli/menu.d.ts +0 -14
  179. package/dist/cli/menu.js +0 -290
  180. package/dist/cli/settings/edit.d.ts +0 -13
  181. package/dist/cli/settings/edit.js +0 -19
  182. package/dist/cli/settings/index.d.ts +0 -8
  183. package/dist/cli/settings/index.js +0 -131
  184. package/dist/cli/settings/prompts.d.ts +0 -7
  185. package/dist/cli/settings/prompts.js +0 -48
  186. package/dist/cli/settings/schema.d.ts +0 -33
  187. package/dist/cli/settings/schema.js +0 -29
package/README.md CHANGED
@@ -31,7 +31,7 @@ works too. [Other ways to install](docs/install.md)
31
31
  ```powershell
32
32
  cd C:\code\my-app
33
33
 
34
- sandbox # menu: start an agent, a plain sandbox, diagnostics, cleanup ...
34
+ sandbox # the menu: start agents, sessions, apps, every setting, diagnostics ...
35
35
  sandbox claude # Claude Code in a sandbox for this folder (or: claude-sandbox)
36
36
  sandbox claude -t # ... in this terminal (Ctrl+] detaches, the session keeps running)
37
37
  sandbox start # just the sandbox, no agent
@@ -49,6 +49,7 @@ The first start downloads what the project needs (a few minutes, once). After th
49
49
  |---|---|
50
50
  | **Safe by default** | The agent sees only your project folder. Credentials are stripped before anything is copied in, your LAN is blocked, and nothing the sandbox writes can run on your PC. |
51
51
  | **Languages** | Node, Python, Rust, C/C++ (GCC or MSVC), .NET (incl. .NET Framework targeting), Go, Java, Ruby, PHP, Deno, Bun, PowerShell 7, with pnpm, yarn, Volta, uv, pyenv, Maven, Gradle, Composer, gh ... |
52
+ | **Apps and tools** | VS Code (set up for your languages, with their extensions) and Windows Terminal, plus 60 optional apps and tools by category: DBeaver, pgAdmin, Bruno, Godot, Blender, kubectl, jq, ripgrep ... and programs of your own |
52
53
  | **Databases** | PostgreSQL, MySQL, MongoDB, Redis and DynamoDB Local with a web UI, started when the project uses them, or any time with `--database` / `sandbox db` |
53
54
  | **Agents** | Claude Code, Codex, Gemini, opencode, Qwen, Crush, Copilot, Kilo, Amp, Cline, Continue, Kimi, Aider, DeepSeek agents |
54
55
  | **Smart** | Detects what the project needs from its files, respects version files, installs in parallel and reuses everything |
@@ -60,7 +61,7 @@ The first start downloads what the project needs (a few minutes, once). After th
60
61
 
61
62
  | Command | |
62
63
  |---|---|
63
- | `sandbox` | the interactive menu: repeat your last command, start agents, every option, and all settings |
64
+ | `sandbox` | the menu, a full-screen app: start agents, attach to sessions, pick apps, edit every setting (yours and the project's), all commands, with search |
64
65
  | `sandbox <agent>`, `claude-sandbox` | start an agent; `-t` for this terminal, `--headless` for no window |
65
66
  | `sandbox start` | the sandbox without an agent |
66
67
  | `sandbox attach` / `sessions` / `status` | work with a running sandbox (this folder's project first) |
@@ -3,12 +3,25 @@
3
3
  "defaultHarness": "claude",
4
4
  "guestWorkspace": "auto",
5
5
  "readOnly": false,
6
+ "workspaceFilter": {
7
+ "enabled": false,
8
+ "exclude": [],
9
+ "include": []
10
+ },
11
+ "audit": {
12
+ "enabled": false,
13
+ "days": 14,
14
+ "maxMb": 100,
15
+ "processes": true,
16
+ "files": true
17
+ },
6
18
  "mappings": [],
7
19
  "sandbox": {
8
20
  "memoryMb": "auto",
9
21
  "maxMemoryMb": 32768,
10
22
  "maxMemoryPercent": 80,
11
23
  "cpuLimit": 80,
24
+ "pageFileMb": 16384,
12
25
  "reserveHostMb": 8192,
13
26
  "vGpu": false,
14
27
  "networking": true,
@@ -17,15 +30,23 @@
17
30
  "videoInput": false,
18
31
  "printerRedirection": false,
19
32
  "protectedClient": true,
20
- "headless": false
33
+ "headless": false,
34
+ "keepAwake": true
21
35
  },
22
36
  "storage": {
23
37
  "root": "%LOCALAPPDATA%\\AgentSandbox",
24
38
  "persistAgentState": true,
25
- "dataName": "default"
39
+ "dataName": "default",
40
+ "markSandboxFiles": true,
41
+ "warnDataGb": 50
26
42
  },
27
43
  "network": {
28
- "blockPrivateRanges": true
44
+ "blockPrivateRanges": true,
45
+ "allowOnly": [],
46
+ "blockHosts": [],
47
+ "forwardPorts": [],
48
+ "dns": [],
49
+ "proxy": ""
29
50
  },
30
51
  "provisioning": {
31
52
  "copyFromHost": true,
@@ -37,6 +58,9 @@
37
58
  },
38
59
  "terminal": {
39
60
  "enabled": true
61
+ },
62
+ "conpty": {
63
+ "enabled": true
40
64
  }
41
65
  },
42
66
  "downloads": {
@@ -135,7 +159,10 @@
135
159
  "vulkan": false
136
160
  },
137
161
  "vscode": {
138
- "enabled": true
162
+ "enabled": true,
163
+ "setup": true,
164
+ "recommended": true,
165
+ "extensions": []
139
166
  },
140
167
  "environment": {}
141
168
  },
@@ -188,6 +215,10 @@
188
215
  "personalization": {
189
216
  "enabled": true
190
217
  },
218
+ "apps": {
219
+ "install": [],
220
+ "custom": {}
221
+ },
191
222
  "claudeProfile": {
192
223
  "enabled": true,
193
224
  "hostDir": "auto",
@@ -203,7 +234,9 @@
203
234
  "copyCredentials": false,
204
235
  "overwrite": true,
205
236
  "syncHistory": true,
206
- "syncClaudeMem": true
237
+ "historyScope": "project",
238
+ "syncClaudeMem": true,
239
+ "syncClaudeMemBack": false
207
240
  },
208
241
  "harnesses": {
209
242
  "claude": {
@@ -29,6 +29,9 @@ export interface CliOptions {
29
29
  all: boolean;
30
30
  /** launch --session <agent>@<folder> (repeatable): several sessions at once, folders made absolute. */
31
31
  sessions: SessionWant[];
32
+ /** audit: only events of this kind, and containing this text. */
33
+ kind?: string;
34
+ grep?: string;
32
35
  }
33
36
  /** One session to start: an agent in a folder, with arguments of its own (after the ones given after --). */
34
37
  export interface SessionWant {
package/dist/cli/args.js CHANGED
@@ -134,7 +134,9 @@ export const parseCli = (argv, wrapper = false) => {
134
134
  all: Boolean(o.all),
135
135
  sessions: (o.session ?? []).map((s) => parseSessionWant(s)),
136
136
  timeout: o.timeout ?? 600,
137
- days: o.days ?? 7,
137
+ days: o.days ?? (name === 'audit' ? 1 : 7),
138
+ kind: o.kind,
139
+ grep: o.grep,
138
140
  };
139
141
  };
140
142
  const defaults = () => ({
@@ -3,7 +3,7 @@ import { activateAccount, listAccounts, removeAccount, saveAccount, setDefaultAc
3
3
  import { restartSessions, runningSessions } from '../../host/channel.js';
4
4
  import { hostClaudeDir, hostGlobalStateFile } from '../../host/claude-profile.js';
5
5
  import { isSandboxRunning } from '../../host/platform.js';
6
- import { fail } from '../../core/util.js';
6
+ import { fail, safeText } from '../../core/util.js';
7
7
  import { printJson, sandboxAlive, table } from './context.js';
8
8
  const label = (a) => '"' + a.name + '"' + (a.email ? ' (' + a.email + ')' : '');
9
9
  /** This PC's Claude login location (config folder + global state file). */
@@ -27,7 +27,7 @@ const use = async (ctx, name) => {
27
27
  console.log('Sandbox now uses ' + label(activateAccount(ctx.storageRoot, ctx.dataPath, name)) + '.');
28
28
  const claudeRunning = isSandboxRunning() && runningSessions(ctx.dataPath).some((s) => s.harness === 'claude');
29
29
  if (claudeRunning && !ctx.options.noRestart)
30
- console.log((await restartSessions(ctx.dataPath, 'claude', sandboxAlive)).message + ' with --continue; the conversation carries on under the new account.');
30
+ console.log(safeText((await restartSessions(ctx.dataPath, 'claude', sandboxAlive)).message) + ' with --continue; the conversation carries on under the new account.');
31
31
  return 0;
32
32
  };
33
33
  export const account = async (ctx) => {
@@ -7,7 +7,7 @@ import { agentCommands, commandsDir, reconcileAgentCommands } from '../../host/a
7
7
  import { insideSandbox } from '../../host/platform.js';
8
8
  import { writeFileAtomic } from '../../shared/rundir.js';
9
9
  import { c } from '../colors.js';
10
- import { editConfigText } from '../settings/edit.js';
10
+ import { editConfigText } from '../menu/settings/config-file.js';
11
11
  import { table } from './context.js';
12
12
  const entryOf = (ctx) => path.join(ctx.packageRoot, 'dist', 'bin', 'sandbox.js');
13
13
  /** Brings this PC in line with agentCommands and says what changed (used after on/off, settings and launch). */
@@ -36,8 +36,9 @@ export const table = (rows) => {
36
36
  };
37
37
  /** Prints `value` as JSON when --json was given; returns whether it did. */
38
38
  export const printJson = (ctx, value) => {
39
+ // JSON.stringify leaves C1 controls (U+0080-U+009F) as they are; text from the sandbox must not reach a terminal raw.
39
40
  if (ctx.options.json)
40
- console.log(JSON.stringify(value, null, 2));
41
+ console.log(JSON.stringify(value, null, 2).replace(/[\u0080-\u009f]/g, (ch) => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0')));
41
42
  return ctx.options.json;
42
43
  };
43
44
  export const sandboxAlive = () => isSandboxRunning();
@@ -17,7 +17,7 @@ const ALL_WARNING = 'removes EVERYTHING the sandbox keeps: toolchains, databases
17
17
  * not a plain relative target inside the data folder: absolute paths, drive letters, `..`, wildcards, unknown areas.
18
18
  */
19
19
  export const resolveCleanTargets = (inputs, dataPath, storageRoot) => {
20
- const usage = 'Targets: all | tools | tools/<name> | tools/agents/<name> | databases | databases/<name> | state | state/<agent> | logs | launches. See them with: sandbox data';
20
+ const usage = 'Targets: all | tools | tools/<name> | tools/agents/<name> | databases | databases/<name> | state | state/<agent> | logs | launches | views. See them with: sandbox data';
21
21
  if (!inputs.length)
22
22
  fail('Say what to remove.', usage);
23
23
  const resolved = inputs.map((raw) => {
@@ -39,6 +39,11 @@ export const resolveCleanTargets = (inputs, dataPath, storageRoot) => {
39
39
  return { target: 'all', path: dataPath, root: path.join(storageRoot, 'data'), warning: ALL_WARNING };
40
40
  if (area === 'launches')
41
41
  return { target, path: path.join(storageRoot, 'launches', ...rest), root: storageRoot };
42
+ if (area === 'views') {
43
+ if (rest.length)
44
+ fail('Remove all views ("views"), not single ones.', usage);
45
+ return { target, path: path.join(storageRoot, 'views'), root: storageRoot, warning: 'Views are the copies of your projects without their git-ignored files (workspaceFilter). Your projects are not touched; what the sandbox built or installed in them (node_modules, build output) goes, and the next start copies the projects again.' };
46
+ }
42
47
  if (area === 'accounts' || area === 'sync')
43
48
  fail('"' + area + '" is kept on this PC on purpose and is not removed here.', area === 'accounts' ? 'Remove a saved login with: sandbox account remove <name>' : usage);
44
49
  if (!DATA_AREAS.includes(area))
@@ -35,7 +35,7 @@ export const collectDataEntries = (dataPath, storageRoot) => {
35
35
  });
36
36
  const otherData = entriesOf(path.join(storageRoot, 'data')).filter((d) => d.isDirectory() && path.join(storageRoot, 'data', d.name).toLowerCase() !== path.resolve(dataPath).toLowerCase());
37
37
  const host = [
38
- ...['launches', 'accounts', 'sync', 'logs', 'run', 'host'].map((name) => ({ area: 'host', name, path: path.join(storageRoot, name), bytes: sizeOf(path.join(storageRoot, name)) })),
38
+ ...['launches', 'views', 'accounts', 'sync', 'logs', 'run', 'host'].map((name) => ({ area: 'host', name, path: path.join(storageRoot, name), bytes: sizeOf(path.join(storageRoot, name)) })),
39
39
  ...otherData.map((d) => ({ area: 'host', name: 'other data folder: ' + d.name, path: path.join(storageRoot, 'data', d.name), bytes: sizeOf(path.join(storageRoot, 'data', d.name)) })),
40
40
  ].filter((r) => fs.existsSync(r.path));
41
41
  return _.sortBy([...rows, ...host], (r) => AREA_ORDER.indexOf(r.area), (r) => -r.bytes, (r) => r.name.toLowerCase());
@@ -48,7 +48,7 @@ export const cleanTargetOf = (entry) => {
48
48
  return entry.name;
49
49
  if (entry.area === 'logs')
50
50
  return 'logs';
51
- if (entry.area === 'host' && entry.name === 'launches')
52
- return 'launches';
51
+ if (entry.area === 'host' && (entry.name === 'launches' || entry.name === 'views'))
52
+ return entry.name;
53
53
  return null;
54
54
  };
@@ -6,5 +6,5 @@ export { repairCommand } from './repair/index.js';
6
6
  export { attach, db, exec, hide, restart, sessions, show, status, stop } from './sessions.js';
7
7
  export { account } from './accounts.js';
8
8
  export { projects } from './projects.js';
9
- export { claudeHost, clean, config, doctor, logs, sync, watcher } from './maintenance.js';
9
+ export { audit, claudeHost, clean, config, doctor, logs, sync, watcher } from './maintenance.js';
10
10
  export { agentCommandsCommand, syncAgentCommands } from './agent-commands.js';
@@ -6,5 +6,5 @@ export { repairCommand } from './repair/index.js';
6
6
  export { attach, db, exec, hide, restart, sessions, show, status, stop } from './sessions.js';
7
7
  export { account } from './accounts.js';
8
8
  export { projects } from './projects.js';
9
- export { claudeHost, clean, config, doctor, logs, sync, watcher } from './maintenance.js';
9
+ export { audit, claudeHost, clean, config, doctor, logs, sync, watcher } from './maintenance.js';
10
10
  export { agentCommandsCommand, syncAgentCommands } from './agent-commands.js';
@@ -13,7 +13,12 @@ import { findSecretFiles } from '../../host/secrets.js';
13
13
  import { checkRepo, saveRepoSnapshot } from '../../host/repo-guard.js';
14
14
  import { assertCanLaunch, insideSandbox, isSandboxRunning, probeHost, spawnDetached, startSandbox } from '../../host/platform.js';
15
15
  import { NeedsRestart, rememberProject, startRegistry } from '../../host/projects.js';
16
- import { ensureTerminal } from '../../host/terminal.js';
16
+ import { ensureConpty, ensureTerminal } from '../../host/terminal.js';
17
+ import { planView, saveActiveViews, viewSkipped, WorkspaceView } from '../../host/workspace-view/index.js';
18
+ import { saveForwards } from '../../host/port-forward.js';
19
+ import { saveAuditSettings } from '../../host/audit/collect.js';
20
+ import { readDataSize, saveMarkedProjects } from '../../host/motw.js';
21
+ import { humanBytes } from '../../core/disk.js';
17
22
  import { hideSandboxWindow, showSandboxWindow } from '../../host/window.js';
18
23
  import { readSandboxStatus } from '../../shared/rundir.js';
19
24
  import { isWithin } from '../../shared/projects.js';
@@ -31,6 +36,7 @@ import _ from 'lodash';
31
36
  import { hereDir, joinHere, readyHere } from './project.js';
32
37
  import { offerRestart } from './restart.js';
33
38
  import { addToList, setSetting } from './user-config.js';
39
+ import { lastStopWarning, startLife, UPDATE_RESTART_WARNING, updateRestartPending } from '../../host/stops.js';
34
40
  /** Attaches this terminal; with attach.autoHide the sandbox window hides meanwhile (best effort, never fails the attach). */
35
41
  export const attachHere = (ctx, session) => {
36
42
  if (ctx.loaded.config.attach.autoHide && !insideSandbox())
@@ -216,6 +222,25 @@ const maybeAddRoot = async (ctx, run) => {
216
222
  if (answer === 'never')
217
223
  setSetting(ctx, ['projects', 'askToAddRoot'], false);
218
224
  };
225
+ /**
226
+ * workspaceFilter: brings the project's filtered view in step before the sandbox maps it, and tells the background
227
+ * watcher to keep it in step while the sandbox runs. Returns the line for the launch table.
228
+ */
229
+ const prepareWorkspaceView = async (ctx, run) => {
230
+ const plan = planView(run.config, ctx.storageRoot, run.workspaceHost);
231
+ saveActiveViews(ctx.storageRoot, ctx.dataPath, plan ? [plan.options] : []);
232
+ saveForwards(ctx.storageRoot, ctx.dataPath, run.config.sandbox.networking ? run.config.network.forwardPorts : []);
233
+ saveAuditSettings(ctx.storageRoot, ctx.dataPath, { ...run.config.audit, projects: [run.workspaceHost] });
234
+ saveMarkedProjects(ctx.storageRoot, ctx.dataPath, run.config.storage.markSandboxFiles ? [run.workspaceHost] : []);
235
+ const size = readDataSize(ctx.storageRoot, ctx.dataPath);
236
+ const limit = run.config.storage.warnDataGb;
237
+ if (limit > 0 && size && size.bytes > limit * 1024 ** 3)
238
+ console.warn(c.yellow('The persisted data takes ' + humanBytes(size.bytes) + ' (more than storage.warnDataGb, ' + limit + ' GB). See what: sandbox data; free space: sandbox data clean <what>.'));
239
+ if (!plan)
240
+ return viewSkipped(run.config, run.workspaceHost) ?? 'off (git-ignored files are visible in the sandbox; workspaceFilter.enabled keeps them out)';
241
+ const stats = await working('Preparing the workspace without its git-ignored files', () => new WorkspaceView(plan.options).prepare());
242
+ return 'on: git-ignored files stay on this PC' + (stats.toView || stats.toHost ? ' (' + stats.toView + ' copied in, ' + stats.toHost + ' out' + (stats.conflicts ? ', ' + stats.conflicts + ' conflicts kept next to the file' : '') + ')' : '') + '; ' + plan.options.view;
243
+ };
219
244
  /** Everything a real launch does before the sandbox starts (skipped by `generate`, which only writes files). */
220
245
  const prepare = async (ctx, run) => {
221
246
  const { options, dataPath, storageRoot } = ctx;
@@ -224,12 +249,19 @@ const prepare = async (ctx, run) => {
224
249
  if (account && run.harnessName === 'claude' && account !== activeAccount(dataPath))
225
250
  activateAccount(storageRoot, dataPath, account);
226
251
  const terminal = config.provisioning.terminal.enabled ? await ensureTerminal(dataPath, config.provisioning.refresh, { pwsh: config.toolchains.pwsh.enabled !== false }) : { exe: null, message: 'disabled' };
252
+ const conpty = config.provisioning.conpty.enabled ? await ensureConpty(dataPath, config.provisioning.refresh) : 'off';
227
253
  const history = syncSummary(config, dataPath, run.workspaceHost);
228
254
  const guarded = checkRepo(dataPath);
229
255
  if (guarded.length)
230
256
  console.warn(c.yellow('SECURITY: neutralised ' + guarded.length + ' change(s) the last sandbox made that could run code on this PC (see sandbox status).'));
257
+ const stopped = lastStopWarning(storageRoot, dataPath, watcherRunning(dataPath));
258
+ if (stopped)
259
+ console.warn(c.yellow(stopped));
260
+ if (updateRestartPending())
261
+ console.warn(c.yellow(UPDATE_RESTART_WARNING));
231
262
  resetRunDir(dataPath);
232
- return { account, terminal: terminal.message, history, pruned: describePrune(pruneStorage(storageRoot)) };
263
+ const filter = await prepareWorkspaceView(ctx, run);
264
+ return { account, terminal: terminal.message + '; pseudo console: ' + conpty, history, pruned: describePrune(pruneStorage(storageRoot)), filter };
233
265
  };
234
266
  export const launch = async (ctx, generateOnly = false, extras = {}) => {
235
267
  const { options, loaded, packageRoot, dataPath, storageRoot } = ctx;
@@ -284,7 +316,7 @@ export const launch = async (ctx, generateOnly = false, extras = {}) => {
284
316
  ['Workspace', run.workspaceHost + ' -> ' + config.guestWorkspace + (config.readOnly ? ' (read-only)' : '')],
285
317
  ['Agent', run.harnessName + (prepared?.account ? ' (account: ' + prepared.account + ')' : '') + ' Memory: ' + generated.memoryMb + ' MB CPU: ' + (config.sandbox.cpuLimit === 'none' ? 'all' : 'max ' + config.sandbox.cpuLimit + '%') + ' Network: ' + config.sandbox.networking + (config.sandbox.headless ? ' HEADLESS' : '')],
286
318
  ['Node', run.versions.node.spec + ' (' + run.versions.node.source + ') Python: ' + run.versions.python.spec + ' (' + run.versions.python.source + ')'],
287
- ...(prepared ? [['Terminal', prepared.terminal], ['History', prepared.history], ['Cleanup', prepared.pruned]] : []),
319
+ ...(prepared ? [['Ignored files', prepared.filter], ['Terminal', prepared.terminal], ['History', prepared.history], ['Cleanup', prepared.pruned]] : []),
288
320
  ['Toolchains', Object.entries(generated.toolchains).filter(([, d]) => d.on).map(([name, d]) => name + ' (' + d.reason + ')').join(', ') || 'Node only (nothing else used by this project)'],
289
321
  ...(generated.databases.length
290
322
  ? generated.databases.map((d, i) => [i ? '' : 'Databases', d.database + (d.database === 'sqlserver' ? ' (cannot run in the sandbox; use Postgres or SQLite for local work)' : ' on port ' + d.port) + ' because ' + d.reasons[0] + (d.reasons.length > 1 ? ' (+' + (d.reasons.length - 1) + ' more)' : '')])
@@ -304,6 +336,7 @@ export const launch = async (ctx, generateOnly = false, extras = {}) => {
304
336
  saveRepoSnapshot(dataPath, run.workspaceHost, projects.map((p) => p.host), roots.map((r) => r.host));
305
337
  startRegistry(storageRoot, { launchId: generated.launchId, protections: protectionsOf(config), primary: { host: run.workspaceHost, guest: config.guestWorkspace }, roots, boot: projects });
306
338
  rememberProject(storageRoot, run.workspaceHost);
339
+ startLife(storageRoot, dataPath, config.sandbox.keepAwake);
307
340
  startSandbox(generated.wsbPath);
308
341
  // Absolute: the watcher runs from another folder, and a relative --config there would make it exit silently.
309
342
  if (!watcherRunning(dataPath))
@@ -6,3 +6,5 @@ export declare const config: (ctx: Context) => number;
6
6
  export declare const doctor: (ctx: Context) => number;
7
7
  export declare const claudeHost: (ctx: Context) => number;
8
8
  export declare const watcher: (ctx: Context) => Promise<void>;
9
+ /** `sandbox audit`: the audit log of the last days (audit.enabled), newest last. */
10
+ export declare const audit: (ctx: Context) => Promise<number>;
@@ -5,14 +5,17 @@ import { pathToFileURL } from 'node:url';
5
5
  import _ from 'lodash';
6
6
  import { DEFAULTS, userConfigPath, userDir } from '../../core/config.js';
7
7
  import { describeOverrides, isTrusted, LOCAL_CONFIG_NAME, normalizeOverrides, readLocalConfig, trustStorePath } from '../../core/local-config.js';
8
- import { expandPath, fail, safeText, sleep, writeJson } from '../../core/util.js';
8
+ import { assertNoLinks, expandPath, fail, plainText, safeText, sleep, writeJson } from '../../core/util.js';
9
+ import { humanBytes } from '../../core/disk.js';
10
+ import { AuditLog } from '../../host/audit/log.js';
9
11
  import { readKnown, readRegistry } from '../../host/projects.js';
10
12
  import { detectWsb } from '../../host/wsb.js';
11
13
  import { describePrune, pruneStorage } from '../../host/cleanup.js';
12
14
  import { doctorRows, hostClaude, insideSandbox, isSandboxRunning } from '../../host/platform.js';
13
15
  import { readSandboxStatus } from '../../shared/rundir.js';
14
16
  import { syncBack, watch } from '../../sync/watcher.js';
15
- import { table } from './context.js';
17
+ import { printJson, table } from './context.js';
18
+ import { c } from '../colors.js';
16
19
  export const sync = (ctx) => {
17
20
  const running = isSandboxRunning();
18
21
  const result = syncBack(ctx.dataPath, !running);
@@ -28,6 +31,8 @@ const newestLog = (dir) => {
28
31
  };
29
32
  export const logs = async (ctx) => {
30
33
  const dir = path.join(ctx.dataPath, 'logs');
34
+ // The sandbox writes this folder: never read through a link it planted there.
35
+ assertNoLinks(ctx.dataPath, dir);
31
36
  const file = newestLog(dir) ?? fail('No sandbox logs yet in ' + dir + '.', 'Logs appear after the first start (sandbox claude).');
32
37
  console.log('== ' + file + ' ==');
33
38
  let printed = 0;
@@ -152,3 +157,31 @@ export const doctor = (ctx) => {
152
157
  };
153
158
  export const claudeHost = (ctx) => hostClaude(ctx.storageRoot, ctx.options.personal, ctx.options.passthrough);
154
159
  export const watcher = (ctx) => watch(ctx.dataPath);
160
+ const KINDS = ['proc', 'file', 'session', 'guard', 'note'];
161
+ const KIND_LABEL = { proc: 'program', file: 'file', session: 'session', guard: 'GUARD', note: 'note' };
162
+ /** `sandbox audit`: the audit log of the last days (audit.enabled), newest last. */
163
+ export const audit = async (ctx) => {
164
+ const settings = ctx.loaded.config.audit;
165
+ const kind = ctx.options.kind;
166
+ if (kind && !KINDS.includes(kind))
167
+ fail('Unknown kind "' + kind + '".', 'Use one of: ' + KINDS.join(', '));
168
+ const log = new AuditLog(AuditLog.dirFor(ctx.storageRoot, ctx.dataPath), settings);
169
+ const events = log.read({ days: ctx.options.days, kinds: kind ? [kind] : undefined, grep: ctx.options.grep });
170
+ if (printJson(ctx, events))
171
+ return 0;
172
+ if (!events.length) {
173
+ console.log(settings.enabled ? 'Nothing in the audit log for the last ' + ctx.options.days + ' day(s).' : 'The audit log is off. Turn it on in the menu (Settings > Security & privacy > Audit log) or with audit.enabled in your config.');
174
+ return 0;
175
+ }
176
+ for (const e of events) {
177
+ // What the sandbox reported is text from the sandbox: no control codes reach your terminal.
178
+ const [p, cmd, parent, dir, act] = [e.p, e.c, e.pp, e.w, e.a].map((v) => plainText(v ?? ''));
179
+ const what = e.k === 'proc' ? (cmd || p) + (parent ? c.dim(' <- ' + path.win32.basename(parent)) : '')
180
+ : e.k === 'file' ? act + ' ' + p + c.dim(' in ' + dir)
181
+ : e.k === 'session' ? act + ' ' + p + c.dim(' ' + dir + ' ' + cmd)
182
+ : p;
183
+ console.log(c.dim(new Date(e.t).toLocaleString()) + ' ' + (e.k === 'guard' ? c.yellow(KIND_LABEL[e.k].padEnd(8)) : KIND_LABEL[e.k].padEnd(8)) + what + (e.n && e.n > 1 ? c.dim(' x' + e.n) : ''));
184
+ }
185
+ console.log(c.dim('\n' + events.length + ' event(s); the log takes ' + humanBytes(log.bytes()) + ' (' + AuditLog.dirFor(ctx.storageRoot, ctx.dataPath) + ').'));
186
+ return 0;
187
+ };
@@ -40,7 +40,7 @@ export const attach = async (ctx) => {
40
40
  const projects = readRegistry(ctx.storageRoot)?.projects ?? [];
41
41
  const chosen = await select({
42
42
  message: 'Which session?' + (here.project ? ' (' + here.project.name + ' first)' : ''),
43
- choices: running.map((s) => ({ value: s.id, name: (here.guestDir && isWithin(here.guestDir, s.cwd) ? '* ' : ' ') + safeText(s.harness).padEnd(9) + ' ' + projectOf(projects, s).padEnd(16) + ' ' + ago(s.startedAt).padEnd(11) + ' ' + s.id })),
43
+ choices: running.map((s) => ({ value: s.id, name: (here.guestDir && isWithin(here.guestDir, s.cwd) ? '* ' : ' ') + safeText(s.harness).padEnd(9) + ' ' + safeText(projectOf(projects, s)).padEnd(16) + ' ' + ago(s.startedAt).padEnd(11) + ' ' + s.id })),
44
44
  });
45
45
  return attachHere(ctx, pickSession(ctx.dataPath, chosen));
46
46
  }
@@ -155,7 +155,7 @@ const printExecResult = (result) => {
155
155
  if (result.stderr)
156
156
  process.stderr.write(safeText(result.stderr));
157
157
  if (!result.ok && !result.stdout && !result.stderr)
158
- console.error(result.message);
158
+ console.error(safeText(result.message));
159
159
  };
160
160
  /** The guest folder for this folder, joining the running sandbox (and setting the project up) when needed. */
161
161
  const guestFolderHere = async (ctx, what) => {
@@ -210,7 +210,7 @@ export const restart = async (ctx) => {
210
210
  const scope = ctx.options.all || !here.project ? undefined : here.project.guest;
211
211
  if (!scope && !ctx.options.all)
212
212
  console.log(c.dim(hereDir(ctx) + ' is not a project of the running sandbox: restarting every session.'));
213
- console.log((await working('Restarting the agents' + (scope ? ' of ' + here.project?.name : ''), () => restartSessions(ctx.dataPath, ctx.options.harness ?? null, sandboxAlive, scope))).message);
213
+ console.log(safeText((await working('Restarting the agents' + (scope ? ' of ' + here.project?.name : ''), () => restartSessions(ctx.dataPath, ctx.options.harness ?? null, sandboxAlive, scope))).message));
214
214
  return 0;
215
215
  };
216
216
  export const stop = async (ctx) => {
@@ -6,7 +6,7 @@ import { parse } from 'jsonc-parser';
6
6
  import _ from 'lodash';
7
7
  import { userConfigPath } from '../../core/config.js';
8
8
  import { fail } from '../../core/util.js';
9
- import { editConfigText } from '../settings/edit.js';
9
+ import { editConfigText } from '../menu/settings/config-file.js';
10
10
  export const configFile = (ctx) => ctx.loaded.configPath ?? userConfigPath();
11
11
  /** The value as written in your file (not merged with the defaults). */
12
12
  export const fileSetting = (ctx, key, fallback) => {
package/dist/cli/index.js CHANGED
@@ -5,11 +5,11 @@ import { buildProgram, parseCli } from './args.js';
5
5
  import { commandSpec } from './manifest.js';
6
6
  import { c } from './colors.js';
7
7
  import { recordHistory } from './history.js';
8
- import { menu } from './menu.js';
8
+ import { menu } from './menu/index.js';
9
9
  import { validateOptions } from './validate.js';
10
10
  import { loadConfig } from '../core/config.js';
11
11
  import * as commands from './commands/index.js';
12
- import { UserError, errMsg, fail } from '../core/util.js';
12
+ import { UserError, errMsg, fail, plainText } from '../core/util.js';
13
13
  /** The package folder (this file is dist/cli/index.js). */
14
14
  export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
15
15
  const entry = path.join(packageRoot, 'dist', 'bin', 'agent-sandbox.js');
@@ -30,6 +30,7 @@ const HANDLERS = {
30
30
  projects: commands.projects,
31
31
  sync: commands.sync,
32
32
  logs: commands.logs,
33
+ audit: commands.audit,
33
34
  clean: commands.clean,
34
35
  config: commands.config,
35
36
  doctor: commands.doctor,
@@ -106,9 +107,10 @@ export const report = (e) => {
106
107
  if (e instanceof CommanderError)
107
108
  return ['commander.helpDisplayed', 'commander.help', 'commander.version'].includes(e.code) ? 0 : e.exitCode || 1;
108
109
  if (e instanceof UserError) {
109
- console.error(c.red('Error: ') + e.message);
110
+ // Messages can quote what the sandbox wrote (a session's status, an address): never with its control codes.
111
+ console.error(c.red('Error: ') + plainText(e.message));
110
112
  if (e.fix)
111
- console.error(' Fix: ' + e.fix);
113
+ console.error(' Fix: ' + plainText(e.fix));
112
114
  return 1;
113
115
  }
114
116
  console.error(c.red('Unexpected error: ') + errMsg(e));
@@ -93,6 +93,11 @@ export const COMMANDS = z.array(CommandSpec).parse([
93
93
  { name: 'data', summary: 'show what is kept between sandboxes (tools, databases, logins, logs), or delete some of it', group: 'Data', args: [{ name: 'action', description: 'show or clean', required: false, choices: ['show', 'clean'] }, { name: 'targets', description: 'what to clean, e.g. tools/go databases logs', required: false, variadic: true, when: 'clean' }], options: [bool('--yes', 'do not ask before deleting', { noMenu: true })], json },
94
94
  { name: 'clean', summary: 'delete old launch folders, logs and leftovers', group: 'Data', args: [], options: [{ flag: '--days', type: 'number', value: 'n', description: 'launch folders older than this many days', default: 7 }] },
95
95
  { name: 'sync', summary: 'copy conversation history back to this PC now', group: 'Data', args: [], options: [] },
96
+ {
97
+ name: 'audit', summary: 'what happened in your sandboxes: programs started, files changed, sessions, repo guard (audit.enabled)', group: 'Health', json,
98
+ args: [],
99
+ options: [{ flag: '--days', type: 'number', value: 'n', description: 'the last n days (default: today)', default: 1 }, text('--kind', 'kind', 'only proc, file, session or guard events', { choices: ['proc', 'file', 'session', 'guard'] }), text('--grep', 'text', 'only events containing this text')],
100
+ },
96
101
  { name: 'logs', summary: 'print the newest sandbox log', group: 'Health', args: [], options: [bool('--follow', 'keep printing new lines', { short: '-f' })] },
97
102
  { name: 'setup', summary: 'check this PC can run Windows Sandbox and offer to turn it on', group: 'Health', args: [], options: [] },
98
103
  { name: 'doctor', summary: 'check this PC and show what will be copied or downloaded', group: 'Health', args: [], options: [text('--harness', 'name', 'agent to check', { choices: 'harnesses' })] },
@@ -0,0 +1,21 @@
1
+ import { App } from '../tui/index.js';
2
+ import type { ConfigFile } from './settings/config-file.js';
3
+ export declare class AppsCatalogue {
4
+ private readonly app;
5
+ private readonly file;
6
+ private readonly dataPath;
7
+ constructor(app: App, file: ConfigFile, dataPath: string);
8
+ /** Downloaded already (into this data folder), so the next sandbox starts it without a download. */
9
+ private installed;
10
+ /** Is this entry on now (its setting, or listed in apps.install)? */
11
+ private chosen;
12
+ private item;
13
+ /** Shows the catalogue until you save (Enter) or leave it (Esc); returns the notice to show afterwards. */
14
+ run(): Promise<string | undefined>;
15
+ private save;
16
+ /** Your own programs: the apps.custom map, each entry a form (GitHub release or link, program, command line or not). */
17
+ private editCustom;
18
+ }
19
+ /** For the home screen: "12 apps chosen". */
20
+ export declare const appsSummary: (file: ConfigFile) => string;
21
+ export declare const appLabel: (id: string) => string;