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
@@ -48,6 +48,14 @@ export const gitAccessSuite = {
48
48
  '--git-dir=x status',
49
49
  'clean -fd',
50
50
  'apply p.diff',
51
+ // reading commands with options that run a program or write a file
52
+ 'grep -Ocalc.exe x',
53
+ 'grep --open-files-in-pager=calc.exe x',
54
+ 'ls-remote --upload-pack=calc.exe origin',
55
+ 'ls-remote -ucalc.exe origin',
56
+ 'diff --output=C:/x.txt',
57
+ 'log --output C:/x.txt',
58
+ 'diff --ext-diff',
51
59
  ];
52
60
  t.equal(writing.filter((command) => allowed(command.split(' '))), [], 'readonly: anything that writes (or overrides config) is refused');
53
61
  t.section('none and full');
@@ -570,6 +570,41 @@ export const hcsSuite = {
570
570
  t.check(refused(['grant-vm-access', 'agent-sandbox-1', notDisk]) &&
571
571
  refused(['grant-vm-access', 'agent-sandbox-1', missing]) &&
572
572
  refused(['grant-vm-access', 'agent-sandbox-1', 'relative.vhdx']), 'grant-vm-access refuses anything but an existing .vhdx at a full path');
573
+ // share (prototype, docs/design/hcs-live-folders.md): refused before HCS is asked, so no Hyper-V is needed.
574
+ const folder = t.dir('hcs-share-folder');
575
+ const share = (over) => ({
576
+ id: 'agent-sandbox-1',
577
+ action: 'add',
578
+ name: 'agentsbx-0123abcd',
579
+ path: folder,
580
+ ...over,
581
+ });
582
+ let link = '';
583
+ try {
584
+ link = path.join(tempRoot, 'share-link');
585
+ fs.symlinkSync(folder, link, 'junction');
586
+ }
587
+ catch {
588
+ link = ''; // a folder that refuses links (a mapped folder): that case is skipped
589
+ }
590
+ t.check(refused(['share'], share({ id: '0b9a3c2e-1f00-4a4c-9d1e-6a7b8c9d0e1f' })) &&
591
+ refused(['share'], share({ id: 'WindowsSandbox' })) &&
592
+ refused(['share'], share({ name: '3ad483dec1a233132da4197c9790dcda06668793230cd6d9e94b1882e0f7f29c' })) &&
593
+ refused(['share'], share({ name: 'agentsbx-XYZ' })) &&
594
+ refused(['share'], share({ action: 'update' })) &&
595
+ refused(['share'], share({ path: 'relative\\folder' })) &&
596
+ refused(['share'], share({ path: 'C:\\' })) &&
597
+ refused(['share'], share({ path: path.join(tempRoot, 'missing-folder') })) &&
598
+ refused(['share'], share({ path: folder + '\\..\\' + path.basename(folder) })) &&
599
+ refused(['share'], share({ path: folder + '/../' + path.basename(folder) })) &&
600
+ refused(['share'], share({ path: folder + ':stream' })) &&
601
+ refused(['share'], share({ path: folder + '.' })) &&
602
+ refused(['share'], share({ path: folder + ' ' })) &&
603
+ refused(['share'], share({ path: 'C:\\PROGRA~1' })) &&
604
+ refused(['share'], share({ readOnly: 'yes' })) &&
605
+ (!link || refused(['share'], share({ path: link }))), 'share refuses a Windows Sandbox or other foreign id, a foreign share name, an update, a drive, a relative, ' +
606
+ "missing, dotted, '/', stream, trailing-dot or -space, short-name or linked folder" +
607
+ (link ? '' : ' (link case skipped)'));
573
608
  const existing = t.write(path.join(tempRoot, 'existing.vhdx'), 'keep me');
574
609
  const over = raw.call(['create-vhdx', existing, '1']);
575
610
  t.check(!over.ok && fs.readFileSync(existing, 'utf8') === 'keep me', 'create-vhdx never touches an existing file', over.message);
@@ -12,7 +12,7 @@ import { writeJsonAtomic } from '../shared/io/index.js';
12
12
  export const hostileInputSuite = {
13
13
  name: 'hostile-input',
14
14
  title: 'file names, deletes, printed text, mappings and state files from an untrusted sandbox',
15
- run: async (t) => {
15
+ run: async (t, f) => {
16
16
  t.section('file names');
17
17
  const unsafe = ['CON.jsonl', 'nul', 'aux.json', 'COM1.txt', 'a:b', 'x.jsonl.', 'x ', '..', 'a\\b', 'LPT3'];
18
18
  t.equal(unsafe.filter(isSafeFileName), [], 'device names, streams and trailing dots are not valid names to copy into this PC');
@@ -52,5 +52,16 @@ export const hostileInputSuite = {
52
52
  t.equal(fs.readdirSync(path.dirname(atomicFile)).length, 1, '... with no temp files left behind');
53
53
  await t.throws(() => assertCmdSafePath('C:\\a\\%TEMP%\\agent.wsb'), 'cannot be started safely', 'a launch path with %VAR% (which cmd.exe would expand) is refused');
54
54
  await t.throws(() => assertCmdSafePath('C:\\a"&calc&"\\agent.wsb'), 'cannot be started safely', 'a launch path with quotes and & is refused');
55
+ t.section('repo files read on this PC (security review 2026-10)');
56
+ const repoDir = t.dir('hostile-versions');
57
+ fs.writeFileSync(path.join(repoDir, '.nvmrc'), '\u001b]52;c;ZXZpbA==\u0007\u001b[2J20\n');
58
+ fs.writeFileSync(path.join(repoDir, '.python-version'), '3.12.1\n');
59
+ const versionsRun = await f.run({ cwd: repoDir });
60
+ t.check(!versionsRun.versions.node.spec.includes('\u001b') &&
61
+ versionsRun.versions.node.source !== '.nvmrc' &&
62
+ versionsRun.versions.python.spec === '3.12.1', 'a version file holding escape codes is ignored (it would reach the launch table); a plain one is used', JSON.stringify(versionsRun.versions));
63
+ fs.writeFileSync(path.join(repoDir, '.sandbox-config.json'), '{"readOnly": true}' + ' '.repeat(1100 * 1024));
64
+ await t.throws(() => f.run({ cwd: repoDir, trustConfig: true }), 'over 1 MB', 'a .sandbox-config.json over 1 MB is refused before it is parsed');
65
+ fs.rmSync(path.join(repoDir, '.sandbox-config.json'));
55
66
  },
56
67
  };
@@ -57,6 +57,7 @@ import { syncSuite } from './sync.test.js';
57
57
  import { testGuardSuite } from './test-guard.test.js';
58
58
  import { toolchainsSuite } from './toolchains.test.js';
59
59
  import { webSuite } from './web.test.js';
60
+ import { remoteSuite } from './remote.test.js';
60
61
  import { workspaceSuite } from './workspace.test.js';
61
62
  import { workspaceViewSuite } from './workspace-view.test.js';
62
63
  import { liveNetworkSuite } from './live-network.test.js';
@@ -107,6 +108,7 @@ export const SUITES = [
107
108
  secretsSuite,
108
109
  hostileInputSuite,
109
110
  webSuite,
111
+ remoteSuite,
110
112
  testGuardSuite,
111
113
  // pure helpers and the command line
112
114
  layoutSuite,
@@ -19,6 +19,8 @@ export const liveDaemonSuite = {
19
19
  Object.assign(session.harness, {
20
20
  arguments: ['/d', '/k', 'echo LIVE-AGENT-READY'],
21
21
  resumeArguments: ['/d', '/k', 'echo LIVE-AGENT-RESUMED'],
22
+ // The agent's own environment (where its keys live): a shell session must not get it.
23
+ environment: { LIVE_AGENT_KEY: 'sk-live-agent' },
22
24
  });
23
25
  t.writeJson(gen.sessionPath, session);
24
26
  const daemon = startDaemon(t, f, { launchPath: gen.launchPath, data, workspace });
@@ -82,6 +84,29 @@ export const liveDaemonSuite = {
82
84
  t.check(Boolean(ping?.ok) &&
83
85
  !fs.existsSync(path.join(daemon.run, 'responses', 'evil.json')) &&
84
86
  !fs.existsSync(path.join(daemon.run, 'requests', 'evil.json')), 'ping is answered; an unknown request kind gets no answer and is deleted', JSON.stringify(ping));
87
+ t.section('live: a plain shell session');
88
+ daemon.request({ kind: 'session', id: 'shell1', harness: 'shell', args: [], cwd: workspace, shell: 'cmd' });
89
+ const shell = await daemon.started('shell1');
90
+ t.check(shell?.phase === 'running' && shell.harness === 'cmd' && shell.kind === 'shell' && Boolean(shell.attach), 'a shell session runs through ptyhost and is listed by its shell, marked as a shell', JSON.stringify(shell) + '\n' + daemon.diagnostics());
91
+ if (shell?.attach) {
92
+ const shellView = await openViewer(shell.attach);
93
+ viewers.push(shellView);
94
+ // Typing before cmd has drawn its prompt can lose the keystrokes: wait for the prompt first.
95
+ await sees(t, shellView, '>');
96
+ shellView.type('echo SHELL-[%LIVE_AGENT_KEY%]-%USERNAME:~0,0%OK');
97
+ const sawShell = await sees(t, shellView, 'SHELL-[%LIVE_AGENT_KEY%]-OK');
98
+ t.check(Boolean(sawShell), "keystrokes reach the shell, and it has none of the agent's own environment", 'screen so far: ' + tail(shellView.output()) + '\n' + daemon.diagnostics());
99
+ daemon.request({ kind: 'close', id: 'close1', session: 'shell1' });
100
+ const closed = await daemon.response('close1');
101
+ const shellEnded = await t.waitFor(() => {
102
+ const s = daemon.status('shell1');
103
+ return s?.phase === 'exited' ? s : null;
104
+ }, WAIT.exit);
105
+ t.check(Boolean(closed?.ok) && shellEnded !== null && shellView.exitCode() !== null, 'close ends the shell: the sandbox answers, the status says exited and the attached client gets the exit', JSON.stringify(closed) + ' ' + JSON.stringify(daemon.status('shell1')));
106
+ daemon.request({ kind: 'close', id: 'close2', session: 'shell1' });
107
+ const again = await daemon.response('close2');
108
+ t.check(again?.ok === false, 'closing a session that has ended says it is not running', JSON.stringify(again));
109
+ }
85
110
  t.section('live: restart and exit');
86
111
  daemon.request({ kind: 'restart', id: 'restart1', harness: 'claude' });
87
112
  const sawResumed = await sees(t, viewer, 'LIVE-AGENT-RESUMED');
@@ -61,6 +61,28 @@ export const localConfigSuite = {
61
61
  await rejectedBecause({ claudeProfile: { hostDir: 'C:\\x' } }, 'claudeProfile.hostDir is not allowed', 'a project file cannot point at the Claude profile folder');
62
62
  await rejectedBecause({ toolchains: { node: { hostDir: 'x' } } }, 'toolchains.node.hostDir is not allowed', 'a project file cannot point at host tool folders');
63
63
  await rejectedBecause({ sandbox: { memoryMb: 12 } }, 'sandbox.memoryMb:', 'values are type-checked (memory too small)');
64
+ // Security review 2026-10: what a repo could reach through fields that looked harmless.
65
+ const secretFile = path.join(f.shared, 'id_ed25519');
66
+ fs.writeFileSync(secretFile, 'PRIVATE KEY');
67
+ await rejectedBecause({ scripts: [secretFile] }, 'only files inside the project', 'a project file cannot name a script outside the project (it would copy that PC file into the launch folder)');
68
+ await rejectedBecause({ scripts: [path.relative(dirC, secretFile)] }, 'only files inside the project', 'a relative ..\\ script path outside the project is refused too');
69
+ fs.symlinkSync(f.shared, path.join(dirC, 'linked'), 'junction');
70
+ await rejectedBecause({ scripts: ['./linked/id_ed25519'] }, 'only files inside the project', 'a script reached through a link to a folder outside the project is refused');
71
+ fs.rmSync(path.join(dirC, 'linked'));
72
+ for (const name of [
73
+ 'AGENT_GIT_ACCESS',
74
+ 'GIT_CONFIG_COUNT',
75
+ 'ANTHROPIC_BASE_URL',
76
+ 'NODE_OPTIONS',
77
+ 'Path',
78
+ 'https_proxy',
79
+ ])
80
+ await rejectedBecause({ toolchains: { environment: { [name]: 'x' } } }, 'cannot be set by a project', 'a project file cannot set ' + name + ' in toolchains.environment');
81
+ await rejectedBecause({ sandbox: { reserveHostMb: 0 } }, 'sandbox.reserveHostMb is not allowed', 'a project file cannot lower the RAM kept for this PC');
82
+ f.writeLocal(dirC, { toolchains: { environment: { NPM_CONFIG_REGISTRY: 'https://registry.example' } } });
83
+ t.equal((await f.run({ cwd: dirC, trustConfig: true })).config.toolchains.environment.NPM_CONFIG_REGISTRY, 'https://registry.example', 'a registry mirror in toolchains.environment is still fine');
84
+ f.writeLocal(dirC, { provisioning: { copyFromHost: true } });
85
+ t.equal((await f.run({ cwd: dirC, trustConfig: true }, { ...f.baseConfig, provisioning: { ...f.baseConfig.provisioning, copyFromHost: false } })).config.provisioning.copyFromHost, false, 'a project file cannot turn on copying Node, Git and the agent from this PC when you turned it off');
64
86
  await rejectedBecause('{\n // a comment\n "readOnly": tru\n}', 'line 3', 'broken JSON is reported with its line');
65
87
  await rejectedBecause({ _comment: 'old style' }, '_comment is not allowed', 'unknown keys are rejected');
66
88
  f.writeLocal(dirC, '// comments and trailing commas are fine\n{\n "readOnly": true, /* inline */\n}\n');
@@ -109,6 +131,14 @@ export const localConfigSuite = {
109
131
  t.check(viaVariable.includes('outside the project') &&
110
132
  viaLink.includes('outside the project') &&
111
133
  (await f.run({ cwd: dirD, trustConfig: true }, withOutside(false))).config.mappings.some((m) => m.host === path.join(dirD, 'assets')), 'outside means where the folder really is (%VAR% expanded, links followed); a folder inside needs no permission');
134
+ // A second mapping of part of the project must not undo readOnly or the workspace filter.
135
+ fs.mkdirSync(path.join(dirD, 'assets'), { recursive: true });
136
+ f.writeLocal(dirD, { mappings: [{ host: '.', guest: 'C:\\Data', readOnly: false }] });
137
+ const readOnlyUser = { ...f.baseConfig, readOnly: true };
138
+ const remapped = (await f.launch({ cwd: dirD, trustConfig: true }, { config: readOnlyUser })).mappings.find((m) => m.guest.toLowerCase() === 'c:\\data');
139
+ const filtered = await t.thrownMessage(() => f.launch({ cwd: dirD, trustConfig: true }, { config: { ...f.baseConfig, workspaceFilter: { ...f.baseConfig.workspaceFilter, enabled: true } } }));
140
+ t.check(remapped?.readOnly === true && filtered.includes('workspaceFilter.enabled'), 'a project folder mapped again is read-only when the project is, and refused while the workspace filter is on', JSON.stringify(remapped) + ' / ' + filtered);
141
+ fs.rmSync(path.join(dirD, '.sandbox-config.json'));
112
142
  t.section('a project can only tighten protections');
113
143
  // Settings that would loosen a protection are listed before approval, then ignored.
114
144
  f.writeLocal(dirC, {
@@ -329,7 +329,7 @@ export function renderScreens(dir) {
329
329
  web: { running: true, port: 47800, address: 'http://127.0.0.1:47800/' },
330
330
  more: [
331
331
  { name: 'sync', summary: 'sync history now' },
332
- { name: 'audit', summary: 'show what the sandbox did' },
332
+ { name: 'config', summary: 'your settings file' },
333
333
  ],
334
334
  };
335
335
  // Two unsaved changes in Settings.
@@ -624,12 +624,12 @@ export const menuSuite = {
624
624
  t.section('apps in a few groups');
625
625
  const groupIds = appsCategories(user2).map((c) => c.id);
626
626
  const appItems = appsItems(user2, t.dir('apps-groups'), new Draft()).filter((i) => i.value !== REQUEST_APP);
627
- t.check(groupIds.length <= 6 &&
628
- appsCategories(ConfigFile.project(t.dir('apps-groups', 'project'))).length <= 5 &&
627
+ t.check(groupIds.length <= 7 &&
628
+ appsCategories(ConfigFile.project(t.dir('apps-groups', 'project'))).length <= 6 &&
629
629
  APP_GROUPS.every((g) => appItems.some((i) => i.category === g.id)) &&
630
630
  appItems.every((i) => groupIds.includes(i.category ?? '')) &&
631
631
  appItems.length === appRows(user2, t.dir('apps-groups')).length &&
632
- APP_CATEGORIES.every((c) => APP_GROUPS.some((g) => g.members.includes(c.id))), 'the apps are in at most 6 groups; every catalogue category belongs to one and every app is listed', groupIds.join());
632
+ APP_CATEGORIES.every((c) => APP_GROUPS.some((g) => g.members.includes(c.id))), 'the apps are in at most 7 groups (with your own programs); every catalogue category belongs to one and every app is listed', groupIds.join());
633
633
  t.section('dialog box');
634
634
  const box = dialogBox({ title: 'Unsaved changes', lines: ['You have 2 unsaved changes.'], buttons: 'Save' }, 61);
635
635
  const base = Array.from({ length: 12 }, () => 'x'.repeat(60));
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const remoteSuite: Suite;