jishushell 0.4.30 → 0.5.22

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 (226) hide show
  1. package/Dockerfile.hermes-slim +2 -5
  2. package/apps/anythingllm-container.yaml +287 -0
  3. package/apps/browserless-chromium-container.yaml +18 -6
  4. package/apps/filebrowser-container.yaml +164 -0
  5. package/apps/ollama-binary.yaml +44 -0
  6. package/apps/ollama-with-hollama-binary.yaml +45 -1
  7. package/apps/openclaw-binary.yaml +8 -0
  8. package/apps/openclaw-container.yaml +9 -1
  9. package/apps/openclaw-with-searxng-container.yaml +4 -0
  10. package/apps/searxng-container.yaml +5 -4
  11. package/apps/weknora-container.yaml +471 -0
  12. package/dist/cli/doctor.js +144 -16
  13. package/dist/cli/doctor.js.map +1 -1
  14. package/dist/cli/panel.js.map +1 -1
  15. package/dist/config.d.ts +19 -0
  16. package/dist/config.js +99 -1
  17. package/dist/config.js.map +1 -1
  18. package/dist/install.js +4 -4
  19. package/dist/install.js.map +1 -1
  20. package/dist/routes/auth.js +2 -2
  21. package/dist/routes/auth.js.map +1 -1
  22. package/dist/routes/backup.js +64 -11
  23. package/dist/routes/backup.js.map +1 -1
  24. package/dist/routes/external-mounts.d.ts +17 -0
  25. package/dist/routes/external-mounts.js +73 -0
  26. package/dist/routes/external-mounts.js.map +1 -0
  27. package/dist/routes/file-mounts.d.ts +13 -0
  28. package/dist/routes/file-mounts.js +90 -0
  29. package/dist/routes/file-mounts.js.map +1 -0
  30. package/dist/routes/files-organize.d.ts +28 -0
  31. package/dist/routes/files-organize.js +167 -0
  32. package/dist/routes/files-organize.js.map +1 -0
  33. package/dist/routes/files.d.ts +31 -0
  34. package/dist/routes/files.js +321 -0
  35. package/dist/routes/files.js.map +1 -0
  36. package/dist/routes/instances.js +87 -12
  37. package/dist/routes/instances.js.map +1 -1
  38. package/dist/routes/internal.d.ts +2 -0
  39. package/dist/routes/internal.js +59 -0
  40. package/dist/routes/internal.js.map +1 -0
  41. package/dist/routes/llm.js +29 -0
  42. package/dist/routes/llm.js.map +1 -1
  43. package/dist/routes/setup.js +9 -9
  44. package/dist/routes/setup.js.map +1 -1
  45. package/dist/routes/system.js +1 -1
  46. package/dist/routes/system.js.map +1 -1
  47. package/dist/routes/webdav.d.ts +17 -0
  48. package/dist/routes/webdav.js +114 -0
  49. package/dist/routes/webdav.js.map +1 -0
  50. package/dist/server.js +358 -6
  51. package/dist/server.js.map +1 -1
  52. package/dist/services/agent-apps/catalog.d.ts +3 -0
  53. package/dist/services/agent-apps/catalog.js +40 -13
  54. package/dist/services/agent-apps/catalog.js.map +1 -1
  55. package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
  56. package/dist/services/agent-apps/installers/shell-script.js +19 -2
  57. package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
  58. package/dist/services/agent-apps/types.d.ts +3 -0
  59. package/dist/services/app/app-compiler.d.ts +1 -1
  60. package/dist/services/app/app-compiler.js +5 -5
  61. package/dist/services/app/app-compiler.js.map +1 -1
  62. package/dist/services/app/app-manager.d.ts +9 -0
  63. package/dist/services/app/app-manager.js +248 -43
  64. package/dist/services/app/app-manager.js.map +1 -1
  65. package/dist/services/app/custom-manager.js.map +1 -1
  66. package/dist/services/app/hermes-agent-manager.js +1 -0
  67. package/dist/services/app/hermes-agent-manager.js.map +1 -1
  68. package/dist/services/app/ollama-manager.js +1 -1
  69. package/dist/services/app/ollama-manager.js.map +1 -1
  70. package/dist/services/app/openclaw-manager.js +37 -5
  71. package/dist/services/app/openclaw-manager.js.map +1 -1
  72. package/dist/services/app/platform-transform.d.ts +32 -0
  73. package/dist/services/app/platform-transform.js +65 -0
  74. package/dist/services/app/platform-transform.js.map +1 -0
  75. package/dist/services/app-passwords.d.ts +61 -0
  76. package/dist/services/app-passwords.js +173 -0
  77. package/dist/services/app-passwords.js.map +1 -0
  78. package/dist/services/backup-manager.d.ts +11 -0
  79. package/dist/services/backup-manager.js +220 -8
  80. package/dist/services/backup-manager.js.map +1 -1
  81. package/dist/services/capability-endpoint-validator.js +26 -7
  82. package/dist/services/capability-endpoint-validator.js.map +1 -1
  83. package/dist/services/connection-apply.d.ts +2 -0
  84. package/dist/services/connection-apply.js +55 -1
  85. package/dist/services/connection-apply.js.map +1 -1
  86. package/dist/services/connection-resolver.js +1 -1
  87. package/dist/services/connection-resolver.js.map +1 -1
  88. package/dist/services/connection-transactor.d.ts +2 -0
  89. package/dist/services/connection-transactor.js +12 -2
  90. package/dist/services/connection-transactor.js.map +1 -1
  91. package/dist/services/external-mounts.d.ts +40 -0
  92. package/dist/services/external-mounts.js +187 -0
  93. package/dist/services/external-mounts.js.map +1 -0
  94. package/dist/services/files-manager.d.ts +252 -0
  95. package/dist/services/files-manager.js +1075 -0
  96. package/dist/services/files-manager.js.map +1 -0
  97. package/dist/services/files-mounts.d.ts +42 -0
  98. package/dist/services/files-mounts.js +207 -0
  99. package/dist/services/files-mounts.js.map +1 -0
  100. package/dist/services/instance-manager.js +90 -32
  101. package/dist/services/instance-manager.js.map +1 -1
  102. package/dist/services/llm-proxy/index.d.ts +28 -0
  103. package/dist/services/llm-proxy/index.js +76 -3
  104. package/dist/services/llm-proxy/index.js.map +1 -1
  105. package/dist/services/llm-proxy/ssrf.js +6 -2
  106. package/dist/services/llm-proxy/ssrf.js.map +1 -1
  107. package/dist/services/llm-proxy/validate-key.d.ts +41 -0
  108. package/dist/services/llm-proxy/validate-key.js +672 -0
  109. package/dist/services/llm-proxy/validate-key.js.map +1 -0
  110. package/dist/services/macos-launchd.d.ts +89 -0
  111. package/dist/services/macos-launchd.js +273 -0
  112. package/dist/services/macos-launchd.js.map +1 -0
  113. package/dist/services/nomad-manager.d.ts +11 -0
  114. package/dist/services/nomad-manager.js +343 -98
  115. package/dist/services/nomad-manager.js.map +1 -1
  116. package/dist/services/organize/applier.d.ts +46 -0
  117. package/dist/services/organize/applier.js +218 -0
  118. package/dist/services/organize/applier.js.map +1 -0
  119. package/dist/services/organize/rules.d.ts +57 -0
  120. package/dist/services/organize/rules.js +286 -0
  121. package/dist/services/organize/rules.js.map +1 -0
  122. package/dist/services/organize/scanner.d.ts +50 -0
  123. package/dist/services/organize/scanner.js +366 -0
  124. package/dist/services/organize/scanner.js.map +1 -0
  125. package/dist/services/organize/store.d.ts +14 -0
  126. package/dist/services/organize/store.js +82 -0
  127. package/dist/services/organize/store.js.map +1 -0
  128. package/dist/services/panel-manager.js +40 -11
  129. package/dist/services/panel-manager.js.map +1 -1
  130. package/dist/services/process-manager.js +3 -2
  131. package/dist/services/process-manager.js.map +1 -1
  132. package/dist/services/runtime/adapters/custom.js +56 -0
  133. package/dist/services/runtime/adapters/custom.js.map +1 -1
  134. package/dist/services/runtime/adapters/hermes.d.ts +4 -3
  135. package/dist/services/runtime/adapters/hermes.js +166 -64
  136. package/dist/services/runtime/adapters/hermes.js.map +1 -1
  137. package/dist/services/runtime/adapters/openclaw-routes.d.ts +8 -2
  138. package/dist/services/runtime/adapters/openclaw-routes.js +68 -0
  139. package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -1
  140. package/dist/services/runtime/adapters/openclaw.d.ts +118 -0
  141. package/dist/services/runtime/adapters/openclaw.js +1459 -49
  142. package/dist/services/runtime/adapters/openclaw.js.map +1 -1
  143. package/dist/services/runtime/instance.d.ts +1 -1
  144. package/dist/services/runtime/instance.js +1 -1
  145. package/dist/services/runtime/instance.js.map +1 -1
  146. package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +46 -0
  147. package/dist/services/runtime/mcp-shims/anythingllm-shim.js +281 -0
  148. package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +1 -0
  149. package/dist/services/runtime/mcp-shims/drive-shim.d.ts +54 -0
  150. package/dist/services/runtime/mcp-shims/drive-shim.js +489 -0
  151. package/dist/services/runtime/mcp-shims/drive-shim.js.map +1 -0
  152. package/dist/services/runtime/types.d.ts +31 -0
  153. package/dist/services/setup-manager.js +190 -68
  154. package/dist/services/setup-manager.js.map +1 -1
  155. package/dist/services/suggestions.js.map +1 -1
  156. package/dist/services/update-manager.js +32 -14
  157. package/dist/services/update-manager.js.map +1 -1
  158. package/dist/services/webdav/server.d.ts +24 -0
  159. package/dist/services/webdav/server.js +420 -0
  160. package/dist/services/webdav/server.js.map +1 -0
  161. package/dist/services/webdav/xml-builder.d.ts +73 -0
  162. package/dist/services/webdav/xml-builder.js +156 -0
  163. package/dist/services/webdav/xml-builder.js.map +1 -0
  164. package/dist/services/workspace-builder.d.ts +29 -0
  165. package/dist/services/workspace-builder.js +188 -0
  166. package/dist/services/workspace-builder.js.map +1 -0
  167. package/dist/types.d.ts +61 -0
  168. package/dist/utils/path-locks.d.ts +30 -0
  169. package/dist/utils/path-locks.js +63 -0
  170. package/dist/utils/path-locks.js.map +1 -0
  171. package/dist/utils/path-safety.d.ts +41 -0
  172. package/dist/utils/path-safety.js +119 -0
  173. package/dist/utils/path-safety.js.map +1 -0
  174. package/dist/utils/safe-write.d.ts +24 -0
  175. package/dist/utils/safe-write.js +82 -0
  176. package/dist/utils/safe-write.js.map +1 -0
  177. package/install/jishu-install.sh +247 -35
  178. package/install/jishu-uninstall.sh +45 -5
  179. package/package.json +20 -2
  180. package/public/assets/ApiKeyField-CvyAOcJS.js +1 -0
  181. package/public/assets/Dashboard-AuJESBlJ.js +1 -0
  182. package/public/assets/{HermesChatPanel-_GHoklgo.js → HermesChatPanel-CByPREwb.js} +1 -1
  183. package/public/assets/HermesConfigForm-DRda8FKX.js +4 -0
  184. package/public/assets/InitPassword-ka4wNpM5.js +1 -0
  185. package/public/assets/InstanceDetail-Cg1nS8HX.js +92 -0
  186. package/public/assets/Login-aPajuQzf.js +1 -0
  187. package/public/assets/NewInstance-Dd1ebNIx.js +1 -0
  188. package/public/assets/ProviderRecommendations-DFmADQ7V.js +1 -0
  189. package/public/assets/Settings-BYQnbLYL.js +1 -0
  190. package/public/assets/Setup-D05lwDOV.js +1 -0
  191. package/public/assets/WeixinLoginPanel-D89kdhP4.js +9 -0
  192. package/public/assets/index-HSXCsceK.css +1 -0
  193. package/public/assets/index-bnBu0nlQ.js +19 -0
  194. package/public/assets/registry-C_qeFTkZ.js +2 -0
  195. package/public/assets/usePolling-Bn93fe7M.js +1 -0
  196. package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-flxcMVeP.js} +2 -2
  197. package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-ZC5T_huj.js} +7 -7
  198. package/public/index.html +4 -4
  199. package/scripts/check-app-spec.mjs +18 -4
  200. package/scripts/check-colima-launchd.mjs +230 -0
  201. package/scripts/check-new-file-tests.mjs +230 -0
  202. package/scripts/check-quarantine-expiry.mjs +105 -0
  203. package/scripts/perf/README.md +49 -0
  204. package/scripts/perf/auth.js +99 -0
  205. package/scripts/perf/config.js +63 -0
  206. package/scripts/perf/instances.js +143 -0
  207. package/scripts/perf/proxy.js +96 -0
  208. package/scripts/smoke/files-w1.sh +142 -0
  209. package/scripts/smoke-backend.mjs +122 -0
  210. package/scripts/smoke-post-publish.mjs +346 -0
  211. package/public/assets/Dashboard-rkWp-CXd.js +0 -1
  212. package/public/assets/HermesConfigForm-anDnwUp_.js +0 -4
  213. package/public/assets/InitPassword-ZU9_-hDr.js +0 -1
  214. package/public/assets/InstanceDetail-CN0FH1aw.js +0 -92
  215. package/public/assets/Login-BItXqYAJ.js +0 -1
  216. package/public/assets/NewInstance-BousE6kY.js +0 -1
  217. package/public/assets/ProviderRecommendations-DFYj7Fb6.js +0 -1
  218. package/public/assets/Settings-Bttc6QmM.js +0 -1
  219. package/public/assets/Setup-Bsxx1zgj.js +0 -1
  220. package/public/assets/WeixinLoginPanel-DPZpAKgO.js +0 -9
  221. package/public/assets/index-8xZy1z5k.css +0 -1
  222. package/public/assets/index-Dw3HhUYE.js +0 -19
  223. package/public/assets/input-paste-CrNVAyOy.js +0 -1
  224. package/public/assets/providers-DtNXh9JD.js +0 -1
  225. package/public/assets/registry-5s2UB6is.js +0 -2
  226. package/public/assets/usePolling-Do5Erqm_.js +0 -1
@@ -14,7 +14,35 @@ import { createTask, emitTask, getRunningTasks, getTask } from "../task-registry
14
14
  import { compileTaskRuntime } from "./app-compiler.js";
15
15
  import * as capabilityRegistry from "../capability-registry.js";
16
16
  import { resolveProvideEndpoint } from "./provide-resolver.js";
17
- const DEFAULT_LIFECYCLE_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
17
+ import { platformTransformSpec } from "./platform-transform.js";
18
+ const DEFAULT_LIFECYCLE_PATH_ENTRIES = [
19
+ "/opt/homebrew/bin",
20
+ "/opt/homebrew/sbin",
21
+ "/usr/local/bin",
22
+ "/usr/local/sbin",
23
+ "/usr/bin",
24
+ "/bin",
25
+ "/usr/sbin",
26
+ "/sbin",
27
+ ];
28
+ const DEFAULT_LIFECYCLE_PATH = DEFAULT_LIFECYCLE_PATH_ENTRIES.join(":");
29
+ const MACOS_LIFECYCLE_PATH_PROBES = [
30
+ "/Applications/Docker.app/Contents/Resources/bin",
31
+ ];
32
+ const ANONYMOUS_DOWNLOAD_IMAGE_ALLOWLIST = new Set([
33
+ "filebrowser/filebrowser:latest",
34
+ "ghcr.io/browserless/chromium:latest",
35
+ "ghcr.io/fmaclen/hollama:latest",
36
+ "ghcr.io/open-webui/open-webui:main",
37
+ "mcr.microsoft.com/playwright:v1.55.0-noble",
38
+ "mintplexlabs/anythingllm:latest",
39
+ "paradedb/paradedb:v0.22.2-pg17",
40
+ "redis:7.0-alpine",
41
+ "searxng/searxng:latest",
42
+ "wechatopenai/weknora-app:latest",
43
+ "wechatopenai/weknora-docreader:latest",
44
+ "wechatopenai/weknora-ui:latest",
45
+ ]);
18
46
  function getConfigValue(name) {
19
47
  return name in config ? config[name] : undefined;
20
48
  }
@@ -81,7 +109,6 @@ function parseBuiltinTemplate(fileName, yamlText) {
81
109
  const runtime = typeof task?.runtime === "string" ? task.runtime.trim() : "";
82
110
  return runtime === "container" || runtime === "process";
83
111
  })
84
- && (tasks.length === 1 || isOllamaTemplate)
85
112
  && (serviceRuntime === "container" || serviceRuntime === "process"),
86
113
  suggestedAppType,
87
114
  yaml: yamlText,
@@ -169,15 +196,44 @@ function expandPath(p) {
169
196
  }
170
197
  return p.replace(/^~(?=\/|$)/, process.env.HOME ?? homedir());
171
198
  }
172
- function buildLifecycleEnv() {
173
- const mergedPath = `${process.env.PATH ?? ""}:${DEFAULT_LIFECYCLE_PATH}`
174
- .split(":")
199
+ function buildDeterministicPath(basePath, extraPaths = []) {
200
+ return [basePath ?? "", DEFAULT_LIFECYCLE_PATH, dirname(process.execPath), ...extraPaths]
201
+ .flatMap((entry) => entry.split(":"))
175
202
  .map((entry) => entry.trim())
176
- .filter(Boolean);
203
+ .filter(Boolean)
204
+ .filter((entry, index, entries) => entries.indexOf(entry) === index)
205
+ .join(":");
206
+ }
207
+ function buildLifecycleEnv() {
208
+ const extraPaths = process.platform === "darwin"
209
+ ? MACOS_LIFECYCLE_PATH_PROBES.filter((entry) => existsSync(entry))
210
+ : [];
211
+ const mergedPath = buildDeterministicPath(process.env.PATH, extraPaths);
212
+ // Surface panel callback hooks to lifecycle scripts. post_start can curl
213
+ // ${JISHUSHELL_PANEL_URL}/api/internal/* with the internal token to read
214
+ // panel-managed state (default provider creds etc.) and self-configure
215
+ // without going through user JWT. Best-effort: if the token file or
216
+ // port lookup fails we just omit the vars and the script gets a clean
217
+ // "missing env" failure path.
218
+ const panelHooks = {};
219
+ try {
220
+ panelHooks.JISHUSHELL_PANEL_URL = `http://127.0.0.1:${config.getPanelPort()}`;
221
+ panelHooks.JISHUSHELL_INTERNAL_TOKEN = config.getInternalMcpToken();
222
+ // LAN host that *other* services (and the post_start script's curls into
223
+ // its own service) can reach. AnythingLLM binds eth0 via Nomad's
224
+ // `external` host_network, so the panel-host loopback (127.0.0.1) won't
225
+ // reach 18097 — post_start needs to hit the LAN IP to check its own
226
+ // health. getPanelLanHost() returns the same IP Nomad publishes ports on.
227
+ panelHooks.JISHUSHELL_LAN_HOST = config.getPanelLanHost();
228
+ }
229
+ catch {
230
+ // tolerate — only post_start cares
231
+ }
177
232
  return {
178
233
  ...process.env,
179
234
  HOME: process.env.HOME ?? homedir(),
180
- PATH: [...new Set(mergedPath)].join(":"),
235
+ PATH: mergedPath,
236
+ ...panelHooks,
181
237
  };
182
238
  }
183
239
  const SUDO_PASSTHROUGH_ENV_KEYS = ["HOME", "PATH", "TMPDIR", "TMP", "TEMP", "XDG_RUNTIME_DIR"];
@@ -325,7 +381,7 @@ export async function validateSudoPassword(sudoPassword) {
325
381
  }
326
382
  resolve();
327
383
  });
328
- child.on("error", (error) => {
384
+ child.on("error", (_error) => {
329
385
  resolve();
330
386
  });
331
387
  });
@@ -334,8 +390,11 @@ export async function validateSudoPassword(sudoPassword) {
334
390
  preparedEnv.cleanup();
335
391
  }
336
392
  }
337
- function prepareLifecycleExecEnv(execOptions) {
338
- const env = buildLifecycleEnv();
393
+ function prepareLifecycleExecEnv(execOptions, envOverrides) {
394
+ const env = {
395
+ ...buildLifecycleEnv(),
396
+ ...(envOverrides ?? {}),
397
+ };
339
398
  const sudoPassword = execOptions?.sudoPassword;
340
399
  if (!sudoPassword) {
341
400
  return { env, cleanup: () => undefined };
@@ -364,6 +423,26 @@ function prepareLifecycleExecEnv(execOptions) {
364
423
  },
365
424
  };
366
425
  }
426
+ function shouldBypassDockerCredentialHelperForDownloadImage(image) {
427
+ return ANONYMOUS_DOWNLOAD_IMAGE_ALLOWLIST.has(image.trim());
428
+ }
429
+ function createAnonymousDockerConfig() {
430
+ const dockerConfigDir = mkdtempSync(join(tmpdir(), "jishushell-docker-config-"));
431
+ writeFileSync(join(dockerConfigDir, "config.json"), `${JSON.stringify({ auths: {} }, null, 2)}\n`, { mode: 0o600 });
432
+ return {
433
+ envOverrides: {
434
+ DOCKER_CONFIG: dockerConfigDir,
435
+ },
436
+ cleanup: () => {
437
+ try {
438
+ rmSync(dockerConfigDir, { recursive: true, force: true });
439
+ }
440
+ catch {
441
+ // best effort cleanup for one-shot anonymous docker config files
442
+ }
443
+ },
444
+ };
445
+ }
367
446
  const ANSI_ESCAPE_RE = /\u001B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
368
447
  function sanitizeTaskLine(line) {
369
448
  return line
@@ -687,11 +766,16 @@ function materializeInstalledSpec(spec, appId, offset) {
687
766
  const portShiftMap = buildPortShiftMap(spec, offset);
688
767
  const rewritten = rewriteInstalledSpecValue(spec, spec.id, appId, portShiftMap);
689
768
  const derivedName = deriveInstalledDisplayName(spec, appId);
690
- return normalizeAppSpec({
769
+ const normalized = normalizeAppSpec({
691
770
  ...rewritten,
692
771
  id: spec.id,
693
772
  ...(derivedName ? { name: derivedName } : {}),
694
773
  });
774
+ // Final step: drop spec fields that work on the spec author's Linux
775
+ // baseline but break on the host platform (e.g. host_network:
776
+ // docker_bridge / user: "host" on darwin). Identity pass-through on
777
+ // Linux — see platform-transform.ts for the rule list.
778
+ return platformTransformSpec(normalized);
695
779
  }
696
780
  function deriveInstalledDisplayName(spec, appId) {
697
781
  const baseName = typeof spec.name === "string" && spec.name.trim() ? spec.name.trim() : spec.id;
@@ -801,7 +885,15 @@ async function resolveInstallTarget(spec, originalSpecYaml, requestedAppId) {
801
885
  return {
802
886
  appId,
803
887
  installedSpec,
804
- installedSpecYaml: offset === 0 ? originalSpecYaml : stringify(installedSpec),
888
+ // Always re-serialize from `installedSpec` so the cached yaml
889
+ // reflects every transform `materializeInstalledSpec` ran —
890
+ // including the platform pass that strips host_network/user fields
891
+ // on darwin. Using `originalSpecYaml` for offset=0 (the previous
892
+ // behavior) bypassed the transformer for multi-instance apps'
893
+ // first install slot and re-introduced the Linux-only fields on
894
+ // disk; subsequent panel restarts then re-read the raw source via
895
+ // `loadInstalledAppSpec` and broke macOS placement again.
896
+ installedSpecYaml: stringify(installedSpec),
805
897
  };
806
898
  }
807
899
  throw new Error(`App '${baseId}' 没有可用安装槽位,目录名或端口已全部占用`);
@@ -819,37 +911,57 @@ const DOCKER_PULL_TIMEOUT_MS = 1_800_000;
819
911
  // Separate from the total timeout above: if docker pull stops producing any
820
912
  // stdout/stderr for long enough, treat it as stalled and retry rather than
821
913
  // waiting the full 30 minutes.
822
- const DOCKER_PULL_IDLE_TIMEOUT_MS = 180_000;
823
- async function pullDockerImageStep(label, image, display, task, timeoutMs = DOCKER_PULL_TIMEOUT_MS) {
914
+ //
915
+ // Why 600s (not 180s): on slow links (especially Docker Hub from China to
916
+ // edge devices), large single layers — AnythingLLM ~500MB, Playwright/Hermes
917
+ // ~2.3GB — can spend 5-8 minutes between progress lines without TTY. 180s
918
+ // idle was killing pulls that were actually making progress, then rolling
919
+ // back the partially-completed image. Layer cache is preserved across
920
+ // retries (docker dedupes by layer sha), so a higher idle ceiling lets each
921
+ // big layer finish without throwing away the layers already on disk.
922
+ const DOCKER_PULL_IDLE_TIMEOUT_MS = 600_000;
923
+ async function pullDockerImageStep(label, image, display, task, timeoutMs = DOCKER_PULL_TIMEOUT_MS, idleTimeoutMs) {
824
924
  if (await dockerImageExists(image)) {
825
925
  const skipMessage = `[lifecycle:${label}] docker image '${image}' already exists locally; skipping pull`;
826
926
  process.stdout.write(` ${skipMessage}\n`);
827
927
  emitInstallTaskLog(task, skipMessage);
828
928
  return;
829
929
  }
930
+ const anonymousDockerConfig = shouldBypassDockerCredentialHelperForDownloadImage(image)
931
+ ? createAnonymousDockerConfig()
932
+ : null;
830
933
  let lastError;
831
- for (let attempt = 1; attempt <= DOCKER_PULL_RETRY_ATTEMPTS; attempt++) {
832
- try {
833
- await spawnStepWithTimeout(label, display, display, "docker", ["pull", image], timeoutMs, task, undefined, undefined, { idleTimeoutMs: Math.min(DOCKER_PULL_IDLE_TIMEOUT_MS, timeoutMs) });
834
- return;
835
- }
836
- catch (error) {
837
- if (await dockerImageExists(image)) {
838
- const recoveredMessage = `[lifecycle:${label}] docker image '${image}' is present locally after pull failure/timeout; treating step as successful`;
839
- process.stdout.write(` ${recoveredMessage}\n`);
840
- emitInstallTaskLog(task, recoveredMessage);
934
+ try {
935
+ for (let attempt = 1; attempt <= DOCKER_PULL_RETRY_ATTEMPTS; attempt++) {
936
+ try {
937
+ await spawnStepWithTimeout(label, display, display, "docker", ["pull", image], timeoutMs, task, undefined, undefined, {
938
+ idleTimeoutMs: idleTimeoutMs ?? Math.min(DOCKER_PULL_IDLE_TIMEOUT_MS, timeoutMs),
939
+ envOverrides: anonymousDockerConfig?.envOverrides,
940
+ stallMessageHint: "Docker credential resolution (for example docker-credential-desktop) may be involved.",
941
+ });
841
942
  return;
842
943
  }
843
- lastError = error;
844
- if (attempt === DOCKER_PULL_RETRY_ATTEMPTS) {
845
- break;
944
+ catch (error) {
945
+ if (await dockerImageExists(image)) {
946
+ const recoveredMessage = `[lifecycle:${label}] docker image '${image}' is present locally after pull failure/timeout; treating step as successful`;
947
+ process.stdout.write(` ${recoveredMessage}\n`);
948
+ emitInstallTaskLog(task, recoveredMessage);
949
+ return;
950
+ }
951
+ lastError = error;
952
+ if (attempt === DOCKER_PULL_RETRY_ATTEMPTS) {
953
+ break;
954
+ }
955
+ const reason = error instanceof Error ? error.message : String(error);
956
+ const retryMessage = `[lifecycle:${label}] docker pull failed for ${image} (attempt ${attempt}/${DOCKER_PULL_RETRY_ATTEMPTS}): ${reason}; retrying`;
957
+ process.stdout.write(` ${retryMessage}\n`);
958
+ emitInstallTaskLog(task, retryMessage);
846
959
  }
847
- const reason = error instanceof Error ? error.message : String(error);
848
- const retryMessage = `[lifecycle:${label}] docker pull failed for ${image} (attempt ${attempt}/${DOCKER_PULL_RETRY_ATTEMPTS}): ${reason}; retrying`;
849
- process.stdout.write(` ${retryMessage}\n`);
850
- emitInstallTaskLog(task, retryMessage);
851
960
  }
852
961
  }
962
+ finally {
963
+ anonymousDockerConfig?.cleanup();
964
+ }
853
965
  throw (lastError instanceof Error ? lastError : new Error(String(lastError)));
854
966
  }
855
967
  async function dockerImageExists(image) {
@@ -866,7 +978,7 @@ function spawnStepWithTimeout(label, display, taskDisplay, cmd, args, timeoutMs,
866
978
  process.stdout.write(` [lifecycle:${label}] ${display}\n`);
867
979
  emitInstallTaskLog(task, `[lifecycle:${label}] ${taskDisplay}`);
868
980
  return new Promise((resolve, reject) => {
869
- const preparedEnv = prepareLifecycleExecEnv(sudo ? execOptions : undefined);
981
+ const preparedEnv = prepareLifecycleExecEnv(sudo ? execOptions : undefined, runOptions?.envOverrides);
870
982
  let cleaned = false;
871
983
  let heartbeatTimer = null;
872
984
  let idleTimer = null;
@@ -904,10 +1016,11 @@ function spawnStepWithTimeout(label, display, taskDisplay, cmd, args, timeoutMs,
904
1016
  clearTimeout(idleTimer);
905
1017
  idleTimer = setTimeout(() => {
906
1018
  const idleSeconds = Math.max(1, Math.round(runOptions.idleTimeoutMs / 1000));
907
- const stallMessage = `[lifecycle:${label}] no output for ${idleSeconds}s; terminating stalled step: ${taskDisplay}`;
1019
+ const stallMessageSuffix = runOptions.stallMessageHint ? ` ${runOptions.stallMessageHint}` : "";
1020
+ const stallMessage = `[lifecycle:${label}] no output for ${idleSeconds}s; terminating stalled step: ${taskDisplay}${stallMessageSuffix}`;
908
1021
  process.stdout.write(` ${stallMessage}\n`);
909
1022
  emitInstallTaskLog(task, stallMessage);
910
- forcedError = new Error(`lifecycle '${label}' step stalled after ${idleSeconds}s with no output: ${display}`);
1023
+ forcedError = new Error(`lifecycle '${label}' step stalled after ${idleSeconds}s with no output: ${display}${stallMessageSuffix}`);
911
1024
  child.kill("SIGTERM");
912
1025
  }, runOptions.idleTimeoutMs);
913
1026
  idleTimer.unref?.();
@@ -1108,7 +1221,7 @@ async function runLifecycleSteps(steps, label, artifacts, task, execOptions) {
1108
1221
  }
1109
1222
  }
1110
1223
  else if ("downloadImage" in step) {
1111
- await pullDockerImageStep(label, step.downloadImage, `downloadImage: ${step.downloadImage}`, task, step.timeout_ms);
1224
+ await pullDockerImageStep(label, step.downloadImage, `downloadImage: ${step.downloadImage}`, task, step.timeout_ms, step.idle_timeout_ms);
1112
1225
  artifacts?.push({ type: "image", path: step.downloadImage });
1113
1226
  }
1114
1227
  else if ("deleteImage" in step) {
@@ -1208,7 +1321,7 @@ function cleanupArtifacts(artifacts, task) {
1208
1321
  }
1209
1322
  const DOCKER_IMAGE_RE = /^[a-zA-Z0-9][a-zA-Z0-9\-_.:/@]*$/;
1210
1323
  const APP_ID_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
1211
- const REGISTRY_PATH = join(APPS_DIR, "capability-registry.json");
1324
+ const _REGISTRY_PATH = join(APPS_DIR, "capability-registry.json");
1212
1325
  const INSTALL_LOCK_FILENAME = "install.lock";
1213
1326
  // ── Directory helpers ─────────────────────────────────────────────────────
1214
1327
  function appDirForId(appId) {
@@ -1723,16 +1836,66 @@ function readRegistry() {
1723
1836
  const file = capabilityRegistry.readRegistry();
1724
1837
  return { capabilities: file.capabilities ?? {}, providersByCapability: file.providersByCapability };
1725
1838
  }
1726
- function installedProvidersForCapability(capability) {
1727
- return listApps()
1728
- .filter((app) => app.spec.provides?.some((provide) => provide.capability === capability))
1729
- .map((app) => app.manifest.id);
1730
- }
1731
1839
  // `ensureRequiredCapabilitiesAvailable` was removed in PR 3 sub-step 3c.
1732
1840
  // install never blocks on missing required providers any more —
1733
1841
  // resolveConnections(..., "preCreate") collects them into the `pending`
1734
1842
  // list and the UI surfaces them after install completes.
1843
+ // `listProvidedCapabilities()` does a full filesystem app scan (listApps →
1844
+ // readdir + per-app spec parse) plus a registry read. `augmentInstanceMetadata`
1845
+ // calls it ~2× per instance (getProvidedCapabilitiesForApp +
1846
+ // getEmbeddedUiHintForApp), so `GET /api/instances` over N instances did ~2N
1847
+ // full disk scans per request — the dashboard warm-path cost.
1848
+ //
1849
+ // Cache the result, but key validity on a CHEAP fingerprint of the
1850
+ // underlying state, not on time alone. A time-only TTL left a staleness
1851
+ // window where a freshly installed/uninstalled app (or created/deleted
1852
+ // instance — INSTANCES_DIR also feeds listApps) was missing from / lingered
1853
+ // in the list until the TTL expired, which broke the Connections UI and the
1854
+ // embedded-UI hint. The fingerprint (sorted dir entries of APPS_DIR +
1855
+ // INSTANCES_DIR, plus the registry file mtime) changes the instant the app
1856
+ // set or registry changes — including external mutations — so the cache
1857
+ // self-busts without having to hook every mutation site. It costs two
1858
+ // readdir + one stat, still vastly cheaper than re-parsing every app spec.
1859
+ // The short TTL stays only as a safety net for in-place spec edits that
1860
+ // don't change the dir set; the explicit register/unregister/markStopped
1861
+ // invalidations remain as a precise fast-path for sub-millisecond registry
1862
+ // writes that could share an mtime tick.
1863
+ const PROVIDED_CAPS_TTL_MS = 5_000;
1864
+ let _providedCapsEntry = null;
1865
+ function providedCapabilitiesFingerprint() {
1866
+ const dirEntries = (dir) => {
1867
+ try {
1868
+ return existsSync(dir) ? readdirSync(dir).sort().join(",") : "";
1869
+ }
1870
+ catch {
1871
+ return "";
1872
+ }
1873
+ };
1874
+ let registryMtime = 0;
1875
+ try {
1876
+ registryMtime = lstatSync(_REGISTRY_PATH).mtimeMs;
1877
+ }
1878
+ catch {
1879
+ /* registry file not written yet → 0 */
1880
+ }
1881
+ return `${dirEntries(APPS_DIR)}|${dirEntries(INSTANCES_DIR)}|${registryMtime}`;
1882
+ }
1883
+ export function invalidateProvidedCapabilitiesCache() {
1884
+ _providedCapsEntry = null;
1885
+ }
1735
1886
  export function listProvidedCapabilities() {
1887
+ const fp = providedCapabilitiesFingerprint();
1888
+ const now = Date.now();
1889
+ if (_providedCapsEntry &&
1890
+ _providedCapsEntry.fp === fp &&
1891
+ now - _providedCapsEntry.ts < PROVIDED_CAPS_TTL_MS) {
1892
+ return _providedCapsEntry.data;
1893
+ }
1894
+ const data = computeProvidedCapabilities();
1895
+ _providedCapsEntry = { data, fp, ts: now };
1896
+ return data;
1897
+ }
1898
+ function computeProvidedCapabilities() {
1736
1899
  const reg = readRegistry();
1737
1900
  return listApps().flatMap((app) => (app.spec.provides ?? []).map((provide) => {
1738
1901
  const url = getProvideUrl(provide) ?? undefined;
@@ -1753,6 +1916,7 @@ export function listProvidedCapabilities() {
1753
1916
  ...(provide.visibility ? { visibility: provide.visibility } : {}),
1754
1917
  ...(provide.description ? { description: provide.description } : {}),
1755
1918
  ...(provide.terminal ? { terminal: provide.terminal } : {}),
1919
+ ...(provide.embedded ? { embedded: provide.embedded } : {}),
1756
1920
  ...(address ? { address } : {}),
1757
1921
  registered: Boolean(registered),
1758
1922
  ...(registered?.address ? { registeredAddress: registered.address } : {}),
@@ -1763,11 +1927,38 @@ export function listProvidedCapabilities() {
1763
1927
  export function getProvidedCapabilitiesForApp(appId) {
1764
1928
  return listProvidedCapabilities().filter((entry) => entry.appId === appId);
1765
1929
  }
1930
+ /**
1931
+ * Resolve the runtime endpoint (host + port) for a given app's capability.
1932
+ * Returns the runtime-resolved port when available, falling back to the
1933
+ * declared AppSpec port. This is used by the capability proxy to target the
1934
+ * correct live endpoint after potential port reallocation.
1935
+ */
1936
+ export function resolveRuntimeCapabilityPort(appId, capabilityName) {
1937
+ const app = getApp(appId);
1938
+ if (!app)
1939
+ return null;
1940
+ const provide = (app.spec.provides ?? []).find((p) => p.capability === capabilityName);
1941
+ if (!provide)
1942
+ return null;
1943
+ const resolved = resolveProvideEndpoint(appId, app.spec, provide);
1944
+ return resolved?.hostPort ?? getProvidePort(app.spec, provide);
1945
+ }
1766
1946
  export function getEmbeddedUiHintForApp(appId) {
1767
1947
  const provides = getProvidedCapabilitiesForApp(appId);
1768
1948
  if (!provides.length)
1769
1949
  return null;
1770
- const preferred = provides.find((provide) => provide.capability === BROWSERLESS_DEBUGGER_CAPABILITY);
1950
+ // Selection priority for which provide becomes the embedded UI:
1951
+ // 1. browserless-debugger (special — dev console, not the API)
1952
+ // 2. any capability whose name ends in "-ui" (the canonical Web UI slot,
1953
+ // by convention served at "/"). Apps like AnythingLLM provide both an
1954
+ // API capability (`knowledge-anythingllm`, path `/api/v1`) AND a UI
1955
+ // capability (`anythingllm-ui`, path `/`). Without this preference
1956
+ // the first-iterated provide wins and the iframe ends up pointing
1957
+ // at `/api/v1` → 404.
1958
+ // 3. fall back to natural order for legacy single-capability apps.
1959
+ const browserlessPreferred = provides.find((provide) => provide.capability === BROWSERLESS_DEBUGGER_CAPABILITY);
1960
+ const uiPreferred = provides.find((provide) => provide.capability.endsWith("-ui"));
1961
+ const preferred = browserlessPreferred ?? uiPreferred ?? null;
1771
1962
  const orderedProvides = preferred
1772
1963
  ? [preferred, ...provides.filter((provide) => provide !== preferred)]
1773
1964
  : provides;
@@ -1787,6 +1978,14 @@ export function getEmbeddedUiHintForApp(appId) {
1787
1978
  }
1788
1979
  if (typeof provide.port !== "number" || provide.port < 1)
1789
1980
  continue;
1981
+ // Honor explicit `embedded` opt-in/out on the provide before the
1982
+ // auto-detection logic runs. `"proxy"` short-circuits to the
1983
+ // same-origin proxy path (needed when upstream is firewall-blocked,
1984
+ // emits X-Frame-Options, or otherwise can't be reached by the
1985
+ // browser directly). `"direct"` forces the direct URL even when
1986
+ // listening only on loopback — caller asserts they know what
1987
+ // they're doing.
1988
+ const embeddedMode = provide.embedded ?? "auto";
1790
1989
  // Prefer a direct upstream URL when the container port is published to
1791
1990
  // a LAN-reachable address (Pi with host_network "external", etc.). The
1792
1991
  // same-origin reverse-proxy path is necessary only when the container
@@ -1797,7 +1996,10 @@ export function getEmbeddedUiHintForApp(appId) {
1797
1996
  // absolute URLs starting with `/api/...` (e.g. OpenWebUI), because
1798
1997
  // those calls bypass `<base href>` and hit the panel API instead.
1799
1998
  const listeningHost = legacyInstanceManager.getListeningHostForPort(provide.port);
1800
- const directlyReachable = listeningHost && listeningHost !== "127.0.0.1" && listeningHost !== "::1";
1999
+ const isLoopback = listeningHost === "127.0.0.1" || listeningHost === "::1";
2000
+ const directlyReachable = embeddedMode !== "proxy"
2001
+ && listeningHost
2002
+ && (!isLoopback || embeddedMode === "direct");
1801
2003
  if (directlyReachable) {
1802
2004
  const advertised = legacyInstanceManager.getAdvertisedHostForPort(provide.port);
1803
2005
  const directUrl = `${protocol}://${advertised}:${provide.port}${provide.path ?? ""}`;
@@ -2162,6 +2364,7 @@ export function registerCapabilities(instanceId, spec, portOverride) {
2162
2364
  };
2163
2365
  capabilityRegistry.registerProvider(entry);
2164
2366
  }
2367
+ invalidateProvidedCapabilitiesCache();
2165
2368
  }
2166
2369
  /**
2167
2370
  * Mark an instance's providers as `stopped` (preferred for stop) or
@@ -2171,9 +2374,11 @@ export function registerCapabilities(instanceId, spec, portOverride) {
2171
2374
  */
2172
2375
  export function unregisterCapabilities(instanceId) {
2173
2376
  capabilityRegistry.unregisterProviders(instanceId);
2377
+ invalidateProvidedCapabilitiesCache();
2174
2378
  }
2175
2379
  export function markCapabilitiesStopped(instanceId) {
2176
2380
  capabilityRegistry.setProviderStatus(instanceId, "stopped");
2381
+ invalidateProvidedCapabilitiesCache();
2177
2382
  }
2178
2383
  export function resolveRequires(spec) {
2179
2384
  if (!spec.requires || spec.requires.length === 0)