windows-development-sandbox 1.25.0 → 1.27.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 (287) hide show
  1. package/config.example.json +17 -5
  2. package/dist/app/features/accounts/services.def.d.ts +1 -1
  3. package/dist/app/features/accounts/services.def.js +1 -1
  4. package/dist/app/features/data/data.def.d.ts +4 -4
  5. package/dist/app/features/data/data.run.d.ts +1 -1
  6. package/dist/app/features/defs.d.ts +356 -6
  7. package/dist/app/features/defs.js +15 -0
  8. package/dist/app/features/projects/projects.def.d.ts +1 -1
  9. package/dist/app/features/projects/projects.run.d.ts +1 -1
  10. package/dist/app/features/settings/categories.def.js +1 -0
  11. package/dist/app/features/unity/index.d.ts +2 -0
  12. package/dist/app/features/unity/index.js +3 -0
  13. package/dist/app/features/unity/unity.def.d.ts +294 -0
  14. package/dist/app/features/unity/unity.def.js +471 -0
  15. package/dist/app/features/unity/unity.run.d.ts +142 -0
  16. package/dist/app/features/unity/unity.run.js +363 -0
  17. package/dist/app/features/unreal/index.d.ts +2 -0
  18. package/dist/app/features/unreal/index.js +3 -0
  19. package/dist/app/features/unreal/unreal.def.d.ts +236 -0
  20. package/dist/app/features/unreal/unreal.def.js +379 -0
  21. package/dist/app/features/unreal/unreal.run.d.ts +96 -0
  22. package/dist/app/features/unreal/unreal.run.js +217 -0
  23. package/dist/app/registry.js +11 -0
  24. package/dist/bin/agent-sandbox.js +2 -2
  25. package/dist/bin/{chunk-IZRGTSQ5.js → chunk-6WWMBCT2.js} +4632 -1941
  26. package/dist/bin/{chunk-SUYYVIDW.js → chunk-JXL6ZOO3.js} +126 -6
  27. package/dist/bin/{chunk-KH4MI2U5.js → chunk-ZXCAVVS4.js} +1 -1
  28. package/dist/bin/claude-host.js +2 -2
  29. package/dist/bin/claude-sbx.js +2 -2
  30. package/dist/bin/codex-sbx.js +2 -2
  31. package/dist/bin/gemini-sbx.js +2 -2
  32. package/dist/bin/sandbox.js +2 -2
  33. package/dist/bin/sbx-hide.js +2 -2
  34. package/dist/bin/sbx-show.js +2 -2
  35. package/dist/cli/commands/launch/launch.js +17 -2
  36. package/dist/cli/commands/registry.js +6 -0
  37. package/dist/cli/manifest.d.ts +51 -1
  38. package/dist/cli/manifest.js +58 -1
  39. package/dist/cli/menu/coverage-exempt.js +2 -0
  40. package/dist/cli/menu/pages/sandbox-page.d.ts +6 -0
  41. package/dist/cli/menu/pages/sandbox-page.js +71 -4
  42. package/dist/cli/menu/views.js +4 -0
  43. package/dist/core/config/defaults.js +33 -6
  44. package/dist/core/schema/accounts.d.ts +3 -0
  45. package/dist/core/schema/accounts.js +1 -0
  46. package/dist/core/schema/config.d.ts +32 -14
  47. package/dist/core/schema/github.d.ts +1 -0
  48. package/dist/core/schema/github.js +3 -0
  49. package/dist/core/schema/pc-installs.d.ts +5 -2
  50. package/dist/core/schema/pc-installs.js +16 -2
  51. package/dist/core/schema/toolchains.d.ts +8 -0
  52. package/dist/core/schema/toolchains.js +20 -5
  53. package/dist/guest/accounts/adapters.js +2 -0
  54. package/dist/guest/accounts/apply.d.ts +4 -2
  55. package/dist/guest/accounts/kinds/epic.js +18 -13
  56. package/dist/guest/accounts/kinds/perplexityapp.d.ts +7 -0
  57. package/dist/guest/accounts/kinds/perplexityapp.js +53 -0
  58. package/dist/guest/accounts/kinds/unity.d.ts +27 -0
  59. package/dist/guest/accounts/kinds/unity.js +164 -7
  60. package/dist/guest/apps/after-install.d.ts +1 -1
  61. package/dist/guest/apps/after-install.js +6 -1
  62. package/dist/guest/apps/app-installer.d.ts +1 -0
  63. package/dist/guest/apps/app-installer.js +31 -3
  64. package/dist/guest/apps/comet-unpack.js +3 -2
  65. package/dist/guest/apps/resolvers-gamedev.js +55 -1
  66. package/dist/guest/apps/store-app.d.ts +4 -0
  67. package/dist/guest/apps/store-app.js +19 -0
  68. package/dist/guest/apps/store-check.d.ts +8 -0
  69. package/dist/guest/apps/store-check.js +33 -0
  70. package/dist/guest/boot/boot.js +15 -2
  71. package/dist/guest/boot/guide.js +42 -0
  72. package/dist/guest/browser/comet.d.ts +3 -1
  73. package/dist/guest/browser/comet.js +1 -1
  74. package/dist/guest/browser/perplexity-app.d.ts +12 -0
  75. package/dist/guest/browser/perplexity-app.js +60 -0
  76. package/dist/guest/daemon/daemon.js +5 -0
  77. package/dist/guest/devtools/devtool.d.ts +1 -1
  78. package/dist/guest/devtools/registry.js +1 -2
  79. package/dist/guest/entries/tool.js +1 -1
  80. package/dist/guest/setup/addon-mcp.d.ts +12 -0
  81. package/dist/guest/setup/addon-mcp.js +29 -4
  82. package/dist/guest/setup/browser-gpu.js +14 -11
  83. package/dist/guest/setup/github-token.js +4 -4
  84. package/dist/guest/setup/headroom.js +4 -0
  85. package/dist/guest/setup/unity-agent.d.ts +11 -0
  86. package/dist/guest/setup/unity-agent.js +36 -0
  87. package/dist/guest/toolchains/engines/epic-launcher.d.ts +26 -0
  88. package/dist/guest/toolchains/engines/epic-launcher.js +127 -0
  89. package/dist/guest/toolchains/engines/unity-cli.d.ts +24 -0
  90. package/dist/guest/toolchains/engines/unity-cli.js +74 -0
  91. package/dist/guest/toolchains/engines/unity-hub.d.ts +2 -0
  92. package/dist/guest/toolchains/engines/unity-hub.js +27 -0
  93. package/dist/guest/toolchains/engines/unity-release.d.ts +4 -15
  94. package/dist/guest/toolchains/engines/unity-release.js +17 -5
  95. package/dist/guest/toolchains/engines/unity-status.d.ts +11 -0
  96. package/dist/guest/toolchains/engines/unity-status.js +39 -0
  97. package/dist/guest/toolchains/engines/unity.d.ts +20 -1
  98. package/dist/guest/toolchains/engines/unity.js +250 -3
  99. package/dist/guest/toolchains/engines/unreal-engines.d.ts +21 -1
  100. package/dist/guest/toolchains/engines/unreal-engines.js +163 -3
  101. package/dist/guest/toolchains/engines/unreal-status.d.ts +13 -0
  102. package/dist/guest/toolchains/engines/unreal-status.js +41 -0
  103. package/dist/guest/toolchains/native/debuggers.d.ts +6 -0
  104. package/dist/guest/toolchains/native/debuggers.js +57 -0
  105. package/dist/guest/toolchains/native/mesa.js +15 -8
  106. package/dist/guest/toolchains/native/rust-extras.d.ts +7 -0
  107. package/dist/guest/toolchains/native/rust-extras.js +23 -1
  108. package/dist/guest/toolchains/native/unreal.d.ts +2 -2
  109. package/dist/guest/toolchains/native/unreal.js +96 -18
  110. package/dist/guest/toolchains/plan.js +17 -2
  111. package/dist/guest/toolchains/tools/cli.js +2 -2
  112. package/dist/guest/toolchains/tools/vscode-profile.js +2 -0
  113. package/dist/host/accounts/deliver.js +4 -0
  114. package/dist/host/accounts/kinds/epic.js +11 -5
  115. package/dist/host/accounts/kinds/perplexityapp.d.ts +2 -0
  116. package/dist/host/accounts/kinds/perplexityapp.js +8 -0
  117. package/dist/host/accounts/kinds/unity.d.ts +1 -1
  118. package/dist/host/accounts/kinds/unity.js +60 -8
  119. package/dist/host/accounts/leases.d.ts +2 -1
  120. package/dist/host/accounts/source.d.ts +2 -0
  121. package/dist/host/accounts/source.js +1 -0
  122. package/dist/host/accounts/sources.js +2 -0
  123. package/dist/host/apps/index.d.ts +1 -0
  124. package/dist/host/apps/index.js +2 -0
  125. package/dist/host/apps/store-package.d.ts +51 -0
  126. package/dist/host/apps/store-package.js +209 -0
  127. package/dist/host/github/deliver.d.ts +8 -3
  128. package/dist/host/github/deliver.js +42 -29
  129. package/dist/host/github/index.d.ts +1 -0
  130. package/dist/host/github/index.js +1 -0
  131. package/dist/host/github/pc-login.d.ts +27 -0
  132. package/dist/host/github/pc-login.js +129 -0
  133. package/dist/host/pc-installs/discover.d.ts +6 -0
  134. package/dist/host/pc-installs/discover.js +37 -5
  135. package/dist/host/pc-installs/epic.d.ts +3 -0
  136. package/dist/host/pc-installs/epic.js +33 -2
  137. package/dist/host/pc-installs/source.d.ts +1 -1
  138. package/dist/host/pc-installs/unity-assets.d.ts +5 -0
  139. package/dist/host/pc-installs/unity-assets.js +18 -0
  140. package/dist/host/pc-installs/unity.d.ts +20 -0
  141. package/dist/host/pc-installs/unity.js +135 -2
  142. package/dist/host/pc-installs/unreal-vault.d.ts +11 -0
  143. package/dist/host/pc-installs/unreal-vault.js +78 -0
  144. package/dist/host/pc-installs/unreal.d.ts +36 -0
  145. package/dist/host/pc-installs/unreal.js +145 -2
  146. package/dist/host/profile/claude-profile.d.ts +8 -0
  147. package/dist/host/profile/claude-profile.js +43 -4
  148. package/dist/host/profile/index.d.ts +1 -1
  149. package/dist/host/profile/index.js +1 -1
  150. package/dist/runtime/agent.js +197 -18
  151. package/dist/runtime/daemon.js +3938 -1386
  152. package/dist/runtime/db.js +197 -18
  153. package/dist/runtime/main.js +4336 -1978
  154. package/dist/runtime/netproxy.js +197 -18
  155. package/dist/runtime/tool.js +214 -38
  156. package/dist/runtime/view.js +197 -18
  157. package/dist/selftest/agent-paths.test.js +1 -0
  158. package/dist/selftest/agents-web.test.js +14 -1
  159. package/dist/selftest/engines.test.js +602 -3
  160. package/dist/selftest/github.test.js +64 -1
  161. package/dist/selftest/heavy-load.test.js +3 -1
  162. package/dist/selftest/hostile-input.test.js +11 -1
  163. package/dist/selftest/index.js +12 -0
  164. package/dist/selftest/menu-screens.js +5 -1
  165. package/dist/selftest/mixamo.test.d.ts +2 -0
  166. package/dist/selftest/mixamo.test.js +60 -0
  167. package/dist/selftest/persisted-installs.test.js +1 -1
  168. package/dist/selftest/secrets.test.js +36 -1
  169. package/dist/selftest/service-accounts.test.js +1 -1
  170. package/dist/selftest/store-app.test.d.ts +2 -0
  171. package/dist/selftest/store-app.test.js +162 -0
  172. package/dist/selftest/support/engine-golden.js +10 -0
  173. package/dist/selftest/support/fixtures.d.ts +13 -2
  174. package/dist/selftest/support/unity-sample.d.ts +2 -0
  175. package/dist/selftest/support/unity-sample.js +80 -0
  176. package/dist/selftest/support/unreal-sample.d.ts +2 -0
  177. package/dist/selftest/support/unreal-sample.js +83 -0
  178. package/dist/selftest/toolchains.test.js +32 -2
  179. package/dist/selftest/unity-accounts.test.d.ts +2 -0
  180. package/dist/selftest/unity-accounts.test.js +106 -0
  181. package/dist/selftest/unity-agent.test.d.ts +2 -0
  182. package/dist/selftest/unity-agent.test.js +47 -0
  183. package/dist/selftest/unity-page.test.d.ts +2 -0
  184. package/dist/selftest/unity-page.test.js +177 -0
  185. package/dist/selftest/unity-setup.test.d.ts +2 -0
  186. package/dist/selftest/unity-setup.test.js +49 -0
  187. package/dist/selftest/unity.test.js +238 -3
  188. package/dist/shared/accounts/index.d.ts +2 -1
  189. package/dist/shared/accounts/index.js +2 -1
  190. package/dist/shared/accounts/kinds/epic.d.ts +6 -1
  191. package/dist/shared/accounts/kinds/epic.js +11 -1
  192. package/dist/shared/accounts/kinds/unity.d.ts +28 -0
  193. package/dist/shared/accounts/kinds/unity.js +88 -0
  194. package/dist/shared/accounts/kinds.d.ts +2 -2
  195. package/dist/shared/accounts/kinds.js +23 -1
  196. package/dist/shared/accounts/payloads.d.ts +52 -0
  197. package/dist/shared/accounts/payloads.js +14 -2
  198. package/dist/shared/apps/associations.d.ts +7 -0
  199. package/dist/shared/apps/associations.js +26 -0
  200. package/dist/shared/apps/catalogue-agents-web.d.ts +11 -1
  201. package/dist/shared/apps/catalogue-agents-web.js +28 -2
  202. package/dist/shared/apps/catalogue-gamedev.d.ts +3 -0
  203. package/dist/shared/apps/catalogue-gamedev.js +102 -1
  204. package/dist/shared/apps/catalogue.d.ts +8 -0
  205. package/dist/shared/apps/index.d.ts +5 -2
  206. package/dist/shared/apps/index.js +5 -2
  207. package/dist/shared/apps/preview.d.ts +2 -0
  208. package/dist/shared/apps/preview.js +10 -0
  209. package/dist/shared/apps/selection.d.ts +6 -0
  210. package/dist/shared/apps/selection.js +9 -0
  211. package/dist/shared/apps/store-package.d.ts +31 -0
  212. package/dist/shared/apps/store-package.js +34 -0
  213. package/dist/shared/browser-policy.d.ts +14 -0
  214. package/dist/shared/browser-policy.js +22 -0
  215. package/dist/shared/channel/schema.d.ts +24 -0
  216. package/dist/shared/channel/secrets.d.ts +17 -0
  217. package/dist/shared/io/index.d.ts +1 -0
  218. package/dist/shared/io/index.js +1 -0
  219. package/dist/shared/io/rotate-log.d.ts +2 -0
  220. package/dist/shared/io/rotate-log.js +16 -0
  221. package/dist/shared/memory/policy.d.ts +5 -0
  222. package/dist/shared/memory/policy.js +7 -0
  223. package/dist/shared/paths/pc-installs.d.ts +13 -2
  224. package/dist/shared/paths/pc-installs.js +2 -0
  225. package/dist/shared/readiness.d.ts +2 -3
  226. package/dist/shared/readiness.js +2 -3
  227. package/dist/shared/signer.d.ts +4 -0
  228. package/dist/shared/signer.js +2 -0
  229. package/dist/shared/unity/asset-cache.d.ts +20 -0
  230. package/dist/shared/unity/asset-cache.js +44 -0
  231. package/dist/shared/unity/cli.d.ts +40 -0
  232. package/dist/shared/unity/cli.js +77 -0
  233. package/dist/shared/unity/editors.d.ts +46 -0
  234. package/dist/shared/unity/editors.js +89 -0
  235. package/dist/shared/unity/guide.d.ts +9 -0
  236. package/dist/shared/unity/guide.js +84 -0
  237. package/dist/shared/unity/index.d.ts +9 -0
  238. package/dist/shared/unity/index.js +11 -0
  239. package/dist/shared/unity/manifest.d.ts +10 -0
  240. package/dist/shared/unity/manifest.js +53 -0
  241. package/dist/shared/unity/mcp.d.ts +8 -0
  242. package/dist/shared/unity/mcp.js +12 -0
  243. package/dist/shared/unity/release.d.ts +28 -0
  244. package/dist/shared/unity/release.js +83 -0
  245. package/dist/shared/unity/status.d.ts +59 -0
  246. package/dist/shared/unity/status.js +45 -0
  247. package/dist/shared/unity/version.d.ts +27 -0
  248. package/dist/shared/unity/version.js +75 -0
  249. package/dist/shared/unreal/engines.d.ts +161 -0
  250. package/dist/shared/unreal/engines.js +308 -0
  251. package/dist/shared/unreal/guide.d.ts +19 -0
  252. package/dist/shared/unreal/guide.js +105 -0
  253. package/dist/shared/unreal/index.d.ts +7 -0
  254. package/dist/shared/unreal/index.js +8 -0
  255. package/dist/shared/unreal/launcher-files.d.ts +88 -0
  256. package/dist/shared/unreal/launcher-files.js +292 -0
  257. package/dist/shared/unreal/mixamo.d.ts +5 -0
  258. package/dist/shared/unreal/mixamo.js +16 -0
  259. package/dist/shared/unreal/room.d.ts +17 -0
  260. package/dist/shared/unreal/room.js +34 -0
  261. package/dist/shared/unreal/status.d.ts +54 -0
  262. package/dist/shared/unreal/status.js +57 -0
  263. package/dist/shared/unreal/vault.d.ts +21 -0
  264. package/dist/shared/unreal/vault.js +67 -0
  265. package/dist/shared/unreal/vs-check.d.ts +39 -0
  266. package/dist/shared/unreal/vs-check.js +79 -0
  267. package/dist/web/index.html +1 -1
  268. package/dist/web/{main-CCILLX2M.js → main-VPMYTSUL.js} +87 -87
  269. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  270. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  271. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  272. package/native/scripts/get-store-package.ps1 +58 -0
  273. package/package.json +1 -1
  274. package/sandbox/addons/openupm-cli/package-lock.json +2921 -0
  275. package/sandbox/addons/openupm-cli/package.json +7 -0
  276. package/sandbox/addons/unity-mcp/requirements.in +1 -0
  277. package/sandbox/addons/unity-mcp/requirements.txt +1165 -0
  278. package/sandbox/csharp/AppActivation.cs +38 -0
  279. package/sandbox/csharp/VsSetup.cs +210 -0
  280. package/sandbox/scripts/get-vs-setup.ps1 +93 -0
  281. package/sandbox/scripts/install-store-package.ps1 +28 -0
  282. package/sandbox/scripts/set-registry-values.ps1 +35 -0
  283. package/sandbox/scripts/start-store-app.ps1 +29 -0
  284. package/schemas/config.schema.json +96 -8
  285. package/schemas/sandbox-config.schema.json +46 -6
  286. package/dist/guest/devtools/unity.d.ts +0 -2
  287. package/dist/guest/devtools/unity.js +0 -7
@@ -9,7 +9,7 @@ import { pageLines } from '../cli/output/index.js';
9
9
  import { DEFAULTS } from '../core/config/index.js';
10
10
  import { applyOverrides, normalizeOverrides } from '../core/local-config/index.js';
11
11
  import { ConfigSchema, LocalOverridesSchema } from '../core/schema/index.js';
12
- import { checkGithubToken, GithubToken, githubTokenAllowed, prepareGithubToken, verifyGithubToken, } from '../host/github/index.js';
12
+ import { checkGithubToken, GithubToken, githubTokenAllowed, parseHostsYml, prepareGithubToken, readPcGhLogin, verifyGithubToken, } from '../host/github/index.js';
13
13
  import { UserError } from '../core/util/index.js';
14
14
  import { report } from '../cli/report.js';
15
15
  import { toWireError, fromWireError } from '../app/kernel/index.js';
@@ -288,6 +288,69 @@ export const githubSuite = {
288
288
  shareOn.github.shareToken = true;
289
289
  t.check(prepareGithubToken(shareOn, shareOn.storage.root, expiring, [], Date.parse('2027-01-30T00:00:00.000Z')).notes.some((n) => n.includes('expires in 5 days')) &&
290
290
  !prepareGithubToken(shareOn, shareOn.storage.root, expiring, [], Date.parse('2026-12-01T00:00:00.000Z')).notes.some((n) => n.includes('expires')), 'a launch mentions a token that ends within 7 days, and only then');
291
+ t.section('the gh login on this PC (github.usePcLogin)');
292
+ const PC_TOKEN = 'gho_' + 'a'.repeat(36);
293
+ const KEYRING_TOKEN = 'gho_' + 'b'.repeat(36);
294
+ const hosts = (token) => [
295
+ 'github.com:',
296
+ ' users:',
297
+ ' other:',
298
+ ' oauth_token: gho_' + 'z'.repeat(36),
299
+ ' josh:',
300
+ ...(token ? [' oauth_token: ' + token] : []),
301
+ ' git_protocol: https',
302
+ ' user: josh',
303
+ '',
304
+ ].join('\r\n');
305
+ t.equal(parseHostsYml(hosts(PC_TOKEN)), { signedIn: true, token: PC_TOKEN }, 'hosts.yml: the active user token');
306
+ t.equal(parseHostsYml('github.com:\n oauth_token: ' + PC_TOKEN + '\n user: josh\n'), { signedIn: true, token: PC_TOKEN }, 'hosts.yml: an older file with the token under the host');
307
+ t.equal(parseHostsYml('ghe.corp:\n oauth_token: ' + PC_TOKEN + '\n'), { signedIn: false, token: null }, 'hosts.yml: other hosts are not read');
308
+ t.equal(parseHostsYml(hosts(null)), { signedIn: true, token: null }, 'hosts.yml: a login kept in Credential Manager');
309
+ t.equal(parseHostsYml(''), { signedIn: false, token: null }, 'hosts.yml: empty');
310
+ const pcDir = t.dir('github-pc');
311
+ const pcEnv = { GH_CONFIG_DIR: pcDir, GH_TOKEN: 'x'.repeat(30) };
312
+ t.check(readPcGhLogin({ env: pcEnv, run: () => 'never' }) === null, 'no hosts.yml: no login, quietly');
313
+ fs.writeFileSync(path.join(pcDir, 'hosts.yml'), hosts(PC_TOKEN));
314
+ t.check(readPcGhLogin({ env: pcEnv, run: () => 'never' }) === PC_TOKEN, 'the token is read from hosts.yml');
315
+ fs.writeFileSync(path.join(pcDir, 'hosts.yml'), hosts(null));
316
+ t.check(readPcGhLogin({ env: pcEnv, run: () => KEYRING_TOKEN }) === null, 'a keyring login with no gh.exe found: none');
317
+ const exeDir = t.dir('github-pc-programs');
318
+ fs.mkdirSync(path.join(exeDir, 'GitHub CLI'), { recursive: true });
319
+ fs.writeFileSync(path.join(exeDir, 'GitHub CLI', 'gh.exe'), '');
320
+ const seen = [];
321
+ const keyring = readPcGhLogin({
322
+ env: { ...pcEnv, ProgramFiles: exeDir },
323
+ run: (exe) => (seen.push(exe), KEYRING_TOKEN + '\n'),
324
+ });
325
+ t.check(keyring === KEYRING_TOKEN && seen.length === 1 && path.isAbsolute(seen[0] ?? ''), 'a keyring login is read with the PC gh.exe, by absolute path', JSON.stringify([keyring, seen]));
326
+ t.check(readPcGhLogin({ env: { ...pcEnv, ProgramFiles: exeDir }, run: () => 'not a token' }) === null, 'whatever gh prints must look like a token');
327
+ const pcConfig = f.makeConfig();
328
+ pcConfig.github.usePcLogin = true;
329
+ const pcStore = new GithubToken(pcConfig.storage.root, SecretStore.memory());
330
+ const pcNoLogin = prepareGithubToken(pcConfig, pcConfig.storage.root, pcStore, [], 0, {
331
+ env: { GH_CONFIG_DIR: t.dir('github-pc-empty') },
332
+ });
333
+ t.equal(pcNoLogin, { notes: [] }, 'on, but the PC has no gh login: nothing is delivered, nothing is said');
334
+ fs.writeFileSync(path.join(pcDir, 'hosts.yml'), hosts(PC_TOKEN));
335
+ const pcEnvOnly = { env: { GH_CONFIG_DIR: pcDir } };
336
+ const pcOn = prepareGithubToken(pcConfig, pcConfig.storage.root, pcStore, [], 0, pcEnvOnly);
337
+ t.check(pcOn.token === PC_TOKEN && pcOn.notes.length === 1 && !JSON.stringify(pcOn.notes).includes(PC_TOKEN), 'on with a gh login: delivered, and the notes never hold it');
338
+ pcConfig.github.usePcLogin = false;
339
+ t.equal(prepareGithubToken(pcConfig, pcConfig.storage.root, pcStore, [], 0, pcEnvOnly), { notes: [] }, 'off: the PC login is not even read');
340
+ pcConfig.github.usePcLogin = true;
341
+ pcConfig.github.shareToken = true;
342
+ pcStore.save(FINE);
343
+ t.check(prepareGithubToken(pcConfig, pcConfig.storage.root, pcStore, [], 0, pcEnvOnly).token === FINE, 'a token saved for sandboxes wins over the PC login');
344
+ pcStore.remove();
345
+ t.check(prepareGithubToken(pcConfig, pcConfig.storage.root, pcStore, [], 0, pcEnvOnly).token === PC_TOKEN, 'with none saved, the PC login is used');
346
+ pcConfig.provisioning.git.access = 'readonly';
347
+ const pcStrict = prepareGithubToken(pcConfig, pcConfig.storage.root, pcStore, [], 0, pcEnvOnly);
348
+ t.check(pcStrict.token === undefined &&
349
+ !githubTokenAllowed(pcConfig) &&
350
+ pcStrict.notes.some((n) => n.includes('"readonly"')), 'git access readonly: the PC login is not handed over');
351
+ const pcSecrets = SecretsFileSchema.safeParse({ githubToken: PC_TOKEN });
352
+ t.check(pcSecrets.success, 'an OAuth token from the PC passes the secrets file limits');
353
+ t.check(DEFAULTS.github.usePcLogin === false, 'the PC login is off by default');
291
354
  t.section('errors with an action');
292
355
  const fixable = new UserError('Broke.', 'Do it.', { op: 'sandbox.stop', label: 'Stop it', cli: 'sandbox stop' });
293
356
  const wire = toWireError(fixable);
@@ -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, } from '../shared/memory/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';
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';
@@ -240,6 +240,8 @@ export const heavyLoadSuite = {
240
240
  chooseTargetDir('auto', 10 * 1024 ** 3).targetDir === 'separate' &&
241
241
  Boolean(chooseTargetDir('auto', 10 * 1024 ** 3).warning) &&
242
242
  chooseTargetDir('shared', 1).targetDir === 'shared', 'targetDir "auto": local with 25 GB free on C:, else separate with a warning; other values stay');
243
+ t.check(cargoLinkerVar('x86_64-pc-windows-msvc') === 'CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER' &&
244
+ cargoLinkerVar('aarch64-pc-windows-msvc') === 'CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER', 'cargo linker variable names the host triple (the msvc host links with lld, not link.exe)');
243
245
  const beat = {
244
246
  at: '2026-11-02T21:31:00.000Z',
245
247
  uptimeS: 2800,
@@ -8,7 +8,7 @@ import { assertSafeMapping } from '../core/safe-paths.js';
8
8
  import { safeText } from '../core/util/index.js';
9
9
  import { isSafeFileName } from '../shared/paths/index.js';
10
10
  import { removeRefusal, safeRemove } from '../shared/io/index.js';
11
- import { writeJsonAtomic } from '../shared/io/index.js';
11
+ import { rotateLog, writeJsonAtomic } from '../shared/io/index.js';
12
12
  export const hostileInputSuite = {
13
13
  name: 'hostile-input',
14
14
  title: 'file names, deletes, printed text, mappings and state files from an untrusted sandbox',
@@ -63,5 +63,15 @@ export const hostileInputSuite = {
63
63
  fs.writeFileSync(path.join(repoDir, '.sandbox-config.json'), '{"readOnly": true}' + ' '.repeat(1100 * 1024));
64
64
  await t.throws(() => f.run({ cwd: repoDir, trustConfig: true }), 'over 1 MB', 'a .sandbox-config.json over 1 MB is refused before it is parsed');
65
65
  fs.rmSync(path.join(repoDir, '.sandbox-config.json'));
66
+ t.section('a log that grows for ever (Headroom appends to one on the data disk)');
67
+ const logFile = t.at('rotate', 'proxy.log');
68
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
69
+ t.check(!rotateLog(logFile, 10), 'no log yet: nothing to rotate');
70
+ fs.writeFileSync(logFile, 'x'.repeat(10));
71
+ t.check(!rotateLog(logFile, 10) && fs.existsSync(logFile), 'a log at the limit stays');
72
+ fs.writeFileSync(logFile + '.1', 'old');
73
+ fs.writeFileSync(logFile, 'y'.repeat(11));
74
+ t.check(rotateLog(logFile, 10), 'a log over the limit is rotated');
75
+ t.check(!fs.existsSync(logFile) && fs.readFileSync(logFile + '.1', 'utf8') === 'y'.repeat(11), 'it replaces the older copy');
66
76
  },
67
77
  };
@@ -16,11 +16,17 @@ import { persistedInstallsSuite } from './persisted-installs.test.js';
16
16
  import { pcInstallsSuite } from './pc-installs.test.js';
17
17
  import { steamSuite } from './steam.test.js';
18
18
  import { enginesSuite } from './engines.test.js';
19
+ import { mixamoSuite } from './mixamo.test.js';
19
20
  import { unitySuite } from './unity.test.js';
21
+ import { unitySetupSuite } from './unity-setup.test.js';
22
+ import { unityAccountsSuite } from './unity-accounts.test.js';
23
+ import { unityPageSuite } from './unity-page.test.js';
24
+ import { unityAgentSuite } from './unity-agent.test.js';
20
25
  import { fivemSuite } from './fivem.test.js';
21
26
  import { androidSuite } from './android.test.js';
22
27
  import { mobileSuite } from './mobile.test.js';
23
28
  import { agentsWebSuite } from './agents-web.test.js';
29
+ import { storeAppSuite } from './store-app.test.js';
24
30
  import { credentialsSuite } from './credentials.test.js';
25
31
  import { vpnSuite } from './vpn.test.js';
26
32
  import { githubSuite } from './github.test.js';
@@ -139,11 +145,17 @@ export const SUITES = [
139
145
  persistedInstallsSuite,
140
146
  steamSuite,
141
147
  enginesSuite,
148
+ mixamoSuite,
142
149
  unitySuite,
150
+ unitySetupSuite,
151
+ unityAccountsSuite,
152
+ unityPageSuite,
153
+ unityAgentSuite,
143
154
  fivemSuite,
144
155
  androidSuite,
145
156
  mobileSuite,
146
157
  agentsWebSuite,
158
+ storeAppSuite,
147
159
  detectSuite,
148
160
  networkSuite,
149
161
  personalizationSuite,
@@ -10,6 +10,8 @@ import { appsOptions, stageTicks } from '../cli/menu/pages/apps-page.js';
10
10
  import { maintenanceOptions } from '../cli/menu/pages/maintenance-page.js';
11
11
  import { sandboxOptions } from '../cli/menu/pages/sandbox-page.js';
12
12
  import { sessionsOptions } from '../cli/menu/pages/sessions-page.js';
13
+ import { UNREAL_SAMPLE } from './support/unreal-sample.js';
14
+ import { UNITY_SAMPLE } from './support/unity-sample.js';
13
15
  import { settingsOf, settingsOptions } from '../cli/menu/pages/settings-page.js';
14
16
  import { CATEGORIES } from '../app/features/settings/index.js';
15
17
  import { agentsOptions } from '../cli/menu/pages/agents-page.js';
@@ -199,6 +201,8 @@ export function renderScreens(dir) {
199
201
  { name: 'redis', running: false, port: 6379, title: 'Redis' },
200
202
  ],
201
203
  defaultAgent: 'claude',
204
+ unreal: UNREAL_SAMPLE,
205
+ unity: UNITY_SAMPLE,
202
206
  };
203
207
  const stopped = {
204
208
  ...sandbox,
@@ -399,7 +403,7 @@ export function renderScreens(dir) {
399
403
  out.push(block('1 Agents · Start', draw(0, { ...agentsOptions(agentsModel, API_FOLDER), startCategory: 'start' }, size, status), size), block('1 Agents · Options (navigation focused)', draw(0, { ...agentsOptions(agentsModel, API_FOLDER), startCategory: 'options' }, size, status, 0, true), size), block('1 Agents · On this PC', draw(0, { ...agentsOptions(agentsModel, API_FOLDER), startCategory: 'host' }, size, status), size), block('2 Sessions', draw(1, sessionsOptions({ sandboxRunning: true, sessions: SESSIONS, defaultAgent: 'claude' }), size, status), size), block('2 Sessions (no sandbox)', draw(1, sessionsOptions({ sandboxRunning: false, sessions: [], defaultAgent: 'claude' }), size), size));
400
404
  for (const category of ['status', 'databases'])
401
405
  out.push(block('3 Sandbox (stopped) · ' + category, draw(2, { ...sandboxOptions(stopped), startCategory: category }, size), size));
402
- for (const category of ['status', 'projects', 'databases'])
406
+ for (const category of ['status', 'projects', 'databases', 'unreal', 'unity'])
403
407
  out.push(block('3 Sandbox · ' + category, draw(2, { ...sandboxOptions(sandbox), startCategory: category }, size, status), size));
404
408
  out.push(block('4 Apps (1 unsaved)', draw(3, appsOptions(user, appsData, appsDraft, 'user'), size, status, appsDraft.count()), size));
405
409
  for (const category of CATEGORIES)
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const mixamoSuite: Suite;
@@ -0,0 +1,60 @@
1
+ // Mixamo: the browser policy table (downloads folder, no GPU) and the guide's Mixamo lines and editor names.
2
+ import { browserPolicies } from '../shared/browser-policy.js';
3
+ import { unrealGuideLines } from '../shared/unreal/guide.js';
4
+ const DOWNLOADS = 'C:\\AgentData\\downloads';
5
+ const PROJECT = { workspace: 'C:\\P\\Game', uproject: 'C:\\P\\Game\\Game.uproject' };
6
+ function statusWith(version) {
7
+ return {
8
+ updatedAt: 'now',
9
+ engines: [
10
+ {
11
+ id: 'UE_' + version,
12
+ dir: 'C:\\E',
13
+ version,
14
+ build: 'installed',
15
+ origin: 'pc',
16
+ writable: true,
17
+ sizeBytes: null,
18
+ },
19
+ ],
20
+ projects: [{ workspace: PROJECT.workspace, engine: 'C:\\E', reason: 'test' }],
21
+ launcher: { installed: false, installLocation: null, problem: null, freeBytes: null, needBytes: 1 },
22
+ visualStudio: { version: '17.14', path: 'C:/VS', how: 'kept', check: 'ok', ok: true },
23
+ debugger: null,
24
+ ddc: { mode: 'persisted', dir: null },
25
+ vaults: [],
26
+ mcpUrl: null,
27
+ };
28
+ }
29
+ export const mixamoSuite = {
30
+ name: 'mixamo',
31
+ title: 'Mixamo: browser downloads folder and the guide lines for bringing characters into Unreal',
32
+ run: async (t) => {
33
+ t.section('browser policies');
34
+ const four = browserPolicies({ downloads: DOWNLOADS });
35
+ t.equal(four.length, 4, 'two browsers, two policies each');
36
+ t.equal(four.filter((p) => p.name === 'DefaultDownloadDirectory').map((p) => p.data), [DOWNLOADS, DOWNLOADS], 'both browsers download to the data disk');
37
+ t.equal(browserPolicies({ downloads: null }).length, 2, 'no folder: only the GPU policy');
38
+ await t.throws(() => browserPolicies({ downloads: 'C:\\a"b' }), 'not a folder', 'a quote in the folder is refused');
39
+ await t.throws(() => browserPolicies({ downloads: 'downloads' }), 'not a folder', 'a relative folder is refused');
40
+ t.section('the guide');
41
+ const on = { mixamo: { downloads: DOWNLOADS, script: null } };
42
+ const guide = unrealGuideLines(statusWith('5.4.4'), [PROJECT], on).join('\n');
43
+ t.includes(guide, 'https://www.mixamo.com', 'the Mixamo line is there');
44
+ t.includes(guide, DOWNLOADS, 'it names the downloads folder');
45
+ t.includes(guide, 'Mixamo into Unreal', 'and the import line');
46
+ t.excludes(guide, 'Mixamo import script', 'no script line without a script');
47
+ const withScript = unrealGuideLines(statusWith('5.4.4'), [PROJECT], {
48
+ mixamo: { downloads: DOWNLOADS, script: 'C:\\x\\import_mixamo.py' },
49
+ }).join('\n');
50
+ t.includes(withScript, 'Mixamo import script', 'a script adds its line');
51
+ t.excludes(unrealGuideLines(statusWith('5.4.4'), [PROJECT]).join('\n'), 'Mixamo', 'off by default');
52
+ const noEngine = { ...statusWith('5.4.4'), engines: [] };
53
+ t.excludes(unrealGuideLines(noEngine, [PROJECT], on).join('\n'), 'Mixamo', 'no engine: no Mixamo lines');
54
+ t.section('UE4 names');
55
+ const ue4 = unrealGuideLines(statusWith('4.27.2'), [PROJECT]).join('\n');
56
+ t.includes(ue4, 'UE4Editor-Cmd.exe', 'a UE4 engine uses UE4Editor-Cmd.exe');
57
+ t.excludes(ue4, 'UnrealEditor', 'and not the UE5 names');
58
+ t.includes(unrealGuideLines(statusWith('5.4.4'), [PROJECT]).join('\n'), 'UnrealEditor-Cmd.exe', 'UE5 keeps its names');
59
+ },
60
+ };
@@ -309,7 +309,7 @@ export const persistedInstallsSuite = {
309
309
  const found = decideToolchains(everything, ['ProjectVersion.txt', 'project.godot', 'fxmanifest.lua'], ['unity']);
310
310
  t.check(NOT_READY.toolchains.every((n) => !found[n].on), '"auto" never turns on a toolchain that is not written yet, whatever the project holds');
311
311
  const shown = settingsOf(ConfigFile.user(t.at('settings', 'config.json'))).map((r) => r.key.join('.'));
312
- t.check(!shown.some((k) => /^toolchains\.(unity|godot|android|flutter|dart|reactNative)\./.test(k)) &&
312
+ t.check(!shown.some((k) => /^toolchains\.(godot|android|flutter|dart|reactNative)\./.test(k)) &&
313
313
  shown.includes('toolchains.unreal.visualStudio') &&
314
314
  shown.some((k) => k.startsWith('toolchains.fivem.')), 'settings do not list the toolchains that are not written yet');
315
315
  },
@@ -1,7 +1,9 @@
1
1
  // What reaches the sandbox from this PC's Claude files, and what is stripped first: API keys, cloud credentials, tokens
2
2
  // in MCP server definitions. Also the secret-looking files in a workspace that the launch warns about.
3
3
  import path from 'node:path';
4
- import { findSecretFiles, sandboxGlobalState, stripMcpServers, stripSettings } from '../host/profile/index.js';
4
+ import fs from 'node:fs';
5
+ import { DEFAULTS } from '../core/config/index.js';
6
+ import { findSecretFiles, pluginFilter, pluginTrimLevel, sandboxGlobalState, stageClaudeProfile, stripMcpServers, stripSettings, } from '../host/profile/index.js';
5
7
  const REMOVED = '[removed by agent-sandbox]';
6
8
  export const secretsSuite = {
7
9
  name: 'secrets',
@@ -111,5 +113,38 @@ export const secretsSuite = {
111
113
  ])
112
114
  t.write(path.join(workspace, file), 'x');
113
115
  t.equal(findSecretFiles(workspace).sort(), ['.env', 'api\\credentials.json', 'server.pem'], 'secret-looking workspace files are found for the launch warning (templates and dependencies ignored)');
116
+ t.section('plugins staged from this PC');
117
+ const hostClaude = t.at('plugins-host');
118
+ const plugins = path.join(hostClaude, 'plugins');
119
+ t.write(path.join(plugins, 'installed_plugins.json'), '{"version":2,"plugins":{}}');
120
+ t.write(path.join(plugins, 'known_marketplaces.json'), '{}');
121
+ t.write(path.join(plugins, 'cache', 'm', 'p', '1.0', 'plugin.json'), '{}');
122
+ t.write(path.join(plugins, 'cache', 'm', 'p', '1.0', 'node_modules', 'dep', 'index.js'), 'x');
123
+ t.write(path.join(plugins, 'cache', 'temp_git_1_abc', 'junk.txt'), 'x');
124
+ t.write(path.join(plugins, 'marketplaces', 'm', 'marketplace.json'), '{}');
125
+ t.write(path.join(plugins, 'marketplaces', 'm', '.git', 'HEAD'), 'ref');
126
+ t.write(path.join(plugins, 'marketplaces', 'm.backup', 'old.txt'), 'x');
127
+ t.write(path.join(plugins, 'marketplaces', 'temp_subdir_2.clone', 'junk.txt'), 'x');
128
+ t.write(path.join(hostClaude, 'settings.json'), '{"enabledPlugins":{"p@m":true}}');
129
+ const launch = t.at('plugins-launch');
130
+ const profile = { ...DEFAULTS.claudeProfile, hostDir: hostClaude, globalStateFile: '' };
131
+ const staged = stageClaudeProfile(profile, launch, 'D:/Code/app', DEFAULTS.secrets);
132
+ const out = path.join(launch, 'claude-profile', 'plugins');
133
+ t.check(staged.staged.includes('plugins'), 'plugins are staged');
134
+ t.check(fs.existsSync(path.join(out, 'cache', 'm', 'p', '1.0', 'plugin.json')) &&
135
+ fs.existsSync(path.join(out, 'marketplaces', 'm', 'marketplace.json')) &&
136
+ fs.existsSync(path.join(out, 'marketplaces', 'm', '.git', 'HEAD')) &&
137
+ fs.existsSync(path.join(out, 'cache', 'm', 'p', '1.0', 'node_modules', 'dep', 'index.js')), 'the plugin cache, marketplaces and (when they fit) their git folders and dependencies are copied');
138
+ t.check(!fs.existsSync(path.join(out, 'cache', 'temp_git_1_abc')) &&
139
+ !fs.existsSync(path.join(out, 'marketplaces', 'm.backup')) &&
140
+ !fs.existsSync(path.join(out, 'marketplaces', 'temp_subdir_2.clone')), 'interrupted clones, backups and staging folders of plugins are left out');
141
+ t.check(pluginTrimLevel(plugins) === 0, 'a plugins folder under the cap is not trimmed');
142
+ const git = path.join(plugins, 'marketplaces', 'm', '.git');
143
+ const deps = path.join(plugins, 'cache', 'm', 'p', '1.0', 'node_modules');
144
+ t.check(pluginFilter(plugins, 0)(git) &&
145
+ !pluginFilter(plugins, 1)(git) &&
146
+ pluginFilter(plugins, 1)(deps) &&
147
+ !pluginFilter(plugins, 2)(deps), 'a plugins folder over the cap loses git history first, then node_modules');
148
+ t.check(fs.readFileSync(path.join(launch, 'claude-profile', 'settings.json'), 'utf8').includes('p@m'), 'enabledPlugins reach the sandbox');
114
149
  },
115
150
  };
@@ -274,7 +274,7 @@ export const serviceAccountsSuite = {
274
274
  process.env.LOCALAPPDATA = oldLocal;
275
275
  }
276
276
  t.section('the account command');
277
- t.check(!COMMANDS.some((c) => ['epic', 'steam', 'unity'].includes(c.name)), 'no command of its own for a service');
277
+ t.check(!COMMANDS.some((c) => ['epic', 'steam'].includes(c.name)), 'no command of its own for a service');
278
278
  const account = buildProgram(() => undefined).commands.find((c) => c.name() === 'account');
279
279
  const help = account.helpInformation();
280
280
  t.check(['status', 'import', 'login', 'logout', '--from', '--yes'].every((w) => help.includes(w)), 'sandbox account --help lists status, import, login, logout, --from and --yes', help);
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const storeAppSuite: Suite;
@@ -0,0 +1,162 @@
1
+ // The Perplexity desktop app, a Microsoft Store package: the catalogue entry, the sign-in kind (sandbox only, like
2
+ // Comet), the package manifest, the PC's fetch and staging with a fake Store tool (no download, nothing run), the
3
+ // sandbox's check of the kept files and the shortcut's command line. Temp folders only.
4
+ import crypto from 'node:crypto';
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { DEFAULTS } from '../core/config/index.js';
8
+ import { OFF_IS_SAFER } from '../core/local-config/index.js';
9
+ import { HOST_SOURCES } from '../host/accounts/index.js';
10
+ import { prepareStorePackages, stagedPackageDir, StorePackages } from '../host/apps/index.js';
11
+ import { checkedPackage } from '../guest/apps/store-check.js';
12
+ import { peekPendingPerplexityApp, perplexityAppPendingFile } from '../guest/accounts/kinds/perplexityapp.js';
13
+ 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';
16
+ const MAIN = 'PerplexityAI.PerplexityApp_1.2.3.0_x64__3jh4kjrg4dzr2.msix';
17
+ const FRAMEWORK = 'Microsoft.VCLibs.140.00_14.0.33519.0_x64__8wekyb3d8bbwe.appx';
18
+ const sha = (text) => crypto.createHash('sha256').update(text).digest('hex');
19
+ /** A fake Store tool: writes the given files into -OutDir and answers like get-store-package.ps1. */
20
+ function fakeStore(files, ok = true) {
21
+ let calls = 0;
22
+ const run = (_exe, args) => {
23
+ calls++;
24
+ if (!ok)
25
+ return Promise.resolve({
26
+ status: 0,
27
+ stdout: JSON.stringify({ ok: false, message: 'no Store access', files: [] }),
28
+ stderr: '',
29
+ });
30
+ const out = args[args.indexOf('-OutDir') + 1];
31
+ fs.mkdirSync(out, { recursive: true });
32
+ for (const [name, text] of Object.entries(files))
33
+ if (path.basename(name) === name)
34
+ fs.writeFileSync(path.join(out, name), text);
35
+ return Promise.resolve({
36
+ status: 0,
37
+ stdout: 'progress\n' + JSON.stringify({ ok: true, message: 'ok', files: Object.keys(files) }),
38
+ stderr: '',
39
+ });
40
+ };
41
+ return { run, calls: () => calls };
42
+ }
43
+ export const storeAppSuite = {
44
+ name: 'store-app',
45
+ title: 'Perplexity desktop app: Store package fetched on the PC, installed in the sandbox, sign-in kept like Comet',
46
+ run: async (t) => {
47
+ t.section('the catalogue entry');
48
+ const entry = appById('perplexity-desktop');
49
+ t.check(entry !== undefined &&
50
+ 'store' in entry.source &&
51
+ entry.source.store.productId === PERPLEXITY_DESKTOP_PRODUCT_ID &&
52
+ entry.source.store.family === PERPLEXITY_DESKTOP_FAMILY &&
53
+ PERPLEXITY_DESKTOP_FAMILY.startsWith(PERPLEXITY_DESKTOP_PACKAGE + '_') &&
54
+ entry.userOnly === true, 'a Store source with the product and the package family, yours only');
55
+ t.equal(storeApps(['rtk', 'perplexity-desktop']).map((a) => a.productId), ['9P9XG917PWCJ'], 'only Store apps are fetched');
56
+ t.check(appsToInstall({ ...DEFAULTS.apps, install: ['perplexity-desktop'] }, 'none').entries.length === 1, 'it needs no 7-Zip and no other app');
57
+ t.section('the sign-in kind');
58
+ const spec = SERVICE_SPECS.perplexityapp;
59
+ t.check(spec.sources.join() === 'sandbox' &&
60
+ spec.returns &&
61
+ !spec.singleHolder &&
62
+ DEFAULTS.accounts.perplexityapp.share === false &&
63
+ OFF_IS_SAFER.includes('accounts.perplexityapp.share') &&
64
+ spec.warnings.some((w) => w.includes("can't be copied from this PC")), 'only from a sandbox, handed back, not shared by default, a project file can only turn it off');
65
+ const source = HOST_SOURCES.perplexityapp;
66
+ t.check(!source.fromPc && !source.fromFile && !source.fromAnswers, 'nothing is read from this PC (the cookies are sealed to it)');
67
+ t.check(appUsersOn(['perplexity-desktop']).join() === 'perplexityapp' &&
68
+ serviceWanted('perplexityapp', new Set(['perplexityapp'])) &&
69
+ !serviceWanted('perplexityapp', new Set(['comet'])) &&
70
+ usedByText(['perplexityapp']) === 'the Perplexity desktop app', 'handed over only where the app is on');
71
+ const cookie = { name: 's', value: 'v', domain: '.perplexity.ai', path: '/', secure: true, httpOnly: true };
72
+ t.check(PAYLOADS.perplexityapp.safeParse({ cookies: [cookie] }).success &&
73
+ !PAYLOADS.perplexityapp.safeParse({ cookies: [{ ...cookie, domain: 'example.com' }] }).success, 'the payload is the perplexity.ai cookie allow-list');
74
+ const pendingRoot = t.dir('pending');
75
+ t.write(perplexityAppPendingFile(pendingRoot), JSON.stringify({ cookies: [cookie] }));
76
+ t.check(peekPendingPerplexityApp(pendingRoot)?.cookies.length === 1, 'a waiting sign-in is read and left in place');
77
+ t.write(perplexityAppPendingFile(pendingRoot), '{"cookies":[{"domain":"example.com"}]}');
78
+ t.check(peekPendingPerplexityApp(pendingRoot) === null, 'one that is not the allow-listed shape is not used');
79
+ t.check(!(await restoreAppSignIn({ cookies: [{ ...cookie, expires: 4_000_000_000 }] }, t.dir('no-app-profile'))), 'with the app not running, nothing is written');
80
+ t.section('the package manifest');
81
+ const file = (name, role) => ({ name, role, sha256: sha(name), bytes: 10 });
82
+ const manifest = (files) => ({
83
+ productId: '9P9XG917PWCJ',
84
+ fetchedAt: '2026-01-01T00:00:00Z',
85
+ files,
86
+ });
87
+ t.check(StorePackageManifestSchema.safeParse(manifest([file(MAIN, 'main'), file(FRAMEWORK, 'dependency')])).success &&
88
+ !StorePackageManifestSchema.safeParse(manifest([file(FRAMEWORK, 'dependency')])).success &&
89
+ !StorePackageManifestSchema.safeParse(manifest([file(MAIN, 'main'), file(MAIN.toUpperCase().replace('.MSIX', '.msix'), 'main')])).success &&
90
+ !StorePackageManifestSchema.safeParse(manifest([file('..\\evil.msix', 'main')])).success &&
91
+ !StorePackageManifestSchema.safeParse(manifest([file('x.exe', 'main')])).success, 'exactly one app file, and only .msix/.appx names (no paths, no programs)');
92
+ t.check(storePackageRole(MAIN, PERPLEXITY_DESKTOP_PACKAGE) === 'main' &&
93
+ storePackageRole(FRAMEWORK, PERPLEXITY_DESKTOP_PACKAGE) === 'dependency' &&
94
+ isStorePackageFile(MAIN) &&
95
+ !isStorePackageFile('a/b.msix'), 'the app is told from its frameworks by name');
96
+ t.section('this PC fetches and stages the package');
97
+ const storageRoot = t.dir('pc', 'storage');
98
+ const dataPath = t.dir('pc', 'storage', 'data', 'set');
99
+ const app = {
100
+ id: 'perplexity-desktop',
101
+ productId: PERPLEXITY_DESKTOP_PRODUCT_ID,
102
+ packageName: PERPLEXITY_DESKTOP_PACKAGE,
103
+ };
104
+ const store = fakeStore({
105
+ [MAIN]: 'app-bytes',
106
+ [FRAMEWORK]: 'framework-bytes',
107
+ '..evil.txt': 'x',
108
+ '../escape.msix': 'x',
109
+ });
110
+ let now = Date.parse('2026-06-01T00:00:00Z');
111
+ const packages = new StorePackages({ storageRoot, dataPath, run: store.run, now: () => now });
112
+ const first = await packages.ensure(app);
113
+ t.check(first.note === 'downloaded' &&
114
+ first.manifest.files.length === 2 &&
115
+ first.manifest.files.find((f) => f.role === 'main')?.sha256 === sha('app-bytes') &&
116
+ packages.cached(app.productId) !== null &&
117
+ !fs.existsSync(packages.cacheDir(app.productId) + '.new'), "the files are hashed here, not taken on the script's word; names that are not packages are ignored");
118
+ const second = await packages.ensure(app);
119
+ t.check(store.calls() === 1 && second.note.startsWith('kept'), 'a fresh cache is not fetched again');
120
+ const staged = packages.stage(app, first.manifest);
121
+ t.check(staged === stagedPackageDir(dataPath, app.id) &&
122
+ fs.readFileSync(path.join(staged, MAIN), 'utf8') === 'app-bytes' &&
123
+ fs.existsSync(path.join(staged, 'store-package.json')), 'the package is copied into the data folder with its manifest');
124
+ fs.writeFileSync(path.join(staged, 'stale.msix'), 'old');
125
+ packages.stage(app, first.manifest);
126
+ t.check(!fs.existsSync(path.join(staged, 'stale.msix')), 'a file the manifest does not list is removed');
127
+ t.section('an old cache and a failing Store');
128
+ now += 40 * 24 * 3600 * 1000;
129
+ const failing = new StorePackages({ storageRoot, dataPath, run: fakeStore({}, false).run, now: () => now });
130
+ const kept = await failing.ensure(app);
131
+ t.check(kept.note.includes('could not be fetched') && kept.note.includes('no Store access'), 'the old package stands, and says why');
132
+ const emptyRoot = t.dir('pc2', 'storage');
133
+ const emptyData = t.dir('pc2', 'storage', 'data', 'set');
134
+ const outcome = await prepareStorePackages([app], {
135
+ storageRoot: emptyRoot,
136
+ dataPath: emptyData,
137
+ run: fakeStore({}, false).run,
138
+ });
139
+ t.check(outcome.notes.length === 1 &&
140
+ outcome.notes[0].includes('is not prepared') &&
141
+ outcome.notes[0].includes('winget download --id 9P9XG917PWCJ'), 'with nothing kept the launch goes on, with a note that says what to fix');
142
+ const good = await prepareStorePackages([app], {
143
+ storageRoot: emptyRoot,
144
+ dataPath: emptyData,
145
+ run: fakeStore({ [MAIN]: 'a' }).run,
146
+ });
147
+ t.check(good.notes[0].includes('downloaded') && fs.existsSync(path.join(stagedPackageDir(emptyData, app.id), MAIN)), 'and a working Store stages it');
148
+ t.section('the sandbox checks the kept files');
149
+ const checked = await checkedPackage(staged);
150
+ t.check(path.basename(checked.main) === MAIN && checked.dependencies.map((d) => path.basename(d)).join() === FRAMEWORK, 'the app and its frameworks, in order');
151
+ fs.writeFileSync(path.join(staged, FRAMEWORK), 'tampered');
152
+ 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');
154
+ t.section('the shortcut');
155
+ const args = perplexityAppShortcutArgs('C:\\AgentLaunch\\sandbox\\scripts\\start-store-app.ps1', 'C:\\Users\\X\\profile');
156
+ t.check(args.includes('-File "C:\\AgentLaunch\\sandbox\\scripts\\start-store-app.ps1"') &&
157
+ args.includes('-PackageName PerplexityAI.PerplexityApp') &&
158
+ args.includes('-UserDataDir "C:\\Users\\X\\profile"') &&
159
+ args.includes('-Port 0') &&
160
+ args.includes('-WindowStyle Hidden'), 'a hidden PowerShell start with the package, its own profile and a free DevTools port');
161
+ },
162
+ };
@@ -44,10 +44,14 @@ export const LAUNCH_FILES_GOLDEN = [
44
44
  'sandbox/addons/context7/package.json',
45
45
  'sandbox/addons/headroom/requirements.in',
46
46
  'sandbox/addons/headroom/requirements.txt',
47
+ 'sandbox/addons/openupm-cli/package-lock.json',
48
+ 'sandbox/addons/openupm-cli/package.json',
47
49
  'sandbox/addons/perplexity-mcp/package-lock.json',
48
50
  'sandbox/addons/perplexity-mcp/package.json',
49
51
  'sandbox/addons/playwright-mcp/package-lock.json',
50
52
  'sandbox/addons/playwright-mcp/package.json',
53
+ 'sandbox/addons/unity-mcp/requirements.in',
54
+ 'sandbox/addons/unity-mcp/requirements.txt',
51
55
  'sandbox/boot/bootstrap.ps1',
52
56
  'sandbox/boot/bootstrap/drives.ps1',
53
57
  'sandbox/boot/bootstrap/files.ps1',
@@ -55,6 +59,7 @@ export const LAUNCH_FILES_GOLDEN = [
55
59
  'sandbox/boot/bootstrap/status.ps1',
56
60
  'sandbox/boot/logon.cmd',
57
61
  'sandbox/boot/trust.cmd',
62
+ 'sandbox/csharp/AppActivation.cs',
58
63
  'sandbox/csharp/BindMount.cs',
59
64
  'sandbox/csharp/CargoShim.cs',
60
65
  'sandbox/csharp/Desktop.cs',
@@ -69,6 +74,7 @@ export const LAUNCH_FILES_GOLDEN = [
69
74
  'sandbox/csharp/SandboxTools/MemoryLog.cs',
70
75
  'sandbox/csharp/SandboxTools/PageFile.cs',
71
76
  'sandbox/csharp/SandboxTools/Program.cs',
77
+ 'sandbox/csharp/VsSetup.cs',
72
78
  'sandbox/csharp/VsWhere.cs',
73
79
  'sandbox/csharp/VtInput.cs',
74
80
  'sandbox/scripts/add-firewall-port.ps1',
@@ -76,7 +82,9 @@ export const LAUNCH_FILES_GOLDEN = [
76
82
  'sandbox/scripts/expand-archive.ps1',
77
83
  'sandbox/scripts/get-gpu-names.ps1',
78
84
  'sandbox/scripts/get-signature.ps1',
85
+ 'sandbox/scripts/get-vs-setup.ps1',
79
86
  'sandbox/scripts/install-shell-path.ps1',
87
+ 'sandbox/scripts/install-store-package.ps1',
80
88
  'sandbox/scripts/link-profile-dirs.ps1',
81
89
  'sandbox/scripts/new-shortcuts.ps1',
82
90
  'sandbox/scripts/repair-wmi-services.ps1',
@@ -85,6 +93,8 @@ export const LAUNCH_FILES_GOLDEN = [
85
93
  'sandbox/scripts/set-desktop.ps1',
86
94
  'sandbox/scripts/set-dns-servers.ps1',
87
95
  'sandbox/scripts/set-machine-environment.ps1',
96
+ 'sandbox/scripts/set-registry-values.ps1',
97
+ 'sandbox/scripts/start-store-app.ps1',
88
98
  'sandbox/templates/fivem/README.txt',
89
99
  'sandbox/templates/fivem/cs/Client/Client.csproj',
90
100
  'sandbox/templates/fivem/cs/Client/ClientMain.cs',
@@ -106,6 +106,7 @@ export declare function createFixtures(t: Harness): {
106
106
  };
107
107
  github: {
108
108
  shareToken: boolean;
109
+ usePcLogin: boolean;
109
110
  };
110
111
  accounts: {
111
112
  epic: {
@@ -138,12 +139,18 @@ export declare function createFixtures(t: Harness): {
138
139
  comet: {
139
140
  share: boolean;
140
141
  };
142
+ perplexityapp: {
143
+ share: boolean;
144
+ };
141
145
  };
142
146
  pcInstalls: {
143
147
  steamGames: boolean | number[];
144
148
  epicGames: boolean | string[];
145
- unrealEngines: boolean | string[];
146
- unityEditors: boolean | string[];
149
+ unrealEngines: boolean | string[] | "auto";
150
+ unrealEngineFolders: string[];
151
+ unrealAssets: boolean | "auto";
152
+ unityEditors: boolean | string[] | "auto";
153
+ unityAssets: boolean | "auto";
147
154
  };
148
155
  provisioning: {
149
156
  copyFromHost: boolean;
@@ -210,6 +217,10 @@ export declare function createFixtures(t: Harness): {
210
217
  modules: string[];
211
218
  hub: boolean;
212
219
  keepVersions: number;
220
+ mcp: boolean;
221
+ agentSkills: boolean;
222
+ cliVersion: string;
223
+ assetCache: "pc" | "persisted" | "local";
213
224
  };
214
225
  godot: {
215
226
  enabled: boolean | "auto";
@@ -0,0 +1,2 @@
1
+ import type { UnityOverview } from '../../app/features/unity/index.js';
2
+ export declare const UNITY_SAMPLE: UnityOverview;