jishushell 0.4.24-beta.2 → 0.4.30
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/browserless-chromium-container.yaml +78 -0
- package/apps/hermes-container.yaml +36 -2
- package/apps/ollama-binary.yaml +45 -8
- package/apps/ollama-cpu-container.yaml +8 -1
- package/apps/ollama-with-hollama-binary.yaml +45 -8
- package/apps/openclaw-binary.yaml +30 -1
- package/apps/openclaw-container.yaml +37 -2
- package/apps/openclaw-with-ollama-container.yaml +11 -2
- package/apps/openclaw-with-searxng-container.yaml +22 -2
- package/apps/openwebui-container.yaml +45 -1
- package/apps/playwright-container.yaml +7 -1
- package/apps/searxng-container.yaml +54 -4
- package/dist/cli/app.js +12 -2
- 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/control.d.ts +7 -6
- package/dist/control.js +7 -6
- package/dist/control.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 +3 -0
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/instances.js +787 -16
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/llm.js +24 -35
- package/dist/routes/llm.js.map +1 -1
- package/dist/routes/setup.js +1 -1
- package/dist/routes/setup.js.map +1 -1
- package/dist/server.d.ts +9 -0
- package/dist/server.js +410 -17
- 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-manager.d.ts +24 -1
- package/dist/services/app/app-manager.js +490 -102
- package/dist/services/app/app-manager.js.map +1 -1
- package/dist/services/app/hermes-agent-manager.js +6 -4
- package/dist/services/app/hermes-agent-manager.js.map +1 -1
- 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/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 +89 -0
- package/dist/services/connection-apply.js +421 -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 +37 -0
- package/dist/services/connection-transactor.js +341 -0
- package/dist/services/connection-transactor.js.map +1 -0
- package/dist/services/instance-manager.d.ts +13 -0
- package/dist/services/instance-manager.js +137 -23
- 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 +18 -7
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/nomad-manager.js +375 -16
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/process-manager.js +1 -1
- 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 +218 -5
- 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.d.ts +87 -0
- package/dist/services/runtime/adapters/openclaw.js +250 -2
- package/dist/services/runtime/adapters/openclaw.js.map +1 -1
- 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 +15 -0
- package/dist/services/setup-manager.js +6 -6
- 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/types.d.ts +169 -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/safe-json.js +55 -22
- package/dist/utils/safe-json.js.map +1 -1
- package/install/jishu-install.sh +323 -26
- package/install/jishu-uninstall.sh +353 -20
- package/package.json +3 -1
- package/public/assets/Dashboard-rkWp-CXd.js +1 -0
- package/public/assets/{HermesChatPanel-D6JI6lLY.js → HermesChatPanel-_GHoklgo.js} +1 -1
- package/public/assets/HermesConfigForm-anDnwUp_.js +4 -0
- package/public/assets/{InitPassword-CFTKsED4.js → InitPassword-ZU9_-hDr.js} +1 -1
- package/public/assets/InstanceDetail-CN0FH1aw.js +92 -0
- package/public/assets/{Login-KB9qrtM0.js → Login-BItXqYAJ.js} +1 -1
- package/public/assets/NewInstance-BousE6kY.js +1 -0
- package/public/assets/ProviderRecommendations-DFYj7Fb6.js +1 -0
- package/public/assets/Settings-Bttc6QmM.js +1 -0
- package/public/assets/Setup-Bsxx1zgj.js +1 -0
- package/public/assets/{WeixinLoginPanel-gca0QTic.js → WeixinLoginPanel-DPZpAKgO.js} +2 -2
- package/public/assets/index-8xZy1z5k.css +1 -0
- package/public/assets/index-Dw3HhUYE.js +19 -0
- package/public/assets/providers-DtNXh9JD.js +1 -0
- package/public/assets/registry-5s2UB6is.js +2 -0
- package/public/index.html +2 -2
- package/scripts/check-app-spec.mjs +443 -0
- package/scripts/check-i18n.mjs +154 -0
- package/scripts/run.sh +4 -4
- package/public/assets/Dashboard-rh9qpYRR.js +0 -1
- package/public/assets/HermesConfigForm-DcbSemaj.js +0 -4
- package/public/assets/InstanceDetail-BhNIKA6Z.js +0 -91
- package/public/assets/NewInstance-CxkO8Hlq.js +0 -1
- package/public/assets/Settings-BVWJvOkU.js +0 -1
- package/public/assets/Setup-X-lzuaUT.js +0 -1
- package/public/assets/index-C8B0cFJM.js +0 -19
- package/public/assets/index-CPhVFEsx.css +0 -1
- package/public/assets/providers-V-vwrExZ.js +0 -1
- package/public/assets/registry-fVUSujib.js +0 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { request as httpRequest } from "http";
|
|
2
2
|
import { request as httpsRequest } from "https";
|
|
3
3
|
import { lookup } from "dns/promises";
|
|
4
|
+
import { isIP } from "net";
|
|
4
5
|
import { Readable } from "stream";
|
|
5
6
|
// SSRF protection — private IP ranges
|
|
6
7
|
const PRIVATE_HOST_PATTERNS = [
|
|
@@ -18,8 +19,14 @@ const PRIVATE_HOST_PATTERNS = [
|
|
|
18
19
|
];
|
|
19
20
|
export const LOCAL_PROVIDER_IDS = new Set(["ollama", "vllm", "sglang", "custom"]);
|
|
20
21
|
const LOCALHOST_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
21
|
-
function
|
|
22
|
+
function isLinkLocalHost(hostname) {
|
|
23
|
+
const cleaned = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
24
|
+
return cleaned.startsWith("169.254.") || cleaned.startsWith("fe80:");
|
|
25
|
+
}
|
|
26
|
+
export function isPrivateHost(hostname) {
|
|
22
27
|
const cleaned = hostname.replace(/^\[|\]$/g, "");
|
|
28
|
+
if (isIP(cleaned) && isPrivateIP(cleaned))
|
|
29
|
+
return true;
|
|
23
30
|
return PRIVATE_HOST_PATTERNS.some((p) => p.test(cleaned));
|
|
24
31
|
}
|
|
25
32
|
function isPrivateIP(ip) {
|
|
@@ -85,21 +92,22 @@ function isPrivateIP(ip) {
|
|
|
85
92
|
export async function validateUpstreamUrl(urlStr, providerId) {
|
|
86
93
|
try {
|
|
87
94
|
const url = new URL(urlStr);
|
|
95
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
96
|
+
throw new Error("upstream URL must use http:// or https://");
|
|
97
|
+
}
|
|
88
98
|
const isLocalHost = LOCALHOST_HOSTS.has(url.hostname);
|
|
89
99
|
if (LOCAL_PROVIDER_IDS.has(providerId)) {
|
|
90
100
|
// Local AI providers (ollama/vllm/sglang): allow localhost and private
|
|
91
101
|
// networks, but block cloud metadata endpoints (169.254.x.x link-local).
|
|
92
102
|
if (isLocalHost)
|
|
93
103
|
return null;
|
|
94
|
-
if (url.hostname
|
|
104
|
+
if (isLinkLocalHost(url.hostname)) {
|
|
95
105
|
throw new Error(`Local provider URL must not target link-local/metadata address ${url.hostname}`);
|
|
96
106
|
}
|
|
97
107
|
// Allow private LAN addresses (192.168.x, 10.x, 172.16-31.x) — admin explicitly configured
|
|
98
108
|
return null;
|
|
99
109
|
}
|
|
100
|
-
//
|
|
101
|
-
// but still allow it (reverse-proxy setups). Do NOT skip SSRF for
|
|
102
|
-
// non-localhost destinations just because providerId looks local.
|
|
110
|
+
// Preserve explicit localhost reverse-proxy setups for remote providers.
|
|
103
111
|
if (isLocalHost)
|
|
104
112
|
return null;
|
|
105
113
|
if (isPrivateHost(url.hostname)) {
|
|
@@ -119,9 +127,12 @@ export async function validateUpstreamUrl(urlStr, providerId) {
|
|
|
119
127
|
}
|
|
120
128
|
}
|
|
121
129
|
catch (err) {
|
|
122
|
-
if (err.message.includes("private address") ||
|
|
130
|
+
if (err.message.includes("private address") ||
|
|
131
|
+
err.message.includes("private IP") ||
|
|
132
|
+
err.message.includes("must use http") ||
|
|
133
|
+
err.message.includes("link-local"))
|
|
123
134
|
throw err;
|
|
124
|
-
throw new Error(
|
|
135
|
+
throw new Error("invalid upstream URL");
|
|
125
136
|
}
|
|
126
137
|
}
|
|
127
138
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssrf.js","sourceRoot":"","sources":["../../../src/services/llm-proxy/ssrf.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,OAAO,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAGlC,sCAAsC;AACtC,MAAM,qBAAqB,GAAG;IAC5B,QAAQ;IACR,OAAO;IACP,4BAA4B;IAC5B,aAAa;IACb,aAAa;IACb,MAAM;IACN,cAAc;IACd,aAAa;IACb,SAAS;IACT,SAAS;IACT,YAAY;CACb,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;AAClF,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;AAE5E,SAAS,aAAa,CAAC,QAAgB;
|
|
1
|
+
{"version":3,"file":"ssrf.js","sourceRoot":"","sources":["../../../src/services/llm-proxy/ssrf.ts"],"names":[],"mappings":"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"}
|
|
@@ -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,6 +439,7 @@ function wrapNomadJob(jid, groupName, task) {
|
|
|
425
439
|
TaskGroups: [{
|
|
426
440
|
Name: groupName,
|
|
427
441
|
Count: 1,
|
|
442
|
+
...(groupNetworks ? { Networks: groupNetworks } : {}),
|
|
428
443
|
RestartPolicy: {
|
|
429
444
|
Attempts: 3,
|
|
430
445
|
Interval: 300000000000,
|
|
@@ -457,6 +472,7 @@ async function buildJob(instanceId) {
|
|
|
457
472
|
if (legacyManager) {
|
|
458
473
|
const runtime = legacyManager.buildRuntime(instanceId);
|
|
459
474
|
const task = legacyManager.buildNomadTask(instanceId, runtime, jid);
|
|
475
|
+
await injectConnectionsRuntimeEnv(instanceId, task);
|
|
460
476
|
return wrapNomadJob(jid, legacyManager.nomadTaskGroupName(), task);
|
|
461
477
|
}
|
|
462
478
|
// Pure adapter dispatch — no more `isHermesInstance()` / kind literals.
|
|
@@ -466,11 +482,54 @@ async function buildJob(instanceId) {
|
|
|
466
482
|
throw new Error(`Runtime adapter "${agentType}" does not implement buildNomadTask(); cannot schedule Nomad job`);
|
|
467
483
|
}
|
|
468
484
|
const task = await adapter.buildNomadTask(instanceId);
|
|
485
|
+
await injectConnectionsRuntimeEnv(instanceId, task);
|
|
469
486
|
// Task group name mirrors the agentType. Log/status helpers resolve the
|
|
470
487
|
// Nomad task name via resolveTaskName(instanceId) → adapter.nomadTaskName.
|
|
471
488
|
const groupName = agentType;
|
|
472
489
|
return wrapNomadJob(jid, groupName, task);
|
|
473
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* Re-resolve `instance.connections` against the live capability registry
|
|
493
|
+
* and merge the resulting env into the freshly-built Nomad task. Idempotent
|
|
494
|
+
* — empty meta.connections short-circuits to a no-op.
|
|
495
|
+
*
|
|
496
|
+
* Resolving at start time (rather than reading the frozen
|
|
497
|
+
* `instance.json["connections-env"]` written by PUT /connections) means
|
|
498
|
+
* provider port / address changes in the registry propagate on next
|
|
499
|
+
* restart without requiring the user to re-bind. Failures here are
|
|
500
|
+
* logged but never block start: a missing required binding still surfaces
|
|
501
|
+
* via the Connections UI status badge.
|
|
502
|
+
*/
|
|
503
|
+
async function injectConnectionsRuntimeEnv(instanceId, task) {
|
|
504
|
+
try {
|
|
505
|
+
const meta = getInstance(instanceId);
|
|
506
|
+
const connections = meta?.connections;
|
|
507
|
+
if (!meta || !connections || Object.keys(connections).length === 0)
|
|
508
|
+
return;
|
|
509
|
+
const { loadCapabilitySpecForLegacyInstance } = await import("./runtime/migrations.js");
|
|
510
|
+
const spec = loadCapabilitySpecForLegacyInstance(meta);
|
|
511
|
+
if (!spec)
|
|
512
|
+
return;
|
|
513
|
+
const { resolveConnections } = await import("./connection-resolver.js");
|
|
514
|
+
const { resolved } = resolveConnections(spec, { connections }, "preCreate");
|
|
515
|
+
if (resolved.length === 0)
|
|
516
|
+
return;
|
|
517
|
+
const { RUNTIME_HOOKS } = await import("./connection-apply.js");
|
|
518
|
+
const merged = {};
|
|
519
|
+
for (const binding of resolved) {
|
|
520
|
+
const hook = RUNTIME_HOOKS[binding.category];
|
|
521
|
+
if (!hook)
|
|
522
|
+
continue;
|
|
523
|
+
Object.assign(merged, await hook(meta, binding));
|
|
524
|
+
}
|
|
525
|
+
if (Object.keys(merged).length === 0)
|
|
526
|
+
return;
|
|
527
|
+
task.Env = { ...(task.Env ?? {}), ...merged };
|
|
528
|
+
}
|
|
529
|
+
catch (e) {
|
|
530
|
+
console.warn(`[nomad] connections runtime env merge failed for ${instanceId}: ${e?.message ?? e}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
474
533
|
async function getRunningAlloc(instanceId) {
|
|
475
534
|
const jid = jobId(instanceId);
|
|
476
535
|
try {
|
|
@@ -607,7 +666,7 @@ export async function getStatus(instanceId) {
|
|
|
607
666
|
async function phaseRunningCheck(instanceId) {
|
|
608
667
|
const status = await getStatus(instanceId);
|
|
609
668
|
if (status.status === "running") {
|
|
610
|
-
return { ok: false, error: "Instance is already running" };
|
|
669
|
+
return { ok: false, error: "Instance is already running", code: "INSTANCE_ALREADY_RUNNING" };
|
|
611
670
|
}
|
|
612
671
|
return { ok: true };
|
|
613
672
|
}
|
|
@@ -674,6 +733,81 @@ async function phasePreStartHook(adapter, instanceId) {
|
|
|
674
733
|
return { ok: false, error: e?.message || String(e) };
|
|
675
734
|
}
|
|
676
735
|
}
|
|
736
|
+
/**
|
|
737
|
+
* §17 / PR 9 — re-render adapter-managed connection config from the
|
|
738
|
+
* current capability registry before each instance start.
|
|
739
|
+
*
|
|
740
|
+
* Without this hook, env values (like `SEARCH_API_BASE_URL` =
|
|
741
|
+
* `http://<host>:<port>/search`) are frozen into the adapter's config
|
|
742
|
+
* files at PUT /connections time. When the host IP changes (DHCP
|
|
743
|
+
* renewal, pi reboot picking up a new lease, network move) or a
|
|
744
|
+
* provider gets re-deployed at a different host:port, the consumer
|
|
745
|
+
* keeps trying the stale address and search/llm/etc. silently fail.
|
|
746
|
+
*
|
|
747
|
+
* What this does on every start:
|
|
748
|
+
* 1. Read connections from instance.json
|
|
749
|
+
* 2. Re-resolve them in `runtime` mode (tolerant: ambiguous/missing
|
|
750
|
+
* becomes empty resolved instead of throwing — start should still
|
|
751
|
+
* proceed even if one binding can't be re-rendered)
|
|
752
|
+
* 3. Collect env via the same persist hooks PUT /connections uses
|
|
753
|
+
* 4. Call adapter.applyConnectionEnv with the fresh env so the
|
|
754
|
+
* adapter rewrites its config files (mcp_servers / openclaw.json /
|
|
755
|
+
* etc.) with the current address
|
|
756
|
+
*
|
|
757
|
+
* Failures here are logged but never block start: a stale config is
|
|
758
|
+
* better than no start. If something is genuinely wrong with the
|
|
759
|
+
* registry, the user will see a connection error in the UI on next
|
|
760
|
+
* use — at which point they can re-bind manually.
|
|
761
|
+
*/
|
|
762
|
+
async function phaseRefreshConnections(adapter, instanceId) {
|
|
763
|
+
if (!adapter.applyConnectionEnv)
|
|
764
|
+
return;
|
|
765
|
+
try {
|
|
766
|
+
const meta = getInstance(instanceId);
|
|
767
|
+
const connections = meta?.connections;
|
|
768
|
+
if (!meta || !connections || Object.keys(connections).length === 0)
|
|
769
|
+
return;
|
|
770
|
+
const { loadCapabilitySpecForLegacyInstance } = await import("./runtime/migrations.js");
|
|
771
|
+
const spec = loadCapabilitySpecForLegacyInstance(meta);
|
|
772
|
+
if (!spec)
|
|
773
|
+
return;
|
|
774
|
+
const { resolveConnections } = await import("./connection-resolver.js");
|
|
775
|
+
const { resolved } = resolveConnections(spec, { connections }, "preCreate");
|
|
776
|
+
if (resolved.length === 0)
|
|
777
|
+
return;
|
|
778
|
+
const { PERSIST_HOOKS } = await import("./connection-apply.js");
|
|
779
|
+
const merged = {};
|
|
780
|
+
const seenEnvKeys = new Set();
|
|
781
|
+
// Accumulate env across all resolved bindings via a stub
|
|
782
|
+
// writeConnectionEnv that just collects into `merged`. We don't
|
|
783
|
+
// run the real persist hooks here because those would re-write
|
|
784
|
+
// generic-app `connections-env` (already handled by
|
|
785
|
+
// injectConnectionsRuntimeEnv); we only want the env so we can
|
|
786
|
+
// pass it to adapter.applyConnectionEnv below.
|
|
787
|
+
const stubCtx = {
|
|
788
|
+
registry: await import("./capability-registry.js"),
|
|
789
|
+
adapter: { applyConnectionEnv: undefined },
|
|
790
|
+
async writeConnectionEnv(_inst, env) {
|
|
791
|
+
for (const [k, v] of Object.entries(env)) {
|
|
792
|
+
merged[k] = v;
|
|
793
|
+
seenEnvKeys.add(k);
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
};
|
|
797
|
+
for (const binding of resolved) {
|
|
798
|
+
const hook = PERSIST_HOOKS[binding.category];
|
|
799
|
+
if (!hook)
|
|
800
|
+
continue;
|
|
801
|
+
await hook(meta, binding, stubCtx);
|
|
802
|
+
}
|
|
803
|
+
if (seenEnvKeys.size === 0)
|
|
804
|
+
return;
|
|
805
|
+
await adapter.applyConnectionEnv(instanceId, merged);
|
|
806
|
+
}
|
|
807
|
+
catch (e) {
|
|
808
|
+
console.warn(`[nomad] connections refresh failed for ${instanceId}: ${e?.message ?? e}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
677
811
|
/**
|
|
678
812
|
* Phase 5: submit to Nomad with a single retry on port race. Between our
|
|
679
813
|
* earlier host probe and Docker's actual bind another process could have
|
|
@@ -741,8 +875,12 @@ export async function startInstance(instanceId) {
|
|
|
741
875
|
return { ok: false, phase, ...rest };
|
|
742
876
|
};
|
|
743
877
|
const running = await phaseRunningCheck(instanceId);
|
|
744
|
-
if (!running.ok)
|
|
745
|
-
|
|
878
|
+
if (!running.ok) {
|
|
879
|
+
const extra = { error: running.error };
|
|
880
|
+
if (running.code)
|
|
881
|
+
extra.code = running.code;
|
|
882
|
+
return failed("running_check", extra);
|
|
883
|
+
}
|
|
746
884
|
const legacyManager = await getLegacyAppManager(instanceId);
|
|
747
885
|
if (legacyManager) {
|
|
748
886
|
const prep = await legacyManager.prepareStart(instanceId);
|
|
@@ -761,6 +899,11 @@ export async function startInstance(instanceId) {
|
|
|
761
899
|
const home = await phaseHomeConflict(instanceId, adapter.findInstancesSharingHome?.(instanceId) ?? []);
|
|
762
900
|
if (!home.ok)
|
|
763
901
|
return failed("home_conflict", { error: home.error });
|
|
902
|
+
// PR 9 — refresh adapter-managed connection config from current
|
|
903
|
+
// capability registry before adapter pre-start. Best-effort: never
|
|
904
|
+
// blocks start (any failure is logged and we proceed with the
|
|
905
|
+
// existing on-disk config). See phaseRefreshConnections doc.
|
|
906
|
+
await phaseRefreshConnections(adapter, instanceId);
|
|
764
907
|
const hook = await phasePreStartHook(adapter, instanceId);
|
|
765
908
|
if (!hook.ok) {
|
|
766
909
|
const extra = { error: hook.error };
|
|
@@ -777,12 +920,65 @@ export async function startInstance(instanceId) {
|
|
|
777
920
|
const submit = await phaseSubmit(instanceId, port.portAllocation);
|
|
778
921
|
if (!submit.ok)
|
|
779
922
|
return failed("submit", { error: submit.error });
|
|
923
|
+
// Auto-register capability providers for legacy instances so they appear
|
|
924
|
+
// in the Connections UI alongside app-installed apps. App-dir installed
|
|
925
|
+
// apps short-circuit at the top of this function and don't reach here.
|
|
926
|
+
await registerLegacyCapabilitiesTopLevel(instanceId);
|
|
780
927
|
return {
|
|
781
928
|
ok: true,
|
|
782
929
|
eval_id: submit.evalId,
|
|
783
930
|
...(submit.portAllocation ? { port_allocation: submit.portAllocation } : {}),
|
|
784
931
|
};
|
|
785
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* Best-effort capability registration for legacy (non-app-installed)
|
|
935
|
+
* hermes/openclaw instances. Loaded synthetic spec via the migrations
|
|
936
|
+
* helper; failures are logged but never block start/stop.
|
|
937
|
+
*/
|
|
938
|
+
async function registerLegacyCapabilitiesTopLevel(instanceId) {
|
|
939
|
+
try {
|
|
940
|
+
const meta = getInstance(instanceId);
|
|
941
|
+
if (!meta)
|
|
942
|
+
return;
|
|
943
|
+
const { loadCapabilitySpecForLegacyInstance } = await import("./runtime/migrations.js");
|
|
944
|
+
const synthSpec = loadCapabilitySpecForLegacyInstance(meta);
|
|
945
|
+
if (!synthSpec?.provides?.length)
|
|
946
|
+
return;
|
|
947
|
+
// The synthetic spec's `name` is the yaml template's display name
|
|
948
|
+
// ("Hermes Agent" / "OpenClaw Container"). For Connections-tab UX we
|
|
949
|
+
// want the candidate to surface the user's instance name (e.g. "h",
|
|
950
|
+
// "claw11"), so override before handing it to registerCapabilities.
|
|
951
|
+
const instanceName = typeof meta.name === "string" && meta.name
|
|
952
|
+
? meta.name
|
|
953
|
+
: instanceId;
|
|
954
|
+
const namedSpec = { ...synthSpec, name: instanceName };
|
|
955
|
+
// The synthetic spec has `tasks: []`, so `resolveProvideEndpoint`
|
|
956
|
+
// can't compute the actual gateway port and falls back to the yaml's
|
|
957
|
+
// declared port (e.g. openclaw default 18789). Legacy instances may
|
|
958
|
+
// have been allocated a different port at creation time (port
|
|
959
|
+
// collision avoidance). Pass the live `getGatewayPort` so the
|
|
960
|
+
// capability registry advertises the port consumers can actually
|
|
961
|
+
// reach.
|
|
962
|
+
const portOverride = getGatewayPort(instanceId);
|
|
963
|
+
const { registerCapabilities } = await import("./app/app-manager.js");
|
|
964
|
+
registerCapabilities(instanceId, namedSpec, portOverride > 0 ? portOverride : undefined);
|
|
965
|
+
}
|
|
966
|
+
catch (e) {
|
|
967
|
+
console.warn(`[legacy-capabilities] register failed for ${instanceId}: ${e?.message ?? e}`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
async function unregisterLegacyCapabilitiesTopLevel(instanceId, purge) {
|
|
971
|
+
try {
|
|
972
|
+
const { markCapabilitiesStopped, unregisterCapabilities } = await import("./app/app-manager.js");
|
|
973
|
+
if (purge)
|
|
974
|
+
unregisterCapabilities(instanceId);
|
|
975
|
+
else
|
|
976
|
+
markCapabilitiesStopped(instanceId);
|
|
977
|
+
}
|
|
978
|
+
catch (e) {
|
|
979
|
+
console.warn(`[legacy-capabilities] unregister failed for ${instanceId}: ${e?.message ?? e}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
786
982
|
export async function stopInstance(instanceId, purge = false) {
|
|
787
983
|
const jid = jobId(instanceId);
|
|
788
984
|
try {
|
|
@@ -794,10 +990,14 @@ export async function stopInstance(instanceId, purge = false) {
|
|
|
794
990
|
}
|
|
795
991
|
catch { /* ignore */ }
|
|
796
992
|
}
|
|
993
|
+
await unregisterLegacyCapabilitiesTopLevel(instanceId, purge);
|
|
797
994
|
return { ok: true };
|
|
798
995
|
}
|
|
799
|
-
if (resp.status === 404)
|
|
996
|
+
if (resp.status === 404) {
|
|
997
|
+
// Already stopped — still mark capabilities stopped so the UI reflects state.
|
|
998
|
+
await unregisterLegacyCapabilitiesTopLevel(instanceId, purge);
|
|
800
999
|
return { ok: false, error: "Instance is not running" };
|
|
1000
|
+
}
|
|
801
1001
|
return { ok: false, error: await resp.text() };
|
|
802
1002
|
}
|
|
803
1003
|
catch (e) {
|
|
@@ -828,6 +1028,11 @@ export async function restartInstance(instanceId) {
|
|
|
828
1028
|
const meta = getInstance(instanceId);
|
|
829
1029
|
const agentType = resolveAgentType(meta);
|
|
830
1030
|
const adapter = getAdapter(agentType);
|
|
1031
|
+
// PR 9 — refresh connection-derived config (mcp_servers /
|
|
1032
|
+
// openclaw.json plugins) from current capability registry so
|
|
1033
|
+
// host IP changes propagate on restart without manual re-bind.
|
|
1034
|
+
// Symmetric with the same call in startInstance.
|
|
1035
|
+
await phaseRefreshConnections(adapter, instanceId);
|
|
831
1036
|
if (adapter.hooks?.onBeforeStart) {
|
|
832
1037
|
await adapter.hooks.onBeforeStart({ instanceId });
|
|
833
1038
|
}
|
|
@@ -841,8 +1046,12 @@ export async function restartInstance(instanceId) {
|
|
|
841
1046
|
TaskName: resolveTaskName(instanceId),
|
|
842
1047
|
AllTasks: false,
|
|
843
1048
|
});
|
|
844
|
-
if (resp.ok)
|
|
1049
|
+
if (resp.ok) {
|
|
1050
|
+
// Re-register capabilities — yaml provides may have changed since
|
|
1051
|
+
// the last start (e.g. a panel upgrade adding `llm-agent`).
|
|
1052
|
+
await registerLegacyCapabilitiesTopLevel(instanceId);
|
|
845
1053
|
return { ok: true, alloc_id: alloc.ID };
|
|
1054
|
+
}
|
|
846
1055
|
// Non-2xx from the restart endpoint falls through to stop+start
|
|
847
1056
|
const errText = await resp.text();
|
|
848
1057
|
console.warn(`[nomad] Native restart failed for ${instanceId} (HTTP ${resp.status}): ${errText} — falling back to stop+start`);
|
|
@@ -1228,7 +1437,8 @@ var UnifiedNomadJobs;
|
|
|
1228
1437
|
return nomadConfigDeclaresHostNetwork("external") ? "external" : undefined;
|
|
1229
1438
|
}
|
|
1230
1439
|
function specRequiresExternalHostNetwork(spec) {
|
|
1231
|
-
return spec.tasks.some((task) =>
|
|
1440
|
+
return spec.tasks.some((task) => task.runtime !== "container"
|
|
1441
|
+
&& (task.ports ?? []).some((port) => (port.visibility ?? "external") !== "internal"));
|
|
1232
1442
|
}
|
|
1233
1443
|
async function validateRequiredHostNetworks(spec) {
|
|
1234
1444
|
if (!specRequiresExternalHostNetwork(spec))
|
|
@@ -1268,7 +1478,18 @@ var UnifiedNomadJobs;
|
|
|
1268
1478
|
Label: portLabel(task.name, port.name),
|
|
1269
1479
|
Value: port.host_port ?? port.port,
|
|
1270
1480
|
...(task.runtime === "container" ? { To: port.container_port ?? port.port } : {}),
|
|
1271
|
-
|
|
1481
|
+
// Attach the named host_network for any externally-visible port —
|
|
1482
|
+
// including container tasks. Without it, Nomad falls back to
|
|
1483
|
+
// HostNetwork="default" (loopback) and the docker driver publishes
|
|
1484
|
+
// to 127.0.0.1, breaking cross-container consumers (e.g. OpenWebUI
|
|
1485
|
+
// calling hermes / openclaw via the connections page). The earlier
|
|
1486
|
+
// restriction to non-container tasks was overcautious — our task
|
|
1487
|
+
// groups use host networking (Mode: "") rather than Nomad bridge
|
|
1488
|
+
// mode, so attaching host_network is safe and is exactly what
|
|
1489
|
+
// searxng-container has been doing in practice all along.
|
|
1490
|
+
...(hostNetworkForPort(port)
|
|
1491
|
+
? { HostNetwork: hostNetworkForPort(port) }
|
|
1492
|
+
: {}),
|
|
1272
1493
|
}));
|
|
1273
1494
|
}
|
|
1274
1495
|
// ── Health check → Nomad service check builder ────────────────────────────
|
|
@@ -2013,12 +2234,52 @@ var UnifiedNomadJobs;
|
|
|
2013
2234
|
const src = v.source.replace(/^~(?=\/|$)/, homedir());
|
|
2014
2235
|
return `${src}:${v.target}${v.readonly ? ":ro" : ":rw"}`;
|
|
2015
2236
|
});
|
|
2237
|
+
// Resolve container task user. On Linux we default to the panel
|
|
2238
|
+
// process's host uid:gid so bind-mounted data dirs (owned by the panel
|
|
2239
|
+
// user, typically `pi`) are writable without forcing the container to
|
|
2240
|
+
// run as root and without needing chown / DAC_OVERRIDE gymnastics.
|
|
2241
|
+
// yaml override `user: "<uid>:<gid>"` wins; explicit `user: "root"` or
|
|
2242
|
+
// `user: "0:0"` keeps the image's root default.
|
|
2243
|
+
//
|
|
2244
|
+
// On macOS we skip the host-uid default. Docker on Mac runs inside a
|
|
2245
|
+
// Linux VM (Colima/Docker Desktop) with its own uid namespace — host
|
|
2246
|
+
// uids like 501 (the standard macOS first-user) are virtualised away
|
|
2247
|
+
// by virtiofs and almost never exist in the image's /etc/passwd. Some
|
|
2248
|
+
// images crash hard when started as an unknown uid: e.g. browserless
|
|
2249
|
+
// calls Node.js `os.userInfo()` very early, which throws
|
|
2250
|
+
// `uv_os_get_passwd returned ENOENT` and the container exits before
|
|
2251
|
+
// the port ever binds. Letting the image's default USER directive
|
|
2252
|
+
// take effect is correct on Mac; users who do need bind-mount
|
|
2253
|
+
// ownership control can still set yaml `user:` explicitly.
|
|
2254
|
+
const containerUser = (() => {
|
|
2255
|
+
if (task.runtime !== "container")
|
|
2256
|
+
return undefined;
|
|
2257
|
+
const declared = typeof task.user === "string" ? task.user.trim() : "";
|
|
2258
|
+
if (declared === "host" || declared === "") {
|
|
2259
|
+
if (process.platform === "darwin")
|
|
2260
|
+
return undefined;
|
|
2261
|
+
const uid = process.getuid?.() ?? 1000;
|
|
2262
|
+
const gid = process.getgid?.() ?? 1000;
|
|
2263
|
+
return `${uid}:${gid}`;
|
|
2264
|
+
}
|
|
2265
|
+
return declared;
|
|
2266
|
+
})();
|
|
2016
2267
|
const taskDef = {
|
|
2017
2268
|
Name: task.name,
|
|
2018
2269
|
Driver: "docker",
|
|
2270
|
+
...(containerUser ? { User: containerUser } : {}),
|
|
2019
2271
|
Config: {
|
|
2020
2272
|
image,
|
|
2021
2273
|
force_pull: false,
|
|
2274
|
+
// Nomad's docker driver default `image_pull_timeout` is 5 minutes;
|
|
2275
|
+
// on Raspberry Pi or other constrained networks a 1+ GiB image
|
|
2276
|
+
// (Open WebUI, OpenClaw, Hermes) can exceed that and the alloc
|
|
2277
|
+
// restart-loops with "Failed to pull: context deadline exceeded"
|
|
2278
|
+
// before it ever starts. Raise to 15 minutes — long enough for
|
|
2279
|
+
// realistic Pi-class pulls, short enough that a genuinely
|
|
2280
|
+
// unreachable registry still surfaces as a failure within a
|
|
2281
|
+
// bounded window.
|
|
2282
|
+
image_pull_timeout: "15m",
|
|
2022
2283
|
...(task.command ? { command: String(task.command) } : {}),
|
|
2023
2284
|
args,
|
|
2024
2285
|
...(publishedPorts.length > 0 ? { ports: publishedPorts } : {}),
|
|
@@ -2602,6 +2863,10 @@ var UnifiedNomadJobs;
|
|
|
2602
2863
|
if (nomadStopped) {
|
|
2603
2864
|
const allocsStopped = await waitForAllocationsToStop(liveAllocIds);
|
|
2604
2865
|
if (!allocsStopped) {
|
|
2866
|
+
const lingeringAlloc = await getRunningAlloc(appId);
|
|
2867
|
+
if (!lingeringAlloc) {
|
|
2868
|
+
return { ok: true };
|
|
2869
|
+
}
|
|
2605
2870
|
return { ok: false, error: `App '${appId}' allocations did not stop in time` };
|
|
2606
2871
|
}
|
|
2607
2872
|
return { ok: true };
|
|
@@ -3155,16 +3420,87 @@ var UnifiedNomadJobs;
|
|
|
3155
3420
|
return getGenericJobStatus(nomadJobId);
|
|
3156
3421
|
}
|
|
3157
3422
|
UnifiedNomadJobs.getInstanceStatus = getInstanceStatus;
|
|
3423
|
+
/**
|
|
3424
|
+
* Capability registration shim for **legacy** (non-app-installed)
|
|
3425
|
+
* hermes/openclaw instances. Loads the synthetic spec via
|
|
3426
|
+
* loadCapabilitySpecForLegacyInstance and routes provides through the
|
|
3427
|
+
* app-manager registry helpers, so legacy instances surface in
|
|
3428
|
+
* Connections candidate lists like app-installed ones.
|
|
3429
|
+
*
|
|
3430
|
+
* Errors are swallowed and logged — capability registration is best-
|
|
3431
|
+
* effort; a failure here must not block start/stop.
|
|
3432
|
+
*/
|
|
3433
|
+
async function registerLegacyCapabilities(instanceId) {
|
|
3434
|
+
try {
|
|
3435
|
+
const meta = getInstance(instanceId);
|
|
3436
|
+
if (!meta)
|
|
3437
|
+
return;
|
|
3438
|
+
const { loadCapabilitySpecForLegacyInstance } = await import("./runtime/migrations.js");
|
|
3439
|
+
const synthSpec = loadCapabilitySpecForLegacyInstance(meta);
|
|
3440
|
+
if (!synthSpec?.provides?.length)
|
|
3441
|
+
return;
|
|
3442
|
+
// Same instance-name override + portOverride passthrough as
|
|
3443
|
+
// registerLegacyCapabilitiesTopLevel — Connections candidates should
|
|
3444
|
+
// show the user's instance name and advertise the actually-allocated
|
|
3445
|
+
// gateway port (not the yaml default).
|
|
3446
|
+
const instanceName = typeof meta.name === "string" && meta.name
|
|
3447
|
+
? meta.name
|
|
3448
|
+
: instanceId;
|
|
3449
|
+
const namedSpec = { ...synthSpec, name: instanceName };
|
|
3450
|
+
const portOverride = getGatewayPort(instanceId);
|
|
3451
|
+
const { registerCapabilities } = await import("./app/app-manager.js");
|
|
3452
|
+
registerCapabilities(instanceId, namedSpec, portOverride > 0 ? portOverride : undefined);
|
|
3453
|
+
}
|
|
3454
|
+
catch (e) {
|
|
3455
|
+
console.warn(`[legacy-capabilities] register failed for ${instanceId}: ${e?.message ?? e}`);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
async function unregisterLegacyCapabilities(instanceId, purge) {
|
|
3459
|
+
try {
|
|
3460
|
+
const { markCapabilitiesStopped, unregisterCapabilities } = await import("./app/app-manager.js");
|
|
3461
|
+
if (purge) {
|
|
3462
|
+
unregisterCapabilities(instanceId);
|
|
3463
|
+
}
|
|
3464
|
+
else {
|
|
3465
|
+
markCapabilitiesStopped(instanceId);
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
catch (e) {
|
|
3469
|
+
console.warn(`[legacy-capabilities] unregister failed for ${instanceId}: ${e?.message ?? e}`);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3158
3472
|
async function startInstance(nomadJobId) {
|
|
3159
3473
|
const instanceBackedApp = await getInstanceBackedInstalledApp(nomadJobId);
|
|
3160
3474
|
if (instanceBackedApp) {
|
|
3475
|
+
// PR 3 sub-step 3d: switch to resolveConnections in runtime mode so
|
|
3476
|
+
// missing required producers / ambiguous prefix candidates throw with
|
|
3477
|
+
// structured codes (412 / 409 / 400). Read the live instance.json so
|
|
3478
|
+
// UI bindings persisted via PUT /connections (PR 4) drive the
|
|
3479
|
+
// resolution; fall back to a stub `{ connections: {} }` when the
|
|
3480
|
+
// instance file isn't readable yet.
|
|
3161
3481
|
let extraEnv = {};
|
|
3162
3482
|
try {
|
|
3163
|
-
const {
|
|
3164
|
-
|
|
3483
|
+
const { resolveConnections, resolvedToLegacyEnv } = await import("./connection-resolver.js");
|
|
3484
|
+
const legacyInstanceManager = await import("./instance-manager.js");
|
|
3485
|
+
const meta = legacyInstanceManager.getInstance(nomadJobId);
|
|
3486
|
+
const instance = { connections: meta?.connections ?? {} };
|
|
3487
|
+
// Validate in runtime mode so missing required / ambiguous still throws
|
|
3488
|
+
// with structured error codes (412 / 409 / 400) before we touch Nomad.
|
|
3489
|
+
const { resolved } = resolveConnections(instanceBackedApp.spec, instance, "runtime");
|
|
3490
|
+
// Render the full RUNTIME_HOOKS env (covers llm/search/browser/mcp)
|
|
3491
|
+
// rather than just the default-category subset, so apply: openai-env
|
|
3492
|
+
// consumers (e.g. OpenWebUI) self-heal across provider port changes.
|
|
3493
|
+
const { renderRuntimeConnectionsEnv } = await import("./connection-apply.js");
|
|
3494
|
+
const runtimeEnv = await renderRuntimeConnectionsEnv(instanceBackedApp.spec, { id: nomadJobId, connections: instance.connections });
|
|
3495
|
+
extraEnv = { ...resolvedToLegacyEnv(resolved), ...runtimeEnv };
|
|
3165
3496
|
}
|
|
3166
3497
|
catch (e) {
|
|
3167
|
-
return {
|
|
3498
|
+
return {
|
|
3499
|
+
ok: false,
|
|
3500
|
+
error: e.message,
|
|
3501
|
+
...(e.code ? { code: e.code } : {}),
|
|
3502
|
+
...(typeof e.statusCode === "number" ? { statusCode: e.statusCode } : {}),
|
|
3503
|
+
};
|
|
3168
3504
|
}
|
|
3169
3505
|
const depCheck = await checkDependencies(instanceBackedApp.spec);
|
|
3170
3506
|
if (!depCheck.ok) {
|
|
@@ -3189,10 +3525,17 @@ var UnifiedNomadJobs;
|
|
|
3189
3525
|
return { ok: false, error: `App '${nomadJobId}' 必须通过 app-manager 启动` };
|
|
3190
3526
|
}
|
|
3191
3527
|
if (existsSync(instanceMetaPath(nomadJobId))) {
|
|
3192
|
-
|
|
3528
|
+
const result = await instanceScheduler.startInstance(nomadJobId);
|
|
3529
|
+
if (result.ok)
|
|
3530
|
+
await registerLegacyCapabilities(nomadJobId);
|
|
3531
|
+
return result;
|
|
3193
3532
|
}
|
|
3194
3533
|
if (nomadJobId.startsWith(OPENCLAW_PREFIX)) {
|
|
3195
|
-
|
|
3534
|
+
const inner = nomadJobId.slice(OPENCLAW_PREFIX.length);
|
|
3535
|
+
const result = await instanceScheduler.startInstance(inner);
|
|
3536
|
+
if (result.ok)
|
|
3537
|
+
await registerLegacyCapabilities(inner);
|
|
3538
|
+
return result;
|
|
3196
3539
|
}
|
|
3197
3540
|
if (!isAppJob(nomadJobId)) {
|
|
3198
3541
|
return { ok: false, error: `Cannot start unmanaged job "${nomadJobId}"` };
|
|
@@ -3213,10 +3556,19 @@ var UnifiedNomadJobs;
|
|
|
3213
3556
|
return { ok: false, error: `App '${nomadJobId}' 必须通过 app-manager 停止` };
|
|
3214
3557
|
}
|
|
3215
3558
|
if (existsSync(instanceMetaPath(nomadJobId))) {
|
|
3216
|
-
|
|
3559
|
+
const result = await instanceScheduler.stopInstance(nomadJobId, purge);
|
|
3560
|
+
if (result.ok || result.error?.includes("not running") || result.error?.includes("not found")) {
|
|
3561
|
+
await unregisterLegacyCapabilities(nomadJobId, purge);
|
|
3562
|
+
}
|
|
3563
|
+
return result;
|
|
3217
3564
|
}
|
|
3218
3565
|
if (nomadJobId.startsWith(OPENCLAW_PREFIX)) {
|
|
3219
|
-
|
|
3566
|
+
const inner = nomadJobId.slice(OPENCLAW_PREFIX.length);
|
|
3567
|
+
const result = await instanceScheduler.stopInstance(inner, purge);
|
|
3568
|
+
if (result.ok || result.error?.includes("not running") || result.error?.includes("not found")) {
|
|
3569
|
+
await unregisterLegacyCapabilities(inner, purge);
|
|
3570
|
+
}
|
|
3571
|
+
return result;
|
|
3220
3572
|
}
|
|
3221
3573
|
try {
|
|
3222
3574
|
const resp = await nomadDelete(`/v1/job/${nomadJobId}?purge=${purge}`);
|
|
@@ -3239,10 +3591,17 @@ var UnifiedNomadJobs;
|
|
|
3239
3591
|
return { ok: false, error: `App '${nomadJobId}' 必须通过 app-manager 重启` };
|
|
3240
3592
|
}
|
|
3241
3593
|
if (existsSync(instanceMetaPath(nomadJobId))) {
|
|
3242
|
-
|
|
3594
|
+
const result = await instanceScheduler.restartInstance(nomadJobId);
|
|
3595
|
+
if (result.ok)
|
|
3596
|
+
await registerLegacyCapabilities(nomadJobId);
|
|
3597
|
+
return result;
|
|
3243
3598
|
}
|
|
3244
3599
|
if (nomadJobId.startsWith(OPENCLAW_PREFIX)) {
|
|
3245
|
-
|
|
3600
|
+
const inner = nomadJobId.slice(OPENCLAW_PREFIX.length);
|
|
3601
|
+
const result = await instanceScheduler.restartInstance(inner);
|
|
3602
|
+
if (result.ok)
|
|
3603
|
+
await registerLegacyCapabilities(inner);
|
|
3604
|
+
return result;
|
|
3246
3605
|
}
|
|
3247
3606
|
if (!isAppJob(nomadJobId)) {
|
|
3248
3607
|
return { ok: false, error: `Cannot restart unmanaged job "${nomadJobId}"` };
|