windows-development-sandbox 1.25.1 → 1.27.1

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 (211) hide show
  1. package/config.example.json +12 -3
  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/defs.d.ts +198 -3
  5. package/dist/app/features/defs.js +8 -0
  6. package/dist/app/features/settings/categories.def.js +1 -0
  7. package/dist/app/features/unity/index.d.ts +2 -0
  8. package/dist/app/features/unity/index.js +3 -0
  9. package/dist/app/features/unity/unity.def.d.ts +294 -0
  10. package/dist/app/features/unity/unity.def.js +471 -0
  11. package/dist/app/features/unity/unity.run.d.ts +142 -0
  12. package/dist/app/features/unity/unity.run.js +363 -0
  13. package/dist/app/features/unreal/unreal.run.js +13 -3
  14. package/dist/app/registry.js +6 -0
  15. package/dist/bin/agent-sandbox.js +2 -2
  16. package/dist/bin/{chunk-G3TG4ZT7.js → chunk-4TLSWPG7.js} +1 -1
  17. package/dist/bin/{chunk-3GAXLATK.js → chunk-QUHNXHVG.js} +72 -5
  18. package/dist/bin/{chunk-F4PVBN23.js → chunk-RSIX7C6D.js} +3633 -1972
  19. package/dist/bin/claude-host.js +2 -2
  20. package/dist/bin/claude-sbx.js +2 -2
  21. package/dist/bin/codex-sbx.js +2 -2
  22. package/dist/bin/gemini-sbx.js +2 -2
  23. package/dist/bin/sandbox.js +2 -2
  24. package/dist/bin/sbx-hide.js +2 -2
  25. package/dist/bin/sbx-show.js +2 -2
  26. package/dist/cli/commands/launch/launch.js +14 -1
  27. package/dist/cli/commands/registry.js +3 -0
  28. package/dist/cli/manifest.d.ts +33 -1
  29. package/dist/cli/manifest.js +32 -1
  30. package/dist/cli/menu/coverage-exempt.js +1 -0
  31. package/dist/cli/menu/pages/sandbox-page.d.ts +3 -0
  32. package/dist/cli/menu/pages/sandbox-page.js +40 -3
  33. package/dist/cli/menu/views.js +2 -0
  34. package/dist/core/config/defaults.js +16 -3
  35. package/dist/core/schema/accounts.d.ts +3 -0
  36. package/dist/core/schema/accounts.js +1 -0
  37. package/dist/core/schema/config.d.ts +29 -13
  38. package/dist/core/schema/github.d.ts +1 -0
  39. package/dist/core/schema/github.js +3 -0
  40. package/dist/core/schema/pc-installs.d.ts +2 -1
  41. package/dist/core/schema/pc-installs.js +6 -1
  42. package/dist/core/schema/toolchains.d.ts +8 -0
  43. package/dist/core/schema/toolchains.js +19 -4
  44. package/dist/guest/accounts/adapters.js +2 -0
  45. package/dist/guest/accounts/apply.d.ts +4 -2
  46. package/dist/guest/accounts/kinds/perplexityapp.d.ts +7 -0
  47. package/dist/guest/accounts/kinds/perplexityapp.js +53 -0
  48. package/dist/guest/accounts/kinds/unity.d.ts +27 -0
  49. package/dist/guest/accounts/kinds/unity.js +164 -7
  50. package/dist/guest/apps/app-installer.js +18 -0
  51. package/dist/guest/apps/store-app.d.ts +4 -0
  52. package/dist/guest/apps/store-app.js +19 -0
  53. package/dist/guest/apps/store-check.d.ts +8 -0
  54. package/dist/guest/apps/store-check.js +33 -0
  55. package/dist/guest/boot/boot.js +1 -1
  56. package/dist/guest/boot/guide.js +18 -0
  57. package/dist/guest/browser/comet.d.ts +3 -1
  58. package/dist/guest/browser/comet.js +1 -1
  59. package/dist/guest/browser/perplexity-app.d.ts +12 -0
  60. package/dist/guest/browser/perplexity-app.js +60 -0
  61. package/dist/guest/daemon/daemon.js +2 -0
  62. package/dist/guest/devtools/devtool.d.ts +1 -1
  63. package/dist/guest/devtools/registry.js +1 -2
  64. package/dist/guest/entries/tool.js +1 -1
  65. package/dist/guest/setup/addon-mcp.d.ts +7 -2
  66. package/dist/guest/setup/addon-mcp.js +19 -9
  67. package/dist/guest/setup/github-token.js +4 -4
  68. package/dist/guest/setup/unity-agent.d.ts +11 -0
  69. package/dist/guest/setup/unity-agent.js +39 -0
  70. package/dist/guest/toolchains/engines/epic-launcher.d.ts +1 -1
  71. package/dist/guest/toolchains/engines/epic-launcher.js +11 -7
  72. package/dist/guest/toolchains/engines/unity-cli.d.ts +24 -0
  73. package/dist/guest/toolchains/engines/unity-cli.js +74 -0
  74. package/dist/guest/toolchains/engines/unity-hub.d.ts +2 -0
  75. package/dist/guest/toolchains/engines/unity-hub.js +27 -0
  76. package/dist/guest/toolchains/engines/unity-release.d.ts +4 -15
  77. package/dist/guest/toolchains/engines/unity-release.js +17 -5
  78. package/dist/guest/toolchains/engines/unity-status.d.ts +11 -0
  79. package/dist/guest/toolchains/engines/unity-status.js +39 -0
  80. package/dist/guest/toolchains/engines/unity.d.ts +20 -1
  81. package/dist/guest/toolchains/engines/unity.js +250 -3
  82. package/dist/guest/toolchains/engines/unreal-status.js +1 -1
  83. package/dist/guest/toolchains/native/mesa.js +35 -11
  84. package/dist/guest/toolchains/native/unreal.js +1 -0
  85. package/dist/guest/toolchains/plan.js +5 -1
  86. package/dist/guest/toolchains/tools/cli.js +2 -2
  87. package/dist/host/accounts/deliver.d.ts +3 -0
  88. package/dist/host/accounts/deliver.js +23 -0
  89. package/dist/host/accounts/kinds/perplexityapp.d.ts +2 -0
  90. package/dist/host/accounts/kinds/perplexityapp.js +8 -0
  91. package/dist/host/accounts/kinds/unity.d.ts +1 -1
  92. package/dist/host/accounts/kinds/unity.js +60 -8
  93. package/dist/host/accounts/leases.d.ts +2 -1
  94. package/dist/host/accounts/source.d.ts +2 -0
  95. package/dist/host/accounts/source.js +2 -0
  96. package/dist/host/accounts/sources.js +2 -0
  97. package/dist/host/apps/index.d.ts +1 -0
  98. package/dist/host/apps/index.js +2 -0
  99. package/dist/host/apps/store-package.d.ts +51 -0
  100. package/dist/host/apps/store-package.js +209 -0
  101. package/dist/host/github/deliver.d.ts +8 -3
  102. package/dist/host/github/deliver.js +42 -29
  103. package/dist/host/github/index.d.ts +1 -0
  104. package/dist/host/github/index.js +1 -0
  105. package/dist/host/github/pc-login.d.ts +27 -0
  106. package/dist/host/github/pc-login.js +129 -0
  107. package/dist/host/pc-installs/discover.d.ts +4 -0
  108. package/dist/host/pc-installs/discover.js +19 -6
  109. package/dist/host/pc-installs/epic.js +3 -0
  110. package/dist/host/pc-installs/source.d.ts +1 -1
  111. package/dist/host/pc-installs/unity-assets.d.ts +5 -0
  112. package/dist/host/pc-installs/unity-assets.js +18 -0
  113. package/dist/host/pc-installs/unity.d.ts +22 -0
  114. package/dist/host/pc-installs/unity.js +144 -2
  115. package/dist/host/pc-installs/unreal.d.ts +2 -0
  116. package/dist/host/pc-installs/unreal.js +24 -8
  117. package/dist/runtime/agent.js +116 -11
  118. package/dist/runtime/daemon.js +2205 -987
  119. package/dist/runtime/db.js +116 -11
  120. package/dist/runtime/main.js +2578 -1375
  121. package/dist/runtime/netproxy.js +116 -11
  122. package/dist/runtime/tool.js +133 -31
  123. package/dist/runtime/view.js +116 -11
  124. package/dist/selftest/agents-web.test.js +2 -1
  125. package/dist/selftest/engines.test.js +32 -3
  126. package/dist/selftest/github.test.js +64 -1
  127. package/dist/selftest/index.js +10 -0
  128. package/dist/selftest/menu-screens.js +3 -1
  129. package/dist/selftest/mixamo.test.js +1 -1
  130. package/dist/selftest/persisted-installs.test.js +1 -1
  131. package/dist/selftest/service-accounts.test.js +13 -2
  132. package/dist/selftest/store-app.test.d.ts +2 -0
  133. package/dist/selftest/store-app.test.js +162 -0
  134. package/dist/selftest/support/engine-golden.js +7 -0
  135. package/dist/selftest/support/fixtures.d.ts +10 -1
  136. package/dist/selftest/support/unity-sample.d.ts +2 -0
  137. package/dist/selftest/support/unity-sample.js +80 -0
  138. package/dist/selftest/unity-accounts.test.d.ts +2 -0
  139. package/dist/selftest/unity-accounts.test.js +106 -0
  140. package/dist/selftest/unity-agent.test.d.ts +2 -0
  141. package/dist/selftest/unity-agent.test.js +47 -0
  142. package/dist/selftest/unity-page.test.d.ts +2 -0
  143. package/dist/selftest/unity-page.test.js +177 -0
  144. package/dist/selftest/unity-setup.test.d.ts +2 -0
  145. package/dist/selftest/unity-setup.test.js +49 -0
  146. package/dist/selftest/unity.test.js +238 -3
  147. package/dist/shared/accounts/index.d.ts +1 -0
  148. package/dist/shared/accounts/index.js +1 -0
  149. package/dist/shared/accounts/kinds/unity.d.ts +28 -0
  150. package/dist/shared/accounts/kinds/unity.js +88 -0
  151. package/dist/shared/accounts/kinds.d.ts +2 -2
  152. package/dist/shared/accounts/kinds.js +23 -1
  153. package/dist/shared/accounts/payloads.d.ts +52 -0
  154. package/dist/shared/accounts/payloads.js +14 -2
  155. package/dist/shared/apps/catalogue-agents-web.d.ts +7 -0
  156. package/dist/shared/apps/catalogue-agents-web.js +23 -0
  157. package/dist/shared/apps/catalogue-gamedev.d.ts +3 -0
  158. package/dist/shared/apps/catalogue-gamedev.js +31 -0
  159. package/dist/shared/apps/catalogue.d.ts +8 -0
  160. package/dist/shared/apps/index.d.ts +3 -2
  161. package/dist/shared/apps/index.js +3 -2
  162. package/dist/shared/apps/selection.d.ts +6 -0
  163. package/dist/shared/apps/selection.js +9 -0
  164. package/dist/shared/apps/store-package.d.ts +31 -0
  165. package/dist/shared/apps/store-package.js +34 -0
  166. package/dist/shared/channel/schema.d.ts +24 -0
  167. package/dist/shared/channel/secrets.d.ts +17 -0
  168. package/dist/shared/paths/pc-installs.d.ts +1 -1
  169. package/dist/shared/paths/pc-installs.js +1 -0
  170. package/dist/shared/readiness.d.ts +2 -2
  171. package/dist/shared/readiness.js +2 -2
  172. package/dist/shared/unity/asset-cache.d.ts +20 -0
  173. package/dist/shared/unity/asset-cache.js +44 -0
  174. package/dist/shared/unity/cli.d.ts +40 -0
  175. package/dist/shared/unity/cli.js +77 -0
  176. package/dist/shared/unity/editors.d.ts +46 -0
  177. package/dist/shared/unity/editors.js +89 -0
  178. package/dist/shared/unity/guide.d.ts +9 -0
  179. package/dist/shared/unity/guide.js +84 -0
  180. package/dist/shared/unity/index.d.ts +9 -0
  181. package/dist/shared/unity/index.js +11 -0
  182. package/dist/shared/unity/manifest.d.ts +10 -0
  183. package/dist/shared/unity/manifest.js +53 -0
  184. package/dist/shared/unity/mcp.d.ts +8 -0
  185. package/dist/shared/unity/mcp.js +12 -0
  186. package/dist/shared/unity/release.d.ts +28 -0
  187. package/dist/shared/unity/release.js +83 -0
  188. package/dist/shared/unity/status.d.ts +59 -0
  189. package/dist/shared/unity/status.js +45 -0
  190. package/dist/shared/unity/version.d.ts +27 -0
  191. package/dist/shared/unity/version.js +75 -0
  192. package/dist/shared/unreal/status.d.ts +1 -0
  193. package/dist/shared/unreal/status.js +2 -0
  194. package/dist/web/index.html +1 -1
  195. package/dist/web/{main-FNE6XEHA.js → main-VPMYTSUL.js} +67 -67
  196. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  197. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  198. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  199. package/native/scripts/get-store-package.ps1 +58 -0
  200. package/package.json +1 -1
  201. package/sandbox/addons/openupm-cli/package-lock.json +2921 -0
  202. package/sandbox/addons/openupm-cli/package.json +7 -0
  203. package/sandbox/addons/unity-mcp/requirements.in +1 -0
  204. package/sandbox/addons/unity-mcp/requirements.txt +1165 -0
  205. package/sandbox/csharp/AppActivation.cs +38 -0
  206. package/sandbox/scripts/install-store-package.ps1 +28 -0
  207. package/sandbox/scripts/start-store-app.ps1 +29 -0
  208. package/schemas/config.schema.json +61 -6
  209. package/schemas/sandbox-config.schema.json +38 -5
  210. package/dist/guest/devtools/unity.d.ts +0 -2
  211. package/dist/guest/devtools/unity.js +0 -7
@@ -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',
@@ -79,6 +84,7 @@ export const LAUNCH_FILES_GOLDEN = [
79
84
  'sandbox/scripts/get-signature.ps1',
80
85
  'sandbox/scripts/get-vs-setup.ps1',
81
86
  'sandbox/scripts/install-shell-path.ps1',
87
+ 'sandbox/scripts/install-store-package.ps1',
82
88
  'sandbox/scripts/link-profile-dirs.ps1',
83
89
  'sandbox/scripts/new-shortcuts.ps1',
84
90
  'sandbox/scripts/repair-wmi-services.ps1',
@@ -88,6 +94,7 @@ export const LAUNCH_FILES_GOLDEN = [
88
94
  'sandbox/scripts/set-dns-servers.ps1',
89
95
  'sandbox/scripts/set-machine-environment.ps1',
90
96
  'sandbox/scripts/set-registry-values.ps1',
97
+ 'sandbox/scripts/start-store-app.ps1',
91
98
  'sandbox/templates/fivem/README.txt',
92
99
  'sandbox/templates/fivem/cs/Client/Client.csproj',
93
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,6 +139,9 @@ 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[];
@@ -145,7 +149,8 @@ export declare function createFixtures(t: Harness): {
145
149
  unrealEngines: boolean | string[] | "auto";
146
150
  unrealEngineFolders: string[];
147
151
  unrealAssets: boolean | "auto";
148
- unityEditors: boolean | string[];
152
+ unityEditors: boolean | string[] | "auto";
153
+ unityAssets: boolean | "auto";
149
154
  };
150
155
  provisioning: {
151
156
  copyFromHost: boolean;
@@ -212,6 +217,10 @@ export declare function createFixtures(t: Harness): {
212
217
  modules: string[];
213
218
  hub: boolean;
214
219
  keepVersions: number;
220
+ mcp: boolean;
221
+ agentSkills: boolean;
222
+ cliVersion: string;
223
+ assetCache: "pc" | "persisted" | "local";
215
224
  };
216
225
  godot: {
217
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;
@@ -0,0 +1,80 @@
1
+ export const UNITY_SAMPLE = {
2
+ setUp: true,
3
+ checkedAt: '2026-11-05T12:00:00.000Z',
4
+ cli: { version: '1.0.0-beta.16', path: 'C:\\AgentData\\tools\\unity\\cli\\1.0.0-beta.16\\unity.exe' },
5
+ editors: [
6
+ {
7
+ version: '6000.0.58f2',
8
+ dir: 'C:\\PcInstalls\\unity\\6000.0.58f2',
9
+ where: 'this PC, read-only',
10
+ writable: false,
11
+ modules: ['android'],
12
+ sizeBytes: null,
13
+ projects: ['Platformer'],
14
+ },
15
+ {
16
+ version: '2022.3.20f1',
17
+ dir: 'C:\\AgentData\\tools\\unity\\editors\\2022.3.20f1',
18
+ where: 'the data disk, kept between sandboxes',
19
+ writable: true,
20
+ modules: [],
21
+ sizeBytes: 9 * 1024 ** 3,
22
+ projects: ['OldGame'],
23
+ },
24
+ ],
25
+ projects: [
26
+ {
27
+ project: 'Platformer',
28
+ folder: 'F:\\Code\\Platformer',
29
+ version: '6000.0.58f2',
30
+ editor: 'Unity 6000.0.58f2',
31
+ reason: 'the project asks for 6000.0.58f2',
32
+ bridge: 'pipeline',
33
+ pipeline: true,
34
+ },
35
+ {
36
+ project: 'OldGame',
37
+ folder: 'F:\\Code\\OldGame',
38
+ version: '2022.3.20f1',
39
+ editor: 'Unity 2022.3.20f1',
40
+ reason: 'the project asks for 2022.3.20f1',
41
+ bridge: null,
42
+ pipeline: false,
43
+ },
44
+ ],
45
+ licence: { state: 'active', kind: 'Personal', detail: 'Unity Personal', user: 'dev@example.com' },
46
+ hub: { installed: false, setting: false },
47
+ assetCache: { mode: 'persisted', dir: 'C:\\AgentData\\tools\\unity\\asset-store' },
48
+ agentTools: { cliVersion: '1.0.0-beta.16', mcp: 'sbx-unity', mcpSetting: true, skills: true, skillsSetting: true },
49
+ pc: {
50
+ setting: 'auto',
51
+ editors: [{ version: '6000.0.58f2', folder: 'C:\\Program Files\\Unity\\Hub\\Editor\\6000.0.58f2' }],
52
+ problem: null,
53
+ },
54
+ assets: {
55
+ setting: 'auto',
56
+ folders: [
57
+ {
58
+ folder: 'C:\\Users\\dev\\AppData\\Roaming\\Unity\\Asset Store-5.x',
59
+ packages: [
60
+ { publisher: 'Kenney', category: '2D', name: 'Platformer Pack', size: '120 MB' },
61
+ { publisher: 'Synty Studios', category: '3D Models', name: 'POLYGON Fantasy', size: '1.4 GB' },
62
+ ],
63
+ more: 0,
64
+ },
65
+ ],
66
+ },
67
+ problem: null,
68
+ settings: {
69
+ enabled: 'auto',
70
+ acceptLicense: true,
71
+ version: 'project',
72
+ modules: 'none',
73
+ hub: false,
74
+ keepVersions: 2,
75
+ mcp: true,
76
+ agentSkills: true,
77
+ cliVersion: '1.0.0-beta.16',
78
+ assetCache: 'persisted',
79
+ },
80
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const unityAccountsSuite: Suite;
@@ -0,0 +1,106 @@
1
+ // Unity licence and sign-in: the host source (a fake ProgramData and LocalAppData in temp folders), the guest adapter
2
+ // (apply and read back, release with an injected runner) and the Accounts page. Nothing real is read or run.
3
+ import path from 'node:path';
4
+ import { serviceRow, servicePage } from '../app/features/accounts/index.js';
5
+ import { pageLines } from '../cli/output/index.js';
6
+ import { AccountLeases, HOST_SOURCES, ServiceAccountStore } from '../host/accounts/index.js';
7
+ import { unitySource } from '../host/accounts/kinds/unity.js';
8
+ import { SecretStore } from '../host/secrets/index.js';
9
+ import { serviceWanted, unityEntitlementFile, unityUlfFile, } from '../shared/accounts/index.js';
10
+ import { activateUnityLicence, unityAdapterFor } from '../guest/accounts/kinds/unity.js';
11
+ const ULF = '<?xml version="1.0"?><root><License id="Terms"><SerialMasked Value="U3-XXXX-XXXX"/></License></root>';
12
+ const ENTITLEMENT = '<?xml version="1.0"?><Entitlement><Id>abc</Id></Entitlement>';
13
+ const PASSWORD = 'hunter2-unity-pw';
14
+ const SERIAL = 'E3-AAAA-BBBB-CCCC-DDDD-EEEE';
15
+ function message(fn) {
16
+ try {
17
+ fn();
18
+ }
19
+ catch (e) {
20
+ return String(e.message);
21
+ }
22
+ return '';
23
+ }
24
+ export const unityAccountsSuite = {
25
+ name: 'unity-accounts',
26
+ title: 'Unity licence and sign-in',
27
+ run: async (t, f) => {
28
+ t.section('the host source');
29
+ const pcDir = t.dir('unity-pc');
30
+ const pc = {
31
+ programData: path.join(pcDir, 'ProgramData'),
32
+ localAppData: path.join(pcDir, 'Local'),
33
+ appData: path.join(pcDir, 'Roaming'),
34
+ };
35
+ t.check(unitySource.fromPc(pc) === null, 'a PC with no Unity licence gives nothing');
36
+ t.write(unityUlfFile(pc.programData), ULF);
37
+ t.write(unityEntitlementFile(pc.localAppData), ENTITLEMENT);
38
+ const fromPc = unitySource.fromPc(pc);
39
+ t.check(fromPc?.ulf === ULF && fromPc.entitlement === ENTITLEMENT, 'the PC licence and entitlement are read');
40
+ const other = t.dir('unity-other');
41
+ t.write(unityUlfFile(other), ULF);
42
+ t.check(unitySource.fromPc({ ...pc, localAppData: '' }, other)?.ulf === ULF, '--from names another ProgramData folder');
43
+ const virtual = t.dir('unity-virtual');
44
+ t.write(path.join(virtual, 'VirtualStore', 'ProgramData', 'Unity', 'Unity_lic.ulf'), ULF);
45
+ t.check(unitySource.fromPc({ programData: path.join(virtual, 'none'), localAppData: virtual, appData: '' })?.ulf === ULF, 'the VirtualStore copy is the fallback');
46
+ const refusal = message(() => unitySource.fromFile(Buffer.from('hello'), {}));
47
+ t.check(refusal.includes('That is not a Unity licence file (Unity_lic.ulf) or entitlement file.'), 'a file that is not a licence is refused with the plain text', refusal);
48
+ const ulfPayload = unitySource.fromFile(Buffer.from(ULF), { email: 'me@example.com' });
49
+ const entPayload = unitySource.fromFile(Buffer.from(ENTITLEMENT), {});
50
+ t.check(ulfPayload.ulf === ULF && ulfPayload.email === 'me@example.com' && entPayload.entitlement === ENTITLEMENT, 'a ULF becomes ulf, an Entitlement file becomes entitlement, answers are added');
51
+ t.check(message(() => unitySource.fromAnswers({ serial: SERIAL, email: 'me@example.com' })).includes('A serial needs your Unity ID email and password.'), 'a serial without email and password is refused');
52
+ const typed = unitySource.fromAnswers({ email: 'me@example.com', password: PASSWORD, serial: SERIAL });
53
+ t.check(typed.email === 'me@example.com' && typed.password === PASSWORD && typed.serial === SERIAL, 'typed answers');
54
+ const summary = unitySource.summary(typed) ?? '';
55
+ t.check(!summary.includes(PASSWORD) && !summary.includes(SERIAL), 'the summary never holds a secret');
56
+ t.section('the guest adapter');
57
+ const runs = [];
58
+ const run = (file, args) => {
59
+ runs.push(file + ' ' + args.join(' '));
60
+ return Promise.resolve({ code: 0, stdout: '{"status":"active","type":"Personal"}' });
61
+ };
62
+ const dirs = {
63
+ programData: t.dir('unity-guest-pd'),
64
+ localAppData: t.dir('unity-guest-local'),
65
+ secrets: t.dir('unity-guest-secrets'),
66
+ };
67
+ const adapter = unityAdapterFor(dirs, run);
68
+ t.check(adapter.current() === null, 'nothing is held at first');
69
+ const payload = { ulf: ULF, entitlement: ENTITLEMENT, email: 'me@example.com', password: PASSWORD };
70
+ const applied = await adapter.apply(payload);
71
+ const back = adapter.current();
72
+ t.check(back?.ulf === ULF &&
73
+ back.entitlement === ENTITLEMENT &&
74
+ back.email === 'me@example.com' &&
75
+ back.password === PASSWORD, 'apply then current keeps the licence files and the password');
76
+ t.check(!applied.summary.includes(PASSWORD) && !applied.summary.includes('me@example.com'), 'the summary is safe');
77
+ await adapter.release();
78
+ t.check(runs.length === 0, 'release without activated.json runs nothing');
79
+ const serialDirs = { ...dirs, secrets: t.dir('unity-guest-secrets2') };
80
+ const serialRuns = [];
81
+ const record = (file, args, o) => {
82
+ serialRuns.push(file + ' ' + args.join(' '));
83
+ return run(file, args, o);
84
+ };
85
+ const serialAdapter = unityAdapterFor(serialDirs, record);
86
+ await serialAdapter.apply({ email: 'me@example.com', password: PASSWORD, serial: SERIAL });
87
+ const licence = await activateUnityLicence({ editorExe: 'C:\\Editor\\Unity.exe', unityExe: 'C:\\Cli\\unity.exe' }, serialDirs, record);
88
+ t.check(serialRuns[0].includes('-serial') && serialRuns[0].includes('activate.log') && licence.state === 'active', 'a serial is activated with the editor, then the licence status is read');
89
+ t.check(!JSON.stringify(licence).includes(PASSWORD) && !JSON.stringify(licence).includes(SERIAL), 'the licence status holds no secret');
90
+ await serialAdapter.release();
91
+ t.check(serialRuns.some((r) => r.includes('-returnlicense') && r.includes('return.log')), 'release returns the seat once activated.json exists');
92
+ const noLicence = () => Promise.resolve({ code: 0, stdout: '{"licenses":[]}' });
93
+ const none = await activateUnityLicence({ editorExe: null, unityExe: 'C:\\Cli\\unity.exe' }, { programData: t.dir('unity-empty-pd'), localAppData: t.dir('unity-empty-la'), secrets: t.dir('unity-empty-s') }, noLicence);
94
+ t.check(none.state === 'none' && none.detail === 'none: sign in to Unity once in this sandbox', 'no licence: sign in once');
95
+ const refused = await activateUnityLicence({ editorExe: null, unityExe: 'C:\\Cli\\unity.exe', source: 'this PC' }, dirs, noLicence);
96
+ t.check(refused.state === 'invalid' && refused.detail.includes('is not accepted here: sign in again'), 'a licence file that is not accepted says to sign in again');
97
+ t.section('who wants it, and the page');
98
+ t.check(serviceWanted('unity', new Set(['unity'])) && !serviceWanted('unity', new Set(['steam'])), 'only the Unity toolchain wants the Unity sign-in');
99
+ t.check(HOST_SOURCES.unity === unitySource, 'the Unity source is registered');
100
+ const config = f.makeConfig();
101
+ const store = new ServiceAccountStore(config.storage.root, SecretStore.memory());
102
+ const row = serviceRow('unity', config, store, new AccountLeases(config.storage.root));
103
+ const text = pageLines(servicePage(row)).join('\n');
104
+ t.check(row.canImport && row.canLogin && row.warnings.length > 0 && !text.includes('sandbox account'), 'the Accounts section offers import and sign in, and shows no "sandbox account" command text', text);
105
+ },
106
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const unityAgentSuite: Suite;
@@ -0,0 +1,47 @@
1
+ // Unity agent tools: sbx-unity in Claude Code's and Codex's MCP settings, the Apps add-ons (MCP for Unity, openupm-cli)
2
+ // and their lock files. Pure: nothing is installed or run.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { addonMcpServer, APPS, codexWithAddonMcpServers, withAddonMcpServers, } from '../shared/apps/index.js';
6
+ import { UNITY_MCP_NAME, unityCliMcpServer } from '../shared/unity/index.js';
7
+ export const unityAgentSuite = {
8
+ name: 'unity-agent',
9
+ title: 'Unity agent tools: MCP, guide, Apps add-ons',
10
+ run: (t) => {
11
+ t.section('sbx-unity in the agents');
12
+ const server = unityCliMcpServer('C:\\AgentData\\tools\\unity\\cli\\1.0.0\\unity.exe', 'C:\\Workspace\\game');
13
+ const mine = { mine: { type: 'stdio', command: 'mine.exe', args: [], env: {} } };
14
+ // The guest's ENGINE_MCP_NAMES (guest/setup/addon-mcp.ts) cannot be imported here: it loads the guest runtime.
15
+ const engineNames = ['sbx-unreal-editor', UNITY_MCP_NAME];
16
+ const names = [...APPS.flatMap((a) => (a.mcp ? [a.mcp.name] : [])), ...engineNames];
17
+ const on = withAddonMcpServers(mine, { [UNITY_MCP_NAME]: server }, names);
18
+ t.check('mine' in on && UNITY_MCP_NAME in on, 'your own servers are kept and sbx-unity is added');
19
+ const off = withAddonMcpServers(on, {}, names);
20
+ t.check('mine' in off && !(UNITY_MCP_NAME in off), 'sbx-unity is removed when it is off; your servers stay');
21
+ const codex = codexWithAddonMcpServers('', { [UNITY_MCP_NAME]: server }, engineNames);
22
+ t.check(codex.toml.includes('[mcp_servers.sbx-unity]') && codex.toml.includes('args = ["mcp", "--cli-tools"]'), 'Codex gets [mcp_servers.sbx-unity] with args = ["mcp", "--cli-tools"]');
23
+ t.section('Apps add-ons');
24
+ const unity = APPS.find((a) => a.id === 'unity-mcp');
25
+ const built = addonMcpServer('C:\\x', unity.mcp, { exe: 'C:\\x\\venv\\Scripts\\mcp-for-unity.exe' });
26
+ t.check(built?.type === 'stdio' &&
27
+ built.command === 'C:\\x\\venv\\Scripts\\mcp-for-unity.exe' &&
28
+ built.args.join(' ') === '--transport stdio' &&
29
+ built.env.DISABLE_TELEMETRY === '1' &&
30
+ built.env.UNITY_MCP_DISABLE_TELEMETRY === '1' &&
31
+ built.env.MCP_DISABLE_TELEMETRY === '1' &&
32
+ unity.mcp?.name === 'sbx-unity-mcp' &&
33
+ unity.mcp.codex === true, 'MCP for Unity runs mcp-for-unity.exe --transport stdio with telemetry off, for Claude Code and Codex');
34
+ const openupm = APPS.find((a) => a.id === 'openupm-cli');
35
+ t.check(openupm?.exe === 'openupm.cmd' && openupm.category === 'gamedev' && !openupm.mcp, 'openupm-cli is a game-development app');
36
+ const addons = path.join(process.cwd(), 'sandbox', 'addons');
37
+ const reqs = fs.readFileSync(path.join(addons, 'unity-mcp', 'requirements.txt'), 'utf8');
38
+ t.check(fs
39
+ .readFileSync(path.join(addons, 'unity-mcp', 'requirements.in'), 'utf8')
40
+ .includes('mcpforunityserver==10.3.0') &&
41
+ /^mcpforunityserver==10\.3\.0 \\\r?\n\s+--hash=sha256:[0-9a-f]{64}/m.test(reqs), 'the MCP for Unity lock file pins mcpforunityserver 10.3.0 with hashes');
42
+ const lock = fs.readFileSync(path.join(addons, 'openupm-cli', 'package-lock.json'), 'utf8');
43
+ t.check(fs.existsSync(path.join(addons, 'openupm-cli', 'package.json')) &&
44
+ lock.includes('"node_modules/openupm-cli"') &&
45
+ lock.includes('"version": "4.5.3"'), 'the openupm-cli lock file pins 4.5.3');
46
+ },
47
+ };
@@ -0,0 +1,2 @@
1
+ import type { Suite } from './support/suite.js';
2
+ export declare const unityPageSuite: Suite;