windows-development-sandbox 1.11.7 → 1.13.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 (246) hide show
  1. package/README.md +13 -6
  2. package/config.example.json +15 -4
  3. package/dist/app/features/access/access.def.d.ts +190 -0
  4. package/dist/app/features/access/access.def.js +273 -0
  5. package/dist/app/features/access/access.run.d.ts +79 -0
  6. package/dist/app/features/access/access.run.js +129 -0
  7. package/dist/app/features/access/index.d.ts +2 -0
  8. package/dist/app/features/access/index.js +3 -0
  9. package/dist/app/features/agents/agent-entry.js +5 -0
  10. package/dist/app/features/agents/agents.def.js +1 -0
  11. package/dist/app/features/apps/apps-rows.d.ts +4 -1
  12. package/dist/app/features/apps/apps-rows.js +7 -2
  13. package/dist/app/features/apps/apps.def.d.ts +19 -0
  14. package/dist/app/features/apps/apps.def.js +50 -12
  15. package/dist/app/features/apps/apps.run.d.ts +2 -0
  16. package/dist/app/features/apps/apps.run.js +1 -1
  17. package/dist/app/features/audit/audit.def.d.ts +350 -0
  18. package/dist/app/features/audit/audit.def.js +457 -0
  19. package/dist/app/features/audit/audit.run.d.ts +110 -0
  20. package/dist/app/features/audit/audit.run.js +133 -0
  21. package/dist/app/features/audit/index.d.ts +2 -0
  22. package/dist/app/features/audit/index.js +3 -0
  23. package/dist/app/features/defs.d.ts +349 -4
  24. package/dist/app/features/defs.js +16 -1
  25. package/dist/app/features/files/files.def.js +1 -0
  26. package/dist/app/features/health/health.def.js +1 -0
  27. package/dist/app/features/sandbox/sandbox.def.js +3 -0
  28. package/dist/app/features/sessions/sessions.def.d.ts +36 -0
  29. package/dist/app/features/sessions/sessions.def.js +77 -9
  30. package/dist/app/features/sessions/sessions.run.d.ts +25 -2
  31. package/dist/app/features/sessions/sessions.run.js +71 -3
  32. package/dist/app/features/settings/settings.def.d.ts +8 -0
  33. package/dist/app/features/settings/settings.def.js +17 -0
  34. package/dist/app/features/settings/settings.run.d.ts +3 -0
  35. package/dist/app/features/settings/settings.run.js +16 -2
  36. package/dist/app/features/web/web.def.d.ts +9 -0
  37. package/dist/app/features/web/web.def.js +26 -1
  38. package/dist/app/features/web/web.run.d.ts +3 -0
  39. package/dist/app/features/web/web.run.js +11 -3
  40. package/dist/app/kernel/access.d.ts +33 -0
  41. package/dist/app/kernel/access.js +44 -0
  42. package/dist/app/kernel/define.d.ts +26 -2
  43. package/dist/app/kernel/index.d.ts +1 -0
  44. package/dist/app/kernel/index.js +1 -0
  45. package/dist/app/kernel/rpc.d.ts +5 -2
  46. package/dist/app/registry.d.ts +4 -1
  47. package/dist/app/registry.js +26 -4
  48. package/dist/cli/args.d.ts +0 -3
  49. package/dist/cli/args.js +3 -4
  50. package/dist/cli/commands/index.d.ts +1 -1
  51. package/dist/cli/commands/index.js +1 -1
  52. package/dist/cli/commands/launch/prepare.js +1 -1
  53. package/dist/cli/commands/maintenance/index.d.ts +0 -1
  54. package/dist/cli/commands/maintenance/index.js +0 -1
  55. package/dist/cli/commands/registry.js +3 -2
  56. package/dist/cli/commands/remote.d.ts +2 -0
  57. package/dist/cli/commands/remote.js +49 -0
  58. package/dist/cli/commands/sessions/reopen.js +3 -0
  59. package/dist/cli/commands/web.js +66 -0
  60. package/dist/cli/manifest.d.ts +63 -45
  61. package/dist/cli/manifest.js +33 -18
  62. package/dist/cli/menu/io-menu.d.ts +5 -3
  63. package/dist/cli/menu/io-menu.js +12 -5
  64. package/dist/cli/menu/pages/apps-page.js +2 -15
  65. package/dist/cli/menu/pages/maintenance-page.d.ts +4 -0
  66. package/dist/cli/menu/pages/maintenance-page.js +51 -8
  67. package/dist/cli/menu/remote-menu.d.ts +19 -0
  68. package/dist/cli/menu/remote-menu.js +108 -0
  69. package/dist/cli/menu/views.js +4 -0
  70. package/dist/cli/operations/handler.js +15 -1
  71. package/dist/cli/services/remote-client.d.ts +16 -0
  72. package/dist/cli/services/remote-client.js +141 -0
  73. package/dist/cli/services/sessions-service.d.ts +5 -0
  74. package/dist/cli/services/sessions-service.js +17 -1
  75. package/dist/core/config/defaults.js +12 -2
  76. package/dist/core/local-config/file.js +16 -2
  77. package/dist/core/local-config/overrides.js +7 -0
  78. package/dist/core/run/versions.js +9 -8
  79. package/dist/core/schema/apps.d.ts +9 -0
  80. package/dist/core/schema/apps.js +9 -0
  81. package/dist/core/schema/config.d.ts +23 -3
  82. package/dist/core/schema/config.js +42 -5
  83. package/dist/core/schema/sandbox.d.ts +1 -0
  84. package/dist/core/schema/sandbox.js +11 -3
  85. package/dist/core/schema/web.d.ts +51 -0
  86. package/dist/core/schema/web.js +46 -1
  87. package/dist/core/util/fs.d.ts +5 -0
  88. package/dist/core/util/fs.js +23 -0
  89. package/dist/core/util/index.d.ts +1 -1
  90. package/dist/core/util/index.js +1 -1
  91. package/dist/guest/apps/app-installer.js +50 -4
  92. package/dist/guest/apps/index.d.ts +1 -1
  93. package/dist/guest/apps/index.js +1 -1
  94. package/dist/guest/apps/sources.js +1 -1
  95. package/dist/guest/audit/parse.d.ts +32 -2
  96. package/dist/guest/audit/parse.js +43 -5
  97. package/dist/guest/audit/process-audit.d.ts +3 -1
  98. package/dist/guest/audit/process-audit.js +9 -6
  99. package/dist/guest/boot/boot.js +12 -3
  100. package/dist/guest/boot/guide.d.ts +1 -1
  101. package/dist/guest/boot/guide.js +27 -3
  102. package/dist/guest/daemon/daemon.js +2 -0
  103. package/dist/guest/daemon/idle-shutdown.js +2 -1
  104. package/dist/guest/daemon/sessions.d.ts +23 -2
  105. package/dist/guest/daemon/sessions.js +151 -20
  106. package/dist/guest/entries/daemon.js +2 -1
  107. package/dist/guest/git/decision.js +7 -0
  108. package/dist/guest/sessions/cmdline.d.ts +14 -0
  109. package/dist/guest/sessions/cmdline.js +10 -0
  110. package/dist/guest/setup/addon-mcp.d.ts +2 -0
  111. package/dist/guest/setup/addon-mcp.js +49 -0
  112. package/dist/guest/setup/agent-addons.d.ts +9 -0
  113. package/dist/guest/setup/agent-addons.js +22 -0
  114. package/dist/guest/setup/headroom.d.ts +7 -0
  115. package/dist/guest/setup/headroom.js +60 -0
  116. package/dist/guest/setup/managed-policy.d.ts +2 -1
  117. package/dist/guest/setup/managed-policy.js +15 -6
  118. package/dist/guest/toolchains/plan.js +8 -3
  119. package/dist/guest/toolchains/setup.js +3 -0
  120. package/dist/host/audit/attribute.d.ts +46 -0
  121. package/dist/host/audit/attribute.js +76 -0
  122. package/dist/host/audit/collect.d.ts +29 -3
  123. package/dist/host/audit/collect.js +256 -61
  124. package/dist/host/audit/git-commits.d.ts +29 -0
  125. package/dist/host/audit/git-commits.js +157 -0
  126. package/dist/host/audit/index.d.ts +5 -0
  127. package/dist/host/audit/index.js +6 -0
  128. package/dist/host/audit/log.d.ts +45 -7
  129. package/dist/host/audit/log.js +148 -45
  130. package/dist/host/audit/redact.d.ts +2 -0
  131. package/dist/host/audit/redact.js +67 -0
  132. package/dist/host/channel/channel.d.ts +5 -1
  133. package/dist/host/channel/channel.js +12 -0
  134. package/dist/host/guard/agent-files.js +7 -1
  135. package/dist/host/guard/commands.js +13 -1
  136. package/dist/host/guard/git-config.js +27 -7
  137. package/dist/host/guard/git-dirs.js +78 -3
  138. package/dist/host/guard/mark-of-the-web.js +3 -1
  139. package/dist/host/guard/nested-repos.js +7 -2
  140. package/dist/host/guard/store.js +1 -0
  141. package/dist/host/guard/types.d.ts +2 -0
  142. package/dist/host/launch/mappings.js +11 -1
  143. package/dist/host/launch/project-scan.js +2 -21
  144. package/dist/host/launch/stage.js +4 -0
  145. package/dist/host/life/session-ledger.js +2 -1
  146. package/dist/host/remote/access-store.d.ts +156 -0
  147. package/dist/host/remote/access-store.js +308 -0
  148. package/dist/host/remote/cert.d.ts +20 -0
  149. package/dist/host/remote/cert.js +103 -0
  150. package/dist/host/remote/door-client.d.ts +13 -0
  151. package/dist/host/remote/door-client.js +54 -0
  152. package/dist/host/remote/firewall.d.ts +44 -0
  153. package/dist/host/remote/firewall.js +118 -0
  154. package/dist/host/remote/gate.d.ts +125 -0
  155. package/dist/host/remote/gate.js +391 -0
  156. package/dist/host/remote/index.d.ts +8 -0
  157. package/dist/host/remote/index.js +10 -0
  158. package/dist/host/remote/lan.d.ts +24 -0
  159. package/dist/host/remote/lan.js +88 -0
  160. package/dist/host/remote/tls-store.d.ts +22 -0
  161. package/dist/host/remote/tls-store.js +63 -0
  162. package/dist/host/remote/windows-helper.d.ts +67 -0
  163. package/dist/host/remote/windows-helper.js +129 -0
  164. package/dist/host/web/bridge.d.ts +5 -1
  165. package/dist/host/web/bridge.js +6 -2
  166. package/dist/host/web/browser.js +1 -1
  167. package/dist/host/web/control.d.ts +4 -1
  168. package/dist/host/web/control.js +28 -4
  169. package/dist/host/web/rpc.d.ts +16 -2
  170. package/dist/host/web/rpc.js +37 -4
  171. package/dist/host/web/runner.d.ts +4 -0
  172. package/dist/host/web/runner.js +12 -0
  173. package/dist/host/web/server.d.ts +18 -2
  174. package/dist/host/web/server.js +97 -24
  175. package/dist/host/web/state.d.ts +5 -0
  176. package/dist/host/web/state.js +8 -0
  177. package/dist/runtime/agent.js +184 -18
  178. package/dist/runtime/daemon.js +772 -338
  179. package/dist/runtime/db.js +184 -18
  180. package/dist/runtime/gitguard.js +6 -0
  181. package/dist/runtime/main.js +966 -553
  182. package/dist/runtime/netproxy.js +184 -18
  183. package/dist/runtime/view.js +186 -18
  184. package/dist/selftest/app.test.js +66 -7
  185. package/dist/selftest/audit.test.js +354 -47
  186. package/dist/selftest/channel.test.js +16 -0
  187. package/dist/selftest/cmdline.test.js +16 -0
  188. package/dist/selftest/git-access.test.js +8 -0
  189. package/dist/selftest/hcs.test.js +35 -0
  190. package/dist/selftest/hostile-input.test.js +12 -1
  191. package/dist/selftest/index.js +2 -0
  192. package/dist/selftest/live-daemon.test.js +25 -0
  193. package/dist/selftest/local-config.test.js +30 -0
  194. package/dist/selftest/menu-screens.js +1 -1
  195. package/dist/selftest/menu.test.js +3 -3
  196. package/dist/selftest/remote.test.d.ts +2 -0
  197. package/dist/selftest/remote.test.js +536 -0
  198. package/dist/selftest/repo-guard.test.js +56 -2
  199. package/dist/selftest/resume.test.js +2 -1
  200. package/dist/selftest/sharing.test.js +9 -2
  201. package/dist/selftest/support/engine-golden.js +9 -0
  202. package/dist/selftest/support/fixtures.d.ts +14 -0
  203. package/dist/selftest/support/web-preview.js +157 -0
  204. package/dist/selftest/toolchains.test.js +147 -2
  205. package/dist/selftest/web.test.js +2 -0
  206. package/dist/shared/apps/agent-addons.d.ts +64 -0
  207. package/dist/shared/apps/agent-addons.js +95 -0
  208. package/dist/shared/apps/catalogue.d.ts +33 -2
  209. package/dist/shared/apps/catalogue.js +102 -11
  210. package/dist/shared/apps/index.d.ts +3 -2
  211. package/dist/shared/apps/index.js +3 -2
  212. package/dist/shared/apps/selection.d.ts +2 -0
  213. package/dist/shared/apps/selection.js +7 -0
  214. package/dist/shared/channel/schema.d.ts +23 -0
  215. package/dist/shared/channel/schema.js +8 -0
  216. package/dist/shared/pty/protocol.d.ts +2 -0
  217. package/dist/shared/pty/protocol.js +2 -0
  218. package/dist/sync/watcher.js +9 -1
  219. package/dist/web/index.html +2 -2
  220. package/dist/web/{main-RQFN4WBY.css → main-UAQPI5XD.css} +1 -1
  221. package/dist/web/main-Z2WYAE2Z.js +221 -0
  222. package/native/csharp/HcsHost/AssemblyInfo.cs +5 -3
  223. package/native/csharp/HcsHost/Hcs.cs +9 -0
  224. package/native/csharp/HcsHost/Program.cs +26 -1
  225. package/native/csharp/HcsHost/Share.cs +173 -0
  226. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  227. package/native/csharp/HostTools/Program.cs +17 -1
  228. package/native/csharp/HostTools/WebHelper.cs +499 -0
  229. package/native/scripts/get-network-profile.ps1 +19 -0
  230. package/native/scripts/set-folder-access.ps1 +11 -0
  231. package/native/scripts/set-web-firewall.ps1 +103 -0
  232. package/package.json +1 -1
  233. package/sandbox/addons/ccusage/package-lock.json +111 -0
  234. package/sandbox/addons/ccusage/package.json +7 -0
  235. package/sandbox/addons/context7/package-lock.json +1313 -0
  236. package/sandbox/addons/context7/package.json +7 -0
  237. package/sandbox/addons/headroom/requirements.in +1 -0
  238. package/sandbox/addons/headroom/requirements.txt +2922 -0
  239. package/sandbox/addons/playwright-mcp/package-lock.json +56 -0
  240. package/sandbox/addons/playwright-mcp/package.json +7 -0
  241. package/sandbox/csharp/PtyHost.cs +20 -0
  242. package/schemas/config.schema.json +78 -4
  243. package/schemas/sandbox-config.schema.json +2 -20
  244. package/dist/cli/commands/maintenance/audit.d.ts +0 -3
  245. package/dist/cli/commands/maintenance/audit.js +0 -58
  246. package/dist/web/main-K2JMCAZI.js +0 -219
@@ -17,8 +17,11 @@ const CLAUDE_RISKY_KEYS = [
17
17
  'enableAllProjectMcpServers',
18
18
  'enabledMcpjsonServers',
19
19
  'disableAllHooks',
20
+ // Plugins bring their own hooks and MCP servers.
21
+ 'enabledPlugins',
22
+ 'extraKnownMarketplaces',
20
23
  ];
21
- const VSCODE_RISKY_KEY = /path|executable|command|interpreter|shell|profile|args|env|exec|program|server|launch|runtime|binary/i;
24
+ const VSCODE_RISKY_KEY = /path|executable|command|interpreter|shell|profile|args|env|exec|program|server|launch|runtime|binary|alternatetools|home|goroot|tsdk|sdk/i;
22
25
  const MAX_AGENT_FILE_BYTES = 1024 * 1024;
23
26
  function claudeRisk(d) {
24
27
  const s = (_.isPlainObject(d) ? d : {});
@@ -26,6 +29,9 @@ function claudeRisk(d) {
26
29
  return {
27
30
  ..._.pick(s, CLAUDE_RISKY_KEYS),
28
31
  permissions: _.pick(permissions, ['defaultMode', 'additionalDirectories']),
32
+ // A whole-tool rule ("Bash", "Bash(*)") lets Claude Code on this PC run any command without asking; narrow rules
33
+ // (Bash(npm test), what "always allow" writes) are ordinary edits.
34
+ broadAllow: (Array.isArray(permissions.allow) ? permissions.allow : []).filter((r) => typeof r !== 'string' || !r.includes('(') || /\(\s*:?\*\s*\)\s*$/.test(r)),
29
35
  };
30
36
  }
31
37
  /**
@@ -1,5 +1,15 @@
1
1
  import { filesIn } from './file-hash.js';
2
2
  import { quarantine } from './quarantine.js';
3
+ /** What cmd runs from the current folder: a fixed list plus this PC's PATHEXT (the Python installer adds .PY, .PYW). */
4
+ const EXTENSIONS = [
5
+ ...new Set([
6
+ ...['exe', 'com', 'bat', 'cmd', 'vbs', 'vbe', 'js', 'jse', 'wsf', 'wsh', 'msc', 'ps1', 'cpl'],
7
+ ...(process.env.PATHEXT ?? '')
8
+ .split(';')
9
+ .map((e) => e.replace(/^\./, '').toLowerCase())
10
+ .filter((e) => /^[a-z0-9]{1,8}$/.test(e)),
11
+ ]),
12
+ ];
3
13
  /** Names you are likely to run in a project folder; with any of these extensions Windows would pick the local copy. */
4
14
  export const COMMANDS = new RegExp('^(' +
5
15
  [
@@ -108,7 +118,9 @@ export const COMMANDS = new RegExp('^(' +
108
118
  'ssh',
109
119
  'scp',
110
120
  ].join('|') +
111
- ')\\.(exe|com|bat|cmd|vbs|vbe|js|jse|wsf|wsh|msc|ps1)$', 'i');
121
+ ')\\.(' +
122
+ EXTENSIONS.join('|') +
123
+ ')$', 'i');
112
124
  /** Fingerprints of the files in the project root named like one of COMMANDS. */
113
125
  export function plantedCommands(workspace) {
114
126
  return filesIn(workspace, (n) => COMMANDS.test(n));
@@ -5,13 +5,28 @@ import _ from 'lodash';
5
5
  import { writeFileAtomic } from '../../shared/io/index.js';
6
6
  import { fileExists, isLink } from '../../core/util/index.js';
7
7
  /** Config keys that make git run a program, or load more config. */
8
- const DANGEROUS_KEY = /^(fsmonitor|hookspath|sshcommand|pager|editor|askpass|program|gpgprogram|textconv|command|cmd|clean|smudge|process|driver|helper|uploadpack|receivepack|path|attributesfile|excludesfile|worktree|worktreeconfig|gitproxy|proxycommand|sendemailvalidate|tool|external|difffilter|defaultkeycommand|alternaterefscommand)\s*=/i;
9
- const SECTION = /^\s*\[([^\]]*)\]\s*/;
8
+ const DANGEROUS_KEY = /^(fsmonitor|hookspath|sshcommand|pager|editor|askpass|program|gpgprogram|textconv|command|cmd|tocmd|cccmd|smtpserver|httpd|vcs|packobjectshook|clean|smudge|process|driver|helper|uploadpack|receivepack|path|attributesfile|excludesfile|worktree|worktreeconfig|gitproxy|proxycommand|sendemailvalidate|tool|external|difffilter|defaultkeycommand|alternaterefscommand)\s*(=|$)/i;
9
+ /**
10
+ * A section header as git reads it: a name (the old [section.sub] form included), then an optional quoted subsection
11
+ * in which `]` and escaped quotes are allowed ([filter "a]b"] must not hide the key after it).
12
+ */
13
+ const SECTION = /^\s*\[\s*([A-Za-z0-9.-]+)(\s+"(?:[^"\\]|\\.)*")?\s*\]\s*/;
14
+ /** Sections where any key is risky: [protocol] turns on transports that run commands (ext::), [url] rewrites URLs. */
15
+ const RISKY_SECTIONS = new Set(['pager', 'protocol', 'url']);
10
16
  const MARK = '# [agent-sandbox] disabled, added inside the sandbox: ';
11
17
  const MAX_CONFIG_BYTES = 4 * 1024 * 1024;
12
- /** Sections where any key runs a program ([pager] log = x), or where a "!" value does ([alias] st = !x). */
18
+ /**
19
+ * Sections where any key runs a program ([pager] log = x), where a "!" value does ([alias] st = !x, also quoted or on a
20
+ * continued line: every new alias line with a "!" counts), or a "!" submodule update command.
21
+ */
13
22
  function dangerousInSection(section, keyPart) {
14
- return (section === 'pager' && keyPart.includes('=')) || (section === 'alias' && /^[^=]+=\s*"?\s*!/.test(keyPart));
23
+ if (keyPart === '' || /^[#;]/.test(keyPart))
24
+ return false;
25
+ if (RISKY_SECTIONS.has(section))
26
+ return true;
27
+ if (section === 'alias')
28
+ return keyPart.includes('!');
29
+ return section === 'submodule' && /^update\s*=/i.test(keyPart) && keyPart.includes('!');
15
30
  }
16
31
  /** The lines of <dir>\config (none for a link or a file over 4 MB). */
17
32
  export function configLines(dir) {
@@ -30,16 +45,21 @@ export function disableConfig(dir, before) {
30
45
  let section = '';
31
46
  const fixed = lines.map((line) => {
32
47
  const header = SECTION.exec(line);
48
+ // A line that starts like a header but is not one git would accept: treat it as unknown (risky) below.
49
+ const badHeader = !header && /^\s*\[/.test(line);
33
50
  if (header) {
34
- section = header[1].trim().toLowerCase().split(/\s/)[0];
35
- inInclude = /^include(if\s+"[^"]*")?$/i.test(header[1].trim());
51
+ section = header[1].toLowerCase().split('.')[0];
52
+ inInclude = section === 'include' || section === 'includeif';
36
53
  }
37
54
  seen[line] = (seen[line] ?? 0) + 1;
38
55
  if (line.startsWith(MARK) || seen[line] <= (known[line] ?? 0))
39
56
  return line;
40
57
  // git also accepts "[core] fsmonitor = x" on one line, so look past a leading section header.
41
58
  const keyPart = line.replace(SECTION, '').trim();
42
- if (!(DANGEROUS_KEY.test(keyPart) || (inInclude && keyPart.includes('=')) || dangerousInSection(section, keyPart)))
59
+ if (!(badHeader ||
60
+ DANGEROUS_KEY.test(keyPart) ||
61
+ (inInclude && keyPart.includes('=')) ||
62
+ dangerousInSection(section, keyPart)))
43
63
  return line;
44
64
  findings.push({ kind: 'config', detail: line.trim() });
45
65
  return MARK + line.trim();
@@ -1,22 +1,84 @@
1
1
  // A git folder's hooks, config and redirect files, and the rule that guards the project's own .git.
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { dirExists, isLink } from '../../core/util/index.js';
4
+ import { dirExists, isInsideOrEqual, isLink } from '../../core/util/index.js';
5
5
  import { filesIn } from './file-hash.js';
6
6
  import { configLines, disableConfig } from './git-config.js';
7
7
  import { quarantine } from './quarantine.js';
8
8
  /** Files in .git that make git read config/hooks from somewhere else. */
9
9
  const REDIRECTS = ['commondir', 'config.worktree'];
10
10
  const isHook = (name) => !name.endsWith('.sample');
11
- export const EMPTY_GIT = { config: [], hooks: {}, redirects: [] };
11
+ export const EMPTY_GIT = { config: [], hooks: {}, redirects: [], pathHooks: {} };
12
+ /** The names git runs as hooks (a core.hooksPath folder inside the project holds other files too). */
13
+ const HOOK_NAMES = new Set([
14
+ 'applypatch-msg',
15
+ 'pre-applypatch',
16
+ 'post-applypatch',
17
+ 'pre-commit',
18
+ 'pre-merge-commit',
19
+ 'prepare-commit-msg',
20
+ 'commit-msg',
21
+ 'post-commit',
22
+ 'pre-rebase',
23
+ 'post-checkout',
24
+ 'post-merge',
25
+ 'pre-push',
26
+ 'pre-receive',
27
+ 'update',
28
+ 'proc-receive',
29
+ 'post-receive',
30
+ 'post-update',
31
+ 'reference-transaction',
32
+ 'push-to-checkout',
33
+ 'pre-auto-gc',
34
+ 'post-rewrite',
35
+ 'sendemail-validate',
36
+ 'fsmonitor-watchman',
37
+ 'p4-changelist',
38
+ 'p4-prepare-changelist',
39
+ 'p4-post-changelist',
40
+ 'p4-pre-submit',
41
+ 'post-index-change',
42
+ ]);
43
+ /**
44
+ * Folders inside the work tree that the repo's own core.hooksPath points at (husky: .husky/_, whose stubs run the
45
+ * hooks in .husky/). The sandbox can write them, and git on this PC runs them at the next commit.
46
+ */
47
+ function hooksPathDirs(dir, config) {
48
+ if (path.basename(dir).toLowerCase() !== '.git')
49
+ return [];
50
+ const workTree = path.dirname(dir);
51
+ let section = '';
52
+ let value = null;
53
+ for (const line of config) {
54
+ const header = /^\s*\[\s*([A-Za-z0-9.-]+)/.exec(line);
55
+ if (header)
56
+ section = header[1].toLowerCase();
57
+ const pair = /^\s*(?:\[[^\]]*\]\s*)?hookspath\s*=\s*"?([^"]*?)"?\s*$/i.exec(line);
58
+ if (pair && section === 'core')
59
+ value = pair[1];
60
+ }
61
+ if (!value || value.startsWith('~'))
62
+ return [];
63
+ const target = path.resolve(workTree, value);
64
+ if (!isInsideOrEqual(workTree, target) || !dirExists(target) || isLink(target))
65
+ return [];
66
+ return path.basename(target) === '_' ? [target, path.dirname(target)] : [target];
67
+ }
68
+ const hooksIn = (dirs, workTree) => Object.fromEntries(dirs.flatMap((d) => Object.entries(filesIn(d, (n) => HOOK_NAMES.has(n))).map(([n, h]) => [
69
+ path.relative(workTree, path.join(d, n)),
70
+ h,
71
+ ])));
12
72
  /** <workspace>\.git when it is a real folder (not a link or a "gitdir:" file). */
13
73
  export function plainGitDir(workspace) {
14
74
  const dir = path.join(workspace, '.git');
15
75
  return dirExists(dir) && !isLink(dir) ? dir : null;
16
76
  }
17
77
  export function gitState(dir) {
78
+ const config = dir ? configLines(dir) : [];
18
79
  return {
19
- config: dir ? configLines(dir) : [],
80
+ config,
81
+ pathHooks: dir ? hooksIn(hooksPathDirs(dir, config), path.dirname(dir)) : {},
20
82
  hooks: dir ? filesIn(path.join(dir, 'hooks'), isHook) : {},
21
83
  redirects: dir ? REDIRECTS.filter((n) => fs.existsSync(path.join(dir, n))) : [],
22
84
  };
@@ -29,8 +91,21 @@ export function guardGitDir(dir, before) {
29
91
  ...quarantine(hooksDir, Object.keys(hooksNow).filter((n) => before.hooks[n] !== hooksNow[n]), dir, 'hook'),
30
92
  ...quarantine(dir, REDIRECTS.filter((n) => fs.existsSync(path.join(dir, n)) && !before.redirects.includes(n)), dir, 'git'),
31
93
  ...disableConfig(dir, before.config),
94
+ ...guardPathHooks(dir, before),
32
95
  ];
33
96
  }
97
+ /** New or changed hooks in a core.hooksPath folder inside the project (not checked against older snapshots). */
98
+ function guardPathHooks(dir, before) {
99
+ if (!before.pathHooks)
100
+ return [];
101
+ const workTree = path.dirname(dir);
102
+ const now = hooksIn(hooksPathDirs(dir, before.config), workTree);
103
+ const changed = Object.keys(now).filter((rel) => before.pathHooks?.[rel] !== now[rel]);
104
+ return changed.flatMap((rel) => quarantine(path.dirname(path.join(workTree, rel)), [path.basename(rel)], dir, 'hook').map((f) => ({
105
+ ...f,
106
+ detail: rel + ' (core.hooksPath): ' + f.detail,
107
+ })));
108
+ }
34
109
  /** The project's own .git; a .git replaced by a link or a "gitdir:" file is reported, never followed. */
35
110
  export class GitDirRule {
36
111
  kind = 'git-dir';
@@ -15,7 +15,9 @@ export function markFromSandbox(file) {
15
15
  return false;
16
16
  try {
17
17
  const stat = fs.lstatSync(file);
18
- if (!stat.isFile())
18
+ // Already marked: writing the mark again would fire another change event for this file, so every marked file was
19
+ // marked (and reported) again every second for as long as the sandbox ran.
20
+ if (!stat.isFile() || isMarked(file))
19
21
  return false;
20
22
  fs.writeFileSync(file + ':Zone.Identifier', MARK);
21
23
  return true;
@@ -16,6 +16,7 @@ const SKIP_DIRS = new Set([
16
16
  '.venv',
17
17
  'venv',
18
18
  ]);
19
+ const NEVER_CHECKED = new Set(['node_modules', '.git', 'agent-sandbox-quarantine']);
19
20
  const notSkipped = (d) => !SKIP_DIRS.has(path.basename(d).toLowerCase());
20
21
  function plainDirs(dir, limit) {
21
22
  try {
@@ -57,8 +58,12 @@ export function otherGitDirs(workspace) {
57
58
  };
58
59
  if (main)
59
60
  walk(path.join(main, 'modules'), 0);
60
- for (const sub of plainDirs(workspace, 500).filter(notSkipped)) {
61
- for (const candidate of [sub, ...plainDirs(sub, 200).filter(notSkipped)]) {
61
+ // A repository directly in build\, dist\, bin\ ... is still one VS Code's repository scan opens (it skips only
62
+ // node_modules), so those folders are checked themselves; only their insides are skipped.
63
+ const neverChecked = (d) => NEVER_CHECKED.has(path.basename(d).toLowerCase());
64
+ for (const sub of plainDirs(workspace, 500).filter((d) => !neverChecked(d))) {
65
+ const inside = notSkipped(sub) ? plainDirs(sub, 200).filter(notSkipped) : [];
66
+ for (const candidate of [sub, ...inside]) {
62
67
  if (found.length >= MAX_OTHER_REPOS)
63
68
  return found;
64
69
  const git = plainGitDir(candidate);
@@ -18,6 +18,7 @@ const GitStateSchema = z.object({
18
18
  config: z.array(z.string()),
19
19
  hooks: z.record(z.string(), z.string()),
20
20
  redirects: z.array(z.string()),
21
+ pathHooks: z.record(z.string(), z.string()).optional(),
21
22
  });
22
23
  const SavedSchema = z.object({
23
24
  workspace: z
@@ -6,6 +6,8 @@ export interface GitState {
6
6
  readonly config: string[];
7
7
  readonly hooks: Record<string, string>;
8
8
  readonly redirects: string[];
9
+ /** Hooks in a core.hooksPath folder inside the project, by relative path (missing in older snapshots). */
10
+ readonly pathHooks?: Record<string, string>;
9
11
  }
10
12
  export interface RepoSnapshot extends GitState {
11
13
  readonly gitDir: boolean;
@@ -9,6 +9,7 @@ import { deriveGuest, isValidGuestPath, guestConflict, guestFix, PROJECTS_GUEST,
9
9
  import { dataMapping } from '../storage/providers/index.js';
10
10
  import { GUEST } from './wsb-file.js';
11
11
  import { guestGitDir, planView } from '../workspace-view/index.js';
12
+ import { insideProject } from '../../core/local-config/index.js';
12
13
  /**
13
14
  * Every mapping, placed. An extra folder appears at its `guest` path, or at the same path as on this PC when it has
14
15
  * none. A folder that cannot appear there (a reserved or relative path, one already in use, a folder that is not
@@ -72,6 +73,15 @@ export function collectMappings(o, launchPath, boot = { roots: [], projects: [],
72
73
  '.');
73
74
  continue;
74
75
  }
76
+ // A second mapping of (part of) the project would show it without the filter (the git-ignored .env files) and
77
+ // writable when the project is read-only: a repo's .sandbox-config.json must not undo either.
78
+ const inProject = insideProject(workspaceHost, host);
79
+ if (inProject && view) {
80
+ problems.push(host +
81
+ ': it is inside the project, and workspaceFilter.enabled keeps git-ignored files out of the sandbox. Fix: remove the mapping, or turn the filter off in your own settings.');
82
+ continue;
83
+ }
84
+ const readOnly = m.readOnly || (inProject && config.readOnly);
75
85
  const wanted = tidyGuestPath(m.guest ?? deriveGuest(host, workspaceHost, config.guestWorkspace));
76
86
  // The same folder at the same place twice (your settings and the project file): once is enough.
77
87
  if (extra.some((x) => x.host.toLowerCase() === host.toLowerCase() && x.guest.toLowerCase() === wanted.toLowerCase()))
@@ -82,7 +92,7 @@ export function collectMappings(o, launchPath, boot = { roots: [], projects: [],
82
92
  continue;
83
93
  }
84
94
  const label = outside.has(path.resolve(host).toLowerCase()) ? 'outside the project, allowed' : undefined;
85
- extra.push({ host, guest: wanted, readOnly: m.readOnly, ...(label ? { label } : {}) });
95
+ extra.push({ host, guest: wanted, readOnly, ...(label ? { label } : {}) });
86
96
  }
87
97
  if (problems.length)
88
98
  fail((problems.length === 1
@@ -2,30 +2,11 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { INTERESTING_FILE } from '../../core/detect/index.js';
5
+ import { readSmallFile } from '../../core/util/index.js';
5
6
  const SKIP = /^(node_modules|\.git|\.hg|\.svn|vendor|target|bin|obj|dist|build|out|\.next|\.nuxt|\.venv|venv|env|__pycache__|\.gradle|\.idea|\.vs|coverage|packages)$/i;
6
7
  const MAX_FILES = 400;
7
8
  const MAX_BYTES = 256 * 1024;
8
9
  const MAX_FOLDERS = 2000;
9
- /** Reads a file through one handle, only if that handle is a regular file of at most MAX_BYTES. */
10
- function readSmallFile(file) {
11
- let fd = null;
12
- try {
13
- fd = fs.openSync(file, 'r');
14
- const stat = fs.fstatSync(fd);
15
- if (!stat.isFile() || stat.size > MAX_BYTES)
16
- return null;
17
- const buffer = Buffer.alloc(stat.size);
18
- fs.readSync(fd, buffer, 0, stat.size, 0);
19
- return buffer.toString('utf8');
20
- }
21
- catch {
22
- return null;
23
- }
24
- finally {
25
- if (fd !== null)
26
- fs.closeSync(fd);
27
- }
28
- }
29
10
  /** Manifest-type files in the project, up to 3 folders deep. Links are not followed (the guest may have written it). */
30
11
  export function readProjectFiles(root, depth = 3) {
31
12
  const files = [];
@@ -45,7 +26,7 @@ export function readProjectFiles(root, depth = 3) {
45
26
  return;
46
27
  const full = path.join(dir, entry.name);
47
28
  if (entry.isFile() && INTERESTING_FILE.test(entry.name)) {
48
- const text = readSmallFile(full);
29
+ const text = readSmallFile(full, MAX_BYTES);
49
30
  if (text !== null)
50
31
  files.push({ path: path.relative(root, full), text });
51
32
  }
@@ -17,12 +17,16 @@ export function stageHooks(packageRoot, launchPath, scripts) {
17
17
  }
18
18
  stageScripts(path.join(launchPath, 'hooks', 'pre-agent'), scripts);
19
19
  }
20
+ const MAX_SCRIPT_BYTES = 4 * 1024 * 1024;
20
21
  /** --script / "scripts" files, run before the agent in this order. */
21
22
  export function stageScripts(target, scripts) {
22
23
  ensureDir(target);
23
24
  scripts.forEach((script, index) => {
24
25
  if (!fileExists(script))
25
26
  fail('Script file not found: ' + script, 'Check the --script path.');
27
+ const stat = fs.statSync(script);
28
+ if (!stat.isFile() || stat.size > MAX_SCRIPT_BYTES)
29
+ fail('Script ' + script + ' is not a plain file of at most 4 MB.', 'Check the --script path.');
26
30
  fs.copyFileSync(script, path.join(target, '90-extra-' + index + '-' + path.basename(script)));
27
31
  });
28
32
  }
@@ -106,7 +106,8 @@ export class SessionLedger {
106
106
  const mapped = [...registry.projects, ...registry.roots];
107
107
  const before = new Map(ledger.sessions.map((s) => [key(s), s]));
108
108
  const noted = sessions
109
- .filter((s) => s.phase === 'running')
109
+ // A shell is not reopened: what it was doing is gone with the sandbox.
110
+ .filter((s) => s.phase === 'running' && s.kind !== 'shell')
110
111
  .flatMap((s) => {
111
112
  const project = projectForGuestDir(mapped, s.cwd);
112
113
  if (!project)
@@ -0,0 +1,156 @@
1
+ import { z } from 'zod';
2
+ import type { RemoteRole } from '../../core/schema/web.js';
3
+ import type { SecretStore } from '../secrets/index.js';
4
+ /** A device code is good for this long. */
5
+ export declare const CODE_LIFETIME_MS: number;
6
+ /** A code that has seen this many wrong tries is void (the PC says so). */
7
+ export declare const CODE_MAX_WRONG = 10;
8
+ /** Most devices and Windows sign-ins kept (the oldest go first). */
9
+ export declare const MAX_DEVICES = 50;
10
+ export declare const MAX_WINDOWS_GRANTS = 100;
11
+ export declare const DeviceSchema: z.ZodObject<{
12
+ id: z.ZodString;
13
+ name: z.ZodString;
14
+ role: z.ZodEnum<{
15
+ view: "view";
16
+ control: "control";
17
+ }>;
18
+ address: z.ZodString;
19
+ createdAt: z.ZodString;
20
+ expiresAt: z.ZodString;
21
+ lastSeen: z.ZodOptional<z.ZodString>;
22
+ tokenHash: z.ZodString;
23
+ }, z.core.$strict>;
24
+ export type Device = z.output<typeof DeviceSchema>;
25
+ export declare const WindowsGrantSchema: z.ZodObject<{
26
+ id: z.ZodString;
27
+ sid: z.ZodString;
28
+ user: z.ZodString;
29
+ createdAt: z.ZodString;
30
+ expiresAt: z.ZodString;
31
+ tokenHash: z.ZodString;
32
+ }, z.core.$strict>;
33
+ export type WindowsGrant = z.output<typeof WindowsGrantSchema>;
34
+ export declare const PendingCodeSchema: z.ZodObject<{
35
+ hash: z.ZodString;
36
+ role: z.ZodEnum<{
37
+ view: "view";
38
+ control: "control";
39
+ }>;
40
+ createdAt: z.ZodString;
41
+ expiresAt: z.ZodString;
42
+ wrong: z.ZodNumber;
43
+ }, z.core.$strict>;
44
+ export type PendingCode = z.output<typeof PendingCodeSchema>;
45
+ export declare const AccessDataSchema: z.ZodObject<{
46
+ epoch: z.ZodNumber;
47
+ devices: z.ZodArray<z.ZodObject<{
48
+ id: z.ZodString;
49
+ name: z.ZodString;
50
+ role: z.ZodEnum<{
51
+ view: "view";
52
+ control: "control";
53
+ }>;
54
+ address: z.ZodString;
55
+ createdAt: z.ZodString;
56
+ expiresAt: z.ZodString;
57
+ lastSeen: z.ZodOptional<z.ZodString>;
58
+ tokenHash: z.ZodString;
59
+ }, z.core.$strict>>;
60
+ windows: z.ZodArray<z.ZodObject<{
61
+ id: z.ZodString;
62
+ sid: z.ZodString;
63
+ user: z.ZodString;
64
+ createdAt: z.ZodString;
65
+ expiresAt: z.ZodString;
66
+ tokenHash: z.ZodString;
67
+ }, z.core.$strict>>;
68
+ code: z.ZodNullable<z.ZodObject<{
69
+ hash: z.ZodString;
70
+ role: z.ZodEnum<{
71
+ view: "view";
72
+ control: "control";
73
+ }>;
74
+ createdAt: z.ZodString;
75
+ expiresAt: z.ZodString;
76
+ wrong: z.ZodNumber;
77
+ }, z.core.$strict>>;
78
+ network: z.ZodNullable<z.ZodString>;
79
+ codeVoided: z.ZodBoolean;
80
+ }, z.core.$strict>;
81
+ export type AccessData = z.output<typeof AccessDataSchema>;
82
+ export declare const EMPTY_ACCESS: AccessData;
83
+ /** SHA-256 of a code or token, hex. */
84
+ export declare function secretHash(value: string): string;
85
+ /** A new code: 8 characters (40 bits), shown as ABCD-EFGH. */
86
+ export declare function newCode(random?: (n: number) => Buffer): string;
87
+ /** ABCDEFGH -> ABCD-EFGH. */
88
+ export declare function showCode(code: string): string;
89
+ /** What a person typed, as a code: upper case, no dashes or spaces, O as 0, I or L as 1; null when it is not one. */
90
+ export declare function normalizeCode(typed: unknown): string | null;
91
+ /** A credential as the cookie carries it: `<id>.<token>`. */
92
+ export declare function credentialValue(id: string, token: string): string;
93
+ /** `<16 hex>.<64 hex>`, or null. */
94
+ export declare function parseCredential(value: string | undefined): {
95
+ id: string;
96
+ token: string;
97
+ } | null;
98
+ export type RedeemResult = {
99
+ readonly ok: true;
100
+ readonly device: Device;
101
+ readonly credential: string;
102
+ } | {
103
+ readonly ok: false;
104
+ readonly reason: 'none' | 'wrong' | 'expired' | 'void';
105
+ };
106
+ export interface AccessStoreOptions {
107
+ readonly now?: () => number;
108
+ readonly random?: (n: number) => Buffer;
109
+ /** Makes the folder private (the web state folder already is; tests pass a no-op). */
110
+ readonly protect?: (dir: string) => void;
111
+ }
112
+ export declare class AccessStore {
113
+ readonly dir: string;
114
+ private readonly secrets;
115
+ private readonly options;
116
+ private readonly file;
117
+ private cache;
118
+ private readonly now;
119
+ private readonly random;
120
+ constructor(dir: string, secrets: SecretStore, options?: AccessStoreOptions);
121
+ /** <storage>\web. */
122
+ static at(storageRoot: string, secrets: SecretStore, options?: AccessStoreOptions): AccessStore;
123
+ private stamp;
124
+ /** The list as it is now (read again only when the file changed). A damaged or foreign file counts as empty. */
125
+ read(): AccessData;
126
+ private write;
127
+ /** Changes the list (expired credentials are dropped on the way). */
128
+ update(change: (data: AccessData) => AccessData): AccessData;
129
+ private iso;
130
+ private alive;
131
+ private withoutExpired;
132
+ private newId;
133
+ private newToken;
134
+ /** A new device code for this role (replaces a pending one). Returns the code; only its hash is kept. */
135
+ newDeviceCode(role: RemoteRole): {
136
+ code: string;
137
+ expiresAt: string;
138
+ };
139
+ /** Whether a code is pending (attempts with none pending are not counted anywhere). */
140
+ codePending(): boolean;
141
+ /** Trades the pending code for a new device credential (one use); counts a wrong code. */
142
+ redeem(typed: unknown, name: string, address: string, rememberDays: number): RedeemResult;
143
+ /** The device a cookie value stands for: known id, same token, not expired. */
144
+ device(value: string | undefined): Device | null;
145
+ /** Notes when a device was last seen (at most every 10 minutes per device: each write seals the list again). */
146
+ seen(id: string): void;
147
+ /** A credential for a Windows user (after the pipe sign-in): the cookie value. */
148
+ issueWindows(sid: string, user: string, rememberDays: number): string;
149
+ windowsGrant(value: string | undefined): WindowsGrant | null;
150
+ /** Ends one device or Windows sign-in; false when there is no such id. */
151
+ revoke(id: string): boolean;
152
+ /** Ends every device (and, unless only devices are asked for, every Windows sign-in); voids the pending code. */
153
+ revokeAll(what?: 'devices' | 'everything'): void;
154
+ /** Remembers which network LAN mode was turned on for (null: forget it). */
155
+ setNetwork(network: string | null): void;
156
+ }