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
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connection-transactor — PUT /connections atomic 5-step flow (§10.3).
|
|
3
|
+
*
|
|
4
|
+
* 1. Validate — resolveConnections runtime mode + per-binding checks
|
|
5
|
+
* 2. Snapshot — capture instance.json plus the side-effect files that
|
|
6
|
+
* persist hooks may write through: provider.env (LLM
|
|
7
|
+
* proxy-upstream), mcporter.json (MCP),
|
|
8
|
+
* openclaw.json (LLM proxy-upstream + adapter
|
|
9
|
+
* applyConnectionEnv), and Hermes config.yaml / .env.
|
|
10
|
+
* Snapshot now covers OpenClaw (V2 + legacy),
|
|
11
|
+
* Hermes (V2 + legacy), and generic app-dir consumers.
|
|
12
|
+
* 3. Apply hooks — invoke PERSIST_HOOKS for each resolved binding in
|
|
13
|
+
* deterministic order (default-env → search → browser
|
|
14
|
+
* → llm → mcp). On any failure, jump to step 5.
|
|
15
|
+
* 4. Persist — read-modify-write instance.json: replace `connections`
|
|
16
|
+
* map only, keep other fields (notably the
|
|
17
|
+
* `connections-env` written by step 3 hooks for generic
|
|
18
|
+
* apps without an adapter).
|
|
19
|
+
* 5. Rollback — reverse-order recovery from snapshots: restore each
|
|
20
|
+
* side-effect file (or unlink if it didn't exist
|
|
21
|
+
* pre-apply) before restoring instance.json.
|
|
22
|
+
*
|
|
23
|
+
* Returns either { ok: true, resolved, pending } or a structured error.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
import { INSTANCES_DIR, APPS_DIR } from "../config.js";
|
|
28
|
+
import { resolveConnections, ConnectionError } from "./connection-resolver.js";
|
|
29
|
+
import { PERSIST_HOOKS, UNPERSIST_HOOKS } from "./connection-apply.js";
|
|
30
|
+
import { categoryFromProviderCapability } from "./connection-resolver.js";
|
|
31
|
+
import * as capabilityRegistry from "./capability-registry.js";
|
|
32
|
+
import { withInstanceLock } from "../utils/instance-lock.js";
|
|
33
|
+
const APPLY_ORDER = ["default", "search", "browser", "llm", "mcp"];
|
|
34
|
+
export async function applyConnections(input) {
|
|
35
|
+
// Serialize against startApp/stopApp/restartApp on the same consumer
|
|
36
|
+
// instance. Without this, a concurrent stop can flip a bound provider
|
|
37
|
+
// to status:stopped between Validate (step 1) and Apply (step 3),
|
|
38
|
+
// leaving instance.json + side-effect files inconsistent.
|
|
39
|
+
return withInstanceLock(input.instance.id, () => applyConnectionsImpl(input));
|
|
40
|
+
}
|
|
41
|
+
async function applyConnectionsImpl(input) {
|
|
42
|
+
const { instance, spec, newConnections, saveInstanceJson, readInstanceJson, adapter } = input;
|
|
43
|
+
// ── Step 1: Validate ──────────────────────────────────────────────────
|
|
44
|
+
// Resolve the *new* connections in runtime mode so missing-required /
|
|
45
|
+
// ambiguous / invalid-binding all surface as ConnectionError before we
|
|
46
|
+
// touch any state.
|
|
47
|
+
const candidate = {
|
|
48
|
+
...instance,
|
|
49
|
+
connections: newConnections,
|
|
50
|
+
};
|
|
51
|
+
const { resolved } = resolveConnections(spec, candidate, "runtime");
|
|
52
|
+
// ── Step 1d: validateCapabilityEndpoint per binding (§12) ─────────────
|
|
53
|
+
// Catches registry tampering / drift before any persist hook fires:
|
|
54
|
+
// - Fresh re-resolve must match the registry entry (host/port/path)
|
|
55
|
+
// - Provider host must be on loopback or LAN
|
|
56
|
+
// - Protocol must be in the category whitelist
|
|
57
|
+
// Only applies to entries whose provider spec we can resolve; legacy
|
|
58
|
+
// instances without a synthesized spec degrade gracefully (skip).
|
|
59
|
+
await validateResolvedEndpoints(resolved);
|
|
60
|
+
// ── Step 2: Snapshot ──────────────────────────────────────────────────
|
|
61
|
+
// 覆盖 OpenClaw (V2+legacy)、Hermes (V2+legacy)、generic app-dir 三类
|
|
62
|
+
// consumer 的具体写入路径;详见 snapshotSideEffectFiles。
|
|
63
|
+
const snapshot = {
|
|
64
|
+
instanceJson: await readInstanceJson(instance.id),
|
|
65
|
+
files: snapshotSideEffectFiles(instance.id),
|
|
66
|
+
};
|
|
67
|
+
// ── Step 2b: Compute shrunk slots ─────────────────────────────────────
|
|
68
|
+
// PERSIST_HOOKS only sees slots that *currently* have a binding. When
|
|
69
|
+
// the user removes a binding (sets the slot to null or drops it from the
|
|
70
|
+
// map) we need to undo the durable state the prior persist hook wrote —
|
|
71
|
+
// otherwise unbinding LLM_ENDPOINT leaves x-jishushell.proxy.upstream
|
|
72
|
+
// pointing at the gone provider, etc. `shrunkSlots` enumerates exactly
|
|
73
|
+
// those: present-and-non-null in the on-disk connections, missing or
|
|
74
|
+
// null in `newConnections`.
|
|
75
|
+
const previousConnections = (snapshot.instanceJson?.connections ?? {});
|
|
76
|
+
const shrunkSlots = [];
|
|
77
|
+
for (const [slot, prior] of Object.entries(previousConnections)) {
|
|
78
|
+
if (prior == null)
|
|
79
|
+
continue; // already empty — nothing to undo
|
|
80
|
+
const next = newConnections?.[slot];
|
|
81
|
+
if (next != null)
|
|
82
|
+
continue; // still bound (or rebound) — persist hook handles it
|
|
83
|
+
const req = spec.requires?.find((r) => r.inject_as === slot);
|
|
84
|
+
const cap = req?.capability ?? "";
|
|
85
|
+
const fromToken = cap && (UNPERSIST_HOOKS[cap] ? cap : null);
|
|
86
|
+
const fromProvide = !fromToken ? categoryFromProviderCapability(cap) : null;
|
|
87
|
+
shrunkSlots.push({ slot, category: fromToken ?? fromProvide ?? "default" });
|
|
88
|
+
}
|
|
89
|
+
// ── Step 3: Apply hooks ───────────────────────────────────────────────
|
|
90
|
+
const ctx = makeApplyContext({ adapter, saveInstanceJson });
|
|
91
|
+
const appliedHooks = [];
|
|
92
|
+
try {
|
|
93
|
+
// 3a: unpersist removed slots BEFORE persisting new ones — if the user
|
|
94
|
+
// swapped one LLM provider for another, persist's overwrite already
|
|
95
|
+
// handles it; this loop only fires for slots that genuinely went
|
|
96
|
+
// away, so it's safe to run first. Failures here are logged and
|
|
97
|
+
// swallowed so a stale env var or stale upstream config doesn't
|
|
98
|
+
// block legitimate new bindings; the rollback step still recovers
|
|
99
|
+
// the full snapshot if a later persist throws.
|
|
100
|
+
for (const { slot, category } of shrunkSlots) {
|
|
101
|
+
const hook = UNPERSIST_HOOKS[category];
|
|
102
|
+
if (!hook)
|
|
103
|
+
continue;
|
|
104
|
+
try {
|
|
105
|
+
await hook(candidate, slot, ctx);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
console.warn(`[connection-transactor] unpersist ${category}/${slot} for ${instance.id} failed: ${e?.message ?? e}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const category of APPLY_ORDER) {
|
|
112
|
+
const hook = PERSIST_HOOKS[category];
|
|
113
|
+
if (!hook)
|
|
114
|
+
continue;
|
|
115
|
+
for (const binding of resolved) {
|
|
116
|
+
if (binding.category !== category)
|
|
117
|
+
continue;
|
|
118
|
+
await hook(candidate, binding, ctx);
|
|
119
|
+
appliedHooks.push(binding);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
await rollback(input, snapshot);
|
|
125
|
+
if (e instanceof ConnectionError)
|
|
126
|
+
throw e;
|
|
127
|
+
throw new ConnectionError("CONNECTION_APPLY_FAILED", 500, `Connection apply failed: ${e?.message ?? e}`, { applied: appliedHooks.map((b) => b.slot) });
|
|
128
|
+
}
|
|
129
|
+
// ── Step 4: Persist connections ───────────────────────────────────────
|
|
130
|
+
// Read-modify-write so we don't clobber `connections-env` that step 3
|
|
131
|
+
// hooks may have written for generic apps without an adapter.
|
|
132
|
+
try {
|
|
133
|
+
await saveInstanceJson(instance.id, (cur) => ({
|
|
134
|
+
...cur,
|
|
135
|
+
connections: newConnections,
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
await rollback(input, snapshot);
|
|
140
|
+
throw new ConnectionError("CONNECTION_PERSIST_FAILED", 500, `Failed to persist connections: ${e?.message ?? e}`);
|
|
141
|
+
}
|
|
142
|
+
return { resolved };
|
|
143
|
+
}
|
|
144
|
+
function makeApplyContext(opts) {
|
|
145
|
+
return {
|
|
146
|
+
registry: capabilityRegistry,
|
|
147
|
+
adapter: opts.adapter,
|
|
148
|
+
async writeConnectionEnv(instance, env) {
|
|
149
|
+
if (Object.keys(env).length === 0)
|
|
150
|
+
return;
|
|
151
|
+
if (opts.adapter && typeof opts.adapter.applyConnectionEnv === "function") {
|
|
152
|
+
await opts.adapter.applyConnectionEnv(instance.id, env);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Generic container app — persist into instance.json["connections-env"].
|
|
156
|
+
await opts.saveInstanceJson(instance.id, (cur) => ({
|
|
157
|
+
...cur,
|
|
158
|
+
"connections-env": {
|
|
159
|
+
...(cur["connections-env"] ?? {}),
|
|
160
|
+
...env,
|
|
161
|
+
},
|
|
162
|
+
}));
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Test-only export so unit tests can exercise the snapshot/restore path
|
|
168
|
+
* without spinning up the full transactor. Production code should not
|
|
169
|
+
* import these names directly.
|
|
170
|
+
*/
|
|
171
|
+
export const __testing__ = {
|
|
172
|
+
snapshotSideEffectFiles: (instanceId) => snapshotSideEffectFiles(instanceId),
|
|
173
|
+
applyFileSnapshots: (snapshots) => applyFileSnapshots(snapshots),
|
|
174
|
+
validateResolvedEndpoints: (resolved) => validateResolvedEndpoints(resolved),
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Walk every resolved binding and validate each provider entry's endpoint
|
|
178
|
+
* via `validateCapabilityEndpoint` (§12). Looks the provider spec up
|
|
179
|
+
* either through the app-manager (app-installed providers) or through the
|
|
180
|
+
* legacy synthesizer (instance-backed providers). When neither lookup
|
|
181
|
+
* yields a spec, the entry is left un-validated rather than rejected —
|
|
182
|
+
* the design's protection target is registry tampering after legitimate
|
|
183
|
+
* registration, and there's no spec to compare against in the missing case.
|
|
184
|
+
*/
|
|
185
|
+
async function validateResolvedEndpoints(resolved) {
|
|
186
|
+
if (resolved.length === 0)
|
|
187
|
+
return;
|
|
188
|
+
const { validateCapabilityEndpoint, findProviderProvide } = await import("./capability-endpoint-validator.js");
|
|
189
|
+
const { getApp } = await import("./app/app-manager.js");
|
|
190
|
+
const { loadCapabilitySpecForLegacyInstance } = await import("./runtime/migrations.js");
|
|
191
|
+
const legacyInstanceManager = await import("./instance-manager.js");
|
|
192
|
+
for (const binding of resolved) {
|
|
193
|
+
const category = binding.category;
|
|
194
|
+
for (const entry of binding.entries) {
|
|
195
|
+
// Look up the provider spec. Two paths:
|
|
196
|
+
// 1. App-installed provider — `getApp(entry.instanceId).spec`
|
|
197
|
+
// 2. Legacy instance-backed provider — synthesize from agent template
|
|
198
|
+
let providerSpec = null;
|
|
199
|
+
let isLegacySynthetic = false;
|
|
200
|
+
const appRecord = getApp(entry.instanceId);
|
|
201
|
+
if (appRecord) {
|
|
202
|
+
providerSpec = appRecord.spec;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
const meta = legacyInstanceManager.getInstance(entry.instanceId);
|
|
206
|
+
providerSpec = loadCapabilitySpecForLegacyInstance(meta);
|
|
207
|
+
isLegacySynthetic = !!providerSpec;
|
|
208
|
+
}
|
|
209
|
+
if (!providerSpec) {
|
|
210
|
+
// No spec we can validate against AND no live instance to synthesize
|
|
211
|
+
// one from. The registry entry is orphaned — treat as a hard reject
|
|
212
|
+
// so a tampered registry can't sneak past the validator simply by
|
|
213
|
+
// pointing at an instanceId that no longer exists.
|
|
214
|
+
const { ConnectionError } = await import("./connection-resolver.js");
|
|
215
|
+
throw new ConnectionError("INVALID_CAPABILITY_ENDPOINT", 400, `Provider '${entry.instanceId}' has no installed app or live instance — registry entry is orphaned`, {
|
|
216
|
+
providerInstanceId: entry.instanceId,
|
|
217
|
+
capability: entry.capability,
|
|
218
|
+
reason: "provider-spec-missing",
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
const provide = findProviderProvide(providerSpec, entry.capability);
|
|
222
|
+
if (!provide) {
|
|
223
|
+
if (isLegacySynthetic) {
|
|
224
|
+
// Legacy synthetic specs are minimal (id/name/requires/provides
|
|
225
|
+
// only). If the agent template hasn't declared this capability
|
|
226
|
+
// there's nothing to validate against — degrade rather than
|
|
227
|
+
// reject so we don't break working bindings on older instances.
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
// App-installed provider whose spec doesn't declare this capability.
|
|
231
|
+
// Either the registry was tampered to forge a capability the spec
|
|
232
|
+
// doesn't expose, or the spec drifted out of sync. Either way the
|
|
233
|
+
// safe move is to reject — apply hooks would otherwise consume a
|
|
234
|
+
// registry-supplied host/port/protocol with no spec confirmation.
|
|
235
|
+
const { ConnectionError } = await import("./connection-resolver.js");
|
|
236
|
+
throw new ConnectionError("INVALID_CAPABILITY_ENDPOINT", 400, `Provider '${entry.instanceId}' app spec does not declare capability '${entry.capability}'`, {
|
|
237
|
+
providerInstanceId: entry.instanceId,
|
|
238
|
+
capability: entry.capability,
|
|
239
|
+
reason: "capability-not-in-spec",
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
validateCapabilityEndpoint(providerSpec, entry.instanceId, provide, entry, category);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Capture raw bytes of files that PERSIST_HOOKS may write through. Used to
|
|
248
|
+
* roll back partial mutations when a later hook throws. Files that did not
|
|
249
|
+
* exist at snapshot time are recorded with `bytes: null` and unlinked on
|
|
250
|
+
* rollback so we don't leave behind half-written state.
|
|
251
|
+
*
|
|
252
|
+
* 覆盖三类 consumer 的所有已知写入路径:
|
|
253
|
+
* - OpenClaw:provider.env / openclaw.json / mcporter.json(V2 + legacy)
|
|
254
|
+
* - Hermes:agent-home/config.yaml + agent-home/.env(V2 + legacy)
|
|
255
|
+
* - Generic app-dir:apps/<id>/instance.json
|
|
256
|
+
*
|
|
257
|
+
* 路径前缀来自 INSTANCES_DIR / APPS_DIR(均读 JISHUSHELL_HOME 环境变量),
|
|
258
|
+
* 不存在的路径记为 bytes:null,回滚时做 unlink 而非写入。
|
|
259
|
+
*/
|
|
260
|
+
function snapshotSideEffectFiles(instanceId) {
|
|
261
|
+
const legacyHome = join(INSTANCES_DIR, instanceId);
|
|
262
|
+
const v2Home = join(APPS_DIR, instanceId);
|
|
263
|
+
const candidates = [
|
|
264
|
+
// ── Per-instance secret bag (LLM proxy-upstream) ──
|
|
265
|
+
join(legacyHome, "provider.env"),
|
|
266
|
+
// ── OpenClaw-native config (V2 + legacy layouts) ──
|
|
267
|
+
join(legacyHome, "openclaw-home", ".openclaw", "openclaw.json"),
|
|
268
|
+
join(v2Home, "openclaw-home", ".openclaw", "openclaw.json"),
|
|
269
|
+
// ── MCP server registry consumed by the OpenClaw runtime ──
|
|
270
|
+
join(legacyHome, "openclaw-home", ".openclaw", "workspace", "config", "mcporter.json"),
|
|
271
|
+
join(v2Home, "openclaw-home", ".openclaw", "workspace", "config", "mcporter.json"),
|
|
272
|
+
// ── Hermes adapter writes (V2 layout): config.yaml + .env via
|
|
273
|
+
// applyConnectionEnv. Without these in the snapshot, a partial MCP
|
|
274
|
+
// apply for the search slot leaves a half-written config.yaml when
|
|
275
|
+
// a later LLM hook fails. Path matches `resolveHermesPaths`.
|
|
276
|
+
join(v2Home, "agent-home", "config.yaml"),
|
|
277
|
+
join(v2Home, "agent-home", ".env"),
|
|
278
|
+
// ── Hermes legacy layout (pre-V2 instances) ──
|
|
279
|
+
join(legacyHome, "agent-home", "config.yaml"),
|
|
280
|
+
join(legacyHome, "agent-home", ".env"),
|
|
281
|
+
// ── Generic app-dir consumer (e.g. OpenWebUI) — instance.json
|
|
282
|
+
// holds the persisted `connections-env` map written by the
|
|
283
|
+
// openai-env hook for non-adapter apps. Already covered by
|
|
284
|
+
// `snapshot.instanceJson`, but listed here as an extra safety net
|
|
285
|
+
// so a partial direct write at apps/<id>/instance.json (outside
|
|
286
|
+
// the saveInstanceJson code path) still rolls back. ──
|
|
287
|
+
join(v2Home, "instance.json"),
|
|
288
|
+
];
|
|
289
|
+
const out = [];
|
|
290
|
+
for (const path of candidates) {
|
|
291
|
+
try {
|
|
292
|
+
out.push({ path, bytes: existsSync(path) ? readFileSync(path) : null });
|
|
293
|
+
}
|
|
294
|
+
catch (e) {
|
|
295
|
+
console.warn(`[connection-transactor] snapshot read failed for ${path}: ${e?.message ?? e}`);
|
|
296
|
+
out.push({ path, bytes: null });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return out;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Restore each file to its snapshotted bytes. Files whose snapshot recorded
|
|
303
|
+
* `bytes: null` (didn't exist pre-apply) are unlinked. Errors are logged
|
|
304
|
+
* per-file so a single failed restore doesn't block the rest.
|
|
305
|
+
*/
|
|
306
|
+
function applyFileSnapshots(snapshots) {
|
|
307
|
+
for (const file of snapshots) {
|
|
308
|
+
try {
|
|
309
|
+
if (file.bytes === null) {
|
|
310
|
+
if (existsSync(file.path))
|
|
311
|
+
unlinkSync(file.path);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
try {
|
|
315
|
+
mkdirSync(dirname(file.path), { recursive: true });
|
|
316
|
+
}
|
|
317
|
+
catch { /* dir likely exists */ }
|
|
318
|
+
writeFileSync(file.path, file.bytes);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (e) {
|
|
322
|
+
console.error(`[connection-transactor] rollback file restore failed for ${file.path}: ${e?.message ?? e}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function rollback(input, snapshot) {
|
|
327
|
+
// Restore side-effect files first so their state is consistent before the
|
|
328
|
+
// instance.json `connections` map is rewound. Failures are logged but not
|
|
329
|
+
// propagated — at this point the caller has already seen a CONNECTION_*
|
|
330
|
+
// error and we want to give every later restore step a chance to run.
|
|
331
|
+
applyFileSnapshots(snapshot.files);
|
|
332
|
+
try {
|
|
333
|
+
// Single read-modify-write that overlays the snapshot on the current
|
|
334
|
+
// instance.json — atomic rollback for the single-file state.
|
|
335
|
+
await input.saveInstanceJson(input.instance.id, () => snapshot.instanceJson);
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
console.error(`[connection-transactor] rollback failed: ${e?.message ?? e}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
//# sourceMappingURL=connection-transactor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-transactor.js","sourceRoot":"","sources":["../../src/services/connection-transactor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE/E,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAEvE,OAAO,EAAE,8BAA8B,EAAE,MAAM,0BAA0B,CAAC;AAC1E,OAAO,KAAK,kBAAkB,MAAM,0BAA0B,CAAC;AAG/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAI7D,MAAM,WAAW,GAAe,CAAC,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;AA+B/E,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAAsB;IAC3D,qEAAqE;IACrE,sEAAsE;IACtE,kEAAkE;IAClE,0DAA0D;IAC1D,OAAO,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC,CAAC;AAChF,CAAC;AAED,KAAK,UAAU,oBAAoB,CAAC,KAAsB;IACxD,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,cAAc,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,OAAO,EAAE,GAAG,KAAK,CAAC;IAE9F,yEAAyE;IACzE,sEAAsE;IACtE,uEAAuE;IACvE,mBAAmB;IACnB,MAAM,SAAS,GAAgB;QAC7B,GAAG,QAAQ;QACX,WAAW,EAAE,cAAc;KAC5B,CAAC;IACF,MAAM,EAAE,QAAQ,EAAE,GAAG,kBAAkB,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;IAEpE,yEAAyE;IACzE,oEAAoE;IACpE,sEAAsE;IACtE,+CAA+C;IAC/C,iDAAiD;IACjD,qEAAqE;IACrE,kEAAkE;IAClE,MAAM,yBAAyB,CAAC,QAAQ,CAAC,CAAC;IAE1C,yEAAyE;IACzE,gEAAgE;IAChE,+CAA+C;IAC/C,MAAM,QAAQ,GAAuB;QACnC,YAAY,EAAE,MAAM,gBAAgB,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjD,KAAK,EAAE,uBAAuB,CAAC,QAAQ,CAAC,EAAE,CAAC;KAC5C,CAAC;IAEF,yEAAyE;IACzE,sEAAsE;IACtE,yEAAyE;IACzE,wEAAwE;IACxE,sEAAsE;IACtE,uEAAuE;IACvE,qEAAqE;IACrE,4BAA4B;IAC5B,MAAM,mBAAmB,GAAG,CAAC,QAAQ,CAAC,YAAY,EAAE,WAAW,IAAI,EAAE,CAA4B,CAAC;IAClG,MAAM,WAAW,GAA8C,EAAE,CAAC;IAClE,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,mBAAmB,CAAC,EAAE,CAAC;QAChE,IAAI,KAAK,IAAI,IAAI;YAAE,SAAS,CAAC,kCAAkC;QAC/D,MAAM,IAAI,GAAI,cAA0C,EAAE,CAAC,IAAI,CAAC,CAAC;QACjE,IAAI,IAAI,IAAI,IAAI;YAAE,SAAS,CAAC,qDAAqD;QACjF,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC;QAC7D,MAAM,GAAG,GAAG,GAAG,EAAE,UAAU,IAAI,EAAE,CAAC;QAClC,MAAM,SAAS,GAAG,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC7D,MAAM,WAAW,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,8BAA8B,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC5E,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,IAAI,WAAW,IAAI,SAAS,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,yEAAyE;IACzE,MAAM,GAAG,GAAG,gBAAgB,CAAC,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAC5D,MAAM,YAAY,GAAyB,EAAE,CAAC;IAC9C,IAAI,CAAC;QACH,uEAAuE;QACvE,wEAAwE;QACxE,qEAAqE;QACrE,oEAAoE;QACpE,oEAAoE;QACpE,sEAAsE;QACtE,mDAAmD;QACnD,KAAK,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,WAAW,EAAE,CAAC;YAC7C,MAAM,IAAI,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;YACvC,IAAI,CAAC,IAAI;gBAAE,SAAS;YACpB,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;YACnC,CAAC;YAAC,OAAO,CAAM,EAAE,CAAC;gBAChB,OAAO,CAAC,IAAI,CACV,qCAAqC,QAAQ,IAAI,IAAI,QAAQ,QAAQ,CAAC,EAAE,YAAY,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CACtG,CAAC;YACJ,CAAC;QACH,CAAC;QACD,KAAK,MAAM,QAAQ,IAAI,WAAW,EAAE,CAAC;YACnC,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;YACrC,IAAI,CAAC,IAAI;gBAAE,SAAS;YACpB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;gBAC/B,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ;oBAAE,SAAS;gBAC5C,MAAM,IAAI,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;gBACpC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,CAAM,EAAE,CAAC;QAChB,MAAM,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAChC,IAAI,CAAC,YAAY,eAAe;YAAE,MAAM,CAAC,CAAC;QAC1C,MAAM,IAAI,eAAe,CACvB,yBAAyB,EACzB,GAAG,EACH,4BAA4B,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,EAC7C,EAAE,OAAO,EAAE,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAC7C,CAAC;IACJ,CAAC;IAED,yEAAyE;IACzE,sEAAsE;IACtE,8DAA8D;IAC9D,IAAI,CAAC;QACH,MAAM,gBAAgB,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;YAC5C,GAAG,GAAG;YACN,WAAW,EAAE,cAAc;SAC5B,CAAC,CAAC,CAAC;IACN,CAAC;IAAC,OAAO,CAAM,EAAE,CAAC;QAChB,MAAM,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAChC,MAAM,IAAI,eAAe,CACvB,2BAA2B,EAC3B,GAAG,EACH,kCAAkC,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CACpD,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,CAAC;AACtB,CAAC;AAED,SAAS,gBAAgB,CAAC,IAGzB;IACC,OAAO;QACL,QAAQ,EAAE,kBAAkB;QAC5B,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,KAAK,CAAC,kBAAkB,CAAC,QAAQ,EAAE,GAAG;YACpC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO;YAC1C,IAAI,IAAI,CAAC,OAAO,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,kBAAkB,KAAK,UAAU,EAAE,CAAC;gBAC1E,MAAM,IAAI,CAAC,OAAO,CAAC,kBAAkB,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;gBACxD,OAAO;YACT,CAAC;YACD,yEAAyE;YACzE,MAAM,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBACjD,GAAG,GAAG;gBACN,iBAAiB,EAAE;oBACjB,GAAG,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC;oBACjC,GAAG,GAAG;iBACP;aACF,CAAC,CAAC,CAAC;QACN,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,uBAAuB,EAAE,CAAC,UAAkB,EAAE,EAAE,CAAC,uBAAuB,CAAC,UAAU,CAAC;IACpF,kBAAkB,EAAE,CAAC,SAAyB,EAAE,EAAE,CAAC,kBAAkB,CAAC,SAAS,CAAC;IAChF,yBAAyB,EAAE,CAAC,QAA8B,EAAE,EAAE,CAAC,yBAAyB,CAAC,QAAQ,CAAC;CACnG,CAAC;AAEF;;;;;;;;GAQG;AACH,KAAK,UAAU,yBAAyB,CAAC,QAA8B;IACrE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAElC,MAAM,EAAE,0BAA0B,EAAE,mBAAmB,EAAE,GAAG,MAAM,MAAM,CACtE,oCAAoC,CACrC,CAAC;IACF,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,sBAAsB,CAAC,CAAC;IACxD,MAAM,EAAE,mCAAmC,EAAE,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC;IACxF,MAAM,qBAAqB,GAAG,MAAM,MAAM,CAAC,uBAAuB,CAAC,CAAC;IAEpE,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAI,OAAO,CAAC,QAA6D,CAAC;QACxF,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;YACpC,wCAAwC;YACxC,gEAAgE;YAChE,wEAAwE;YACxE,IAAI,YAAY,GAAG,IAA6D,CAAC;YACjF,IAAI,iBAAiB,GAAG,KAAK,CAAC;YAC9B,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAC3C,IAAI,SAAS,EAAE,CAAC;gBACd,YAAY,GAAG,SAAS,CAAC,IAAI,CAAC;YAChC,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,GAAG,qBAAqB,CAAC,WAAW,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;gBACjE,YAAY,GAAG,mCAAmC,CAAC,IAAI,CAAC,CAAC;gBACzD,iBAAiB,GAAG,CAAC,CAAC,YAAY,CAAC;YACrC,CAAC;YAED,IAAI,CAAC,YAAY,EAAE,CAAC;gBAClB,qEAAqE;gBACrE,oEAAoE;gBACpE,kEAAkE;gBAClE,mDAAmD;gBACnD,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAC;gBACrE,MAAM,IAAI,eAAe,CACvB,6BAA6B,EAC7B,GAAG,EACH,aAAa,KAAK,CAAC,UAAU,sEAAsE,EACnG;oBACE,kBAAkB,EAAE,KAAK,CAAC,UAAU;oBACpC,UAAU,EAAE,KAAK,CAAC,UAAU;oBAC5B,MAAM,EAAE,uBAAuB;iBAChC,CACF,CAAC;YACJ,CAAC;YAED,MAAM,OAAO,GAAG,mBAAmB,CAAC,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;YACpE,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,IAAI,iBAAiB,EAAE,CAAC;oBACtB,gEAAgE;oBAChE,+DAA+D;oBAC/D,4DAA4D;oBAC5D,gEAAgE;oBAChE,SAAS;gBACX,CAAC;gBACD,qEAAqE;gBACrE,kEAAkE;gBAClE,kEAAkE;gBAClE,iEAAiE;gBACjE,kEAAkE;gBAClE,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAC;gBACrE,MAAM,IAAI,eAAe,CACvB,6BAA6B,EAC7B,GAAG,EACH,aAAa,KAAK,CAAC,UAAU,2CAA2C,KAAK,CAAC,UAAU,GAAG,EAC3F;oBACE,kBAAkB,EAAE,KAAK,CAAC,UAAU;oBACpC,UAAU,EAAE,KAAK,CAAC,UAAU;oBAC5B,MAAM,EAAE,wBAAwB;iBACjC,CACF,CAAC;YACJ,CAAC;YACD,0BAA0B,CAAC,YAAY,EAAE,KAAK,CAAC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;QACvF,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,uBAAuB,CAAC,UAAkB;IACjD,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAC1C,MAAM,UAAU,GAAG;QACjB,qDAAqD;QACrD,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC;QAChC,qDAAqD;QACrD,IAAI,CAAC,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,eAAe,CAAC;QAC/D,IAAI,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,EAAE,eAAe,CAAC;QAC3D,6DAA6D;QAC7D,IAAI,CAAC,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,eAAe,CAAC;QACtF,IAAI,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,eAAe,CAAC;QAClF,+DAA+D;QAC/D,sEAAsE;QACtE,sEAAsE;QACtE,gEAAgE;QAChE,IAAI,CAAC,MAAM,EAAE,YAAY,EAAE,aAAa,CAAC;QACzC,IAAI,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,CAAC;QAClC,gDAAgD;QAChD,IAAI,CAAC,UAAU,EAAE,YAAY,EAAE,aAAa,CAAC;QAC7C,IAAI,CAAC,UAAU,EAAE,YAAY,EAAE,MAAM,CAAC;QACtC,+DAA+D;QAC/D,8DAA8D;QAC9D,8DAA8D;QAC9D,qEAAqE;QACrE,mEAAmE;QACnE,0DAA0D;QAC1D,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC;KAC9B,CAAC;IACF,MAAM,GAAG,GAAmB,EAAE,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC1E,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,OAAO,CAAC,IAAI,CAAC,oDAAoD,IAAI,KAAK,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;YAC7F,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,SAAS,kBAAkB,CAAC,SAAyB;IACnD,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,IAAI,IAAI,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;gBACxB,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;oBAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnD,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC;oBACH,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBACrD,CAAC;gBAAC,MAAM,CAAC,CAAC,uBAAuB,CAAC,CAAC;gBACnC,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;YACvC,CAAC;QACH,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,OAAO,CAAC,KAAK,CAAC,4DAA4D,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;QAC7G,CAAC;IACH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,KAAsB,EAAE,QAA4B;IAC1E,0EAA0E;IAC1E,0EAA0E;IAC1E,wEAAwE;IACxE,sEAAsE;IACtE,kBAAkB,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAEnC,IAAI,CAAC;QACH,qEAAqE;QACrE,6DAA6D;QAC7D,MAAM,KAAK,CAAC,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IAC/E,CAAC;IAAC,OAAO,CAAM,EAAE,CAAC;QAChB,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/E,CAAC;AACH,CAAC"}
|
|
@@ -133,6 +133,19 @@ export declare function getGatewayPort(instanceId: string): number;
|
|
|
133
133
|
export declare function getListeningHostForPort(port: number): string;
|
|
134
134
|
export declare function getAdvertisedHostForPort(port: number): string;
|
|
135
135
|
export declare function getGatewayHost(instanceId: string): Promise<string>;
|
|
136
|
+
/**
|
|
137
|
+
* Resolve the host IP for a specific port belonging to an app's Nomad job.
|
|
138
|
+
*
|
|
139
|
+
* On macOS with Colima + docker driver the container port is bound *inside*
|
|
140
|
+
* the VM, not on the host. `ss` (or `netstat`) run on the macOS host see
|
|
141
|
+
* nothing on that port, so `getListeningHostForPort` returns 127.0.0.1 and
|
|
142
|
+
* the panel proxy gets a 502.
|
|
143
|
+
*
|
|
144
|
+
* This function mirrors the logic of `getGatewayHost` but matches by port
|
|
145
|
+
* *value* rather than by the "gateway" label, making it suitable for
|
|
146
|
+
* arbitrary capability ports exposed by app tasks.
|
|
147
|
+
*/
|
|
148
|
+
export declare function getHostForAppPort(appId: string, port: number): Promise<string>;
|
|
136
149
|
/**
|
|
137
150
|
* Wrap an IPv6 literal in brackets for safe URL host-component / Host-header
|
|
138
151
|
* use. Bare names ("gateway.local") and IPv4 ("127.0.0.1") contain no colon
|
|
@@ -133,13 +133,22 @@ const _pendingPorts = new Set();
|
|
|
133
133
|
export function extractGatewayPort(runtime, agentType) {
|
|
134
134
|
if (!runtime)
|
|
135
135
|
return null;
|
|
136
|
-
// Primary: RuntimeSpec.ports[] declaration
|
|
136
|
+
// Primary: RuntimeSpec.ports[] declaration. Prefer a port literally named
|
|
137
|
+
// "gateway" (the convention OpenClaw / Hermes templates use); otherwise
|
|
138
|
+
// fall back to the first declared port. This makes the helper work for
|
|
139
|
+
// generic V2 apps (SearXNG = "http", Open WebUI = "http", Ollama =
|
|
140
|
+
// "ollama", etc.) without forcing every template to rename its port.
|
|
137
141
|
const ports = Array.isArray(runtime.ports) ? runtime.ports : [];
|
|
138
142
|
for (const port of ports) {
|
|
139
143
|
if (port?.name === "gateway" && Number.isInteger(port.hostPort) && port.hostPort > 0) {
|
|
140
144
|
return port.hostPort;
|
|
141
145
|
}
|
|
142
146
|
}
|
|
147
|
+
for (const port of ports) {
|
|
148
|
+
if (Number.isInteger(port?.hostPort) && port.hostPort > 0) {
|
|
149
|
+
return port.hostPort;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
143
152
|
// Fall back to the adapter's legacy reader. Default agentType "openclaw".
|
|
144
153
|
try {
|
|
145
154
|
const adapter = getAdapter(agentType || "openclaw");
|
|
@@ -467,7 +476,8 @@ export function listInstances() {
|
|
|
467
476
|
// even though the backup chain still holds valid content on disk.
|
|
468
477
|
const meta = safeReadJson(metaPath, `instance:${name}`);
|
|
469
478
|
if (meta) {
|
|
470
|
-
|
|
479
|
+
const normalised = backfillInstanceMeta(meta);
|
|
480
|
+
deduped.set(name, normalised);
|
|
471
481
|
continue;
|
|
472
482
|
}
|
|
473
483
|
// safeReadJson → null can mean any of (a) primary missing + no
|
|
@@ -497,8 +507,10 @@ export function getInstance(instanceId) {
|
|
|
497
507
|
// is still served from the .bak chain. Returning null on "truly gone"
|
|
498
508
|
// (no primary, no backups) keeps the existing 404 behavior intact.
|
|
499
509
|
const meta = safeReadJson(metaPath, `instance:${instanceId}`);
|
|
500
|
-
if (meta)
|
|
501
|
-
|
|
510
|
+
if (meta) {
|
|
511
|
+
const normalised = backfillInstanceMeta(meta);
|
|
512
|
+
return normalised;
|
|
513
|
+
}
|
|
502
514
|
// safeReadJson swallows every read error internally, which is exactly
|
|
503
515
|
// wrong for the EACCES case — a root-owned primary would silently
|
|
504
516
|
// return null and callers would report "Instance not found" instead
|
|
@@ -876,26 +888,68 @@ export function getGatewayPort(instanceId) {
|
|
|
876
888
|
const _gwHostCache = new Map();
|
|
877
889
|
const GW_HOST_CACHE_TTL = 30000;
|
|
878
890
|
export function getListeningHostForPort(port) {
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
891
|
+
// Linux: ss is universal and parses cleanly. macOS/BSD ships lsof but
|
|
892
|
+
// not ss, so without this fallback execFileSync throws ENOENT, the
|
|
893
|
+
// catch block silently returns 127.0.0.1, and any caller that fans
|
|
894
|
+
// out to the wrong host fails (the capability HTTP proxy in
|
|
895
|
+
// `proxyProvidedCapability`, the gateway-host fallback for raw_exec
|
|
896
|
+
// instances, etc.). On macOS docker driver mode the actual published
|
|
897
|
+
// host can be e.g. 10.188.0.21 via `host_network = "external"`, never
|
|
898
|
+
// loopback — so silently picking 127.0.0.1 leaves the panel fetching
|
|
899
|
+
// a host nothing is listening on.
|
|
900
|
+
const safe = safePort(port);
|
|
901
|
+
const fromSs = () => {
|
|
902
|
+
try {
|
|
903
|
+
const out = execFileSync("ss", ["-tlnH", "sport", "=", ":" + safe], {
|
|
904
|
+
encoding: "utf-8",
|
|
905
|
+
timeout: 3000,
|
|
906
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
907
|
+
});
|
|
908
|
+
for (const line of out.split("\n")) {
|
|
909
|
+
let match = line.match(/\s([\d.]+):(\d+)\s/);
|
|
910
|
+
if (!match)
|
|
911
|
+
match = line.match(/\s\[([0-9a-fA-F:]+)\]:(\d+)\s/);
|
|
912
|
+
if (match && match[2] === String(port))
|
|
913
|
+
return match[1];
|
|
894
914
|
}
|
|
895
915
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
916
|
+
catch { /* ss not available or no match */ }
|
|
917
|
+
return null;
|
|
918
|
+
};
|
|
919
|
+
const fromLsof = () => {
|
|
920
|
+
try {
|
|
921
|
+
// `-FnP` keeps numeric output (no name resolution, no /etc/services).
|
|
922
|
+
// Lines beginning with `n` carry the bind address: e.g. `n10.188.0.21:8080`,
|
|
923
|
+
// `n127.0.0.1:8080`, `n[::1]:8080`, or `n*:8080` for wildcard binds.
|
|
924
|
+
const out = execFileSync("lsof", ["-nP", "-iTCP:" + safe, "-sTCP:LISTEN", "-FnP"], {
|
|
925
|
+
encoding: "utf-8",
|
|
926
|
+
timeout: 3000,
|
|
927
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
928
|
+
});
|
|
929
|
+
for (const line of out.split("\n")) {
|
|
930
|
+
if (!line.startsWith("n"))
|
|
931
|
+
continue;
|
|
932
|
+
const body = line.slice(1);
|
|
933
|
+
// IPv6 form: [::1]:8080 → strip brackets
|
|
934
|
+
const v6 = body.match(/^\[([0-9a-fA-F:]+)\]:(\d+)$/);
|
|
935
|
+
if (v6 && v6[2] === String(port))
|
|
936
|
+
return v6[1];
|
|
937
|
+
const idx = body.lastIndexOf(":");
|
|
938
|
+
if (idx <= 0)
|
|
939
|
+
continue;
|
|
940
|
+
const addr = body.slice(0, idx);
|
|
941
|
+
const portPart = body.slice(idx + 1);
|
|
942
|
+
if (portPart === String(port))
|
|
943
|
+
return addr === "*" ? "0.0.0.0" : addr;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
catch { /* lsof unavailable or no match */ }
|
|
947
|
+
return null;
|
|
948
|
+
};
|
|
949
|
+
const detected = (process.platform === "linux" ? fromSs() : fromLsof()) ?? fromLsof() ?? fromSs();
|
|
950
|
+
if (!detected)
|
|
951
|
+
return "127.0.0.1";
|
|
952
|
+
return detected === "0.0.0.0" ? "127.0.0.1" : detected;
|
|
899
953
|
}
|
|
900
954
|
function getPrimaryIpv4Address() {
|
|
901
955
|
try {
|
|
@@ -941,7 +995,13 @@ export async function getGatewayHost(instanceId) {
|
|
|
941
995
|
}
|
|
942
996
|
}
|
|
943
997
|
catch { /* use fallback prefix */ }
|
|
944
|
-
|
|
998
|
+
// Avoid double-prefix when the instance ID already carries the
|
|
999
|
+
// adapter prefix (e.g. apps stored as `openclaw-c1` whose Nomad
|
|
1000
|
+
// job name is also `openclaw-c1`). Without this guard we'd ask
|
|
1001
|
+
// Nomad for `openclaw-openclaw-c1`, get 0 allocs, and fall back
|
|
1002
|
+
// to the 127.0.0.1 default — which silently breaks the gateway
|
|
1003
|
+
// proxy on docker driver where the real HostIP differs.
|
|
1004
|
+
const jid = instanceId.startsWith(prefix) ? instanceId : `${prefix}${instanceId}`;
|
|
945
1005
|
const headers = { "Content-Type": "application/json" };
|
|
946
1006
|
const token = getNomadToken();
|
|
947
1007
|
if (token)
|
|
@@ -1007,6 +1067,60 @@ export async function getGatewayHost(instanceId) {
|
|
|
1007
1067
|
_gwHostCache.set(instanceId, { host: result, ts: Date.now() });
|
|
1008
1068
|
return result;
|
|
1009
1069
|
}
|
|
1070
|
+
const _appPortHostCache = new Map();
|
|
1071
|
+
const APP_PORT_HOST_CACHE_TTL = 30000;
|
|
1072
|
+
/**
|
|
1073
|
+
* Resolve the host IP for a specific port belonging to an app's Nomad job.
|
|
1074
|
+
*
|
|
1075
|
+
* On macOS with Colima + docker driver the container port is bound *inside*
|
|
1076
|
+
* the VM, not on the host. `ss` (or `netstat`) run on the macOS host see
|
|
1077
|
+
* nothing on that port, so `getListeningHostForPort` returns 127.0.0.1 and
|
|
1078
|
+
* the panel proxy gets a 502.
|
|
1079
|
+
*
|
|
1080
|
+
* This function mirrors the logic of `getGatewayHost` but matches by port
|
|
1081
|
+
* *value* rather than by the "gateway" label, making it suitable for
|
|
1082
|
+
* arbitrary capability ports exposed by app tasks.
|
|
1083
|
+
*/
|
|
1084
|
+
export async function getHostForAppPort(appId, port) {
|
|
1085
|
+
const cacheKey = `${appId}:${port}`;
|
|
1086
|
+
const cached = _appPortHostCache.get(cacheKey);
|
|
1087
|
+
if (cached && Date.now() - cached.ts < APP_PORT_HOST_CACHE_TTL)
|
|
1088
|
+
return cached.host;
|
|
1089
|
+
let result = "127.0.0.1";
|
|
1090
|
+
try {
|
|
1091
|
+
const { getNomadDriver, getNomadAddr, getNomadToken } = await import("../config.js");
|
|
1092
|
+
if (getNomadDriver() === "docker") {
|
|
1093
|
+
const headers = { "Content-Type": "application/json" };
|
|
1094
|
+
const token = getNomadToken();
|
|
1095
|
+
if (token)
|
|
1096
|
+
headers["X-Nomad-Token"] = token;
|
|
1097
|
+
const resp = await fetch(`${getNomadAddr()}/v1/job/${encodeURIComponent(appId)}/allocations`, { headers, signal: AbortSignal.timeout(5000) });
|
|
1098
|
+
if (resp.ok) {
|
|
1099
|
+
const allocs = await resp.json();
|
|
1100
|
+
const alloc = allocs.find((a) => a.ClientStatus === "running")
|
|
1101
|
+
?? allocs.find((a) => a.ClientStatus === "pending");
|
|
1102
|
+
if (alloc) {
|
|
1103
|
+
const detail = await fetch(`${getNomadAddr()}/v1/allocation/${encodeURIComponent(alloc.ID)}`, { headers, signal: AbortSignal.timeout(5000) });
|
|
1104
|
+
if (detail.ok) {
|
|
1105
|
+
const d = await detail.json();
|
|
1106
|
+
// Bridge / host-network mode: ports listed under Shared.Ports
|
|
1107
|
+
const sharedPorts = d?.AllocatedResources?.Shared?.Ports ?? [];
|
|
1108
|
+
const matched = sharedPorts.find((p) => p.Value === port || p.To === port);
|
|
1109
|
+
if (matched?.HostIP && matched.HostIP !== "0.0.0.0") {
|
|
1110
|
+
result = matched.HostIP;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
catch { /* fall through to ss-based detection */ }
|
|
1118
|
+
if (result === "127.0.0.1") {
|
|
1119
|
+
result = getListeningHostForPort(port);
|
|
1120
|
+
}
|
|
1121
|
+
_appPortHostCache.set(cacheKey, { host: result, ts: Date.now() });
|
|
1122
|
+
return result;
|
|
1123
|
+
}
|
|
1010
1124
|
/**
|
|
1011
1125
|
* Wrap an IPv6 literal in brackets for safe URL host-component / Host-header
|
|
1012
1126
|
* use. Bare names ("gateway.local") and IPv4 ("127.0.0.1") contain no colon
|