jishushell 0.4.24 → 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.
Files changed (167) hide show
  1. package/INSTALL-NOTICE +11 -0
  2. package/apps/browserless-chromium-container.yaml +78 -0
  3. package/apps/hermes-container.yaml +36 -2
  4. package/apps/ollama-binary.yaml +91 -90
  5. package/apps/ollama-cpu-container.yaml +8 -1
  6. package/apps/ollama-with-hollama-binary.yaml +91 -90
  7. package/apps/openclaw-binary.yaml +30 -1
  8. package/apps/openclaw-container.yaml +37 -2
  9. package/apps/openclaw-with-ollama-container.yaml +11 -2
  10. package/apps/openclaw-with-searxng-container.yaml +22 -2
  11. package/apps/openwebui-container.yaml +45 -1
  12. package/apps/playwright-container.yaml +7 -1
  13. package/apps/searxng-container.yaml +54 -4
  14. package/dist/cli/app.js +79 -9
  15. package/dist/cli/app.js.map +1 -1
  16. package/dist/cli/doctor.d.ts +12 -12
  17. package/dist/cli/doctor.js +242 -55
  18. package/dist/cli/doctor.js.map +1 -1
  19. package/dist/cli/llm.d.ts +4 -3
  20. package/dist/cli/llm.js +4 -3
  21. package/dist/cli/llm.js.map +1 -1
  22. package/dist/cli/panel.d.ts +6 -5
  23. package/dist/cli/panel.js +10 -9
  24. package/dist/cli/panel.js.map +1 -1
  25. package/dist/control.d.ts +7 -6
  26. package/dist/control.js +7 -6
  27. package/dist/control.js.map +1 -1
  28. package/dist/routes/agent-apps.d.ts +1 -1
  29. package/dist/routes/agent-apps.js +1 -1
  30. package/dist/routes/apps.js +44 -11
  31. package/dist/routes/apps.js.map +1 -1
  32. package/dist/routes/auth.js +3 -0
  33. package/dist/routes/auth.js.map +1 -1
  34. package/dist/routes/instances.js +787 -16
  35. package/dist/routes/instances.js.map +1 -1
  36. package/dist/routes/llm.js +24 -35
  37. package/dist/routes/llm.js.map +1 -1
  38. package/dist/routes/setup.js +1 -1
  39. package/dist/routes/setup.js.map +1 -1
  40. package/dist/server.d.ts +9 -0
  41. package/dist/server.js +410 -17
  42. package/dist/server.js.map +1 -1
  43. package/dist/services/agent-apps/catalog.js +4 -3
  44. package/dist/services/agent-apps/catalog.js.map +1 -1
  45. package/dist/services/agent-apps/index.d.ts +1 -1
  46. package/dist/services/agent-apps/index.js +1 -1
  47. package/dist/services/agent-apps/installers/adapter.d.ts +1 -1
  48. package/dist/services/agent-apps/installers/adapter.js +1 -1
  49. package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
  50. package/dist/services/agent-apps/installers/shell-script.js +3 -3
  51. package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
  52. package/dist/services/agent-apps/types.d.ts +2 -2
  53. package/dist/services/agent-apps/types.js +1 -1
  54. package/dist/services/app/app-manager.d.ts +24 -1
  55. package/dist/services/app/app-manager.js +664 -116
  56. package/dist/services/app/app-manager.js.map +1 -1
  57. package/dist/services/app/hermes-agent-manager.js +6 -4
  58. package/dist/services/app/hermes-agent-manager.js.map +1 -1
  59. package/dist/services/app/provide-resolver.d.ts +29 -0
  60. package/dist/services/app/provide-resolver.js +112 -0
  61. package/dist/services/app/provide-resolver.js.map +1 -0
  62. package/dist/services/capability-endpoint-validator.d.ts +41 -0
  63. package/dist/services/capability-endpoint-validator.js +104 -0
  64. package/dist/services/capability-endpoint-validator.js.map +1 -0
  65. package/dist/services/capability-health.d.ts +16 -0
  66. package/dist/services/capability-health.js +121 -0
  67. package/dist/services/capability-health.js.map +1 -0
  68. package/dist/services/capability-registry.d.ts +106 -0
  69. package/dist/services/capability-registry.js +313 -0
  70. package/dist/services/capability-registry.js.map +1 -0
  71. package/dist/services/connection-apply.d.ts +89 -0
  72. package/dist/services/connection-apply.js +421 -0
  73. package/dist/services/connection-apply.js.map +1 -0
  74. package/dist/services/connection-resolver.d.ts +65 -0
  75. package/dist/services/connection-resolver.js +281 -0
  76. package/dist/services/connection-resolver.js.map +1 -0
  77. package/dist/services/connection-transactor.d.ts +37 -0
  78. package/dist/services/connection-transactor.js +341 -0
  79. package/dist/services/connection-transactor.js.map +1 -0
  80. package/dist/services/instance-manager.d.ts +13 -0
  81. package/dist/services/instance-manager.js +137 -23
  82. package/dist/services/instance-manager.js.map +1 -1
  83. package/dist/services/llm-proxy/index.d.ts +16 -2
  84. package/dist/services/llm-proxy/index.js +48 -44
  85. package/dist/services/llm-proxy/index.js.map +1 -1
  86. package/dist/services/llm-proxy/probe.d.ts +6 -0
  87. package/dist/services/llm-proxy/probe.js +85 -0
  88. package/dist/services/llm-proxy/probe.js.map +1 -0
  89. package/dist/services/llm-proxy/ssrf.d.ts +1 -0
  90. package/dist/services/llm-proxy/ssrf.js +18 -7
  91. package/dist/services/llm-proxy/ssrf.js.map +1 -1
  92. package/dist/services/nomad-manager.js +375 -16
  93. package/dist/services/nomad-manager.js.map +1 -1
  94. package/dist/services/process-manager.js +1 -1
  95. package/dist/services/process-manager.js.map +1 -1
  96. package/dist/services/runtime/adapters/hermes.d.ts +30 -1
  97. package/dist/services/runtime/adapters/hermes.js +218 -5
  98. package/dist/services/runtime/adapters/hermes.js.map +1 -1
  99. package/dist/services/runtime/adapters/openclaw-mcporter.d.ts +45 -0
  100. package/dist/services/runtime/adapters/openclaw-mcporter.js +108 -0
  101. package/dist/services/runtime/adapters/openclaw-mcporter.js.map +1 -0
  102. package/dist/services/runtime/adapters/openclaw.d.ts +87 -0
  103. package/dist/services/runtime/adapters/openclaw.js +250 -2
  104. package/dist/services/runtime/adapters/openclaw.js.map +1 -1
  105. package/dist/services/runtime/mcp-shims/firewall.d.ts +26 -0
  106. package/dist/services/runtime/mcp-shims/firewall.js +129 -0
  107. package/dist/services/runtime/mcp-shims/firewall.js.map +1 -0
  108. package/dist/services/runtime/mcp-shims/searxng-shim.d.ts +27 -0
  109. package/dist/services/runtime/mcp-shims/searxng-shim.js +125 -0
  110. package/dist/services/runtime/mcp-shims/searxng-shim.js.map +1 -0
  111. package/dist/services/runtime/mcp-shims/write-mcp-entry.d.ts +83 -0
  112. package/dist/services/runtime/mcp-shims/write-mcp-entry.js +127 -0
  113. package/dist/services/runtime/mcp-shims/write-mcp-entry.js.map +1 -0
  114. package/dist/services/runtime/migrations.d.ts +8 -0
  115. package/dist/services/runtime/migrations.js +100 -0
  116. package/dist/services/runtime/migrations.js.map +1 -1
  117. package/dist/services/runtime/types.d.ts +15 -0
  118. package/dist/services/setup-manager.js +6 -6
  119. package/dist/services/setup-manager.js.map +1 -1
  120. package/dist/services/suggestions.d.ts +27 -0
  121. package/dist/services/suggestions.js +133 -0
  122. package/dist/services/suggestions.js.map +1 -0
  123. package/dist/services/task-registry.js +4 -2
  124. package/dist/services/task-registry.js.map +1 -1
  125. package/dist/services/telemetry/device-fingerprint.d.ts +1 -1
  126. package/dist/services/telemetry/device-fingerprint.js +1 -1
  127. package/dist/services/types-shim.d.ts +16 -0
  128. package/dist/services/types-shim.js +2 -0
  129. package/dist/services/types-shim.js.map +1 -0
  130. package/dist/types.d.ts +171 -1
  131. package/dist/utils/instance-lock.d.ts +22 -0
  132. package/dist/utils/instance-lock.js +48 -0
  133. package/dist/utils/instance-lock.js.map +1 -0
  134. package/dist/utils/safe-json.js +55 -22
  135. package/dist/utils/safe-json.js.map +1 -1
  136. package/install/jishu-install.sh +323 -27
  137. package/install/jishu-uninstall.sh +353 -20
  138. package/package.json +3 -1
  139. package/public/assets/Dashboard-rkWp-CXd.js +1 -0
  140. package/public/assets/{HermesChatPanel-mFSureyc.js → HermesChatPanel-_GHoklgo.js} +1 -1
  141. package/public/assets/HermesConfigForm-anDnwUp_.js +4 -0
  142. package/public/assets/{InitPassword-CVA8wQA6.js → InitPassword-ZU9_-hDr.js} +1 -1
  143. package/public/assets/InstanceDetail-CN0FH1aw.js +92 -0
  144. package/public/assets/{Login-BWsZH2mu.js → Login-BItXqYAJ.js} +1 -1
  145. package/public/assets/NewInstance-BousE6kY.js +1 -0
  146. package/public/assets/ProviderRecommendations-DFYj7Fb6.js +1 -0
  147. package/public/assets/Settings-Bttc6QmM.js +1 -0
  148. package/public/assets/Setup-Bsxx1zgj.js +1 -0
  149. package/public/assets/{WeixinLoginPanel-CnjR8xMu.js → WeixinLoginPanel-DPZpAKgO.js} +2 -2
  150. package/public/assets/index-8xZy1z5k.css +1 -0
  151. package/public/assets/index-Dw3HhUYE.js +19 -0
  152. package/public/assets/providers-DtNXh9JD.js +1 -0
  153. package/public/assets/registry-5s2UB6is.js +2 -0
  154. package/public/index.html +2 -2
  155. package/scripts/check-app-spec.mjs +443 -0
  156. package/scripts/check-i18n.mjs +154 -0
  157. package/scripts/run.sh +4 -4
  158. package/public/assets/Dashboard-B-JoOjBQ.js +0 -1
  159. package/public/assets/HermesConfigForm-DvR05LK1.js +0 -4
  160. package/public/assets/InstanceDetail-DcZW2QGO.js +0 -91
  161. package/public/assets/NewInstance-BCIrAd86.js +0 -1
  162. package/public/assets/Settings-xkDcduFz.js +0 -1
  163. package/public/assets/Setup-Cfuwj4gV.js +0 -1
  164. package/public/assets/index-CPhVFEsx.css +0 -1
  165. package/public/assets/index-DQsM6Joa.js +0 -19
  166. package/public/assets/providers-V-vwrExZ.js +0 -1
  167. package/public/assets/registry-B4UFJdpA.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":"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"}
@@ -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 first gateway-labeled port wins.
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
- deduped.set(name, backfillInstanceMeta(meta));
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
- return backfillInstanceMeta(meta);
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
- let result = "127.0.0.1";
880
- try {
881
- const out = execFileSync("ss", ["-tlnH", "sport", "=", ":" + safePort(port)], {
882
- encoding: "utf-8",
883
- timeout: 3000,
884
- stdio: ["pipe", "pipe", "pipe"],
885
- });
886
- for (const line of out.split("\n")) {
887
- let match = line.match(/\s([\d.]+):(\d+)\s/);
888
- if (!match)
889
- match = line.match(/\s\[([0-9a-fA-F:]+)\]:(\d+)\s/);
890
- if (match && match[2] === String(port)) {
891
- const addr = match[1];
892
- result = addr === "0.0.0.0" ? "127.0.0.1" : addr;
893
- break;
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
- catch { /* fall through */ }
898
- return result;
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
- const jid = `${prefix}${instanceId}`;
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