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
@@ -57,6 +57,7 @@ export const resumeSuite = {
57
57
  status({ id: 's2', harness: 'codex', cwd: 'C:\\Workspace\\web\\src', startedAt: minutes(20) }),
58
58
  status({ id: 's3', phase: 'exited' }),
59
59
  status({ id: 's4', cwd: 'D:\\nowhere' }),
60
+ status({ id: 's5', harness: 'pwsh', kind: 'shell' }),
60
61
  ], reg, NOW);
61
62
  const saved = ledger.read();
62
63
  t.equal([saved?.sessions.map((s) => [s.harness, s.project, s.conversation]), saved?.launch.projects], [
@@ -65,7 +66,7 @@ export const resumeSuite = {
65
66
  ['codex', 'F:\\code\\web\\src', undefined],
66
67
  ],
67
68
  ['F:\\code\\web'],
68
- ], 'running sessions are noted with their folder on this PC (ended ones and unknown folders are not), and the projects');
69
+ ], 'running sessions are noted with their folder on this PC (ended ones, unknown folders and shells are not), and the projects');
69
70
  ledger.recordSessions([
70
71
  status({ startedAt: minutes(30) }),
71
72
  status({ id: 's2', harness: 'codex', cwd: 'C:\\Workspace\\web\\src', startedAt: minutes(20) }),
@@ -142,7 +142,7 @@ async function permissionPlan(t) {
142
142
  await t.throws(() => sharingAclPlan({ storageUserDir, dataPath: t.at('elsewhere', 'default'), groupSid: GROUP_SID, on: true }), 'not under', 'a data set outside the user folder is refused');
143
143
  }
144
144
  /** The PowerShell script: it parses, changes a temp folder's permissions, and refuses links. Windows only. */
145
- function folderScript(t) {
145
+ async function folderScript(t) {
146
146
  t.section('set-folder-access.ps1');
147
147
  if (process.platform !== 'win32') {
148
148
  t.skip('the folder-permission script runs only on Windows');
@@ -223,6 +223,13 @@ function folderScript(t) {
223
223
  fs.mkdirSync(path.join(own, 'child'));
224
224
  fs.writeFileSync(path.join(own, 'child', 'a.txt'), 'x');
225
225
  t.check(fs.readFileSync(path.join(own, 'child', 'a.txt'), 'utf8') === 'x', 'the owner can still use a private folder');
226
+ // Security review 2026-10: under an all-users install another member of the group can create users\<you> first.
227
+ const theirs = t.dir('acl-someone-elses');
228
+ const given = spawnSync(SYSTEM.icacls(), [theirs, '/setowner', '*S-1-5-18'], { encoding: 'utf8', windowsHide: true });
229
+ if (given.status !== 0)
230
+ t.skip('a folder owned by someone else (cannot give a folder to SYSTEM here)');
231
+ else
232
+ await t.throws(() => runPrivateFolder(theirs), 'not to you', 'a folder someone else owns is refused, never made "private" for you (its owner could open it again)');
226
233
  }
227
234
  export const sharingSuite = {
228
235
  name: 'sharing',
@@ -236,7 +243,7 @@ export const sharingSuite = {
236
243
  recordsSection(t);
237
244
  ownership(t);
238
245
  await permissionPlan(t);
239
- folderScript(t);
246
+ await folderScript(t);
240
247
  }
241
248
  finally {
242
249
  // Later suites load the settings too: the machine file this suite wrote must not outlive it.
@@ -30,6 +30,15 @@ export const LAUNCH_FILES_GOLDEN = [
30
30
  'runtime/main.js',
31
31
  'runtime/netproxy.js',
32
32
  'runtime/view.js',
33
+ // The agent add-ons' lock files (pinned installs), copied with the rest of sandbox\.
34
+ 'sandbox/addons/ccusage/package-lock.json',
35
+ 'sandbox/addons/ccusage/package.json',
36
+ 'sandbox/addons/context7/package-lock.json',
37
+ 'sandbox/addons/context7/package.json',
38
+ 'sandbox/addons/headroom/requirements.in',
39
+ 'sandbox/addons/headroom/requirements.txt',
40
+ 'sandbox/addons/playwright-mcp/package-lock.json',
41
+ 'sandbox/addons/playwright-mcp/package.json',
33
42
  'sandbox/boot/bootstrap.ps1',
34
43
  'sandbox/boot/bootstrap/drives.ps1',
35
44
  'sandbox/boot/bootstrap/files.ps1',
@@ -45,6 +45,7 @@ export declare function createFixtures(t: Harness): {
45
45
  maxMb: number;
46
46
  processes: boolean;
47
47
  files: boolean;
48
+ commits: boolean;
48
49
  };
49
50
  mappings: {
50
51
  host: string;
@@ -262,6 +263,19 @@ export declare function createFixtures(t: Harness): {
262
263
  enabled: boolean;
263
264
  port: number;
264
265
  openOnLaunch: boolean;
266
+ remote: {
267
+ windowsSignIn: boolean;
268
+ users: {
269
+ name: string;
270
+ role: "view" | "control";
271
+ }[];
272
+ rememberDays: number;
273
+ lan: {
274
+ enabled: boolean;
275
+ port: number;
276
+ address: string;
277
+ };
278
+ };
265
279
  };
266
280
  agentCommands: {
267
281
  enabled: boolean;
@@ -17,6 +17,8 @@ import { showData } from '../../app/features/data/index.js';
17
17
  import { updatesStatus } from '../../app/features/updates/index.js';
18
18
  import { containedStatus, showStorage } from '../../app/features/storage/index.js';
19
19
  import { listSessions } from '../../app/features/sessions/index.js';
20
+ import { auditListing, listAudit } from '../../app/features/audit/index.js';
21
+ import { AuditLog } from '../../host/audit/index.js';
20
22
  import { cancelled } from '../../app/kernel/index.js';
21
23
  import { runDefinition } from '../../app/registry.js';
22
24
  import { webSession } from '../../cli/services/index.js';
@@ -509,12 +511,159 @@ function sampleLogTail(input) {
509
511
  next: 0,
510
512
  });
511
513
  }
514
+ /** A plausible audit log: two agents' sessions, the sandbox's own setup, a commit each, a guard finding. */
515
+ function sampleAuditLog(dir) {
516
+ const log = new AuditLog(dir, { days: 7, maxMb: 100 });
517
+ const at = (minutesAgo) => new Date(Date.now() - minutesAgo * MINUTE).toISOString();
518
+ const claude = { s: SESSIONS[0].id, ag: 'claude', pj: 'windows-sandbox', sv: 'p' };
519
+ const codex = { s: 'mva7codex2k91x', ag: 'codex', pj: 'mw2-agent', sv: 'p' };
520
+ const git = 'C:\\AgentData\\tools\\git\\cmd\\git.exe';
521
+ const node = 'C:\\nvm\\nodejs\\node.exe';
522
+ const ws = 'F:\\AgentProjects\\windows-sandbox';
523
+ const mw = 'F:\\AgentProjects\\mw2-agent';
524
+ const proc = (m, p, c, who, n) => ({
525
+ t: at(m),
526
+ k: 'proc',
527
+ p,
528
+ c,
529
+ pp: node,
530
+ ...who,
531
+ ...(n ? { n } : {}),
532
+ });
533
+ const file = (m, p, w, who) => ({
534
+ t: at(m),
535
+ k: 'file',
536
+ a: 'change',
537
+ p,
538
+ w,
539
+ ...who,
540
+ });
541
+ log.append([
542
+ { t: at(95), k: 'note', p: 'sandbox started; audit: programs, files, commits, sessions' },
543
+ proc(94, 'C:\\AgentData\\tools\\uv\\uv.exe', 'uv tool install aider-chat', {}),
544
+ { t: at(90), k: 'session', a: 'start', p: 'claude', w: SESSIONS[0].cwd, c: claude.s, ...claude, sv: undefined },
545
+ proc(88, git, 'git status --short', claude),
546
+ proc(87, node, 'node dist/bin/agent-sandbox.js self-test audit', claude, 2),
547
+ file(80, 'src/host/audit/collect.ts', ws, claude),
548
+ file(79, 'src/app/features/audit/audit.def.ts', ws, claude),
549
+ proc(70, 'C:\\nvm\\nodejs\\npm.cmd', 'npm run build', claude),
550
+ {
551
+ t: at(66),
552
+ k: 'session',
553
+ a: 'start',
554
+ p: 'codex',
555
+ w: 'C:\\Projects\\mw2-agent',
556
+ c: codex.s,
557
+ ...codex,
558
+ sv: undefined,
559
+ },
560
+ proc(64, 'C:\\Users\\WDAGUtilityAccount\\.cargo\\bin\\cargo.exe', 'cargo build --release', codex, 3),
561
+ file(60, 'src/main.rs', mw, { ...codex, sv: 't' }),
562
+ proc(52, git, 'git commit -m "Audit: link programs to their session"', claude),
563
+ {
564
+ t: at(52),
565
+ k: 'commit',
566
+ a: 'commit',
567
+ p: 'Audit: link programs to their session',
568
+ h: 'a3f9c2e81b7d4c60a1e2f3b4c5d6e7f8a9b0c1d2',
569
+ au: 'Josh <josh@example.com>',
570
+ w: ws,
571
+ ...claude,
572
+ },
573
+ proc(40, git, 'git push --set-upstream origin https://***@github.com/example/mw2-agent.git', codex),
574
+ { t: at(38), k: 'guard', p: 'hook: a pre-push hook added inside the sandbox was neutralised' },
575
+ proc(30, 'C:\\Users\\WDAGUtilityAccount\\.cargo\\bin\\cargo.exe', 'cargo test', codex, 4),
576
+ {
577
+ t: at(28),
578
+ k: 'commit',
579
+ a: 'commit',
580
+ p: 'Fix the map loader',
581
+ h: '7be0d41c9a2f4e3b8c1d0e9f8a7b6c5d4e3f2a1b',
582
+ au: 'Josh <josh@example.com>',
583
+ w: mw,
584
+ ...codex,
585
+ },
586
+ {
587
+ t: at(20),
588
+ k: 'session',
589
+ a: 'end',
590
+ p: 'codex',
591
+ w: 'C:\\Projects\\mw2-agent',
592
+ c: codex.s + ' exit 0',
593
+ ...codex,
594
+ sv: undefined,
595
+ },
596
+ proc(5, node, 'node dist/bin/agent-sandbox.js self-test app', claude),
597
+ { t: at(4), k: 'remote', p: 'sessions.restart', a: 'ok', au: "device Josh's iPhone" },
598
+ { t: at(3), k: 'remote', p: 'settings.set', a: 'refused', au: 'Windows user DESKTOP-JOSH\\sam' },
599
+ ]);
600
+ return log;
601
+ }
602
+ const ACCESS_FINGERPRINT = '3A:7F:01:C2:9B:44:E8:15:6D:A0:2B:93:CC:71:58:0E:F4:12:9A:BD:63:07:E5:2C:48:D9:B1:76:0A:3F:C8:E2';
603
+ /** Remote access: LAN mode on, two devices, one Windows sign-in, a code waiting. */
604
+ function sampleAccess() {
605
+ const at = (hours) => new Date(Date.now() + hours * 3_600_000).toISOString();
606
+ return {
607
+ lan: {
608
+ enabled: true,
609
+ running: true,
610
+ address: 'https://192.168.1.20:47801',
611
+ fingerprint: ACCESS_FINGERPRINT,
612
+ problem: null,
613
+ network: 'Home Wi-Fi',
614
+ },
615
+ windowsSignIn: true,
616
+ users: [
617
+ { name: 'sam', role: 'view' },
618
+ { name: 'OFFICE\\kim', role: 'control' },
619
+ ],
620
+ rememberDays: 30,
621
+ devices: [
622
+ {
623
+ id: '4f2c9a7e1b3d5f60',
624
+ name: "Josh's iPhone",
625
+ role: 'control',
626
+ address: '192.168.1.42',
627
+ createdAt: at(-24 * 6),
628
+ expiresAt: at(24 * 24),
629
+ lastSeen: at(-0.2),
630
+ },
631
+ {
632
+ id: '9d1e0b2a7c4f6e83',
633
+ name: 'Kitchen tablet',
634
+ role: 'view',
635
+ address: '192.168.1.57',
636
+ createdAt: at(-24 * 20),
637
+ expiresAt: at(24 * 10),
638
+ lastSeen: at(-30),
639
+ },
640
+ ],
641
+ windows: [{ id: '1a2b3c4d5e6f7a8b', user: 'DESKTOP-JOSH\\sam', createdAt: at(-3), expiresAt: at(24 * 30 - 3) }],
642
+ pending: { role: 'view', expiresAt: at(0.06) },
643
+ codeVoided: false,
644
+ };
645
+ }
512
646
  /** The real features where they only touch the temp folder, sample data for the rest. */
513
647
  function previewRpc(ctx, state) {
648
+ const auditDir = path.join(ctx.storageRoot, 'audit', 'preview');
649
+ const audit = sampleAuditLog(auditDir);
514
650
  const sample = async (id, input, io) => {
515
651
  switch (id) {
652
+ case 'audit.list':
653
+ // The real listing over a sample log in the temp folder.
654
+ return auditListing(audit, { enabled: true, days: 7, maxMb: 100 }, listAudit.input.parse(input ?? {}), auditDir);
516
655
  case 'sandbox.status':
517
656
  return sampleStatus(state.running);
657
+ case 'access.status':
658
+ return sampleAccess();
659
+ case 'access.allowDevice':
660
+ return {
661
+ code: 'K7QD-M2XP',
662
+ expiresAt: new Date(Date.now() + 5 * 60_000).toISOString(),
663
+ role: input?.role ?? 'view',
664
+ address: 'https://192.168.1.20:47801',
665
+ fingerprint: ACCESS_FINGERPRINT,
666
+ };
518
667
  case 'vpn.status':
519
668
  return vpnStatus.output.parse({
520
669
  enabled: true,
@@ -792,9 +941,17 @@ function previewRpc(ctx, state) {
792
941
  'logs.tail': 'query',
793
942
  'engine.status': 'query',
794
943
  'engine.leftovers': 'query',
944
+ 'audit.list': 'query',
945
+ 'access.status': 'query',
946
+ 'access.allowDevice': 'operation',
947
+ 'access.revoke': 'operation',
948
+ 'access.revokeAll': 'operation',
949
+ 'access.lanOn': 'operation',
950
+ 'access.lanOff': 'operation',
795
951
  };
796
952
  return {
797
953
  kind: (id) => kinds[id],
954
+ need: (id) => (kinds[id] ? 'view' : undefined),
798
955
  liveMs: (id) => (id === 'sandbox.status' || id === 'sessions.list' ? 2500 : id === 'logs.tail' ? 1000 : undefined),
799
956
  run: async (id, input, io) => id !== 'apps.request' && (id.startsWith('settings.') || id.startsWith('apps.') || id === 'files.browse')
800
957
  ? runDefinition(id, ctx, input, io)
@@ -1,12 +1,13 @@
1
1
  // Picking tool versions and release assets: Node/Go/Python/Java version rules, GitHub release pages, PowerShell 7,
2
- // cargo-binstall, the small CLI tools (gh, zstd, openssl), and when PowerShell 7 is wanted.
2
+ // cargo-binstall, the small CLI tools (gh, zstd, openssl), when PowerShell 7 is wanted, and the apps' agent add-ons.
3
+ import fs from 'node:fs';
3
4
  import path from 'node:path';
4
5
  import { javaMajorFrom, pickGoRelease, pickNodeVersion, pickPackageVersion, pickSpreadVersions, projectUses, specFromVersionFile, } from '../core/detect/index.js';
5
6
  import { parseReleasePage, parseReleaseTags } from '../shared/download/index.js';
6
7
  import { decidePwsh, pcHasPwsh, PWSH_MAX_BYTES, readPwshScripts, usesPwsh } from '../core/detect/index.js';
7
8
  import { binstallAsset, CLI_TOOLS, decideCli, pwshAsset } from '../guest/toolchains/catalogue.js';
8
9
  import { missingExtensions, vscodeExtensions, vscodeSettings } from '../guest/toolchains/tools/vscode-profile.js';
9
- import { APP_CATEGORIES, APPS, appsNeedMesa, appsToInstall, exeMatcher } from '../shared/apps/index.js';
10
+ import { addonGuideLine, addonMcpServer, APP_CATEGORIES, APPS, appsNeedMesa, appsNeedUv, appsToInstall, CCUSAGE_VERSION, claudeManagedSettings, CONTEXT7_MCP_VERSION, exeMatcher, HEADROOM_VERSION, headroomArgs, headroomEnv, headroomUrl, PLAYWRIGHT_MCP_VERSION, RTK_VERSION, withAddonMcpServers, } from '../shared/apps/index.js';
10
11
  import { ConfigSchema, LocalOverridesSchema } from '../core/schema/index.js';
11
12
  export const toolchainsSuite = {
12
13
  name: 'toolchains',
@@ -236,5 +237,149 @@ export const toolchainsSuite = {
236
237
  !custom({ name: 'E', github: 'o/r', asset: 'x', url: 'https://x.org', exe: 'e.exe' }), 'apps: your own program comes from a GitHub release or an https link; plain http, a broken pattern or both are refused');
237
238
  t.check(!LocalOverridesSchema.safeParse({ apps: { custom: {} } }).success &&
238
239
  LocalOverridesSchema.safeParse({ apps: { install: ['jq'] } }).success, 'apps: a project file may add catalogue apps but never programs of its own');
240
+ t.section('agent add-ons');
241
+ const rtk = APPS.find((a) => a.id === 'rtk');
242
+ const rtkSource = rtk?.source;
243
+ t.check(rtk?.category === 'agents' &&
244
+ rtk.cli === true &&
245
+ rtk.userOnly === true &&
246
+ rtkSource !== undefined &&
247
+ 'url' in rtkSource &&
248
+ rtkSource.url.startsWith('https://github.com/rtk-ai/rtk/releases/download/v' + RTK_VERSION + '/') &&
249
+ rtkSource.url.endsWith('-x86_64-pc-windows-msvc.zip') &&
250
+ /^[0-9a-f]{64}$/.test(rtkSource.sha256 ?? ''), 'rtk: a pinned https release file of rtk-ai/rtk with its SHA-256, on PATH, in your own settings only');
251
+ t.check(APPS.every((a) => !('url' in a.source) || a.source.url.startsWith('https://')) &&
252
+ APPS.every((a) => !('url' in a.source) || a.source.sha256 === undefined || /^[0-9a-f]{64}$/.test(a.source.sha256)), 'apps: every direct link is https, and a pinned checksum is 64 hex characters');
253
+ const agentApps = APPS.filter((a) => a.category === 'agents').map((a) => a.id);
254
+ t.check(APP_CATEGORIES.some((c) => c.id === 'agents' && c.label === 'Agent add-ons') &&
255
+ agentApps.includes('rtk') &&
256
+ agentApps.includes('claude-mem') &&
257
+ APPS.find((a) => a.id === 'claude-mem')?.setting === 'claudeProfile.syncClaudeMem', 'apps: the Agent add-ons category holds rtk and claude-mem (claude-mem through its own setting)');
258
+ t.check(!LocalOverridesSchema.safeParse({ apps: { install: ['rtk'] } }).success &&
259
+ !LocalOverridesSchema.safeParse({ apps: { install: ['claude-mem'] } }).success &&
260
+ ConfigSchema.shape.apps.shape.install.safeParse(['rtk']).success, 'rtk: only your own settings turn it on; a project file cannot');
261
+ const exe = 'C:\\AgentData\\tools\\apps\\rtk\\rtk.exe';
262
+ const withRtk = claudeManagedSettings([], exe);
263
+ const both = claudeManagedSettings(['Bash(git push:*)'], exe);
264
+ const gitOnly = claudeManagedSettings(['Bash(git push:*)'], null);
265
+ t.equal(withRtk?.hooks?.PreToolUse, [{ matcher: 'Bash', hooks: [{ type: 'command', command: '"C:/AgentData/tools/apps/rtk/rtk.exe" hook claude' }] }], "rtk: its documented Bash PreToolUse hook goes into the sandbox's managed settings");
266
+ t.check(claudeManagedSettings([], null) === null &&
267
+ gitOnly?.hooks === undefined &&
268
+ gitOnly?.permissions?.deny.length === 1 &&
269
+ both?.permissions?.deny.length === 1 &&
270
+ both.hooks?.PreToolUse.length === 1 &&
271
+ withRtk?.permissions === undefined, 'rtk: no hook when it is off; the git deny rules stay beside it; nothing to write when neither');
272
+ t.section('agent add-ons: Headroom, ccusage, Playwright MCP, Context7 MCP');
273
+ const pinned = {
274
+ headroom: ['uv', 'headroom-ai', HEADROOM_VERSION],
275
+ ccusage: ['npm', 'ccusage', CCUSAGE_VERSION],
276
+ 'playwright-mcp': ['npm', '@playwright/mcp', PLAYWRIGHT_MCP_VERSION],
277
+ context7: ['npm', '@upstash/context7-mcp', CONTEXT7_MCP_VERSION],
278
+ };
279
+ for (const [id, [kind, pkg, version]] of Object.entries(pinned)) {
280
+ const entry = APPS.find((a) => a.id === id);
281
+ const source = entry?.source;
282
+ t.check(entry?.category === 'agents' &&
283
+ entry.userOnly === true &&
284
+ source !== undefined &&
285
+ 'locked' in source &&
286
+ source.locked === kind &&
287
+ source.package === pkg &&
288
+ source.version === version &&
289
+ /^\d+\.\d+\.\d+$/.test(version) &&
290
+ !LocalOverridesSchema.safeParse({ apps: { install: [id] } }).success &&
291
+ ConfigSchema.shape.apps.shape.install.safeParse([id]).success, id + ': in Agent add-ons, a pinned ' + kind + ' package (' + pkg + ' ' + version + '), your own settings only');
292
+ const lockDir = path.join(t.packageRoot, 'sandbox', 'addons', id);
293
+ if (kind === 'npm') {
294
+ const manifest = JSON.parse(fs.readFileSync(path.join(lockDir, 'package.json'), 'utf8'));
295
+ const lock = JSON.parse(fs.readFileSync(path.join(lockDir, 'package-lock.json'), 'utf8'));
296
+ const deps = Object.entries(lock.packages).filter(([key]) => key !== '');
297
+ t.check(manifest.dependencies[pkg] === version &&
298
+ Object.keys(manifest.dependencies).length === 1 &&
299
+ lock.lockfileVersion === 3 &&
300
+ lock.packages['node_modules/' + pkg]?.version === version &&
301
+ deps.length > 0 &&
302
+ deps.every(([, p]) => !p.link &&
303
+ (p.resolved ?? '').startsWith('https://registry.npmjs.org/') &&
304
+ /^sha512-[A-Za-z0-9+/]+=*$/.test(p.integrity ?? '')), id + ': its lock file pins ' + pkg + ' ' + version + '; every package comes from npm with a sha512 integrity');
305
+ }
306
+ else {
307
+ const lines = fs.readFileSync(path.join(lockDir, 'requirements.txt'), 'utf8').split(/\r?\n/);
308
+ const requirements = lines.filter((l) => /^[A-Za-z0-9]/.test(l));
309
+ const blocks = lines
310
+ .join('\n')
311
+ .split(/\n(?=[A-Za-z0-9])/)
312
+ .slice(1);
313
+ t.check(requirements.includes(pkg + '==' + version + ' \\') &&
314
+ requirements.length > 10 &&
315
+ requirements.every((l) => /^[A-Za-z0-9._-]+(\[[^\]]+\])?==[^\s]+ \\$/.test(l)) &&
316
+ blocks.every((b) => /--hash=sha256:[0-9a-f]{64}/.test(b)) &&
317
+ !lines.some((l) => /^\s*(-e|--index-url|--extra-index-url|-f|--find-links|git\+|https?:)/.test(l.trim())), id + ': its requirements.txt pins every package to one version with SHA-256 hashes, from PyPI only');
318
+ }
319
+ }
320
+ t.check(appsNeedUv({ install: ['headroom'], custom: {} }) &&
321
+ !appsNeedUv({ install: ['ccusage', 'rtk'], custom: {} }) &&
322
+ !appsNeedUv(undefined), 'headroom: choosing it brings the uv step (it installs with uv)');
323
+ const services = APPS.filter((a) => a.service).map((a) => a.id);
324
+ t.check(['headroom', 'playwright-mcp', 'context7'].every((id) => services.includes(id)) &&
325
+ APPS.find((a) => a.id === 'ccusage')?.cli === true &&
326
+ !services.includes('ccusage'), 'add-ons: the proxy and MCP servers get no shortcut and no PATH entry; ccusage is a command on PATH');
327
+ // Headroom: loopback only, Anthropic only, no telemetry, and none of the agents' secrets in its environment.
328
+ const args = headroomArgs();
329
+ t.check(args[0] === 'proxy' &&
330
+ args[args.indexOf('--host') + 1] === '127.0.0.1' &&
331
+ args[args.indexOf('--port') + 1] === '8787' &&
332
+ args[args.indexOf('--anthropic-api-url') + 1] === 'https://api.anthropic.com' &&
333
+ args.includes('--no-telemetry') &&
334
+ headroomUrl() === 'http://127.0.0.1:8787', 'headroom: the proxy listens on 127.0.0.1 only and forwards only to api.anthropic.com, telemetry off');
335
+ const proxyEnv = headroomEnv({
336
+ PATH: 'C:\\Windows',
337
+ ANTHROPIC_API_KEY: 'sk-ant-x',
338
+ CLAUDE_CODE_OAUTH_TOKEN: 'oauth',
339
+ GITHUB_TOKEN: 'ghp',
340
+ OPENAI_API_KEY: 'sk',
341
+ DB_PASSWORD: 'pw',
342
+ HTTPS_PROXY: 'http://127.0.0.1:3128',
343
+ HEADROOM_OFFLINE: '0',
344
+ });
345
+ t.check(proxyEnv.PATH === 'C:\\Windows' &&
346
+ proxyEnv.HTTPS_PROXY === 'http://127.0.0.1:3128' &&
347
+ !Object.values(proxyEnv).some((v) => ['sk-ant-x', 'oauth', 'ghp', 'sk', 'pw'].includes(v)) &&
348
+ proxyEnv.HEADROOM_OFFLINE === '1' &&
349
+ proxyEnv.HEADROOM_TELEMETRY === 'off' &&
350
+ proxyEnv.HEADROOM_UPDATE_CHECK === 'off', 'headroom: starts without keys or tokens, offline apart from forwarding (no telemetry, updates or downloads)');
351
+ const routed = claudeManagedSettings([], null, headroomUrl());
352
+ t.check(routed?.env?.ANTHROPIC_BASE_URL === 'http://127.0.0.1:8787' &&
353
+ claudeManagedSettings([], exe)?.env === undefined &&
354
+ claudeManagedSettings(['Bash(git push:*)'], null)?.env === undefined &&
355
+ claudeManagedSettings(['Bash(git push:*)'], exe, headroomUrl())?.hooks?.PreToolUse.length === 1, "headroom: Claude Code is pointed at the proxy only while it runs (the sandbox's managed settings env)");
356
+ // MCP servers: set while on, removed when off, your own servers never touched.
357
+ const playwright = APPS.find((a) => a.id === 'playwright-mcp')?.mcp;
358
+ const context7 = APPS.find((a) => a.id === 'context7')?.mcp;
359
+ const known = APPS.flatMap((a) => (a.mcp ? [a.mcp.name] : []));
360
+ const pw = playwright ? addonMcpServer('C:\\AgentData\\tools\\apps\\playwright-mcp', playwright) : null;
361
+ t.equal(pw, {
362
+ type: 'stdio',
363
+ command: 'node',
364
+ args: [
365
+ 'C:\\AgentData\\tools\\apps\\playwright-mcp\\node_modules\\@playwright\\mcp\\cli.js',
366
+ '--browser',
367
+ 'msedge',
368
+ ],
369
+ env: {},
370
+ }, 'playwright-mcp: a stdio MCP server run by node from its own folder, with Edge');
371
+ const mine = { playwright: { type: 'stdio', command: 'npx', args: ['@playwright/mcp'] } };
372
+ const on = withAddonMcpServers(mine, pw ? { 'sbx-playwright': pw } : {}, known);
373
+ const off = withAddonMcpServers({ ...on, 'sbx-context7': {} }, {}, known);
374
+ t.check(known.length === 2 &&
375
+ known.every((n) => n.startsWith('sbx-')) &&
376
+ context7?.name === 'sbx-context7' &&
377
+ 'sbx-playwright' in on &&
378
+ 'playwright' in on &&
379
+ Object.keys(off).join() === 'playwright' &&
380
+ withAddonMcpServers({ 'sbx-other': 1 }, {}, known)['sbx-other'] === 1, 'add-ons: their sbx-... MCP servers are set while on and removed when off; servers of your own stay');
381
+ t.check(addonGuideLine([], false).length === 0 &&
382
+ addonGuideLine(['sbx-playwright'], true).length === 2 &&
383
+ addonGuideLine(['sbx-playwright'], false)[0].includes('sbx-playwright'), 'add-ons: the agent guide names the MCP servers and ccusage only when they are on');
239
384
  },
240
385
  };
@@ -105,6 +105,7 @@ function fakeRpc() {
105
105
  const host = {
106
106
  kind: (id) => (id === 'test.work' ? 'operation' : id === 'test.live' ? 'query' : undefined),
107
107
  liveMs: (id) => (id === 'test.live' ? 50 : undefined),
108
+ need: (id) => (id === 'test.work' ? 'control' : id === 'test.live' ? 'view' : undefined),
108
109
  run: async (id, input, io) => {
109
110
  ran.push(id);
110
111
  if (id === 'test.live')
@@ -216,6 +217,7 @@ export const webSuite = {
216
217
  let reads = 0;
217
218
  const host = {
218
219
  kind: () => 'query',
220
+ need: () => 'view',
219
221
  liveMs: () => 40,
220
222
  run: async () => {
221
223
  reads++;
@@ -0,0 +1,64 @@
1
+ import type { AddonMcp } from './catalogue.js';
2
+ /** One hook command in Claude Code's settings. */
3
+ interface ClaudeHook {
4
+ readonly type: 'command';
5
+ readonly command: string;
6
+ }
7
+ /** Claude Code's managed-settings.json as the sandbox writes it. */
8
+ export interface ManagedSettings {
9
+ readonly permissions?: {
10
+ readonly deny: readonly string[];
11
+ };
12
+ readonly hooks?: {
13
+ readonly PreToolUse: readonly {
14
+ readonly matcher: string;
15
+ readonly hooks: readonly ClaudeHook[];
16
+ }[];
17
+ };
18
+ readonly env?: Readonly<Record<string, string>>;
19
+ }
20
+ /** The command Claude Code runs for rtk's hook: the program by its full path (quoted, forward slashes for Git Bash). */
21
+ export declare function rtkHookCommand(exe: string): string;
22
+ /**
23
+ * The managed settings for the git deny rules, rtk's Bash hook when rtk is installed (`rtkExe`) and the Headroom proxy
24
+ * when it runs (`headroomUrl`). Null when there is nothing to write (the file is removed then).
25
+ */
26
+ export declare function claudeManagedSettings(deny: readonly string[], rtkExe: string | null, headroomUrl?: string | null): ManagedSettings | null;
27
+ /** The line every agent's sandbox guide gets while rtk is installed (Claude's commands go through it on their own). */
28
+ export declare const RTK_GUIDE_LINE = "- rtk is on PATH and compacts command output: Claude Code's Bash commands go through it on their own; in other agents prefix shell commands with `rtk` (`rtk git status`, `rtk cargo test`, `rtk ls`).";
29
+ /** Headroom listens here, on loopback only (its default port). */
30
+ export declare const HEADROOM_PORT = 8787;
31
+ /** The only upstream Headroom forwards to: Anthropic's API, set on its command line. */
32
+ export declare const HEADROOM_UPSTREAM = "https://api.anthropic.com";
33
+ /** Claude Code's ANTHROPIC_BASE_URL while the proxy runs. */
34
+ export declare function headroomUrl(port?: number): string;
35
+ /** `headroom proxy` on loopback, forwarding only to Anthropic's API, with no telemetry. */
36
+ export declare function headroomArgs(port?: number): string[];
37
+ /**
38
+ * Headroom's own switches: HEADROOM_OFFLINE refuses every connection Headroom would make on its own (telemetry, update
39
+ * checks, model downloads, usage polling) and keeps only the forwarding of the agent's requests.
40
+ */
41
+ export declare const HEADROOM_ENV: Readonly<Record<string, string>>;
42
+ /** The environment Headroom starts with: the sandbox's, minus anything secret, plus its own switches. */
43
+ export declare function headroomEnv(base: Readonly<Record<string, string | undefined>>): Record<string, string>;
44
+ /** The guide line while the proxy runs. */
45
+ export declare function headroomGuideLine(logFile: string): string;
46
+ /** A user-scope MCP server in Claude Code's .claude.json. */
47
+ export interface McpServerConfig {
48
+ readonly type: 'stdio';
49
+ readonly command: string;
50
+ readonly args: readonly string[];
51
+ readonly env: Readonly<Record<string, string>>;
52
+ }
53
+ /** The name prefix of the sandbox's own MCP servers: names with it are the sandbox's to add and remove. */
54
+ export declare const ADDON_MCP_PREFIX = "sbx-";
55
+ /** The MCP server of an installed add-on: `node <folder>\<script> <args>` (Node is on PATH in every sandbox). */
56
+ export declare function addonMcpServer(folder: string, mcp: AddonMcp): McpServerConfig;
57
+ /**
58
+ * The user-scope MCP servers with the add-ons' servers set: every sbx-... server that `known` lists and that is not in
59
+ * `servers` is removed (an add-on turned off); servers of your own are never touched.
60
+ */
61
+ export declare function withAddonMcpServers(current: Readonly<Record<string, unknown>>, servers: Readonly<Record<string, McpServerConfig>>, known: readonly string[]): Record<string, unknown>;
62
+ /** The guide line for the add-ons' MCP servers and tools (none: no line). */
63
+ export declare function addonGuideLine(mcpNames: readonly string[], ccusage: boolean): string[];
64
+ export {};
@@ -0,0 +1,95 @@
1
+ /** The command Claude Code runs for rtk's hook: the program by its full path (quoted, forward slashes for Git Bash). */
2
+ export function rtkHookCommand(exe) {
3
+ return '"' + exe.replace(/\\/g, '/') + '" hook claude';
4
+ }
5
+ /**
6
+ * The managed settings for the git deny rules, rtk's Bash hook when rtk is installed (`rtkExe`) and the Headroom proxy
7
+ * when it runs (`headroomUrl`). Null when there is nothing to write (the file is removed then).
8
+ */
9
+ export function claudeManagedSettings(deny, rtkExe, headroomUrl = null) {
10
+ if (!deny.length && !rtkExe && !headroomUrl)
11
+ return null;
12
+ return {
13
+ ...(deny.length ? { permissions: { deny } } : {}),
14
+ ...(rtkExe
15
+ ? { hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [{ type: 'command', command: rtkHookCommand(rtkExe) }] }] } }
16
+ : {}),
17
+ ...(headroomUrl ? { env: { ANTHROPIC_BASE_URL: headroomUrl } } : {}),
18
+ };
19
+ }
20
+ /** The line every agent's sandbox guide gets while rtk is installed (Claude's commands go through it on their own). */
21
+ export const RTK_GUIDE_LINE = "- rtk is on PATH and compacts command output: Claude Code's Bash commands go through it on their own; in other agents prefix shell commands with `rtk` (`rtk git status`, `rtk cargo test`, `rtk ls`).";
22
+ /** Headroom listens here, on loopback only (its default port). */
23
+ export const HEADROOM_PORT = 8787;
24
+ /** The only upstream Headroom forwards to: Anthropic's API, set on its command line. */
25
+ export const HEADROOM_UPSTREAM = 'https://api.anthropic.com';
26
+ /** Claude Code's ANTHROPIC_BASE_URL while the proxy runs. */
27
+ export function headroomUrl(port = HEADROOM_PORT) {
28
+ return 'http://127.0.0.1:' + port;
29
+ }
30
+ /** `headroom proxy` on loopback, forwarding only to Anthropic's API, with no telemetry. */
31
+ export function headroomArgs(port = HEADROOM_PORT) {
32
+ return [
33
+ 'proxy',
34
+ '--host',
35
+ '127.0.0.1',
36
+ '--port',
37
+ String(port),
38
+ '--anthropic-api-url',
39
+ HEADROOM_UPSTREAM,
40
+ '--no-telemetry',
41
+ ];
42
+ }
43
+ /**
44
+ * Headroom's own switches: HEADROOM_OFFLINE refuses every connection Headroom would make on its own (telemetry, update
45
+ * checks, model downloads, usage polling) and keeps only the forwarding of the agent's requests.
46
+ */
47
+ export const HEADROOM_ENV = {
48
+ HEADROOM_OFFLINE: '1',
49
+ HEADROOM_TELEMETRY: 'off',
50
+ HEADROOM_BEACON: 'off',
51
+ HEADROOM_UPDATE_CHECK: 'off',
52
+ DO_NOT_TRACK: '1',
53
+ HF_HUB_OFFLINE: '1',
54
+ PYTHONIOENCODING: 'utf-8',
55
+ };
56
+ /** Variables the proxy never gets: keys, tokens and passwords of the agents (it only forwards what Claude sends). */
57
+ const SECRET_NAME = /(KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)/i;
58
+ /** The environment Headroom starts with: the sandbox's, minus anything secret, plus its own switches. */
59
+ export function headroomEnv(base) {
60
+ const env = {};
61
+ for (const [name, value] of Object.entries(base))
62
+ if (value !== undefined && !SECRET_NAME.test(name) && !/^(ANTHROPIC|OPENAI|CLAUDE)_/i.test(name))
63
+ env[name] = value;
64
+ return { ...env, ...HEADROOM_ENV };
65
+ }
66
+ /** The guide line while the proxy runs. */
67
+ export function headroomGuideLine(logFile) {
68
+ return ("- Headroom compresses Claude Code's requests: Claude Code reaches the API through its proxy on " +
69
+ headroomUrl() +
70
+ ' (log: `' +
71
+ logFile +
72
+ '`). If Claude Code cannot reach the API, the proxy stopped: it starts again with the next sandbox, or turn Headroom off in Apps.');
73
+ }
74
+ /** The name prefix of the sandbox's own MCP servers: names with it are the sandbox's to add and remove. */
75
+ export const ADDON_MCP_PREFIX = 'sbx-';
76
+ /** The MCP server of an installed add-on: `node <folder>\<script> <args>` (Node is on PATH in every sandbox). */
77
+ export function addonMcpServer(folder, mcp) {
78
+ const script = folder.replace(/[\\/]+$/, '') + '\\' + mcp.script.replace(/\//g, '\\');
79
+ return { type: 'stdio', command: 'node', args: [script, ...(mcp.args ?? [])], env: {} };
80
+ }
81
+ /**
82
+ * The user-scope MCP servers with the add-ons' servers set: every sbx-... server that `known` lists and that is not in
83
+ * `servers` is removed (an add-on turned off); servers of your own are never touched.
84
+ */
85
+ export function withAddonMcpServers(current, servers, known) {
86
+ const kept = Object.fromEntries(Object.entries(current).filter(([name]) => !known.includes(name) || name in servers));
87
+ return { ...kept, ...servers };
88
+ }
89
+ /** The guide line for the add-ons' MCP servers and tools (none: no line). */
90
+ export function addonGuideLine(mcpNames, ccusage) {
91
+ return [
92
+ ...(mcpNames.length ? ['- MCP servers in Claude Code from Apps: ' + mcpNames.join(', ') + '.'] : []),
93
+ ...(ccusage ? ['- ccusage is on PATH: `ccusage daily` (or `monthly`, `session`) reports token use and cost.'] : []),
94
+ ];
95
+ }