jishushell 0.4.24 → 0.5.15

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 (281) hide show
  1. package/INSTALL-NOTICE +11 -0
  2. package/apps/anythingllm-container.yaml +287 -0
  3. package/apps/browserless-chromium-container.yaml +90 -0
  4. package/apps/filebrowser-container.yaml +163 -0
  5. package/apps/hermes-container.yaml +36 -2
  6. package/apps/ollama-binary.yaml +91 -90
  7. package/apps/ollama-cpu-container.yaml +8 -1
  8. package/apps/ollama-with-hollama-binary.yaml +91 -90
  9. package/apps/openclaw-binary.yaml +38 -1
  10. package/apps/openclaw-container.yaml +45 -2
  11. package/apps/openclaw-with-ollama-container.yaml +11 -2
  12. package/apps/openclaw-with-searxng-container.yaml +26 -2
  13. package/apps/openwebui-container.yaml +45 -1
  14. package/apps/playwright-container.yaml +7 -1
  15. package/apps/searxng-container.yaml +58 -7
  16. package/apps/weknora-container.yaml +471 -0
  17. package/dist/cli/app.js +79 -9
  18. package/dist/cli/app.js.map +1 -1
  19. package/dist/cli/doctor.d.ts +12 -12
  20. package/dist/cli/doctor.js +242 -55
  21. package/dist/cli/doctor.js.map +1 -1
  22. package/dist/cli/llm.d.ts +4 -3
  23. package/dist/cli/llm.js +4 -3
  24. package/dist/cli/llm.js.map +1 -1
  25. package/dist/cli/panel.d.ts +6 -5
  26. package/dist/cli/panel.js +10 -9
  27. package/dist/cli/panel.js.map +1 -1
  28. package/dist/config.d.ts +19 -0
  29. package/dist/config.js +99 -1
  30. package/dist/config.js.map +1 -1
  31. package/dist/control.d.ts +7 -6
  32. package/dist/control.js +7 -6
  33. package/dist/control.js.map +1 -1
  34. package/dist/install.js +3 -3
  35. package/dist/install.js.map +1 -1
  36. package/dist/routes/agent-apps.d.ts +1 -1
  37. package/dist/routes/agent-apps.js +1 -1
  38. package/dist/routes/apps.js +44 -11
  39. package/dist/routes/apps.js.map +1 -1
  40. package/dist/routes/auth.js +5 -2
  41. package/dist/routes/auth.js.map +1 -1
  42. package/dist/routes/backup.js +64 -11
  43. package/dist/routes/backup.js.map +1 -1
  44. package/dist/routes/external-mounts.d.ts +17 -0
  45. package/dist/routes/external-mounts.js +73 -0
  46. package/dist/routes/external-mounts.js.map +1 -0
  47. package/dist/routes/file-mounts.d.ts +13 -0
  48. package/dist/routes/file-mounts.js +90 -0
  49. package/dist/routes/file-mounts.js.map +1 -0
  50. package/dist/routes/files-organize.d.ts +28 -0
  51. package/dist/routes/files-organize.js +167 -0
  52. package/dist/routes/files-organize.js.map +1 -0
  53. package/dist/routes/files.d.ts +31 -0
  54. package/dist/routes/files.js +321 -0
  55. package/dist/routes/files.js.map +1 -0
  56. package/dist/routes/instances.js +826 -17
  57. package/dist/routes/instances.js.map +1 -1
  58. package/dist/routes/internal.d.ts +2 -0
  59. package/dist/routes/internal.js +59 -0
  60. package/dist/routes/internal.js.map +1 -0
  61. package/dist/routes/llm.js +24 -35
  62. package/dist/routes/llm.js.map +1 -1
  63. package/dist/routes/setup.js +10 -10
  64. package/dist/routes/setup.js.map +1 -1
  65. package/dist/routes/system.js +1 -1
  66. package/dist/routes/system.js.map +1 -1
  67. package/dist/routes/webdav.d.ts +17 -0
  68. package/dist/routes/webdav.js +114 -0
  69. package/dist/routes/webdav.js.map +1 -0
  70. package/dist/server.d.ts +9 -0
  71. package/dist/server.js +751 -20
  72. package/dist/server.js.map +1 -1
  73. package/dist/services/agent-apps/catalog.js +4 -3
  74. package/dist/services/agent-apps/catalog.js.map +1 -1
  75. package/dist/services/agent-apps/index.d.ts +1 -1
  76. package/dist/services/agent-apps/index.js +1 -1
  77. package/dist/services/agent-apps/installers/adapter.d.ts +1 -1
  78. package/dist/services/agent-apps/installers/adapter.js +1 -1
  79. package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
  80. package/dist/services/agent-apps/installers/shell-script.js +3 -3
  81. package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
  82. package/dist/services/agent-apps/types.d.ts +2 -2
  83. package/dist/services/agent-apps/types.js +1 -1
  84. package/dist/services/app/app-compiler.d.ts +1 -1
  85. package/dist/services/app/app-compiler.js +5 -5
  86. package/dist/services/app/app-compiler.js.map +1 -1
  87. package/dist/services/app/app-manager.d.ts +25 -1
  88. package/dist/services/app/app-manager.js +829 -150
  89. package/dist/services/app/app-manager.js.map +1 -1
  90. package/dist/services/app/custom-manager.js.map +1 -1
  91. package/dist/services/app/hermes-agent-manager.js +7 -4
  92. package/dist/services/app/hermes-agent-manager.js.map +1 -1
  93. package/dist/services/app/ollama-manager.js +1 -1
  94. package/dist/services/app/ollama-manager.js.map +1 -1
  95. package/dist/services/app/openclaw-manager.js +20 -3
  96. package/dist/services/app/openclaw-manager.js.map +1 -1
  97. package/dist/services/app/platform-transform.d.ts +32 -0
  98. package/dist/services/app/platform-transform.js +65 -0
  99. package/dist/services/app/platform-transform.js.map +1 -0
  100. package/dist/services/app/provide-resolver.d.ts +29 -0
  101. package/dist/services/app/provide-resolver.js +112 -0
  102. package/dist/services/app/provide-resolver.js.map +1 -0
  103. package/dist/services/app-passwords.d.ts +61 -0
  104. package/dist/services/app-passwords.js +173 -0
  105. package/dist/services/app-passwords.js.map +1 -0
  106. package/dist/services/backup-manager.d.ts +11 -0
  107. package/dist/services/backup-manager.js +177 -4
  108. package/dist/services/backup-manager.js.map +1 -1
  109. package/dist/services/capability-endpoint-validator.d.ts +41 -0
  110. package/dist/services/capability-endpoint-validator.js +104 -0
  111. package/dist/services/capability-endpoint-validator.js.map +1 -0
  112. package/dist/services/capability-health.d.ts +16 -0
  113. package/dist/services/capability-health.js +121 -0
  114. package/dist/services/capability-health.js.map +1 -0
  115. package/dist/services/capability-registry.d.ts +106 -0
  116. package/dist/services/capability-registry.js +313 -0
  117. package/dist/services/capability-registry.js.map +1 -0
  118. package/dist/services/connection-apply.d.ts +91 -0
  119. package/dist/services/connection-apply.js +475 -0
  120. package/dist/services/connection-apply.js.map +1 -0
  121. package/dist/services/connection-resolver.d.ts +65 -0
  122. package/dist/services/connection-resolver.js +281 -0
  123. package/dist/services/connection-resolver.js.map +1 -0
  124. package/dist/services/connection-transactor.d.ts +39 -0
  125. package/dist/services/connection-transactor.js +351 -0
  126. package/dist/services/connection-transactor.js.map +1 -0
  127. package/dist/services/external-mounts.d.ts +40 -0
  128. package/dist/services/external-mounts.js +187 -0
  129. package/dist/services/external-mounts.js.map +1 -0
  130. package/dist/services/files-manager.d.ts +252 -0
  131. package/dist/services/files-manager.js +1075 -0
  132. package/dist/services/files-manager.js.map +1 -0
  133. package/dist/services/files-mounts.d.ts +42 -0
  134. package/dist/services/files-mounts.js +207 -0
  135. package/dist/services/files-mounts.js.map +1 -0
  136. package/dist/services/instance-manager.d.ts +13 -0
  137. package/dist/services/instance-manager.js +138 -46
  138. package/dist/services/instance-manager.js.map +1 -1
  139. package/dist/services/llm-proxy/index.d.ts +16 -2
  140. package/dist/services/llm-proxy/index.js +48 -44
  141. package/dist/services/llm-proxy/index.js.map +1 -1
  142. package/dist/services/llm-proxy/probe.d.ts +6 -0
  143. package/dist/services/llm-proxy/probe.js +85 -0
  144. package/dist/services/llm-proxy/probe.js.map +1 -0
  145. package/dist/services/llm-proxy/ssrf.d.ts +1 -0
  146. package/dist/services/llm-proxy/ssrf.js +24 -9
  147. package/dist/services/llm-proxy/ssrf.js.map +1 -1
  148. package/dist/services/nomad-manager.d.ts +4 -0
  149. package/dist/services/nomad-manager.js +428 -35
  150. package/dist/services/nomad-manager.js.map +1 -1
  151. package/dist/services/organize/applier.d.ts +46 -0
  152. package/dist/services/organize/applier.js +218 -0
  153. package/dist/services/organize/applier.js.map +1 -0
  154. package/dist/services/organize/rules.d.ts +57 -0
  155. package/dist/services/organize/rules.js +286 -0
  156. package/dist/services/organize/rules.js.map +1 -0
  157. package/dist/services/organize/scanner.d.ts +50 -0
  158. package/dist/services/organize/scanner.js +366 -0
  159. package/dist/services/organize/scanner.js.map +1 -0
  160. package/dist/services/organize/store.d.ts +14 -0
  161. package/dist/services/organize/store.js +82 -0
  162. package/dist/services/organize/store.js.map +1 -0
  163. package/dist/services/panel-manager.js +20 -1
  164. package/dist/services/panel-manager.js.map +1 -1
  165. package/dist/services/process-manager.js +4 -3
  166. package/dist/services/process-manager.js.map +1 -1
  167. package/dist/services/runtime/adapters/hermes.d.ts +30 -1
  168. package/dist/services/runtime/adapters/hermes.js +219 -6
  169. package/dist/services/runtime/adapters/hermes.js.map +1 -1
  170. package/dist/services/runtime/adapters/openclaw-mcporter.d.ts +45 -0
  171. package/dist/services/runtime/adapters/openclaw-mcporter.js +108 -0
  172. package/dist/services/runtime/adapters/openclaw-mcporter.js.map +1 -0
  173. package/dist/services/runtime/adapters/openclaw-routes.d.ts +8 -2
  174. package/dist/services/runtime/adapters/openclaw-routes.js +68 -0
  175. package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -1
  176. package/dist/services/runtime/adapters/openclaw.d.ts +177 -0
  177. package/dist/services/runtime/adapters/openclaw.js +1171 -11
  178. package/dist/services/runtime/adapters/openclaw.js.map +1 -1
  179. package/dist/services/runtime/instance.d.ts +1 -1
  180. package/dist/services/runtime/instance.js +1 -1
  181. package/dist/services/runtime/instance.js.map +1 -1
  182. package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +46 -0
  183. package/dist/services/runtime/mcp-shims/anythingllm-shim.js +281 -0
  184. package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +1 -0
  185. package/dist/services/runtime/mcp-shims/drive-shim.d.ts +54 -0
  186. package/dist/services/runtime/mcp-shims/drive-shim.js +489 -0
  187. package/dist/services/runtime/mcp-shims/drive-shim.js.map +1 -0
  188. package/dist/services/runtime/mcp-shims/firewall.d.ts +26 -0
  189. package/dist/services/runtime/mcp-shims/firewall.js +129 -0
  190. package/dist/services/runtime/mcp-shims/firewall.js.map +1 -0
  191. package/dist/services/runtime/mcp-shims/searxng-shim.d.ts +27 -0
  192. package/dist/services/runtime/mcp-shims/searxng-shim.js +125 -0
  193. package/dist/services/runtime/mcp-shims/searxng-shim.js.map +1 -0
  194. package/dist/services/runtime/mcp-shims/write-mcp-entry.d.ts +83 -0
  195. package/dist/services/runtime/mcp-shims/write-mcp-entry.js +127 -0
  196. package/dist/services/runtime/mcp-shims/write-mcp-entry.js.map +1 -0
  197. package/dist/services/runtime/migrations.d.ts +8 -0
  198. package/dist/services/runtime/migrations.js +100 -0
  199. package/dist/services/runtime/migrations.js.map +1 -1
  200. package/dist/services/runtime/types.d.ts +46 -0
  201. package/dist/services/setup-manager.js +99 -24
  202. package/dist/services/setup-manager.js.map +1 -1
  203. package/dist/services/suggestions.d.ts +27 -0
  204. package/dist/services/suggestions.js +133 -0
  205. package/dist/services/suggestions.js.map +1 -0
  206. package/dist/services/task-registry.js +4 -2
  207. package/dist/services/task-registry.js.map +1 -1
  208. package/dist/services/telemetry/device-fingerprint.d.ts +1 -1
  209. package/dist/services/telemetry/device-fingerprint.js +1 -1
  210. package/dist/services/types-shim.d.ts +16 -0
  211. package/dist/services/types-shim.js +2 -0
  212. package/dist/services/types-shim.js.map +1 -0
  213. package/dist/services/webdav/server.d.ts +24 -0
  214. package/dist/services/webdav/server.js +420 -0
  215. package/dist/services/webdav/server.js.map +1 -0
  216. package/dist/services/webdav/xml-builder.d.ts +73 -0
  217. package/dist/services/webdav/xml-builder.js +156 -0
  218. package/dist/services/webdav/xml-builder.js.map +1 -0
  219. package/dist/services/workspace-builder.d.ts +29 -0
  220. package/dist/services/workspace-builder.js +188 -0
  221. package/dist/services/workspace-builder.js.map +1 -0
  222. package/dist/types.d.ts +231 -1
  223. package/dist/utils/instance-lock.d.ts +22 -0
  224. package/dist/utils/instance-lock.js +48 -0
  225. package/dist/utils/instance-lock.js.map +1 -0
  226. package/dist/utils/path-locks.d.ts +30 -0
  227. package/dist/utils/path-locks.js +63 -0
  228. package/dist/utils/path-locks.js.map +1 -0
  229. package/dist/utils/path-safety.d.ts +41 -0
  230. package/dist/utils/path-safety.js +119 -0
  231. package/dist/utils/path-safety.js.map +1 -0
  232. package/dist/utils/safe-json.js +55 -22
  233. package/dist/utils/safe-json.js.map +1 -1
  234. package/dist/utils/safe-write.d.ts +24 -0
  235. package/dist/utils/safe-write.js +82 -0
  236. package/dist/utils/safe-write.js.map +1 -0
  237. package/install/jishu-install.sh +323 -27
  238. package/install/jishu-uninstall.sh +353 -20
  239. package/package.json +18 -1
  240. package/public/assets/Dashboard-BdWPtroF.js +1 -0
  241. package/public/assets/{HermesChatPanel-mFSureyc.js → HermesChatPanel-B_2HlVBQ.js} +1 -1
  242. package/public/assets/HermesConfigForm-DVlhg3WV.js +4 -0
  243. package/public/assets/{InitPassword-CVA8wQA6.js → InitPassword-D7glTExX.js} +1 -1
  244. package/public/assets/InstanceDetail-CxSy2cpe.js +92 -0
  245. package/public/assets/{Login-BWsZH2mu.js → Login-Cfr5c2sv.js} +1 -1
  246. package/public/assets/NewInstance-BIYDmJis.js +1 -0
  247. package/public/assets/ProviderRecommendations-BuRnvRcI.js +1 -0
  248. package/public/assets/Settings-Cc-tYBil.js +1 -0
  249. package/public/assets/Setup-lGZEk5jq.js +1 -0
  250. package/public/assets/{WeixinLoginPanel-CnjR8xMu.js → WeixinLoginPanel-CoGqzxeV.js} +2 -2
  251. package/public/assets/index-87IJXG-w.css +1 -0
  252. package/public/assets/index-BZc5zH7u.js +19 -0
  253. package/public/assets/providers-DtNXh9JD.js +1 -0
  254. package/public/assets/registry-BWnkJgZ1.js +2 -0
  255. package/public/assets/{usePolling-Do5Erqm_.js → usePolling-CwwT9KrC.js} +1 -1
  256. package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-y9V7Sfuu.js} +1 -1
  257. package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-BWrEVJVb.js} +6 -6
  258. package/public/index.html +4 -4
  259. package/scripts/check-app-spec.mjs +457 -0
  260. package/scripts/check-i18n.mjs +154 -0
  261. package/scripts/check-new-file-tests.mjs +230 -0
  262. package/scripts/check-quarantine-expiry.mjs +105 -0
  263. package/scripts/perf/README.md +49 -0
  264. package/scripts/perf/auth.js +99 -0
  265. package/scripts/perf/config.js +63 -0
  266. package/scripts/perf/instances.js +143 -0
  267. package/scripts/perf/proxy.js +96 -0
  268. package/scripts/run.sh +4 -4
  269. package/scripts/smoke/files-w1.sh +142 -0
  270. package/scripts/smoke-backend.mjs +122 -0
  271. package/scripts/smoke-post-publish.mjs +346 -0
  272. package/public/assets/Dashboard-B-JoOjBQ.js +0 -1
  273. package/public/assets/HermesConfigForm-DvR05LK1.js +0 -4
  274. package/public/assets/InstanceDetail-DcZW2QGO.js +0 -91
  275. package/public/assets/NewInstance-BCIrAd86.js +0 -1
  276. package/public/assets/Settings-xkDcduFz.js +0 -1
  277. package/public/assets/Setup-Cfuwj4gV.js +0 -1
  278. package/public/assets/index-CPhVFEsx.css +0 -1
  279. package/public/assets/index-DQsM6Joa.js +0 -19
  280. package/public/assets/providers-V-vwrExZ.js +0 -1
  281. package/public/assets/registry-B4UFJdpA.js +0 -2
@@ -30,7 +30,7 @@ function resolveConfigPath(value, fallback) {
30
30
  }
31
31
  const JISHUSHELL_HOME = resolveConfigPath(getConfigValue("JISHUSHELL_HOME"), join(process.env.HOME ?? homedir(), ".jishushell"));
32
32
  const APPS_DIR = resolveConfigPath(getConfigValue("APPS_DIR"), join(JISHUSHELL_HOME, "apps"));
33
- const INSTANCES_DIR = resolveConfigPath(getConfigValue("INSTANCES_DIR"), join(JISHUSHELL_HOME, "instances"));
33
+ const _INSTANCES_DIR = resolveConfigPath(getConfigValue("INSTANCES_DIR"), join(JISHUSHELL_HOME, "instances"));
34
34
  const getNomadAddrValue = getConfigValue("getNomadAddr");
35
35
  const getNomadDriverValue = getConfigValue("getNomadDriver");
36
36
  const getNomadTokenValue = getConfigValue("getNomadToken");
@@ -415,6 +415,20 @@ function getInstanceAgentType(instanceId) {
415
415
  }
416
416
  }
417
417
  function wrapNomadJob(jid, groupName, task) {
418
+ // Adapters declare port reservations on `task.Resources.Networks` (legacy
419
+ // schema). The docker driver's `Config.ports = [<label>]` lookup, however,
420
+ // resolves labels against the TaskGroup-level `Networks` block. Move (not
421
+ // copy) the network block so the docker driver can find the port and so
422
+ // HostNetwork ("external") is honored — without this, ports publish to
423
+ // 127.0.0.1 by default. Keeping it on both levels would make Nomad reject
424
+ // the job with "port label already in use".
425
+ const taskNetworks = Array.isArray(task?.Resources?.Networks)
426
+ ? task.Resources.Networks
427
+ : [];
428
+ const groupNetworks = taskNetworks.length > 0 ? taskNetworks.map((n) => ({ ...n })) : undefined;
429
+ if (groupNetworks && task?.Resources && typeof task.Resources === "object") {
430
+ delete task.Resources.Networks;
431
+ }
418
432
  return {
419
433
  Job: {
420
434
  ID: jid,
@@ -425,14 +439,24 @@ function wrapNomadJob(jid, groupName, task) {
425
439
  TaskGroups: [{
426
440
  Name: groupName,
427
441
  Count: 1,
442
+ ...(groupNetworks ? { Networks: groupNetworks } : {}),
428
443
  RestartPolicy: {
429
- Attempts: 3,
430
- Interval: 300000000000,
444
+ // 10 attempts × 15s delay = ~2.5min recovery window. Multi-task
445
+ // groups (e.g. weknora-app racing paradedb cold-init) commonly
446
+ // need 3-5 restarts before the dependency's external port-publish
447
+ // settles. Previous 3-attempt cap caused alloc-fail cascades on
448
+ // first-boot of stacks with DB sidecars.
449
+ Attempts: 10,
450
+ Interval: 600000000000,
431
451
  Delay: 15000000000,
432
452
  Mode: "fail",
433
453
  },
434
454
  Reschedule: {
435
- Attempts: 0,
455
+ // Allow Nomad to reschedule the whole alloc up to 3 times if all
456
+ // restarts fail (e.g. transient image pull or host reboot).
457
+ // Unlimited stays false to keep alloc churn bounded.
458
+ Attempts: 3,
459
+ Interval: 3600000000000,
436
460
  Unlimited: false,
437
461
  },
438
462
  Update: {
@@ -457,6 +481,7 @@ async function buildJob(instanceId) {
457
481
  if (legacyManager) {
458
482
  const runtime = legacyManager.buildRuntime(instanceId);
459
483
  const task = legacyManager.buildNomadTask(instanceId, runtime, jid);
484
+ await injectConnectionsRuntimeEnv(instanceId, task);
460
485
  return wrapNomadJob(jid, legacyManager.nomadTaskGroupName(), task);
461
486
  }
462
487
  // Pure adapter dispatch — no more `isHermesInstance()` / kind literals.
@@ -466,11 +491,54 @@ async function buildJob(instanceId) {
466
491
  throw new Error(`Runtime adapter "${agentType}" does not implement buildNomadTask(); cannot schedule Nomad job`);
467
492
  }
468
493
  const task = await adapter.buildNomadTask(instanceId);
494
+ await injectConnectionsRuntimeEnv(instanceId, task);
469
495
  // Task group name mirrors the agentType. Log/status helpers resolve the
470
496
  // Nomad task name via resolveTaskName(instanceId) → adapter.nomadTaskName.
471
497
  const groupName = agentType;
472
498
  return wrapNomadJob(jid, groupName, task);
473
499
  }
500
+ /**
501
+ * Re-resolve `instance.connections` against the live capability registry
502
+ * and merge the resulting env into the freshly-built Nomad task. Idempotent
503
+ * — empty meta.connections short-circuits to a no-op.
504
+ *
505
+ * Resolving at start time (rather than reading the frozen
506
+ * `instance.json["connections-env"]` written by PUT /connections) means
507
+ * provider port / address changes in the registry propagate on next
508
+ * restart without requiring the user to re-bind. Failures here are
509
+ * logged but never block start: a missing required binding still surfaces
510
+ * via the Connections UI status badge.
511
+ */
512
+ async function injectConnectionsRuntimeEnv(instanceId, task) {
513
+ try {
514
+ const meta = getInstance(instanceId);
515
+ const connections = meta?.connections;
516
+ if (!meta || !connections || Object.keys(connections).length === 0)
517
+ return;
518
+ const { loadCapabilitySpecForLegacyInstance } = await import("./runtime/migrations.js");
519
+ const spec = loadCapabilitySpecForLegacyInstance(meta);
520
+ if (!spec)
521
+ return;
522
+ const { resolveConnections } = await import("./connection-resolver.js");
523
+ const { resolved } = resolveConnections(spec, { connections }, "preCreate");
524
+ if (resolved.length === 0)
525
+ return;
526
+ const { RUNTIME_HOOKS } = await import("./connection-apply.js");
527
+ const merged = {};
528
+ for (const binding of resolved) {
529
+ const hook = RUNTIME_HOOKS[binding.category];
530
+ if (!hook)
531
+ continue;
532
+ Object.assign(merged, await hook(meta, binding));
533
+ }
534
+ if (Object.keys(merged).length === 0)
535
+ return;
536
+ task.Env = { ...(task.Env ?? {}), ...merged };
537
+ }
538
+ catch (e) {
539
+ console.warn(`[nomad] connections runtime env merge failed for ${instanceId}: ${e?.message ?? e}`);
540
+ }
541
+ }
474
542
  async function getRunningAlloc(instanceId) {
475
543
  const jid = jobId(instanceId);
476
544
  try {
@@ -607,7 +675,7 @@ export async function getStatus(instanceId) {
607
675
  async function phaseRunningCheck(instanceId) {
608
676
  const status = await getStatus(instanceId);
609
677
  if (status.status === "running") {
610
- return { ok: false, error: "Instance is already running" };
678
+ return { ok: false, error: "Instance is already running", code: "INSTANCE_ALREADY_RUNNING" };
611
679
  }
612
680
  return { ok: true };
613
681
  }
@@ -618,7 +686,7 @@ async function phaseRunningCheck(instanceId) {
618
686
  * each instance owns its own bind-mount) leave the hook unset and this
619
687
  * phase is a no-op.
620
688
  */
621
- async function phaseHomeConflict(instanceId, sharedHomeIds) {
689
+ async function phaseHomeConflict(_instanceId, sharedHomeIds) {
622
690
  const homeConflicts = [];
623
691
  for (const otherId of sharedHomeIds) {
624
692
  const otherStatus = await getStatus(otherId);
@@ -674,6 +742,81 @@ async function phasePreStartHook(adapter, instanceId) {
674
742
  return { ok: false, error: e?.message || String(e) };
675
743
  }
676
744
  }
745
+ /**
746
+ * §17 / PR 9 — re-render adapter-managed connection config from the
747
+ * current capability registry before each instance start.
748
+ *
749
+ * Without this hook, env values (like `SEARCH_API_BASE_URL` =
750
+ * `http://<host>:<port>/search`) are frozen into the adapter's config
751
+ * files at PUT /connections time. When the host IP changes (DHCP
752
+ * renewal, pi reboot picking up a new lease, network move) or a
753
+ * provider gets re-deployed at a different host:port, the consumer
754
+ * keeps trying the stale address and search/llm/etc. silently fail.
755
+ *
756
+ * What this does on every start:
757
+ * 1. Read connections from instance.json
758
+ * 2. Re-resolve them in `runtime` mode (tolerant: ambiguous/missing
759
+ * becomes empty resolved instead of throwing — start should still
760
+ * proceed even if one binding can't be re-rendered)
761
+ * 3. Collect env via the same persist hooks PUT /connections uses
762
+ * 4. Call adapter.applyConnectionEnv with the fresh env so the
763
+ * adapter rewrites its config files (mcp_servers / openclaw.json /
764
+ * etc.) with the current address
765
+ *
766
+ * Failures here are logged but never block start: a stale config is
767
+ * better than no start. If something is genuinely wrong with the
768
+ * registry, the user will see a connection error in the UI on next
769
+ * use — at which point they can re-bind manually.
770
+ */
771
+ async function phaseRefreshConnections(adapter, instanceId) {
772
+ if (!adapter.applyConnectionEnv)
773
+ return;
774
+ try {
775
+ const meta = getInstance(instanceId);
776
+ const connections = meta?.connections;
777
+ if (!meta || !connections || Object.keys(connections).length === 0)
778
+ return;
779
+ const { loadCapabilitySpecForLegacyInstance } = await import("./runtime/migrations.js");
780
+ const spec = loadCapabilitySpecForLegacyInstance(meta);
781
+ if (!spec)
782
+ return;
783
+ const { resolveConnections } = await import("./connection-resolver.js");
784
+ const { resolved } = resolveConnections(spec, { connections }, "preCreate");
785
+ if (resolved.length === 0)
786
+ return;
787
+ const { PERSIST_HOOKS } = await import("./connection-apply.js");
788
+ const merged = {};
789
+ const seenEnvKeys = new Set();
790
+ // Accumulate env across all resolved bindings via a stub
791
+ // writeConnectionEnv that just collects into `merged`. We don't
792
+ // run the real persist hooks here because those would re-write
793
+ // generic-app `connections-env` (already handled by
794
+ // injectConnectionsRuntimeEnv); we only want the env so we can
795
+ // pass it to adapter.applyConnectionEnv below.
796
+ const stubCtx = {
797
+ registry: await import("./capability-registry.js"),
798
+ adapter: { applyConnectionEnv: undefined },
799
+ async writeConnectionEnv(_inst, env) {
800
+ for (const [k, v] of Object.entries(env)) {
801
+ merged[k] = v;
802
+ seenEnvKeys.add(k);
803
+ }
804
+ },
805
+ };
806
+ for (const binding of resolved) {
807
+ const hook = PERSIST_HOOKS[binding.category];
808
+ if (!hook)
809
+ continue;
810
+ await hook(meta, binding, stubCtx);
811
+ }
812
+ if (seenEnvKeys.size === 0)
813
+ return;
814
+ await adapter.applyConnectionEnv(instanceId, merged);
815
+ }
816
+ catch (e) {
817
+ console.warn(`[nomad] connections refresh failed for ${instanceId}: ${e?.message ?? e}`);
818
+ }
819
+ }
677
820
  /**
678
821
  * Phase 5: submit to Nomad with a single retry on port race. Between our
679
822
  * earlier host probe and Docker's actual bind another process could have
@@ -741,8 +884,12 @@ export async function startInstance(instanceId) {
741
884
  return { ok: false, phase, ...rest };
742
885
  };
743
886
  const running = await phaseRunningCheck(instanceId);
744
- if (!running.ok)
745
- return failed("running_check", { error: running.error });
887
+ if (!running.ok) {
888
+ const extra = { error: running.error };
889
+ if (running.code)
890
+ extra.code = running.code;
891
+ return failed("running_check", extra);
892
+ }
746
893
  const legacyManager = await getLegacyAppManager(instanceId);
747
894
  if (legacyManager) {
748
895
  const prep = await legacyManager.prepareStart(instanceId);
@@ -761,6 +908,11 @@ export async function startInstance(instanceId) {
761
908
  const home = await phaseHomeConflict(instanceId, adapter.findInstancesSharingHome?.(instanceId) ?? []);
762
909
  if (!home.ok)
763
910
  return failed("home_conflict", { error: home.error });
911
+ // PR 9 — refresh adapter-managed connection config from current
912
+ // capability registry before adapter pre-start. Best-effort: never
913
+ // blocks start (any failure is logged and we proceed with the
914
+ // existing on-disk config). See phaseRefreshConnections doc.
915
+ await phaseRefreshConnections(adapter, instanceId);
764
916
  const hook = await phasePreStartHook(adapter, instanceId);
765
917
  if (!hook.ok) {
766
918
  const extra = { error: hook.error };
@@ -777,12 +929,65 @@ export async function startInstance(instanceId) {
777
929
  const submit = await phaseSubmit(instanceId, port.portAllocation);
778
930
  if (!submit.ok)
779
931
  return failed("submit", { error: submit.error });
932
+ // Auto-register capability providers for legacy instances so they appear
933
+ // in the Connections UI alongside app-installed apps. App-dir installed
934
+ // apps short-circuit at the top of this function and don't reach here.
935
+ await registerLegacyCapabilitiesTopLevel(instanceId);
780
936
  return {
781
937
  ok: true,
782
938
  eval_id: submit.evalId,
783
939
  ...(submit.portAllocation ? { port_allocation: submit.portAllocation } : {}),
784
940
  };
785
941
  }
942
+ /**
943
+ * Best-effort capability registration for legacy (non-app-installed)
944
+ * hermes/openclaw instances. Loaded synthetic spec via the migrations
945
+ * helper; failures are logged but never block start/stop.
946
+ */
947
+ async function registerLegacyCapabilitiesTopLevel(instanceId) {
948
+ try {
949
+ const meta = getInstance(instanceId);
950
+ if (!meta)
951
+ return;
952
+ const { loadCapabilitySpecForLegacyInstance } = await import("./runtime/migrations.js");
953
+ const synthSpec = loadCapabilitySpecForLegacyInstance(meta);
954
+ if (!synthSpec?.provides?.length)
955
+ return;
956
+ // The synthetic spec's `name` is the yaml template's display name
957
+ // ("Hermes Agent" / "OpenClaw Container"). For Connections-tab UX we
958
+ // want the candidate to surface the user's instance name (e.g. "h",
959
+ // "claw11"), so override before handing it to registerCapabilities.
960
+ const instanceName = typeof meta.name === "string" && meta.name
961
+ ? meta.name
962
+ : instanceId;
963
+ const namedSpec = { ...synthSpec, name: instanceName };
964
+ // The synthetic spec has `tasks: []`, so `resolveProvideEndpoint`
965
+ // can't compute the actual gateway port and falls back to the yaml's
966
+ // declared port (e.g. openclaw default 18789). Legacy instances may
967
+ // have been allocated a different port at creation time (port
968
+ // collision avoidance). Pass the live `getGatewayPort` so the
969
+ // capability registry advertises the port consumers can actually
970
+ // reach.
971
+ const portOverride = getGatewayPort(instanceId);
972
+ const { registerCapabilities } = await import("./app/app-manager.js");
973
+ registerCapabilities(instanceId, namedSpec, portOverride > 0 ? portOverride : undefined);
974
+ }
975
+ catch (e) {
976
+ console.warn(`[legacy-capabilities] register failed for ${instanceId}: ${e?.message ?? e}`);
977
+ }
978
+ }
979
+ async function unregisterLegacyCapabilitiesTopLevel(instanceId, purge) {
980
+ try {
981
+ const { markCapabilitiesStopped, unregisterCapabilities } = await import("./app/app-manager.js");
982
+ if (purge)
983
+ unregisterCapabilities(instanceId);
984
+ else
985
+ markCapabilitiesStopped(instanceId);
986
+ }
987
+ catch (e) {
988
+ console.warn(`[legacy-capabilities] unregister failed for ${instanceId}: ${e?.message ?? e}`);
989
+ }
990
+ }
786
991
  export async function stopInstance(instanceId, purge = false) {
787
992
  const jid = jobId(instanceId);
788
993
  try {
@@ -794,10 +999,14 @@ export async function stopInstance(instanceId, purge = false) {
794
999
  }
795
1000
  catch { /* ignore */ }
796
1001
  }
1002
+ await unregisterLegacyCapabilitiesTopLevel(instanceId, purge);
797
1003
  return { ok: true };
798
1004
  }
799
- if (resp.status === 404)
1005
+ if (resp.status === 404) {
1006
+ // Already stopped — still mark capabilities stopped so the UI reflects state.
1007
+ await unregisterLegacyCapabilitiesTopLevel(instanceId, purge);
800
1008
  return { ok: false, error: "Instance is not running" };
1009
+ }
801
1010
  return { ok: false, error: await resp.text() };
802
1011
  }
803
1012
  catch (e) {
@@ -828,6 +1037,11 @@ export async function restartInstance(instanceId) {
828
1037
  const meta = getInstance(instanceId);
829
1038
  const agentType = resolveAgentType(meta);
830
1039
  const adapter = getAdapter(agentType);
1040
+ // PR 9 — refresh connection-derived config (mcp_servers /
1041
+ // openclaw.json plugins) from current capability registry so
1042
+ // host IP changes propagate on restart without manual re-bind.
1043
+ // Symmetric with the same call in startInstance.
1044
+ await phaseRefreshConnections(adapter, instanceId);
831
1045
  if (adapter.hooks?.onBeforeStart) {
832
1046
  await adapter.hooks.onBeforeStart({ instanceId });
833
1047
  }
@@ -841,8 +1055,12 @@ export async function restartInstance(instanceId) {
841
1055
  TaskName: resolveTaskName(instanceId),
842
1056
  AllTasks: false,
843
1057
  });
844
- if (resp.ok)
1058
+ if (resp.ok) {
1059
+ // Re-register capabilities — yaml provides may have changed since
1060
+ // the last start (e.g. a panel upgrade adding `llm-agent`).
1061
+ await registerLegacyCapabilitiesTopLevel(instanceId);
845
1062
  return { ok: true, alloc_id: alloc.ID };
1063
+ }
846
1064
  // Non-2xx from the restart endpoint falls through to stop+start
847
1065
  const errText = await resp.text();
848
1066
  console.warn(`[nomad] Native restart failed for ${instanceId} (HTTP ${resp.status}): ${errText} — falling back to stop+start`);
@@ -1005,7 +1223,7 @@ var UnifiedNomadJobs;
1005
1223
  const MAX_MEMORY_MAX_MB = 4096; // 4 GB hard limit
1006
1224
  const DEFAULT_PIDS_LIMIT = 512;
1007
1225
  const NOMAD_CONFIG_PATH = join(JISHUSHELL_HOME, "nomad", "nomad.hcl");
1008
- const DEFAULT_CWD = homedir();
1226
+ const _DEFAULT_CWD = homedir();
1009
1227
  function appDirForId(appId) {
1010
1228
  return join(APPS_DIR, appId);
1011
1229
  }
@@ -1199,12 +1417,12 @@ var UnifiedNomadJobs;
1199
1417
  if (!s)
1200
1418
  return defaultNs;
1201
1419
  if (s.endsWith("ms"))
1202
- return parseInt(s) * 1_000_000;
1420
+ return parseInt(s, 10) * 1_000_000;
1203
1421
  if (s.endsWith("s"))
1204
- return parseInt(s) * 1_000_000_000;
1422
+ return parseInt(s, 10) * 1_000_000_000;
1205
1423
  if (s.endsWith("m"))
1206
- return parseInt(s) * 60_000_000_000;
1207
- return parseInt(s) * 1_000_000_000;
1424
+ return parseInt(s, 10) * 60_000_000_000;
1425
+ return parseInt(s, 10) * 1_000_000_000;
1208
1426
  }
1209
1427
  function portLabel(taskName, portName) {
1210
1428
  const sanitize = (value) => value.replace(/[^a-zA-Z0-9_-]/g, "-");
@@ -1225,10 +1443,19 @@ var UnifiedNomadJobs;
1225
1443
  function hostNetworkForPort(port) {
1226
1444
  if ((port.visibility ?? "external") === "internal")
1227
1445
  return undefined;
1446
+ // Honor explicit host_network from spec (e.g. weknora's ports declare
1447
+ // `host_network: docker_bridge` so peer tasks reach the published port
1448
+ // via host.docker.internal). Verify the named network is actually
1449
+ // declared in nomad.hcl — otherwise Nomad rejects job submission with
1450
+ // "unknown host network". Falls back to "external" when unset.
1451
+ const requested = (port.host_network ?? "").trim();
1452
+ if (requested && nomadConfigDeclaresHostNetwork(requested))
1453
+ return requested;
1228
1454
  return nomadConfigDeclaresHostNetwork("external") ? "external" : undefined;
1229
1455
  }
1230
1456
  function specRequiresExternalHostNetwork(spec) {
1231
- return spec.tasks.some((task) => (task.ports ?? []).some((port) => (port.visibility ?? "external") !== "internal"));
1457
+ return spec.tasks.some((task) => task.runtime !== "container"
1458
+ && (task.ports ?? []).some((port) => (port.visibility ?? "external") !== "internal"));
1232
1459
  }
1233
1460
  async function validateRequiredHostNetworks(spec) {
1234
1461
  if (!specRequiresExternalHostNetwork(spec))
@@ -1268,7 +1495,18 @@ var UnifiedNomadJobs;
1268
1495
  Label: portLabel(task.name, port.name),
1269
1496
  Value: port.host_port ?? port.port,
1270
1497
  ...(task.runtime === "container" ? { To: port.container_port ?? port.port } : {}),
1271
- ...(hostNetworkForPort(port) ? { HostNetwork: hostNetworkForPort(port) } : {}),
1498
+ // Attach the named host_network for any externally-visible port —
1499
+ // including container tasks. Without it, Nomad falls back to
1500
+ // HostNetwork="default" (loopback) and the docker driver publishes
1501
+ // to 127.0.0.1, breaking cross-container consumers (e.g. OpenWebUI
1502
+ // calling hermes / openclaw via the connections page). The earlier
1503
+ // restriction to non-container tasks was overcautious — our task
1504
+ // groups use host networking (Mode: "") rather than Nomad bridge
1505
+ // mode, so attaching host_network is safe and is exactly what
1506
+ // searxng-container has been doing in practice all along.
1507
+ ...(hostNetworkForPort(port)
1508
+ ? { HostNetwork: hostNetworkForPort(port) }
1509
+ : {}),
1272
1510
  }));
1273
1511
  }
1274
1512
  // ── Health check → Nomad service check builder ────────────────────────────
@@ -1489,12 +1727,6 @@ var UnifiedNomadJobs;
1489
1727
  return null;
1490
1728
  return command.replace(/^~(?=\/|$)/, homedir());
1491
1729
  }
1492
- function taskCommandLine(task) {
1493
- const command = expandTaskCommand(task.command);
1494
- if (!command)
1495
- return null;
1496
- return [command, ...(task.args ?? []).map(String)].join(" ").trim();
1497
- }
1498
1730
  function commandLineMatchesTask(commandLine, task) {
1499
1731
  const normalized = commandLine.trim();
1500
1732
  const command = expandTaskCommand(task.command);
@@ -2013,17 +2245,73 @@ var UnifiedNomadJobs;
2013
2245
  const src = v.source.replace(/^~(?=\/|$)/, homedir());
2014
2246
  return `${src}:${v.target}${v.readonly ? ":ro" : ":rw"}`;
2015
2247
  });
2248
+ // Resolve container task user. On Linux we default to the panel
2249
+ // process's host uid:gid so bind-mounted data dirs (owned by the panel
2250
+ // user, typically `pi`) are writable without forcing the container to
2251
+ // run as root and without needing chown / DAC_OVERRIDE gymnastics.
2252
+ // yaml override `user: "<uid>:<gid>"` wins; explicit `user: "root"` or
2253
+ // `user: "0:0"` keeps the image's root default.
2254
+ //
2255
+ // On macOS we skip the host-uid default. Docker on Mac runs inside a
2256
+ // Linux VM (Colima/Docker Desktop) with its own uid namespace — host
2257
+ // uids like 501 (the standard macOS first-user) are virtualised away
2258
+ // by virtiofs and almost never exist in the image's /etc/passwd. Some
2259
+ // images crash hard when started as an unknown uid: e.g. browserless
2260
+ // calls Node.js `os.userInfo()` very early, which throws
2261
+ // `uv_os_get_passwd returned ENOENT` and the container exits before
2262
+ // the port ever binds. Letting the image's default USER directive
2263
+ // take effect is correct on Mac; users who do need bind-mount
2264
+ // ownership control can still set yaml `user:` explicitly.
2265
+ const containerUser = (() => {
2266
+ if (task.runtime !== "container")
2267
+ return undefined;
2268
+ const declared = typeof task.user === "string" ? task.user.trim() : "";
2269
+ if (declared === "host" || declared === "") {
2270
+ if (process.platform === "darwin")
2271
+ return undefined;
2272
+ const uid = process.getuid?.() ?? 1000;
2273
+ const gid = process.getgid?.() ?? 1000;
2274
+ return `${uid}:${gid}`;
2275
+ }
2276
+ return declared;
2277
+ })();
2278
+ // Validate cap_add against a tight allowlist. The full Linux cap surface
2279
+ // is large; we only honor capabilities image entrypoints actually need
2280
+ // for the canonical "start as root + gosu to user" pattern. Anything
2281
+ // outside this list is silently dropped — failing closed protects
2282
+ // against typo'd / hostile specs widening the container's capability
2283
+ // bounds beyond what the panel author signed off on.
2284
+ const ALLOWED_CAPS = new Set([
2285
+ "CHOWN", "DAC_OVERRIDE", "FOWNER",
2286
+ "SETUID", "SETGID", "SETPCAP", "NET_BIND_SERVICE",
2287
+ ]);
2288
+ const capAdd = Array.isArray(task.cap_add)
2289
+ ? task.cap_add
2290
+ .map((c) => typeof c === "string" ? c.trim().toUpperCase().replace(/^CAP_/, "") : "")
2291
+ .filter((c) => ALLOWED_CAPS.has(c))
2292
+ : [];
2016
2293
  const taskDef = {
2017
2294
  Name: task.name,
2018
2295
  Driver: "docker",
2296
+ ...(containerUser ? { User: containerUser } : {}),
2019
2297
  Config: {
2020
2298
  image,
2021
2299
  force_pull: false,
2300
+ // Nomad's docker driver default `image_pull_timeout` is 5 minutes;
2301
+ // on Raspberry Pi or other constrained networks a 1+ GiB image
2302
+ // (Open WebUI, OpenClaw, Hermes) can exceed that and the alloc
2303
+ // restart-loops with "Failed to pull: context deadline exceeded"
2304
+ // before it ever starts. Raise to 15 minutes — long enough for
2305
+ // realistic Pi-class pulls, short enough that a genuinely
2306
+ // unreachable registry still surfaces as a failure within a
2307
+ // bounded window.
2308
+ image_pull_timeout: "15m",
2022
2309
  ...(task.command ? { command: String(task.command) } : {}),
2023
2310
  args,
2024
2311
  ...(publishedPorts.length > 0 ? { ports: publishedPorts } : {}),
2025
2312
  extra_hosts: ["host.docker.internal:host-gateway"],
2026
2313
  cap_drop: ["ALL"],
2314
+ ...(capAdd.length > 0 ? { cap_add: capAdd } : {}),
2027
2315
  security_opt: ["no-new-privileges"],
2028
2316
  pids_limit: DEFAULT_PIDS_LIMIT,
2029
2317
  readonly_rootfs: false,
@@ -2093,13 +2381,17 @@ var UnifiedNomadJobs;
2093
2381
  ? { Networks: [{ ReservedPorts: groupReservedPorts }] }
2094
2382
  : {}),
2095
2383
  RestartPolicy: {
2096
- Attempts: 3,
2097
- Interval: 300_000_000_000,
2384
+ // 10 attempts × 15s delay = ~2.5min recovery window. Multi-task
2385
+ // app groups commonly need 3-5 restarts before sidecar
2386
+ // dependencies' external port-publish settles.
2387
+ Attempts: 10,
2388
+ Interval: 600_000_000_000,
2098
2389
  Delay: 15_000_000_000,
2099
2390
  Mode: "fail",
2100
2391
  },
2101
2392
  Reschedule: {
2102
- Attempts: 0,
2393
+ Attempts: 3,
2394
+ Interval: 3_600_000_000_000,
2103
2395
  Unlimited: false,
2104
2396
  },
2105
2397
  Update: {
@@ -2224,6 +2516,7 @@ var UnifiedNomadJobs;
2224
2516
  }
2225
2517
  return statuses[0];
2226
2518
  }
2519
+ UnifiedNomadJobs.aggregateHealthStatus = aggregateHealthStatus;
2227
2520
  async function getRunningAlloc(appId) {
2228
2521
  return pickLiveAlloc(await getAllocs(appId));
2229
2522
  }
@@ -2602,6 +2895,10 @@ var UnifiedNomadJobs;
2602
2895
  if (nomadStopped) {
2603
2896
  const allocsStopped = await waitForAllocationsToStop(liveAllocIds);
2604
2897
  if (!allocsStopped) {
2898
+ const lingeringAlloc = await getRunningAlloc(appId);
2899
+ if (!lingeringAlloc) {
2900
+ return { ok: true };
2901
+ }
2605
2902
  return { ok: false, error: `App '${appId}' allocations did not stop in time` };
2606
2903
  }
2607
2904
  return { ok: true };
@@ -3155,16 +3452,87 @@ var UnifiedNomadJobs;
3155
3452
  return getGenericJobStatus(nomadJobId);
3156
3453
  }
3157
3454
  UnifiedNomadJobs.getInstanceStatus = getInstanceStatus;
3455
+ /**
3456
+ * Capability registration shim for **legacy** (non-app-installed)
3457
+ * hermes/openclaw instances. Loads the synthetic spec via
3458
+ * loadCapabilitySpecForLegacyInstance and routes provides through the
3459
+ * app-manager registry helpers, so legacy instances surface in
3460
+ * Connections candidate lists like app-installed ones.
3461
+ *
3462
+ * Errors are swallowed and logged — capability registration is best-
3463
+ * effort; a failure here must not block start/stop.
3464
+ */
3465
+ async function registerLegacyCapabilities(instanceId) {
3466
+ try {
3467
+ const meta = getInstance(instanceId);
3468
+ if (!meta)
3469
+ return;
3470
+ const { loadCapabilitySpecForLegacyInstance } = await import("./runtime/migrations.js");
3471
+ const synthSpec = loadCapabilitySpecForLegacyInstance(meta);
3472
+ if (!synthSpec?.provides?.length)
3473
+ return;
3474
+ // Same instance-name override + portOverride passthrough as
3475
+ // registerLegacyCapabilitiesTopLevel — Connections candidates should
3476
+ // show the user's instance name and advertise the actually-allocated
3477
+ // gateway port (not the yaml default).
3478
+ const instanceName = typeof meta.name === "string" && meta.name
3479
+ ? meta.name
3480
+ : instanceId;
3481
+ const namedSpec = { ...synthSpec, name: instanceName };
3482
+ const portOverride = getGatewayPort(instanceId);
3483
+ const { registerCapabilities } = await import("./app/app-manager.js");
3484
+ registerCapabilities(instanceId, namedSpec, portOverride > 0 ? portOverride : undefined);
3485
+ }
3486
+ catch (e) {
3487
+ console.warn(`[legacy-capabilities] register failed for ${instanceId}: ${e?.message ?? e}`);
3488
+ }
3489
+ }
3490
+ async function unregisterLegacyCapabilities(instanceId, purge) {
3491
+ try {
3492
+ const { markCapabilitiesStopped, unregisterCapabilities } = await import("./app/app-manager.js");
3493
+ if (purge) {
3494
+ unregisterCapabilities(instanceId);
3495
+ }
3496
+ else {
3497
+ markCapabilitiesStopped(instanceId);
3498
+ }
3499
+ }
3500
+ catch (e) {
3501
+ console.warn(`[legacy-capabilities] unregister failed for ${instanceId}: ${e?.message ?? e}`);
3502
+ }
3503
+ }
3158
3504
  async function startInstance(nomadJobId) {
3159
3505
  const instanceBackedApp = await getInstanceBackedInstalledApp(nomadJobId);
3160
3506
  if (instanceBackedApp) {
3507
+ // PR 3 sub-step 3d: switch to resolveConnections in runtime mode so
3508
+ // missing required producers / ambiguous prefix candidates throw with
3509
+ // structured codes (412 / 409 / 400). Read the live instance.json so
3510
+ // UI bindings persisted via PUT /connections (PR 4) drive the
3511
+ // resolution; fall back to a stub `{ connections: {} }` when the
3512
+ // instance file isn't readable yet.
3161
3513
  let extraEnv = {};
3162
3514
  try {
3163
- const { resolveRequires } = await import("./app/app-manager.js");
3164
- extraEnv = resolveRequires(instanceBackedApp.spec);
3515
+ const { resolveConnections, resolvedToLegacyEnv } = await import("./connection-resolver.js");
3516
+ const legacyInstanceManager = await import("./instance-manager.js");
3517
+ const meta = legacyInstanceManager.getInstance(nomadJobId);
3518
+ const instance = { connections: meta?.connections ?? {} };
3519
+ // Validate in runtime mode so missing required / ambiguous still throws
3520
+ // with structured error codes (412 / 409 / 400) before we touch Nomad.
3521
+ const { resolved } = resolveConnections(instanceBackedApp.spec, instance, "runtime");
3522
+ // Render the full RUNTIME_HOOKS env (covers llm/search/browser/mcp)
3523
+ // rather than just the default-category subset, so apply: openai-env
3524
+ // consumers (e.g. OpenWebUI) self-heal across provider port changes.
3525
+ const { renderRuntimeConnectionsEnv } = await import("./connection-apply.js");
3526
+ const runtimeEnv = await renderRuntimeConnectionsEnv(instanceBackedApp.spec, { id: nomadJobId, connections: instance.connections });
3527
+ extraEnv = { ...resolvedToLegacyEnv(resolved), ...runtimeEnv };
3165
3528
  }
3166
3529
  catch (e) {
3167
- return { ok: false, error: e.message };
3530
+ return {
3531
+ ok: false,
3532
+ error: e.message,
3533
+ ...(e.code ? { code: e.code } : {}),
3534
+ ...(typeof e.statusCode === "number" ? { statusCode: e.statusCode } : {}),
3535
+ };
3168
3536
  }
3169
3537
  const depCheck = await checkDependencies(instanceBackedApp.spec);
3170
3538
  if (!depCheck.ok) {
@@ -3189,10 +3557,17 @@ var UnifiedNomadJobs;
3189
3557
  return { ok: false, error: `App '${nomadJobId}' 必须通过 app-manager 启动` };
3190
3558
  }
3191
3559
  if (existsSync(instanceMetaPath(nomadJobId))) {
3192
- return instanceScheduler.startInstance(nomadJobId);
3560
+ const result = await instanceScheduler.startInstance(nomadJobId);
3561
+ if (result.ok)
3562
+ await registerLegacyCapabilities(nomadJobId);
3563
+ return result;
3193
3564
  }
3194
3565
  if (nomadJobId.startsWith(OPENCLAW_PREFIX)) {
3195
- return instanceScheduler.startInstance(nomadJobId.slice(OPENCLAW_PREFIX.length));
3566
+ const inner = nomadJobId.slice(OPENCLAW_PREFIX.length);
3567
+ const result = await instanceScheduler.startInstance(inner);
3568
+ if (result.ok)
3569
+ await registerLegacyCapabilities(inner);
3570
+ return result;
3196
3571
  }
3197
3572
  if (!isAppJob(nomadJobId)) {
3198
3573
  return { ok: false, error: `Cannot start unmanaged job "${nomadJobId}"` };
@@ -3213,10 +3588,19 @@ var UnifiedNomadJobs;
3213
3588
  return { ok: false, error: `App '${nomadJobId}' 必须通过 app-manager 停止` };
3214
3589
  }
3215
3590
  if (existsSync(instanceMetaPath(nomadJobId))) {
3216
- return instanceScheduler.stopInstance(nomadJobId, purge);
3591
+ const result = await instanceScheduler.stopInstance(nomadJobId, purge);
3592
+ if (result.ok || result.error?.includes("not running") || result.error?.includes("not found")) {
3593
+ await unregisterLegacyCapabilities(nomadJobId, purge);
3594
+ }
3595
+ return result;
3217
3596
  }
3218
3597
  if (nomadJobId.startsWith(OPENCLAW_PREFIX)) {
3219
- return instanceScheduler.stopInstance(nomadJobId.slice(OPENCLAW_PREFIX.length), purge);
3598
+ const inner = nomadJobId.slice(OPENCLAW_PREFIX.length);
3599
+ const result = await instanceScheduler.stopInstance(inner, purge);
3600
+ if (result.ok || result.error?.includes("not running") || result.error?.includes("not found")) {
3601
+ await unregisterLegacyCapabilities(inner, purge);
3602
+ }
3603
+ return result;
3220
3604
  }
3221
3605
  try {
3222
3606
  const resp = await nomadDelete(`/v1/job/${nomadJobId}?purge=${purge}`);
@@ -3239,10 +3623,17 @@ var UnifiedNomadJobs;
3239
3623
  return { ok: false, error: `App '${nomadJobId}' 必须通过 app-manager 重启` };
3240
3624
  }
3241
3625
  if (existsSync(instanceMetaPath(nomadJobId))) {
3242
- return instanceScheduler.restartInstance(nomadJobId);
3626
+ const result = await instanceScheduler.restartInstance(nomadJobId);
3627
+ if (result.ok)
3628
+ await registerLegacyCapabilities(nomadJobId);
3629
+ return result;
3243
3630
  }
3244
3631
  if (nomadJobId.startsWith(OPENCLAW_PREFIX)) {
3245
- return instanceScheduler.restartInstance(nomadJobId.slice(OPENCLAW_PREFIX.length));
3632
+ const inner = nomadJobId.slice(OPENCLAW_PREFIX.length);
3633
+ const result = await instanceScheduler.restartInstance(inner);
3634
+ if (result.ok)
3635
+ await registerLegacyCapabilities(inner);
3636
+ return result;
3246
3637
  }
3247
3638
  if (!isAppJob(nomadJobId)) {
3248
3639
  return { ok: false, error: `Cannot restart unmanaged job "${nomadJobId}"` };
@@ -3346,4 +3737,6 @@ export const shouldAutoStartNomadJob = UnifiedNomadJobs.shouldAutoStart;
3346
3737
  export const startNomadJobInstance = UnifiedNomadJobs.startInstance;
3347
3738
  export const stopNomadJobInstance = UnifiedNomadJobs.stopInstance;
3348
3739
  export const restartNomadJobInstance = UnifiedNomadJobs.restartInstance;
3740
+ // @internal — exposed for Phase 10.4 unit testing only.
3741
+ export const __aggregateHealthStatusForTests = UnifiedNomadJobs.aggregateHealthStatus;
3349
3742
  //# sourceMappingURL=nomad-manager.js.map