windows-development-sandbox 1.1.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/.sandbox-config.example.json +2 -1
  2. package/README.md +3 -2
  3. package/config.example.json +39 -5
  4. package/dist/cli/args.d.ts +3 -0
  5. package/dist/cli/args.js +3 -1
  6. package/dist/cli/boot-progress.d.ts +22 -0
  7. package/dist/cli/boot-progress.js +59 -0
  8. package/dist/cli/commands/accounts.js +2 -2
  9. package/dist/cli/commands/agent-commands.js +1 -1
  10. package/dist/cli/commands/context.js +2 -1
  11. package/dist/cli/commands/data/clean.js +6 -1
  12. package/dist/cli/commands/data/collect.js +3 -3
  13. package/dist/cli/commands/index.d.ts +1 -1
  14. package/dist/cli/commands/index.js +1 -1
  15. package/dist/cli/commands/launch.d.ts +5 -2
  16. package/dist/cli/commands/launch.js +113 -25
  17. package/dist/cli/commands/maintenance.d.ts +2 -0
  18. package/dist/cli/commands/maintenance.js +70 -4
  19. package/dist/cli/commands/project.d.ts +10 -6
  20. package/dist/cli/commands/project.js +42 -3
  21. package/dist/cli/commands/restart.js +17 -7
  22. package/dist/cli/commands/sessions.js +5 -4
  23. package/dist/cli/commands/user-config.js +1 -1
  24. package/dist/cli/index.js +6 -4
  25. package/dist/cli/manifest.js +6 -1
  26. package/dist/cli/menu/apps.d.ts +21 -0
  27. package/dist/cli/menu/apps.js +122 -0
  28. package/dist/cli/menu/command-form.d.ts +28 -0
  29. package/dist/cli/menu/command-form.js +219 -0
  30. package/dist/cli/menu/index.d.ts +26 -0
  31. package/dist/cli/menu/index.js +180 -0
  32. package/dist/cli/menu/settings/categories.d.ts +9 -0
  33. package/dist/cli/menu/settings/categories.js +28 -0
  34. package/dist/cli/menu/settings/config-file.d.ts +40 -0
  35. package/dist/cli/menu/settings/config-file.js +71 -0
  36. package/dist/cli/menu/settings/editors.d.ts +30 -0
  37. package/dist/cli/menu/settings/editors.js +305 -0
  38. package/dist/cli/menu/settings/index.d.ts +32 -0
  39. package/dist/cli/menu/settings/index.js +136 -0
  40. package/dist/cli/menu/settings/schema-node.d.ts +62 -0
  41. package/dist/cli/menu/settings/schema-node.js +104 -0
  42. package/dist/cli/{menu-multi.d.ts → menu/several.d.ts} +10 -2
  43. package/dist/cli/menu/several.js +102 -0
  44. package/dist/cli/progress.d.ts +15 -0
  45. package/dist/cli/progress.js +52 -0
  46. package/dist/cli/tui/app.d.ts +39 -0
  47. package/dist/cli/tui/app.js +52 -0
  48. package/dist/cli/tui/dialogs.d.ts +57 -0
  49. package/dist/cli/tui/dialogs.js +162 -0
  50. package/dist/cli/tui/frame.d.ts +18 -0
  51. package/dist/cli/tui/frame.js +27 -0
  52. package/dist/cli/tui/index.d.ts +7 -0
  53. package/dist/cli/tui/index.js +8 -0
  54. package/dist/cli/tui/list-screen.d.ts +101 -0
  55. package/dist/cli/tui/list-screen.js +299 -0
  56. package/dist/cli/tui/terminal.d.ts +37 -0
  57. package/dist/cli/tui/terminal.js +89 -0
  58. package/dist/cli/tui/text.d.ts +20 -0
  59. package/dist/cli/tui/text.js +105 -0
  60. package/dist/cli/tui/theme.d.ts +45 -0
  61. package/dist/cli/tui/theme.js +30 -0
  62. package/dist/core/config.js +10 -7
  63. package/dist/core/local-config.d.ts +31 -1
  64. package/dist/core/local-config.js +72 -7
  65. package/dist/core/project-policy.d.ts +50 -3
  66. package/dist/core/project-policy.js +90 -4
  67. package/dist/core/schema/apps.d.ts +40 -0
  68. package/dist/core/schema/apps.js +27 -0
  69. package/dist/core/schema/config.d.ts +69 -3
  70. package/dist/core/schema/config.js +18 -6
  71. package/dist/core/schema/profile.d.ts +5 -0
  72. package/dist/core/schema/profile.js +4 -2
  73. package/dist/core/schema/projects.d.ts +1 -0
  74. package/dist/core/schema/projects.js +1 -0
  75. package/dist/core/schema/sandbox.d.ts +29 -1
  76. package/dist/core/schema/sandbox.js +29 -1
  77. package/dist/core/schema/toolchains.d.ts +3 -0
  78. package/dist/core/schema/toolchains.js +6 -1
  79. package/dist/core/types.d.ts +24 -1
  80. package/dist/core/util.d.ts +2 -0
  81. package/dist/core/util.js +2 -0
  82. package/dist/core/workspace.d.ts +21 -4
  83. package/dist/core/workspace.js +99 -21
  84. package/dist/guest/apps/index.d.ts +10 -0
  85. package/dist/guest/apps/index.js +206 -0
  86. package/dist/guest/apps/resolvers.d.ts +8 -0
  87. package/dist/guest/apps/resolvers.js +90 -0
  88. package/dist/guest/audit-parse.d.ts +7 -0
  89. package/dist/guest/audit-parse.js +24 -0
  90. package/dist/guest/audit.d.ts +4 -0
  91. package/dist/guest/audit.js +47 -0
  92. package/dist/guest/daemon/relays.d.ts +1 -0
  93. package/dist/guest/daemon/relays.js +18 -0
  94. package/dist/guest/daemon.js +4 -0
  95. package/dist/guest/env.d.ts +7 -2
  96. package/dist/guest/env.js +17 -3
  97. package/dist/guest/helpers.d.ts +1 -1
  98. package/dist/guest/main.js +20 -3
  99. package/dist/guest/netproxy.d.ts +1 -0
  100. package/dist/guest/netproxy.js +100 -0
  101. package/dist/guest/ptyhost.js +14 -4
  102. package/dist/guest/setup/claude.d.ts +0 -4
  103. package/dist/guest/setup/claude.js +48 -2
  104. package/dist/guest/setup/network.d.ts +9 -0
  105. package/dist/guest/setup/network.js +97 -4
  106. package/dist/guest/setup/user.js +4 -0
  107. package/dist/guest/toolchains/index.js +29 -3
  108. package/dist/guest/toolchains/native/mesa.js +13 -3
  109. package/dist/guest/toolchains/plan.d.ts +3 -1
  110. package/dist/guest/toolchains/plan.js +53 -9
  111. package/dist/guest/toolchains/python.js +26 -2
  112. package/dist/guest/toolchains/vscode-profile.d.ts +30 -0
  113. package/dist/guest/toolchains/vscode-profile.js +69 -0
  114. package/dist/guest/toolchains/vscode.d.ts +17 -0
  115. package/dist/guest/toolchains/vscode.js +87 -1
  116. package/dist/host/attach.js +2 -1
  117. package/dist/host/audit/collect.d.ts +34 -0
  118. package/dist/host/audit/collect.js +126 -0
  119. package/dist/host/audit/log.d.ts +46 -0
  120. package/dist/host/audit/log.js +143 -0
  121. package/dist/host/channel.d.ts +2 -2
  122. package/dist/host/channel.js +3 -2
  123. package/dist/host/generate.d.ts +7 -1
  124. package/dist/host/generate.js +23 -6
  125. package/dist/host/join.d.ts +7 -0
  126. package/dist/host/join.js +23 -9
  127. package/dist/host/launch/mappings.d.ts +7 -2
  128. package/dist/host/launch/mappings.js +68 -18
  129. package/dist/host/launch/projects.d.ts +2 -0
  130. package/dist/host/launch/projects.js +11 -5
  131. package/dist/host/launch/stage.js +1 -1
  132. package/dist/host/motw.d.ts +17 -0
  133. package/dist/host/motw.js +74 -0
  134. package/dist/host/platform.js +3 -0
  135. package/dist/host/port-forward.d.ts +8 -0
  136. package/dist/host/port-forward.js +37 -0
  137. package/dist/host/projects.d.ts +25 -2
  138. package/dist/host/projects.js +54 -5
  139. package/dist/host/repo-guard.d.ts +10 -3
  140. package/dist/host/repo-guard.js +142 -14
  141. package/dist/host/stops.d.ts +67 -0
  142. package/dist/host/stops.js +126 -0
  143. package/dist/host/terminal.d.ts +8 -0
  144. package/dist/host/terminal.js +45 -1
  145. package/dist/host/workspace-view/index.d.ts +20 -0
  146. package/dist/host/workspace-view/index.js +53 -0
  147. package/dist/host/workspace-view/rules.d.ts +26 -0
  148. package/dist/host/workspace-view/rules.js +93 -0
  149. package/dist/host/workspace-view/view.d.ts +50 -0
  150. package/dist/host/workspace-view/view.js +281 -0
  151. package/dist/runtime/daemon.js +1387 -558
  152. package/dist/runtime/db.js +199 -14
  153. package/dist/runtime/main.js +3082 -631
  154. package/dist/runtime/netproxy.js +25948 -0
  155. package/dist/runtime/view.js +531 -148
  156. package/dist/selftest/audit.test.d.ts +2 -0
  157. package/dist/selftest/audit.test.js +57 -0
  158. package/dist/selftest/channel.test.js +45 -2
  159. package/dist/selftest/cli.test.js +47 -17
  160. package/dist/selftest/fixtures.d.ts +47 -1
  161. package/dist/selftest/fixtures.js +3 -1
  162. package/dist/selftest/generate.test.js +7 -2
  163. package/dist/selftest/index.js +6 -2
  164. package/dist/selftest/live-network.test.d.ts +2 -0
  165. package/dist/selftest/live-network.test.js +63 -0
  166. package/dist/selftest/live-terminal.test.d.ts +2 -0
  167. package/dist/selftest/live-terminal.test.js +169 -0
  168. package/dist/selftest/local-config.test.js +42 -4
  169. package/dist/selftest/mirror.test.js +36 -7
  170. package/dist/selftest/network.test.js +12 -0
  171. package/dist/selftest/projects.test.js +77 -8
  172. package/dist/selftest/repo-guard.test.js +59 -1
  173. package/dist/selftest/sync.test.js +50 -10
  174. package/dist/selftest/toolchains.test.js +30 -0
  175. package/dist/selftest/workspace-view.test.d.ts +2 -0
  176. package/dist/selftest/workspace-view.test.js +89 -0
  177. package/dist/shared/apps.d.ts +49 -0
  178. package/dist/shared/apps.js +114 -0
  179. package/dist/shared/console-input.d.ts +11 -0
  180. package/dist/shared/console-input.js +61 -0
  181. package/dist/shared/download.d.ts +3 -0
  182. package/dist/shared/download.js +25 -2
  183. package/dist/shared/guest-paths.d.ts +27 -0
  184. package/dist/shared/guest-paths.js +68 -0
  185. package/dist/shared/netpolicy.d.ts +27 -0
  186. package/dist/shared/netpolicy.js +51 -0
  187. package/dist/shared/projects.d.ts +27 -4
  188. package/dist/shared/projects.js +72 -13
  189. package/dist/shared/pty-client.d.ts +26 -4
  190. package/dist/shared/pty-client.js +204 -25
  191. package/dist/shared/rundir.d.ts +2 -0
  192. package/dist/shared/rundir.js +2 -0
  193. package/dist/shared/schema.d.ts +34 -0
  194. package/dist/shared/schema.js +14 -0
  195. package/dist/shared/system.d.ts +1 -0
  196. package/dist/shared/system.js +1 -0
  197. package/dist/sync/claude-mem.js +3 -2
  198. package/dist/sync/history.d.ts +6 -0
  199. package/dist/sync/history.js +26 -11
  200. package/dist/sync/watcher.js +78 -6
  201. package/package.json +2 -1
  202. package/sandbox/cargoshim.cs +56 -0
  203. package/sandbox/cpucap.cs +33 -0
  204. package/sandbox/cpucap.ps1 +4 -35
  205. package/sandbox/logon.cmd +6 -0
  206. package/sandbox/memlog.ps1 +34 -0
  207. package/sandbox/pagefile.cs +31 -0
  208. package/sandbox/pagefile.ps1 +21 -0
  209. package/sandbox/ptyhost.cs +162 -7
  210. package/schemas/config.schema.json +318 -3
  211. package/schemas/sandbox-config.schema.json +129 -14
  212. package/dist/cli/menu-multi.js +0 -82
  213. package/dist/cli/menu.d.ts +0 -14
  214. package/dist/cli/menu.js +0 -290
  215. package/dist/cli/settings/edit.d.ts +0 -13
  216. package/dist/cli/settings/edit.js +0 -19
  217. package/dist/cli/settings/index.d.ts +0 -8
  218. package/dist/cli/settings/index.js +0 -131
  219. package/dist/cli/settings/prompts.d.ts +0 -7
  220. package/dist/cli/settings/prompts.js +0 -48
  221. package/dist/cli/settings/schema.d.ts +0 -33
  222. package/dist/cli/settings/schema.js +0 -29
@@ -0,0 +1,206 @@
1
+ // Installs the apps you chose (apps.install from the catalogue, apps.custom of your own) into C:\AgentData\tools\apps,
2
+ // once each: later sandboxes find them there. Programs get a desktop and Start menu shortcut; command-line tools go on
3
+ // PATH. Installers are never run: archives are unpacked, NSIS installers opened with 7-Zip and MSIs extracted
4
+ // (msiexec /a), so nothing is registered and nothing is lost when the sandbox closes. One app failing never stops the
5
+ // others.
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { appsToInstall, exeMatcher } from '../../shared/apps.js';
10
+ import { download, extractArchive, githubAsset, moveIntoPlace, stagingFolder } from '../../shared/download.js';
11
+ import { safeRemove } from '../../shared/safe-remove.js';
12
+ import { SYSTEM } from '../../shared/system.js';
13
+ import { dryRun, errMsg, execAsync, exists, log, mkdirp, powershell, warn } from '../env.js';
14
+ import { arch, tool } from '../toolchains/common.js';
15
+ import { RESOLVERS } from './resolvers.js';
16
+ export { appsNeedMesa } from '../../shared/apps.js';
17
+ const MARKER = '.agent-sandbox-app';
18
+ /** How many apps download and unpack at once (they are big: a few at a time keeps the disk and network busy). */
19
+ const PARALLEL = 3;
20
+ const sevenZip = () => tool('apps', '7zip', '7z.exe');
21
+ /** Where to download an entry from. */
22
+ const resolve = async (entry) => {
23
+ const s = entry.source;
24
+ if ('github' in s) {
25
+ const a = await githubAsset(s.github, new RegExp(s.asset), true);
26
+ return { url: a.url, fileName: a.name, sha256: a.sha256 };
27
+ }
28
+ if ('url' in s) {
29
+ const name = decodeURIComponent(new URL(s.url).pathname.split('/').pop() || entry.id);
30
+ return { url: s.url, fileName: /\.\w{2,5}$/.test(name) ? name : name + '.zip' };
31
+ }
32
+ if ('resolve' in s && RESOLVERS[s.resolve])
33
+ return RESOLVERS[s.resolve]();
34
+ throw new Error('no download source for ' + entry.name);
35
+ };
36
+ /** The program in an installed folder (breadth first, bounded: big apps have tens of thousands of files). */
37
+ export const findExe = (root, exe) => {
38
+ const match = exeMatcher(exe);
39
+ let queue = [root];
40
+ let seen = 0;
41
+ for (let depth = 0; depth < 7 && queue.length; depth++) {
42
+ const next = [];
43
+ for (const dir of queue) {
44
+ let entries;
45
+ try {
46
+ entries = fs.readdirSync(dir, { withFileTypes: true });
47
+ }
48
+ catch {
49
+ continue;
50
+ }
51
+ const hit = entries.find((e) => e.isFile() && match(e.name));
52
+ if (hit)
53
+ return path.join(dir, hit.name);
54
+ next.push(...entries.filter((e) => e.isDirectory() && !e.isSymbolicLink()).map((e) => path.join(dir, e.name)));
55
+ if ((seen += entries.length) > 50000)
56
+ return null;
57
+ }
58
+ queue = next;
59
+ }
60
+ return null;
61
+ };
62
+ /** Downloads and unpacks one entry into its folder (replaced in one step: never half there). */
63
+ const installApp = async (entry, dest) => {
64
+ const work = stagingFolder(dest);
65
+ const out = path.join(work, 'out');
66
+ try {
67
+ if ('npm' in entry.source) {
68
+ const npmCli = path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
69
+ log('Installing ' + entry.name + ' (npm) ...');
70
+ await execAsync(process.execPath, [npmCli, 'install', '--prefix', mkdirp(out), '--no-audit', '--no-fund', '--loglevel=error', entry.source.npm]);
71
+ }
72
+ else {
73
+ const d = await resolve(entry);
74
+ log('Downloading ' + entry.name + ' (' + d.fileName + ', ~' + entry.size + ' once) ...');
75
+ const file = path.join(mkdirp(work), d.fileName.replace(/[\\/:*?"<>|]/g, '_'));
76
+ await download(d.url, file, { sha256: d.sha256 ?? null });
77
+ await unpack(entry, file, out, d);
78
+ }
79
+ fs.writeFileSync(path.join(out, MARKER), JSON.stringify({ id: entry.id, at: new Date().toISOString() }));
80
+ moveIntoPlace(out, dest);
81
+ }
82
+ finally {
83
+ safeRemove(work, path.dirname(work));
84
+ }
85
+ };
86
+ const unpack = async (entry, file, out, d) => {
87
+ if (entry.id === '7zip') {
88
+ // 7zr.exe (the small standalone 7-Zip) opens the full 7-Zip's installer.
89
+ const sevenZr = path.join(path.dirname(file), '7zr.exe');
90
+ const zr = await githubAsset('ip7z/7zip', /^7zr\.exe$/);
91
+ await download(zr.url, sevenZr, zr.sha256);
92
+ await execAsync(sevenZr, ['x', '-y', '-o' + out, file], { stdio: 'ignore' });
93
+ return;
94
+ }
95
+ switch (entry.unpack ?? 'archive') {
96
+ case 'file': {
97
+ mkdirp(out);
98
+ fs.renameSync(file, path.join(out, /^\//.test(entry.exe) ? path.basename(file) : entry.exe));
99
+ return;
100
+ }
101
+ case 'nsis': {
102
+ if (!exists(sevenZip()))
103
+ throw new Error('7-Zip is needed to unpack ' + entry.name + ' and could not be installed');
104
+ const raw = path.join(path.dirname(out), 'raw');
105
+ await execAsync(sevenZip(), ['x', '-y', '-o' + raw, file], { stdio: 'ignore' });
106
+ // electron-builder installers carry the app as one more archive inside.
107
+ const inner = findExe(raw, '/^app-64\\.7z$/');
108
+ if (inner)
109
+ await execAsync(sevenZip(), ['x', '-y', '-o' + out, inner], { stdio: 'ignore' });
110
+ else
111
+ fs.renameSync(raw, out);
112
+ return;
113
+ }
114
+ case 'msi': {
115
+ // An administrative install only extracts the files: nothing is registered with Windows.
116
+ mkdirp(out);
117
+ await execAsync(SYSTEM.msiexec(), ['/a', file, '/qn', 'TARGETDIR=' + out], { stdio: 'ignore' });
118
+ return;
119
+ }
120
+ default:
121
+ await extractArchive(file, out, d.members);
122
+ }
123
+ };
124
+ /** Settings an app needs on every boot (the sandbox's registry is new each time), and files next to its program. */
125
+ const afterInstall = async (entry, exe) => {
126
+ const dir = path.dirname(exe);
127
+ if (entry.needs?.includes('mesa')) {
128
+ const mesa = tool('mesa', arch === 'arm64' ? 'arm64' : 'x64');
129
+ for (const dll of ['opengl32.dll', 'libgallium_wgl.dll', 'dxil.dll']) {
130
+ if (exists(path.join(mesa, dll)) && !exists(path.join(dir, dll)))
131
+ fs.copyFileSync(path.join(mesa, dll), path.join(dir, dll));
132
+ }
133
+ }
134
+ // Godot and Sublime keep their settings next to the program instead of in the profile, which is wiped on close.
135
+ if (entry.id === 'godot' && !exists(path.join(dir, '._sc_')))
136
+ fs.writeFileSync(path.join(dir, '._sc_'), '');
137
+ if (entry.id === 'sublime')
138
+ mkdirp(path.join(dir, 'Data'));
139
+ if (entry.id === 'sysinternals') {
140
+ for (const name of ['Process Explorer', 'Process Monitor', 'Autoruns', 'TCPView', 'PsExec', 'PsList', 'PsKill', 'Handle', 'ListDLLs', 'Sigcheck', 'Strings', 'VMMap', 'RAMMap', 'DebugView', 'Disk2vhd', 'Du', 'ZoomIt']) {
141
+ await execAsync(SYSTEM.reg(), ['add', 'HKCU\\Software\\Sysinternals\\' + name, '/v', 'EulaAccepted', '/t', 'REG_DWORD', '/d', '1', '/f'], { stdio: 'ignore' }).catch(() => undefined);
142
+ }
143
+ }
144
+ };
145
+ /** Desktop and Start menu shortcuts, all in one PowerShell call. */
146
+ const shortcuts = (apps) => {
147
+ if (!apps.length)
148
+ return;
149
+ const q = (s) => "'" + s.replace(/'/g, "''") + "'";
150
+ const desktop = path.join(os.homedir(), 'Desktop');
151
+ const start = path.join(process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming'), 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Sandbox apps');
152
+ const lines = ['$w = New-Object -ComObject WScript.Shell', 'New-Item -ItemType Directory -Force -Path ' + q(start) + ' | Out-Null'];
153
+ for (const app of apps) {
154
+ for (const folder of [desktop, start]) {
155
+ const name = app.name.replace(/[\\/:*?"<>|]/g, '');
156
+ lines.push('$s = $w.CreateShortcut(' + q(path.join(folder, name + '.lnk')) + '); $s.TargetPath = ' + q(app.exe) + '; $s.WorkingDirectory = ' + q(path.dirname(app.exe)) + '; $s.Save()');
157
+ }
158
+ }
159
+ powershell(lines.join('\n'));
160
+ };
161
+ export const setupApps = async (env, apps, o) => {
162
+ const { entries, skipped } = appsToInstall(apps, o.gitAccess);
163
+ for (const s of skipped)
164
+ env.summary.push('app skipped: ' + s);
165
+ if (dryRun) {
166
+ log('(dry run) would install apps: ' + entries.map((e) => e.name).join(', '));
167
+ return;
168
+ }
169
+ const gui = [];
170
+ const installed = [];
171
+ const one = async (entry) => {
172
+ try {
173
+ const dest = tool('apps', ...entry.id.split('/'));
174
+ if (!exists(path.join(dest, MARKER)) || o.refresh)
175
+ await installApp(entry, dest);
176
+ const exe = findExe(dest, entry.exe);
177
+ if (!exe)
178
+ throw new Error(entry.exe + ' not found in the download');
179
+ await afterInstall(entry, exe);
180
+ if (entry.cli || entry.id === 'sysinternals')
181
+ env.prepend.push(path.dirname(exe));
182
+ if (!entry.cli)
183
+ gui.push({ name: entry.name, exe });
184
+ installed.push(entry.name);
185
+ }
186
+ catch (e) {
187
+ warn(entry.name + ' could not be installed and is skipped; the other apps are not affected. Reason: ' + errMsg(e));
188
+ env.summary.push(entry.name + ': FAILED');
189
+ }
190
+ };
191
+ // 7-Zip first: the NSIS apps need it to unpack.
192
+ const [first, rest] = entries[0]?.id === '7zip' ? [[entries[0]], entries.slice(1)] : [[], entries];
193
+ for (const e of first)
194
+ await one(e);
195
+ const queue = [...rest];
196
+ await Promise.all(Array.from({ length: Math.min(PARALLEL, queue.length) }, async () => { for (let e = queue.shift(); e; e = queue.shift())
197
+ await one(e); }));
198
+ try {
199
+ shortcuts(gui);
200
+ }
201
+ catch (e) {
202
+ warn('App shortcuts could not be created: ' + errMsg(e));
203
+ }
204
+ if (installed.length)
205
+ env.summary.push('apps: ' + installed.join(', ') + ' (shortcuts on the desktop and in Start > Sandbox apps)');
206
+ };
@@ -0,0 +1,8 @@
1
+ export interface Download {
2
+ url: string;
3
+ fileName: string;
4
+ sha256?: string | null;
5
+ /** Only these paths of the archive (pgAdmin out of the PostgreSQL zip). */
6
+ members?: string[];
7
+ }
8
+ export declare const RESOLVERS: Record<string, () => Promise<Download>>;
@@ -0,0 +1,90 @@
1
+ // Where the latest version of an app is, for apps that are not a plain GitHub release: each looks it up on the
2
+ // vendor's own site (and its published checksum, where there is one). Named by AppSource.resolve in shared/apps.ts.
3
+ import { fetchJson, fetchText, githubAsset, latestGithubTag } from '../../shared/download.js';
4
+ /** The SHA-256 on the line of a SHA256SUMS-style file that names `file`. */
5
+ const sumFor = (sums, file) => sums.split(/\r?\n/).map((l) => /^([0-9a-f]{64})\s+\*?(.+)$/i.exec(l.trim())).find((m) => m && m[2].trim().endsWith(file))?.[1].toLowerCase() ?? null;
6
+ const firstHash = (text) => /\b([0-9a-f]{64})\b/i.exec(text)?.[1].toLowerCase() ?? null;
7
+ const stripV = (tag) => tag.replace(/^v/, '');
8
+ export const RESOLVERS = {
9
+ sublime: async () => {
10
+ // Sublime publishes no checksums; the download is HTTPS from sublimetext.com.
11
+ const { latest_version: build } = await fetchJson('https://www.sublimetext.com/updates/4/stable_update_check');
12
+ return { url: 'https://download.sublimetext.com/sublime_text_build_' + build + '_x64.zip', fileName: 'sublime_text_' + build + '.zip' };
13
+ },
14
+ pgadmin: async () => {
15
+ // pgAdmin is only an installer on pgadmin.org; EnterpriseDB's PostgreSQL binaries zip has a portable copy.
16
+ const versions = (await fetchJson('https://www.postgresql.org/versions.json')).filter((v) => v.supported);
17
+ const current = versions.find((v) => v.current) ?? versions[versions.length - 1];
18
+ const exact = current.major + '.' + current.latestMinor;
19
+ return { url: 'https://get.enterprisedb.com/postgresql/postgresql-' + exact + '-1-windows-x64-binaries.zip', fileName: 'postgresql-' + exact + '.zip', members: ['pgsql/pgAdmin 4'] };
20
+ },
21
+ everything: async () => {
22
+ const page = await fetchText('https://www.voidtools.com/downloads/');
23
+ const version = /Everything-([\d.]+)\.x64\.zip/.exec(page)?.[1];
24
+ if (!version)
25
+ throw new Error('could not find the Everything download on voidtools.com');
26
+ const file = 'Everything-' + version + '.x64.zip';
27
+ const sums = await fetchText('https://www.voidtools.com/Everything-' + version + '.sha256').catch(() => '');
28
+ return { url: 'https://www.voidtools.com/' + file, fileName: file, sha256: sumFor(sums, file) };
29
+ },
30
+ chrome: async () => {
31
+ const data = await fetchJson('https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json');
32
+ const hit = data.channels.Stable.downloads.chrome.find((d) => d.platform === 'win64');
33
+ if (!hit)
34
+ throw new Error('no win64 Chrome for Testing build listed');
35
+ return { url: hit.url, fileName: 'chrome-win64.zip' };
36
+ },
37
+ firefox: async () => {
38
+ const { LATEST_FIREFOX_VERSION: v } = await fetchJson('https://product-details.mozilla.org/1.0/firefox_versions.json');
39
+ const base = 'https://archive.mozilla.org/pub/firefox/releases/' + v + '/';
40
+ const sums = await fetchText(base + 'SHA256SUMS');
41
+ return { url: base + 'win64/en-US/Firefox%20Setup%20' + v + '.exe', fileName: 'firefox-setup-' + v + '.exe', sha256: sumFor(sums, 'win64/en-US/Firefox Setup ' + v + '.exe') };
42
+ },
43
+ blender: async () => {
44
+ const releases = await fetchText('https://download.blender.org/release/');
45
+ const series = [...releases.matchAll(/href="Blender(\d+)\.(\d+)\/"/g)].map((m) => [Number(m[1]), Number(m[2])]).sort((a, b) => a[0] - b[0] || a[1] - b[1]).pop();
46
+ if (!series)
47
+ throw new Error('could not list Blender releases');
48
+ const folder = 'https://download.blender.org/release/Blender' + series[0] + '.' + series[1] + '/';
49
+ const listing = await fetchText(folder);
50
+ const files = [...listing.matchAll(/href="(blender-([\d.]+)-windows-x64\.zip)"/g)].map((m) => ({ file: m[1], v: m[2].split('.').map(Number) }));
51
+ const latest = files.sort((a, b) => a.v[2] - b.v[2]).pop();
52
+ if (!latest)
53
+ throw new Error('no Windows build in ' + folder);
54
+ const sums = await fetchText(folder + 'blender-' + latest.v.join('.') + '.sha256').catch(() => '');
55
+ return { url: folder + latest.file, fileName: latest.file, sha256: sumFor(sums, latest.file) };
56
+ },
57
+ kubectl: async () => {
58
+ const v = (await fetchText('https://dl.k8s.io/release/stable.txt')).trim();
59
+ const url = 'https://dl.k8s.io/release/' + v + '/bin/windows/amd64/kubectl.exe';
60
+ return { url, fileName: 'kubectl.exe', sha256: firstHash(await fetchText(url + '.sha256')) };
61
+ },
62
+ helm: async () => {
63
+ const v = await latestGithubTag('helm/helm');
64
+ const url = 'https://get.helm.sh/helm-' + v + '-windows-amd64.zip';
65
+ return { url, fileName: 'helm-' + v + '.zip', sha256: firstHash(await fetchText(url + '.sha256sum')) };
66
+ },
67
+ terraform: async () => {
68
+ const { current_version: v } = await fetchJson('https://checkpoint-api.hashicorp.com/v1/check/terraform');
69
+ const base = 'https://releases.hashicorp.com/terraform/' + v + '/';
70
+ const file = 'terraform_' + v + '_windows_amd64.zip';
71
+ return { url: base + file, fileName: file, sha256: sumFor(await fetchText(base + 'terraform_' + v + '_SHA256SUMS'), file) };
72
+ },
73
+ mitmproxy: async () => {
74
+ // mitmproxy's GitHub releases have no files; its own download server has them (no published checksums).
75
+ const v = stripV(await latestGithubTag('mitmproxy/mitmproxy'));
76
+ return { url: 'https://downloads.mitmproxy.org/' + v + '/mitmproxy-' + v + '-windows-x86_64.zip', fileName: 'mitmproxy-' + v + '.zip' };
77
+ },
78
+ sqlite: async () => {
79
+ // The download page lists every file in a CSV comment: PRODUCT,<version>,<year>/<file>,<size>,<sha3>.
80
+ const page = await fetchText('https://sqlite.org/download.html');
81
+ const file = /PRODUCT,[^,]+,(\d{4}\/sqlite-tools-win-x64-\d+\.zip),/.exec(page)?.[1];
82
+ if (!file)
83
+ throw new Error('could not find the sqlite tools download on sqlite.org');
84
+ return { url: 'https://sqlite.org/' + file, fileName: file.split('/')[1] };
85
+ },
86
+ '7zip': async () => {
87
+ const asset = await githubAsset('ip7z/7zip', /^7z\d+-x64\.exe$/);
88
+ return { url: asset.url, fileName: asset.name, sha256: asset.sha256 };
89
+ },
90
+ };
@@ -0,0 +1,7 @@
1
+ /** Background programs of Windows itself: never what an agent ran. */
2
+ export declare const NOISE: Set<string>;
3
+ /** The events in wevtutil's XML output as audit lines, with the newest record id seen. */
4
+ export declare const parseProcessEvents: (xml: string) => {
5
+ lines: string[];
6
+ last: number;
7
+ };
@@ -0,0 +1,24 @@
1
+ // Reading Windows process-creation events (Security log, 4688, as wevtutil prints them) into audit lines. Pure, so
2
+ // the self-test can check it on the PC side.
3
+ import path from 'node:path';
4
+ /** Background programs of Windows itself: never what an agent ran. */
5
+ export const NOISE = new Set(['conhost.exe', 'svchost.exe', 'backgroundtaskhost.exe', 'runtimebroker.exe', 'wmiprvse.exe', 'dllhost.exe', 'searchprotocolhost.exe', 'searchfilterhost.exe', 'taskhostw.exe', 'sppsvc.exe', 'wermgr.exe', 'consent.exe', 'smartscreen.exe', 'mpcmdrun.exe', 'compattelrunner.exe', 'wevtutil.exe', 'audiodg.exe', 'ctfmon.exe', 'sihost.exe', 'werfault.exe', 'msmpeng.exe', 'nissrv.exe', 'usoclient.exe', 'musnotification.exe']);
6
+ const decode = (s) => s.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, '&');
7
+ const field = (event, name) => decode(new RegExp("<Data Name='" + name + "'>([^<]*)</Data>").exec(event)?.[1] ?? '');
8
+ /** The events in wevtutil's XML output as audit lines, with the newest record id seen. */
9
+ export const parseProcessEvents = (xml) => {
10
+ const lines = [];
11
+ let last = 0;
12
+ for (const event of xml.split('</Event>')) {
13
+ const id = Number(/<EventRecordID>(\d+)<\/EventRecordID>/.exec(event)?.[1] ?? 0);
14
+ if (!id)
15
+ continue;
16
+ last = Math.max(last, id);
17
+ const program = field(event, 'NewProcessName');
18
+ if (!program || NOISE.has(path.win32.basename(program).toLowerCase()))
19
+ continue;
20
+ const t = /SystemTime='([^']+)'/.exec(event)?.[1] ?? new Date().toISOString();
21
+ lines.push(JSON.stringify({ t, k: 'proc', p: program.slice(0, 1024), c: field(event, 'CommandLine').slice(0, 2000), pp: field(event, 'ParentProcessName').slice(0, 1024) }));
22
+ }
23
+ return { lines, last };
24
+ };
@@ -0,0 +1,4 @@
1
+ /** Turns on process-creation auditing with command lines (the registry is new every boot). */
2
+ export declare const enableProcessAudit: () => void;
3
+ /** Reads new process events every 15 s for the daemon's life (from the moment it started: older ones are boot noise). */
4
+ export declare const startProcessCollector: () => void;
@@ -0,0 +1,47 @@
1
+ // audit.processes, sandbox side: Windows records every program started (Security log, event 4688, with its command
2
+ // line); the daemon reads the new events every 15 s and leaves them in run\audit\ as small .jsonl files, which the
3
+ // host takes into its own log (and deletes). Windows' own background programs are left out. The host treats all of it
4
+ // as untrusted (it is checked line by line there).
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { spawnSync } from 'node:child_process';
8
+ import { SYSTEM } from '../shared/system.js';
9
+ import { parseProcessEvents } from './audit-parse.js';
10
+ import { dryRun, errMsg, isWin, log, mkdirp, run, session, warn } from './env.js';
11
+ /** The "Process Creation" audit subcategory (a GUID, so it works in every Windows language). */
12
+ const PROCESS_CREATION = '{0CCE922B-69AE-11D9-BED3-505054503030}';
13
+ const MAX_PER_PASS = 2000;
14
+ const auditOn = () => Boolean(session.auditProcesses) && isWin && !dryRun;
15
+ /** Turns on process-creation auditing with command lines (the registry is new every boot). */
16
+ export const enableProcessAudit = () => {
17
+ if (!auditOn())
18
+ return;
19
+ spawnSync(path.join(process.env.WINDIR ?? 'C:\\Windows', 'System32', 'auditpol.exe'), ['/set', '/subcategory:' + PROCESS_CREATION, '/success:enable'], { windowsHide: true, stdio: 'ignore' });
20
+ spawnSync(SYSTEM.reg(), ['add', 'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System\\Audit', '/v', 'ProcessCreationIncludeCmdLine_Enabled', '/t', 'REG_DWORD', '/d', '1', '/f'], { windowsHide: true, stdio: 'ignore' });
21
+ log('Audit: programs started in the sandbox are recorded (audit.processes).');
22
+ };
23
+ const wevtutil = () => path.join(process.env.WINDIR ?? 'C:\\Windows', 'System32', 'wevtutil.exe');
24
+ const newestRecord = () => {
25
+ const r = spawnSync(wevtutil(), ['qe', 'Security', '/q:*[System[(EventID=4688)]]', '/f:xml', '/c:1', '/rd:true'], { encoding: 'utf8', windowsHide: true });
26
+ return parseProcessEvents(r.stdout ?? '').last;
27
+ };
28
+ /** Reads new process events every 15 s for the daemon's life (from the moment it started: older ones are boot noise). */
29
+ export const startProcessCollector = () => {
30
+ if (!auditOn())
31
+ return;
32
+ let last = newestRecord();
33
+ const pass = () => {
34
+ try {
35
+ const r = spawnSync(wevtutil(), ['qe', 'Security', '/q:*[System[(EventID=4688) and (EventRecordID>' + last + ')]]', '/f:xml', '/c:' + MAX_PER_PASS], { encoding: 'utf8', windowsHide: true, maxBuffer: 64 * 1024 * 1024 });
36
+ const { lines, last: newest } = parseProcessEvents(r.stdout ?? '');
37
+ if (newest)
38
+ last = newest;
39
+ if (lines.length)
40
+ fs.writeFileSync(path.join(mkdirp(run.audit), Date.now() + '-' + process.pid + '.jsonl'), lines.join('\n') + '\n');
41
+ }
42
+ catch (e) {
43
+ warn('audit: ' + errMsg(e));
44
+ }
45
+ };
46
+ setInterval(pass, 15000).unref?.();
47
+ };
@@ -0,0 +1 @@
1
+ export declare const startRelays: () => void;
@@ -0,0 +1,18 @@
1
+ // network.forwardPorts, sandbox side: one relay per forward, listening on its relay port (reachable only from the
2
+ // sandbox's own subnet, i.e. the PC: see the firewall rule in setup/network.ts) and passing each connection on to
3
+ // localhost:<sandbox port>. So a dev server that only listens on localhost is reachable from the PC too.
4
+ import net from 'node:net';
5
+ import { relayPort } from '../../shared/netpolicy.js';
6
+ import { errMsg, log, session, warn } from '../env.js';
7
+ export const startRelays = () => {
8
+ (session.network.forwardPorts ?? []).forEach((f, i) => {
9
+ const server = net.createServer((client) => {
10
+ const target = net.connect(f.sandbox, '127.0.0.1');
11
+ client.pipe(target).pipe(client);
12
+ target.on('error', () => client.destroy());
13
+ client.on('error', () => target.destroy());
14
+ });
15
+ server.on('error', (e) => warn('Port forward ' + f.pc + ' -> ' + f.sandbox + ' could not start: ' + errMsg(e)));
16
+ server.listen(relayPort(i), '0.0.0.0', () => log('Port forward: the PC\'s localhost:' + f.pc + ' reaches localhost:' + f.sandbox + ' here.'));
17
+ });
18
+ };
@@ -6,6 +6,8 @@ import fs from 'node:fs';
6
6
  import { readSession, scanRequests } from '../shared/rundir.js';
7
7
  import { saveClaudeMem } from './setup/claude.js';
8
8
  import { execCommand } from './daemon/exec.js';
9
+ import { startRelays } from './daemon/relays.js';
10
+ import { startProcessCollector } from './audit.js';
9
11
  import { prepareProject, restartSessions, startSession } from './daemon/session.js';
10
12
  import { live, respond, safely } from './daemon/state.js';
11
13
  import { dataDir, errMsg, log, run, warn } from './env.js';
@@ -50,4 +52,6 @@ const loop = () => {
50
52
  }
51
53
  };
52
54
  log('Session daemon started (pid ' + process.pid + ').');
55
+ safely('port forwards', startRelays);
56
+ safely('audit', startProcessCollector);
53
57
  loop();
@@ -1,5 +1,6 @@
1
1
  import { type SpawnOptions, type SpawnSyncOptions } from 'node:child_process';
2
2
  import type { GuestSession, SandboxStatus } from '../core/types.js';
3
+ import type { BootProgressT } from '../shared/schema.js';
3
4
  export declare const isWin: boolean;
4
5
  export declare const dryRun: boolean;
5
6
  /** Real provisioning, but nothing machine-wide (environment variables): for end-to-end tests outside a sandbox. */
@@ -18,6 +19,7 @@ export declare const run: {
18
19
  responses: string;
19
20
  sessions: string;
20
21
  projects: string;
22
+ audit: string;
21
23
  request: (id: string) => string;
22
24
  response: (id: string) => string;
23
25
  session: (id: string) => string;
@@ -33,8 +35,11 @@ export declare const errMsg: (e: unknown) => string;
33
35
  export declare const exists: (p: string) => boolean;
34
36
  export declare const mkdirp: (p: string) => string;
35
37
  export declare const logPath: string;
36
- /** Best-effort: progress reporting must never be what breaks the boot. */
37
- export declare const setSandboxStatus: (phase: SandboxStatus["phase"], message: string) => void;
38
+ /**
39
+ * Best-effort: progress reporting must never be what breaks the boot. `update` changes the structured progress
40
+ * (a new step, what is being set up); it stays as it was otherwise.
41
+ */
42
+ export declare const setSandboxStatus: (phase: SandboxStatus["phase"], message: string, update?: Partial<BootProgressT>) => void;
38
43
  /** Runs a program, streaming output, and throws with a readable message when it fails. */
39
44
  export declare const exec: (file: string, args: string[], options?: SpawnSyncOptions) => void;
40
45
  /** exec without blocking: other toolchains keep downloading while this one runs an installer. */
package/dist/guest/env.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Shared state for the guest scripts (main, daemon, view): fixed paths, the session description, logging, status.
2
2
  import fs from 'node:fs';
3
+ import os from 'node:os';
3
4
  import path from 'node:path';
4
5
  import { spawnSync } from 'node:child_process';
5
6
  import { moveIntoPlace, setChecksumRequired, spawnAsync, stagingFolder } from '../shared/download.js';
@@ -35,17 +36,30 @@ export const errMsg = (e) => (e instanceof Error ? e.message : String(e));
35
36
  export const exists = (p) => fs.existsSync(p);
36
37
  export const mkdirp = (p) => { fs.mkdirSync(p, { recursive: true }); return p; };
37
38
  export const logPath = logFile;
38
- /** Best-effort: progress reporting must never be what breaks the boot. */
39
- export const setSandboxStatus = (phase, message) => {
39
+ /** The boot's structured progress (the host shows it as one line); kept between status writes. */
40
+ let progress;
41
+ /**
42
+ * Best-effort: progress reporting must never be what breaks the boot. `update` changes the structured progress
43
+ * (a new step, what is being set up); it stays as it was otherwise.
44
+ */
45
+ export const setSandboxStatus = (phase, message, update) => {
40
46
  try {
47
+ if (update)
48
+ progress = fitProgress({ step: 1, steps: 1, title: '', active: [], done: 0, total: 0, ...progress, ...update });
41
49
  writeJsonAtomicChecked(phase, message);
42
50
  }
43
51
  catch (e) {
44
52
  warn('Could not update sandbox status: ' + errMsg(e));
45
53
  }
46
54
  };
55
+ /** Within the limits the host accepts (it drops a progress that is not). */
56
+ const fitProgress = (p) => ({
57
+ ...p, title: p.title.slice(0, 120), done: Math.min(p.done, 1000), total: Math.min(p.total, 1000),
58
+ active: p.active.slice(0, 8).map((a) => ({ name: a.name.slice(0, 64), ...(a.percent === undefined ? {} : { percent: Math.max(0, Math.min(100, Math.round(a.percent))) }) })),
59
+ });
60
+ const ownAddresses = () => Object.values(os.networkInterfaces()).flat().filter((i) => Boolean(i) && i.family === 'IPv4' && !i.internal).map((i) => i.address).slice(0, 8);
47
61
  const writeJsonAtomicChecked = (phase, message) => {
48
- writeJsonAtomic(run.sandbox, { launchId: session.launchId, phase, message, updatedAt: now(), workspaceHost: session.workspaceHost, guestWorkspace: workspace, headless: session.headless });
62
+ writeJsonAtomic(run.sandbox, { launchId: session.launchId, phase, message, updatedAt: now(), workspaceHost: session.workspaceHost, guestWorkspace: workspace, headless: session.headless, addresses: ownAddresses(), ...(progress ? { progress } : {}) });
49
63
  };
50
64
  /** Runs a program, streaming output, and throws with a readable message when it fails. */
51
65
  export const exec = (file, args, options = {}) => {
@@ -1,3 +1,3 @@
1
- export type Helper = 'ptyhost' | 'gitshim' | 'vswhere' | 'openssl';
1
+ export type Helper = 'ptyhost' | 'gitshim' | 'vswhere' | 'openssl' | 'cargoshim';
2
2
  /** %TEMP%\agent-sandbox\<name>-<hash>.exe, compiled from this launch's sandbox\<name>.cs the first time it is needed. */
3
3
  export declare const ensureCompiled: (name: Helper) => string;
@@ -9,18 +9,25 @@ import { provisionHarness, resolveLaunch } from './setup/agents.js';
9
9
  import { restoreClaudeMem, syncClaudeProfile } from './setup/claude.js';
10
10
  import { agentEnv, persistEnvironment } from './setup/environment.js';
11
11
  import { runHooks } from './setup/hooks.js';
12
- import { applyNetworkPolicy } from './setup/network.js';
12
+ import { applyAllowOnly, applyNetworkPolicy, proxyEnv } from './setup/network.js';
13
13
  import { writeManagedSettings } from './setup/policy.js';
14
14
  import { applyPersonalization, setupGitConfig } from './setup/user.js';
15
15
  import { ensurePtyHost } from './ptyhost.js';
16
16
  import { pruneToolLeftovers } from './toolchains/leftovers.js';
17
+ import { wanted } from './toolchains/common.js';
18
+ import { configureVscode } from './toolchains/vscode.js';
19
+ import { enableProcessAudit } from './audit.js';
17
20
  import { now, readSession, writeJsonAtomic } from '../shared/rundir.js';
18
21
  import { viewSession } from './view.js';
19
22
  import { saveBootEnv } from './boot-env.js';
20
23
  import { primaryProject, saveProjectStatus } from './project-status.js';
21
24
  import { dataDir, dryRun, errMsg, isWin, localDir, log, logPath, logsDir, mkdirp, run, session, setSandboxStatus, toolsDir, warn, workspace } from './env.js';
22
25
  const STEPS = 8;
23
- const step = (n, title) => { log('\n\x1b[36m[' + n + '/' + STEPS + '] ' + title + '\x1b[0m'); setSandboxStatus(n === 1 ? 'booting' : 'provisioning', '[' + n + '/' + STEPS + '] ' + title); };
26
+ const step = (n, title) => {
27
+ log('\n\x1b[36m[' + n + '/' + STEPS + '] ' + title + '\x1b[0m');
28
+ // The host shows the short title ("Developer tools") with what is being set up in it.
29
+ setSandboxStatus(n === 1 ? 'booting' : 'provisioning', '[' + n + '/' + STEPS + '] ' + title, { step: n, steps: STEPS, title: title.replace(/ \(.*\)$/, ''), active: [], done: 0, total: 0 });
30
+ };
24
31
  const soft = async (what, work) => { try {
25
32
  await work();
26
33
  }
@@ -62,6 +69,7 @@ const main = async () => {
62
69
  await soft('Building the session host', () => { ptyHost = ensurePtyHost(); });
63
70
  let lan = session.networking ? 'open' : 'offline';
64
71
  await soft('Private-network blocking', async () => { lan = await applyNetworkPolicy(ptyHost); });
72
+ await soft('Audit', enableProcessAudit);
65
73
  step(2, 'Pre-provision hooks and desktop');
66
74
  runHooks('pre-provision', process.env);
67
75
  await soft('Personalisation', applyPersonalization);
@@ -69,10 +77,19 @@ const main = async () => {
69
77
  step(3, 'Developer tools (the first start downloads them once; later starts are quick)');
70
78
  const basePath = process.env.PATH ?? '';
71
79
  const tools = await setupToolchains(session.toolchains, session.versions, {
72
- gitEnabled: session.provisioning.git.enabled, gitAccess, hostGit: session.hostTools.git, refresh: session.provisioning.refresh, databases: session.databases ?? [],
80
+ gitEnabled: session.provisioning.git.enabled, gitAccess, hostGit: session.hostTools.git, refresh: session.provisioning.refresh, databases: session.databases ?? [], apps: session.apps,
73
81
  });
74
82
  if (!dryRun)
75
83
  tools.prepend.push(writeDbCommand());
84
+ // network.allowOnly / network.proxy: everything started from now on uses the proxy.
85
+ Object.assign(tools.vars, proxyEnv());
86
+ await soft('Network: allow only some hosts', async () => { if (await applyAllowOnly())
87
+ tools.summary.push('network: only network.allowOnly hosts (refused: logs\\netproxy.log)'); });
88
+ const vscode = session.toolchains.vscode;
89
+ if (vscode.enabled && vscode.setup !== false) {
90
+ const languages = ['python', 'java', 'go', 'rust', 'cpp', 'msvc', 'dotnet', 'ruby', 'php', 'deno', 'pwsh'].filter((t) => wanted(session.toolchains[t]?.enabled ?? false, t));
91
+ await soft('VS Code setup', () => configureVscode(tools, { toolchains: ['node', ...languages], databases: (session.databases ?? []).map((d) => d.database), config: vscode, workspace }));
92
+ }
76
93
  const env = agentEnv(tools);
77
94
  persistEnvironment(env, Object.keys(tools.vars));
78
95
  saveBootEnv({ basePath, toolVars: Object.keys(tools.vars), steps: tools.steps, summary: tools.summary, lan });
@@ -0,0 +1 @@
1
+ export {};