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.
- package/INSTALL-NOTICE +11 -0
- package/apps/anythingllm-container.yaml +287 -0
- package/apps/browserless-chromium-container.yaml +90 -0
- package/apps/filebrowser-container.yaml +163 -0
- package/apps/hermes-container.yaml +36 -2
- package/apps/ollama-binary.yaml +91 -90
- package/apps/ollama-cpu-container.yaml +8 -1
- package/apps/ollama-with-hollama-binary.yaml +91 -90
- package/apps/openclaw-binary.yaml +38 -1
- package/apps/openclaw-container.yaml +45 -2
- package/apps/openclaw-with-ollama-container.yaml +11 -2
- package/apps/openclaw-with-searxng-container.yaml +26 -2
- package/apps/openwebui-container.yaml +45 -1
- package/apps/playwright-container.yaml +7 -1
- package/apps/searxng-container.yaml +58 -7
- package/apps/weknora-container.yaml +471 -0
- package/dist/cli/app.js +79 -9
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/doctor.d.ts +12 -12
- package/dist/cli/doctor.js +242 -55
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/llm.d.ts +4 -3
- package/dist/cli/llm.js +4 -3
- package/dist/cli/llm.js.map +1 -1
- package/dist/cli/panel.d.ts +6 -5
- package/dist/cli/panel.js +10 -9
- package/dist/cli/panel.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +99 -1
- package/dist/config.js.map +1 -1
- package/dist/control.d.ts +7 -6
- package/dist/control.js +7 -6
- package/dist/control.js.map +1 -1
- package/dist/install.js +3 -3
- package/dist/install.js.map +1 -1
- package/dist/routes/agent-apps.d.ts +1 -1
- package/dist/routes/agent-apps.js +1 -1
- package/dist/routes/apps.js +44 -11
- package/dist/routes/apps.js.map +1 -1
- package/dist/routes/auth.js +5 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/backup.js +64 -11
- package/dist/routes/backup.js.map +1 -1
- package/dist/routes/external-mounts.d.ts +17 -0
- package/dist/routes/external-mounts.js +73 -0
- package/dist/routes/external-mounts.js.map +1 -0
- package/dist/routes/file-mounts.d.ts +13 -0
- package/dist/routes/file-mounts.js +90 -0
- package/dist/routes/file-mounts.js.map +1 -0
- package/dist/routes/files-organize.d.ts +28 -0
- package/dist/routes/files-organize.js +167 -0
- package/dist/routes/files-organize.js.map +1 -0
- package/dist/routes/files.d.ts +31 -0
- package/dist/routes/files.js +321 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/instances.js +826 -17
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/internal.d.ts +2 -0
- package/dist/routes/internal.js +59 -0
- package/dist/routes/internal.js.map +1 -0
- package/dist/routes/llm.js +24 -35
- package/dist/routes/llm.js.map +1 -1
- package/dist/routes/setup.js +10 -10
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +1 -1
- package/dist/routes/system.js.map +1 -1
- package/dist/routes/webdav.d.ts +17 -0
- package/dist/routes/webdav.js +114 -0
- package/dist/routes/webdav.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +751 -20
- package/dist/server.js.map +1 -1
- package/dist/services/agent-apps/catalog.js +4 -3
- package/dist/services/agent-apps/catalog.js.map +1 -1
- package/dist/services/agent-apps/index.d.ts +1 -1
- package/dist/services/agent-apps/index.js +1 -1
- package/dist/services/agent-apps/installers/adapter.d.ts +1 -1
- package/dist/services/agent-apps/installers/adapter.js +1 -1
- package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
- package/dist/services/agent-apps/installers/shell-script.js +3 -3
- package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
- package/dist/services/agent-apps/types.d.ts +2 -2
- package/dist/services/agent-apps/types.js +1 -1
- package/dist/services/app/app-compiler.d.ts +1 -1
- package/dist/services/app/app-compiler.js +5 -5
- package/dist/services/app/app-compiler.js.map +1 -1
- package/dist/services/app/app-manager.d.ts +25 -1
- package/dist/services/app/app-manager.js +829 -150
- package/dist/services/app/app-manager.js.map +1 -1
- package/dist/services/app/custom-manager.js.map +1 -1
- package/dist/services/app/hermes-agent-manager.js +7 -4
- package/dist/services/app/hermes-agent-manager.js.map +1 -1
- package/dist/services/app/ollama-manager.js +1 -1
- package/dist/services/app/ollama-manager.js.map +1 -1
- package/dist/services/app/openclaw-manager.js +20 -3
- package/dist/services/app/openclaw-manager.js.map +1 -1
- package/dist/services/app/platform-transform.d.ts +32 -0
- package/dist/services/app/platform-transform.js +65 -0
- package/dist/services/app/platform-transform.js.map +1 -0
- package/dist/services/app/provide-resolver.d.ts +29 -0
- package/dist/services/app/provide-resolver.js +112 -0
- package/dist/services/app/provide-resolver.js.map +1 -0
- package/dist/services/app-passwords.d.ts +61 -0
- package/dist/services/app-passwords.js +173 -0
- package/dist/services/app-passwords.js.map +1 -0
- package/dist/services/backup-manager.d.ts +11 -0
- package/dist/services/backup-manager.js +177 -4
- package/dist/services/backup-manager.js.map +1 -1
- package/dist/services/capability-endpoint-validator.d.ts +41 -0
- package/dist/services/capability-endpoint-validator.js +104 -0
- package/dist/services/capability-endpoint-validator.js.map +1 -0
- package/dist/services/capability-health.d.ts +16 -0
- package/dist/services/capability-health.js +121 -0
- package/dist/services/capability-health.js.map +1 -0
- package/dist/services/capability-registry.d.ts +106 -0
- package/dist/services/capability-registry.js +313 -0
- package/dist/services/capability-registry.js.map +1 -0
- package/dist/services/connection-apply.d.ts +91 -0
- package/dist/services/connection-apply.js +475 -0
- package/dist/services/connection-apply.js.map +1 -0
- package/dist/services/connection-resolver.d.ts +65 -0
- package/dist/services/connection-resolver.js +281 -0
- package/dist/services/connection-resolver.js.map +1 -0
- package/dist/services/connection-transactor.d.ts +39 -0
- package/dist/services/connection-transactor.js +351 -0
- package/dist/services/connection-transactor.js.map +1 -0
- package/dist/services/external-mounts.d.ts +40 -0
- package/dist/services/external-mounts.js +187 -0
- package/dist/services/external-mounts.js.map +1 -0
- package/dist/services/files-manager.d.ts +252 -0
- package/dist/services/files-manager.js +1075 -0
- package/dist/services/files-manager.js.map +1 -0
- package/dist/services/files-mounts.d.ts +42 -0
- package/dist/services/files-mounts.js +207 -0
- package/dist/services/files-mounts.js.map +1 -0
- package/dist/services/instance-manager.d.ts +13 -0
- package/dist/services/instance-manager.js +138 -46
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +16 -2
- package/dist/services/llm-proxy/index.js +48 -44
- package/dist/services/llm-proxy/index.js.map +1 -1
- package/dist/services/llm-proxy/probe.d.ts +6 -0
- package/dist/services/llm-proxy/probe.js +85 -0
- package/dist/services/llm-proxy/probe.js.map +1 -0
- package/dist/services/llm-proxy/ssrf.d.ts +1 -0
- package/dist/services/llm-proxy/ssrf.js +24 -9
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/nomad-manager.d.ts +4 -0
- package/dist/services/nomad-manager.js +428 -35
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/organize/applier.d.ts +46 -0
- package/dist/services/organize/applier.js +218 -0
- package/dist/services/organize/applier.js.map +1 -0
- package/dist/services/organize/rules.d.ts +57 -0
- package/dist/services/organize/rules.js +286 -0
- package/dist/services/organize/rules.js.map +1 -0
- package/dist/services/organize/scanner.d.ts +50 -0
- package/dist/services/organize/scanner.js +366 -0
- package/dist/services/organize/scanner.js.map +1 -0
- package/dist/services/organize/store.d.ts +14 -0
- package/dist/services/organize/store.js +82 -0
- package/dist/services/organize/store.js.map +1 -0
- package/dist/services/panel-manager.js +20 -1
- package/dist/services/panel-manager.js.map +1 -1
- package/dist/services/process-manager.js +4 -3
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/runtime/adapters/hermes.d.ts +30 -1
- package/dist/services/runtime/adapters/hermes.js +219 -6
- package/dist/services/runtime/adapters/hermes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw-mcporter.d.ts +45 -0
- package/dist/services/runtime/adapters/openclaw-mcporter.js +108 -0
- package/dist/services/runtime/adapters/openclaw-mcporter.js.map +1 -0
- package/dist/services/runtime/adapters/openclaw-routes.d.ts +8 -2
- package/dist/services/runtime/adapters/openclaw-routes.js +68 -0
- package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw.d.ts +177 -0
- package/dist/services/runtime/adapters/openclaw.js +1171 -11
- package/dist/services/runtime/adapters/openclaw.js.map +1 -1
- package/dist/services/runtime/instance.d.ts +1 -1
- package/dist/services/runtime/instance.js +1 -1
- package/dist/services/runtime/instance.js.map +1 -1
- package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +46 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js +281 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/drive-shim.d.ts +54 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js +489 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/firewall.d.ts +26 -0
- package/dist/services/runtime/mcp-shims/firewall.js +129 -0
- package/dist/services/runtime/mcp-shims/firewall.js.map +1 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.d.ts +27 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.js +125 -0
- package/dist/services/runtime/mcp-shims/searxng-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.d.ts +83 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.js +127 -0
- package/dist/services/runtime/mcp-shims/write-mcp-entry.js.map +1 -0
- package/dist/services/runtime/migrations.d.ts +8 -0
- package/dist/services/runtime/migrations.js +100 -0
- package/dist/services/runtime/migrations.js.map +1 -1
- package/dist/services/runtime/types.d.ts +46 -0
- package/dist/services/setup-manager.js +99 -24
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/suggestions.d.ts +27 -0
- package/dist/services/suggestions.js +133 -0
- package/dist/services/suggestions.js.map +1 -0
- package/dist/services/task-registry.js +4 -2
- package/dist/services/task-registry.js.map +1 -1
- package/dist/services/telemetry/device-fingerprint.d.ts +1 -1
- package/dist/services/telemetry/device-fingerprint.js +1 -1
- package/dist/services/types-shim.d.ts +16 -0
- package/dist/services/types-shim.js +2 -0
- package/dist/services/types-shim.js.map +1 -0
- package/dist/services/webdav/server.d.ts +24 -0
- package/dist/services/webdav/server.js +420 -0
- package/dist/services/webdav/server.js.map +1 -0
- package/dist/services/webdav/xml-builder.d.ts +73 -0
- package/dist/services/webdav/xml-builder.js +156 -0
- package/dist/services/webdav/xml-builder.js.map +1 -0
- package/dist/services/workspace-builder.d.ts +29 -0
- package/dist/services/workspace-builder.js +188 -0
- package/dist/services/workspace-builder.js.map +1 -0
- package/dist/types.d.ts +231 -1
- package/dist/utils/instance-lock.d.ts +22 -0
- package/dist/utils/instance-lock.js +48 -0
- package/dist/utils/instance-lock.js.map +1 -0
- package/dist/utils/path-locks.d.ts +30 -0
- package/dist/utils/path-locks.js +63 -0
- package/dist/utils/path-locks.js.map +1 -0
- package/dist/utils/path-safety.d.ts +41 -0
- package/dist/utils/path-safety.js +119 -0
- package/dist/utils/path-safety.js.map +1 -0
- package/dist/utils/safe-json.js +55 -22
- package/dist/utils/safe-json.js.map +1 -1
- package/dist/utils/safe-write.d.ts +24 -0
- package/dist/utils/safe-write.js +82 -0
- package/dist/utils/safe-write.js.map +1 -0
- package/install/jishu-install.sh +323 -27
- package/install/jishu-uninstall.sh +353 -20
- package/package.json +18 -1
- package/public/assets/Dashboard-BdWPtroF.js +1 -0
- package/public/assets/{HermesChatPanel-mFSureyc.js → HermesChatPanel-B_2HlVBQ.js} +1 -1
- package/public/assets/HermesConfigForm-DVlhg3WV.js +4 -0
- package/public/assets/{InitPassword-CVA8wQA6.js → InitPassword-D7glTExX.js} +1 -1
- package/public/assets/InstanceDetail-CxSy2cpe.js +92 -0
- package/public/assets/{Login-BWsZH2mu.js → Login-Cfr5c2sv.js} +1 -1
- package/public/assets/NewInstance-BIYDmJis.js +1 -0
- package/public/assets/ProviderRecommendations-BuRnvRcI.js +1 -0
- package/public/assets/Settings-Cc-tYBil.js +1 -0
- package/public/assets/Setup-lGZEk5jq.js +1 -0
- package/public/assets/{WeixinLoginPanel-CnjR8xMu.js → WeixinLoginPanel-CoGqzxeV.js} +2 -2
- package/public/assets/index-87IJXG-w.css +1 -0
- package/public/assets/index-BZc5zH7u.js +19 -0
- package/public/assets/providers-DtNXh9JD.js +1 -0
- package/public/assets/registry-BWnkJgZ1.js +2 -0
- package/public/assets/{usePolling-Do5Erqm_.js → usePolling-CwwT9KrC.js} +1 -1
- package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-y9V7Sfuu.js} +1 -1
- package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-BWrEVJVb.js} +6 -6
- package/public/index.html +4 -4
- package/scripts/check-app-spec.mjs +457 -0
- package/scripts/check-i18n.mjs +154 -0
- package/scripts/check-new-file-tests.mjs +230 -0
- package/scripts/check-quarantine-expiry.mjs +105 -0
- package/scripts/perf/README.md +49 -0
- package/scripts/perf/auth.js +99 -0
- package/scripts/perf/config.js +63 -0
- package/scripts/perf/instances.js +143 -0
- package/scripts/perf/proxy.js +96 -0
- package/scripts/run.sh +4 -4
- package/scripts/smoke/files-w1.sh +142 -0
- package/scripts/smoke-backend.mjs +122 -0
- package/scripts/smoke-post-publish.mjs +346 -0
- package/public/assets/Dashboard-B-JoOjBQ.js +0 -1
- package/public/assets/HermesConfigForm-DvR05LK1.js +0 -4
- package/public/assets/InstanceDetail-DcZW2QGO.js +0 -91
- package/public/assets/NewInstance-BCIrAd86.js +0 -1
- package/public/assets/Settings-xkDcduFz.js +0 -1
- package/public/assets/Setup-Cfuwj4gV.js +0 -1
- package/public/assets/index-CPhVFEsx.css +0 -1
- package/public/assets/index-DQsM6Joa.js +0 -19
- package/public/assets/providers-V-vwrExZ.js +0 -1
- 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
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
2097
|
-
|
|
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:
|
|
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 {
|
|
3164
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|