windows-development-sandbox 1.27.3 → 1.29.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 (125) hide show
  1. package/README.md +5 -4
  2. package/dist/app/features/sandbox/sandbox.run.js +1 -1
  3. package/dist/bin/{chunk-FA5VWLP4.js → chunk-O4EL32TM.js} +1 -1
  4. package/dist/bin/{chunk-RU7XRN62.js → chunk-QYRRWGFY.js} +1637 -1354
  5. package/dist/bin/sandbox.js +10772 -3
  6. package/dist/cli/commands/launch/launch.js +1 -1
  7. package/dist/cli/commands/launch/running.js +2 -2
  8. package/dist/cli/index.js +1 -1
  9. package/dist/cli/menu/pages/agents-page.js +1 -1
  10. package/dist/cli/services/sandbox-service.js +1 -1
  11. package/dist/core/schema/sandbox.js +2 -2
  12. package/dist/guest/apps/app-installer.js +1 -1
  13. package/dist/guest/apps/store-check.d.ts +2 -0
  14. package/dist/guest/apps/store-check.js +3 -1
  15. package/dist/guest/boot/guide-lines.d.ts +2 -0
  16. package/dist/guest/boot/guide-lines.js +7 -0
  17. package/dist/guest/boot/guide.js +2 -1
  18. package/dist/guest/daemon/heartbeat.js +1 -0
  19. package/dist/guest/governor/core.d.ts +53 -1
  20. package/dist/guest/governor/core.js +95 -1
  21. package/dist/guest/governor/decide.d.ts +4 -0
  22. package/dist/guest/governor/decide.js +2 -0
  23. package/dist/guest/governor/governor.js +23 -9
  24. package/dist/guest/toolchains/runtimes.js +7 -6
  25. package/dist/host/accounts/deliver.js +16 -2
  26. package/dist/host/apps/store-package.d.ts +2 -0
  27. package/dist/host/apps/store-package.js +16 -15
  28. package/dist/host/channel/attach.js +1 -1
  29. package/dist/host/channel/channel.js +3 -3
  30. package/dist/host/guard/commands.js +1 -0
  31. package/dist/host/health/path-copies.d.ts +1 -1
  32. package/dist/host/health/path-copies.js +2 -2
  33. package/dist/host/install/index.d.ts +1 -1
  34. package/dist/host/install/index.js +1 -1
  35. package/dist/host/install/shims.d.ts +8 -1
  36. package/dist/host/install/shims.js +39 -1
  37. package/dist/host/launch/generate.js +4 -0
  38. package/dist/host/launch/index.d.ts +1 -0
  39. package/dist/host/launch/index.js +1 -0
  40. package/dist/host/launch/pc-logs.d.ts +6 -0
  41. package/dist/host/launch/pc-logs.js +129 -0
  42. package/dist/host/life/crash-report.d.ts +4 -0
  43. package/dist/host/life/crash-report.js +30 -1
  44. package/dist/host/life/last-end.js +1 -0
  45. package/dist/host/sandbox/platform.js +1 -1
  46. package/dist/host/sandbox/window.js +1 -1
  47. package/dist/host/tools/windows-terminal.d.ts +4 -1
  48. package/dist/host/tools/windows-terminal.js +25 -9
  49. package/dist/runtime/agent.js +6 -2
  50. package/dist/runtime/daemon.js +111 -16
  51. package/dist/runtime/db.js +6 -2
  52. package/dist/runtime/main.js +199 -35
  53. package/dist/runtime/netproxy.js +6 -2
  54. package/dist/runtime/tool.js +6 -2
  55. package/dist/runtime/view.js +126 -28
  56. package/dist/selftest/agent-paths.test.js +3 -1
  57. package/dist/selftest/channel.test.js +33 -0
  58. package/dist/selftest/cli.test.js +5 -5
  59. package/dist/selftest/config.test.js +1 -1
  60. package/dist/selftest/credentials.test.js +1 -1
  61. package/dist/selftest/engine.test.js +1 -1
  62. package/dist/selftest/generate.test.js +66 -1
  63. package/dist/selftest/hcs.test.js +1 -1
  64. package/dist/selftest/health.test.js +21 -10
  65. package/dist/selftest/heavy-load.test.js +112 -4
  66. package/dist/selftest/host-sim.test.js +13 -14
  67. package/dist/selftest/install.test.js +6 -2
  68. package/dist/selftest/live-terminal.test.js +103 -0
  69. package/dist/selftest/personalization.test.js +2 -1
  70. package/dist/selftest/store-app.test.js +62 -4
  71. package/dist/selftest/support/engine-golden.js +2 -0
  72. package/dist/selftest/support/web-preview.js +2 -2
  73. package/dist/shared/accounts/kinds.d.ts +5 -0
  74. package/dist/shared/accounts/kinds.js +8 -0
  75. package/dist/shared/apps/index.d.ts +1 -1
  76. package/dist/shared/apps/index.js +1 -1
  77. package/dist/shared/apps/selection.d.ts +1 -0
  78. package/dist/shared/apps/selection.js +4 -3
  79. package/dist/shared/apps/store-package.d.ts +13 -0
  80. package/dist/shared/apps/store-package.js +24 -0
  81. package/dist/shared/channel/schema.d.ts +2 -0
  82. package/dist/shared/channel/schema.js +4 -0
  83. package/dist/shared/memory/policy.d.ts +12 -0
  84. package/dist/shared/memory/policy.js +4 -0
  85. package/dist/shared/pty/clipboard-watch.d.ts +4 -0
  86. package/dist/shared/pty/clipboard-watch.js +40 -0
  87. package/dist/shared/pty/ctrl-c.d.ts +13 -7
  88. package/dist/shared/pty/ctrl-c.js +35 -19
  89. package/dist/shared/pty/index.d.ts +1 -0
  90. package/dist/shared/pty/index.js +1 -0
  91. package/dist/shared/pty/terminal.d.ts +1 -1
  92. package/dist/shared/pty/terminal.js +16 -13
  93. package/dist/shared/web/ctrl-c.d.ts +26 -0
  94. package/dist/shared/web/ctrl-c.js +64 -0
  95. package/dist/shared/web/index.d.ts +1 -0
  96. package/dist/shared/web/index.js +1 -0
  97. package/dist/web/index.html +1 -1
  98. package/dist/web/{main-NKM7ECSB.js → main-7NY4X6YU.js} +67 -67
  99. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  100. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  101. package/native/csharp/HostTools/Window.cs +1 -1
  102. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  103. package/native/scripts/get-store-package.ps1 +24 -13
  104. package/package.json +4 -14
  105. package/sandbox/csharp/ClipboardWatch.cs +59 -0
  106. package/sandbox/csharp/MemGov.cs +752 -7
  107. package/sandbox/scripts/install-store-package.ps1 +11 -4
  108. package/sandbox/scripts/watch-clipboard.ps1 +8 -0
  109. package/schemas/config.schema.json +2 -2
  110. package/schemas/sandbox-config.schema.json +1 -1
  111. package/dist/bin/agent-sandbox.d.ts +0 -2
  112. package/dist/bin/agent-sandbox.js +0 -9
  113. package/dist/bin/chunk-4M5SZZ5P.js +0 -10778
  114. package/dist/bin/claude-host.d.ts +0 -2
  115. package/dist/bin/claude-host.js +0 -11
  116. package/dist/bin/claude-sbx.d.ts +0 -2
  117. package/dist/bin/claude-sbx.js +0 -9
  118. package/dist/bin/codex-sbx.d.ts +0 -2
  119. package/dist/bin/codex-sbx.js +0 -9
  120. package/dist/bin/gemini-sbx.d.ts +0 -2
  121. package/dist/bin/gemini-sbx.js +0 -9
  122. package/dist/bin/sbx-hide.d.ts +0 -2
  123. package/dist/bin/sbx-hide.js +0 -9
  124. package/dist/bin/sbx-show.d.ts +0 -2
  125. package/dist/bin/sbx-show.js +0 -9
@@ -8,7 +8,7 @@ import { DEFAULTS } from '../core/config/index.js';
8
8
  import { isWin } from '../core/util/index.js';
9
9
  import { SYSTEM } from '../shared/system.js';
10
10
  import { GuestRequestSchema, PressureStatusSchema } from '../shared/channel/index.js';
11
- import { compilersAllowed, DIAL_CALM_SAMPLES, DIAL_STEPS, HOST_THRESHOLDS, buildJobs, cargoJobs, linkSlotsFor, usableCpus, effectiveCpuLimit, gentleCpuLimit, maxCompilers, chooseTargetDir, cargoLinkerVar, } from '../shared/memory/index.js';
11
+ import { BURST_GATE, compilersAllowed, DIAL_CALM_SAMPLES, DIAL_STEPS, HOST_THRESHOLDS, buildJobs, cargoJobs, linkSlotsFor, usableCpus, effectiveCpuLimit, gentleCpuLimit, maxCompilers, chooseTargetDir, cargoLinkerVar, } from '../shared/memory/index.js';
12
12
  import { HeartbeatSchema } from '../shared/channel/index.js';
13
13
  import { heartbeatLine, lastEndOf, vanishedCause, stopWarning } from '../host/life/index.js';
14
14
  import { cappedMemoryMb, decideMemory, memoryNote, residentCapMb, residentNote, resolveMemoryMb, } from '../host/launch/index.js';
@@ -16,10 +16,10 @@ import { vgpuWarning, GENTLE_NOTE } from '../host/launch/risk-notes.js';
16
16
  import { judgeSandbox } from '../host/health/index.js';
17
17
  import { cleanUpLeftovers, cleanupQuestion, killArgs } from '../host/sandbox/index.js';
18
18
  import { parseTasklist } from '../host/sharing/index.js';
19
- import { bugcheckCode, cpuPercentBetween, explainStop, findDumps, findingLines, freezeLine, hostPressure, lowReason, severeHost, pageFileRisk, parseHostMemory, crashReportText, watchdogStep, WATCHDOG_START, } from '../host/life/index.js';
19
+ import { bugcheckCode, cpuPercentBetween, explainStop, findDumps, findingLines, freezeLine, hostPressure, lowReason, memoryWasFineLine, severeHost, pageFileRisk, parseHostMemory, crashReportText, watchdogStep, WATCHDOG_START, } from '../host/life/index.js';
20
20
  // The pure governor files only: governor.ts loads the guest runtime (session.json), which exists only in a sandbox.
21
- import { canLower, canPause, decide, INITIAL_STATE, isLinker, linkerSlots, ROTATE_MS, } from '../guest/governor/decide.js';
22
- import { GovernorCore } from '../guest/governor/core.js';
21
+ import { canLower, canPause, decide, GATE_NAMES, INITIAL_STATE, isLinker, isNeverTouched, linkerSlots, ROTATE_MS, } from '../guest/governor/decide.js';
22
+ import { GateEventSchema, GovernorCore, memgovArgs } from '../guest/governor/core.js';
23
23
  const GB = 1024;
24
24
  const MB_SANDBOX = DEFAULTS.sandbox;
25
25
  const proc = (pid, name, mb, prio = 'normal') => ({
@@ -374,6 +374,105 @@ export const heavyLoadSuite = {
374
374
  t.check(decide({ ...INITIAL_STATE, hostUntil: clock + 60000, hostStep: 4 }, sample(60, 40, procs), clock, 'normal').state
375
375
  .step === 4, 'with plenty of memory in the sandbox, a severe PC still turns the dial to 4');
376
376
  t.check(core.current.hostUntil === clock + 60000, 'a host request is remembered for its seconds');
377
+ t.section('the memory gate (rate of growth)');
378
+ t.check(['cl', 'link', 'rustc', 'clang-cl', 'lld-link', 'shadercompileworker'].every((n) => GATE_NAMES.includes(n)), 'the gate may freeze compilers, linkers and Unreal shader workers', GATE_NAMES.join(','));
379
+ t.check([
380
+ 'node',
381
+ 'claude',
382
+ 'codex',
383
+ 'opencode',
384
+ 'cmd',
385
+ 'pwsh',
386
+ 'powershell',
387
+ 'msbuild',
388
+ 'dotnet',
389
+ 'cargo',
390
+ 'ninja',
391
+ 'unrealbuildtool',
392
+ 'ubahost',
393
+ 'unrealeditor',
394
+ 'unrealeditor-cmd',
395
+ 'mspdbsrv',
396
+ 'ptyhost',
397
+ 'memgov',
398
+ ].every((n) => !GATE_NAMES.includes(n)) &&
399
+ GATE_NAMES.every((n) => (canPause(n) || n === 'shadercompileworker') && !isNeverTouched(n)), 'agents, shells, build coordinators, the editor and the tools themselves are never frozen');
400
+ t.check(JSON.stringify(memgovArgs('normal')) ===
401
+ JSON.stringify([
402
+ 'serve',
403
+ '--interval',
404
+ '2',
405
+ '--gate',
406
+ '4096:3000:1500:30000',
407
+ '--gate-names',
408
+ GATE_NAMES.join(','),
409
+ ]) && memgovArgs('gentle').includes('3072:3000:2000:30000'), 'memgov starts with the gate for the profile', memgovArgs('gentle').join(' '));
410
+ t.check(['normal', 'gentle'].every((p) => {
411
+ const g = BURST_GATE[p];
412
+ return g.growthMb >= 1024 && g.maxHoldMs <= 30000 && g.releaseMs < g.maxHoldMs && g.windowMs >= 1000;
413
+ }) && BURST_GATE.gentle.growthMb <= BURST_GATE.normal.growthMb, 'the gate is sane: a real burst size, every hold ends within 30 s, vGPU is not looser');
414
+ const gateExamples = [
415
+ { event: 'gate', at: 1, kind: 'ready', mode: 'notify', growthMb: 4096, windowMs: 3000 },
416
+ { event: 'gate', at: 1, kind: 'off', reason: 'bad --gate' },
417
+ { event: 'gate', at: 1, kind: 'trip', grewMb: 4210, ms: 1800, jobMb: 23456, held: [{ pid: 101, name: 'cl' }] },
418
+ { event: 'gate', at: 1, kind: 'hold', pid: 101, name: 'cl' },
419
+ { event: 'gate', at: 1, kind: 'release', pid: 101, name: 'cl', heldMs: 1500, forced: false },
420
+ ];
421
+ const heldOf = (n) => Array.from({ length: n }, (_, i) => ({ pid: i, name: 'cl' }));
422
+ t.check(gateExamples.every((e) => GateEventSchema.safeParse(e).success) &&
423
+ !GateEventSchema.safeParse({ event: 'gate', at: 1, kind: 'hold', pid: 1, name: 'x'.repeat(129) }).success &&
424
+ !GateEventSchema.safeParse({ event: 'gate', at: 1, kind: 'trip', grewMb: 1, ms: 1, jobMb: 1, held: heldOf(65) })
425
+ .success &&
426
+ !GateEventSchema.safeParse({ event: 'gate', at: 1, kind: 'boom' }).success &&
427
+ !GateEventSchema.safeParse({ at: 1, kind: 'hold', pid: 1, name: 'cl' }).success, 'gate events from memgov are checked: one of each kind passes; a long name, 65 held, a bad kind or no event fail');
428
+ {
429
+ const gateLogs = [];
430
+ const gateCore = new GovernorCore({
431
+ port,
432
+ profile: 'normal',
433
+ now: () => 0,
434
+ logLine: (l) => gateLogs.push(l),
435
+ saveStatus: () => undefined,
436
+ warn: (line) => gateLogs.push(line),
437
+ });
438
+ gateCore.onGate({
439
+ event: 'gate',
440
+ at: 1,
441
+ kind: 'trip',
442
+ grewMb: 4210,
443
+ ms: 1800,
444
+ jobMb: 23456,
445
+ held: [{ pid: 101, name: 'cl' }],
446
+ });
447
+ gateCore.onGate({
448
+ event: 'gate',
449
+ at: 2,
450
+ kind: 'release',
451
+ pid: 101,
452
+ name: 'cl',
453
+ heldMs: 30000,
454
+ forced: true,
455
+ });
456
+ t.check(gateLogs.some((l) => l.includes("governor: gate: the agents' programs grew 4210 MB in 1.8 s (now 23456 MB); holding 1: cl (101)")) &&
457
+ gateLogs.some((l) => l.endsWith('(the longest it may wait)')) &&
458
+ gateCore.current.step === 0, 'gate events go to the memory log and leave the dial alone', gateLogs.join('\n'));
459
+ const heldStatuses = [];
460
+ const heldCore = new GovernorCore({
461
+ port,
462
+ profile: 'normal',
463
+ now: () => 0,
464
+ logLine: () => undefined,
465
+ saveStatus: (s) => heldStatuses.push(s),
466
+ warn: () => undefined,
467
+ });
468
+ heldCore.onSample({ ...sample(60, 40, procs), gateHeld: 3 });
469
+ const heldStatus = heldStatuses[heldStatuses.length - 1];
470
+ t.check(PressureStatusSchema.safeParse(heldStatus).success &&
471
+ heldStatus.held === 3 &&
472
+ heldCore.snapshot?.held === 3, 'the number of held builds reaches the status file and the snapshot', JSON.stringify(heldStatus));
473
+ t.check(HeartbeatSchema.safeParse({ ...beat, held: 3 }).success &&
474
+ heartbeatLine({ ...beat, held: 3 }).includes('3 builds held by the memory gate'), 'the heartbeat carries the held builds into the crash report line');
475
+ }
377
476
  t.section("the PC's watchdog");
378
477
  const input = (memory, nowMs, cpu = 10, profile = 'normal') => ({
379
478
  memory,
@@ -541,6 +640,13 @@ export const heavyLoadSuite = {
541
640
  meaning.includes('froze, lost power') &&
542
641
  meaning.includes('hardware error') &&
543
642
  meaning.includes('vmwp.exe'), 'what each finding usually means, in plain words', meaning);
643
+ t.check(findingLines([ev(1000, 'Application Error', '2026-10-15T19:41:12Z', 'Faulting application name: vmwp.exe')])
644
+ .join('\n')
645
+ .includes('not a program inside the sandbox running out of memory'), 'a crash of vmwp.exe is named as a fault in Windows or a driver, not memory');
646
+ t.check(memoryWasFineLine('x\n2026-11-08T15:30:05.411Z | RAM free/total MB 42325/63170 | commit free/limit MB 52051/75458 | page file auto\n')?.includes('still had 41 GB of RAM and 50 GB') === true &&
647
+ memoryWasFineLine('2026-10-15T19:40:50Z | RAM free/total MB 900/32768 | commit free/limit MB 9000/40000') ===
648
+ null &&
649
+ memoryWasFineLine('') === null, 'the report says when this PC still had plenty of memory, and says nothing when it was low');
544
650
  const dumps = path.join(t.at('crash-dumps'), 'Minidump');
545
651
  fs.mkdirSync(dumps, { recursive: true });
546
652
  fs.writeFileSync(path.join(dumps, '101526-1.dmp'), 'x');
@@ -576,6 +682,7 @@ export const heavyLoadSuite = {
576
682
  };
577
683
  const sandboxCs = (n) => path.join(t.packageRoot, 'sandbox', 'csharp', n);
578
684
  build('ptyhost', [sandboxCs('PtyHost.cs')]);
685
+ build('clipboardwatch', [sandboxCs('ClipboardWatch.cs')], 'library');
579
686
  const memgov = build('memgov', [sandboxCs('MemGov.cs')]);
580
687
  const hostDir = path.join(t.packageRoot, 'native', 'csharp', 'HostTools');
581
688
  const tools = build('HostTools', fs
@@ -607,6 +714,7 @@ export const heavyLoadSuite = {
607
714
  // Not JSON: the check below fails.
608
715
  }
609
716
  t.check(sampleOk && served.status === 0, 'memgov serve prints a well-formed sample and ends when its input closes', firstLine);
717
+ t.check(!(served.stdout ?? '').split(/\r?\n/).some((l) => l.includes('"event":"gate"')), 'a memgov started without --gate never takes the job port (no gate events)');
610
718
  for (const script of ['memory-status.ps1', 'stop-events.ps1']) {
611
719
  const text = fs.readFileSync(path.join(t.packageRoot, 'native', 'scripts', script), 'utf8');
612
720
  t.check(text.startsWith('#Requires -Version 5.1'), script + ' says its PowerShell version');
@@ -27,9 +27,9 @@ const SANDBOX_ONLY_VARS = [
27
27
  'AGENT_SANDBOX_CONFIG',
28
28
  ];
29
29
  const PARALLEL = 2;
30
- const sandbox = (label, ...args) => ({ bin: 'agent-sandbox', label, args, config: true });
30
+ const sandbox = (label, ...args) => ({ bin: 'sandbox', label, args, config: true });
31
31
  const COMMANDS = [
32
- { bin: 'agent-sandbox', label: '--help', args: ['--help'] },
32
+ { bin: 'sandbox', label: '--help', args: ['--help'] },
33
33
  sandbox('status', 'status'),
34
34
  sandbox('status --json', 'status', '--json'),
35
35
  sandbox('sessions', 'sessions'),
@@ -38,26 +38,25 @@ const COMMANDS = [
38
38
  sandbox('vpn status', 'vpn', 'status'),
39
39
  sandbox('vpn status --json', 'vpn', 'status', '--json'),
40
40
  sandbox('account use nobody', 'account', 'use', 'nobody'),
41
- { bin: 'agent-sandbox', label: 'config path', args: ['config', 'path'] },
41
+ { bin: 'sandbox', label: 'config path', args: ['config', 'path'] },
42
42
  sandbox('config show', 'config', 'show'),
43
- { bin: 'agent-sandbox', label: 'config schema', args: ['config', 'schema'] },
43
+ { bin: 'sandbox', label: 'config schema', args: ['config', 'schema'] },
44
44
  sandbox('clean', 'clean'),
45
45
  sandbox('sync', 'sync'),
46
46
  sandbox('logs', 'logs'),
47
47
  sandbox('attach', 'attach'),
48
- { bin: 'agent-sandbox', label: 'exec', args: ['exec'], after: ['--', 'echo', 'hi'], config: true },
48
+ { bin: 'sandbox', label: 'exec', args: ['exec'], after: ['--', 'echo', 'hi'], config: true },
49
49
  sandbox('restart', 'restart'),
50
50
  sandbox('stop', 'stop'),
51
51
  sandbox('show', 'show'),
52
52
  sandbox('hide', 'hide'),
53
53
  sandbox('generate', 'generate'),
54
54
  sandbox('launch', 'launch'),
55
- { bin: 'agent-sandbox', label: 'unknown command', args: ['frobnicate'] },
56
- { bin: 'claude-sbx', label: 'claude-sbx --headless --continue', args: ['--headless', '--continue'], config: true },
57
- { bin: 'codex-sbx', label: 'codex-sbx', args: [], config: true },
58
- { bin: 'gemini-sbx', label: 'gemini-sbx', args: [], config: true },
59
- { bin: 'sbx-show', label: 'sbx-show', args: [], config: true },
60
- { bin: 'claude-host', label: 'claude-host --version', args: ['--version'] },
55
+ { bin: 'sandbox', label: 'unknown command', args: ['frobnicate'] },
56
+ { bin: 'sandbox', label: 'claude --headless --continue', args: ['claude', '--headless', '--continue'], config: true },
57
+ { bin: 'sandbox', label: 'codex', args: ['codex'], config: true },
58
+ { bin: 'sandbox', label: 'gemini', args: ['gemini'], config: true },
59
+ { bin: 'sandbox', label: 'host claude --version', args: ['host', 'claude', '--', '--version'] },
61
60
  ];
62
61
  export const hostSimSuite = {
63
62
  name: 'host-sim',
@@ -93,7 +92,7 @@ export const hostSimSuite = {
93
92
  const args = command.config ? [...command.args, '--config', pc.config, ...(command.after ?? [])] : command.args;
94
93
  const r = await f.runBin(command.bin, args, { cwd: project, env: envFor(pc.home), timeoutMs: 120000 });
95
94
  const crashed = r.status === null || r.status > 1 || /Unexpected error|node:internal|^\s+at .+:\d+:\d+\)?$/m.test(r.out);
96
- const unexplained = r.status === 1 && !/(^|\n)(Error: |error: )/.test(r.out) && command.bin !== 'claude-host';
95
+ const unexplained = r.status === 1 && !/(^|\n)(Error: |error: )/.test(r.out) && !command.label.startsWith('host ');
97
96
  if (crashed || unexplained)
98
97
  broken.push(command.label +
99
98
  ' (exit ' +
@@ -110,8 +109,8 @@ export const hostSimSuite = {
110
109
  await runOne(next.command, next.index);
111
110
  });
112
111
  const initRuns = (async () => {
113
- await runOne({ bin: 'agent-sandbox', label: 'config init', args: ['config', 'init'] }, 1000, initPc);
114
- await runOne({ bin: 'agent-sandbox', label: 'config init (again)', args: ['config', 'init'] }, 1001, initPc);
112
+ await runOne({ bin: 'sandbox', label: 'config init', args: ['config', 'init'] }, 1000, initPc);
113
+ await runOne({ bin: 'sandbox', label: 'config init (again)', args: ['config', 'init'] }, 1001, initPc);
115
114
  })();
116
115
  await Promise.all([...workers, initRuns]);
117
116
  t.equal(broken, [], 'all ' + (COMMANDS.length + 2) + ' commands work or fail with a clear message when run outside a sandbox');
@@ -3,7 +3,7 @@
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { spawnSync } from 'node:child_process';
6
- import { shimText } from '../host/install/index.js';
6
+ import { REMOVED_COMMANDS, shimText } from '../host/install/index.js';
7
7
  import { nativeScript } from '../host/native.js';
8
8
  import { SYSTEM } from '../shared/system.js';
9
9
  /** The text install.ps1 writes for one shim, run by powershell.exe as a script file. */
@@ -39,11 +39,15 @@ function installerText(t, script) {
39
39
  const cases = [
40
40
  ['sandbox', undefined],
41
41
  ['claude-sbx', 'dist/bin/claude-sbx.js'],
42
- ['agent-sandbox', 'dist/bin/agent-sandbox.js'],
42
+ ['sbx-show', undefined],
43
43
  ];
44
44
  for (const [name, target] of cases)
45
45
  for (const kind of ['cmd', 'sh'])
46
46
  t.equal(printedShim(script, kind, name, target), shimText(name, kind, target), name + '.' + kind + ' is identical');
47
+ t.section('install.ps1 and package.json: the commands');
48
+ const bin = JSON.parse(fs.readFileSync(path.join(t.packageRoot, 'package.json'), 'utf8')).bin;
49
+ t.equal(Object.keys(bin).sort(), ['sandbox', 'windows-development-sandbox'], 'the package declares only those commands');
50
+ t.equal(Object.keys(REMOVED_COMMANDS).filter((name) => !source.includes("'" + name + "'")), [], 'the installer removes the shim of every removed command');
47
51
  t.includes(printedShim(script, 'cmd', 'x y'), 'exit', 'a name that is not a command name is refused');
48
52
  }
49
53
  export const installSuite = {
@@ -11,6 +11,7 @@ import { spawn, spawnSync } from 'node:child_process';
11
11
  import { pathToFileURL } from 'node:url';
12
12
  import { encodeFrame, FrameType } from '../shared/pty/index.js';
13
13
  import { SYSTEM } from '../shared/system.js';
14
+ import { GateEventSchema } from '../guest/governor/core.js';
14
15
  import { CSC, openViewer, tail, WAIT } from './support/live.js';
15
16
  /**
16
17
  * The stand-in agent: VT input and SGR mouse tracking on, echoes what it reads ("got <1b>[<0;10;5M"). L prints 500
@@ -268,6 +269,108 @@ export const liveTerminalSuite = {
268
269
  Number(set[1]) === Math.max(Math.floor(commit / 2), commit - Math.max(1536, Math.floor((commit * 3) / 100))), 'the ptyhost that creates the job sets the backstop just below the commit limit (auto: 3 %, at least 1.5 GB)', read('job-first.log'));
269
270
  t.check(read('job-second.log').includes('joined the existing one'), 'a ptyhost that joins the job never changes its backstop', read('job-second.log'));
270
271
  }
272
+ t.section('the memory gate: a burst is held at once and let go in turn');
273
+ {
274
+ // A job of the test's own (never the agents' real one) for both ptyhosts and memgov.
275
+ const jobName = 'Local\\AgentSandboxAgentsJobTest' + process.pid + '-gate-' + Date.now();
276
+ const memgovExe = t.at('memgov-gate.exe');
277
+ const burstExe = t.at('MemoryBurst.exe');
278
+ const gateErrors = compile(path.join(t.packageRoot, 'sandbox', 'csharp', 'MemGov.cs'), memgovExe) ||
279
+ compile(path.join(t.packageRoot, 'src', 'selftest', 'fixtures', 'memory-burst', 'MemoryBurst.cs'), burstExe);
280
+ t.check(!gateErrors, 'memgov and the memory burst program compile', gateErrors);
281
+ if (!gateErrors) {
282
+ const ping = path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'PING.EXE');
283
+ const holder = '"' + SYSTEM.cmd() + '" /c "' + ping + '" -n 60 127.0.0.1 >nul';
284
+ const progress = t.at('burst-progress.txt');
285
+ const startHost = async (command, log) => spawn(ptyhost, [String(await freePort()), 'tok', '80', '24', t.at(log), command], {
286
+ env: { ...process.env, PTYHOST_BIND: 'loopback', PTYHOST_BACKSTOP_MB: 'none', PTYHOST_JOB_NAME: jobName },
287
+ stdio: 'ignore',
288
+ windowsHide: true,
289
+ });
290
+ const read = (file) => (fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '');
291
+ const hosts = [];
292
+ let memgov = null;
293
+ try {
294
+ hosts.push(await startHost(holder, 'gate-a.log'));
295
+ await t.waitFor(() => read(t.at('gate-a.log')).includes('agent job:'), WAIT.screen);
296
+ memgov = spawn(memgovExe, ['serve', '--interval', '1', '--gate', '512:3000:800:4000', '--gate-names', 'memoryburst'], {
297
+ env: { ...process.env, MEMGOV_JOB_NAME: jobName },
298
+ stdio: ['pipe', 'pipe', 'ignore'],
299
+ windowsHide: true,
300
+ });
301
+ let text = '';
302
+ memgov.stdout.on('data', (chunk) => (text += chunk.toString('utf8')));
303
+ const events = () => text
304
+ .split(/\r?\n/)
305
+ .filter((l) => l.includes('"event":"gate"'))
306
+ .flatMap((l) => {
307
+ try {
308
+ const parsed = GateEventSchema.safeParse(JSON.parse(l));
309
+ return parsed.success ? [parsed.data] : [];
310
+ }
311
+ catch {
312
+ return [];
313
+ }
314
+ });
315
+ const ready = await t.waitFor(() => events().some((e) => e.kind === 'ready'), WAIT.screen);
316
+ const readyEvent = events().find((e) => e.kind === 'ready');
317
+ t.check(Boolean(ready) && readyEvent?.kind === 'ready' && readyEvent.mode === 'notify', "the gate listens to the job's notifications (no polling)", text.slice(-400));
318
+ hosts.push(await startHost('"' + burstExe + '" 1536 "' + progress + '"', 'gate-b.log'));
319
+ const finished = await t.waitFor(() => read(progress).includes('done'), 15000);
320
+ const marks = read(progress)
321
+ .split(/\r?\n/)
322
+ .flatMap((l) => {
323
+ const m = /^(\d+) \d+$/.exec(l.trim());
324
+ return m ? [Number(m[1])] : [];
325
+ });
326
+ const all = events();
327
+ const trips = all.flatMap((e) => (e.kind === 'trip' ? [e] : []));
328
+ const releases = all.flatMap((e) => (e.kind === 'release' ? [e] : []));
329
+ const evidence = text.slice(-1200) + '\nprogress: ' + marks.length + ' lines';
330
+ const firstTrip = trips[0];
331
+ t.check(trips.some((e) => e.held.some((h) => h.name === 'memoryburst')) &&
332
+ firstTrip !== undefined &&
333
+ firstTrip.grewMb >= 512 &&
334
+ firstTrip.grewMb <= 1024, 'a burst past the gate is held at once', evidence);
335
+ const stalls = [];
336
+ for (const trip of trips) {
337
+ if (!trip.held.some((h) => h.name === 'memoryburst'))
338
+ continue;
339
+ const release = releases.find((r) => r.at >= trip.at && r.name === 'memoryburst');
340
+ const until = release ? release.at : Infinity;
341
+ if (marks.some((m) => m > trip.at + 200 && m < until))
342
+ stalls.push('trip at ' + trip.at);
343
+ }
344
+ t.check(stalls.length === 0, 'the held program made no progress more than 200 ms after the trip until it was let go', stalls.join(', ') + '\n' + evidence);
345
+ t.check(releases.length > 0 && releases.every((r) => r.heldMs <= 4500) && releases.some((r) => !r.forced), 'held programs go on in turn, never longer than the most allowed', evidence);
346
+ t.check(Boolean(finished) && trips.length >= 2, 'the burst program reached its end after at least 2 trips', evidence);
347
+ }
348
+ finally {
349
+ if (memgov) {
350
+ memgov.stdin.end();
351
+ const ended = memgov;
352
+ await t.waitFor(() => ended.exitCode !== null, 5000);
353
+ t.check(ended.exitCode === 0, 'memgov with the gate ends cleanly when its input closes');
354
+ try {
355
+ if (ended.exitCode === null)
356
+ ended.kill();
357
+ }
358
+ catch {
359
+ /* it ended between the check and the kill: nothing left to stop */
360
+ }
361
+ }
362
+ for (const c of hosts) {
363
+ try {
364
+ if (c.exitCode === null)
365
+ c.kill();
366
+ }
367
+ catch {
368
+ /* it ended between the check and the kill: nothing left to stop */
369
+ }
370
+ }
371
+ }
372
+ }
373
+ }
271
374
  const client = t.at('client.mjs');
272
375
  fs.writeFileSync(client, [
273
376
  'import { connect, runTerminalClient } from ' +
@@ -47,7 +47,8 @@ export const personalizationSuite = {
47
47
  const untouched = fs.readFileSync(settings, 'utf8');
48
48
  t.check(untouched.startsWith('// edited') &&
49
49
  /"guid": "\{x\}",\s*\}/.test(untouched) &&
50
- /"reloadEnvironmentVariables":\s*true/.test(untouched), 'until pwsh is installed only the reload switch is added (comments and trailing commas survive)');
50
+ /"reloadEnvironmentVariables":\s*true/.test(untouched) &&
51
+ /"copyOnSelect":\s*true/.test(untouched), 'until pwsh is installed only the default switches are added (comments and trailing commas survive)');
51
52
  t.write(path.join(data, 'tools', 'pwsh', 'pwsh.exe'), 'MZ');
52
53
  syncPwshProfile(data, true);
53
54
  const added = read();
@@ -8,11 +8,12 @@ import { DEFAULTS } from '../core/config/index.js';
8
8
  import { OFF_IS_SAFER } from '../core/local-config/index.js';
9
9
  import { HOST_SOURCES } from '../host/accounts/index.js';
10
10
  import { prepareStorePackages, stagedPackageDir, StorePackages } from '../host/apps/index.js';
11
+ import { prepareServiceAccounts } from '../host/accounts/index.js';
11
12
  import { checkedPackage } from '../guest/apps/store-check.js';
12
13
  import { peekPendingPerplexityApp, perplexityAppPendingFile } from '../guest/accounts/kinds/perplexityapp.js';
13
14
  import { perplexityAppShortcutArgs, restoreAppSignIn } from '../guest/browser/perplexity-app.js';
14
- import { appsToInstall, appById, isStorePackageFile, PERPLEXITY_DESKTOP_FAMILY, PERPLEXITY_DESKTOP_PACKAGE, PERPLEXITY_DESKTOP_PRODUCT_ID, storeApps, StorePackageManifestSchema, storePackageRole, } from '../shared/apps/index.js';
15
- import { appUsersOn, PAYLOADS, SERVICE_SPECS, serviceWanted, usedByText, } from '../shared/accounts/index.js';
15
+ import { appsToInstall, appById, isStorePackageFile, PERPLEXITY_DESKTOP_FAMILY, PERPLEXITY_DESKTOP_PACKAGE, PERPLEXITY_DESKTOP_PRODUCT_ID, assignStoreRoles, storeApps, StorePackageManifestSchema, storePackageFitsX64, storePackageRole, } from '../shared/apps/index.js';
16
+ import { appsNeededFor, appUsersOn, PAYLOADS, SERVICE_SPECS, serviceWanted, usedByText, } from '../shared/accounts/index.js';
16
17
  const MAIN = 'PerplexityAI.PerplexityApp_1.2.3.0_x64__3jh4kjrg4dzr2.msix';
17
18
  const FRAMEWORK = 'Microsoft.VCLibs.140.00_14.0.33519.0_x64__8wekyb3d8bbwe.appx';
18
19
  const sha = (text) => crypto.createHash('sha256').update(text).digest('hex');
@@ -137,7 +138,9 @@ export const storeAppSuite = {
137
138
  run: fakeStore({}, false).run,
138
139
  });
139
140
  t.check(outcome.notes.length === 1 &&
140
- outcome.notes[0].includes('is not prepared') &&
141
+ outcome.notes[0].includes('NOT installed in this sandbox') &&
142
+ outcome.notes[0].includes('no Store access') &&
143
+ outcome.notes[0].startsWith('perplexity-desktop:') &&
141
144
  outcome.notes[0].includes('winget download --id 9P9XG917PWCJ'), 'with nothing kept the launch goes on, with a note that says what to fix');
142
145
  const good = await prepareStorePackages([app], {
143
146
  storageRoot: emptyRoot,
@@ -145,12 +148,67 @@ export const storeAppSuite = {
145
148
  run: fakeStore({ [MAIN]: 'a' }).run,
146
149
  });
147
150
  t.check(good.notes[0].includes('downloaded') && fs.existsSync(path.join(stagedPackageDir(emptyData, app.id), MAIN)), 'and a working Store stages it');
151
+ t.section('frameworks: the right builds, and the app told from them');
152
+ const VCLIBS_X86 = 'Microsoft.VCLibs.140.00_14.0.33519.0_x86__8wekyb3d8bbwe.appx';
153
+ const VCLIBS_ARM = 'Microsoft.VCLibs.140.00_14.0.33519.0_arm64__8wekyb3d8bbwe.appx';
154
+ t.check(storePackageFitsX64(FRAMEWORK) &&
155
+ !storePackageFitsX64(VCLIBS_X86) &&
156
+ !storePackageFitsX64(VCLIBS_ARM) &&
157
+ storePackageFitsX64('Microsoft.WindowsAppRuntime.1.6_6000.311.0_neutral__8wekyb3d8bbwe.msix'), '32-bit and ARM frameworks are left out, x64 and neutral ones stay');
158
+ t.equal(assignStoreRoles(['Perplexity_x64.msixbundle', FRAMEWORK], PERPLEXITY_DESKTOP_PACKAGE, [FRAMEWORK]).map((f) => f.role), ['main', 'dependency'], 'an app winget names another way is still found: it is the file outside Dependencies');
159
+ t.check((() => {
160
+ try {
161
+ assignStoreRoles([FRAMEWORK], PERPLEXITY_DESKTOP_PACKAGE, [FRAMEWORK]);
162
+ return false;
163
+ }
164
+ catch (e) {
165
+ return String(e).includes('only frameworks');
166
+ }
167
+ })(), 'frameworks without the app are refused with a plain reason');
168
+ const rich = await new StorePackages({
169
+ storageRoot: t.dir('pc3', 'storage'),
170
+ dataPath: t.dir('pc3', 'storage', 'data', 'set'),
171
+ run: (_exe, args) => {
172
+ const out = args[args.indexOf('-OutDir') + 1];
173
+ fs.mkdirSync(out, { recursive: true });
174
+ for (const n of ['Perplexity_1.2_x64.msixbundle', FRAMEWORK, VCLIBS_X86, VCLIBS_ARM])
175
+ fs.writeFileSync(path.join(out, n), n);
176
+ return Promise.resolve({
177
+ status: 0,
178
+ stdout: JSON.stringify({
179
+ ok: true,
180
+ message: 'ok',
181
+ files: ['Perplexity_1.2_x64.msixbundle', FRAMEWORK, VCLIBS_X86, VCLIBS_ARM],
182
+ dependencies: [FRAMEWORK, VCLIBS_X86, VCLIBS_ARM],
183
+ }),
184
+ stderr: '',
185
+ });
186
+ },
187
+ }).fetch(app);
188
+ t.check(rich.files.map((f) => f.role).join() === 'main,dependency' &&
189
+ rich.files.every((f) => storePackageFitsX64(f.name)), 'a fetch keeps the app and only the x64 framework, with the roles the script reported');
190
+ t.section('a sign-in for an app that is not installed says so');
191
+ t.equal(appsNeededFor('perplexityapp'), ['perplexity-desktop'], 'the app a sign-in needs');
192
+ t.equal(appsNeededFor('epic'), [], 'a sign-in a toolchain uses needs no app');
193
+ const sharing = { ...DEFAULTS, accounts: { ...DEFAULTS.accounts, perplexityapp: { share: true } } };
194
+ const deliverFor = (on) => prepareServiceAccounts({
195
+ config: sharing,
196
+ others: [],
197
+ on: new Set(on),
198
+ storageRoot: t.dir('pc4', 'storage'),
199
+ dataPath: t.dir('pc4', 'storage', 'data', 'set'),
200
+ launchId: 'L-1',
201
+ isRunning: () => false,
202
+ });
203
+ const missing = deliverFor([]);
204
+ t.check(missing.notes.some((n) => n.includes('accounts.perplexityapp.share') && n.includes('Fix: turn the app on in Apps')) && !missing.leased.length, 'share on with the app off: a launch note names the setting and the app to turn on');
205
+ t.check(!deliverFor(['perplexityapp']).notes.some((n) => n.includes('is not installed')), 'with the app on there is no such note');
148
206
  t.section('the sandbox checks the kept files');
149
207
  const checked = await checkedPackage(staged);
150
208
  t.check(path.basename(checked.main) === MAIN && checked.dependencies.map((d) => path.basename(d)).join() === FRAMEWORK, 'the app and its frameworks, in order');
151
209
  fs.writeFileSync(path.join(staged, FRAMEWORK), 'tampered');
152
210
  t.check((await t.thrownMessage(() => checkedPackage(staged))).includes('missing or changed'), 'a changed file stops the install');
153
- t.check((await t.thrownMessage(() => checkedPackage(t.dir('nothing')))).includes('not handed over'), 'no package says so');
211
+ t.check((await t.thrownMessage(() => checkedPackage(t.dir('nothing')))).includes('did not hand over its package'), 'no package says so');
154
212
  t.section('the shortcut');
155
213
  const args = perplexityAppShortcutArgs('C:\\AgentLaunch\\sandbox\\scripts\\start-store-app.ps1', 'C:\\Users\\X\\profile');
156
214
  t.check(args.includes('-File "C:\\AgentLaunch\\sandbox\\scripts\\start-store-app.ps1"') &&
@@ -62,6 +62,7 @@ export const LAUNCH_FILES_GOLDEN = [
62
62
  'sandbox/csharp/AppActivation.cs',
63
63
  'sandbox/csharp/BindMount.cs',
64
64
  'sandbox/csharp/CargoShim.cs',
65
+ 'sandbox/csharp/ClipboardWatch.cs',
65
66
  'sandbox/csharp/Desktop.cs',
66
67
  'sandbox/csharp/EnvBroadcast.cs',
67
68
  'sandbox/csharp/GitShim.cs',
@@ -95,6 +96,7 @@ export const LAUNCH_FILES_GOLDEN = [
95
96
  'sandbox/scripts/set-machine-environment.ps1',
96
97
  'sandbox/scripts/set-registry-values.ps1',
97
98
  'sandbox/scripts/start-store-app.ps1',
99
+ 'sandbox/scripts/watch-clipboard.ps1',
98
100
  'sandbox/templates/fivem/README.txt',
99
101
  'sandbox/templates/fivem/cs/Client/Client.csproj',
100
102
  'sandbox/templates/fivem/cs/Client/ClientMain.cs',
@@ -563,7 +563,7 @@ export function sampleAuditLog(dir) {
563
563
  proc(94, 'C:\\AgentData\\tools\\uv\\uv.exe', 'uv tool install aider-chat', {}),
564
564
  { t: at(90), k: 'session', a: 'start', p: 'claude', w: SESSIONS[0].cwd, c: claude.s, ...claude, sv: undefined },
565
565
  proc(88, git, 'git status --short', claude),
566
- proc(87, node, 'node dist/bin/agent-sandbox.js self-test audit', claude, 2),
566
+ proc(87, node, 'node dist/bin/sandbox.js self-test audit', claude, 2),
567
567
  file(80, 'src/host/audit/collect.ts', ws, claude),
568
568
  file(79, 'src/app/features/audit/audit.def.ts', ws, claude),
569
569
  proc(70, 'C:\\nvm\\nodejs\\npm.cmd', 'npm run build', claude),
@@ -613,7 +613,7 @@ export function sampleAuditLog(dir) {
613
613
  ...codex,
614
614
  sv: undefined,
615
615
  },
616
- proc(5, node, 'node dist/bin/agent-sandbox.js self-test app', claude),
616
+ proc(5, node, 'node dist/bin/sandbox.js self-test app', claude),
617
617
  { t: at(4), k: 'remote', p: 'sessions.restart', a: 'ok', au: "device Josh's iPhone" },
618
618
  { t: at(3), k: 'remote', p: 'settings.set', a: 'refused', au: 'Windows user DESKTOP-JOSH\\sam' },
619
619
  ]);
@@ -58,3 +58,8 @@ export declare function usedByText(usedBy: readonly string[]): string;
58
58
  export declare function appUsersOn(install: readonly string[]): AppUser[];
59
59
  /** Whether a sign-in is wanted when these toolchains (and apps) are on. */
60
60
  export declare function serviceWanted(kind: ServiceKind, on: ReadonlySet<UsedBy>): boolean;
61
+ /**
62
+ * For a sign-in that only apps use (Perplexity API, Comet, the Perplexity app): the apps.install ids to turn on for it
63
+ * to be handed over; empty for a kind a toolchain uses. A sign-in shared with its app off would otherwise do nothing.
64
+ */
65
+ export declare function appsNeededFor(kind: ServiceKind): string[];
@@ -256,3 +256,11 @@ export function serviceWanted(kind, on) {
256
256
  return true;
257
257
  return spec.usedBy.includes('android') && ANDROID_USERS.some((u) => on.has(u));
258
258
  }
259
+ /**
260
+ * For a sign-in that only apps use (Perplexity API, Comet, the Perplexity app): the apps.install ids to turn on for it
261
+ * to be handed over; empty for a kind a toolchain uses. A sign-in shared with its app off would otherwise do nothing.
262
+ */
263
+ export function appsNeededFor(kind) {
264
+ const { usedBy } = SERVICE_SPECS[kind];
265
+ return usedBy.every((u) => u in APP_USERS) ? usedBy.flatMap((u) => [...APP_USERS[u]]) : [];
266
+ }
@@ -5,4 +5,4 @@ export { exeMatcher, appsToInstall, appsNeedMesa, appsNeedUv, storeApps } from '
5
5
  export { signerMatches, type AppSigner, type Signature } from '../signer.js';
6
6
  export { AGENTS_WEB_APPS, BROWSER_USE_VERSION, CHROME_DEVTOOLS_MCP_VERSION, COMET_DOWNLOAD_URL, PERPLEXITY_DESKTOP_FAMILY, PERPLEXITY_DESKTOP_PACKAGE, PERPLEXITY_DESKTOP_PRODUCT_ID, COMET_MCP_VERSION, COMET_SIGNER, PERPLEXITY_COMPUTER_MCP_URL, PERPLEXITY_MCP_VERSION, } from './catalogue-agents-web.js';
7
7
  export { ADDON_MCP_PREFIX, addonGuideLine, addonMcpServer, type AddonPlace, claudeManagedSettings, codexWithAddonMcpServers, type CodexMcpServer, HEADROOM_ENV, HEADROOM_PORT, HEADROOM_UPSTREAM, headroomArgs, headroomEnv, headroomGuideLine, headroomUrl, type ManagedSettings, type McpServerConfig, rtkHookCommand, RTK_GUIDE_LINE, withAddonMcpServers, } from './agent-addons.js';
8
- export { isStorePackageFile, STORE_PACKAGE_MANIFEST, StorePackageFileSchema, StorePackageManifestSchema, storePackageRole, type StorePackageFile, type StorePackageManifest, } from './store-package.js';
8
+ export { isStorePackageFile, STORE_PACKAGE_MANIFEST, StorePackageFileSchema, StorePackageManifestSchema, storePackageRole, storePackageFitsX64, assignStoreRoles, type StorePackageFile, type StorePackageManifest, } from './store-package.js';
@@ -5,4 +5,4 @@ export { exeMatcher, appsToInstall, appsNeedMesa, appsNeedUv, storeApps } from '
5
5
  export { signerMatches } from '../signer.js';
6
6
  export { AGENTS_WEB_APPS, BROWSER_USE_VERSION, CHROME_DEVTOOLS_MCP_VERSION, COMET_DOWNLOAD_URL, PERPLEXITY_DESKTOP_FAMILY, PERPLEXITY_DESKTOP_PACKAGE, PERPLEXITY_DESKTOP_PRODUCT_ID, COMET_MCP_VERSION, COMET_SIGNER, PERPLEXITY_COMPUTER_MCP_URL, PERPLEXITY_MCP_VERSION, } from './catalogue-agents-web.js';
7
7
  export { ADDON_MCP_PREFIX, addonGuideLine, addonMcpServer, claudeManagedSettings, codexWithAddonMcpServers, HEADROOM_ENV, HEADROOM_PORT, HEADROOM_UPSTREAM, headroomArgs, headroomEnv, headroomGuideLine, headroomUrl, rtkHookCommand, RTK_GUIDE_LINE, withAddonMcpServers, } from './agent-addons.js';
8
- export { isStorePackageFile, STORE_PACKAGE_MANIFEST, StorePackageFileSchema, StorePackageManifestSchema, storePackageRole, } from './store-package.js';
8
+ export { isStorePackageFile, STORE_PACKAGE_MANIFEST, StorePackageFileSchema, StorePackageManifestSchema, storePackageRole, storePackageFitsX64, assignStoreRoles, } from './store-package.js';
@@ -14,6 +14,7 @@ export declare function appsNeedMesa(apps: Config['apps'] | undefined): boolean;
14
14
  /** The chosen apps that come from the Microsoft Store: what this PC has to fetch for them. */
15
15
  export declare function storeApps(install: readonly string[]): {
16
16
  id: string;
17
+ name: string;
17
18
  productId: string;
18
19
  packageName: string;
19
20
  }[];
@@ -50,9 +50,10 @@ export function appsNeedMesa(apps) {
50
50
  /** The chosen apps that come from the Microsoft Store: what this PC has to fetch for them. */
51
51
  export function storeApps(install) {
52
52
  return install.flatMap((id) => {
53
- const source = appById(id)?.source;
54
- return source !== undefined && 'store' in source
55
- ? [{ id, productId: source.store.productId, packageName: source.store.packageName }]
53
+ const entry = appById(id);
54
+ const source = entry?.source;
55
+ return entry && source !== undefined && 'store' in source
56
+ ? [{ id, name: entry.name, productId: source.store.productId, packageName: source.store.packageName }]
56
57
  : [];
57
58
  });
58
59
  }
@@ -29,3 +29,16 @@ export declare const STORE_PACKAGE_MANIFEST = "store-package.json";
29
29
  export declare function isStorePackageFile(name: string): boolean;
30
30
  /** The app itself (named <package name>_<version>_...), as opposed to a framework it depends on (VCLibs ...). */
31
31
  export declare function storePackageRole(fileName: string, packageName: string): 'main' | 'dependency';
32
+ /**
33
+ * Whether a dependency package suits this sandbox (64-bit Windows): winget may also fetch the 32-bit and ARM builds
34
+ * of a framework, and Add-AppxPackage is only handed the builds that can run here.
35
+ */
36
+ export declare function storePackageFitsX64(fileName: string): boolean;
37
+ /**
38
+ * Which fetched file is the app and which are its frameworks. The app is named after its package; when winget names
39
+ * it otherwise, the one file that did not come from the Dependencies folder is the app. Throws when that is unclear.
40
+ */
41
+ export declare function assignStoreRoles(names: readonly string[], packageName: string, dependencyNames: readonly string[]): {
42
+ name: string;
43
+ role: 'main' | 'dependency';
44
+ }[];
@@ -32,3 +32,27 @@ export function isStorePackageFile(name) {
32
32
  export function storePackageRole(fileName, packageName) {
33
33
  return fileName.toLowerCase().startsWith(packageName.toLowerCase() + '_') ? 'main' : 'dependency';
34
34
  }
35
+ /**
36
+ * Whether a dependency package suits this sandbox (64-bit Windows): winget may also fetch the 32-bit and ARM builds
37
+ * of a framework, and Add-AppxPackage is only handed the builds that can run here.
38
+ */
39
+ export function storePackageFitsX64(fileName) {
40
+ return !/[_.-](x86|arm|arm64)(?=[_.-]|$)/i.test(fileName.replace(/\.(msix|appx|msixbundle|appxbundle)$/i, ''));
41
+ }
42
+ /**
43
+ * Which fetched file is the app and which are its frameworks. The app is named after its package; when winget names
44
+ * it otherwise, the one file that did not come from the Dependencies folder is the app. Throws when that is unclear.
45
+ */
46
+ export function assignStoreRoles(names, packageName, dependencyNames) {
47
+ const listed = new Set(dependencyNames.map((n) => n.toLowerCase()));
48
+ let mains = names.filter((n) => storePackageRole(n, packageName) === 'main');
49
+ if (mains.length === 0)
50
+ mains = names.filter((n) => !listed.has(n.toLowerCase()));
51
+ if (mains.length > 1)
52
+ mains = mains.filter(storePackageFitsX64);
53
+ if (mains.length !== 1)
54
+ throw new Error(mains.length === 0
55
+ ? 'winget gave only frameworks, not the app itself (' + names.join(', ') + ')'
56
+ : 'winget gave more than one app package and it is not clear which to install (' + mains.join(', ') + ')');
57
+ return names.map((name) => ({ name, role: name === mains[0] ? 'main' : 'dependency' }));
58
+ }