windows-development-sandbox 1.1.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +3 -2
  2. package/config.example.json +38 -5
  3. package/dist/cli/args.d.ts +3 -0
  4. package/dist/cli/args.js +3 -1
  5. package/dist/cli/commands/accounts.js +2 -2
  6. package/dist/cli/commands/agent-commands.js +1 -1
  7. package/dist/cli/commands/context.js +2 -1
  8. package/dist/cli/commands/data/clean.js +6 -1
  9. package/dist/cli/commands/data/collect.js +3 -3
  10. package/dist/cli/commands/index.d.ts +1 -1
  11. package/dist/cli/commands/index.js +1 -1
  12. package/dist/cli/commands/launch.js +36 -3
  13. package/dist/cli/commands/maintenance.d.ts +2 -0
  14. package/dist/cli/commands/maintenance.js +35 -2
  15. package/dist/cli/commands/sessions.js +3 -3
  16. package/dist/cli/commands/user-config.js +1 -1
  17. package/dist/cli/index.js +6 -4
  18. package/dist/cli/manifest.js +5 -0
  19. package/dist/cli/menu/apps.d.ts +21 -0
  20. package/dist/cli/menu/apps.js +122 -0
  21. package/dist/cli/menu/command-form.d.ts +28 -0
  22. package/dist/cli/menu/command-form.js +219 -0
  23. package/dist/cli/menu/index.d.ts +26 -0
  24. package/dist/cli/menu/index.js +180 -0
  25. package/dist/cli/menu/settings/categories.d.ts +9 -0
  26. package/dist/cli/menu/settings/categories.js +28 -0
  27. package/dist/cli/menu/settings/config-file.d.ts +40 -0
  28. package/dist/cli/menu/settings/config-file.js +71 -0
  29. package/dist/cli/menu/settings/editors.d.ts +30 -0
  30. package/dist/cli/menu/settings/editors.js +305 -0
  31. package/dist/cli/menu/settings/index.d.ts +32 -0
  32. package/dist/cli/menu/settings/index.js +136 -0
  33. package/dist/cli/menu/settings/schema-node.d.ts +62 -0
  34. package/dist/cli/menu/settings/schema-node.js +104 -0
  35. package/dist/cli/{menu-multi.d.ts → menu/several.d.ts} +10 -2
  36. package/dist/cli/menu/several.js +102 -0
  37. package/dist/cli/tui/app.d.ts +39 -0
  38. package/dist/cli/tui/app.js +52 -0
  39. package/dist/cli/tui/dialogs.d.ts +57 -0
  40. package/dist/cli/tui/dialogs.js +162 -0
  41. package/dist/cli/tui/frame.d.ts +18 -0
  42. package/dist/cli/tui/frame.js +27 -0
  43. package/dist/cli/tui/index.d.ts +7 -0
  44. package/dist/cli/tui/index.js +8 -0
  45. package/dist/cli/tui/list-screen.d.ts +101 -0
  46. package/dist/cli/tui/list-screen.js +299 -0
  47. package/dist/cli/tui/terminal.d.ts +37 -0
  48. package/dist/cli/tui/terminal.js +89 -0
  49. package/dist/cli/tui/text.d.ts +20 -0
  50. package/dist/cli/tui/text.js +105 -0
  51. package/dist/cli/tui/theme.d.ts +45 -0
  52. package/dist/cli/tui/theme.js +30 -0
  53. package/dist/core/config.js +9 -6
  54. package/dist/core/local-config.d.ts +15 -0
  55. package/dist/core/local-config.js +63 -4
  56. package/dist/core/schema/apps.d.ts +40 -0
  57. package/dist/core/schema/apps.js +27 -0
  58. package/dist/core/schema/config.d.ts +66 -1
  59. package/dist/core/schema/config.js +18 -6
  60. package/dist/core/schema/profile.d.ts +5 -0
  61. package/dist/core/schema/profile.js +4 -2
  62. package/dist/core/schema/sandbox.d.ts +28 -0
  63. package/dist/core/schema/sandbox.js +28 -0
  64. package/dist/core/schema/toolchains.d.ts +3 -0
  65. package/dist/core/schema/toolchains.js +6 -1
  66. package/dist/core/types.d.ts +6 -0
  67. package/dist/core/util.d.ts +2 -0
  68. package/dist/core/util.js +2 -0
  69. package/dist/core/workspace.js +22 -5
  70. package/dist/guest/apps/index.d.ts +10 -0
  71. package/dist/guest/apps/index.js +206 -0
  72. package/dist/guest/apps/resolvers.d.ts +8 -0
  73. package/dist/guest/apps/resolvers.js +90 -0
  74. package/dist/guest/audit-parse.d.ts +7 -0
  75. package/dist/guest/audit-parse.js +24 -0
  76. package/dist/guest/audit.d.ts +4 -0
  77. package/dist/guest/audit.js +47 -0
  78. package/dist/guest/daemon/relays.d.ts +1 -0
  79. package/dist/guest/daemon/relays.js +18 -0
  80. package/dist/guest/daemon.js +4 -0
  81. package/dist/guest/env.d.ts +1 -0
  82. package/dist/guest/env.js +3 -1
  83. package/dist/guest/helpers.d.ts +1 -1
  84. package/dist/guest/main.js +15 -2
  85. package/dist/guest/netproxy.d.ts +1 -0
  86. package/dist/guest/netproxy.js +100 -0
  87. package/dist/guest/ptyhost.js +14 -4
  88. package/dist/guest/setup/claude.d.ts +0 -4
  89. package/dist/guest/setup/claude.js +48 -2
  90. package/dist/guest/setup/network.d.ts +9 -0
  91. package/dist/guest/setup/network.js +97 -4
  92. package/dist/guest/setup/user.js +4 -0
  93. package/dist/guest/toolchains/native/mesa.js +13 -3
  94. package/dist/guest/toolchains/plan.d.ts +3 -1
  95. package/dist/guest/toolchains/plan.js +53 -9
  96. package/dist/guest/toolchains/python.js +26 -2
  97. package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
  98. package/dist/guest/toolchains/vscode-profile.js +69 -0
  99. package/dist/guest/toolchains/vscode.d.ts +17 -0
  100. package/dist/guest/toolchains/vscode.js +87 -1
  101. package/dist/host/attach.js +2 -1
  102. package/dist/host/audit/collect.d.ts +34 -0
  103. package/dist/host/audit/collect.js +126 -0
  104. package/dist/host/audit/log.d.ts +46 -0
  105. package/dist/host/audit/log.js +143 -0
  106. package/dist/host/generate.d.ts +6 -0
  107. package/dist/host/generate.js +10 -0
  108. package/dist/host/launch/mappings.js +5 -1
  109. package/dist/host/launch/stage.js +1 -1
  110. package/dist/host/motw.d.ts +17 -0
  111. package/dist/host/motw.js +74 -0
  112. package/dist/host/platform.js +3 -0
  113. package/dist/host/port-forward.d.ts +8 -0
  114. package/dist/host/port-forward.js +37 -0
  115. package/dist/host/repo-guard.d.ts +10 -3
  116. package/dist/host/repo-guard.js +142 -14
  117. package/dist/host/stops.d.ts +67 -0
  118. package/dist/host/stops.js +126 -0
  119. package/dist/host/terminal.d.ts +8 -0
  120. package/dist/host/terminal.js +45 -1
  121. package/dist/host/workspace-view/index.d.ts +20 -0
  122. package/dist/host/workspace-view/index.js +53 -0
  123. package/dist/host/workspace-view/rules.d.ts +26 -0
  124. package/dist/host/workspace-view/rules.js +93 -0
  125. package/dist/host/workspace-view/view.d.ts +50 -0
  126. package/dist/host/workspace-view/view.js +281 -0
  127. package/dist/runtime/daemon.js +1319 -552
  128. package/dist/runtime/db.js +168 -13
  129. package/dist/runtime/main.js +2898 -608
  130. package/dist/runtime/netproxy.js +25936 -0
  131. package/dist/runtime/view.js +272 -23
  132. package/dist/selftest/audit.test.d.ts +2 -0
  133. package/dist/selftest/audit.test.js +57 -0
  134. package/dist/selftest/channel.test.js +7 -0
  135. package/dist/selftest/cli.test.js +47 -17
  136. package/dist/selftest/fixtures.d.ts +45 -0
  137. package/dist/selftest/fixtures.js +3 -1
  138. package/dist/selftest/generate.test.js +7 -2
  139. package/dist/selftest/index.js +5 -2
  140. package/dist/selftest/live-network.test.d.ts +2 -0
  141. package/dist/selftest/live-network.test.js +63 -0
  142. package/dist/selftest/local-config.test.js +16 -3
  143. package/dist/selftest/network.test.js +12 -0
  144. package/dist/selftest/projects.test.js +1 -1
  145. package/dist/selftest/repo-guard.test.js +59 -1
  146. package/dist/selftest/sync.test.js +50 -10
  147. package/dist/selftest/toolchains.test.js +30 -0
  148. package/dist/selftest/workspace-view.test.d.ts +2 -0
  149. package/dist/selftest/workspace-view.test.js +89 -0
  150. package/dist/shared/apps.d.ts +49 -0
  151. package/dist/shared/apps.js +114 -0
  152. package/dist/shared/netpolicy.d.ts +27 -0
  153. package/dist/shared/netpolicy.js +51 -0
  154. package/dist/shared/pty-client.d.ts +9 -4
  155. package/dist/shared/pty-client.js +139 -17
  156. package/dist/shared/rundir.d.ts +2 -0
  157. package/dist/shared/rundir.js +2 -0
  158. package/dist/shared/schema.d.ts +10 -0
  159. package/dist/shared/schema.js +2 -0
  160. package/dist/shared/system.d.ts +1 -0
  161. package/dist/shared/system.js +1 -0
  162. package/dist/sync/claude-mem.js +3 -2
  163. package/dist/sync/history.d.ts +6 -0
  164. package/dist/sync/history.js +26 -11
  165. package/dist/sync/watcher.js +78 -6
  166. package/package.json +2 -1
  167. package/sandbox/cargoshim.cs +56 -0
  168. package/sandbox/cpucap.cs +33 -0
  169. package/sandbox/cpucap.ps1 +4 -35
  170. package/sandbox/logon.cmd +6 -0
  171. package/sandbox/memlog.ps1 +34 -0
  172. package/sandbox/pagefile.cs +31 -0
  173. package/sandbox/pagefile.ps1 +21 -0
  174. package/sandbox/ptyhost.cs +105 -7
  175. package/schemas/config.schema.json +303 -2
  176. package/schemas/sandbox-config.schema.json +126 -12
  177. package/dist/cli/menu-multi.js +0 -82
  178. package/dist/cli/menu.d.ts +0 -14
  179. package/dist/cli/menu.js +0 -290
  180. package/dist/cli/settings/edit.d.ts +0 -13
  181. package/dist/cli/settings/edit.js +0 -19
  182. package/dist/cli/settings/index.d.ts +0 -8
  183. package/dist/cli/settings/index.js +0 -131
  184. package/dist/cli/settings/prompts.d.ts +0 -7
  185. package/dist/cli/settings/prompts.js +0 -48
  186. package/dist/cli/settings/schema.d.ts +0 -33
  187. package/dist/cli/settings/schema.js +0 -29
@@ -35,6 +35,27 @@ const shimPair = (folder, version, dir) => {
35
35
  shim(folder, 'python' + version, python);
36
36
  shim(folder, 'pip' + version, python + ' -m pip');
37
37
  };
38
+ /**
39
+ * `py` like the Python launcher Windows installs: `py -3 x.py`, `py -3.12 ...`, `py -2 ...`, `py x.py`. The launcher
40
+ * itself is not portable, so py.cmd runs a small script that picks the interpreter (cmd's %* ignores shift).
41
+ */
42
+ const pyLauncher = (folder, py3, py2) => {
43
+ const script = path.join(mkdirp(folder), 'py-launcher.py');
44
+ fs.writeFileSync(script, [
45
+ 'import subprocess, sys',
46
+ 'PY3, PY2 = ' + JSON.stringify(path.join(py3, 'python.exe')) + ', ' + JSON.stringify(py2 ? path.join(py2, 'python.exe') : null),
47
+ 'args = sys.argv[1:]',
48
+ 'exe = PY3',
49
+ "if args and args[0].startswith('-2'):",
50
+ ' if not PY2: sys.exit("py: Python 2 is not installed (toolchains.python.python2)")',
51
+ ' exe, args = PY2, args[1:]',
52
+ "elif args and (args[0] == '-3' or args[0].startswith('-3.') or args[0].startswith('-3-')):",
53
+ ' args = args[1:]',
54
+ 'sys.exit(subprocess.call([exe] + args))',
55
+ '',
56
+ ].join('\n'));
57
+ shim(folder, 'py', '"' + path.join(py3, 'python.exe') + '" "' + script + '"');
58
+ };
38
59
  export const setupPython = async (env, config, versions) => {
39
60
  if (dryRun) {
40
61
  log('(dry run) would provision Python ' + versions.python.spec + (config.python2 ? ' and Python 2.7' : ''));
@@ -45,10 +66,13 @@ export const setupPython = async (env, config, versions) => {
45
66
  shimPair(shims, '3', py3);
46
67
  env.prepend.push(py3, path.join(py3, 'Scripts'), shims);
47
68
  env.vars.PYTHONIOENCODING = 'utf-8';
48
- env.summary.push('python: ' + path.basename(py3).replace('python-', '') + ' (from ' + versions.python.source + ')');
49
- if (!config.python2)
69
+ env.summary.push('python: ' + path.basename(py3).replace('python-', '') + ' (from ' + versions.python.source + '; py works too)');
70
+ if (!config.python2) {
71
+ pyLauncher(shims, py3, null);
50
72
  return;
73
+ }
51
74
  const py2 = await installPython('python2', '2.7');
52
75
  shimPair(shims, '2', py2);
76
+ pyLauncher(shims, py3, py2);
53
77
  env.summary.push('python2: ' + path.basename(py2).replace('python2-', ''));
54
78
  };
@@ -0,0 +1,30 @@
1
+ /** Where the sandbox's tools are (absent = not installed). */
2
+ export interface ToolPaths {
3
+ javaHome?: string;
4
+ python?: string;
5
+ goRoot?: string;
6
+ go?: string;
7
+ gcc?: string;
8
+ cl?: string;
9
+ cmake?: string;
10
+ dotnet?: string;
11
+ node?: string;
12
+ pwsh?: string;
13
+ git?: string;
14
+ bash?: string;
15
+ php?: string;
16
+ ruby?: string;
17
+ deno?: string;
18
+ }
19
+ /** User settings for the sandbox's VS Code. Only keys with a value are set; the caller removes the ones it set before. */
20
+ export declare const vscodeSettings: (p: ToolPaths) => Record<string, unknown>;
21
+ export declare const EXTENSION_ID: RegExp;
22
+ /** The extensions to have: always EditorConfig, then per toolchain and database in use, the project's, and yours. */
23
+ export declare const vscodeExtensions: (o: {
24
+ toolchains: string[];
25
+ databases: string[];
26
+ recommended: string[];
27
+ extra: string[];
28
+ }) => string[];
29
+ /** Extensions not installed yet, from the names of the folders in VS Code's extensions folder (publisher.name-1.2.3). */
30
+ export declare const missingExtensions: (wanted: string[], folders: string[]) => string[];
@@ -0,0 +1,69 @@
1
+ // What VS Code is told about the sandbox: the settings that point it at the sandbox's own tools, and the extensions
2
+ // for the languages in use. Pure (tested); vscode.ts finds the paths and writes them.
3
+ /** The JDK's major version from its folder name (jdk-21.0.5+11 -> 21). */
4
+ const javaMajor = (home) => /(?:jdk|jre)?-?(\d{1,2})(?:[.+_-]|$)/i.exec(home.split(/[\\/]/).pop() ?? '')?.[1];
5
+ /** User settings for the sandbox's VS Code. Only keys with a value are set; the caller removes the ones it set before. */
6
+ export const vscodeSettings = (p) => {
7
+ const profiles = {};
8
+ if (p.pwsh)
9
+ profiles['PowerShell 7'] = { path: p.pwsh, args: ['-NoLogo'] };
10
+ if (p.bash)
11
+ profiles['Git Bash'] = { path: p.bash, args: ['--login', '-i'] };
12
+ const settings = {
13
+ 'telemetry.telemetryLevel': 'off',
14
+ 'update.mode': 'none',
15
+ 'workbench.startupEditor': 'none',
16
+ 'git.path': p.git,
17
+ 'terminal.integrated.profiles.windows': Object.keys(profiles).length ? profiles : undefined,
18
+ 'terminal.integrated.defaultProfile.windows': p.pwsh ? 'PowerShell 7' : undefined,
19
+ 'python.defaultInterpreterPath': p.python,
20
+ 'java.jdt.ls.java.home': p.javaHome,
21
+ 'java.configuration.runtimes': p.javaHome ? [{ name: 'JavaSE-' + (javaMajor(p.javaHome) ?? '21'), path: p.javaHome, default: true }] : undefined,
22
+ 'maven.terminal.useJavaHome': p.javaHome ? true : undefined,
23
+ 'go.goroot': p.goRoot,
24
+ 'go.alternateTools': p.go ? { go: p.go } : undefined,
25
+ 'C_Cpp.default.compilerPath': p.cl ?? p.gcc,
26
+ 'cmake.cmakePath': p.cmake,
27
+ 'dotnet.dotnetPath': p.dotnet ? p.dotnet.replace(/[\\/]dotnet\.exe$/i, '') : undefined,
28
+ 'dotnetAcquisitionExtension.existingDotnetPath': p.dotnet ? [{ extensionId: 'ms-dotnettools.csharp', path: p.dotnet }] : undefined,
29
+ 'php.validate.executablePath': p.php,
30
+ 'php.debug.executablePath': p.php,
31
+ 'rubyLsp.rubyVersionManager': p.ruby ? { identifier: 'none' } : undefined,
32
+ 'deno.path': p.deno,
33
+ 'powershell.powerShellAdditionalExePaths': p.pwsh ? { 'PowerShell 7 (sandbox)': p.pwsh } : undefined,
34
+ 'npm.packageManager': 'auto',
35
+ };
36
+ return Object.fromEntries(Object.entries(settings).filter(([, v]) => v !== undefined));
37
+ };
38
+ /** Extensions per toolchain (Marketplace ids). */
39
+ const BY_TOOLCHAIN = {
40
+ node: ['dbaeumer.vscode-eslint', 'esbenp.prettier-vscode'],
41
+ python: ['ms-python.python', 'ms-python.debugpy'],
42
+ java: ['redhat.java', 'vscjava.vscode-java-debug', 'vscjava.vscode-java-test', 'vscjava.vscode-maven', 'vscjava.vscode-gradle'],
43
+ go: ['golang.go'],
44
+ rust: ['rust-lang.rust-analyzer', 'vadimcn.vscode-lldb'],
45
+ cpp: ['ms-vscode.cpptools', 'ms-vscode.cmake-tools'],
46
+ msvc: ['ms-vscode.cpptools', 'ms-vscode.cmake-tools'],
47
+ dotnet: ['ms-dotnettools.csharp'],
48
+ ruby: ['shopify.ruby-lsp'],
49
+ php: ['bmewburn.vscode-intelephense-client', 'xdebug.php-debug'],
50
+ deno: ['denoland.vscode-deno'],
51
+ pwsh: ['ms-vscode.powershell'],
52
+ };
53
+ const BY_DATABASE = {
54
+ postgres: ['mtxr.sqltools', 'mtxr.sqltools-driver-pg'],
55
+ mysql: ['mtxr.sqltools', 'mtxr.sqltools-driver-mysql'],
56
+ mongodb: ['mongodb.mongodb-vscode'],
57
+ redis: ['redis.redis-for-vscode'],
58
+ };
59
+ export const EXTENSION_ID = /^[\w-]+\.[\w-]+$/;
60
+ /** The extensions to have: always EditorConfig, then per toolchain and database in use, the project's, and yours. */
61
+ export const vscodeExtensions = (o) => {
62
+ const all = ['editorconfig.editorconfig', ...o.toolchains.flatMap((t) => BY_TOOLCHAIN[t] ?? []), ...o.databases.flatMap((d) => BY_DATABASE[d] ?? []), ...o.recommended, ...o.extra];
63
+ return [...new Set(all.map((e) => e.toLowerCase()))].filter((e) => EXTENSION_ID.test(e));
64
+ };
65
+ /** Extensions not installed yet, from the names of the folders in VS Code's extensions folder (publisher.name-1.2.3). */
66
+ export const missingExtensions = (wanted, folders) => {
67
+ const have = folders.map((f) => f.toLowerCase());
68
+ return wanted.filter((id) => !have.some((f) => f.startsWith(id + '-')));
69
+ };
@@ -1,2 +1,19 @@
1
+ import type { Toolchains } from '../../core/types.js';
1
2
  import { type ToolEnv } from './common.js';
3
+ /**
4
+ * After every toolchain is installed: points VS Code at the sandbox's own JDK, Python, Go, compilers, .NET, Git and
5
+ * terminals (user settings; only the keys this tool manages are changed, your own stay), and installs the extensions
6
+ * for the languages in use in the background (they persist, so only new ones are downloaded).
7
+ */
8
+ export declare const configureVscode: (tools: {
9
+ vars: Record<string, string>;
10
+ prepend: string[];
11
+ append: string[];
12
+ summary: string[];
13
+ }, o: {
14
+ toolchains: string[];
15
+ databases: string[];
16
+ config: Toolchains["vscode"];
17
+ workspace: string;
18
+ }) => void;
2
19
  export declare const setupVscode: (env: ToolEnv, refresh: boolean) => Promise<void>;
@@ -3,9 +3,13 @@
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
+ import { applyEdits, modify, parse as parseJsonc } from 'jsonc-parser';
6
8
  import { fetchJson, installArchive } from '../../shared/download.js';
7
- import { dryRun, exists, isWin, log, mkdirp, powershell } from '../env.js';
9
+ import { SYSTEM } from '../../shared/system.js';
10
+ import { dryRun, exists, isWin, log, logsDir, mkdirp, powershell } from '../env.js';
8
11
  import { arch, tool } from './common.js';
12
+ import { EXTENSION_ID, missingExtensions, vscodeExtensions, vscodeSettings } from './vscode-profile.js';
9
13
  /** Newer builds keep their files under a commit-hash folder, so the installed version is recorded here. */
10
14
  const VERSION_MARKER = '.agent-sandbox-version';
11
15
  const desktopShortcut = (exe) => {
@@ -15,6 +19,88 @@ const desktopShortcut = (exe) => {
15
19
  const q = (s) => "'" + s.replace(/'/g, "''") + "'";
16
20
  powershell('$s = (New-Object -ComObject WScript.Shell).CreateShortcut(' + q(link) + '); $s.TargetPath = ' + q(exe) + '; $s.Arguments = ' + q('"' + (process.env.AGENT_WORKSPACE ?? '') + '"') + '; $s.Save()');
17
21
  };
22
+ /** The first of `names` in one of `dirs`. */
23
+ const findIn = (dirs, ...names) => {
24
+ for (const dir of dirs)
25
+ for (const name of names) {
26
+ const file = path.join(dir, name);
27
+ if (exists(file))
28
+ return file;
29
+ }
30
+ return undefined;
31
+ };
32
+ /** The project's .vscode/extensions.json recommendations (bounded; anything odd is ignored). */
33
+ const recommendedBy = (workspace) => {
34
+ const file = path.join(workspace, '.vscode', 'extensions.json');
35
+ try {
36
+ if (!exists(file) || fs.statSync(file).size > 64 * 1024)
37
+ return [];
38
+ const data = parseJsonc(fs.readFileSync(file, 'utf8'));
39
+ return Array.isArray(data?.recommendations) ? data.recommendations.filter((r) => typeof r === 'string' && EXTENSION_ID.test(r)).slice(0, 50) : [];
40
+ }
41
+ catch {
42
+ return [];
43
+ }
44
+ };
45
+ /**
46
+ * After every toolchain is installed: points VS Code at the sandbox's own JDK, Python, Go, compilers, .NET, Git and
47
+ * terminals (user settings; only the keys this tool manages are changed, your own stay), and installs the extensions
48
+ * for the languages in use in the background (they persist, so only new ones are downloaded).
49
+ */
50
+ export const configureVscode = (tools, o) => {
51
+ const root = tool('vscode');
52
+ const code = path.join(root, 'bin', 'code.cmd');
53
+ if (dryRun) {
54
+ log('(dry run) would set up VS Code for: ' + o.toolchains.join(', '));
55
+ return;
56
+ }
57
+ if (!exists(path.join(root, 'Code.exe')))
58
+ return;
59
+ const dirs = [...tools.prepend, ...tools.append];
60
+ const git = findIn([tool('git', 'cmd'), ...dirs], 'git.exe');
61
+ const settings = vscodeSettings({
62
+ javaHome: tools.vars.JAVA_HOME, goRoot: tools.vars.GOROOT, go: findIn(dirs, 'go.exe'), python: findIn(dirs, 'python.exe'), gcc: findIn(dirs, 'gcc.exe'), cl: findIn(dirs, 'cl.exe'),
63
+ cmake: findIn(dirs, 'cmake.exe'), dotnet: findIn(dirs, 'dotnet.exe'), node: findIn(dirs, 'node.exe'), pwsh: findIn(dirs, 'pwsh.exe'), git,
64
+ bash: git ? findIn([path.join(path.dirname(path.dirname(git)), 'bin')], 'bash.exe') : undefined, php: findIn(dirs, 'php.exe'), ruby: findIn(dirs, 'ruby.exe'), deno: findIn(dirs, 'deno.exe'),
65
+ });
66
+ writeManagedSettings(path.join(root, 'data', 'user-data', 'User', 'settings.json'), path.join(root, 'data', '.agent-sandbox-managed.json'), settings);
67
+ const wanted = vscodeExtensions({ toolchains: o.toolchains, databases: o.databases, recommended: o.config.recommended ? recommendedBy(o.workspace) : [], extra: o.config.extensions });
68
+ let folders = [];
69
+ try {
70
+ folders = fs.readdirSync(path.join(root, 'data', 'extensions'));
71
+ }
72
+ catch { /* none yet */ }
73
+ const missing = missingExtensions(wanted, folders);
74
+ tools.summary.push('vscode: set up for ' + (o.toolchains.join(', ') || 'Node') + (missing.length ? '; installing ' + missing.length + ' extension(s) in the background' : '; extensions ready'));
75
+ if (!missing.length)
76
+ return;
77
+ // In the background: the Marketplace can take a minute, and nothing else waits for it.
78
+ const logFile = fs.openSync(path.join(logsDir, 'vscode-extensions.log'), 'a');
79
+ spawn(SYSTEM.cmd(), ['/d', '/c', code, ...missing.flatMap((id) => ['--install-extension', id])], { detached: true, stdio: ['ignore', logFile, logFile], windowsHide: true }).unref();
80
+ fs.closeSync(logFile);
81
+ };
82
+ /**
83
+ * Sets `values` in a settings file, keeping everything else (comments too), and removes keys this tool set last time
84
+ * but no longer has a value for (a toolchain turned off). The keys it manages are remembered in `managedFile`.
85
+ */
86
+ const writeManagedSettings = (file, managedFile, values) => {
87
+ let text = exists(file) ? fs.readFileSync(file, 'utf8') : '{}';
88
+ let before = [];
89
+ try {
90
+ before = JSON.parse(fs.readFileSync(managedFile, 'utf8'));
91
+ }
92
+ catch { /* first time */ }
93
+ const format = { formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' } };
94
+ for (const key of before.filter((k) => !(k in values))) {
95
+ if (parseJsonc(text)?.[key] !== undefined)
96
+ text = applyEdits(text, modify(text, [key], undefined, format));
97
+ }
98
+ for (const [key, value] of Object.entries(values))
99
+ text = applyEdits(text, modify(text, [key], value, format));
100
+ mkdirp(path.dirname(file));
101
+ fs.writeFileSync(file, text);
102
+ fs.writeFileSync(managedFile, JSON.stringify(Object.keys(values)));
103
+ };
18
104
  export const setupVscode = async (env, refresh) => {
19
105
  const root = tool('vscode');
20
106
  const exe = path.join(root, 'Code.exe');
@@ -14,7 +14,8 @@ const ipToInt = (ip) => ip.split('.').reduce((n, part) => n * 256 + Number(part)
14
14
  export const isSandboxAddress = (ip, interfaces = os.networkInterfaces()) => {
15
15
  if (!net.isIPv4(ip))
16
16
  return false;
17
- return Object.entries(interfaces).some(([name, list]) => /^vEthernet/i.test(name) && (list ?? []).some((i) => {
17
+ // WSL's adapter is a Hyper-V adapter too, but no sandbox lives there: keystrokes must never go to a WSL VM.
18
+ return Object.entries(interfaces).some(([name, list]) => /^vEthernet/i.test(name) && !/WSL/i.test(name) && (list ?? []).some((i) => {
18
19
  if (i.family !== 'IPv4' || i.internal)
19
20
  return false;
20
21
  const mask = ipToInt(i.netmask);
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ declare const AuditSettings: z.ZodObject<{
3
+ enabled: z.ZodBoolean;
4
+ days: z.ZodNumber;
5
+ maxMb: z.ZodNumber;
6
+ processes: z.ZodBoolean;
7
+ files: z.ZodBoolean;
8
+ projects: z.ZodArray<z.ZodString>;
9
+ }, z.core.$strip>;
10
+ export type AuditSettings = z.output<typeof AuditSettings>;
11
+ /** Host-only: the audit settings of the running sandbox (written at launch, read by the watcher). */
12
+ export declare const saveAuditSettings: (storageRoot: string, dataPath: string, settings: AuditSettings) => void;
13
+ export declare const readAuditSettings: (storageRoot: string, dataPath: string) => AuditSettings | null;
14
+ export declare class AuditCollector {
15
+ private readonly dataPath;
16
+ private readonly settings;
17
+ private readonly log;
18
+ private pending;
19
+ private sessions;
20
+ private watchers;
21
+ constructor(dataPath: string, storageRoot: string, settings: AuditSettings);
22
+ private add;
23
+ /** Starts watching the projects' files (when audit.files). */
24
+ start(): void;
25
+ /** Programs the sandbox reported: taken in (checked) and deleted. */
26
+ private takeGuestReports;
27
+ private trackSessions;
28
+ guard(detail: string): void;
29
+ /** Writes what came in since the last call (repeats folded); call every few seconds. */
30
+ flush(): void;
31
+ stop(): void;
32
+ maintain(): void;
33
+ }
34
+ export {};
@@ -0,0 +1,126 @@
1
+ // What goes into the audit log, from the background watcher while the sandbox runs:
2
+ // - programs started in the sandbox: the .jsonl files it leaves in run\audit\ (untrusted: links refused, each file
3
+ // and line size-capped, every line checked and forced to kind "proc"), taken in and deleted;
4
+ // - files changed in the projects: watched from this PC (the sandbox cannot hide or fake these);
5
+ // - sessions starting and ending, and what the repo guard caught.
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { z } from 'zod';
9
+ import { assertNoLinks } from '../../core/util.js';
10
+ import { listSessions, readValid, runPaths, writeJsonAtomic } from '../../shared/rundir.js';
11
+ import { AuditEvent, AuditLog, fold } from './log.js';
12
+ const MAX_FILES_PER_PASS = 200;
13
+ const MAX_FILE_BYTES = 8 * 1024 * 1024;
14
+ /** Paths in a project not worth a line each (git's own bookkeeping, dependency folders). */
15
+ const QUIET = /(^|[\\/])(\.git[\\/](objects|logs|refs|index\.lock|FETCH_HEAD|ORIG_HEAD)|node_modules[\\/]|target[\\/]|\.venv[\\/]|__pycache__[\\/])/i;
16
+ const AuditSettings = z.object({
17
+ enabled: z.boolean(), days: z.number().int().min(1).max(365), maxMb: z.number().int().min(5).max(5000), processes: z.boolean(), files: z.boolean(),
18
+ projects: z.array(z.string().max(1024)).max(64),
19
+ });
20
+ const settingsFile = (storageRoot, dataPath) => path.join(storageRoot, 'sync', path.basename(dataPath) + '-audit.json');
21
+ /** Host-only: the audit settings of the running sandbox (written at launch, read by the watcher). */
22
+ export const saveAuditSettings = (storageRoot, dataPath, settings) => writeJsonAtomic(settingsFile(storageRoot, dataPath), settings);
23
+ export const readAuditSettings = (storageRoot, dataPath) => readValid(AuditSettings, settingsFile(storageRoot, dataPath));
24
+ export class AuditCollector {
25
+ dataPath;
26
+ settings;
27
+ log;
28
+ pending = [];
29
+ sessions = new Map();
30
+ watchers = [];
31
+ constructor(dataPath, storageRoot, settings) {
32
+ this.dataPath = dataPath;
33
+ this.settings = settings;
34
+ this.log = new AuditLog(AuditLog.dirFor(storageRoot, dataPath), settings);
35
+ }
36
+ add(e) { this.pending.push({ t: new Date().toISOString(), ...e }); }
37
+ /** Starts watching the projects' files (when audit.files). */
38
+ start() {
39
+ this.add({ k: 'note', p: 'sandbox started; audit: ' + [this.settings.processes && 'programs', this.settings.files && 'files', 'sessions'].filter(Boolean).join(', ') });
40
+ if (!this.settings.files)
41
+ return;
42
+ for (const project of this.settings.projects) {
43
+ try {
44
+ this.watchers.push(fs.watch(project, { recursive: true }, (event, file) => {
45
+ const rel = file ? String(file) : '';
46
+ if (!rel || QUIET.test(rel))
47
+ return;
48
+ this.add({ k: 'file', a: event === 'rename' ? 'rename' : 'change', p: rel.replace(/\\/g, '/').slice(0, 1024), w: project });
49
+ }));
50
+ }
51
+ catch { /* a project that is gone */ }
52
+ }
53
+ }
54
+ /** Programs the sandbox reported: taken in (checked) and deleted. */
55
+ takeGuestReports() {
56
+ if (!this.settings.processes)
57
+ return;
58
+ const dir = runPaths(this.dataPath).audit;
59
+ assertNoLinks(this.dataPath, dir);
60
+ let names = [];
61
+ try {
62
+ names = fs.readdirSync(dir).filter((n) => /^[\w-]+\.jsonl$/.test(n)).sort().slice(0, MAX_FILES_PER_PASS);
63
+ }
64
+ catch {
65
+ return;
66
+ }
67
+ for (const name of names) {
68
+ const file = path.join(dir, name);
69
+ try {
70
+ const stat = fs.lstatSync(file);
71
+ if (stat.isFile() && stat.size <= MAX_FILE_BYTES) {
72
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
73
+ if (!line.trim() || line.length > 8192)
74
+ continue;
75
+ let raw;
76
+ try {
77
+ raw = JSON.parse(line);
78
+ }
79
+ catch {
80
+ continue;
81
+ }
82
+ const e = AuditEvent.safeParse(raw);
83
+ if (e.success)
84
+ this.pending.push({ ...e.data, k: 'proc', w: undefined, a: undefined });
85
+ }
86
+ }
87
+ }
88
+ finally {
89
+ fs.rmSync(file, { force: true });
90
+ }
91
+ }
92
+ }
93
+ trackSessions() {
94
+ const now = new Map(listSessions(this.dataPath).map((s) => [s.id, s.phase]));
95
+ for (const s of listSessions(this.dataPath)) {
96
+ const before = this.sessions.get(s.id);
97
+ if (!before && s.phase === 'running')
98
+ this.add({ k: 'session', a: 'start', p: s.harness, w: s.cwd, c: s.id });
99
+ if (before === 'running' && (s.phase === 'exited' || s.phase === 'failed'))
100
+ this.add({ k: 'session', a: 'end', p: s.harness, w: s.cwd, c: s.id + (s.exitCode !== undefined ? ' exit ' + s.exitCode : '') });
101
+ }
102
+ this.sessions = now;
103
+ }
104
+ guard(detail) { this.add({ k: 'guard', p: detail.slice(0, 1024) }); }
105
+ /** Writes what came in since the last call (repeats folded); call every few seconds. */
106
+ flush() {
107
+ try {
108
+ this.takeGuestReports();
109
+ }
110
+ catch { /* next pass */ }
111
+ try {
112
+ this.trackSessions();
113
+ }
114
+ catch { /* next pass */ }
115
+ const events = this.pending.sort((a, b) => a.t.localeCompare(b.t));
116
+ this.pending = [];
117
+ this.log.append(fold(events));
118
+ }
119
+ stop() {
120
+ this.watchers.forEach((w) => w.close());
121
+ this.add({ k: 'note', p: 'sandbox closed' });
122
+ this.flush();
123
+ this.log.maintain();
124
+ }
125
+ maintain() { this.log.maintain(); }
126
+ }
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod';
2
+ /** One event. k: proc (a program started), file (changed in a project), session, guard (repo guard), note. */
3
+ export declare const AuditEvent: z.ZodObject<{
4
+ t: z.ZodString;
5
+ k: z.ZodEnum<{
6
+ file: "file";
7
+ session: "session";
8
+ proc: "proc";
9
+ guard: "guard";
10
+ note: "note";
11
+ }>;
12
+ p: z.ZodOptional<z.ZodString>;
13
+ c: z.ZodOptional<z.ZodString>;
14
+ pp: z.ZodOptional<z.ZodString>;
15
+ a: z.ZodOptional<z.ZodString>;
16
+ w: z.ZodOptional<z.ZodString>;
17
+ n: z.ZodOptional<z.ZodNumber>;
18
+ }, z.core.$strip>;
19
+ export type AuditEvent = z.output<typeof AuditEvent>;
20
+ export declare class AuditLog {
21
+ readonly dir: string;
22
+ private readonly limits;
23
+ constructor(dir: string, limits: {
24
+ days: number;
25
+ maxMb: number;
26
+ });
27
+ /** <storage>\audit\<data set name>. */
28
+ static dirFor(storageRoot: string, dataPath: string): string;
29
+ private dayFile;
30
+ append(events: AuditEvent[]): void;
31
+ /** Compresses finished days, then deletes days past the limits. Cheap when there is nothing to do. */
32
+ maintain(today?: string): void;
33
+ /** Events of the last `days` days (newest last), optionally of some kinds and containing a text. */
34
+ read(o?: {
35
+ days?: number;
36
+ kinds?: AuditEvent['k'][];
37
+ grep?: string;
38
+ }): AuditEvent[];
39
+ /** Total size on disk. */
40
+ bytes(): number;
41
+ }
42
+ /**
43
+ * Folds repeats: the same event (kind, program/path, command line, action, project) within one minute becomes one line
44
+ * with n = how many times; after `cap` distinct lines a minute, the rest of that minute is one "note" line.
45
+ */
46
+ export declare const fold: (events: AuditEvent[], cap?: number) => AuditEvent[];
@@ -0,0 +1,143 @@
1
+ // The audit log (audit.enabled): one file per day of JSON lines in host-only storage (<storage>\audit\<data set>),
2
+ // which the sandbox cannot reach. Days before today are gzipped; days past audit.days and, past audit.maxMb, the
3
+ // oldest days are deleted. Lines are short (field names are one or two letters) and capped in length.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import zlib from 'node:zlib';
7
+ import { z } from 'zod';
8
+ /** One event. k: proc (a program started), file (changed in a project), session, guard (repo guard), note. */
9
+ export const AuditEvent = z.object({
10
+ /** ISO time. */
11
+ t: z.string().max(40),
12
+ k: z.enum(['proc', 'file', 'session', 'guard', 'note']),
13
+ /** proc: program; file: path in the project; session: agent; guard/note: what happened. */
14
+ p: z.string().max(1024).optional(),
15
+ /** proc: its command line (capped). */
16
+ c: z.string().max(2048).optional(),
17
+ /** proc: the program that started it. */
18
+ pp: z.string().max(1024).optional(),
19
+ /** file: change | rename; session: start | end. */
20
+ a: z.string().max(32).optional(),
21
+ /** project folder (file, session). */
22
+ w: z.string().max(1024).optional(),
23
+ /** how many times it happened in this line's minute (repeats are counted, not repeated). */
24
+ n: z.number().int().min(1).max(1e9).optional(),
25
+ });
26
+ const DAY_FILE = /^(\d{4}-\d{2}-\d{2})\.jsonl(\.gz)?$/;
27
+ export class AuditLog {
28
+ dir;
29
+ limits;
30
+ constructor(dir, limits) {
31
+ this.dir = dir;
32
+ this.limits = limits;
33
+ }
34
+ /** <storage>\audit\<data set name>. */
35
+ static dirFor(storageRoot, dataPath) { return path.join(storageRoot, 'audit', path.basename(dataPath)); }
36
+ dayFile(day, gz = false) { return path.join(this.dir, day + '.jsonl' + (gz ? '.gz' : '')); }
37
+ append(events) {
38
+ if (!events.length)
39
+ return;
40
+ fs.mkdirSync(this.dir, { recursive: true });
41
+ const byDay = new Map();
42
+ for (const e of events) {
43
+ const day = e.t.slice(0, 10);
44
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(day))
45
+ continue;
46
+ byDay.set(day, [...(byDay.get(day) ?? []), JSON.stringify(e)]);
47
+ }
48
+ for (const [day, lines] of byDay)
49
+ fs.appendFileSync(this.dayFile(day), lines.join('\n') + '\n');
50
+ }
51
+ /** Compresses finished days, then deletes days past the limits. Cheap when there is nothing to do. */
52
+ maintain(today = new Date().toISOString().slice(0, 10)) {
53
+ if (!fs.existsSync(this.dir))
54
+ return;
55
+ for (const name of fs.readdirSync(this.dir)) {
56
+ const m = DAY_FILE.exec(name);
57
+ if (!m || m[2] || m[1] >= today)
58
+ continue;
59
+ const file = path.join(this.dir, name);
60
+ fs.writeFileSync(file + '.gz', zlib.gzipSync(fs.readFileSync(file)));
61
+ fs.rmSync(file);
62
+ }
63
+ const oldest = new Date(Date.parse(today) - (this.limits.days - 1) * 86400000).toISOString().slice(0, 10);
64
+ const files = fs.readdirSync(this.dir).filter((n) => DAY_FILE.test(n)).sort();
65
+ for (const name of files.filter((n) => DAY_FILE.exec(n)[1] < oldest))
66
+ fs.rmSync(path.join(this.dir, name), { force: true });
67
+ let left = fs.readdirSync(this.dir).filter((n) => DAY_FILE.test(n)).sort();
68
+ const size = () => left.reduce((sum, n) => sum + fs.statSync(path.join(this.dir, n)).size, 0);
69
+ while (left.length > 1 && size() > this.limits.maxMb * 1024 * 1024) {
70
+ fs.rmSync(path.join(this.dir, left[0]), { force: true });
71
+ left = left.slice(1);
72
+ }
73
+ }
74
+ /** Events of the last `days` days (newest last), optionally of some kinds and containing a text. */
75
+ read(o = {}) {
76
+ if (!fs.existsSync(this.dir))
77
+ return [];
78
+ const since = new Date(Date.now() - ((o.days ?? 1) - 1) * 86400000).toISOString().slice(0, 10);
79
+ const needle = o.grep?.toLowerCase();
80
+ const events = [];
81
+ for (const name of fs.readdirSync(this.dir).filter((n) => DAY_FILE.test(n) && DAY_FILE.exec(n)[1] >= since).sort()) {
82
+ const raw = fs.readFileSync(path.join(this.dir, name));
83
+ const text = name.endsWith('.gz') ? zlib.gunzipSync(raw).toString('utf8') : raw.toString('utf8');
84
+ for (const line of text.split('\n')) {
85
+ if (!line.trim())
86
+ continue;
87
+ let parsed;
88
+ try {
89
+ parsed = JSON.parse(line);
90
+ }
91
+ catch {
92
+ continue;
93
+ }
94
+ const e = AuditEvent.safeParse(parsed);
95
+ if (!e.success)
96
+ continue;
97
+ if (o.kinds?.length && !o.kinds.includes(e.data.k))
98
+ continue;
99
+ if (needle && !line.toLowerCase().includes(needle))
100
+ continue;
101
+ events.push(e.data);
102
+ }
103
+ }
104
+ return events;
105
+ }
106
+ /** Total size on disk. */
107
+ bytes() {
108
+ if (!fs.existsSync(this.dir))
109
+ return 0;
110
+ return fs.readdirSync(this.dir).filter((n) => DAY_FILE.test(n)).reduce((sum, n) => sum + fs.statSync(path.join(this.dir, n)).size, 0);
111
+ }
112
+ }
113
+ /**
114
+ * Folds repeats: the same event (kind, program/path, command line, action, project) within one minute becomes one line
115
+ * with n = how many times; after `cap` distinct lines a minute, the rest of that minute is one "note" line.
116
+ */
117
+ export const fold = (events, cap = 300) => {
118
+ const out = [];
119
+ const index = new Map();
120
+ const perMinute = new Map();
121
+ const dropped = new Map();
122
+ for (const e of events) {
123
+ const minute = e.t.slice(0, 16);
124
+ const key = [minute, e.k, e.p, e.c, e.a, e.w].join('\0');
125
+ const seen = index.get(key);
126
+ if (seen) {
127
+ seen.n = (seen.n ?? 1) + (e.n ?? 1);
128
+ continue;
129
+ }
130
+ const count = (perMinute.get(minute) ?? 0) + 1;
131
+ perMinute.set(minute, count);
132
+ if (count > cap) {
133
+ dropped.set(minute, (dropped.get(minute) ?? 0) + (e.n ?? 1));
134
+ continue;
135
+ }
136
+ const copy = { ...e };
137
+ index.set(key, copy);
138
+ out.push(copy);
139
+ }
140
+ for (const [minute, n] of dropped)
141
+ out.push({ t: minute + ':59.999Z', k: 'note', p: n + ' more events in this minute were not listed (audit cap)' });
142
+ return out;
143
+ };
@@ -2,6 +2,12 @@ import type { GenerateOptions, Generated } from '../core/types.js';
2
2
  import { decideToolchains } from '../core/detect-toolchains.js';
3
3
  /** Which toolchains the project needs ("auto" settings follow the project's files and what this PC has). */
4
4
  export declare const decideProjectToolchains: (o: Pick<GenerateOptions, "run" | "pcLanguages">) => ReturnType<typeof decideToolchains>;
5
+ /**
6
+ * env.cmd, which logon.cmd calls before it opens Windows Terminal: Terminal expands "%AGENT_WORKSPACE%" (the starting
7
+ * folder of new tabs) from its own environment, and the runtime only sets the variable machine-wide after Terminal
8
+ * started. `%` is doubled for cmd; a Windows path holds no quotes.
9
+ */
10
+ export declare const logonEnv: (workspace: string) => string;
5
11
  export declare const generateSandbox: (o: GenerateOptions) => Generated & {
6
12
  personalization: string;
7
13
  };