windows-development-sandbox 0.0.0-stage → 1.0.5

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 (231) hide show
  1. package/.sandbox-config.example.json +14 -0
  2. package/README.md +78 -2
  3. package/config.example.json +332 -0
  4. package/dist/api.d.ts +31 -0
  5. package/dist/api.js +30 -0
  6. package/dist/bin/agent-sandbox.d.ts +2 -0
  7. package/dist/bin/agent-sandbox.js +3 -0
  8. package/dist/bin/claude-host.d.ts +2 -0
  9. package/dist/bin/claude-host.js +5 -0
  10. package/dist/bin/claude-sbx.d.ts +2 -0
  11. package/dist/bin/claude-sbx.js +3 -0
  12. package/dist/bin/codex-sbx.d.ts +2 -0
  13. package/dist/bin/codex-sbx.js +3 -0
  14. package/dist/bin/gemini-sbx.d.ts +2 -0
  15. package/dist/bin/gemini-sbx.js +3 -0
  16. package/dist/bin/sandbox.d.ts +2 -0
  17. package/dist/bin/sandbox.js +4 -0
  18. package/dist/bin/sbx-hide.d.ts +2 -0
  19. package/dist/bin/sbx-hide.js +3 -0
  20. package/dist/bin/sbx-show.d.ts +2 -0
  21. package/dist/bin/sbx-show.js +3 -0
  22. package/dist/cli/args.d.ts +32 -0
  23. package/dist/cli/args.js +111 -0
  24. package/dist/cli/commands/accounts.d.ts +2 -0
  25. package/dist/cli/commands/accounts.js +52 -0
  26. package/dist/cli/commands/context.d.ts +21 -0
  27. package/dist/cli/commands/context.js +54 -0
  28. package/dist/cli/commands/data.d.ts +46 -0
  29. package/dist/cli/commands/data.js +249 -0
  30. package/dist/cli/commands/index.d.ts +8 -0
  31. package/dist/cli/commands/index.js +8 -0
  32. package/dist/cli/commands/launch.d.ts +5 -0
  33. package/dist/cli/commands/launch.js +152 -0
  34. package/dist/cli/commands/maintenance.d.ts +8 -0
  35. package/dist/cli/commands/maintenance.js +125 -0
  36. package/dist/cli/commands/repair.d.ts +59 -0
  37. package/dist/cli/commands/repair.js +332 -0
  38. package/dist/cli/commands/sessions.d.ts +11 -0
  39. package/dist/cli/commands/sessions.js +113 -0
  40. package/dist/cli/commands/setup.d.ts +45 -0
  41. package/dist/cli/commands/setup.js +152 -0
  42. package/dist/cli/index.d.ts +14 -0
  43. package/dist/cli/index.js +96 -0
  44. package/dist/cli/manifest.d.ts +95 -0
  45. package/dist/cli/manifest.js +87 -0
  46. package/dist/cli/menu.d.ts +22 -0
  47. package/dist/cli/menu.js +187 -0
  48. package/dist/cli/settings.d.ts +11 -0
  49. package/dist/cli/settings.js +156 -0
  50. package/dist/core/config.d.ts +17 -0
  51. package/dist/core/config.js +103 -0
  52. package/dist/core/detect-databases.d.ts +15 -0
  53. package/dist/core/detect-databases.js +147 -0
  54. package/dist/core/detect-pwsh.d.ts +18 -0
  55. package/dist/core/detect-pwsh.js +111 -0
  56. package/dist/core/detect-toolchains.d.ts +30 -0
  57. package/dist/core/detect-toolchains.js +58 -0
  58. package/dist/core/local-config.d.ts +12 -0
  59. package/dist/core/local-config.js +64 -0
  60. package/dist/core/schema.d.ts +604 -0
  61. package/dist/core/schema.js +264 -0
  62. package/dist/core/types.d.ts +162 -0
  63. package/dist/core/types.js +1 -0
  64. package/dist/core/util.d.ts +53 -0
  65. package/dist/core/util.js +145 -0
  66. package/dist/core/workspace.d.ts +23 -0
  67. package/dist/core/workspace.js +162 -0
  68. package/dist/guest/daemon.d.ts +1 -0
  69. package/dist/guest/daemon.js +186 -0
  70. package/dist/guest/db.d.ts +1 -0
  71. package/dist/guest/db.js +60 -0
  72. package/dist/guest/env.d.ts +49 -0
  73. package/dist/guest/env.js +99 -0
  74. package/dist/guest/gitguard.d.ts +7 -0
  75. package/dist/guest/gitguard.js +60 -0
  76. package/dist/guest/gitrules.d.ts +2 -0
  77. package/dist/guest/gitrules.js +11 -0
  78. package/dist/guest/guide.d.ts +12 -0
  79. package/dist/guest/guide.js +68 -0
  80. package/dist/guest/main.d.ts +1 -0
  81. package/dist/guest/main.js +120 -0
  82. package/dist/guest/ptyhost.d.ts +40 -0
  83. package/dist/guest/ptyhost.js +94 -0
  84. package/dist/guest/setup/agents.d.ts +8 -0
  85. package/dist/guest/setup/agents.js +73 -0
  86. package/dist/guest/setup/claude.d.ts +21 -0
  87. package/dist/guest/setup/claude.js +138 -0
  88. package/dist/guest/setup/environment.d.ts +6 -0
  89. package/dist/guest/setup/environment.js +65 -0
  90. package/dist/guest/setup/hooks.d.ts +1 -0
  91. package/dist/guest/setup/hooks.js +36 -0
  92. package/dist/guest/setup/network.d.ts +10 -0
  93. package/dist/guest/setup/network.js +99 -0
  94. package/dist/guest/setup/policy.d.ts +3 -0
  95. package/dist/guest/setup/policy.js +18 -0
  96. package/dist/guest/setup/user.d.ts +7 -0
  97. package/dist/guest/setup/user.js +47 -0
  98. package/dist/guest/toolchains/binstall.d.ts +7 -0
  99. package/dist/guest/toolchains/binstall.js +55 -0
  100. package/dist/guest/toolchains/catalogue.d.ts +51 -0
  101. package/dist/guest/toolchains/catalogue.js +31 -0
  102. package/dist/guest/toolchains/cli.d.ts +10 -0
  103. package/dist/guest/toolchains/cli.js +64 -0
  104. package/dist/guest/toolchains/common.d.ts +37 -0
  105. package/dist/guest/toolchains/common.js +124 -0
  106. package/dist/guest/toolchains/databases.d.ts +7 -0
  107. package/dist/guest/toolchains/databases.js +226 -0
  108. package/dist/guest/toolchains/git.d.ts +3 -0
  109. package/dist/guest/toolchains/git.js +112 -0
  110. package/dist/guest/toolchains/go.d.ts +3 -0
  111. package/dist/guest/toolchains/go.js +33 -0
  112. package/dist/guest/toolchains/index.d.ts +13 -0
  113. package/dist/guest/toolchains/index.js +122 -0
  114. package/dist/guest/toolchains/java.d.ts +8 -0
  115. package/dist/guest/toolchains/java.js +96 -0
  116. package/dist/guest/toolchains/managers.d.ts +16 -0
  117. package/dist/guest/toolchains/managers.js +82 -0
  118. package/dist/guest/toolchains/msvc.d.ts +10 -0
  119. package/dist/guest/toolchains/msvc.js +216 -0
  120. package/dist/guest/toolchains/native.d.ts +12 -0
  121. package/dist/guest/toolchains/native.js +220 -0
  122. package/dist/guest/toolchains/node.d.ts +3 -0
  123. package/dist/guest/toolchains/node.js +132 -0
  124. package/dist/guest/toolchains/pwsh.d.ts +4 -0
  125. package/dist/guest/toolchains/pwsh.js +14 -0
  126. package/dist/guest/toolchains/python.d.ts +3 -0
  127. package/dist/guest/toolchains/python.js +47 -0
  128. package/dist/guest/toolchains/runtimes.d.ts +17 -0
  129. package/dist/guest/toolchains/runtimes.js +40 -0
  130. package/dist/guest/toolchains/scripting.d.ts +10 -0
  131. package/dist/guest/toolchains/scripting.js +137 -0
  132. package/dist/guest/toolchains/versions.d.ts +47 -0
  133. package/dist/guest/toolchains/versions.js +77 -0
  134. package/dist/guest/toolchains/vscode.d.ts +2 -0
  135. package/dist/guest/toolchains/vscode.js +53 -0
  136. package/dist/guest/view.d.ts +1 -0
  137. package/dist/guest/view.js +22 -0
  138. package/dist/host/accounts.d.ts +32 -0
  139. package/dist/host/accounts.js +119 -0
  140. package/dist/host/channel.d.ts +37 -0
  141. package/dist/host/channel.js +180 -0
  142. package/dist/host/claude-profile.d.ts +15 -0
  143. package/dist/host/claude-profile.js +103 -0
  144. package/dist/host/cleanup.d.ts +23 -0
  145. package/dist/host/cleanup.js +66 -0
  146. package/dist/host/generate.d.ts +15 -0
  147. package/dist/host/generate.js +185 -0
  148. package/dist/host/personalize.d.ts +10 -0
  149. package/dist/host/personalize.js +97 -0
  150. package/dist/host/platform.d.ts +17 -0
  151. package/dist/host/platform.js +114 -0
  152. package/dist/host/project-scan.d.ts +23 -0
  153. package/dist/host/project-scan.js +173 -0
  154. package/dist/host/repo-guard.d.ts +23 -0
  155. package/dist/host/repo-guard.js +128 -0
  156. package/dist/host/secrets.d.ts +14 -0
  157. package/dist/host/secrets.js +133 -0
  158. package/dist/host/terminal.d.ts +32 -0
  159. package/dist/host/terminal.js +127 -0
  160. package/dist/host/window.d.ts +4 -0
  161. package/dist/host/window.js +68 -0
  162. package/dist/runtime/daemon.js +20291 -0
  163. package/dist/runtime/db.js +20423 -0
  164. package/dist/runtime/gitguard.js +84 -0
  165. package/dist/runtime/main.js +22398 -0
  166. package/dist/runtime/view.js +19966 -0
  167. package/dist/selftest/cli.test.d.ts +2 -0
  168. package/dist/selftest/cli.test.js +30 -0
  169. package/dist/selftest/config.test.d.ts +3 -0
  170. package/dist/selftest/config.test.js +74 -0
  171. package/dist/selftest/data.test.d.ts +2 -0
  172. package/dist/selftest/data.test.js +73 -0
  173. package/dist/selftest/detect.test.d.ts +2 -0
  174. package/dist/selftest/detect.test.js +42 -0
  175. package/dist/selftest/fixtures.d.ts +232 -0
  176. package/dist/selftest/fixtures.js +63 -0
  177. package/dist/selftest/guest.test.d.ts +3 -0
  178. package/dist/selftest/guest.test.js +61 -0
  179. package/dist/selftest/harness.d.ts +14 -0
  180. package/dist/selftest/harness.js +36 -0
  181. package/dist/selftest/helpers.test.d.ts +2 -0
  182. package/dist/selftest/helpers.test.js +125 -0
  183. package/dist/selftest/host.test.d.ts +2 -0
  184. package/dist/selftest/host.test.js +71 -0
  185. package/dist/selftest/index.d.ts +1 -0
  186. package/dist/selftest/index.js +56 -0
  187. package/dist/selftest/live.test.d.ts +7 -0
  188. package/dist/selftest/live.test.js +127 -0
  189. package/dist/selftest/mirror.test.d.ts +3 -0
  190. package/dist/selftest/mirror.test.js +74 -0
  191. package/dist/selftest/security.test.d.ts +3 -0
  192. package/dist/selftest/security.test.js +143 -0
  193. package/dist/selftest/setup.test.d.ts +2 -0
  194. package/dist/selftest/setup.test.js +23 -0
  195. package/dist/selftest/sync.test.d.ts +3 -0
  196. package/dist/selftest/sync.test.js +82 -0
  197. package/dist/shared/download.d.ts +58 -0
  198. package/dist/shared/download.js +200 -0
  199. package/dist/shared/guest-paths.d.ts +30 -0
  200. package/dist/shared/guest-paths.js +61 -0
  201. package/dist/shared/netrules.d.ts +18 -0
  202. package/dist/shared/netrules.js +98 -0
  203. package/dist/shared/paths.d.ts +9 -0
  204. package/dist/shared/paths.js +22 -0
  205. package/dist/shared/pty-client.d.ts +86 -0
  206. package/dist/shared/pty-client.js +227 -0
  207. package/dist/shared/rundir.d.ts +30 -0
  208. package/dist/shared/rundir.js +86 -0
  209. package/dist/shared/safe-remove.d.ts +10 -0
  210. package/dist/shared/safe-remove.js +55 -0
  211. package/dist/shared/schema.d.ts +84 -0
  212. package/dist/shared/schema.js +51 -0
  213. package/dist/shared/system.d.ts +15 -0
  214. package/dist/shared/system.js +19 -0
  215. package/dist/sync/claude-mem.d.ts +8 -0
  216. package/dist/sync/claude-mem.js +77 -0
  217. package/dist/sync/history.d.ts +30 -0
  218. package/dist/sync/history.js +129 -0
  219. package/dist/sync/watcher.d.ts +13 -0
  220. package/dist/sync/watcher.js +116 -0
  221. package/hooks/post-agent/README.txt +1 -0
  222. package/hooks/pre-provision/README.txt +1 -0
  223. package/package.json +91 -4
  224. package/sandbox/bootstrap.ps1 +258 -0
  225. package/sandbox/gitshim.cs +35 -0
  226. package/sandbox/logon.cmd +16 -0
  227. package/sandbox/ptyhost.cs +603 -0
  228. package/sandbox/trust.cmd +35 -0
  229. package/sandbox/vswhere.cs +83 -0
  230. package/schemas/config.schema.json +1062 -0
  231. package/schemas/sandbox-config.schema.json +952 -0
@@ -0,0 +1,60 @@
1
+ // `git` wrapper used when provisioning.git.access is 'readonly' or 'none'. Installed first on PATH (git.cmd) and as a
2
+ // bash function (BASH_ENV), it decides per call and then runs the real git. Standalone on purpose: it runs on every
3
+ // git call, so it reads only two environment variables (AGENT_GIT_ACCESS, AGENT_REAL_GIT).
4
+ import { spawnSync } from 'node:child_process';
5
+ /** Subcommands that only read. */
6
+ const READ_ONLY = new Set([
7
+ 'status', 'log', 'diff', 'show', 'blame', 'annotate', 'ls-files', 'ls-tree', 'ls-remote', 'rev-parse', 'rev-list', 'describe', 'shortlog',
8
+ 'grep', 'cat-file', 'reflog', 'whatchanged', 'name-rev', 'for-each-ref', 'show-ref', 'merge-base', 'count-objects', 'check-ignore',
9
+ 'check-attr', 'var', 'version', 'help', 'diff-tree', 'diff-files', 'diff-index', 'cherry', 'range-diff', 'show-branch', 'verify-commit', 'verify-tag',
10
+ ]);
11
+ /**
12
+ * `git branch` / `git tag` list when every argument is a listing flag, or when a listing flag is present and the
13
+ * rest are patterns/commits (`git branch --contains HEAD`). A bare name (`git branch new`) creates, so it is refused.
14
+ */
15
+ const listingOnly = (flags, listFlags) => (rest) => rest.every((a) => flags.test(a)) || (rest.some((a) => listFlags.test(a)) && rest.every((a) => flags.test(a) || !a.startsWith('-')));
16
+ /** Subcommands that are read-only only in their listing forms. */
17
+ const LISTING = {
18
+ branch: listingOnly(/^(-a|-r|-v|-vv|--all|--remotes|--list|-l|--show-current|--verbose|--no-color|--color(=.*)?|--contains|--no-contains|--merged|--no-merged|--sort=.*|--format=.*)$/, /^(--list|-l|--contains|--no-contains|--merged|--no-merged)$/),
19
+ tag: listingOnly(/^(-l|--list|-n\d*|--contains|--no-contains|--merged|--no-merged|--points-at|--sort=.*|--format=.*)$/, /^(-l|--list|--contains|--no-contains|--merged|--no-merged|--points-at)$/),
20
+ remote: (rest) => rest.length === 0 || ['-v', '--verbose', 'show', 'get-url'].includes(rest[0]),
21
+ config: (rest) => rest.some((a) => /^(--get|--get-all|--get-regexp|--list|-l|--show-origin|--show-scope|--name-only)$/.test(a)) && !rest.some((a) => /^(--add|--unset|--unset-all|--replace-all|--rename-section|--remove-section|-e|--edit)$/.test(a)),
22
+ stash: (rest) => ['list', 'show'].includes(rest[0] ?? ''),
23
+ worktree: (rest) => rest[0] === 'list',
24
+ submodule: (rest) => rest.length === 0 || rest[0] === 'status' || rest[0] === 'summary',
25
+ notes: (rest) => rest.length === 0 || ['list', 'show'].includes(rest[0]),
26
+ };
27
+ /** Global options that take a value (`git -C dir status`). Config overrides (-c, --config-env) are refused outright. */
28
+ const WITH_VALUE = new Set(['-C', '--namespace']);
29
+ const FORBIDDEN_GLOBAL = /^(-c|--config-env|--exec-path|--git-dir|--work-tree|--super-prefix)(=|$)/;
30
+ export const gitDecision = (args, access) => {
31
+ if (access === 'full')
32
+ return { allow: true };
33
+ if (access !== 'readonly')
34
+ return { allow: false, reason: 'git is disabled in this sandbox (provisioning.git.access = ' + (access || 'none') + ').' };
35
+ let i = 0;
36
+ while (i < args.length && args[i].startsWith('-')) {
37
+ if (FORBIDDEN_GLOBAL.test(args[i]))
38
+ return { allow: false, reason: 'git ' + args[i] + ' is not allowed: git is read-only in this sandbox.' };
39
+ i += WITH_VALUE.has(args[i]) ? 2 : 1;
40
+ }
41
+ const [command, ...rest] = args.slice(i);
42
+ if (command === undefined || command === '--version' || READ_ONLY.has(command) || (LISTING[command]?.(rest) ?? false))
43
+ return { allow: true };
44
+ return { allow: false, reason: 'git ' + command + ' is blocked: git is read-only in this sandbox (provisioning.git.access = readonly). Reading (status, log, diff, show, blame) is allowed.' };
45
+ };
46
+ if (process.argv[1]?.toLowerCase().endsWith('gitguard.js')) {
47
+ const args = process.argv.slice(2);
48
+ const decision = gitDecision(args, process.env.AGENT_GIT_ACCESS ?? 'none');
49
+ if (!decision.allow) {
50
+ console.error('fatal: ' + decision.reason);
51
+ process.exit(128);
52
+ }
53
+ const real = process.env.AGENT_REAL_GIT;
54
+ if (!real) {
55
+ console.error('fatal: git is not available in this sandbox.');
56
+ process.exit(128);
57
+ }
58
+ const result = spawnSync(real, args, { stdio: 'inherit', windowsHide: true });
59
+ process.exit(result.status ?? 1);
60
+ }
@@ -0,0 +1,2 @@
1
+ import type { GitAccess } from '../core/types.js';
2
+ export declare const gitDenyRules: (access: GitAccess) => string[];
@@ -0,0 +1,11 @@
1
+ const SHELLS = ['Bash', 'PowerShell'];
2
+ const WRITE_COMMANDS = ['commit', 'push', 'add', 'rm', 'mv', 'merge', 'rebase', 'reset', 'checkout', 'switch', 'restore', 'stash', 'tag', 'cherry-pick', 'revert', 'am', 'apply', 'clean', 'pull', 'fetch', 'clone', 'init', 'gc', 'prune', 'update-ref', 'update-index', 'filter-branch', 'replace', 'worktree add', 'branch -d', 'branch -D', 'branch -m', 'branch -M', 'config --add', 'config --unset', 'remote add', 'remote remove', 'remote set-url'];
3
+ const GIT_DIR_PATHS = ['**/.git', '**/.git/**'];
4
+ export const gitDenyRules = (access) => {
5
+ if (access === 'full')
6
+ return [];
7
+ const commands = access === 'none' ? ['git'] : WRITE_COMMANDS.map((c) => 'git ' + c);
8
+ const shellRules = SHELLS.flatMap((shell) => commands.map((c) => shell + '(' + c + ':*)'));
9
+ const fileRules = GIT_DIR_PATHS.flatMap((p) => (access === 'none' ? ['Read(' + p + ')', 'Edit(' + p + ')'] : ['Edit(' + p + ')']));
10
+ return [...shellRules, ...fileRules];
11
+ };
@@ -0,0 +1,12 @@
1
+ /** Deliberately tiny: it is loaded into every conversation. */
2
+ export declare const sandboxGuide: (toolSummary: string[], lan?: "blocked" | "open" | "offline") => string;
3
+ /** Inserts or replaces the generated section, keeping whatever else the file contains (the host's CLAUDE.md). */
4
+ export declare const upsertGuide: (file: string, guide: string) => void;
5
+ /**
6
+ * Claude Code loads the machine-wide managed memory (C:\Program Files\ClaudeCode\CLAUDE.md on Windows) before the
7
+ * user and project CLAUDE.md, in every session. The sandbox is disposable and we are admin, so the manifest lives
8
+ * there: the user's own global CLAUDE.md and every project's CLAUDE.md stay exactly as they are.
9
+ * Codex/Gemini have no machine-level file, so the block goes into their sandbox-only global instruction files.
10
+ */
11
+ export declare const managedClaudeMd: () => string;
12
+ export declare const writeGuides: (toolSummary: string[], lan: "blocked" | "open" | "offline") => string;
@@ -0,0 +1,68 @@
1
+ // Tells the agent where it is. A generated section (between markers, rewritten every boot) is added to the agent's
2
+ // global instructions: Claude's CLAUDE.md and Codex's AGENTS.md inside the sandbox. It never travels back to this PC.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { claudeDir, dataDir, launchDir, session, stateDir, toolsDir, workspace } from './env.js';
6
+ const BEGIN = '<!-- agent-sandbox:begin (generated every boot, do not edit) -->';
7
+ const END = '<!-- agent-sandbox:end -->';
8
+ /** Drives other than C: are subst drives onto C:\Mirror\<letter> (mapped folders shown at their PC paths). */
9
+ const mirrorLine = () => {
10
+ // Only the ones bootstrap.ps1 could create (a letter already in use is reported there and skipped).
11
+ const drives = (session.substDrives ?? []).filter((d) => fs.existsSync(d.letter + ':\\'));
12
+ if (!drives.length)
13
+ return [];
14
+ return ['- ' + drives.map((d) => d.letter + ':').join(', ') + ' = mapped folders at their PC paths (subst onto `' + drives.map((d) => d.target).join('`, `') + '`; real-path tools may print that form).'];
15
+ };
16
+ /** Deliberately tiny: it is loaded into every conversation. */
17
+ export const sandboxGuide = (toolSummary, lan = 'blocked') => [
18
+ BEGIN,
19
+ '## Sandbox manifest — you are in a DISPOSABLE Windows Sandbox',
20
+ 'Wiped on close: everything except the paths below (installers, winget/choco, C:\\Users, Program Files, registry).',
21
+ '- RW, persists: `' + workspace + '` = host folder ' + session.workspaceHost,
22
+ '- RW, persists: `' + toolsDir + '` (toolchains) — put portable exes in `' + path.join(toolsDir, 'bin') + '` (on PATH)',
23
+ '- RW, persists: `' + stateDir + '` (agent config/history; do not edit logins) · logs: `' + path.join(dataDir, 'logs') + '`',
24
+ '- RO: `' + launchDir + '` (this boot\'s scripts) · lost on close: everything else, incl. `C:\\nvm` (Node symlink)',
25
+ ...mirrorLine(),
26
+ '- Tools: ' + toolSummary.join(' · '),
27
+ '- Persistent installs: `npm i -g` (per nvm version), `pip`, `cargo install`, `dotnet tool -g`, `uv tool`, `bun add -g`. Real installers must re-run each boot via a hook (ask the user).',
28
+ '- Mapped folders (workspace, C:\\AgentData) allow NO symlinks or junctions: pnpm runs hoisted (set), npm workspaces / `file:` deps and deno node_modules dirs fail there (use a folder on C:\\ outside them), bun cannot save its lockfile there (`bun install --no-save`).',
29
+ '- Network: ' + (lan === 'offline' ? 'OFF' : lan === 'blocked' ? 'internet yes, host LAN blocked' : 'internet yes, host LAN NOT blocked this time') + '. Other sessions may share this workspace; the user may be typing from the host.',
30
+ END,
31
+ ].join('\n');
32
+ /** Inserts or replaces the generated section, keeping whatever else the file contains (the host's CLAUDE.md). */
33
+ export const upsertGuide = (file, guide) => {
34
+ fs.mkdirSync(path.dirname(file), { recursive: true });
35
+ const current = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
36
+ const start = current.indexOf(BEGIN);
37
+ const end = current.indexOf(END);
38
+ const without = start >= 0 && end > start ? current.slice(0, start) + current.slice(end + END.length) : current;
39
+ fs.writeFileSync(file, without.trimEnd() + (without.trim() ? '\n\n' : '') + guide + '\n', 'utf8');
40
+ };
41
+ /**
42
+ * Claude Code loads the machine-wide managed memory (C:\Program Files\ClaudeCode\CLAUDE.md on Windows) before the
43
+ * user and project CLAUDE.md, in every session. The sandbox is disposable and we are admin, so the manifest lives
44
+ * there: the user's own global CLAUDE.md and every project's CLAUDE.md stay exactly as they are.
45
+ * Codex/Gemini have no machine-level file, so the block goes into their sandbox-only global instruction files.
46
+ */
47
+ export const managedClaudeMd = () => path.join(process.env.AGENT_MANAGED_DIR ?? path.join(process.env.ProgramFiles ?? 'C:\\Program Files', 'ClaudeCode'), 'CLAUDE.md');
48
+ export const writeGuides = (toolSummary, lan) => {
49
+ const guide = sandboxGuide(toolSummary, lan);
50
+ const managed = managedClaudeMd();
51
+ fs.mkdirSync(path.dirname(managed), { recursive: true });
52
+ fs.writeFileSync(managed, guide + '\n', 'utf8');
53
+ fs.writeFileSync(path.join(dataDir, 'SANDBOX.md'), guide + '\n', 'utf8');
54
+ upsertGuide(path.join(stateDir, 'codex', 'AGENTS.md'), guide);
55
+ upsertGuide(path.join(stateDir, 'gemini', '.gemini', 'GEMINI.md'), guide);
56
+ removeGuide(path.join(claudeDir, 'CLAUDE.md'));
57
+ return managed;
58
+ };
59
+ /** Older versions put the block into the user's CLAUDE.md; take it out again. */
60
+ const removeGuide = (file) => {
61
+ if (!fs.existsSync(file))
62
+ return;
63
+ const current = fs.readFileSync(file, 'utf8');
64
+ const start = current.indexOf(BEGIN);
65
+ const end = current.indexOf(END);
66
+ if (start >= 0 && end > start)
67
+ fs.writeFileSync(file, (current.slice(0, start) + current.slice(end + END.length)).trim() + '\n', 'utf8');
68
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,120 @@
1
+ // Guest entry point, started by bootstrap.ps1 in the sandbox window. Provisions everything, starts the session
2
+ // daemon, asks it for the first agent session and then shows that session in this window.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+ import { writeGuides } from './guide.js';
7
+ import { setupToolchains } from './toolchains/index.js';
8
+ import { provisionHarness, resolveLaunch } from './setup/agents.js';
9
+ import { restoreClaudeMem, syncClaudeProfile } from './setup/claude.js';
10
+ import { agentEnv, persistEnvironment } from './setup/environment.js';
11
+ import { runHooks } from './setup/hooks.js';
12
+ import { applyNetworkPolicy } from './setup/network.js';
13
+ import { writeManagedSettings } from './setup/policy.js';
14
+ import { applyPersonalization, setupGitConfig } from './setup/user.js';
15
+ import { ensurePtyHost, pruneToolLeftovers } from './ptyhost.js';
16
+ import { now, readSession, writeJsonAtomic } from '../shared/rundir.js';
17
+ import { viewSession } from './view.js';
18
+ import { dataDir, dryRun, errMsg, isWin, localDir, log, logPath, logsDir, mkdirp, run, session, setSandboxStatus, toolsDir, warn, workspace } from './env.js';
19
+ const STEPS = 8;
20
+ const step = (n, title) => { log('\n\x1b[36m[' + n + '/' + STEPS + '] ' + title + '\x1b[0m'); setSandboxStatus(n === 1 ? 'booting' : 'provisioning', '[' + n + '/' + STEPS + '] ' + title); };
21
+ const soft = async (what, work) => { try {
22
+ await work();
23
+ }
24
+ catch (e) {
25
+ warn(what + ' failed (continuing): ' + errMsg(e));
26
+ } };
27
+ const startDaemon = (env) => {
28
+ spawn(process.execPath, [path.join(path.dirname(process.argv[1]), 'daemon.js')], { detached: true, stdio: 'ignore', windowsHide: true, env: { ...env, AGENT_LOG_PREFIX: 'daemon' } }).unref();
29
+ };
30
+ /** `sbx-db` for terminals and `sandbox db`: this boot's Node running runtime\db.js (on the sandbox's disk, rewritten each boot). */
31
+ const writeDbCommand = () => {
32
+ const dir = mkdirp(path.join(localDir, 'bin'));
33
+ const db = path.join(path.dirname(process.argv[1]), 'db.js');
34
+ fs.writeFileSync(path.join(dir, 'sbx-db.cmd'), '@setlocal\r\n@set AGENT_LOG_PREFIX=db\r\n@"' + process.execPath + '" "' + db + '" %*\r\n');
35
+ return dir;
36
+ };
37
+ const waitForPrimary = async () => {
38
+ for (let i = 0; i < 600; i++) {
39
+ const s = readSession(dataDir, session.launchId);
40
+ if (s?.phase === 'running')
41
+ return true;
42
+ if (s?.phase === 'failed' || s?.phase === 'exited') {
43
+ warn(s.message);
44
+ return false;
45
+ }
46
+ await new Promise((r) => setTimeout(r, 500));
47
+ }
48
+ return false;
49
+ };
50
+ const main = async () => {
51
+ log('Agent sandbox: ' + session.project + ' | workspace ' + workspace + ' | agent ' + session.harnessName + ' | log ' + logPath);
52
+ if (!fs.statSync(workspace, { throwIfNoEntry: false })?.isDirectory())
53
+ throw new Error('Workspace folder is missing in the sandbox: ' + workspace);
54
+ mkdirp(toolsDir);
55
+ mkdirp(run.run);
56
+ step(1, 'Network and session host');
57
+ let ptyHost = null;
58
+ if (isWin && !dryRun && session.attach.enabled)
59
+ await soft('Building the session host', () => { ptyHost = ensurePtyHost(); });
60
+ let lan = session.networking ? 'open' : 'offline';
61
+ await soft('Private-network blocking', async () => { lan = await applyNetworkPolicy(ptyHost); });
62
+ step(2, 'Pre-provision hooks and desktop');
63
+ runHooks('pre-provision', process.env);
64
+ await soft('Personalisation', applyPersonalization);
65
+ const gitAccess = session.provisioning.git.access ?? 'full';
66
+ step(3, 'Toolchains (Git, nvm/Node, Python, Rust, .NET, Bun, uv)');
67
+ const tools = await setupToolchains(session.toolchains, session.versions, { gitEnabled: session.provisioning.git.enabled, gitAccess, hostGit: session.hostTools.git, refresh: session.provisioning.refresh, databases: session.databases ?? [] });
68
+ if (!dryRun)
69
+ tools.prepend.push(writeDbCommand());
70
+ const env = agentEnv(tools);
71
+ persistEnvironment(env, Object.keys(tools.vars));
72
+ for (const line of tools.summary)
73
+ log(' ' + line);
74
+ await soft('Cleaning old tool builds', () => pruneToolLeftovers());
75
+ step(4, 'Git identity');
76
+ await soft('Git config', () => setupGitConfig(env));
77
+ step(5, 'Claude settings, history and claude-mem');
78
+ if (session.harnessName === 'claude')
79
+ await soft('Claude settings', syncClaudeProfile);
80
+ await soft('claude-mem restore', restoreClaudeMem);
81
+ await soft('Sandbox guide for the agent', () => writeGuides(tools.summary, lan));
82
+ await soft('Claude managed settings', () => writeManagedSettings(gitAccess));
83
+ step(6, 'Agent: ' + session.harnessName);
84
+ if (session.harness)
85
+ provisionHarness(session.harnessName, session.harness, env);
86
+ step(7, 'Pre-agent hooks');
87
+ runHooks('pre-agent', env);
88
+ step(8, 'Starting ' + session.harnessName);
89
+ setSandboxStatus('ready', 'Ready');
90
+ if (!session.harness) {
91
+ if (!dryRun)
92
+ startDaemon(env);
93
+ log('Ready (no agent requested). Workspace: ' + workspace);
94
+ return;
95
+ }
96
+ if (dryRun) {
97
+ const launch = resolveLaunch(session.harnessName, session.harness);
98
+ log('(dry run) would run: ' + [launch.file, ...launch.args, ...session.harness.arguments, ...session.harnessArguments].join(' ') + ' | cwd=' + workspace);
99
+ runHooks('post-agent', env);
100
+ return;
101
+ }
102
+ startDaemon(env);
103
+ writeJsonAtomic(run.request(session.launchId), { kind: 'session', id: session.launchId, harness: session.harnessName, args: session.harnessArguments, cwd: workspace, createdAt: now() });
104
+ if (!(await waitForPrimary()))
105
+ throw new Error('The agent session did not start. Logs: ' + logsDir);
106
+ log('If Claude asks you to sign in, do it once; the login is saved in persistent storage.');
107
+ if (process.stdin.isTTY)
108
+ await viewSession(session.launchId);
109
+ log('\nThis window stays open in ' + workspace + '. Other sessions keep running in their own tabs.');
110
+ };
111
+ main().catch((e) => {
112
+ const message = errMsg(e) + (e instanceof Error && e.stack ? '\n' + e.stack : '');
113
+ try {
114
+ setSandboxStatus('failed', errMsg(e));
115
+ fs.writeFileSync(path.join(logsDir, 'guest-error.txt'), message);
116
+ }
117
+ catch { /* ignore */ }
118
+ console.error('\n\x1b[31mFATAL: ' + message + '\x1b[0m\nLogs: ' + logsDir);
119
+ process.exitCode = 1;
120
+ });
@@ -0,0 +1,40 @@
1
+ import { type ChildProcess } from 'node:child_process';
2
+ export declare const quoteArg: (arg: string) => string;
3
+ /**
4
+ * Arguments on a `cmd /d /c <agent>.cmd ...` line (npm agents). cmd parses them twice: the /c line, then the .cmd's %*.
5
+ * & | < > ^ are literal inside quotes, so arguments with them are quoted (https://x?a=1&b=2 survives). % expands even
6
+ * inside quotes, so each one sits between quoted pieces ("50"%" off"): every %name% cmd could see then starts with a
7
+ * quote, and no variable is called that. cmd's quote state runs across the whole line (an embedded \" toggles it too),
8
+ * so a metacharacter it would see outside quotes gets ^^^ (one caret left after each parse). Arguments without any
9
+ * of these are quoted as usual.
10
+ */
11
+ export declare const cmdArgs: (args: string[]) => string;
12
+ /** The command line ptyhost starts; when the program is cmd.exe its arguments are quoted for cmd (cmdArgs). */
13
+ export declare const commandLine: (file: string, args: string[]) => string;
14
+ export type Helper = 'ptyhost' | 'gitshim' | 'vswhere';
15
+ /** %TEMP%\agent-sandbox\<name>-<hash>.exe, compiled from this launch's sandbox\<name>.cs the first time it is needed. */
16
+ export declare const ensureCompiled: (name: Helper) => string;
17
+ export declare const ensurePtyHost: () => string;
18
+ export declare const freePort: () => Promise<number>;
19
+ /** Resolves when ptyhost listens; rejects early if it exits first (e.g. the agent could not be started). */
20
+ export declare const waitForPtyHost: (child: ChildProcess, port: number, timeoutMs: number) => Promise<void>;
21
+ export declare const ptyHostLog: (id: string) => string;
22
+ /** The last lines of a session's ptyhost log (the agent's final output is written there when it exits). */
23
+ export declare const ptyHostLogTail: (id: string, lines?: number) => string;
24
+ export interface PtyHostOptions {
25
+ exe: string;
26
+ port: number;
27
+ token: string;
28
+ cols: number;
29
+ rows: number;
30
+ command: string;
31
+ restartCommand: string;
32
+ cwd: string;
33
+ env: NodeJS.ProcessEnv;
34
+ id: string;
35
+ remote: boolean;
36
+ }
37
+ /** Starts ptyhost detached, so closing a terminal tab never kills the agent; only the agent exiting ends it. */
38
+ export declare const startPtyHost: (o: PtyHostOptions) => ChildProcess;
39
+ /** Removes helper builds older versions left in persistent storage, and interrupted downloads in the tools folder. */
40
+ export declare const pruneToolLeftovers: () => void;
@@ -0,0 +1,94 @@
1
+ // Builds the sandbox's small C# helpers (ptyhost.cs, gitshim.cs) with the .NET Framework compiler that ships with
2
+ // Windows, and starts one ptyhost per agent session.
3
+ //
4
+ // The helpers are compiled onto the sandbox's own disk (wiped on close) on every boot, never cached in persistent
5
+ // storage: a tampered ptyhost.exe there would survive the wipe and see every keystroke typed from this PC.
6
+ import fs from 'node:fs';
7
+ import net from 'node:net';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import crypto from 'node:crypto';
11
+ import { spawn, spawnSync } from 'node:child_process';
12
+ import { exists, launchDir, logsDir, mkdirp, toolsDir } from './env.js';
13
+ const CSC = path.join(process.env.WINDIR ?? 'C:\\Windows', 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe');
14
+ const quoted = (arg) => '"' + arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1') + '"';
15
+ export const quoteArg = (arg) => (arg !== '' && !/[\s"]/.test(arg) ? arg : quoted(arg));
16
+ /**
17
+ * Arguments on a `cmd /d /c <agent>.cmd ...` line (npm agents). cmd parses them twice: the /c line, then the .cmd's %*.
18
+ * & | < > ^ are literal inside quotes, so arguments with them are quoted (https://x?a=1&b=2 survives). % expands even
19
+ * inside quotes, so each one sits between quoted pieces ("50"%" off"): every %name% cmd could see then starts with a
20
+ * quote, and no variable is called that. cmd's quote state runs across the whole line (an embedded \" toggles it too),
21
+ * so a metacharacter it would see outside quotes gets ^^^ (one caret left after each parse). Arguments without any
22
+ * of these are quoted as usual.
23
+ */
24
+ export const cmdArgs = (args) => {
25
+ const pieces = args.map((arg) => (/[&|<>^%]/.test(arg) ? arg.split('%').map(quoted).join('%') : quoteArg(arg))).join(' ');
26
+ let inQuotes = false;
27
+ return [...pieces].map((c) => {
28
+ if (c === '"')
29
+ inQuotes = !inQuotes;
30
+ return !inQuotes && /[&|<>^]/.test(c) ? '^^^' + c : c;
31
+ }).join('');
32
+ };
33
+ /** The command line ptyhost starts; when the program is cmd.exe its arguments are quoted for cmd (cmdArgs). */
34
+ export const commandLine = (file, args) => path.win32.basename(file).toLowerCase() === 'cmd.exe' ? [quoteArg(file), cmdArgs(args)].filter(Boolean).join(' ') : [file, ...args].map(quoteArg).join(' ');
35
+ const sourceHash = (name) => crypto.createHash('sha256').update(fs.readFileSync(path.join(launchDir, 'sandbox', name + '.cs'))).digest('hex').slice(0, 12);
36
+ /** %TEMP%\agent-sandbox\<name>-<hash>.exe, compiled from this launch's sandbox\<name>.cs the first time it is needed. */
37
+ export const ensureCompiled = (name) => {
38
+ const exe = path.join(os.tmpdir(), 'agent-sandbox', name + '-' + sourceHash(name) + '.exe');
39
+ if (exists(exe))
40
+ return exe;
41
+ if (!exists(CSC))
42
+ throw new Error('The .NET Framework C# compiler was not found at ' + CSC + ' (it ships with Windows 10/11).');
43
+ mkdirp(path.dirname(exe));
44
+ const result = spawnSync(CSC, ['/nologo', '/optimize', '/target:exe', '/out:' + exe, path.join(launchDir, 'sandbox', name + '.cs')], { encoding: 'utf8', windowsHide: true });
45
+ if (result.status !== 0)
46
+ throw new Error('Compiling ' + name + ' failed: ' + ((result.stdout ?? '') + (result.stderr ?? '')).trim().slice(0, 1500));
47
+ return exe;
48
+ };
49
+ export const ensurePtyHost = () => ensureCompiled('ptyhost');
50
+ export const freePort = () => new Promise((resolve, reject) => {
51
+ const server = net.createServer();
52
+ server.once('error', reject);
53
+ // Loopback: finding a port must not count as "node listens on the network" (that is what raises firewall prompts).
54
+ server.listen(0, '127.0.0.1', () => { const { port } = server.address(); server.close(() => resolve(port)); });
55
+ });
56
+ const portOpen = (port) => new Promise((resolve) => {
57
+ const socket = net.connect({ host: '127.0.0.1', port }, () => { socket.destroy(); resolve(true); });
58
+ socket.once('error', () => resolve(false));
59
+ });
60
+ /** Resolves when ptyhost listens; rejects early if it exits first (e.g. the agent could not be started). */
61
+ export const waitForPtyHost = async (child, port, timeoutMs) => {
62
+ let exitCode;
63
+ child.once('exit', (code) => { exitCode = code; });
64
+ const deadline = Date.now() + timeoutMs;
65
+ while (Date.now() < deadline) {
66
+ if (exitCode !== undefined)
67
+ throw new Error('the session host stopped right away (exit ' + exitCode + ')');
68
+ if (await portOpen(port))
69
+ return;
70
+ await new Promise((r) => setTimeout(r, 200));
71
+ }
72
+ throw new Error('the session host did not start listening on port ' + port);
73
+ };
74
+ export const ptyHostLog = (id) => path.join(logsDir, 'ptyhost-' + id + '.log');
75
+ /** The last lines of a session's ptyhost log (the agent's final output is written there when it exits). */
76
+ export const ptyHostLogTail = (id, lines = 12) => {
77
+ try {
78
+ return fs.readFileSync(ptyHostLog(id), 'utf8').trim().split(/\r?\n/).slice(-lines).join('\n');
79
+ }
80
+ catch {
81
+ return '';
82
+ }
83
+ };
84
+ /** Starts ptyhost detached, so closing a terminal tab never kills the agent; only the agent exiting ends it. */
85
+ export const startPtyHost = (o) => spawn(o.exe, [String(o.port), o.token, String(o.cols), String(o.rows), ptyHostLog(o.id), o.command, o.restartCommand], {
86
+ cwd: o.cwd, env: { ...o.env, PTYHOST_BIND: o.remote ? 'any' : 'loopback' }, detached: true, stdio: 'ignore', windowsHide: true,
87
+ });
88
+ /** Removes helper builds older versions left in persistent storage, and interrupted downloads in the tools folder. */
89
+ export const pruneToolLeftovers = () => {
90
+ for (const legacy of ['ptyhost', 'gitshim'])
91
+ fs.rmSync(path.join(toolsDir, legacy), { recursive: true, force: true });
92
+ for (const name of fs.readdirSync(toolsDir).filter((n) => /\.staging-[0-9a-f]+$/i.test(n)))
93
+ fs.rmSync(path.join(toolsDir, name), { recursive: true, force: true });
94
+ };
@@ -0,0 +1,8 @@
1
+ import type { Harness } from '../../core/types.js';
2
+ /** Copies the agent from this PC when it was mapped in (primary agent only), otherwise installs it with npm or uv. */
3
+ export declare const provisionHarness: (name: string, harness: Harness, env: NodeJS.ProcessEnv, refresh?: boolean) => void;
4
+ /** Program + arguments that start the agent (an npm .cmd shim has to go through cmd.exe). */
5
+ export declare const resolveLaunch: (name: string, harness: Harness) => {
6
+ file: string;
7
+ args: string[];
8
+ };
@@ -0,0 +1,73 @@
1
+ // Getting the agent (claude, codex, gemini, opencode, aider, ...) into persistent storage and working out how to start it.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { SYSTEM } from '../../shared/system.js';
5
+ import { dryRun, exec, exists, hostToolsDir, isWin, log, mkdirp, session, toolsDir, warn } from '../env.js';
6
+ const harnessPaths = (name, harness) => {
7
+ const dir = path.join(toolsDir, 'agents', name);
8
+ const exe = harness.hostExecutable ?? (harness.uvBin ? harness.uvBin + '.exe' : null);
9
+ return { dir, exe: exe ? path.join(dir, exe) : null, cmd: harness.npmBin ? path.join(dir, harness.npmBin + '.cmd') : null };
10
+ };
11
+ /** npm install with the runner's own npm into tools\agents\<name> (no global state outside persistent storage). */
12
+ const installViaNpm = (name, harness, dir, env) => {
13
+ if (!harness.npmPackage)
14
+ throw new Error('No npmPackage configured for ' + name + ' and no host executable was available.');
15
+ const npmCli = path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
16
+ const args = [npmCli, 'install', '--global', '--prefix', dir, harness.npmPackage, '--no-fund', '--no-audit'];
17
+ log('Installing ' + harness.npmPackage + ' (this can take a minute)...');
18
+ if (dryRun) {
19
+ log('(dry run) ' + process.execPath + ' ' + args.join(' '));
20
+ return;
21
+ }
22
+ if (!exists(npmCli))
23
+ throw new Error('npm not found at ' + npmCli);
24
+ exec(process.execPath, args, { env });
25
+ };
26
+ /** `uv tool install` with its command put straight into tools\agents\<name> (Python agents such as aider). */
27
+ const installViaUv = (name, harness, dir, env) => {
28
+ const uv = path.join(toolsDir, 'uv', 'uv.exe');
29
+ log('Installing ' + harness.uvPackage + ' with uv (this can take a minute)...');
30
+ if (dryRun) {
31
+ log('(dry run) uv tool install ' + harness.uvPackage);
32
+ return;
33
+ }
34
+ if (!exists(uv))
35
+ throw new Error(name + ' is installed with uv; enable toolchains.uv');
36
+ exec(uv, ['tool', 'install', '--force', '--python', '3.12', harness.uvPackage], { env: { ...env, UV_TOOL_BIN_DIR: dir, UV_TOOL_DIR: path.join(toolsDir, 'uv-tools'), UV_PYTHON_INSTALL_DIR: path.join(toolsDir, 'uv-python'), UV_CACHE_DIR: path.join(toolsDir, 'uv-cache') } });
37
+ };
38
+ /** Copies the agent from this PC when it was mapped in (primary agent only), otherwise installs it with npm or uv. */
39
+ export const provisionHarness = (name, harness, env, refresh = session.provisioning.refresh) => {
40
+ const p = harnessPaths(name, harness);
41
+ const cached = (p.exe !== null && exists(p.exe)) || (p.cmd !== null && exists(p.cmd));
42
+ if (cached && !refresh) {
43
+ log(name + ' already cached in persistent storage.');
44
+ return;
45
+ }
46
+ mkdirp(p.dir);
47
+ if (session.provisioning.copyFromHost && harness.hostExecutable && name === session.harnessName) {
48
+ const source = path.join(hostToolsDir, 'agents', name, harness.hostExecutable);
49
+ if (exists(source) && p.exe) {
50
+ fs.copyFileSync(source, p.exe);
51
+ log('Copied ' + name + ' from this PC: ' + source);
52
+ return;
53
+ }
54
+ if (!session.provisioning.fallbackToInstall)
55
+ throw new Error('Host copy of ' + name + ' unavailable and fallbackToInstall is false.');
56
+ warn('Host copy of ' + name + ' not available; installing instead.');
57
+ }
58
+ if (harness.uvPackage && !harness.npmPackage)
59
+ installViaUv(name, harness, p.dir, env);
60
+ else
61
+ installViaNpm(name, harness, p.dir, env);
62
+ };
63
+ /** Program + arguments that start the agent (an npm .cmd shim has to go through cmd.exe). */
64
+ export const resolveLaunch = (name, harness) => {
65
+ const p = harnessPaths(name, harness);
66
+ if (p.exe && exists(p.exe))
67
+ return { file: p.exe, args: [] };
68
+ if (p.cmd && exists(p.cmd))
69
+ return isWin ? { file: SYSTEM.cmd(), args: ['/d', '/c', p.cmd] } : { file: p.cmd, args: [] };
70
+ if (dryRun)
71
+ return { file: p.exe ?? p.cmd ?? name, args: [] };
72
+ throw new Error('Cannot find an executable for ' + name + ' after provisioning.');
73
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The sandbox keeps its own .claude.json (login, onboarding, per-project state), so MCP servers from this PC are
3
+ * merged into it each boot: user-scope `mcpServers`, and this project's local-scope servers under the sandbox path.
4
+ * Servers with the same name are replaced by this PC's definition; servers added inside the sandbox are kept.
5
+ */
6
+ export declare const mergeMcpServers: (hostFile: string, guestFile: string, hostWorkspace: string, guestWorkspace: string) => number;
7
+ export declare const syncClaudeProfile: () => void;
8
+ /**
9
+ * claude-mem keeps SQLite + a vector store. They run from the sandbox's own disk (SQLite and mapped folders do not
10
+ * mix) and are mirrored to persistent storage, from where the host syncs them back to your PC.
11
+ */
12
+ export declare const claudeMemPaths: {
13
+ store: string;
14
+ local: string;
15
+ };
16
+ export declare const restoreClaudeMem: () => void;
17
+ /**
18
+ * Copies the live claude-mem folder back to persistent storage (staging folder + swap, never a half copy). Called from
19
+ * the daemon tick: when no database changed since the last save it does nothing (no copy of the whole folder).
20
+ */
21
+ export declare const saveClaudeMem: () => void;