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,64 @@
1
+ // Small portable command-line tools (toolchains.cli): one table entry each instead of one step per binary, plus an
2
+ // `openssl` shim onto the copy Git for Windows already ships.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { dryRun, errMsg, exists, log, mkdirp, stateDir, warn, workspace } from '../env.js';
6
+ import { CLI_TOOLS, decideCli } from './catalogue.js';
7
+ import { arch, tool } from './common.js';
8
+ import { setupSingleExe } from './runtimes.js';
9
+ /** gh keeps `gh auth login` here, under the agent state (persists like Claude's own login; never copied from the PC). */
10
+ export const ghConfigDir = () => path.join(stateDir, 'gh');
11
+ const toolVars = {
12
+ gh: () => ({ GH_CONFIG_DIR: mkdirp(ghConfigDir()), GH_NO_UPDATE_NOTIFIER: '1', GH_NO_EXTENSION_UPDATE_NOTIFIER: '1' }),
13
+ };
14
+ /**
15
+ * openssl.cmd in tools\shims, running Git's mingw64\bin\openssl.exe. Only the shim: putting mingw64\bin itself on PATH
16
+ * would bring Git's own gcc-runtime DLLs, curl, and friends along and could shadow MinGW's gcc from the C/C++ step.
17
+ * setlocal keeps OPENSSL_CONF from leaking into a cmd prompt that runs `openssl`. (A .cmd is found by shells and
18
+ * by programs that use cmd /c; Rust's Command::new("openssl") looks only for openssl.exe.)
19
+ */
20
+ const shimOpenssl = (env) => {
21
+ const exe = tool('git', 'mingw64', 'bin', 'openssl.exe');
22
+ if (!exists(exe)) {
23
+ env.summary.push('openssl: not in this Git for Windows (skipped)');
24
+ return;
25
+ }
26
+ const conf = tool('git', 'mingw64', 'etc', 'ssl', 'openssl.cnf');
27
+ const lines = ['@echo off', 'setlocal', ...(exists(conf) ? ['if not defined OPENSSL_CONF set "OPENSSL_CONF=' + conf + '"'] : []), '"' + exe + '" %*'];
28
+ fs.writeFileSync(path.join(mkdirp(tool('shims')), 'openssl.cmd'), lines.join('\r\n') + '\r\n');
29
+ env.prepend.push(tool('shims'));
30
+ env.summary.push('openssl: Git for Windows\' copy (shim)');
31
+ };
32
+ /**
33
+ * Installs the tools toolchains.cli asks for. Settings still "auto" here (normally the host decides) use the same
34
+ * rules on the workspace as the host would. The gh rule is enforced here again, whatever the host sent.
35
+ */
36
+ export const setupCli = async (env, settings, gitAccess, refresh = false) => {
37
+ const decisions = decideCli(settings, { gitAccess, hasGithubDir: exists(path.join(workspace, '.github')) });
38
+ if (settings.gh && !decisions.gh.on) {
39
+ env.summary.push('gh: off (' + decisions.gh.reason + ')');
40
+ // A login from an earlier full-access sandbox is still on disk; the agent could read the token from there.
41
+ if (exists(path.join(ghConfigDir(), 'hosts.yml')))
42
+ warn('gh is off (git access is not "full"), but an earlier gh login is still stored in ' + ghConfigDir() + ' and readable by the agent. Run `gh auth logout` in a full-access sandbox, or delete that folder.');
43
+ }
44
+ if (dryRun) {
45
+ log('(dry run) would provision CLI tools: ' + Object.entries(decisions).filter(([, d]) => d.on).map(([n]) => n).join(', '));
46
+ return;
47
+ }
48
+ for (const entry of CLI_TOOLS) {
49
+ const name = entry.name;
50
+ if (!decisions[name].on)
51
+ continue;
52
+ try {
53
+ await setupSingleExe(env, name, entry.repo, entry.asset(arch), entry.exe, tool('cli', name), { refresh, subdir: entry.subdir });
54
+ Object.assign(env.vars, toolVars[name]?.() ?? {});
55
+ }
56
+ catch (e) {
57
+ // One tool failing never takes the others down.
58
+ warn(name + ' setup failed (continuing without it): ' + errMsg(e));
59
+ env.summary.push(entry.name + ': FAILED');
60
+ }
61
+ }
62
+ if (decisions.openssl.on)
63
+ shimOpenssl(env);
64
+ };
@@ -0,0 +1,37 @@
1
+ /** What the toolchains contribute to the agent's environment. */
2
+ export interface ToolEnv {
3
+ vars: Record<string, string>;
4
+ prepend: string[];
5
+ append: string[];
6
+ summary: string[];
7
+ }
8
+ export declare const emptyEnv: () => ToolEnv;
9
+ export declare const arch: string;
10
+ export declare const tool: (...parts: string[]) => string;
11
+ /** First x.y.z in a tool's `--version` output. */
12
+ export declare const versionOf: (exe: string, args?: string[], options?: {}) => string;
13
+ /**
14
+ * Resolves a version online and remembers it for a day (provisioning.refresh looks again), so most boots make no
15
+ * lookups and an offline boot reuses what was installed last time. After a network failure the remaining lookups go
16
+ * straight to the remembered versions instead of each waiting for the network; other errors (no such version) do not.
17
+ */
18
+ export declare const resolveCached: (key: string, resolve: () => Promise<string>) => Promise<string>;
19
+ /** Runs one installer; a failure is reported and skipped so it never blocks the other toolchains. */
20
+ export declare const step: (name: string, env: ToolEnv, work: () => Promise<void>) => Promise<void>;
21
+ /** File names in the workspace and its direct sub-folders ("api/go.mod"); read once per boot. */
22
+ export declare const workspaceEntries: () => string[];
23
+ /**
24
+ * true / false as configured. The host already turns "auto" into a decision at launch (core/detect-toolchains.ts);
25
+ * this only applies when a session file still says "auto": the workspace has one of the marker files.
26
+ */
27
+ export declare const wanted: (setting: boolean | "auto", toolchain: string) => boolean;
28
+ /** The first of these version files in the workspace root, parsed (e.g. .ruby-version "3.3.1"). */
29
+ export declare const versionFromFiles: (files: string[], parse?: (text: string, name: string) => string | null) => {
30
+ spec: string;
31
+ source: string;
32
+ } | null;
33
+ /**
34
+ * Installs each package that is not installed yet (remembered in tools\installed-packages.json under `group`, e.g.
35
+ * "npm@22.11.0"). One failing package is reported and skipped; it is tried again next boot.
36
+ */
37
+ export declare const installOnce: (env: ToolEnv, group: string, packages: string[], install: (pkg: string) => void | Promise<void>) => Promise<void>;
@@ -0,0 +1,124 @@
1
+ // Shared plumbing for the toolchain installers. Everything lives in C:\AgentData\tools (persistent), so only the first
2
+ // boot downloads; later boots only put things on PATH.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { z } from 'zod';
6
+ import { writeJsonAtomic } from '../../shared/rundir.js';
7
+ import { errMsg, execOutput, exists, log, session, toolsDir, warn, workspace } from '../env.js';
8
+ import { MARKERS } from '../../core/detect-toolchains.js';
9
+ import { projectUses, specFromVersionFile } from './versions.js';
10
+ export const emptyEnv = () => ({ vars: {}, prepend: [], append: [], summary: [] });
11
+ export const arch = process.arch === 'arm64' ? 'arm64' : 'x64';
12
+ export const tool = (...parts) => path.join(toolsDir, ...parts);
13
+ /** First x.y.z in a tool's `--version` output. */
14
+ export const versionOf = (exe, args = ['--version'], options = {}) => /\d+\.\d+\.\d+\S*/.exec(execOutput(exe, args, options))?.[0] ?? 'installed';
15
+ /** Small JSON state files in tools\: read with zod (a half-written file is just empty), written atomically. */
16
+ const readState = (file, schema, empty) => {
17
+ try {
18
+ return schema.safeParse(JSON.parse(fs.readFileSync(file, 'utf8').replace(/^/, ''))).data ?? empty;
19
+ }
20
+ catch {
21
+ return empty;
22
+ }
23
+ };
24
+ /** A remembered lookup; plain strings are what older versions wrote (treated as stale, still usable offline). */
25
+ const Resolved = z.record(z.string(), z.union([z.string(), z.strictObject({ value: z.string(), at: z.number() })]));
26
+ const resolvedFile = () => tool('resolved-versions.json');
27
+ const remembered = () => Object.fromEntries(Object.entries(readState(resolvedFile(), Resolved, {})).map(([k, v]) => [k, typeof v === 'string' ? { value: v, at: 0 } : v]));
28
+ /** How long a looked-up version is trusted: boots within a day make no version lookups at all. */
29
+ const FRESH_MS = 24 * 60 * 60 * 1000;
30
+ let offline = false;
31
+ const networkError = (e) => /fetch failed|timed? ?out|aborted|ENOTFOUND|EAI_AGAIN|ECONN|ENETUNREACH|EHOSTUNREACH|HTTP 5\d\d|socket/i.test(errMsg(e) + ' ' + String(e?.cause ?? ''));
32
+ /**
33
+ * Resolves a version online and remembers it for a day (provisioning.refresh looks again), so most boots make no
34
+ * lookups and an offline boot reuses what was installed last time. After a network failure the remaining lookups go
35
+ * straight to the remembered versions instead of each waiting for the network; other errors (no such version) do not.
36
+ */
37
+ export const resolveCached = async (key, resolve) => {
38
+ const cached = remembered()[key];
39
+ if (cached && (offline || (!session.provisioning.refresh && Date.now() - cached.at < FRESH_MS)))
40
+ return cached.value;
41
+ try {
42
+ const value = await resolve();
43
+ fs.mkdirSync(toolsDir, { recursive: true });
44
+ writeJsonAtomic(resolvedFile(), { ...remembered(), [key]: { value, at: Date.now() } });
45
+ return value;
46
+ }
47
+ catch (e) {
48
+ if (networkError(e))
49
+ offline = true;
50
+ if (!cached)
51
+ throw e;
52
+ warn('Could not look up ' + key + ' online (' + errMsg(e) + '); using ' + cached.value + ' from an earlier boot.');
53
+ return cached.value;
54
+ }
55
+ };
56
+ /** Runs one installer; a failure is reported and skipped so it never blocks the other toolchains. */
57
+ export const step = async (name, env, work) => {
58
+ try {
59
+ await work();
60
+ }
61
+ catch (e) {
62
+ warn(name + ' setup failed (continuing without it): ' + errMsg(e));
63
+ env.summary.push(name + ': FAILED');
64
+ }
65
+ };
66
+ // ---- which toolchains a project needs ------------------------------------------------------------------------------
67
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'target', 'bin', 'obj', 'dist', 'build', 'vendor', '.venv', 'venv']);
68
+ let entries = null;
69
+ /** File names in the workspace and its direct sub-folders ("api/go.mod"); read once per boot. */
70
+ export const workspaceEntries = () => {
71
+ if (entries)
72
+ return entries;
73
+ const list = (dir) => { try {
74
+ return fs.readdirSync(dir, { withFileTypes: true });
75
+ }
76
+ catch {
77
+ return [];
78
+ } };
79
+ const top = list(workspace);
80
+ const below = top.filter((d) => d.isDirectory() && !d.name.startsWith('.') && !SKIP_DIRS.has(d.name.toLowerCase()))
81
+ .flatMap((d) => list(path.join(workspace, d.name)).filter((f) => f.isFile()).map((f) => d.name + '/' + f.name));
82
+ entries = [...top.filter((d) => d.isFile()).map((d) => d.name), ...below];
83
+ return entries;
84
+ };
85
+ /**
86
+ * true / false as configured. The host already turns "auto" into a decision at launch (core/detect-toolchains.ts);
87
+ * this only applies when a session file still says "auto": the workspace has one of the marker files.
88
+ */
89
+ export const wanted = (setting, toolchain) => setting === 'auto' ? projectUses(workspaceEntries(), MARKERS[toolchain] ?? []) : setting;
90
+ /** The first of these version files in the workspace root, parsed (e.g. .ruby-version "3.3.1"). */
91
+ export const versionFromFiles = (files, parse = (t) => specFromVersionFile(t)) => {
92
+ for (const name of files) {
93
+ const file = path.join(workspace, name);
94
+ if (!exists(file))
95
+ continue;
96
+ const spec = parse(fs.readFileSync(file, 'utf8'), name);
97
+ if (spec)
98
+ return { spec, source: name };
99
+ }
100
+ return null;
101
+ };
102
+ // ---- global packages, installed once -------------------------------------------------------------------------------
103
+ const installedFile = () => tool('installed-packages.json');
104
+ const installed = () => readState(installedFile(), z.record(z.string(), z.string()), {});
105
+ /**
106
+ * Installs each package that is not installed yet (remembered in tools\installed-packages.json under `group`, e.g.
107
+ * "npm@22.11.0"). One failing package is reported and skipped; it is tried again next boot.
108
+ */
109
+ export const installOnce = async (env, group, packages, install) => {
110
+ const todo = packages.filter((p) => !installed()[group + ':' + p]);
111
+ for (const pkg of todo) {
112
+ try {
113
+ log('Installing ' + pkg + ' (' + group + ') ...');
114
+ await install(pkg);
115
+ writeJsonAtomic(installedFile(), { ...installed(), [group + ':' + pkg]: new Date().toISOString() });
116
+ }
117
+ catch (e) {
118
+ warn(group + ' package ' + pkg + ' failed: ' + errMsg(e));
119
+ }
120
+ }
121
+ const ok = packages.filter((p) => installed()[group + ':' + p]);
122
+ if (ok.length)
123
+ env.summary.push(group.split('@')[0] + ' packages: ' + ok.join(', '));
124
+ };
@@ -0,0 +1,7 @@
1
+ import type { DatabasePlan } from '../../core/types.js';
2
+ import { type ToolEnv } from './common.js';
3
+ export declare const portOpen: (port: number) => Promise<boolean>;
4
+ export declare const DATABASE_NAMES: Record<DatabasePlan['database'], string>;
5
+ /** Stops a database (and DynamoDB's admin UI). PostgreSQL shuts down cleanly through pg_ctl; the others are ended. */
6
+ export declare const stopDatabase: (plan: DatabasePlan) => Promise<boolean>;
7
+ export declare const setupDatabase: (env: ToolEnv, plan: DatabasePlan) => Promise<void>;
@@ -0,0 +1,226 @@
1
+ // Databases inside the sandbox: PostgreSQL, MariaDB (MySQL-compatible), MongoDB and Redis. Binaries live in
2
+ // C:\AgentData\tools, data in C:\AgentData\databases (both persist), and every server listens on localhost only.
3
+ // Which ones run was decided on the host (session.databases): only what the project needs.
4
+ import fs from 'node:fs';
5
+ import net from 'node:net';
6
+ import path from 'node:path';
7
+ import { spawn, spawnSync } from 'node:child_process';
8
+ import { fetchJson, fetchText, githubAsset, installArchive } from '../../shared/download.js';
9
+ import { SYSTEM } from '../../shared/system.js';
10
+ import { ensureJdk } from './java.js';
11
+ import { dataDir, dryRun, execAsync, exists, log, logsDir, mkdirp } from '../env.js';
12
+ import { resolveCached, tool } from './common.js';
13
+ const dataFor = (name) => path.join(dataDir, 'databases', name);
14
+ export const portOpen = (port) => new Promise((resolve) => {
15
+ const socket = net.connect({ host: '127.0.0.1', port }, () => { socket.destroy(); resolve(true); });
16
+ socket.once('error', () => resolve(false));
17
+ });
18
+ const waitForPort = async (port, seconds) => {
19
+ for (const end = Date.now() + seconds * 1000; Date.now() < end;) {
20
+ if (await portOpen(port))
21
+ return true;
22
+ await new Promise((r) => setTimeout(r, 300));
23
+ }
24
+ return false;
25
+ };
26
+ /** Starts a server detached (it outlives the boot script) with its output in logs\<name>.log, and waits for its port. */
27
+ const startServer = async (name, exe, args, port) => {
28
+ if (await portOpen(port))
29
+ throw new Error('port ' + port + ' is already in use (change databases.' + name + '.port)');
30
+ const logFile = path.join(logsDir, name + '.log');
31
+ const out = fs.openSync(logFile, 'a');
32
+ spawn(exe, args, { detached: true, stdio: ['ignore', out, out], windowsHide: true }).unref();
33
+ fs.closeSync(out);
34
+ if (!(await waitForPort(port, 60)))
35
+ throw new Error(name + ' did not start listening on port ' + port + '; see ' + logFile);
36
+ };
37
+ /** Lock files a sandbox that was closed without shutting the server down leaves behind (this VM never ran it). */
38
+ const clearStaleLocks = (dir, files) => { for (const f of files)
39
+ fs.rmSync(path.join(dir, f), { force: true }); };
40
+ const setupPostgres = async (env, plan) => {
41
+ const exact = await resolveCached('postgres:' + plan.version, async () => {
42
+ const versions = (await fetchJson('https://www.postgresql.org/versions.json')).filter((v) => v.supported);
43
+ const hit = plan.version === 'latest' ? versions.find((v) => v.current) ?? versions[versions.length - 1] : versions.find((v) => v.major === plan.version.split('.')[0]);
44
+ return hit ? hit.major + '.' + hit.latestMinor : Promise.reject(new Error('No supported PostgreSQL ' + plan.version));
45
+ });
46
+ const home = tool('postgres', exact);
47
+ const bin = path.join(home, 'bin');
48
+ if (!exists(path.join(bin, 'pg_ctl.exe'))) {
49
+ log('Downloading PostgreSQL ' + exact + ' (~330 MB once) ...');
50
+ // EDB publishes no checksums; the download is HTTPS from EnterpriseDB, PostgreSQL's official Windows packager.
51
+ await installArchive('https://get.enterprisedb.com/postgresql/postgresql-' + exact + '-1-windows-x64-binaries.zip', home, { locate: 'pg_ctl.exe', up: 1, fileName: 'postgresql-' + exact + '.zip', members: ['pgsql/bin', 'pgsql/lib', 'pgsql/share', 'pgsql/include'] });
52
+ }
53
+ const data = dataFor('postgres-' + exact.split('.')[0]);
54
+ if (!exists(path.join(data, 'PG_VERSION'))) {
55
+ log('Creating the PostgreSQL database cluster ...');
56
+ mkdirp(path.dirname(data));
57
+ await execAsync(path.join(bin, 'initdb.exe'), ['-D', data, '-U', 'postgres', '-A', 'trust', '-E', 'UTF8', '--locale=C'], { stdio: 'ignore' });
58
+ }
59
+ clearStaleLocks(data, ['postmaster.pid']);
60
+ // postgres.exe refuses to run as an administrator (the sandbox user is one); pg_ctl drops the rights first.
61
+ if (await portOpen(plan.port))
62
+ throw new Error('port ' + plan.port + ' is already in use (change databases.postgres.port)');
63
+ await execAsync(path.join(bin, 'pg_ctl.exe'), ['start', '-D', data, '-w', '-t', '60', '-l', path.join(logsDir, 'postgres.log'), '-o', '-p ' + plan.port + ' -c listen_addresses=localhost'], { stdio: 'ignore' });
64
+ Object.assign(env.vars, { PGHOST: 'localhost', PGPORT: String(plan.port), PGUSER: 'postgres' });
65
+ env.prepend.push(bin);
66
+ env.summary.push('postgres ' + exact + ': localhost:' + plan.port + ', user postgres, no password (postgres://postgres@localhost:' + plan.port + '/postgres)');
67
+ };
68
+ const MARIA = 'https://downloads.mariadb.org/rest-api/mariadb/';
69
+ const setupMysql = async (env, plan) => {
70
+ const series = await resolveCached('mariadb:' + plan.version, async () => {
71
+ if (plan.version !== 'lts')
72
+ return plan.version;
73
+ const { major_releases: all } = await fetchJson(MARIA);
74
+ return all.find((r) => r.release_status === 'Stable' && r.release_support_type === 'Long Term Support')?.release_id ?? Promise.reject(new Error('No MariaDB LTS release found'));
75
+ });
76
+ const latest = async () => {
77
+ const { releases } = await fetchJson(MARIA + series + '/latest/');
78
+ const [version, release] = Object.entries(releases)[0] ?? [];
79
+ const zip = release?.files.find((f) => /-winx64\.zip$/.test(f.file_name));
80
+ return zip ? { version, zip } : Promise.reject(new Error('No MariaDB ' + series + ' zip for Windows'));
81
+ };
82
+ const version = await resolveCached('mariadb-version:' + series, async () => (await latest()).version);
83
+ const home = tool('mariadb', version);
84
+ const bin = path.join(home, 'bin');
85
+ if (!exists(path.join(bin, 'mariadbd.exe'))) {
86
+ const { zip } = await latest();
87
+ log('Downloading MariaDB ' + version + ' ...');
88
+ await installArchive(zip.file_download_url.replace(/^http:/, 'https:'), home, { sha256: zip.checksum?.sha256sum ?? null, locate: 'mariadbd.exe', up: 1, fileName: zip.file_name });
89
+ }
90
+ const data = dataFor('mariadb');
91
+ if (!exists(path.join(data, 'mysql'))) {
92
+ log('Creating the MariaDB data folder ...');
93
+ mkdirp(path.dirname(data));
94
+ await execAsync(path.join(bin, 'mariadb-install-db.exe'), ['--datadir=' + data, '--port=' + plan.port], { stdio: 'ignore' });
95
+ }
96
+ await startServer('mysql', path.join(bin, 'mariadbd.exe'), ['--datadir=' + data, '--port=' + plan.port, '--bind-address=127.0.0.1', '--console'], plan.port);
97
+ Object.assign(env.vars, { MYSQL_HOST: '127.0.0.1', MYSQL_TCP_PORT: String(plan.port) });
98
+ env.prepend.push(bin);
99
+ env.summary.push('mysql (MariaDB ' + version + '): 127.0.0.1:' + plan.port + ', user root, no password (mysql://root@127.0.0.1:' + plan.port + ')');
100
+ };
101
+ const setupMongo = async (env, plan) => {
102
+ const find = async () => {
103
+ const { versions } = await fetchJson('https://downloads.mongodb.org/full.json');
104
+ const prod = versions.filter((v) => v.production_release);
105
+ const hit = plan.version === 'latest' ? prod[0] : prod.find((v) => v.version === plan.version || v.version.startsWith(plan.version + '.'));
106
+ const archive = hit?.downloads.find((d) => d.target === 'windows' && d.arch === 'x86_64' && d.edition === 'base')?.archive;
107
+ return hit && archive ? { version: hit.version, archive } : Promise.reject(new Error('No MongoDB ' + plan.version + ' for Windows'));
108
+ };
109
+ const version = await resolveCached('mongodb:' + plan.version, async () => (await find()).version);
110
+ const home = tool('mongodb', version);
111
+ if (!exists(path.join(home, 'bin', 'mongod.exe'))) {
112
+ const { archive } = await find();
113
+ log('Downloading MongoDB ' + version + ' ...');
114
+ await installArchive(archive.url, home, { sha256: archive.sha256, locate: 'mongod.exe', up: 1 });
115
+ }
116
+ const shell = tool('mongosh');
117
+ if (!exists(path.join(shell, 'mongosh.exe'))) {
118
+ const asset = await githubAsset('mongodb-js/mongosh', /^mongosh-[\d.]+-win32-x64\.zip$/);
119
+ log('Downloading mongosh ...');
120
+ await installArchive(asset.url, shell, { sha256: asset.sha256, locate: 'mongosh.exe', fileName: asset.name });
121
+ }
122
+ const data = mkdirp(dataFor('mongodb'));
123
+ clearStaleLocks(data, ['mongod.lock']);
124
+ await startServer('mongodb', path.join(home, 'bin', 'mongod.exe'), ['--dbpath', data, '--bind_ip', '127.0.0.1', '--port', String(plan.port)], plan.port);
125
+ env.prepend.push(path.join(home, 'bin'), shell);
126
+ env.summary.push('mongodb ' + version + ': mongodb://127.0.0.1:' + plan.port + ' (mongosh on PATH)');
127
+ };
128
+ // ---- Redis -------------------------------------------------------------------------------------------------------
129
+ const setupRedis = async (env, plan) => {
130
+ const home = tool('redis');
131
+ if (!exists(path.join(home, 'redis-server.exe'))) {
132
+ // Redis has no official Windows build; this is the maintained community build of upstream Redis (MSYS2).
133
+ const asset = await githubAsset('redis-windows/redis-windows', /^Redis-[\d.]+-Windows-x64-msys2\.zip$/);
134
+ log('Downloading ' + asset.name + ' ...');
135
+ await installArchive(asset.url, home, { sha256: asset.sha256, locate: 'redis-server.exe', fileName: asset.name });
136
+ }
137
+ const data = mkdirp(dataFor('redis'));
138
+ await startServer('redis', path.join(home, 'redis-server.exe'), ['--port', String(plan.port), '--bind', '127.0.0.1', '--dir', data.replace(/\\/g, '/'), '--appendonly', 'yes'], plan.port);
139
+ env.prepend.push(home);
140
+ env.summary.push('redis: redis://127.0.0.1:' + plan.port + ' (redis-cli on PATH)');
141
+ };
142
+ // ---- DynamoDB Local + dynamodb-admin -------------------------------------------------------------------------------
143
+ const DYNAMO_ZIP = 'https://d1ni2b6xgvw0s0.cloudfront.net/v2.x/dynamodb_local_latest.zip';
144
+ /**
145
+ * Amazon's DynamoDB Local (a Java program; the Java toolchain step runs first, else this installs the LTS JDK) and
146
+ * dynamodb-admin, the web UI to browse and edit tables, installed with the runner's own npm. Every AWS SDK picks the
147
+ * local endpoint up from AWS_ENDPOINT_URL_DYNAMODB, with dummy credentials (real ones never reach the sandbox).
148
+ */
149
+ const setupDynamo = async (env, plan) => {
150
+ const java = path.join(await ensureJdk(), 'bin', 'java.exe');
151
+ const home = tool('dynamodb-local');
152
+ if (!exists(path.join(home, 'DynamoDBLocal.jar'))) {
153
+ log('Downloading DynamoDB Local ...');
154
+ const sha256 = (await fetchText(DYNAMO_ZIP + '.sha256')).trim().split(/\s+/)[0];
155
+ await installArchive(DYNAMO_ZIP, home, { sha256, locate: 'DynamoDBLocal.jar', fileName: 'dynamodb_local.zip' });
156
+ }
157
+ const data = mkdirp(dataFor('dynamodb'));
158
+ // -sharedDb: one database whatever region/credentials a client uses, so the admin UI and the app see the same tables.
159
+ await startServer('dynamodb', java, ['-Djava.library.path=' + path.join(home, 'DynamoDBLocal_lib'), '-jar', path.join(home, 'DynamoDBLocal.jar'), '-sharedDb', '-dbPath', data, '-port', String(plan.port)], plan.port);
160
+ const endpoint = 'http://localhost:' + plan.port;
161
+ const aws = { AWS_ENDPOINT_URL_DYNAMODB: endpoint, DYNAMO_ENDPOINT: endpoint, AWS_ACCESS_KEY_ID: 'local', AWS_SECRET_ACCESS_KEY: 'local', AWS_REGION: 'us-east-1', AWS_DEFAULT_REGION: 'us-east-1' };
162
+ Object.assign(env.vars, aws);
163
+ env.summary.push('dynamodb (DynamoDB Local): ' + endpoint + ' (AWS SDKs use it automatically via AWS_ENDPOINT_URL_DYNAMODB; dummy credentials)');
164
+ if (!plan.adminPort)
165
+ return;
166
+ const prefix = tool('dynamodb-admin');
167
+ const script = path.join(prefix, 'node_modules', 'dynamodb-admin', 'dist', 'dynamodb-admin.js');
168
+ if (!exists(script)) {
169
+ log('Installing dynamodb-admin (web UI for DynamoDB) ...');
170
+ const npmCli = path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
171
+ await execAsync(process.execPath, [npmCli, 'install', '--global', '--prefix', prefix, 'dynamodb-admin', '--no-fund', '--no-audit'], { stdio: 'ignore' });
172
+ }
173
+ const admin = { ...process.env, ...aws, DYNAMO_ENDPOINT: endpoint };
174
+ // The database matters more than its UI: a taken port is reported, not fatal.
175
+ if (await portOpen(plan.adminPort)) {
176
+ env.summary.push('dynamodb-admin: NOT started, port ' + plan.adminPort + ' is in use (change databases.dynamodb.adminPort)');
177
+ return;
178
+ }
179
+ const out = fs.openSync(path.join(logsDir, 'dynamodb-admin.log'), 'a');
180
+ spawn(process.execPath, [script, '--port', String(plan.adminPort), '--dynamo-endpoint', endpoint, '--skip-default-credentials'], { detached: true, stdio: ['ignore', out, out], windowsHide: true, env: admin }).unref();
181
+ fs.closeSync(out);
182
+ if (!(await waitForPort(plan.adminPort, 30)))
183
+ throw new Error('dynamodb-admin did not start; see ' + path.join(logsDir, 'dynamodb-admin.log'));
184
+ env.summary.push('dynamodb-admin: http://localhost:' + plan.adminPort + ' (browse and edit tables in the sandbox\'s browser)');
185
+ };
186
+ const SETUP = { postgres: setupPostgres, mysql: setupMysql, mongodb: setupMongo, redis: setupRedis, dynamodb: setupDynamo };
187
+ export const DATABASE_NAMES = { postgres: 'PostgreSQL', mysql: 'MySQL (MariaDB)', mongodb: 'MongoDB', redis: 'Redis', dynamodb: 'DynamoDB Local', sqlserver: 'SQL Server' };
188
+ /** The process listening on a local TCP port (netstat: no PowerShell start-up cost), or null. */
189
+ const listenerPid = (port) => {
190
+ const out = spawnSync(SYSTEM.netstat(), ['-ano', '-p', 'TCP'], { encoding: 'utf8', windowsHide: true }).stdout ?? '';
191
+ const hit = out.split(/\r?\n/).map((l) => /^\s*TCP\s+\S+:(\d+)\s+\S+\s+LISTENING\s+(\d+)/.exec(l)).find((m) => m && Number(m[1]) === port);
192
+ return hit ? Number(hit[2]) : null;
193
+ };
194
+ /** Stops a database (and DynamoDB's admin UI). PostgreSQL shuts down cleanly through pg_ctl; the others are ended. */
195
+ export const stopDatabase = async (plan) => {
196
+ if (!(await portOpen(plan.port)))
197
+ return false;
198
+ if (plan.database === 'postgres' && exists(tool('postgres'))) {
199
+ for (const version of fs.readdirSync(tool('postgres'))) {
200
+ const data = dataFor('postgres-' + version.split('.')[0]);
201
+ const pgCtl = path.join(tool('postgres', version), 'bin', 'pg_ctl.exe');
202
+ if (exists(path.join(data, 'postmaster.pid')) && exists(pgCtl))
203
+ await execAsync(pgCtl, ['stop', '-D', data, '-m', 'fast', '-w'], { stdio: 'ignore' });
204
+ }
205
+ }
206
+ for (const port of [plan.port, plan.adminPort ?? 0]) {
207
+ const pid = port && listenerPid(port);
208
+ if (pid)
209
+ try {
210
+ process.kill(pid);
211
+ }
212
+ catch { /* already gone */ }
213
+ }
214
+ return true;
215
+ };
216
+ export const setupDatabase = async (env, plan) => {
217
+ if (plan.database === 'sqlserver') {
218
+ env.summary.push('sqlserver: NOT available (SQL Server cannot start inside Windows Sandbox). Use the one on the PC via hostAccess.forwards, or switch the project to Postgres/SQLite for local work');
219
+ return;
220
+ }
221
+ if (dryRun) {
222
+ log('(dry run) would run ' + DATABASE_NAMES[plan.database] + ' on port ' + plan.port + ' (' + plan.reasons.join('; ') + ')');
223
+ return;
224
+ }
225
+ await SETUP[plan.database](env, plan);
226
+ };
@@ -0,0 +1,3 @@
1
+ import type { GitAccess } from '../../core/types.js';
2
+ import { type ToolEnv } from './common.js';
3
+ export declare const setupGit: (env: ToolEnv, hostCopy: boolean, refresh: boolean, access: GitAccess) => Promise<void>;
@@ -0,0 +1,112 @@
1
+ // Full Git for Windows (with bash: Claude Code on Windows needs Git Bash). Copied from the host when installed there.
2
+ // provisioning.git.access decides what the agent gets: 'full' git, a 'readonly' guarded git, or 'none' (bash only).
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { download, githubAsset, moveIntoPlace, stagingFolder } from '../../shared/download.js';
6
+ import { dataDir, dryRun, errMsg, execAsync, execOutput, exists, hostToolsDir, launchDir, log, mkdirp, robocopy, warn } from '../env.js';
7
+ import { ensureCompiled } from '../ptyhost.js';
8
+ import { arch, tool } from './common.js';
9
+ const BLOCKED_HOOKS = ['pre-commit', 'commit-msg', 'pre-merge-commit', 'pre-push', 'pre-rebase', 'pre-applypatch', 'applypatch-msg'];
10
+ const posix = (p) => p.replace(/\\/g, '/');
11
+ const installPortableGit = async (root) => {
12
+ const asset = await githubAsset('git-for-windows/git', arch === 'arm64' ? /^PortableGit-[\d.]+-arm64\.7z\.exe$/ : /^PortableGit-[\d.]+-64-bit\.7z\.exe$/);
13
+ const sfx = tool(asset.name);
14
+ log('Downloading ' + asset.name + ' ...');
15
+ await download(asset.url, sfx, asset.sha256);
16
+ await execAsync(sfx, ['-o' + root, '-y'], { stdio: 'ignore' });
17
+ fs.rmSync(sfx, { force: true });
18
+ };
19
+ /**
20
+ * readonly / none: a `git` wrapper first on PATH and as a bash function (Git Bash would otherwise find the real
21
+ * git.exe), plus git config from the environment (which repository config cannot override).
22
+ */
23
+ const installGuard = (env, access, realGit) => {
24
+ const dir = mkdirp(tool('gitguard'));
25
+ const guard = path.join(launchDir, 'runtime', 'gitguard.js');
26
+ // A real git.exe (programs like Claude Code only look for .exe); a .cmd only if the C# compiler is unavailable.
27
+ try {
28
+ fs.copyFileSync(ensureCompiled('gitshim'), path.join(dir, 'git.exe'));
29
+ fs.rmSync(path.join(dir, 'git.cmd'), { force: true });
30
+ }
31
+ catch (e) {
32
+ warn('git guard: falling back to git.cmd (' + errMsg(e) + ')');
33
+ fs.writeFileSync(path.join(dir, 'git.cmd'), '@"' + process.execPath + '" "' + guard + '" %*\r\n');
34
+ }
35
+ fs.writeFileSync(path.join(dir, 'bashenv.sh'), 'git() { "' + posix(process.execPath) + '" "' + posix(guard) + '" "$@"; }\nexport -f git\n');
36
+ env.prepend.unshift(dir);
37
+ Object.assign(env.vars, {
38
+ AGENT_GIT_ACCESS: access, AGENT_REAL_GIT: access === 'readonly' ? realGit : '',
39
+ AGENT_GUARD_NODE: process.execPath, AGENT_GUARD_SCRIPT: guard, BASH_ENV: posix(path.join(dir, 'bashenv.sh')),
40
+ });
41
+ if (access === 'readonly') {
42
+ const hooks = mkdirp(path.join(dir, 'hooks'));
43
+ for (const hook of BLOCKED_HOOKS)
44
+ fs.writeFileSync(path.join(hooks, hook), '#!/bin/sh\necho "git is read-only in this sandbox: ' + hook + ' refused." >&2\nexit 1\n');
45
+ Object.assign(env.vars, { GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'core.hooksPath', GIT_CONFIG_VALUE_0: posix(hooks) });
46
+ }
47
+ else {
48
+ // Even a git.exe found some other way sees no repository.
49
+ env.vars.GIT_DIR = path.join(dataDir, 'git-disabled');
50
+ }
51
+ };
52
+ /**
53
+ * Git copied from this PC keeps its installer's absolute paths in etc\gitconfig, e.g. http.sslCAInfo =
54
+ * C:/Program Files/Git/mingw64/etc/ssl/certs/ca-bundle.crt, which does not exist here: every HTTPS clone/fetch failed
55
+ * with "error setting certificate file". Point it at the bundle inside this copy.
56
+ */
57
+ const pointAtOwnCertificates = (root) => {
58
+ const config = path.join(root, 'etc', 'gitconfig');
59
+ const bundle = path.join(root, 'mingw64', 'etc', 'ssl', 'certs', 'ca-bundle.crt');
60
+ if (!exists(config) || !exists(bundle))
61
+ return;
62
+ const text = fs.readFileSync(config, 'utf8');
63
+ const fixed = text.replace(/^(\s*sslCAInfo\s*=\s*).*$/gim, (_m, key) => key + posix(bundle));
64
+ if (fixed !== text)
65
+ fs.writeFileSync(config, fixed);
66
+ };
67
+ export const setupGit = async (env, hostCopy, refresh, access) => {
68
+ const root = tool('git');
69
+ const bash = path.join(root, 'bin', 'bash.exe');
70
+ const gitExe = path.join(root, 'cmd', 'git.exe');
71
+ const hostGit = path.join(hostToolsDir, 'git');
72
+ if (exists(root) && !exists(bash)) {
73
+ log('Replacing the old MinGit (no bash) with full Git for Windows.');
74
+ fs.rmSync(root, { recursive: true, force: true });
75
+ }
76
+ if (!exists(bash) || refresh) {
77
+ if (dryRun) {
78
+ log('(dry run) would provision Git for Windows' + (hostCopy ? ' from this PC' : '') + ' (access: ' + access + ')');
79
+ return;
80
+ }
81
+ // Into a staging folder, then renamed: an interrupted copy/extraction must not leave a tools\git that has bash.exe
82
+ // (so looks installed) but is missing the rest.
83
+ const work = stagingFolder(root);
84
+ try {
85
+ if (hostCopy && exists(path.join(hostGit, 'bin', 'bash.exe'))) {
86
+ log('Copying Git for Windows from this PC ...');
87
+ robocopy(hostGit, work);
88
+ }
89
+ else
90
+ await installPortableGit(work);
91
+ if (!exists(path.join(work, 'bin', 'bash.exe')))
92
+ throw new Error('bash.exe missing after installing Git');
93
+ moveIntoPlace(work, root);
94
+ }
95
+ finally {
96
+ fs.rmSync(work, { recursive: true, force: true });
97
+ }
98
+ }
99
+ if (!exists(bash))
100
+ throw new Error('bash.exe missing after installing Git');
101
+ pointAtOwnCertificates(root);
102
+ env.vars.CLAUDE_CODE_GIT_BASH_PATH = bash;
103
+ env.append.push(path.join(root, 'usr', 'bin'));
104
+ const version = execOutput(gitExe, ['--version']).replace(/^git version /, '');
105
+ if (access === 'full') {
106
+ env.prepend.push(path.join(root, 'cmd'));
107
+ env.summary.push('git: ' + version);
108
+ return;
109
+ }
110
+ installGuard(env, access, gitExe);
111
+ env.summary.push(access === 'readonly' ? 'git: ' + version + ' READ-ONLY (status/log/diff/show/blame; no commit, push, checkout, ...)' : 'git: DISABLED (bash only; the agent cannot use git)');
112
+ };
@@ -0,0 +1,3 @@
1
+ import type { Toolchains } from '../../core/types.js';
2
+ import { type ToolEnv } from './common.js';
3
+ export declare const setupGo: (env: ToolEnv, config: Toolchains["go"]) => Promise<void>;
@@ -0,0 +1,33 @@
1
+ // Go from go.dev (checksum-verified zip). Modules, build cache and `go install` binaries live in persistent storage;
2
+ // GOTOOLCHAIN=auto lets a go.mod that needs a newer Go fetch it itself.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fetchJson, installArchive } from '../../shared/download.js';
6
+ import { dryRun, execAsync, exists, log } from '../env.js';
7
+ import { arch, installOnce, resolveCached, tool, versionOf } from './common.js';
8
+ import { pickGoRelease } from './versions.js';
9
+ const findRelease = async (spec) => pickGoRelease(await fetchJson('https://go.dev/dl/?mode=json&include=all'), spec) ?? Promise.reject(new Error('No Go release matches "' + spec + '"'));
10
+ export const setupGo = async (env, config) => {
11
+ if (dryRun) {
12
+ log('(dry run) would provision Go ' + config.version);
13
+ return;
14
+ }
15
+ const version = await resolveCached('go:' + config.version, async () => (await findRelease(config.version)).version);
16
+ const root = tool('go', version);
17
+ if (!exists(path.join(root, 'bin', 'go.exe'))) {
18
+ const goArch = arch === 'arm64' ? 'arm64' : 'amd64';
19
+ const file = (await findRelease(version)).files.find((f) => f.os === 'windows' && f.arch === goArch && f.kind === 'archive');
20
+ if (!file)
21
+ throw new Error(version + ' has no Windows ' + goArch + ' zip');
22
+ log('Downloading ' + file.filename + ' ...');
23
+ await installArchive('https://go.dev/dl/' + file.filename, root, { sha256: file.sha256, locate: 'VERSION', fileName: file.filename });
24
+ }
25
+ const GOPATH = tool('go-path');
26
+ const vars = { GOROOT: root, GOPATH, GOBIN: path.join(GOPATH, 'bin'), GOMODCACHE: path.join(GOPATH, 'pkg', 'mod'), GOCACHE: tool('go-cache'), GOTOOLCHAIN: 'auto', GOTELEMETRY: 'off' };
27
+ fs.mkdirSync(vars.GOBIN, { recursive: true });
28
+ Object.assign(env.vars, vars);
29
+ env.prepend.push(path.join(root, 'bin'), vars.GOBIN);
30
+ const go = path.join(root, 'bin', 'go.exe');
31
+ env.summary.push('go: ' + versionOf(go, ['version']));
32
+ await installOnce(env, 'go', config.tools, (pkg) => execAsync(go, ['install', pkg.includes('@') ? pkg : pkg + '@latest'], { env: { ...process.env, ...vars } }));
33
+ };
@@ -0,0 +1,13 @@
1
+ import type { DatabasePlan, GitAccess, Toolchains, ToolVersions } from '../../core/types.js';
2
+ import { type ToolEnv } from './common.js';
3
+ export type { ToolEnv } from './common.js';
4
+ export { pickNodeVersion, pickPackageVersion } from './versions.js';
5
+ export interface ToolchainOptions {
6
+ gitEnabled: boolean;
7
+ gitAccess: GitAccess;
8
+ hostGit: boolean;
9
+ refresh: boolean;
10
+ databases: DatabasePlan[];
11
+ }
12
+ /** Provisions every enabled toolchain and database, and returns the combined environment for the agent. */
13
+ export declare const setupToolchains: (config: Toolchains, versions: ToolVersions, options: ToolchainOptions) => Promise<ToolEnv>;